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
9 changes: 5 additions & 4 deletions package-lock.json

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

1 change: 1 addition & 0 deletions packages/block-editor/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@

### Bug Fixes

- `isBlockSelected`: Return `false` when called without a client ID, instead of matching the `undefined` client ID of an empty selection ([#81212](https://github.com/WordPress/gutenberg/pull/81212)).
- Gate the HEIC canvas conversion fallback on `window.__clientSideMediaProcessing` instead of the redundant `window.__heicUploadSupport` flag, fixing client-side HEIC conversion in Safari on core WordPress installs ([#80452](https://github.com/WordPress/gutenberg/pull/80452)).
- `URLInput`: Request suggestions for a value the field is mounted with, instead of waiting for the input to be focused, and stop requesting initial suggestions on mount when `disableSuggestions` is set ([#80721](https://github.com/WordPress/gutenberg/pull/80721)).
- `URLInput`: Collapse a text selection reaching the start of the field before letting an up arrow press through to the editor, so selecting to the start and pressing up no longer navigates out of the field instead of collapsing the caret ([#80780](https://github.com/WordPress/gutenberg/pull/80780)).
Expand Down
4 changes: 3 additions & 1 deletion packages/block-editor/src/store/selectors.js
Original file line number Diff line number Diff line change
Expand Up @@ -1313,7 +1313,9 @@ export function isBlockSelected( state, clientId ) {
return false;
}

return selectionStart.clientId === clientId;
// Both sides are `undefined` when nothing is selected and the caller
// passes an optional client ID.
return !! clientId && selectionStart.clientId === clientId;
}

/**
Expand Down
11 changes: 11 additions & 0 deletions packages/block-editor/src/store/test/selectors.js
Original file line number Diff line number Diff line change
Expand Up @@ -1945,6 +1945,17 @@ describe( 'selectors', () => {

expect( isBlockSelected( state, '23' ) ).toBe( false );
} );

it( 'should return false if there is no client ID', () => {
const state = {
selection: {
selectionStart: {},
selectionEnd: {},
},
};

expect( isBlockSelected( state, undefined ) ).toBe( false );
} );
} );

describe( 'hasSelectedInnerBlock', () => {
Expand Down
5 changes: 1 addition & 4 deletions packages/block-library/src/paragraph/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,17 +16,14 @@ import save from './save';
import transforms from './transforms';
import { unlock } from '../lock-unlock';

const { fieldsKey, formKey, editableRootKey } = unlock( blocksPrivateApis );
const { fieldsKey, formKey } = unlock( blocksPrivateApis );

const { name } = metadata;

export { metadata, name };

export const settings = {
icon,
// Opt into the editing host behaviour privately. It's a Symbol setting
// rather than a public `supports` key so it stays an internal detail.
[ editableRootKey ]: true,
example: {
attributes: {
content: __(
Expand Down
1 change: 1 addition & 0 deletions packages/blocks/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
### Internal

- Update `memize` to 2.1.1 ([#80764](https://github.com/WordPress/gutenberg/pull/80764)).
- Update `hpq` to 1.4.0 for its bundled TypeScript types ([#81199](https://github.com/WordPress/gutenberg/pull/81199)).

## 15.24.0 (2026-07-14)

Expand Down
2 changes: 1 addition & 1 deletion packages/blocks/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@
"change-case": "^4.1.2",
"colord": "^2.9.3",
"fast-deep-equal": "^3.1.3",
"hpq": "^1.3.0",
"hpq": "^1.4.0",
"is-plain-object": "^5.0.0",
"marked": "^18.0.3",
"memize": "^2.1.1",
Expand Down
1 change: 0 additions & 1 deletion packages/blocks/src/api/matchers.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
/**
* External dependencies
*/
// @ts-expect-error `hpq` does not ship type declarations.
export { attr, prop, text, query } from 'hpq';

/**
Expand Down
31 changes: 18 additions & 13 deletions packages/blocks/src/api/parser/get-block-attributes.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
/**
* External dependencies
*/
// @ts-expect-error `hpq` does not ship type declarations.
import { parse as hpqParse } from 'hpq';
import type { MatcherObj } from 'hpq';
import memoize from 'memize';

/**
Expand Down Expand Up @@ -39,9 +39,9 @@ import type { BlockAttribute, BlockType } from '../../types';
* @return Enhanced hpq matcher.
*/
export const toBooleanAttributeMatcher =
( matcher: ( value: unknown ) => unknown ) =>
( value: unknown ): boolean =>
matcher( value ) !== undefined;
( matcher: ( domNode: Element ) => unknown ) =>
( domNode: Element ): boolean =>
matcher( domNode ) !== undefined;

/**
* Returns true if value is of the given JSON schema type, or false otherwise.
Expand Down Expand Up @@ -205,12 +205,12 @@ export const matcherFromSource = memoize(
): ( ( domNode: Element ) => unknown ) | undefined => {
switch ( sourceConfig.source ) {
case 'attribute': {
let matcher = attr(
const matcher = attr(
sourceConfig.selector,
sourceConfig.attribute
sourceConfig.attribute!
);
if ( sourceConfig.type === 'boolean' ) {
matcher = toBooleanAttributeMatcher( matcher );
return toBooleanAttributeMatcher( matcher );
}
return matcher;
}
Expand All @@ -228,19 +228,24 @@ export const matcherFromSource = memoize(
case 'node':
return node( sourceConfig.selector );
case 'query':
/*
* Sub-matchers may be undefined for unknown source types. hpq
* tolerates this and matches such keys as undefined, but its
* types don't allow for it.
*/
const subMatchers = Object.fromEntries(
Object.entries( sourceConfig.query! ).map(
( [ key, subSourceConfig ] ) => [
key,
matcherFromSource( subSourceConfig ),
]
)
);
return query( sourceConfig.selector, subMatchers );
) as MatcherObj;
return query( sourceConfig.selector!, subMatchers );
case 'tag': {
const matcher = prop( sourceConfig.selector, 'nodeName' );
return ( domNode: Node ) =>
( matcher( domNode ) as string )?.toLowerCase();
return ( domNode: Element ) =>
matcher( domNode )?.toLowerCase();
}
default:
// eslint-disable-next-line no-console
Expand All @@ -259,8 +264,8 @@ export const matcherFromSource = memoize(
*
* @return Parsed DOM node.
*/
function parseHtml( innerHTML: string | Node ): Node {
return hpqParse( innerHTML, ( h: Node ) => h );
function parseHtml( innerHTML: string | Node ): Element {
return hpqParse( innerHTML as string | Element, ( h: Element ) => h );
}

/**
Expand Down
1 change: 1 addition & 0 deletions packages/components/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@

- `GradientPicker`: Add `selectedSlug` prop for slug-based selection and pass the selected preset's slug to `onChange`, so two presets sharing a gradient keep their identity ([#80554](https://github.com/WordPress/gutenberg/pull/80554)).
- `SandBox`: Add `allowPopups` prop to opt into `allow-popups` in the iframe's sandbox attribute ([#69617](https://github.com/WordPress/gutenberg/pull/69617)).
- `SandBox`: Add `allowForms` prop to opt into `allow-forms` in the iframe's sandbox attribute ([#76471](https://github.com/WordPress/gutenberg/pull/76471)).
- Validated form controls: Only move focus to the invalid control for trusted `invalid` events (form submission, `reportValidity()`). Consumers can now dispatch a synthetic `invalid` event to reveal a control's error message without disturbing the user's place in the form ([#80685](https://github.com/WordPress/gutenberg/pull/80685)).

### Bug Fixes
Expand Down
4 changes: 4 additions & 0 deletions packages/components/src/sandbox/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -268,13 +268,15 @@ function IsolatedSandBox( {
onFocus,
tabIndex,
allowPopups = false,
allowForms = false,
}: SandBoxContentProps ) {
const ref = useRef< HTMLIFrameElement >( null );
const [ width, setWidth ] = useState( 0 );
const [ height, setHeight ] = useState( 0 );

const sandbox = clsx( 'allow-scripts', 'allow-presentation', {
'allow-popups': allowPopups,
'allow-forms': allowForms,
} );

const srcDoc = useMemo(
Expand Down Expand Up @@ -393,6 +395,7 @@ function SameOriginSandBox( {
onFocus,
tabIndex,
allowPopups = false,
allowForms = false,
}: SandBoxContentProps ) {
const ref = useRef< HTMLIFrameElement >( null );
const [ width, setWidth ] = useState( 0 );
Expand All @@ -404,6 +407,7 @@ function SameOriginSandBox( {
'allow-presentation',
{
'allow-popups': allowPopups,
'allow-forms': allowForms,
}
);

Expand Down
7 changes: 7 additions & 0 deletions packages/components/src/sandbox/stories/index.story.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,4 +36,11 @@ const Template: StoryFn< typeof SandBox > = ( args ) => <SandBox { ...args } />;
export const Default = Template.bind( {} );
Default.args = {
html: '<p>Arbitrary HTML content</p>',
allowForms: false,
};

export const WithForm = Template.bind( {} );
WithForm.args = {
html: '<form action="#"><label for="name">Name</label><input id="name" type="text" /><button type="submit">Submit</button></form>',
allowForms: true,
};
21 changes: 21 additions & 0 deletions packages/components/src/sandbox/test/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,27 @@ describe( 'SandBox', () => {
);
} );

it( 'should not include allow-forms by default', () => {
render( <SandBox html="<p>Hello</p>" title="No Forms" /> );

const iframe = screen.getByTitle< HTMLIFrameElement >( 'No Forms' );

expect( iframe.getAttribute( 'sandbox' ) ).not.toContain(
'allow-forms'
);
} );

it( 'should include allow-forms when allowForms is set', () => {
render( <SandBox html="<p>Hello</p>" title="Forms" allowForms /> );

const iframe = screen.getByTitle< HTMLIFrameElement >( 'Forms' );

expect( iframe ).toHaveAttribute(
'sandbox',
'allow-scripts allow-presentation allow-forms'
);
} );

it( 'should set srcdoc with the provided html content', () => {
render( <SandBox html="<p>Hello</p>" title="Test Title" /> );

Expand Down
10 changes: 10 additions & 0 deletions packages/components/src/sandbox/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,16 @@ export type SandBoxProps = {
* @default false
*/
allowPopups?: boolean;
/**
* Whether to include `allow-forms` in the iframe's sandbox attribute.
* When true, content inside the iframe is allowed to submit forms.
*
* Enable this for previews whose content includes forms that should be
* submittable.
*
* @default false
*/
allowForms?: boolean;
/**
* The HTML to render in the body of the iframe document.
*
Expand Down
2 changes: 2 additions & 0 deletions packages/dataviews/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,11 @@
- DataViews: Generalize the ordering filter operators (`on`, `notOn`, `before`, `after`, `beforeInc`, `afterInc`, `between`) from dates to temporal values, so they also compare times of day. Comparisons for `date` and `datetime` are unchanged. [#80830](https://github.com/WordPress/gutenberg/pull/80830)
- DataViews: Add Shift+Click range selection through a shared `useSelectionProps` hook that layouts can adopt, wired up in the table and grid layouts.[#80046](https://github.com/WordPress/gutenberg/pull/80046)
- DataViewsPicker: Add Shift+Click range selection to the `picker-table`, `picker-grid`, and `picker-activity` layouts. [#80413](https://github.com/WordPress/gutenberg/pull/80413)
- DataViews: Add an `aspectRatio` layout option to the `grid` and `table` layouts so consumers can configure the aspect ratio of item media previews from a set of preset ratios, instead of the hard-coded square. Defaults to `1/1`, so existing consumers are unaffected. [#79329](https://github.com/WordPress/gutenberg/pull/79329)

### Bug Fix

- DataViews: Pass only the eligible items to a bulk action's `callback`. A bulk action is offered when any one selected item is eligible for it, so the callback could run against items it had declared, through `isEligible`, that it could not handle. [#81198](https://github.com/WordPress/gutenberg/pull/81198)
- DataViews: Render the filter chip for an incomplete `between` range as if no value were set — matching how the filter itself does not apply — instead of showing a dangling bound or the literal string "undefined". A `null` bound, produced when an unfilled bound round-trips through JSON persistence, is now treated as unfilled too. [#80830](https://github.com/WordPress/gutenberg/pull/80830)
- DataForms: Stop the `card` and `details` layouts from hijacking focus when they reveal validation errors. Errors for every field in the container are now shown once focus leaves it, instead of on each internal blur, and revealing them no longer moves focus, so the natural tab sequence is preserved. [#80685](https://github.com/WordPress/gutenberg/pull/80685)
- DataForms: Complete the `richtext` control's autocomplete semantics by associating the textbox with its suggestions list for assistive technology. [#80403](https://github.com/WordPress/gutenberg/pull/80403)
Expand Down
3 changes: 3 additions & 0 deletions packages/dataviews/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -257,12 +257,14 @@ Properties:
| `styles` | ✓ | ✓ | | | | |
| `badgeFields` | | | ✓ | ✓ | | |
| `previewSize` | | | ✓ | ✓ | | |
| `aspectRatio` | ✓ | | ✓ | | | |
`table` and `pickerTable` layouts:
- `density`: one of `comfortable`, `balanced`, or `compact`. Configures the size and spacing of the layout.
- `enableMoving`: whether the table columns should display moving controls.
- `styles`: additional `width`, `maxWidth`, `minWidth`, `align` styles for each field column. The `align` property accepts `'start'`, `'center'`, or `'end'`.
- `aspectRatio` (`table` only): one of the preset ratios `'1/1'`, `'4/3'`, `'3/4'`, `'3/2'`, `'2/3'`, `'16/9'`, or `'9/16'`, applied to the primary column's media preview. Defaults to `'1/1'`.
**For column alignment (`align` property), follow these guidelines:**
Right-align (`'end'`) whenever the cell value is fundamentally quantitative—numbers, decimals, currency, percentages—so that digits and decimal points line up, aiding comparison and calculation. Otherwise, default to left-alignment (`'start'`) for all other types (text, codes, labels, dates).
Expand All @@ -272,6 +274,7 @@ Right-align (`'end'`) whenever the cell value is fundamentally quantitative—nu
- `badgeFields`: a list of field's `id` to render without label and styled as badges.
- `density`: one of `comfortable`, `balanced`, or `compact`. Configures the gap between items in the grid.
- `previewSize`: a `number` representing the size of the preview.
- `aspectRatio` (`grid` only): one of the preset ratios `'1/1'`, `'4/3'`, `'3/4'`, `'3/2'`, `'2/3'`, `'16/9'`, or `'9/16'`, applied uniformly to every item preview, keeping rows aligned. Defaults to `'1/1'`.
`list` layout:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -246,7 +246,7 @@ function ActionButton< Item >( {
action={ action }
onClick={ async () => {
setActionInProgress( action.id );
await action.callback( selectedItems, {
await action.callback( selectedEligibleItems, {
registry,
} );
setActionInProgress( null );
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,12 @@
* External dependencies
*/
import clsx from 'clsx';
import type { ComponentProps, ReactElement, HTMLAttributes } from 'react';
import type {
ComponentProps,
ReactElement,
HTMLAttributes,
CSSProperties,
} from 'react';

/**
* WordPress dependencies
Expand All @@ -27,6 +32,7 @@ import {
* Internal dependencies
*/
import { unlock } from '../../../lock-unlock';
import { MEDIA_ASPECT_RATIOS } from '../../../constants';
import ItemActions from '../../dataviews-item-actions';
import DataViewsSelectionCheckbox from '../../dataviews-selection-checkbox';
import DataViewsContext from '../../dataviews-context';
Expand Down Expand Up @@ -364,6 +370,19 @@ export default function CompositeGrid< Item >( {
const { paginationInfo, resizeObserverRef } =
useContext( DataViewsContext );
const gridColumns = useGridColumns();
// Consumer-configured aspect ratio for item previews, validated against
// the presets (like `density`) so arbitrary values are ignored, and
// surfaced to CSS as a custom property the media field's stylesheet
// reads. Always set (with the square default), so an identically-named
// variable set by a consumer on an ancestor can't leak into the previews
// when the view doesn't configure a ratio.
const gridStyle = {
'--wp-dataviews-media-aspect-ratio':
view.layout?.aspectRatio &&
MEDIA_ASPECT_RATIOS.includes( view.layout.aspectRatio )
? view.layout.aspectRatio
: '1/1',
} as CSSProperties;
const hasBulkActions = useSomeItemHasAPossibleBulkAction( actions, data );
const titleField = fields.find(
( field ) => field.id === view?.titleField
Expand Down Expand Up @@ -432,6 +451,7 @@ export default function CompositeGrid< Item >( {
}
) }
previewSize={ view.layout?.previewSize }
style={ gridStyle }
aria-busy={ isLoading }
ref={ resizeObserverRef }
/>
Expand Down Expand Up @@ -523,6 +543,7 @@ export default function CompositeGrid< Item >( {
! isInfiniteScroll && (
<Composite
role="grid"
style={ gridStyle }
className={ clsx( 'dataviews-view-grid', className, {
[ `has-${ view.layout?.density }-density` ]:
view.layout?.density &&
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@

.dataviews-view-grid__media {
width: 100%;
aspect-ratio: 1/1;
aspect-ratio: var(--wp-dataviews-media-aspect-ratio, 1/1);
background-color: var(--wpds-color-background-surface-neutral-strong);
border-radius: var(--wpds-border-radius-md);
overflow: hidden;
Expand Down
Loading
Loading