-
Notifications
You must be signed in to change notification settings - Fork 1.1k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Accordion #6931
Accordion #6931
Conversation
Hey @reidbarber thanks getting to work on this! I've got a couple of questions regarding the spec the team chose here and would love to provide some input from our own learnings as we implemented both
<DisclosureTrigger isOpen>
<Heading level={3}>
<Button />
</Heading>
<Disclosure>
<Text>I am controlled.</Text>
</Disclosure>
</DisclosureTrigger>
const { buttonProps, regionProps } = isNil(ctxState)
? // eslint-disable-next-line react-hooks/rules-of-hooks
useDisclosureTrigger(state)
: // eslint-disable-next-line react-hooks/rules-of-hooks
useAccordionItem(props, ctxState, objectRef);
export const useDisclosureAnimation = (
...option: AtomicDisclosureAnimationOptions
): AtomicDisclosureAnimation => {
const [ref, isOpen] = option;
const previous = useRef(isOpen);
const [, rerender] = useState({});
const isAnimating = previous.current !== isOpen;
const onEnd = useCallback(() => rerender({}), []);
if (isAnimating) previous.current = isOpen;
useAnimation(ref, isAnimating, onEnd);
return {
isOpening: isOpen && isAnimating,
isClosing: !isOpen && isAnimating,
};
};
|
@nwidynski Thanks for the input! We are definitely still considering different API options, so I appreciate the comments.
I think the main reason is that we want to support additional interactive elements adjacent to the accordion header, and that pattern usually means the entire first child is the trigger.
We haven't given much thought into including a disclosure component, but as it seems like a simple subset of accordion features, I think we could consider having accordion build on top it, maybe at the hook level.
This looks great! I think we definitely want to include these states.
Arrow key navigation between items is optional in ARIA, so we're still considering options for this. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just some initial comments, but I think overall the api makes sense. Might be a bit odd to have a AccordionItem only for RAC without a container Accordion element, but the wrapping container doesn't seem to have much of a purpose so it might be ok. If we feel like that might be confusing, we could change it from "Item" so that it doesn't sound like it is a collection element
<AccordionHeader> | ||
Your files | ||
</AccordionHeader> | ||
<AccordionPanel> | ||
files | ||
</AccordionPanel> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Noticed that if you flip the order of these that the panel renders about the header when opened. This is probably fine if we want to just enforce the order that these elements are provided to the AccordionItem but otherwise we might need to use a flex container + order?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For react spectrum i think enforcing the order is a good idea
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please add react-stately/accordion to the ts strict config
<AccordionHeader> | ||
Your files | ||
</AccordionHeader> | ||
<AccordionPanel> | ||
files | ||
</AccordionPanel> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For react spectrum i think enforcing the order is a good idea
@@ -0,0 +1,128 @@ | |||
/* |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just checking, but recommendation for building a mutually exclusive AccordionGroup with arrow key navigation would be something like this?
function AccordionGroup() {
let [openIndex, setOpenIndex] = useState(0);
let focusManager = useFocusManager({accept: (node) => {
return node.getAttribute('data-accordion-trigger-g1') === true;
}});
return (
<div onKeyDown={(e) => {
if (e.key === 'ArrowDown') {
focusManager.focusNext();
e.preventDefault();
} else if (e.key === 'ArrowUp') {
focusManager.focusPrevious();
e.preventDefault();
}
}}>
<AccordionItem isOpen={openIndex === 0} onOpenChange={val => val && setOpenIndex(0)}>
{({isOpen}) => (
<>
<Header>
<Heading level={3}>
<Button slot="trigger" data-accordion-trigger-g1>{isOpen ? '⬇️' : '➡️'} Item 1</Button>
</Heading>
</Header>
<AccordionPanel>
<p>Content for panel 1.</p>
</AccordionPanel>
</>
)}
</AccordionItem>
<AccordionItem isOpen={openIndex === 1} onOpenChange={val => val && setOpenIndex(1)}>
{({isOpen}) => (
<>
<Header>
<Heading level={3}>
<Button slot="trigger" data-accordion-trigger-g1>{isOpen ? '⬇️' : '➡️'} Item 2</Button>
</Heading>
</Header>
<AccordionPanel>
<p>Panel 2's content.</p>
</AccordionPanel>
</>
)}
</AccordionItem>
</div>
);
}
export const AccordionGroupExample = {
render: () => <FocusScope><AccordionGroup /></FocusScope>
};
It uses more normal numbers
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
behavior looks good, just one other dep that I think we can get rid of
@@ -39,8 +39,10 @@ | |||
"dependencies": { | |||
"@internationalized/date": "^3.5.5", | |||
"@internationalized/string": "^3.2.3", | |||
"@react-aria/accordion": "3.0.0-alpha.33", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can we remove this dep now? Noticed it in @react-spectrum/accordion
's package json too
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Happy for anything else to be follow up
## API Changes
react-aria-components/react-aria-components:Disclosure+Disclosure {
+ children?: ReactNode | ((DisclosureRenderProps & {
+ defaultChildren: ReactNode | undefined
+})) => ReactNode
+ className?: string | ((DisclosureRenderProps & {
+ defaultClassName: string | undefined
+})) => string
+ defaultExpanded?: boolean
+ isDisabled?: boolean
+ isExpanded?: boolean
+ onExpandedChange?: (boolean) => void
+ onHoverChange?: (boolean) => void
+ onHoverEnd?: (HoverEvent) => void
+ onHoverStart?: (HoverEvent) => void
+ slot?: string | null
+ style?: CSSProperties | ((DisclosureRenderProps & {
+ defaultStyle: CSSProperties
+})) => CSSProperties | undefined
+} /react-aria-components:DisclosurePanel+DisclosurePanel {
+ children: ReactNode
+ className?: string | (({
+
+} & {
+ defaultClassName: string | undefined
+})) => string
+ role?: 'group' | 'region' = 'group'
+ style?: CSSProperties | (({
+
+} & {
+ defaultStyle: CSSProperties
+})) => CSSProperties | undefined
+} /react-aria-components:DisclosureStateContext+DisclosureStateContext {
+ UNTYPED
+} /react-aria-components:DisclosureContext+DisclosureContext {
+ UNTYPED
+} /react-aria-components:DisclosureProps+DisclosureProps {
+ children?: ReactNode | ((DisclosureRenderProps & {
+ defaultChildren: ReactNode | undefined
+})) => ReactNode
+ className?: string | ((DisclosureRenderProps & {
+ defaultClassName: string | undefined
+})) => string
+ defaultExpanded?: boolean
+ isDisabled?: boolean
+ isExpanded?: boolean
+ onExpandedChange?: (boolean) => void
+ onHoverChange?: (boolean) => void
+ onHoverEnd?: (HoverEvent) => void
+ onHoverStart?: (HoverEvent) => void
+ slot?: string | null
+ style?: CSSProperties | ((DisclosureRenderProps & {
+ defaultStyle: CSSProperties
+})) => CSSProperties | undefined
+} /react-aria-components:DisclosurePanelProps+DisclosurePanelProps {
+ children: ReactNode
+ className?: string | (({
+
+} & {
+ defaultClassName: string | undefined
+})) => string
+ role?: 'group' | 'region' = 'group'
+ style?: CSSProperties | (({
+
+} & {
+ defaultStyle: CSSProperties
+})) => CSSProperties | undefined
+} @react-spectrum/accordion/@react-spectrum/accordion:Accordion-Accordion <T extends {}> {
+Accordion {
UNSAFE_className?: string
UNSAFE_style?: CSSProperties
alignSelf?: Responsive<'auto' | 'normal' | 'start' | 'end' | 'center' | 'flex-start' | 'flex-end' | 'self-start' | 'self-end' | 'stretch'>
+ aria-describedby?: string
+ aria-details?: string
+ aria-label?: string
+ aria-labelledby?: string
bottom?: Responsive<DimensionValue>
- children: CollectionChildren<{}>
- defaultExpandedKeys?: Iterable<Key>
- disabledKeys?: Iterable<Key>
+ children: React.ReactNode
end?: Responsive<DimensionValue>
- expandedKeys?: Iterable<Key>
flex?: Responsive<string | number | boolean>
flexBasis?: Responsive<number | string>
flexGrow?: Responsive<number>
flexShrink?: Responsive<number>
gridArea?: Responsive<string>
gridColumn?: Responsive<string>
gridColumnEnd?: Responsive<string>
gridColumnStart?: Responsive<string>
gridRow?: Responsive<string>
gridRowEnd?: Responsive<string>
gridRowStart?: Responsive<string>
height?: Responsive<DimensionValue>
id?: string
isHidden?: Responsive<boolean>
- items?: Iterable<{}>
justifySelf?: Responsive<'auto' | 'normal' | 'start' | 'end' | 'flex-start' | 'flex-end' | 'self-start' | 'self-end' | 'center' | 'left' | 'right' | 'stretch'>
left?: Responsive<DimensionValue>
margin?: Responsive<DimensionValue>
marginBottom?: Responsive<DimensionValue>
marginEnd?: Responsive<DimensionValue>
marginStart?: Responsive<DimensionValue>
marginTop?: Responsive<DimensionValue>
marginX?: Responsive<DimensionValue>
marginY?: Responsive<DimensionValue>
maxHeight?: Responsive<DimensionValue>
maxWidth?: Responsive<DimensionValue>
minHeight?: Responsive<DimensionValue>
minWidth?: Responsive<DimensionValue>
- onExpandedChange?: (Set<Key>) => any
order?: Responsive<number>
position?: Responsive<'static' | 'relative' | 'absolute' | 'fixed' | 'sticky'>
right?: Responsive<DimensionValue>
start?: Responsive<DimensionValue>
width?: Responsive<DimensionValue>
zIndex?: Responsive<number>
} /@react-spectrum/accordion:Item-Item <T> {
- props: ItemProps<T>
- returnVal: undefined
-} /@react-spectrum/accordion:SpectrumAccordionProps-SpectrumAccordionProps <T> {
+SpectrumAccordionProps {
UNSAFE_className?: string
UNSAFE_style?: CSSProperties
alignSelf?: Responsive<'auto' | 'normal' | 'start' | 'end' | 'center' | 'flex-start' | 'flex-end' | 'self-start' | 'self-end' | 'stretch'>
+ aria-describedby?: string
+ aria-details?: string
+ aria-label?: string
+ aria-labelledby?: string
bottom?: Responsive<DimensionValue>
- children: CollectionChildren<T>
- defaultExpandedKeys?: Iterable<Key>
- disabledKeys?: Iterable<Key>
+ children: React.ReactNode
end?: Responsive<DimensionValue>
- expandedKeys?: Iterable<Key>
flex?: Responsive<string | number | boolean>
flexBasis?: Responsive<number | string>
flexGrow?: Responsive<number>
flexShrink?: Responsive<number>
gridArea?: Responsive<string>
gridColumn?: Responsive<string>
gridColumnEnd?: Responsive<string>
gridColumnStart?: Responsive<string>
gridRow?: Responsive<string>
gridRowEnd?: Responsive<string>
gridRowStart?: Responsive<string>
height?: Responsive<DimensionValue>
id?: string
isHidden?: Responsive<boolean>
- items?: Iterable<T>
justifySelf?: Responsive<'auto' | 'normal' | 'start' | 'end' | 'flex-start' | 'flex-end' | 'self-start' | 'self-end' | 'center' | 'left' | 'right' | 'stretch'>
left?: Responsive<DimensionValue>
margin?: Responsive<DimensionValue>
marginBottom?: Responsive<DimensionValue>
marginEnd?: Responsive<DimensionValue>
marginStart?: Responsive<DimensionValue>
marginTop?: Responsive<DimensionValue>
marginX?: Responsive<DimensionValue>
marginY?: Responsive<DimensionValue>
maxHeight?: Responsive<DimensionValue>
maxWidth?: Responsive<DimensionValue>
minHeight?: Responsive<DimensionValue>
minWidth?: Responsive<DimensionValue>
- onExpandedChange?: (Set<Key>) => any
order?: Responsive<number>
position?: Responsive<'static' | 'relative' | 'absolute' | 'fixed' | 'sticky'>
right?: Responsive<DimensionValue>
start?: Responsive<DimensionValue>
width?: Responsive<DimensionValue>
zIndex?: Responsive<number>
} /@react-spectrum/accordion:Disclosure+Disclosure {
+ aria-describedby?: string
+ aria-details?: string
+ aria-label?: string
+ aria-labelledby?: string
+ children: [ReactElement<SpectrumDisclosureHeaderProps>, ReactElement<SpectrumDisclosurePanelProps>]
+ className?: string | ((DisclosureRenderProps & {
+ defaultClassName: string | undefined
+})) => string
+ defaultExpanded?: boolean
+ id?: string
+ isDisabled?: boolean
+ isExpanded?: boolean
+ onExpandedChange?: (boolean) => void
+ onHoverChange?: (boolean) => void
+ onHoverEnd?: (HoverEvent) => void
+ onHoverStart?: (HoverEvent) => void
+ slot?: string | null
+ style?: CSSProperties | ((DisclosureRenderProps & {
+ defaultStyle: CSSProperties
+})) => CSSProperties | undefined
+} /@react-spectrum/accordion:DisclosureHeader+DisclosureHeader {
+ aria-describedby?: string
+ aria-details?: string
+ aria-label?: string
+ aria-labelledby?: string
+ children: React.ReactNode
+ id?: string
+ level?: number = 3
+} /@react-spectrum/accordion:DisclosurePanel+DisclosurePanel {
+ aria-describedby?: string
+ aria-details?: string
+ aria-label?: string
+ aria-labelledby?: string
+ children: React.ReactNode
+ className?: string | (({
+
+} & {
+ defaultClassName: string | undefined
+})) => string
+ id?: string
+ role?: 'group' | 'region' = 'group'
+ style?: CSSProperties | (({
+
+} & {
+ defaultStyle: CSSProperties
+})) => CSSProperties | undefined
+} /@react-spectrum/accordion:SpectrumDisclosureProps+SpectrumDisclosureProps {
+ aria-describedby?: string
+ aria-details?: string
+ aria-label?: string
+ aria-labelledby?: string
+ children: [ReactElement<SpectrumDisclosureHeaderProps>, ReactElement<SpectrumDisclosurePanelProps>]
+ className?: string | ((DisclosureRenderProps & {
+ defaultClassName: string | undefined
+})) => string
+ defaultExpanded?: boolean
+ id?: string
+ isDisabled?: boolean
+ isExpanded?: boolean
+ onExpandedChange?: (boolean) => void
+ onHoverChange?: (boolean) => void
+ onHoverEnd?: (HoverEvent) => void
+ onHoverStart?: (HoverEvent) => void
+ slot?: string | null
+ style?: CSSProperties | ((DisclosureRenderProps & {
+ defaultStyle: CSSProperties
+})) => CSSProperties | undefined
+} /@react-spectrum/accordion:SpectrumDisclosurePanelProps+SpectrumDisclosurePanelProps {
+ aria-describedby?: string
+ aria-details?: string
+ aria-label?: string
+ aria-labelledby?: string
+ children: React.ReactNode
+ className?: string | (({
+
+} & {
+ defaultClassName: string | undefined
+})) => string
+ id?: string
+ role?: 'group' | 'region' = 'group'
+ style?: CSSProperties | (({
+
+} & {
+ defaultStyle: CSSProperties
+})) => CSSProperties | undefined
+} /@react-spectrum/accordion:SpectrumDisclosureHeaderProps+SpectrumDisclosureHeaderProps {
+ aria-describedby?: string
+ aria-details?: string
+ aria-label?: string
+ aria-labelledby?: string
+ children: React.ReactNode
+ id?: string
+ level?: number = 3
+} @react-spectrum/s2/@react-spectrum/s2:Accordion+Accordion {
+ UNSAFE_className?: string
+ UNSAFE_style?: CSSProperties
+ children: React.ReactNode
+ density?: 'compact' | 'regular' | 'spacious' = "regular"
+ id?: string
+ isDisabled?: boolean
+ isQuiet?: boolean
+ size?: 'S' | 'M' | 'L' | 'XL' = "M"
+ slot?: string | null
+ styles?: StylesPropWithHeight
+} /@react-spectrum/s2:AccordionContext+AccordionContext {
+ UNTYPED
+} /@react-spectrum/s2:DisclosureHeader+DisclosureHeader {
+ UNSAFE_className?: string
+ UNSAFE_style?: CSSProperties
+ children: React.ReactNode
+ id?: string
+ level?: number = 3
+} /@react-spectrum/s2:Disclosure+Disclosure {
+ UNSAFE_className?: string
+ UNSAFE_style?: CSSProperties
+ children: [ReactElement<DisclosureHeaderProps>, ReactElement<DisclosurePanelProps>]
+ className?: string | ((DisclosureRenderProps & {
+ defaultClassName: string | undefined
+})) => string
+ defaultExpanded?: boolean
+ density?: 'compact' | 'regular' | 'spacious' = "regular"
+ id?: string
+ isDisabled?: boolean
+ isExpanded?: boolean
+ isQuiet?: boolean
+ onExpandedChange?: (boolean) => void
+ onHoverChange?: (boolean) => void
+ onHoverEnd?: (HoverEvent) => void
+ onHoverStart?: (HoverEvent) => void
+ size?: 'S' | 'M' | 'L' | 'XL' = "M"
+ slot?: string | null
+ style?: CSSProperties | ((DisclosureRenderProps & {
+ defaultStyle: CSSProperties
+})) => CSSProperties | undefined
+ styles?: StylesProp
+} /@react-spectrum/s2:DisclosurePanel+DisclosurePanel {
+ UNSAFE_className?: string
+ UNSAFE_style?: CSSProperties
+ aria-describedby?: string
+ aria-details?: string
+ aria-label?: string
+ aria-labelledby?: string
+ children: React.ReactNode
+ className?: string | (({
+
+} & {
+ defaultClassName: string | undefined
+})) => string
+ id?: string
+ role?: 'group' | 'region' = 'group'
+ style?: CSSProperties | (({
+
+} & {
+ defaultStyle: CSSProperties
+})) => CSSProperties | undefined
+} /@react-spectrum/s2:DisclosureContext+DisclosureContext {
+ UNTYPED
+} /@react-spectrum/s2:AccordionProps+AccordionProps {
+ UNSAFE_className?: string
+ UNSAFE_style?: CSSProperties
+ children: React.ReactNode
+ density?: 'compact' | 'regular' | 'spacious' = "regular"
+ id?: string
+ isDisabled?: boolean
+ isQuiet?: boolean
+ size?: 'S' | 'M' | 'L' | 'XL' = "M"
+ slot?: string | null
+ styles?: StylesPropWithHeight
+} /@react-spectrum/s2:DisclosureProps+DisclosureProps {
+ UNSAFE_className?: string
+ UNSAFE_style?: CSSProperties
+ children: [ReactElement<DisclosureHeaderProps>, ReactElement<DisclosurePanelProps>]
+ className?: string | ((DisclosureRenderProps & {
+ defaultClassName: string | undefined
+})) => string
+ defaultExpanded?: boolean
+ density?: 'compact' | 'regular' | 'spacious' = "regular"
+ id?: string
+ isDisabled?: boolean
+ isExpanded?: boolean
+ isQuiet?: boolean
+ onExpandedChange?: (boolean) => void
+ onHoverChange?: (boolean) => void
+ onHoverEnd?: (HoverEvent) => void
+ onHoverStart?: (HoverEvent) => void
+ size?: 'S' | 'M' | 'L' | 'XL' = "M"
+ slot?: string | null
+ style?: CSSProperties | ((DisclosureRenderProps & {
+ defaultStyle: CSSProperties
+})) => CSSProperties | undefined
+ styles?: StylesProp
+} /@react-spectrum/s2:DisclosurePanelProps+DisclosurePanelProps {
+ UNSAFE_className?: string
+ UNSAFE_style?: CSSProperties
+ aria-describedby?: string
+ aria-details?: string
+ aria-label?: string
+ aria-labelledby?: string
+ children: React.ReactNode
+ className?: string | (({
+
+} & {
+ defaultClassName: string | undefined
+})) => string
+ id?: string
+ role?: 'group' | 'region' = 'group'
+ style?: CSSProperties | (({
+
+} & {
+ defaultStyle: CSSProperties
+})) => CSSProperties | undefined
+} @react-aria/disclosure/@react-aria/disclosure:useDisclosure+useDisclosure {
+ props: AriaDisclosureProps
+ state: DisclosureState
+ ref?: RefObject<Element | null>
+ returnVal: undefined
+} /@react-aria/disclosure:DisclosureAria+DisclosureAria {
+ buttonProps: AriaButtonProps
+ contentProps: HTMLAttributes<HTMLElement>
+} /@react-aria/disclosure:AriaDisclosureProps+AriaDisclosureProps {
+ defaultExpanded?: boolean
+ isDisabled?: boolean
+ isExpanded?: boolean
+ onExpandedChange?: (boolean) => void
+} @react-stately/disclosure/@react-stately/disclosure:useDisclosureState+useDisclosureState {
+ props: DisclosureProps
+ returnVal: undefined
+} /@react-stately/disclosure:DisclosureState+DisclosureState {
+ collapse: () => void
+ expand: () => void
+ isExpanded: boolean
+ setExpanded: (boolean) => void
+ toggle: () => void
+} /@react-stately/disclosure:DisclosureProps+DisclosureProps {
+ defaultExpanded?: boolean
+ isExpanded?: boolean
+ onExpandedChange?: (boolean) => void
+} |
Accordion
This implements the Accordion pattern, with an emphasis on individual accordion items as opposed to groups.
Features
API
React Aria/Stately hooks:
React Aria components:
RSP v3 components:
Spectrum 2 components:
Examples
RAC
Spectrum 2
Additional comments
With regards to an Accordion (or AccordionGroup) wrapper around AccordionItem elements in React Aria, there is no ARIA semantics for that element, and keyboard arrow navigation is listed as optional, so we're going to prioritize the individual AccordionItem element for now but may revisit this in a follow-up. The main benefit would be controlling the expanded items' state at the group level and using Up/Down or Home/End keyboard navigation between item headers.
✅ Pull Request Checklist:
📝 Test Instructions:
🧢 Your Project: