Skip to content

Commit 9680566

Browse files
committed
feat(CAccordion): improve accessibility; allow passing or overriding class names for all subcomponents.
1 parent 0acd718 commit 9680566

16 files changed

+585
-92
lines changed

‎packages/coreui-react/src/components/accordion/CAccordion.tsx

+71-6
Original file line numberDiff line numberDiff line change
@@ -2,25 +2,78 @@ import React, { createContext, forwardRef, HTMLAttributes, useState } from 'reac
22
import PropTypes from 'prop-types'
33
import classNames from 'classnames'
44

5+
import { mergeClassNames } from '../../utils'
6+
57
export interface CAccordionProps extends HTMLAttributes<HTMLDivElement> {
68
/**
7-
* The active item key.
9+
* Determines which accordion item is currently active (expanded) by default.
10+
* Accepts a number or string corresponding to the `itemKey` of the desired accordion item.
11+
*
12+
* @example
13+
* <CAccordion activeItemKey="1">...</CAccordion>
814
*/
915
activeItemKey?: number | string
16+
1017
/**
11-
* Make accordion items stay open when another item is opened
18+
* When set to `true`, multiple accordion items within the React Accordion can be open simultaneously.
19+
* This is ideal for scenarios where users need to view multiple sections at once without collapsing others.
20+
*
21+
* @default false
22+
*
23+
* @example
24+
* <CAccordion alwaysOpen>...</CAccordion>
1225
*/
1326
alwaysOpen?: boolean
27+
1428
/**
15-
* A string of all className you want applied to the base component.
29+
* Allows you to apply custom CSS classes to the React Accordion for enhanced styling and theming.
30+
*
31+
* @example
32+
* <CAccordion className="my-custom-accordion">...</CAccordion>
1633
*/
1734
className?: string
35+
1836
/**
19-
* Removes the default background-color, some borders, and some rounded corners to render accordions edge-to-edge with their parent container.
37+
* Allows overriding or extending the default CSS class names used in the component.
38+
*
39+
* - `ACCORDION`: Base class for the accordion component.
40+
* - `ACCORDION_FLUSH`: Class applied when the `flush` prop is set to true, ensuring an edge-to-edge layout.
41+
*
42+
* Use this prop to customize the styles of specific parts of the accordion.
43+
*
44+
* @example
45+
* const customClasses = {
46+
* ACCORDION: 'custom-accordion',
47+
* ACCORDION_FLUSH: 'custom-accordion-flush'
48+
* }
49+
* <CAccordion customClassNames={customClasses}>...</CAccordion>
50+
*/
51+
customClassNames?: Partial<typeof ACCORDION_CLASS_NAMES>
52+
53+
/**
54+
* When `flush` is set to `true`, the React Accordion renders edge-to-edge with its parent container,
55+
* creating a seamless and modern look ideal for minimalist designs.
56+
*
57+
* @default false
58+
*
59+
* @example
60+
* <CAccordion flush>...</CAccordion>
2061
*/
2162
flush?: boolean
2263
}
2364

65+
export const ACCORDION_CLASS_NAMES = {
66+
/**
67+
* Base class for the accordion container.
68+
*/
69+
ACCORDION: 'accordion',
70+
71+
/**
72+
* Applied when the `flush` prop is enabled.
73+
*/
74+
ACCORDION_FLUSH: 'accordion-flush',
75+
}
76+
2477
export interface CAccordionContextProps {
2578
_activeItemKey?: number | string
2679
alwaysOpen?: boolean
@@ -30,12 +83,24 @@ export interface CAccordionContextProps {
3083
export const CAccordionContext = createContext({} as CAccordionContextProps)
3184

3285
export const CAccordion = forwardRef<HTMLDivElement, CAccordionProps>(
33-
({ children, activeItemKey, alwaysOpen = false, className, flush, ...rest }, ref) => {
86+
(
87+
{ children, activeItemKey, alwaysOpen = false, className, customClassNames, flush, ...rest },
88+
ref,
89+
) => {
3490
const [_activeItemKey, setActiveKey] = useState(activeItemKey)
3591

92+
const _classNames = mergeClassNames<typeof ACCORDION_CLASS_NAMES>(
93+
ACCORDION_CLASS_NAMES,
94+
customClassNames,
95+
)
96+
3697
return (
3798
<div
38-
className={classNames('accordion', { 'accordion-flush': flush }, className)}
99+
className={classNames(
100+
_classNames.ACCORDION,
101+
{ [_classNames.ACCORDION_FLUSH]: flush },
102+
className,
103+
)}
39104
{...rest}
40105
ref={ref}
41106
>

‎packages/coreui-react/src/components/accordion/CAccordionBody.tsx

+44-5
Original file line numberDiff line numberDiff line change
@@ -5,21 +5,60 @@ import classNames from 'classnames'
55
import { CAccordionItemContext } from './CAccordionItem'
66

77
import { CCollapse } from './../collapse/CCollapse'
8+
import { mergeClassNames } from '../../utils'
89

910
export interface CAccordionBodyProps extends HTMLAttributes<HTMLDivElement> {
1011
/**
11-
* A string of all className you want applied to the base component.
12+
* Allows you to apply custom CSS classes to the React Accordion Body for enhanced styling and theming.
13+
*
14+
* @example
15+
* <CAccordionBody className="custom-accordion-body">...</CAccordionBody>
1216
*/
1317
className?: string
18+
19+
/**
20+
* Allows overriding or extending the default CSS class names used in the accordion body component.
21+
* Accepts a partial object matching the shape of `ACCORDION_BODY_CLASS_NAMES`, which includes:
22+
*
23+
* - `ACCORDION_COLLAPSE`: Base class for the collapse container in the accordion body.
24+
* - `ACCORDION_BODY`: Base class for the main content container inside the accordion body.
25+
*
26+
* Use this prop to customize the styles of specific parts of the accordion body.
27+
*
28+
* @example
29+
* const customClasses = {
30+
* ACCORDION_COLLAPSE: 'custom-collapse-class',
31+
* ACCORDION_BODY: 'custom-body-class',
32+
* }
33+
* <CAccordionBody customClassNames={customClasses}>...</CAccordionBody>
34+
*/
35+
customClassNames?: Partial<typeof ACCORDION_BODY_CLASS_NAMES>
36+
}
37+
38+
export const ACCORDION_BODY_CLASS_NAMES = {
39+
/**
40+
* Used for managing collapsible behavior in the accordion body.
41+
*/
42+
ACCORDION_COLLAPSE: 'accordion-collapse',
43+
44+
/**
45+
* Styles the main content container inside the accordion.
46+
*/
47+
ACCORDION_BODY: 'accordion-body',
1448
}
1549

1650
export const CAccordionBody = forwardRef<HTMLDivElement, CAccordionBodyProps>(
17-
({ children, className, ...rest }, ref) => {
18-
const { visible } = useContext(CAccordionItemContext)
51+
({ children, className, customClassNames, ...rest }, ref) => {
52+
const { id, visible } = useContext(CAccordionItemContext)
53+
54+
const _classNames = mergeClassNames<typeof ACCORDION_BODY_CLASS_NAMES>(
55+
ACCORDION_BODY_CLASS_NAMES,
56+
customClassNames,
57+
)
1958

2059
return (
21-
<CCollapse className="accordion-collapse" visible={visible}>
22-
<div className={classNames('accordion-body', className)} {...rest} ref={ref}>
60+
<CCollapse id={id} className={_classNames.ACCORDION_COLLAPSE} visible={visible}>
61+
<div className={classNames(_classNames.ACCORDION_BODY, className)} {...rest} ref={ref}>
2362
{children}
2463
</div>
2564
</CCollapse>

‎packages/coreui-react/src/components/accordion/CAccordionButton.tsx

+29-5
Original file line numberDiff line numberDiff line change
@@ -4,22 +4,46 @@ import classNames from 'classnames'
44

55
import { CAccordionItemContext } from './CAccordionItem'
66

7+
import { mergeClassNames } from '../../utils'
8+
79
export interface CAccordionButtonProps extends HTMLAttributes<HTMLButtonElement> {
810
/**
9-
* A string of all className you want applied to the base component.
11+
* Styles the clickable element in the accordion header.
1012
*/
1113
className?: string
14+
15+
/**
16+
* Allows overriding or extending the default CSS class names used in the accordion button component.
17+
* Accepts a partial object matching the shape of `CLASS_NAMES`, which includes:
18+
*
19+
* - `ACCORDION_BUTTON`: Base class for the accordion button.
20+
*
21+
* Use this prop to customize the styles of the accordion button.
22+
*
23+
* @example
24+
* const customClasses = {
25+
* ACCORDION_BUTTON: 'custom-button-class',
26+
* }
27+
* <CAccordionButton customClassNames={customClasses}>...</CAccordionButton>
28+
*/
29+
customClassNames?: Partial<typeof CLASS_NAMES>
30+
}
31+
32+
export const CLASS_NAMES = {
33+
ACCORDION_BUTTON: 'accordion-button',
1234
}
1335

1436
export const CAccordionButton = forwardRef<HTMLButtonElement, CAccordionButtonProps>(
15-
({ children, className, ...rest }, ref) => {
16-
const { visible, setVisible } = useContext(CAccordionItemContext)
37+
({ children, className, customClassNames, ...rest }, ref) => {
38+
const { id, visible, setVisible } = useContext(CAccordionItemContext)
39+
const _classNames = mergeClassNames<typeof CLASS_NAMES>(CLASS_NAMES, customClassNames)
1740

1841
return (
1942
<button
2043
type="button"
21-
className={classNames('accordion-button', { collapsed: !visible }, className)}
22-
aria-expanded={!visible}
44+
className={classNames(_classNames.ACCORDION_BUTTON, { collapsed: !visible }, className)}
45+
aria-controls={id}
46+
aria-expanded={visible}
2347
onClick={() => setVisible(!visible)}
2448
{...rest}
2549
ref={ref}

‎packages/coreui-react/src/components/accordion/CAccordionHeader.tsx

+37-3
Original file line numberDiff line numberDiff line change
@@ -3,19 +3,53 @@ import PropTypes from 'prop-types'
33
import classNames from 'classnames'
44

55
import { CAccordionButton } from './CAccordionButton'
6+
import { mergeClassNames } from '../../utils'
67

78
export interface CAccordionHeaderProps extends HTMLAttributes<HTMLDivElement> {
89
/**
910
* A string of all className you want applied to the base component.
1011
*/
1112
className?: string
13+
/**
14+
* Allows overriding or extending the default CSS class names used in the accordion header component.
15+
* Accepts a partial object matching the shape of `ACCORDION_HEADER_CLASS_NAMES`, which includes:
16+
*
17+
* - `ACCORDION_HEADER`: Base class for the accordion header container.
18+
* - `ACCORDION_BUTTON`: Class applied to the button within the accordion header.
19+
*
20+
* Use this prop to customize the styles of specific parts of the accordion header.
21+
*
22+
* @example
23+
* const customClasses = {
24+
* ACCORDION_HEADER: 'custom-header-class',
25+
* ACCORDION_BUTTON: 'custom-button-class',
26+
* }
27+
* <CAccordionHeader customClassNames={customClasses}>...</CAccordionHeader>
28+
*/
29+
customClassNames?: Partial<typeof ACCORDION_HEADER_CLASS_NAMES>
30+
}
31+
32+
export const ACCORDION_HEADER_CLASS_NAMES = {
33+
/**
34+
* Styles the header container of an accordion item.
35+
*/
36+
ACCORDION_HEADER: 'accordion-header',
37+
38+
/**
39+
* Styles the clickable element in the accordion header.
40+
*/
41+
ACCORDION_BUTTON: 'accordion-button',
1242
}
1343

1444
export const CAccordionHeader = forwardRef<HTMLDivElement, CAccordionHeaderProps>(
15-
({ children, className, ...rest }, ref) => {
45+
({ children, className, customClassNames, ...rest }, ref) => {
46+
const _classNames = mergeClassNames<typeof ACCORDION_HEADER_CLASS_NAMES>(
47+
ACCORDION_HEADER_CLASS_NAMES,
48+
customClassNames,
49+
)
1650
return (
17-
<div className={classNames('accordion-header', className)} {...rest} ref={ref}>
18-
<CAccordionButton>{children}</CAccordionButton>
51+
<div className={classNames(_classNames.ACCORDION_HEADER, className)} {...rest} ref={ref}>
52+
<CAccordionButton className={_classNames.ACCORDION_HEADER}>{children}</CAccordionButton>
1953
</div>
2054
)
2155
},

‎packages/coreui-react/src/components/accordion/CAccordionItem.tsx

+36-3
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,18 @@ import React, {
44
HTMLAttributes,
55
useContext,
66
useEffect,
7+
useId,
78
useRef,
89
useState,
910
} from 'react'
1011
import PropTypes from 'prop-types'
1112
import classNames from 'classnames'
1213

1314
import { CAccordionContext } from './CAccordion'
15+
import { mergeClassNames } from '../../utils'
1416

1517
export interface CAccordionItemContextProps {
18+
id: string
1619
setVisible: (a: boolean) => void
1720
visible?: boolean
1821
}
@@ -24,19 +27,49 @@ export interface CAccordionItemProps extends HTMLAttributes<HTMLDivElement> {
2427
* A string of all className you want applied to the base component.
2528
*/
2629
className?: string
30+
31+
/**
32+
* Allows overriding or extending the default CSS class names used in the accordion item component.
33+
* Accepts a partial object matching the shape of `ACCORDION_ITEM_CLASS_NAMES`, which includes:
34+
*
35+
* - `ACCORDION_ITEM`: Base class for an individual accordion item.
36+
*
37+
* Use this prop to customize the styles of specific parts of the accordion item.
38+
*
39+
* @example
40+
* const customClasses = {
41+
* ACCORDION_ITEM: 'custom-item-class',
42+
* }
43+
* <CAccordionItem customClassNames={customClasses}>...</CAccordionItem>
44+
*/
45+
customClassNames?: Partial<typeof ACCORDION_ITEM_CLASS_NAMES>
46+
2747
/**
2848
* Item key.
2949
*/
3050
itemKey?: number | string
3151
}
3252

53+
export const ACCORDION_ITEM_CLASS_NAMES = {
54+
/**
55+
* Base class for an individual accordion item.
56+
*/
57+
ACCORDION_ITEM: 'accordion-item',
58+
}
59+
3360
export const CAccordionItem = forwardRef<HTMLDivElement, CAccordionItemProps>(
34-
({ children, className, itemKey, ...rest }, ref) => {
61+
({ children, className, customClassNames, itemKey, ...rest }, ref) => {
62+
const id = useId()
3563
const _itemKey = useRef(itemKey ?? Math.random().toString(36).slice(2, 11))
3664

3765
const { _activeItemKey, alwaysOpen, setActiveKey } = useContext(CAccordionContext)
3866
const [visible, setVisible] = useState(Boolean(_activeItemKey === _itemKey.current))
3967

68+
const _classNames = mergeClassNames<typeof ACCORDION_ITEM_CLASS_NAMES>(
69+
ACCORDION_ITEM_CLASS_NAMES,
70+
customClassNames,
71+
)
72+
4073
useEffect(() => {
4174
!alwaysOpen && visible && setActiveKey(_itemKey.current)
4275
}, [visible])
@@ -46,8 +79,8 @@ export const CAccordionItem = forwardRef<HTMLDivElement, CAccordionItemProps>(
4679
}, [_activeItemKey])
4780

4881
return (
49-
<div className={classNames('accordion-item', className)} {...rest} ref={ref}>
50-
<CAccordionItemContext.Provider value={{ setVisible, visible }}>
82+
<div className={classNames(_classNames.ACCORDION_ITEM, className)} {...rest} ref={ref}>
83+
<CAccordionItemContext.Provider value={{ id, setVisible, visible }}>
5184
{children}
5285
</CAccordionItemContext.Provider>
5386
</div>

‎packages/coreui-react/src/utils/index.ts

+2
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import getRTLPlacement from './getRTLPlacement'
44
import getTransitionDurationFromElement from './getTransitionDurationFromElement'
55
import isInViewport from './isInViewport'
66
import isRTL from './isRTL'
7+
import mergeClassNames from './mergeClassNames'
78

89
export {
910
executeAfterTransition,
@@ -12,4 +13,5 @@ export {
1213
getTransitionDurationFromElement,
1314
isInViewport,
1415
isRTL,
16+
mergeClassNames,
1517
}

0 commit comments

Comments
 (0)