From 76267eb161729b3888c9bb5517ee34668a58ed4a Mon Sep 17 00:00:00 2001 From: mrholek Date: Sat, 6 Sep 2025 12:57:25 +0200 Subject: [PATCH 01/10] chore: clean-up --- packages/coreui-react/src/components/focus-trap/utils.ts | 6 ------ 1 file changed, 6 deletions(-) diff --git a/packages/coreui-react/src/components/focus-trap/utils.ts b/packages/coreui-react/src/components/focus-trap/utils.ts index 8f9a76f0..92b4bae3 100644 --- a/packages/coreui-react/src/components/focus-trap/utils.ts +++ b/packages/coreui-react/src/components/focus-trap/utils.ts @@ -56,12 +56,6 @@ export const isElement = (object: unknown): object is Element => { return false } - // Handle jQuery objects - if ('jquery' in object && object.jquery !== undefined) { - const jQueryObject = object as { [key: number]: Element } - return isElement(jQueryObject[0]) - } - return 'nodeType' in object && typeof object.nodeType === 'number' } From cf37758b589de13e8d8c40de7761d4a0953d86de Mon Sep 17 00:00:00 2001 From: mrholek Date: Sun, 14 Sep 2025 14:08:08 +0200 Subject: [PATCH 02/10] fix(CDropdown): closes #431; prevents closing the dropdown menu when clicking on the scrollbar --- .../src/components/dropdown/CDropdown.tsx | 34 ++++++++++----- ...ropdownOptionsAutoCloseBehaviorExample.tsx | 41 +++++++++++++++++++ .../examples/DropdownOptionsExample.tsx | 25 +++++++++++ .../content/components/dropdown/index.mdx | 16 ++++++++ 4 files changed, 105 insertions(+), 11 deletions(-) create mode 100644 packages/docs/content/components/dropdown/examples/DropdownOptionsAutoCloseBehaviorExample.tsx create mode 100644 packages/docs/content/components/dropdown/examples/DropdownOptionsExample.tsx diff --git a/packages/coreui-react/src/components/dropdown/CDropdown.tsx b/packages/coreui-react/src/components/dropdown/CDropdown.tsx index e32efa65..df1ad839 100644 --- a/packages/coreui-react/src/components/dropdown/CDropdown.tsx +++ b/packages/coreui-react/src/components/dropdown/CDropdown.tsx @@ -281,7 +281,7 @@ export const CDropdown: PolymorphicRefForwardingComponent<'div', CDropdownProps> toggleElement?.removeEventListener('keydown', handleKeydown) menuElement?.removeEventListener('keydown', handleKeydown) - window.removeEventListener('mouseup', handleMouseUp) + window.removeEventListener('click', handleClick) window.removeEventListener('keyup', handleKeyup) onHide?.() @@ -314,25 +314,37 @@ export const CDropdown: PolymorphicRefForwardingComponent<'div', CDropdownProps> [autoClose, handleHide] ) - const handleMouseUp = useCallback( - (event: Event) => { + const handleClick = useCallback( + (event: MouseEvent) => { if (!dropdownToggleElement || !dropdownMenuRef.current) { return } - if (dropdownToggleElement.contains(event.target as HTMLElement)) { + if ((event as MouseEvent).button === 2) { + return + } + + const composedPath = event.composedPath() + const isOnToggle = composedPath.includes(dropdownToggleElement) + const isOnMenu = composedPath.includes(dropdownMenuRef.current) + + if (isOnToggle) { + return + } + + const target = event.target as HTMLElement | null + const FORM_TAG_RE = /^(input|select|option|textarea|form|button|label)$/i + + if (isOnMenu && target && FORM_TAG_RE.test(target.tagName)) { return } if ( autoClose === true || - (autoClose === 'inside' && - dropdownMenuRef.current.contains(event.target as HTMLElement)) || - (autoClose === 'outside' && - !dropdownMenuRef.current.contains(event.target as HTMLElement)) + (autoClose === 'inside' && isOnMenu) || + (autoClose === 'outside' && !isOnMenu) ) { setTimeout(() => handleHide(), 1) - return } }, [autoClose, dropdownToggleElement, handleHide] @@ -354,7 +366,7 @@ export const CDropdown: PolymorphicRefForwardingComponent<'div', CDropdownProps> toggleElement.addEventListener('keydown', handleKeydown) menuElement.addEventListener('keydown', handleKeydown) - window.addEventListener('mouseup', handleMouseUp) + window.addEventListener('click', handleClick) window.addEventListener('keyup', handleKeyup) if (event && (event.key === 'ArrowDown' || event.key === 'ArrowUp')) { @@ -369,8 +381,8 @@ export const CDropdown: PolymorphicRefForwardingComponent<'div', CDropdownProps> allowPopperUse, initPopper, computedPopperConfig, + handleClick, handleKeydown, - handleMouseUp, handleKeyup, onShow, ] diff --git a/packages/docs/content/components/dropdown/examples/DropdownOptionsAutoCloseBehaviorExample.tsx b/packages/docs/content/components/dropdown/examples/DropdownOptionsAutoCloseBehaviorExample.tsx new file mode 100644 index 00000000..23d90df6 --- /dev/null +++ b/packages/docs/content/components/dropdown/examples/DropdownOptionsAutoCloseBehaviorExample.tsx @@ -0,0 +1,41 @@ +import React from 'react' +import { CDropdown, CDropdownItem, CDropdownMenu, CDropdownToggle } from '@coreui/react' + +export const DropdownOptionsAutoCloseBehaviorExample = () => { + return ( +
+ + Default dropdown + + Action + Another action + Something else here + + + + Clickable inside + + Action + Another action + Something else here + + + + Clickable outside + + Action + Another action + Something else here + + + + Manual close + + Action + Another action + Something else here + + +
+ ) +} diff --git a/packages/docs/content/components/dropdown/examples/DropdownOptionsExample.tsx b/packages/docs/content/components/dropdown/examples/DropdownOptionsExample.tsx new file mode 100644 index 00000000..fd6b5aae --- /dev/null +++ b/packages/docs/content/components/dropdown/examples/DropdownOptionsExample.tsx @@ -0,0 +1,25 @@ +import React from 'react' +import { CDropdown, CDropdownItem, CDropdownMenu, CDropdownToggle } from '@coreui/react' + +export const DropdownOptionsExample = () => { + return ( +
+ + Offset + + Action + Another action + Something else here + + + + Portal + + Action + Another action + Something else here + + +
+ ) +} diff --git a/packages/docs/content/components/dropdown/index.mdx b/packages/docs/content/components/dropdown/index.mdx index c5890f31..b0e8ba47 100644 --- a/packages/docs/content/components/dropdown/index.mdx +++ b/packages/docs/content/components/dropdown/index.mdx @@ -172,6 +172,22 @@ Put a form within a dropdown menu, or make it into a dropdown menu. +## Dropdown options + +Use `offset` to displace the dropdown from its default position. The value is a string with two numbers separated by a comma, e.g. `offset={[10, 20]}`. Use `portal` property to render dropdowns in `body` instead of the parent element. This helps to avoid any overflow or z-index issues. + + + +### Auto close behavior + +By default, dropdowns are closed when clicking outside of the dropdown menu or the toggle button. You can change this behavior with the `autoClose` property. Set `autoClose` to: + +- `true` - Close on clicks inside or outside of the React.js dropdown menu. +- `false` - Disable auto-close; close manually by setting the `visible={false}` (also not closed by `Escape`). +- `'inside'` - Close only when clicking inside the React.js dropdown menu. +- `'outside'` - Close only when clicking outside the React.js dropdown menu. + + ## API From 9a8af5395b16dde742aed5abf47107129916c4aa Mon Sep 17 00:00:00 2001 From: mrholek Date: Sun, 14 Sep 2025 14:55:45 +0200 Subject: [PATCH 03/10] refactor(CDropdown): add focus trap when used with portal --- .../src/components/dropdown/CDropdown.tsx | 49 +++++++++++-------- .../examples/DropdownOptionsExample.tsx | 8 +-- 2 files changed, 33 insertions(+), 24 deletions(-) diff --git a/packages/coreui-react/src/components/dropdown/CDropdown.tsx b/packages/coreui-react/src/components/dropdown/CDropdown.tsx index df1ad839..2ea29199 100644 --- a/packages/coreui-react/src/components/dropdown/CDropdown.tsx +++ b/packages/coreui-react/src/components/dropdown/CDropdown.tsx @@ -22,6 +22,7 @@ import { getNextActiveElement, isRTL } from '../../utils' import type { Alignments, Directions } from './types' import { getPlacement } from './utils' +import { CFocusTrap } from '../focus-trap' export interface CDropdownProps extends HTMLAttributes { /** @@ -288,7 +289,11 @@ export const CDropdown: PolymorphicRefForwardingComponent<'div', CDropdownProps> }, [dropdownToggleElement, allowPopperUse, destroyPopper, onHide]) const handleKeydown = useCallback((event: KeyboardEvent) => { - if (dropdownMenuRef.current && (event.key === 'ArrowDown' || event.key === 'ArrowUp')) { + if (!dropdownMenuRef.current) { + return + } + + if (event.key === 'ArrowDown' || event.key === 'ArrowUp') { event.preventDefault() const target = event.target as HTMLElement const items = [ @@ -404,26 +409,28 @@ export const CDropdown: PolymorphicRefForwardingComponent<'div', CDropdownProps> return ( - {variant === 'input-group' ? ( - <>{children} - ) : ( - - {children} - - )} + + {variant === 'input-group' ? ( + <>{children} + ) : ( + + {children} + + )} + ) } diff --git a/packages/docs/content/components/dropdown/examples/DropdownOptionsExample.tsx b/packages/docs/content/components/dropdown/examples/DropdownOptionsExample.tsx index fd6b5aae..a6d0455c 100644 --- a/packages/docs/content/components/dropdown/examples/DropdownOptionsExample.tsx +++ b/packages/docs/content/components/dropdown/examples/DropdownOptionsExample.tsx @@ -12,9 +12,11 @@ export const DropdownOptionsExample = () => { Something else here - - Portal - + + + Portal + + Action Another action Something else here From b06b99da111c94c1323c89a6891ec99dd9b7037a Mon Sep 17 00:00:00 2001 From: mrholek Date: Sun, 14 Sep 2025 20:17:26 +0200 Subject: [PATCH 04/10] feat(CDropdown): add reference prop for custom positioning targets --- .../src/components/dropdown/CDropdown.tsx | 52 ++++++++++++++----- .../src/components/dropdown/utils.ts | 21 ++++++++ packages/docs/content/api/CDropdown.api.mdx | 24 +++++++++ .../examples/DropdownOptionsExample.tsx | 13 ++++- 4 files changed, 94 insertions(+), 16 deletions(-) diff --git a/packages/coreui-react/src/components/dropdown/CDropdown.tsx b/packages/coreui-react/src/components/dropdown/CDropdown.tsx index 2ea29199..d9a77f90 100644 --- a/packages/coreui-react/src/components/dropdown/CDropdown.tsx +++ b/packages/coreui-react/src/components/dropdown/CDropdown.tsx @@ -21,7 +21,7 @@ import type { Placements } from '../../types' import { getNextActiveElement, isRTL } from '../../utils' import type { Alignments, Directions } from './types' -import { getPlacement } from './utils' +import { getPlacement, getReferenceElement } from './utils' import { CFocusTrap } from '../focus-trap' export interface CDropdownProps extends HTMLAttributes { @@ -161,6 +161,27 @@ export interface CDropdownProps extends HTMLAttributes + * Toggle dropdown + * + * Action + * Another Action + * + * + * + * @since 5.9.0 + */ + reference?: 'parent' | 'toggle' | HTMLElement | React.RefObject + /** * Defines the visual variant of the React Dropdown */ @@ -204,6 +225,7 @@ export const CDropdown: PolymorphicRefForwardingComponent<'div', CDropdownProps> popper = true, popperConfig, portal = false, + reference = 'toggle', variant = 'btn-group', visible = false, ...rest @@ -255,12 +277,12 @@ export const CDropdown: PolymorphicRefForwardingComponent<'div', CDropdownProps> }, [visible]) useEffect(() => { - const toggleElement = dropdownToggleElement + const referenceElement = getReferenceElement(reference, dropdownToggleElement, dropdownRef) const menuElement = dropdownMenuRef.current - if (allowPopperUse && menuElement && toggleElement && _visible) { - initPopper(toggleElement, menuElement, computedPopperConfig) + if (allowPopperUse && menuElement && referenceElement && _visible) { + initPopper(referenceElement, menuElement, computedPopperConfig) } - }, [dropdownToggleElement]) + }, [dropdownToggleElement, reference]) useEffect(() => { if (pendingKeyDownEvent !== null) { @@ -272,21 +294,21 @@ export const CDropdown: PolymorphicRefForwardingComponent<'div', CDropdownProps> const handleHide = useCallback(() => { setVisible(false) - const toggleElement = dropdownToggleElement const menuElement = dropdownMenuRef.current + const toggleElement = dropdownToggleElement if (allowPopperUse) { destroyPopper() } - toggleElement?.removeEventListener('keydown', handleKeydown) menuElement?.removeEventListener('keydown', handleKeydown) + toggleElement?.removeEventListener('keydown', handleKeydown) window.removeEventListener('click', handleClick) window.removeEventListener('keyup', handleKeyup) onHide?.() - }, [dropdownToggleElement, allowPopperUse, destroyPopper, onHide]) + }, [allowPopperUse, dropdownToggleElement, destroyPopper, onHide]) const handleKeydown = useCallback((event: KeyboardEvent) => { if (!dropdownMenuRef.current) { @@ -316,7 +338,7 @@ export const CDropdown: PolymorphicRefForwardingComponent<'div', CDropdownProps> dropdownToggleElement?.focus() } }, - [autoClose, handleHide] + [autoClose, dropdownToggleElement, handleHide] ) const handleClick = useCallback( @@ -357,14 +379,15 @@ export const CDropdown: PolymorphicRefForwardingComponent<'div', CDropdownProps> const handleShow = useCallback( (event?: KeyboardEvent) => { - const toggleElement = dropdownToggleElement const menuElement = dropdownMenuRef.current + const referenceElement = getReferenceElement(reference, dropdownToggleElement, dropdownRef) + const toggleElement = dropdownToggleElement - if (toggleElement && menuElement) { + if (menuElement && referenceElement && toggleElement) { setVisible(true) if (allowPopperUse) { - initPopper(toggleElement, menuElement, computedPopperConfig) + initPopper(referenceElement, menuElement, computedPopperConfig) } toggleElement.focus() @@ -382,13 +405,14 @@ export const CDropdown: PolymorphicRefForwardingComponent<'div', CDropdownProps> } }, [ - dropdownToggleElement, allowPopperUse, - initPopper, computedPopperConfig, + dropdownToggleElement, + reference, handleClick, handleKeydown, handleKeyup, + initPopper, onShow, ] ) diff --git a/packages/coreui-react/src/components/dropdown/utils.ts b/packages/coreui-react/src/components/dropdown/utils.ts index 8b4a213f..171fa0f7 100644 --- a/packages/coreui-react/src/components/dropdown/utils.ts +++ b/packages/coreui-react/src/components/dropdown/utils.ts @@ -1,3 +1,4 @@ +import React from 'react' import type { Placement } from '@popperjs/core' import type { Placements } from '../../types' import type { Alignments, Breakpoints } from './types' @@ -49,3 +50,23 @@ export const getPlacement = ( return _placement } + +export const getReferenceElement = ( + reference: 'parent' | 'toggle' | React.RefObject | HTMLElement, + dropdownToggleElement: HTMLElement | null, + dropdownRef: React.RefObject +): HTMLElement | null => { + if (reference === 'parent') { + return dropdownRef.current + } + + if (reference instanceof HTMLElement) { + return reference + } + + if (reference instanceof Object && 'current' in reference) { + return reference.current + } + + return dropdownToggleElement +} diff --git a/packages/docs/content/api/CDropdown.api.mdx b/packages/docs/content/api/CDropdown.api.mdx index 765fbd96..ef211307 100644 --- a/packages/docs/content/api/CDropdown.api.mdx +++ b/packages/docs/content/api/CDropdown.api.mdx @@ -190,6 +190,30 @@ const myContainer = document.getElementById('my-container')

Renders the React Dropdown Menu using a React Portal, allowing it to escape the DOM hierarchy for improved positioning.

+ + reference#5.9.0+ + {`toggle`} + {`HTMLElement`}, {`'parent'`}, {`'toggle'`}, {`RefObject\`} + + + +

Sets the reference element for positioning the React Dropdown Menu.

+
    +
  • {`toggle`} - The React Dropdown Toggle button (default).
  • +
  • {`parent`} - The React Dropdown wrapper element.
  • +
  • {`HTMLElement`} - A custom HTML element.
  • +
  • {`React.RefObject`} - A custom reference element.
  • +
+ + Toggle dropdown + + Action + Another Action + +
`} /> + + variant# {`btn-group`} diff --git a/packages/docs/content/components/dropdown/examples/DropdownOptionsExample.tsx b/packages/docs/content/components/dropdown/examples/DropdownOptionsExample.tsx index a6d0455c..688797e1 100644 --- a/packages/docs/content/components/dropdown/examples/DropdownOptionsExample.tsx +++ b/packages/docs/content/components/dropdown/examples/DropdownOptionsExample.tsx @@ -1,5 +1,5 @@ import React from 'react' -import { CDropdown, CDropdownItem, CDropdownMenu, CDropdownToggle } from '@coreui/react' +import { CButton, CDropdown, CDropdownItem, CDropdownMenu, CDropdownToggle } from '@coreui/react' export const DropdownOptionsExample = () => { return ( @@ -12,7 +12,7 @@ export const DropdownOptionsExample = () => { Something else here - + Portal @@ -22,6 +22,15 @@ export const DropdownOptionsExample = () => { Something else here + + Reference + + + Action + Another action + Something else here + + ) } From e6c3def22cde02d2af7dfae089572c76904eb412 Mon Sep 17 00:00:00 2001 From: mrholek Date: Sun, 14 Sep 2025 20:59:15 +0200 Subject: [PATCH 05/10] perf(CDropdown): improve performance with useMemo for context and cleanup on unmount --- .../src/components/dropdown/CDropdown.tsx | 51 ++++++++++++++----- .../components/dropdown/CDropdownHeader.tsx | 1 + .../dropdown/CDropdownItemPlain.tsx | 1 + .../src/components/dropdown/CDropdownMenu.tsx | 1 + .../components/dropdown/CDropdownToggle.tsx | 4 ++ 5 files changed, 44 insertions(+), 14 deletions(-) diff --git a/packages/coreui-react/src/components/dropdown/CDropdown.tsx b/packages/coreui-react/src/components/dropdown/CDropdown.tsx index d9a77f90..3c47a20b 100644 --- a/packages/coreui-react/src/components/dropdown/CDropdown.tsx +++ b/packages/coreui-react/src/components/dropdown/CDropdown.tsx @@ -276,6 +276,14 @@ export const CDropdown: PolymorphicRefForwardingComponent<'div', CDropdownProps> } }, [visible]) + useEffect(() => { + return () => { + if (_visible) { + handleHide() + } + } + }, []) + useEffect(() => { const referenceElement = getReferenceElement(reference, dropdownToggleElement, dropdownRef) const menuElement = dropdownMenuRef.current @@ -371,7 +379,7 @@ export const CDropdown: PolymorphicRefForwardingComponent<'div', CDropdownProps> (autoClose === 'inside' && isOnMenu) || (autoClose === 'outside' && !isOnMenu) ) { - setTimeout(() => handleHide(), 1) + handleHide() } }, [autoClose, dropdownToggleElement, handleHide] @@ -417,19 +425,34 @@ export const CDropdown: PolymorphicRefForwardingComponent<'div', CDropdownProps> ] ) - const contextValues = { - alignment, - container, - dark, - dropdownMenuRef, - dropdownToggleRef, - handleHide, - handleShow, - popper: allowPopperUse, - portal, - variant, - visible: _visible, - } + const contextValues = useMemo( + () => ({ + alignment, + container, + dark, + dropdownMenuRef, + dropdownToggleRef, + handleHide, + handleShow, + popper: allowPopperUse, + portal, + variant, + visible: _visible, + }), + [ + alignment, + container, + dark, + dropdownMenuRef, + dropdownToggleRef, + handleHide, + handleShow, + allowPopperUse, + portal, + variant, + _visible, + ] + ) return ( diff --git a/packages/coreui-react/src/components/dropdown/CDropdownHeader.tsx b/packages/coreui-react/src/components/dropdown/CDropdownHeader.tsx index 89e53673..1aff66e9 100644 --- a/packages/coreui-react/src/components/dropdown/CDropdownHeader.tsx +++ b/packages/coreui-react/src/components/dropdown/CDropdownHeader.tsx @@ -9,6 +9,7 @@ export interface CDropdownHeaderProps extends HTMLAttributes * Component used for the root node. Either a string to use a HTML element or a component. */ as?: ElementType + /** * A string of all className you want applied to the component. */ diff --git a/packages/coreui-react/src/components/dropdown/CDropdownItemPlain.tsx b/packages/coreui-react/src/components/dropdown/CDropdownItemPlain.tsx index bc4ae0bb..7d489da3 100644 --- a/packages/coreui-react/src/components/dropdown/CDropdownItemPlain.tsx +++ b/packages/coreui-react/src/components/dropdown/CDropdownItemPlain.tsx @@ -9,6 +9,7 @@ export interface CDropdownItemPlainProps extends HTMLAttributes * Component used for the root node. Either a string to use a HTML element or a component. */ as?: ElementType + /** * A string of all className you want applied to the component. */ diff --git a/packages/coreui-react/src/components/dropdown/CDropdownMenu.tsx b/packages/coreui-react/src/components/dropdown/CDropdownMenu.tsx index 59442d63..3ee88ad5 100644 --- a/packages/coreui-react/src/components/dropdown/CDropdownMenu.tsx +++ b/packages/coreui-react/src/components/dropdown/CDropdownMenu.tsx @@ -15,6 +15,7 @@ export interface CDropdownMenuProps extends HTMLAttributes { * Enables pseudo element caret on toggler. */ caret?: boolean + /** * Create a custom toggler which accepts any content. */ custom?: boolean + /** * If a dropdown `variant` is set to `nav-item` then render the toggler as a * link instead of a button. @@ -24,12 +26,14 @@ export interface CDropdownToggleProps extends Omit { * @since 5.0.0 */ navLink?: boolean + /** * Similarly, create split button dropdowns with virtually the same markup as * single button dropdowns, but with the addition of `.dropdown-toggle-split` * className for proper spacing around the dropdown caret. */ split?: boolean + /** * Sets which event handlers you'd like provided to your toggle prop. You can * specify one trigger or an array of them. From 2ed7aae80e2aa85914e19b9dcc660c1b74e7c496 Mon Sep 17 00:00:00 2001 From: mrholek Date: Mon, 15 Sep 2025 18:04:36 +0200 Subject: [PATCH 06/10] feat(CDropdownToggle): add splitLabel prop for customizable screen reader text --- .../src/components/dropdown/CDropdownToggle.tsx | 12 +++++++++++- packages/docs/content/api/CDropdownToggle.api.mdx | 10 ++++++++++ .../dropdown/examples/DropdownDropendExample.tsx | 2 +- .../dropdown/examples/DropdownDropstartExample.tsx | 2 +- 4 files changed, 23 insertions(+), 3 deletions(-) diff --git a/packages/coreui-react/src/components/dropdown/CDropdownToggle.tsx b/packages/coreui-react/src/components/dropdown/CDropdownToggle.tsx index 99d9a011..c70c8a12 100644 --- a/packages/coreui-react/src/components/dropdown/CDropdownToggle.tsx +++ b/packages/coreui-react/src/components/dropdown/CDropdownToggle.tsx @@ -34,6 +34,14 @@ export interface CDropdownToggleProps extends Omit { */ split?: boolean + /** + * Screen reader label for split button dropdown toggle. + * + * @default 'Toggle Dropdown' + * @since 5.9.0 + */ + splitLabel?: string + /** * Sets which event handlers you'd like provided to your toggle prop. You can * specify one trigger or an array of them. @@ -50,6 +58,7 @@ export const CDropdownToggle: FC = ({ className, navLink = true, split, + splitLabel = 'Toggle Dropdown', trigger = 'click', ...rest }) => { @@ -117,7 +126,7 @@ export const CDropdownToggle: FC = ({ return ( {children} - {split && Toggle Dropdown} + {split && {splitLabel}} ) } @@ -128,6 +137,7 @@ CDropdownToggle.propTypes = { className: PropTypes.string, custom: PropTypes.bool, split: PropTypes.bool, + splitLabel: PropTypes.string, trigger: triggerPropType, } diff --git a/packages/docs/content/api/CDropdownToggle.api.mdx b/packages/docs/content/api/CDropdownToggle.api.mdx index 1f6572e1..cd56fb6f 100644 --- a/packages/docs/content/api/CDropdownToggle.api.mdx +++ b/packages/docs/content/api/CDropdownToggle.api.mdx @@ -145,6 +145,16 @@ import CDropdownToggle from '@coreui/react/src/components/dropdown/CDropdownTogg

Similarly, create split button dropdowns with virtually the same markup as single button dropdowns, but with the addition of {`.dropdown-toggle-split`} className for proper spacing around the dropdown caret.

+ + splitLabel#5.9.0+ + {`Toggle Dropdown`} + {`string`} + + + +

Screen reader label for split button dropdown toggle.

+ + trigger# {`click`} diff --git a/packages/docs/content/components/dropdown/examples/DropdownDropendExample.tsx b/packages/docs/content/components/dropdown/examples/DropdownDropendExample.tsx index e9d0656e..a6a1bd68 100644 --- a/packages/docs/content/components/dropdown/examples/DropdownDropendExample.tsx +++ b/packages/docs/content/components/dropdown/examples/DropdownDropendExample.tsx @@ -24,7 +24,7 @@ export const DropdownDropendExample = () => { Small split button - + Action Another action diff --git a/packages/docs/content/components/dropdown/examples/DropdownDropstartExample.tsx b/packages/docs/content/components/dropdown/examples/DropdownDropstartExample.tsx index 276b5cff..195cd05e 100644 --- a/packages/docs/content/components/dropdown/examples/DropdownDropstartExample.tsx +++ b/packages/docs/content/components/dropdown/examples/DropdownDropstartExample.tsx @@ -25,7 +25,7 @@ export const DropdownDropstartExample = () => { - + Action Another action From 7ac3348c80ce7ae8e0227cc13f0dfe3117fa42e3 Mon Sep 17 00:00:00 2001 From: mrholek Date: Mon, 15 Sep 2025 18:15:09 +0200 Subject: [PATCH 07/10] docs: update the link to the roadmap. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index c0e48a57..f303eefb 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ · Request feature · - Roadmap + Roadmap · Blog

From 871e523780b156d329f00ec57a4c45d88b43f94e Mon Sep 17 00:00:00 2001 From: mrholek Date: Wed, 17 Sep 2025 13:11:39 +0200 Subject: [PATCH 08/10] chore: update dependencies and devDependencies MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit @docsearch/css ^3.9.0 → ^4.0.1 @docsearch/react ^3.9.0 → ^4.0.1 @types/react ^19.1.12 → ^19.1.13 @typescript-eslint/parser ^8.42.0 → ^8.44.0 eslint ^9.34.0 → ^9.35.0 eslint-plugin-unicorn ^60.0.0 → ^61.0.2 globals ^16.3.0 → ^16.4.0 lerna ^8.2.3 → ^8.2.4 rollup ^4.50.0 → ^4.50.2 sass ^1.91.0 → ^1.92.1 ts-jest ^29.4.1 → ^29.4.2 typescript-eslint ^8.42.0 → ^8.44.0 --- package.json | 12 ++++++------ packages/coreui-react/package.json | 8 ++++---- packages/docs/package.json | 6 +++--- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/package.json b/package.json index 58169718..cdacc619 100644 --- a/package.json +++ b/package.json @@ -22,18 +22,18 @@ "test:update": "npm-run-all charts:test:update icons:test:update lib:test:update" }, "devDependencies": { - "@typescript-eslint/parser": "^8.42.0", - "eslint": "^9.34.0", + "@typescript-eslint/parser": "^8.44.0", + "eslint": "^9.35.0", "eslint-config-prettier": "^10.1.8", "eslint-plugin-prettier": "^5.5.4", "eslint-plugin-react": "^7.37.5", "eslint-plugin-react-hooks": "^5.2.0", - "eslint-plugin-unicorn": "^60.0.0", - "globals": "^16.3.0", - "lerna": "^8.2.3", + "eslint-plugin-unicorn": "^61.0.2", + "globals": "^16.4.0", + "lerna": "^8.2.4", "npm-run-all": "^4.1.5", "prettier": "^3.6.2", - "typescript-eslint": "^8.42.0" + "typescript-eslint": "^8.44.0" }, "overrides": { "gatsby-remark-external-links": { diff --git a/packages/coreui-react/package.json b/packages/coreui-react/package.json index 99d28f01..d2a184f9 100644 --- a/packages/coreui-react/package.json +++ b/packages/coreui-react/package.json @@ -53,8 +53,8 @@ "@testing-library/jest-dom": "^6.8.0", "@testing-library/react": "^16.3.0", "@types/jest": "^29.5.14", - "@types/prop-types": "15.8.05", - "@types/react": "^19.1.12", + "@types/prop-types": "15.7.15", + "@types/react": "^19.1.13", "@types/react-dom": "^19.1.9", "@types/react-transition-group": "^4.4.12", "classnames": "^2.5.1", @@ -64,8 +64,8 @@ "react": "^18.3.1", "react-dom": "^18.3.1", "react-transition-group": "^4.4.5", - "rollup": "^4.50.0", - "ts-jest": "^29.4.1", + "rollup": "^4.50.2", + "ts-jest": "^29.4.2", "tslib": "^2.8.1", "typescript": "^5.9.2" }, diff --git a/packages/docs/package.json b/packages/docs/package.json index 400d494a..d8105af8 100644 --- a/packages/docs/package.json +++ b/packages/docs/package.json @@ -30,8 +30,8 @@ "@coreui/icons-react": "^2.3.0", "@coreui/react-chartjs": "^3.0.0", "@coreui/utils": "^2.0.2", - "@docsearch/css": "^3.9.0", - "@docsearch/react": "^3.9.0", + "@docsearch/css": "^4.0.1", + "@docsearch/react": "^4.0.1", "@mdx-js/mdx": "^3.1.1", "@mdx-js/react": "^3.1.1", "@stackblitz/sdk": "^1.11.0", @@ -58,7 +58,7 @@ "react-imask": "^7.6.1", "react-markdown": "^10.1.0", "rimraf": "^6.0.1", - "sass": "^1.91.0", + "sass": "^1.92.1", "showdown": "^2.1.0" }, "devDependencies": { From 83c29d948e8bd9ec143c50e42ef9421a4475baa7 Mon Sep 17 00:00:00 2001 From: mrholek Date: Wed, 17 Sep 2025 13:15:34 +0200 Subject: [PATCH 09/10] docs: update @docsearch to v4 --- packages/docs/src/styles/search.scss | 39 ++++++++++++---------------- 1 file changed, 17 insertions(+), 22 deletions(-) diff --git a/packages/docs/src/styles/search.scss b/packages/docs/src/styles/search.scss index fb794847..16fa476c 100644 --- a/packages/docs/src/styles/search.scss +++ b/packages/docs/src/styles/search.scss @@ -19,7 +19,9 @@ // stylelint-disable selector-class-pattern :root { --docsearch-primary-color: var(--cui-primary); + --docsearch-muted-color: var(--cui-secondary-color); --docsearch-logo-color: var(--cui-primary); + --docsearch-key-color: var(--cui-secondary-color); } @include color-mode(dark, true) { @@ -43,7 +45,6 @@ } .DocSearch-Container { - --docsearch-muted-color: var(--cui-secondary-color); --docsearch-hit-shadow: none; position: fixed; @@ -74,7 +75,8 @@ @include box-shadow($input-box-shadow); @include transition($input-transition); - &:focus { + &:focus, + &:hover { color: $input-focus-color; background-color: $input-focus-bg; border-color: $input-focus-border-color; @@ -87,10 +89,6 @@ } } - &:hover:not(:disabled):not([readonly])::file-selector-button { - background-color: $form-file-button-hover-bg; - } - @include media-breakpoint-down(md) { &, &:hover, @@ -109,32 +107,29 @@ .DocSearch-Button-Keys { min-width: 0; padding: 0 .25rem; - background: rgba($black, .125); + background: var(--cui-secondary-bg); @include border-radius(.25rem); } .DocSearch-Button-Key { - top: 0; width: auto; - height: 1.5rem; - padding: 0 .125rem; - margin-right: 0; - font-size: .875rem; + padding: 0; background: none; + border: 0; box-shadow: none; -} -.DocSearch-Commands-Key { - padding-left: 1px; - font-size: .875rem; - background-color: rgba($black, .1); - background-image: none; - box-shadow: none; + &:first-child { + margin-right: 0; + } } -.DocSearch-Form { - @include border-radius(var(--cui-border-radius)); -} +// .DocSearch-Commands-Key { +// padding-left: 1px; +// font-size: .875rem; +// background-color: rgba($black, .1); +// background-image: none; +// box-shadow: none; +// } .DocSearch-Hits { mark { From 68a6c8c2ad8ba1bcb749a90a4ff36fce6bf4640d Mon Sep 17 00:00:00 2001 From: mrholek Date: Wed, 17 Sep 2025 13:22:04 +0200 Subject: [PATCH 10/10] release: v5.9.0 --- .claude/settings.local.json | 11 +++ CLAUDE.md | 135 +++++++++++++++++++++++++++ README.md | 2 +- lerna.json | 2 +- packages/coreui-react/README.md | 2 +- packages/coreui-react/package.json | 2 +- packages/docs/package.json | 2 +- packages/docs/src/components/Seo.tsx | 2 +- 8 files changed, 152 insertions(+), 6 deletions(-) create mode 100644 .claude/settings.local.json create mode 100644 CLAUDE.md diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 00000000..74d8bc85 --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,11 @@ +{ + "permissions": { + "allow": [ + "mcp__ide__getDiagnostics", + "Bash(npm run lint:*)", + "Bash(npm test:*)" + ], + "deny": [], + "ask": [] + } +} \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 00000000..33a8fd20 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,135 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Overview + +CoreUI for React is a comprehensive React.js components library built on top of Bootstrap 5 and TypeScript. It's organized as a monorepo using Lerna with multiple packages including the main React component library, icons, charts, and documentation. + +## Repository Structure + +This is a **Lerna monorepo** with the following key packages: +- `packages/coreui-react/` - Main React components library (TypeScript) +- `packages/coreui-icons-react/` - Icon components for React +- `packages/coreui-react-chartjs/` - Chart.js integration for React +- `packages/docs/` - Gatsby-based documentation site + +## Development Commands + +### Root Level Commands +- `npm run lint` - Lint all packages +- `npm run test` - Run tests for all packages +- `npm run test:update` - Update snapshots for all packages + +### Package-Specific Commands (using Lerna) +- `npm run lib:build` - Build main React library +- `npm run lib:test` - Test main React library only +- `npm run lib:test:update` - Update main library test snapshots +- `npm run icons:build` - Build icons package +- `npm run charts:build` - Build charts package +- `npm run docs:dev` - Start documentation dev server +- `npm run docs:build` - Build documentation + +### Working with Individual Packages +Navigate to specific packages to run commands directly: +```bash +cd packages/coreui-react +npm test -- src/components/focus-trap/__tests__/CFocusTrap.spec.tsx # Run specific test +npm run build # Build this package only +``` + +### Running Single Tests +To run a specific test file: +```bash +cd packages/coreui-react +npm test -- path/to/test.spec.tsx +``` + +## Architecture + +### Component Organization +Each component follows a consistent structure: +``` +components/[component-name]/ +├── C[ComponentName].tsx # Main component +├── C[ComponentName]Part.tsx # Sub-components +├── index.ts # Exports +├── types.ts # TypeScript types (if complex) +├── utils.ts # Utility functions (if any) +├── const.ts # Constants (if any) +└── __tests__/ # Tests and snapshots + ├── C[ComponentName].spec.tsx + └── __snapshots__/ +``` + +### Component Development Patterns + +**Props Interface**: All components have well-documented TypeScript interfaces with JSDoc comments focusing on accessibility and SEO benefits. + +**Ref Forwarding**: Components forward refs properly to DOM elements for accessibility and integration. + +**Testing**: Uses React Testing Library with Jest, focusing on behavior over implementation details. Each component has snapshot tests and behavioral tests. + +**Styling**: Components use Bootstrap 5 classes and are compatible with `@coreui/coreui` CSS library. + +### Key Development Principles + +**TypeScript First**: All components are written in TypeScript with proper type definitions. + +**Accessibility Focus**: Components implement WCAG 2.1 standards and include proper ARIA attributes. + +**Bootstrap Compatible**: Components extend Bootstrap 5 functionality while maintaining compatibility. + +**No Extra DOM**: Many components avoid adding wrapper elements, using ref merging instead (see `focus-trap` component). + +**Utility Separation**: Complex components separate utilities into dedicated files (`utils.ts`, `const.ts`). + +## Testing + +### Test Structure +- Snapshot tests for UI consistency +- Behavioral tests for user interactions +- Accessibility tests for focus management +- Props validation tests + +### Test Environment +- Jest with JSDOM environment +- React Testing Library for component testing +- `@testing-library/jest-dom` for DOM assertions + +### Running Tests +Tests are run at the package level. Some complex focus management tests may not work perfectly in JSDOM but will work in real browsers. + +## Build System + +### Rollup Configuration +Each package uses Rollup for building: +- ESM and CommonJS outputs +- TypeScript compilation +- Separate bundles for different environments + +### Package Dependencies +- `@coreui/coreui` - Core CSS library +- `@popperjs/core` - For positioning (tooltips, dropdowns) +- `prop-types` - Runtime type checking +- React 17+ peer dependency + +## Component Development + +### Creating New Components +1. Follow the directory structure pattern +2. Use TypeScript interfaces with comprehensive JSDoc +3. Implement proper ref forwarding +4. Add comprehensive tests (snapshot + behavioral) +5. Export from package index files +6. Consider accessibility from the start + +### Refactoring Components +When refactoring complex components: +1. Separate utilities into `utils.ts` and constants into `const.ts` +2. Maintain backward compatibility with existing props +3. Update tests to match new structure +4. Keep the same export interface + +### Focus Management +For components requiring focus management (modals, dropdowns), use the patterns established in the `focus-trap` component, which implements proper Tab/Shift+Tab cycling and external focus redirection. \ No newline at end of file diff --git a/README.md b/README.md index f303eefb..edd87fba 100644 --- a/README.md +++ b/README.md @@ -48,7 +48,7 @@ Several quick start options are available: -- [Download the latest release](https://github.com/coreui/coreui-react/archive/v5.8.0.zip) +- [Download the latest release](https://github.com/coreui/coreui-react/archive/v5.9.0.zip) - Clone the repo: `git clone https://github.com/coreui/coreui-react.git` - Install with [npm](https://www.npmjs.com/): `npm install @coreui/react` - Install with [yarn](https://yarnpkg.com/): `yarn add @coreui/react` diff --git a/lerna.json b/lerna.json index 8a0e6ac0..0a49658b 100644 --- a/lerna.json +++ b/lerna.json @@ -1,6 +1,6 @@ { "npmClient": "yarn", "packages": ["packages/*"], - "version": "5.8.0", + "version": "5.9.0", "$schema": "node_modules/lerna/schemas/lerna-schema.json" } diff --git a/packages/coreui-react/README.md b/packages/coreui-react/README.md index 269cb3cd..ddfb584b 100644 --- a/packages/coreui-react/README.md +++ b/packages/coreui-react/README.md @@ -46,7 +46,7 @@ Several quick start options are available: -- [Download the latest release](https://github.com/coreui/coreui-react/archive/v5.8.0.zip) +- [Download the latest release](https://github.com/coreui/coreui-react/archive/v5.9.0.zip) - Clone the repo: `git clone https://github.com/coreui/coreui-react.git` - Install with [npm](https://www.npmjs.com/): `npm install @coreui/react` - Install with [yarn](https://yarnpkg.com/): `yarn add @coreui/react` diff --git a/packages/coreui-react/package.json b/packages/coreui-react/package.json index d2a184f9..c1e7c2cc 100644 --- a/packages/coreui-react/package.json +++ b/packages/coreui-react/package.json @@ -1,6 +1,6 @@ { "name": "@coreui/react", - "version": "5.8.0", + "version": "5.9.0", "description": "UI Components Library for React.js", "keywords": [ "react", diff --git a/packages/docs/package.json b/packages/docs/package.json index d8105af8..54647ee2 100644 --- a/packages/docs/package.json +++ b/packages/docs/package.json @@ -1,6 +1,6 @@ { "name": "@coreui/react-docs", - "version": "5.8.0", + "version": "5.9.0", "private": true, "description": "", "homepage": "https://coreui.io/react/", diff --git a/packages/docs/src/components/Seo.tsx b/packages/docs/src/components/Seo.tsx index 1d1763f3..befabf25 100644 --- a/packages/docs/src/components/Seo.tsx +++ b/packages/docs/src/components/Seo.tsx @@ -154,7 +154,7 @@ const SEO = ({ title, description, name, image, article, pro }: SEOProps) => { '@type': 'WebPage', '@id': seo.url.replace('docs//', 'docs/'), }, - version: pro ? '5.17.1' : '5.8.0', + version: pro ? '5.17.1' : '5.9.0', proficiencyLevel: 'Beginner', }, ]