diff --git a/packages/ui/CHANGELOG.md b/packages/ui/CHANGELOG.md index 1e5bea6e67afa8..7af6d21a72e450 100644 --- a/packages/ui/CHANGELOG.md +++ b/packages/ui/CHANGELOG.md @@ -19,6 +19,7 @@ - `Notice`: Improve narrow layout by letting description and actions span the icon column when a title is present ([#76202](https://github.com/WordPress/gutenberg/pull/76202)). - `Notice`: Use `Text` component for `Title` and `Description` typography ([#75870](https://github.com/WordPress/gutenberg/pull/75870)). - `Card`, `CollapsibleCard`: update padding to match legacy `Card` component ([#76368](https://github.com/WordPress/gutenberg/pull/76368)). +- `CollapsibleCard`: move trigger to the header ([#76265](https://github.com/WordPress/gutenberg/pull/76265)). ## 0.8.0 (2026-03-04) diff --git a/packages/ui/src/collapsible-card/header.tsx b/packages/ui/src/collapsible-card/header.tsx index cf4c880bb5aabe..a1dbb0add52878 100644 --- a/packages/ui/src/collapsible-card/header.tsx +++ b/packages/ui/src/collapsible-card/header.tsx @@ -1,12 +1,11 @@ import clsx from 'clsx'; -import type { MouseEvent } from 'react'; -import { forwardRef, useCallback, useRef } from '@wordpress/element'; -import { __ } from '@wordpress/i18n'; -import { chevronDown, chevronUp } from '@wordpress/icons'; +import { forwardRef } from '@wordpress/element'; +import { chevronDown } from '@wordpress/icons'; import * as Card from '../card'; import * as Collapsible from '../collapsible'; -import { IconButton } from '../icon-button'; +import { Icon } from '../icon'; import styles from './style.module.css'; +import focusStyles from '../utils/css/focus.module.css'; import type { HeaderProps } from './types'; /** @@ -20,62 +19,35 @@ import type { HeaderProps } from './types'; */ export const Header = forwardRef< HTMLDivElement, HeaderProps >( function CollapsibleCardHeader( - { children, className, onClick, ...restProps }, + { children, className, render, ...restProps }, ref ) { - const triggerRef = useRef< HTMLButtonElement >( null ); - - const handleHeaderClick = useCallback( - ( event: MouseEvent< HTMLDivElement > ) => { - const trigger = triggerRef.current; - if ( - trigger && - event.target instanceof Node && - ! trigger.contains( event.target ) - ) { - trigger.click(); - } - - onClick?.( event ); - }, - [ onClick ] - ); - return ( - + } + nativeButton={ false } >
{ children }
- ( - +
-
+ ); } ); diff --git a/packages/ui/src/collapsible-card/style.module.css b/packages/ui/src/collapsible-card/style.module.css index cc1f8659afc1d5..55b0744ebb6532 100644 --- a/packages/ui/src/collapsible-card/style.module.css +++ b/packages/ui/src/collapsible-card/style.module.css @@ -19,7 +19,19 @@ /* Offset by half the button's own height so it visually centers at the wrapper's midpoint (which `align-self: center` places at the vertical center of the header). */ - transform: translateY(-50%); + translate: 0 -50%; + + /* For an outline that looks like `IconButton`'s */ + border-radius: var(--wpds-border-radius-sm); + } + + .header[data-panel-open] .header-trigger { + rotate: 180deg; + } + + /* Simulate disabled button styles */ + .header[data-disabled] .header-trigger { + color: var(--wpds-color-fg-interactive-neutral-disabled); } } @@ -29,8 +41,9 @@ flex-direction: row; align-items: stretch; gap: var(--wpds-dimension-gap-sm); + outline: none; - &:has(.header-trigger:not([data-disabled])) { + &:not([data-disabled]) { cursor: var(--wpds-cursor-control); } } diff --git a/packages/ui/src/collapsible-card/test/index.test.tsx b/packages/ui/src/collapsible-card/test/index.test.tsx index 6c2004807c4229..445f7e2da5da6e 100644 --- a/packages/ui/src/collapsible-card/test/index.test.tsx +++ b/packages/ui/src/collapsible-card/test/index.test.tsx @@ -93,7 +93,8 @@ describe( 'CollapsibleCard', () => { await user.click( screen.getByRole( 'button', { - name: 'Expand or collapse card', + name: 'Title', + expanded: false, } ) ); @@ -101,7 +102,8 @@ describe( 'CollapsibleCard', () => { await user.click( screen.getByRole( 'button', { - name: 'Expand or collapse card', + name: 'Title', + expanded: true, } ) ); @@ -110,31 +112,6 @@ describe( 'CollapsibleCard', () => { ).not.toBeInTheDocument(); } ); - it( 'toggles content when clicking the header area', async () => { - const user = userEvent.setup(); - - render( - - - Header click test - - -

Header toggled content

-
-
- ); - - expect( - screen.queryByText( 'Header toggled content' ) - ).not.toBeInTheDocument(); - - await user.click( screen.getByText( 'Header click test' ) ); - - expect( - screen.getByText( 'Header toggled content' ) - ).toBeVisible(); - } ); - it( 'calls onOpenChange when toggled', async () => { const onOpenChange = jest.fn(); const user = userEvent.setup(); @@ -152,7 +129,8 @@ describe( 'CollapsibleCard', () => { await user.click( screen.getByRole( 'button', { - name: 'Expand or collapse card', + name: 'Title', + expanded: false, } ) ); @@ -179,7 +157,8 @@ describe( 'CollapsibleCard', () => { await user.click( screen.getByRole( 'button', { - name: 'Expand or collapse card', + name: 'Title', + expanded: true, } ) ); @@ -188,7 +167,7 @@ describe( 'CollapsibleCard', () => { } ); describe( 'trigger', () => { - it( 'renders a toggle button', () => { + it( 'renders the header as a toggle button', () => { render( @@ -199,7 +178,8 @@ describe( 'CollapsibleCard', () => { expect( screen.getByRole( 'button', { - name: 'Expand or collapse card', + name: 'Title', + expanded: false, } ) ).toBeVisible(); } ); diff --git a/packages/ui/src/utils/css/focus.module.css b/packages/ui/src/utils/css/focus.module.css index 9a48e96826a460..d11f440305dca8 100644 --- a/packages/ui/src/utils/css/focus.module.css +++ b/packages/ui/src/utils/css/focus.module.css @@ -6,7 +6,8 @@ .outset-ring--focus-visible, .outset-ring--focus-within, .outset-ring--focus-within-except-active, - .outset-ring--focus-within-visible { + .outset-ring--focus-within-visible, + .outset-ring--focus-parent-visible { @media not ( prefers-reduced-motion ) { transition: outline 0.1s ease-out; } @@ -24,7 +25,8 @@ .outset-ring--focus-visible:focus-visible, .outset-ring--focus-within:focus-within, .outset-ring--focus-within-except-active:focus-within:not(:has(:active)), - .outset-ring--focus-within-visible:focus-within:has(:focus-visible) { + .outset-ring--focus-within-visible:focus-within:has(:focus-visible), + :focus-visible .outset-ring--focus-parent-visible { outline-width: var(--wpds-border-width-focus); outline-color: var(--wpds-color-stroke-focus-brand); }