From 716b316173b7297a23d6e54cb093b60e25e5952f Mon Sep 17 00:00:00 2001 From: stdavis Date: Thu, 16 Oct 2025 13:27:36 -0600 Subject: [PATCH] fix(Disclosure): Prevent incorrectly showing content when isDisabled is toggled Fixes #9004 --- .../disclosure/src/useDisclosure.ts | 2 +- .../disclosure/test/useDisclosure.test.ts | 45 ++++++++---- .../stories/DisclosureGroup.stories.tsx | 69 +++++++++++++++++++ .../test/Disclosure.test.js | 4 +- 4 files changed, 104 insertions(+), 16 deletions(-) create mode 100644 packages/react-aria-components/stories/DisclosureGroup.stories.tsx diff --git a/packages/@react-aria/disclosure/src/useDisclosure.ts b/packages/@react-aria/disclosure/src/useDisclosure.ts index e640b95af28..ab55538a91f 100644 --- a/packages/@react-aria/disclosure/src/useDisclosure.ts +++ b/packages/@react-aria/disclosure/src/useDisclosure.ts @@ -155,7 +155,7 @@ export function useDisclosure(props: AriaDisclosureProps, state: DisclosureState role: 'group', 'aria-labelledby': triggerId, 'aria-hidden': !state.isExpanded, - hidden: (isSSR || isDisabled) ? (isDisabled || !state.isExpanded) : undefined + hidden: !state.isExpanded || undefined } }; } diff --git a/packages/@react-aria/disclosure/test/useDisclosure.test.ts b/packages/@react-aria/disclosure/test/useDisclosure.test.ts index 665c3d52909..b3bcf38ffe5 100644 --- a/packages/@react-aria/disclosure/test/useDisclosure.test.ts +++ b/packages/@react-aria/disclosure/test/useDisclosure.test.ts @@ -90,6 +90,25 @@ describe('useDisclosure', () => { expect(result.current.state.isExpanded).toBe(false); }); + it('should keep panel hidden when toggling disabled state', () => { + let {result, rerender} = renderHook(({isDisabled}: {isDisabled: boolean}) => { + let state = useDisclosureState({}); + return useDisclosure({isDisabled}, state, ref); + }, {initialProps: {isDisabled: false}}); + + act(() => { + rerender({isDisabled: true}); + }); + + expect(result.current.panelProps.hidden).toBe(true); + + act(() => { + rerender({isDisabled: false}); + }); + + expect(result.current.panelProps.hidden).toBe(true); + }); + it('should set correct IDs for accessibility', () => { let {result} = renderHook(() => { let state = useDisclosureState({}); @@ -111,27 +130,27 @@ describe('useDisclosure', () => { writable: true, configurable: true }); - + const ref = {current: document.createElement('div')}; - + const {result} = renderHook(() => { const state = useDisclosureState({}); const disclosure = useDisclosure({}, state, ref); return {state, disclosure}; }); - + expect(result.current.state.isExpanded).toBe(false); expect(ref.current.getAttribute('hidden')).toBe('until-found'); - + // Simulate the 'beforematch' event act(() => { const event = new Event('beforematch', {bubbles: true}); ref.current.dispatchEvent(event); }); - + expect(result.current.state.isExpanded).toBe(true); expect(ref.current.hasAttribute('hidden')).toBe(false); - + Object.defineProperty(document.body, 'onbeforematch', { value: originalOnBeforeMatch, writable: true, @@ -148,31 +167,31 @@ describe('useDisclosure', () => { writable: true, configurable: true }); - + const ref = {current: document.createElement('div')}; - + const onExpandedChange = jest.fn(); - + const {result} = renderHook(() => { const state = useDisclosureState({isExpanded: false, onExpandedChange}); const disclosure = useDisclosure({isExpanded: false}, state, ref); return {state, disclosure}; }); - + expect(result.current.state.isExpanded).toBe(false); expect(ref.current.getAttribute('hidden')).toBe('until-found'); - + // Simulate the 'beforematch' event act(() => { const event = new Event('beforematch', {bubbles: true}); ref.current.dispatchEvent(event); }); - + expect(result.current.state.isExpanded).toBe(false); expect(ref.current.getAttribute('hidden')).toBe('until-found'); expect(onExpandedChange).toHaveBeenCalledTimes(1); expect(onExpandedChange).toHaveBeenCalledWith(true); - + Object.defineProperty(document.body, 'onbeforematch', { value: originalOnBeforeMatch, writable: true, diff --git a/packages/react-aria-components/stories/DisclosureGroup.stories.tsx b/packages/react-aria-components/stories/DisclosureGroup.stories.tsx new file mode 100644 index 00000000000..5d4a19b3ee1 --- /dev/null +++ b/packages/react-aria-components/stories/DisclosureGroup.stories.tsx @@ -0,0 +1,69 @@ +/* + * Copyright 2024 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ + +import {Button, Heading} from 'react-aria-components'; +import { + Disclosure, + DisclosureGroup, + DisclosurePanel +} from '../src/Disclosure'; +import {Meta, StoryFn} from '@storybook/react'; +import React from 'react'; +import './styles.css'; + +export default { + title: 'React Aria Components/DisclosureGroup', + component: DisclosureGroup +} as Meta; + +export type DisclosureGroupStory = StoryFn; + +export const DisclosureGroupExample: DisclosureGroupStory = (args) => { + const [isDisabled, setIsDisabled] = React.useState(false); + const toggleDisabled = () => setIsDisabled((d) => !d); + + return ( + <> + + + + {({isExpanded}) => ( + <> + + + + +

This is the content of the disclosure panel.

+
+ + )} +
+ + {({isExpanded}) => ( + <> + + + + +

This is the content of the disclosure panel.

+
+ + )} +
+
+ + ); +}; diff --git a/packages/react-aria-components/test/Disclosure.test.js b/packages/react-aria-components/test/Disclosure.test.js index 56b8303d1fb..1ebfb13e91e 100644 --- a/packages/react-aria-components/test/Disclosure.test.js +++ b/packages/react-aria-components/test/Disclosure.test.js @@ -98,7 +98,7 @@ describe('Disclosure', () => { expect(panel).not.toBeVisible(); }); - it('should not expand a disabled disclosure via isExpanded', () => { + it('should expand a disabled disclosure via isExpanded', () => { const {getByTestId, queryByText} = render( @@ -112,7 +112,7 @@ describe('Disclosure', () => { const disclosure = getByTestId('disclosure'); expect(disclosure).toHaveAttribute('data-disabled', 'true'); const panel = queryByText('Content'); - expect(panel).not.toBeVisible(); + expect(panel).toBeVisible(); }); it('should support controlled isExpanded prop', async () => {