diff --git a/packages/@react-aria/disclosure/src/useDisclosure.ts b/packages/@react-aria/disclosure/src/useDisclosure.ts index 66c9f5e5b5c..d67a942e01d 100644 --- a/packages/@react-aria/disclosure/src/useDisclosure.ts +++ b/packages/@react-aria/disclosure/src/useDisclosure.ts @@ -41,14 +41,13 @@ export interface DisclosureAria { * @param state - State for the disclosure, as returned by `useDisclosureState`. * @param ref - A ref for the disclosure panel. */ -export function useDisclosure(props: AriaDisclosureProps, state: DisclosureState, ref: RefObject): DisclosureAria { +export function useDisclosure(props: AriaDisclosureProps, state: DisclosureState, ref: RefObject): DisclosureAria { let { isDisabled } = props; let triggerId = useId(); let panelId = useId(); let isSSR = useIsSSR(); - let supportsBeforeMatch = !isSSR && 'onbeforematch' in document.body; let raf = useRef(null); @@ -66,22 +65,64 @@ export function useDisclosure(props: AriaDisclosureProps, state: DisclosureState }, [ref, state]); // @ts-ignore https://github.com/facebook/react/pull/24741 - useEvent(ref, 'beforematch', supportsBeforeMatch ? handleBeforeMatch : null); + useEvent(ref, 'beforematch', handleBeforeMatch); + let isExpandedRef = useRef(null); useLayoutEffect(() => { // Cancel any pending RAF to prevent stale updates if (raf.current) { cancelAnimationFrame(raf.current); } - // Until React supports hidden="until-found": https://github.com/facebook/react/pull/24741 - if (supportsBeforeMatch && ref.current && !isDisabled) { - if (state.isExpanded) { - ref.current.removeAttribute('hidden'); - } else { - ref.current.setAttribute('hidden', 'until-found'); + if (ref.current && !isDisabled && !isSSR) { + let panel = ref.current; + + if (isExpandedRef.current == null || typeof panel.getAnimations !== 'function') { + // On initial render (and in tests), set attributes without animation. + if (state.isExpanded) { + panel.removeAttribute('hidden'); + panel.style.setProperty('--disclosure-panel-width', 'auto'); + panel.style.setProperty('--disclosure-panel-height', 'auto'); + } else { + panel.setAttribute('hidden', 'until-found'); + panel.style.setProperty('--disclosure-panel-width', '0px'); + panel.style.setProperty('--disclosure-panel-height', '0px'); + } + } else if (state.isExpanded !== isExpandedRef.current) { + if (state.isExpanded) { + panel.removeAttribute('hidden'); + + // Set the width and height as pixels so they can be animated. + panel.style.setProperty('--disclosure-panel-width', panel.scrollWidth + 'px'); + panel.style.setProperty('--disclosure-panel-height', panel.scrollHeight + 'px'); + + Promise.all(panel.getAnimations().map(a => a.finished)) + .then(() => { + // After the animations complete, switch back to auto so the content can resize. + panel.style.setProperty('--disclosure-panel-width', 'auto'); + panel.style.setProperty('--disclosure-panel-height', 'auto'); + }) + .catch(() => {}); + } else { + panel.style.setProperty('--disclosure-panel-width', panel.scrollWidth + 'px'); + panel.style.setProperty('--disclosure-panel-height', panel.scrollHeight + 'px'); + + // Force style re-calculation to trigger animations. + window.getComputedStyle(panel).height; + + // Animate to zero size. + panel.style.setProperty('--disclosure-panel-width', '0px'); + panel.style.setProperty('--disclosure-panel-height', '0px'); + + // Wait for animations to apply the hidden attribute. + Promise.all(panel.getAnimations().map(a => a.finished)) + .then(() => panel.setAttribute('hidden', 'until-found')) + .catch(() => {}); + } } + + isExpandedRef.current = state.isExpanded; } - }, [isDisabled, ref, state.isExpanded, supportsBeforeMatch]); + }, [isDisabled, ref, state.isExpanded, isSSR]); useEffect(() => { return () => { @@ -114,7 +155,7 @@ export function useDisclosure(props: AriaDisclosureProps, state: DisclosureState role: 'group', 'aria-labelledby': triggerId, 'aria-hidden': !state.isExpanded, - hidden: supportsBeforeMatch ? true : !state.isExpanded + hidden: isSSR ? !state.isExpanded : undefined } }; } diff --git a/packages/@react-aria/disclosure/test/useDisclosure.test.ts b/packages/@react-aria/disclosure/test/useDisclosure.test.ts index 6c23bfd9576..665c3d52909 100644 --- a/packages/@react-aria/disclosure/test/useDisclosure.test.ts +++ b/packages/@react-aria/disclosure/test/useDisclosure.test.ts @@ -31,7 +31,6 @@ describe('useDisclosure', () => { let {buttonProps, panelProps} = result.current; expect(buttonProps['aria-expanded']).toBe(false); - expect(panelProps.hidden).toBe(true); expect(panelProps['aria-hidden']).toBe(true); }); @@ -44,7 +43,7 @@ describe('useDisclosure', () => { let {buttonProps, panelProps} = result.current; expect(buttonProps['aria-expanded']).toBe(true); - expect(panelProps.hidden).toBe(false); + expect(panelProps['aria-hidden']).toBe(false); }); it('should handle expanding on press event (with mouse)', () => { diff --git a/packages/@react-spectrum/s2/src/Disclosure.tsx b/packages/@react-spectrum/s2/src/Disclosure.tsx index 65e72b3254b..7511a77176b 100644 --- a/packages/@react-spectrum/s2/src/Disclosure.tsx +++ b/packages/@react-spectrum/s2/src/Disclosure.tsx @@ -297,20 +297,20 @@ export interface DisclosurePanelProps extends Omit - {props.children} + className={(UNSAFE_className ?? '') + panelStyles}> +
+ {props.children} +
); }); diff --git a/packages/react-aria-components/docs/Disclosure.mdx b/packages/react-aria-components/docs/Disclosure.mdx index b23bd5ca725..49b30fd02f2 100644 --- a/packages/react-aria-components/docs/Disclosure.mdx +++ b/packages/react-aria-components/docs/Disclosure.mdx @@ -50,7 +50,7 @@ import {ChevronRight} from 'lucide-react'; -

Details about system requirements here.

+ Details about system requirements here.
``` @@ -74,6 +74,7 @@ import {ChevronRight} from 'lucide-react'; display: flex; align-items: center; gap: 8px; + padding: 8px 0; svg { rotate: 0deg; @@ -84,13 +85,20 @@ import {ChevronRight} from 'lucide-react'; } } + .react-aria-Heading { + margin-bottom: 0; + } + &[data-expanded] .react-aria-Button[slot=trigger] svg { rotate: 90deg; } } .react-aria-DisclosurePanel { - margin-left: 32px; + margin-left: 26px; + height: var(--disclosure-panel-height); + transition: height 250ms; + overflow: clip; } ``` @@ -149,7 +157,7 @@ function MyDisclosure({title, children, ...props}: MyDisclosureProps) { -

{children}

+ {children}
) @@ -213,7 +221,7 @@ In some use cases, you may want to add an interactive element, like a button, ad -

Details about system requirements here.

+ Details about system requirements here.
``` @@ -282,6 +290,8 @@ A `Disclosure` can be targeted with the `.react-aria-Disclosure` CSS selector, o +Use the `--disclosure-panel-width` and `--disclosure-panel-height` CSS variables to implement animations. + ### Button A `Button` can be targeted with the `.react-aria-Button` CSS selector, or by overriding with a custom `className`. It supports the following states: diff --git a/packages/react-aria-components/docs/DisclosureGroup.mdx b/packages/react-aria-components/docs/DisclosureGroup.mdx index b6e2feb2b16..dac3c1e37f5 100644 --- a/packages/react-aria-components/docs/DisclosureGroup.mdx +++ b/packages/react-aria-components/docs/DisclosureGroup.mdx @@ -53,7 +53,7 @@ import {ChevronRight} from 'lucide-react'; -

Personal information form here.

+ Personal information form here.
@@ -64,7 +64,7 @@ import {ChevronRight} from 'lucide-react'; -

Billing address form here.

+ Billing address form here.
@@ -139,7 +139,7 @@ function MyDisclosure({title, children, ...props}: MyDisclosureProps) { -

{children}

+ {children}
) @@ -228,7 +228,7 @@ In some use cases, you may want to add an interactive element, like a button, ad -

Details about system requirements here.

+ Details about system requirements here.
@@ -242,7 +242,7 @@ In some use cases, you may want to add an interactive element, like a button, ad -

Details about personal information here.

+ Details about personal information here.
@@ -329,6 +329,8 @@ A `Disclosure` can be targeted with the `.react-aria-Disclosure` CSS selector, o +Use the `--disclosure-panel-width` and `--disclosure-panel-height` CSS variables to implement animations. + ### Button A `Button` can be targeted with the `.react-aria-Button` CSS selector, or by overriding with a custom `className`. It supports the following states: diff --git a/starters/docs/src/Disclosure.css b/starters/docs/src/Disclosure.css index f42aa703aff..e76a2d28504 100644 --- a/starters/docs/src/Disclosure.css +++ b/starters/docs/src/Disclosure.css @@ -33,6 +33,9 @@ } .react-aria-DisclosurePanel { - margin-left: 32px; + margin-left: 26px; color: var(--text-color); + height: var(--disclosure-panel-height); + transition: height 250ms; + overflow: clip; } diff --git a/starters/tailwind/src/Disclosure.tsx b/starters/tailwind/src/Disclosure.tsx index ea67f0ce46a..d88dc40fe33 100644 --- a/starters/tailwind/src/Disclosure.tsx +++ b/starters/tailwind/src/Disclosure.tsx @@ -97,8 +97,8 @@ export interface DisclosurePanelProps extends AriaDisclosurePanelProps { export function DisclosurePanel({ children, ...props }: DisclosurePanelProps) { return ( - - {children} + +
{children}
); }