Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 52 additions & 11 deletions packages/@react-aria/disclosure/src/useDisclosure.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Element | null>): DisclosureAria {
export function useDisclosure(props: AriaDisclosureProps, state: DisclosureState, ref: RefObject<HTMLElement | null>): DisclosureAria {
let {
isDisabled
} = props;
let triggerId = useId();
let panelId = useId();
let isSSR = useIsSSR();
let supportsBeforeMatch = !isSSR && 'onbeforematch' in document.body;

let raf = useRef<number | null>(null);

Expand All @@ -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<boolean | null>(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 () => {
Expand Down Expand Up @@ -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
}
};
}
3 changes: 1 addition & 2 deletions packages/@react-aria/disclosure/test/useDisclosure.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});

Expand All @@ -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)', () => {
Expand Down
33 changes: 17 additions & 16 deletions packages/@react-spectrum/s2/src/Disclosure.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -297,20 +297,20 @@ export interface DisclosurePanelProps extends Omit<RACDisclosurePanelProps, 'cla

const panelStyles = style({
font: 'body',
paddingTop: {
isExpanded: 8
},
paddingBottom: {
isExpanded: 16
},
height: '--disclosure-panel-height',
overflow: 'clip',
transition: '[height]'
});

const panelInner = style({
paddingTop: 8,
paddingBottom: 16,
paddingX: {
isExpanded: {
size: {
S: 8,
M: space(9),
L: 12,
XL: space(15)
}
size: {
S: 8,
M: space(9),
L: 12,
XL: space(15)
}
}
});
Expand All @@ -326,15 +326,16 @@ export const DisclosurePanel = forwardRef(function DisclosurePanel(props: Disclo
} = props;
const domProps = filterDOMProps(otherProps);
let {size} = useSlottedContext(DisclosureContext)!;
let {isExpanded} = useContext(DisclosureStateContext)!;
let panelRef = useDOMRef(ref);
return (
<RACDisclosurePanel
{...domProps}
ref={panelRef}
style={UNSAFE_style}
className={(UNSAFE_className ?? '') + panelStyles({size, isExpanded})}>
{props.children}
className={(UNSAFE_className ?? '') + panelStyles}>
<div className={panelInner({size})}>
{props.children}
</div>
</RACDisclosurePanel>
);
});
Expand Down
18 changes: 14 additions & 4 deletions packages/react-aria-components/docs/Disclosure.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ import {ChevronRight} from 'lucide-react';
</Button>
</Heading>
<DisclosurePanel>
<p>Details about system requirements here.</p>
Details about system requirements here.
</DisclosurePanel>
</Disclosure>
```
Expand All @@ -74,6 +74,7 @@ import {ChevronRight} from 'lucide-react';
display: flex;
align-items: center;
gap: 8px;
padding: 8px 0;

svg {
rotate: 0deg;
Expand All @@ -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;
}
```

Expand Down Expand Up @@ -149,7 +157,7 @@ function MyDisclosure({title, children, ...props}: MyDisclosureProps) {
</Button>
</Heading>
<DisclosurePanel>
<p>{children}</p>
{children}
</DisclosurePanel>
</Disclosure>
)
Expand Down Expand Up @@ -213,7 +221,7 @@ In some use cases, you may want to add an interactive element, like a button, ad
<Button>Click me</Button>
</div>
<DisclosurePanel>
<p>Details about system requirements here.</p>
Details about system requirements here.
</DisclosurePanel>
</Disclosure>
```
Expand Down Expand Up @@ -282,6 +290,8 @@ A `Disclosure` can be targeted with the `.react-aria-Disclosure` CSS selector, o

<StateTable properties={docs.exports.DisclosureRenderProps.properties} />

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:
Expand Down
12 changes: 7 additions & 5 deletions packages/react-aria-components/docs/DisclosureGroup.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ import {ChevronRight} from 'lucide-react';
</Button>
</Heading>
<DisclosurePanel>
<p>Personal information form here.</p>
Personal information form here.
</DisclosurePanel>
</Disclosure>
<Disclosure id="billing">
Expand All @@ -64,7 +64,7 @@ import {ChevronRight} from 'lucide-react';
</Button>
</Heading>
<DisclosurePanel>
<p>Billing address form here.</p>
Billing address form here.
</DisclosurePanel>
</Disclosure>
</DisclosureGroup>
Expand Down Expand Up @@ -139,7 +139,7 @@ function MyDisclosure({title, children, ...props}: MyDisclosureProps) {
</Button>
</Heading>
<DisclosurePanel>
<p>{children}</p>
{children}
</DisclosurePanel>
</Disclosure>
)
Expand Down Expand Up @@ -228,7 +228,7 @@ In some use cases, you may want to add an interactive element, like a button, ad
<Button>Click me</Button>
</div>
<DisclosurePanel>
<p>Details about system requirements here.</p>
Details about system requirements here.
</DisclosurePanel>
</Disclosure>
<Disclosure id="personal">
Expand All @@ -242,7 +242,7 @@ In some use cases, you may want to add an interactive element, like a button, ad
<Button>Click me</Button>
</div>
<DisclosurePanel>
<p>Details about personal information here.</p>
Details about personal information here.
</DisclosurePanel>
</Disclosure>
</DisclosureGroup>
Expand Down Expand Up @@ -329,6 +329,8 @@ A `Disclosure` can be targeted with the `.react-aria-Disclosure` CSS selector, o

<StateTable properties={docs.exports.DisclosureRenderProps.properties} />

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:
Expand Down
5 changes: 4 additions & 1 deletion starters/docs/src/Disclosure.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
4 changes: 2 additions & 2 deletions starters/tailwind/src/Disclosure.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -97,8 +97,8 @@ export interface DisclosurePanelProps extends AriaDisclosurePanelProps {

export function DisclosurePanel({ children, ...props }: DisclosurePanelProps) {
return (
<AriaDisclosurePanel {...props} className={composeTailwindRenderProps(props.className, 'group-data-[expanded]:px-4 group-data-[expanded]:py-2')}>
{children}
<AriaDisclosurePanel {...props} className={composeTailwindRenderProps(props.className, 'h-(--disclosure-panel-height) transition-[height] overflow-clip')}>
<div className="px-4 py-2">{children}</div>
</AriaDisclosurePanel>
);
}
Expand Down