Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
5ce67c4
DataViews: Migrate modals from @wordpress/components Modal to @wordpr…
ciampo Mar 26, 2026
8567e19
DataViews: Add ActionModal unit tests
ciampo Apr 8, 2026
1999c92
Update CHANGELOG
ciampo Mar 30, 2026
6a4e7af
DataViews: Simplify focus callbacks using Dialog's smart default
ciampo Mar 30, 2026
58bf660
DataViews: Minor cleanups for Dialog migration
ciampo Apr 8, 2026
d06056c
Update CHANGELOG: move entries to Unreleased, consolidate
ciampo Apr 8, 2026
0a85172
DataViews: Use the `portal` prop for per-instance dialog z-index
ciampo Apr 8, 2026
b32b2cf
Fix ts errors in types
ciampo Apr 15, 2026
b303141
DataViews: Fix focus trap, tooltip z-index, and legacy-compat CSS layer
ciampo Apr 15, 2026
e521e68
fix dep order
ciampo Apr 16, 2026
a7af87c
DataViews: Polish ActionModal — version, types, scoping, scrolling
ciampo Apr 30, 2026
876c37c
DataViews: Share focus-on-mount mapping between modal layouts
ciampo Apr 30, 2026
96cccbc
DataViews: Note the 'firstElement' focus-on-mount semantic shift in C…
ciampo Apr 30, 2026
5f1d231
DataViews: Keep Dialog mounted for entry/exit animations
ciampo May 6, 2026
e0dc85d
DataViews: Drop optional renderPopup gate in PanelModal
ciampo May 6, 2026
bccb670
DataViews: Make ActionModal own one stable action per instance
ciampo Apr 30, 2026
3b520e3
DataViews: Bind PanelModal session state to popup lifecycle
ciampo Apr 30, 2026
442257b
DataViews: pull Dialog.Root and Dialog.Trigger out of ActionModal to …
ciampo May 1, 2026
6c18def
DataViews PanelModal: idiomatic Dialog.Action for Cancel and Apply
ciampo May 1, 2026
37b712c
DataViews item-actions: compose triggers via render
ciampo May 6, 2026
5eee5d6
DataViews: tidy CHANGELOGs and useMapFocusOnMount JSDoc
ciampo May 7, 2026
a755cb6
DataForm PanelModal: idiomatic Dialog.Trigger composition + Apply gating
ciampo May 7, 2026
7b35dba
DataViews ItemActions: hoist Dialog.Root above the kebab Menu
ciampo May 7, 2026
817a516
DataViews bulk actions: compose ActionWithModal through ActionTrigger
ciampo May 7, 2026
8ef7da9
DataViews: drop direct Base UI references from comments
ciampo May 7, 2026
98bc2ce
DataForm PanelModal: read isValid straight from useFormValidity
ciampo May 7, 2026
402c139
DataViews CHANGELOG: Refresh stale Code Quality entry
ciampo May 7, 2026
ac1baac
DataViews ItemActionsMenu: Rename triggerRender to renderTrigger
ciampo May 7, 2026
4d5e4ab
DataForm SummaryButton: Drop default aria-haspopup, pass explicitly
ciampo May 7, 2026
99ad8b8
DataForm SummaryButton: Drop event-type cast in handleKeyDown
ciampo May 7, 2026
31079ea
DataViews: Extract getActionLabel helper
ciampo May 7, 2026
190cc4b
DataViews: Clarify wp-ui-legacy-compat scoping rationale
ciampo May 7, 2026
ca27558
DataViews ActionModal: Drop 'full' from modalSize public surface
ciampo May 7, 2026
7a3d4a3
DataViews ActionModal: Translate 'fill' silently instead of deprecating
ciampo May 7, 2026
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
1 change: 1 addition & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

14 changes: 11 additions & 3 deletions packages/dataviews/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,19 @@

## Unreleased

## 14.2.0 (2026-04-29)

### Enhancements

- DataForm: Render field `description` as help text in the `array` control.[#77554](https://github.com/WordPress/gutenberg/pull/77554)
- DataForm: `PanelModal`'s Apply button is now disabled while the in-progress form has validation errors, preventing invalid edits from being committed silently. Cancel remains enabled so users can always discard the draft. ([#78028](https://github.com/WordPress/gutenberg/pull/78028))

### Code Quality

- DataViews: Item actions now share a single `ItemActionsMenu` host that renders the kebab `<Menu>` and the per-action `Dialog.Root` as siblings. The dialog is hoisted out of `Menu.Popover`'s `unmountOnHide` subtree so a modal action stays mounted while the menu closes. `ButtonTrigger` (used in the inline-button path) gained `forwardRef` and spreads unknown props so it composes under `Dialog.Trigger`. A new `genericForwardRef` helper centralises the `forwardRef`-with-generics TypeScript workaround. ([#78028](https://github.com/WordPress/gutenberg/pull/78028))

### Breaking Changes

- DataViews: Migrate action modals and DataForm panel modals from `@wordpress/components` `Modal` to `@wordpress/ui` `Dialog`. Action modals with `hideModalHeader: true` now render a `Dialog.Popup` with `role="alertdialog"` and `disablePointerDismissal`, requiring an explicit user action to dismiss. Custom CSS targeting `.components-modal__*` classes inside action modals will no longer work. The `dataforms-layouts-panel__modal` CSS class on the panel modal and the `dataforms-layouts-panel__modal-footer` CSS class have been removed. The `modalFocusOnMount` value `'firstElement'` now behaves like `'firstContentElement'` (the new Dialog primitive's smart default already skips the close icon and focuses the first content tabbable, so the legacy distinction between the two is no longer meaningful). ([#78028](https://github.com/WordPress/gutenberg/pull/78028))

## 14.2.0 (2026-04-29)

## 14.1.0 (2026-04-15)

Expand Down
1 change: 1 addition & 0 deletions packages/dataviews/global.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@
// To ensure that global types are included, we need to
// explicitly reference them here.
import '@testing-library/jest-dom';
import '@wordpress/jest-console';
1 change: 1 addition & 0 deletions packages/dataviews/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@
"@storybook/react-vite": "^10.2.8",
"@testing-library/jest-dom": "^6.9.1",
"@types/jest": "^29.5.14",
"@wordpress/jest-console": "file:../jest-console",
"esbuild": "^0.27.2",
"storybook": "^10.2.8"
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,7 @@ function PanelDropdown< Item >( {
disabled={ fieldDefinition.readOnly === true }
onClick={ onToggle }
aria-expanded={ isOpen }
aria-haspopup="true"
/>
) }
renderContent={ ( { onClose } ) => (
Expand Down
143 changes: 69 additions & 74 deletions packages/dataviews/src/components/dataform-layouts/panel/modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,9 @@ import deepMerge from 'deepmerge';
/**
* WordPress dependencies
*/
import {
__experimentalSpacer as Spacer,
Button,
Modal,
} from '@wordpress/components';

import { useContext, useMemo, useRef, useState } from '@wordpress/element';
import { useFocusOnMount, useMergeRefs } from '@wordpress/compose';
import { Stack } from '@wordpress/ui';
// eslint-disable-next-line @wordpress/use-recommended-components
import { Dialog } from '@wordpress/ui';

/**
* Internal dependencies
Expand All @@ -31,29 +25,29 @@ import { DataFormLayout } from '../data-form-layout';
import { DEFAULT_LAYOUT } from '../normalize-form';
import SummaryButton from './summary-button';
import useFormValidity from '../../../hooks/use-form-validity';
import useMapFocusOnMount from '../../../hooks/use-map-focus-on-mount';
import useReportValidity from '../../../hooks/use-report-validity';
import DataFormContext from '../../dataform-context';
import useFieldFromFormField from './utils/use-field-from-form-field';

function ModalContent< Item >( {
function PanelDialogContent< Item >( {
data,
field,
onChange,
fieldLabel,
onClose,
touched,
}: {
data: Item;
field: NormalizedFormField;
onChange: ( data: Partial< Item > ) => void;
onClose: () => void;
fieldLabel: string;
touched: boolean;
} ) {
const { openAs } = field.layout as NormalizedPanelLayout;
const { applyLabel, cancelLabel } = openAs as PanelOpenAsModal;
const { fields } = useContext( DataFormContext );
const [ changes, setChanges ] = useState< Partial< Item > >( {} );

const modalData = useMemo( () => {
return deepMerge( data, changes, {
arrayMerge: ( target, source ) => source,
Expand Down Expand Up @@ -84,12 +78,11 @@ function ModalContent< Item >( {
maxLength: f.isValid.maxLength?.constraint,
},
} ) );
const { validity } = useFormValidity( modalData, fieldsAsFieldType, form );

const onApply = () => {
onChange( changes );
onClose();
};
const { validity, isValid } = useFormValidity(
modalData,
fieldsAsFieldType,
form
);

const handleOnChange = ( newValue: Partial< Item > ) => {
setChanges( ( prev ) =>
Expand All @@ -99,23 +92,21 @@ function ModalContent< Item >( {
);
};

const focusOnMountRef = useFocusOnMount( 'firstInputElement' );
const contentRef = useRef< HTMLDivElement >( null );
const mergedRef = useMergeRefs( [ focusOnMountRef, contentRef ] );

// When the modal is opened after being previously closed (touched),
// trigger reportValidity to show field-level errors.
useReportValidity( contentRef, touched );

const initialFocus = useMapFocusOnMount( 'firstInputElement', contentRef );

return (
<Modal
className="dataforms-layouts-panel__modal"
onRequestClose={ onClose }
isFullScreen={ false }
title={ fieldLabel }
size="medium"
>
<div ref={ mergedRef }>
<Dialog.Popup size="medium" initialFocus={ initialFocus }>
<Dialog.Header>
<Dialog.Title>{ fieldLabel }</Dialog.Title>
<Dialog.CloseIcon />
</Dialog.Header>
<Dialog.Content ref={ contentRef }>
<DataFormLayout
data={ modalData }
form={ form }
Expand All @@ -139,29 +130,17 @@ function ModalContent< Item >( {
/>
) }
</DataFormLayout>
</div>
<Stack
direction="row"
className="dataforms-layouts-panel__modal-footer"
gap="md"
>
<Spacer style={ { flex: 1 } } />
<Button
variant="tertiary"
onClick={ onClose }
__next40pxDefaultSize
>
{ cancelLabel }
</Button>
<Button
variant="primary"
onClick={ onApply }
__next40pxDefaultSize
</Dialog.Content>
<Dialog.Footer>
<Dialog.Action variant="outline">{ cancelLabel }</Dialog.Action>
<Dialog.Action
onClick={ () => onChange( changes ) }
disabled={ ! isValid }
>
{ applyLabel }
</Button>
</Stack>
</Modal>
</Dialog.Action>
</Dialog.Footer>
</Dialog.Popup>
);
}

Expand All @@ -172,44 +151,60 @@ function PanelModal< Item >( {
validity,
}: FieldLayoutProps< Item > ) {
const [ touched, setTouched ] = useState( false );

const [ isOpen, setIsOpen ] = useState( false );
// `Dialog.Root` stays mounted across opens, so the session component
// holding the in-progress `changes` state would also persist by default.
// Bump `sessionKey` on `onOpenChangeComplete` (when the exit animation
// finishes) to force-remount `<PanelDialogContent>` between sessions —
// this preserves the existing "Cancel/close always wipes the draft"
// semantic without disturbing the form contents during the exit
// animation itself.
const [ sessionKey, setSessionKey ] = useState( 0 );

const { fieldDefinition, fieldLabel, summaryFields } =
useFieldFromFormField( field );
if ( ! fieldDefinition ) {
return null;
}

const handleClose = () => {
setIsOpen( false );
setTouched( true );
};

return (
<>
<SummaryButton
<Dialog.Root
onOpenChange={ ( open ) => {
if ( ! open ) {
// Mark the field as "touched" once the dialog has been
// opened and dismissed at least once, so validation
// messages on the summary trigger the next time the
// user opens it.
setTouched( true );
}
} }
onOpenChangeComplete={ ( open ) => {
if ( ! open ) {
setSessionKey( ( k ) => k + 1 );
}
} }
>
<Dialog.Trigger
render={
<SummaryButton
data={ data }
field={ field }
fieldLabel={ fieldLabel }
summaryFields={ summaryFields }
validity={ validity }
touched={ touched }
disabled={ fieldDefinition.readOnly === true }
/>
}
/>
<PanelDialogContent
key={ sessionKey }
data={ data }
field={ field }
fieldLabel={ fieldLabel }
summaryFields={ summaryFields }
validity={ validity }
onChange={ onChange }
fieldLabel={ fieldLabel ?? '' }
touched={ touched }
disabled={ fieldDefinition.readOnly === true }
onClick={ () => setIsOpen( true ) }
aria-expanded={ isOpen }
/>
{ isOpen && (
<ModalContent
data={ data }
field={ field }
onChange={ onChange }
fieldLabel={ fieldLabel ?? '' }
onClose={ handleClose }
touched={ touched }
/>
) }
</>
</Dialog.Root>
);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -159,10 +159,6 @@
margin-bottom: $grid-unit-20;
}

.dataforms-layouts-panel__modal-footer {
margin-top: $grid-unit-20;
}

.components-popover.components-dropdown__content.dataforms-layouts-panel__field-dropdown {
z-index: z-index(".components-popover.components-dropdown__content.dataforms-layouts-panel__field-dropdown");
}
Loading
Loading