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
35 changes: 35 additions & 0 deletions resources/js/visual-editor/a11y.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/**
* Shared accessibility helpers for the visual-editor package.
*
* - `prefers-reduced-motion` respects WCAG 2.3.3 / 2.2.2 by stripping
* animations and long transitions for users who request reduced
* motion at the OS level.
* - `.screen-reader-text` is the WordPress-standard utility for
* visually-hiding content while keeping it discoverable to assistive
* tech. Some packages also expose this class via component CSS; this
* file guarantees it is always available when either editor mounts.
*/

@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
scroll-behavior: auto !important;
}
}

.screen-reader-text {
position: absolute;
width: 1px;
height: 1px;
margin: -1px;
padding: 0;
overflow: hidden;
-webkit-clip-path: inset(50%);
clip-path: inset(50%);
white-space: nowrap;
border: 0;
}
7 changes: 4 additions & 3 deletions resources/js/visual-editor/editor/__tests__/top-bar.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,13 @@ afterEach(() => {
});

describe('TopBar', () => {
it('renders an accessible toolbar landmark', () => {
it('renders the top bar as a banner header', () => {
render(<TopBar {...defaultProps()} />);

const toolbar = screen.getByRole('toolbar', { name: 'Editor top bar' });
const header = screen.getByTestId('ap-visual-editor-top-bar');

expect(toolbar).toBeInTheDocument();
expect(header).toBeInTheDocument();
expect(header.tagName).toBe('HEADER');
});

it('does not render title, slug, or status inputs (moved to canvas/sidebar)', () => {
Expand Down
9 changes: 5 additions & 4 deletions resources/js/visual-editor/editor/block-library-sidebar.css
Original file line number Diff line number Diff line change
Expand Up @@ -200,8 +200,9 @@
* longer rendered (the wrapper moved inside `TreeGridItem` divs in a
* recent refactor), so the two buttons stack with the table cell's
* natural vertical-align spacing, leaving a big gap between ^ and v.
* These overrides shrink the buttons to 16px tall and zero their
* surrounding margins so the arrows sit snugly one atop the other.
* These overrides zero the surrounding margins so the arrows sit snugly
* one atop the other, while maintaining the 24x24px minimum required by
* WCAG 2.5.5 Level AA (Target Size).
*/
.ap-visual-editor-block-library__panel--layouts
.block-editor-list-view-block__mover-cell {
Expand All @@ -218,9 +219,9 @@
.ap-visual-editor-block-library__panel--layouts
.block-editor-list-view-leaf
.block-editor-block-mover-button {
height: 16px;
min-height: 16px;
min-height: 24px;
width: 28px;
min-width: 24px;
margin: 0;
padding: 0;
}
Expand Down
20 changes: 18 additions & 2 deletions resources/js/visual-editor/editor/editor-app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import {
useCallback,
useEffect,
useId,
useMemo,
useRef,
useState,
Expand All @@ -26,7 +27,7 @@ import { EntityProvider } from '@wordpress/core-data';
// blocks. Without this import the toolbar renders but the formats are
// empty (#343).
import '@wordpress/format-library';
import { __ } from '@wordpress/i18n';
import { __, sprintf } from '@wordpress/i18n';
import type { BlockInstance } from '@wordpress/blocks';

import { addFilter } from '@wordpress/hooks';
Expand Down Expand Up @@ -416,6 +417,7 @@ function EditorAppShell(props: EditorAppProps): JSX.Element {
saveErrorMessage: saveError?.message ?? null,
});

const mainHeadingId = useId();
const [title, setTitle] = useState<string>(initialTitle);
const [slug, setSlug] = useState<string>(initialSlug);
const [status, setStatus] = useState<PostStatus>(
Expand Down Expand Up @@ -925,6 +927,12 @@ function EditorAppShell(props: EditorAppProps): JSX.Element {
editorBody
);

const mainHeadingText = sprintf(
/* translators: %s: post title or fallback. */
__('Editing: %s', TEXT_DOMAIN),
title === '' ? __('Untitled', TEXT_DOMAIN) : title
);

return (
<SlotFillProvider>
<div
Expand All @@ -934,7 +942,15 @@ function EditorAppShell(props: EditorAppProps): JSX.Element {
data-document-type={documentType ?? 'unset'}
>
{topBar}
<div className="ap-visual-editor__body">{wrappedBody}</div>
<main
className="ap-visual-editor__body"
aria-labelledby={mainHeadingId}
>
<h1 id={mainHeadingId} className="screen-reader-text">
{mainHeadingText}
</h1>
{wrappedBody}
</main>
{shortcutsModal}
</div>
</SlotFillProvider>
Expand Down
2 changes: 2 additions & 0 deletions resources/js/visual-editor/editor/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
import { createElement } from 'react';
import { createRoot, type Root } from 'react-dom/client';

import '../a11y.css';

import type {
AuthorOption,
DocumentSupports,
Expand Down
2 changes: 0 additions & 2 deletions resources/js/visual-editor/editor/top-bar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -366,8 +366,6 @@ export function TopBar(props: TopBarProps): JSX.Element {
return (
<header
className="ap-visual-editor-top-bar"
role="toolbar"
aria-label={__('Editor top bar', TEXT_DOMAIN)}
data-testid="ap-visual-editor-top-bar"
>
<div className="ap-visual-editor-top-bar__group ap-visual-editor-top-bar__group--start">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,7 @@ export function CreateEntityDialog<K extends EntityKind>(
onClick={onClose}
data-testid={`ap-site-editor-create-dialog-close-${kind}`}
>
{'×'}
<span aria-hidden="true">{'×'}</span>
</button>
</header>
<form
Expand Down
2 changes: 2 additions & 0 deletions resources/js/visual-editor/site-editor/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
import { createElement } from 'react';
import { createRoot, type Root } from 'react-dom/client';

import '../a11y.css';

const MOUNT_SELECTOR = '[data-ap-site-editor]';
const ROOT_SYMBOL: unique symbol = Symbol('ap-site-editor-root');
const READY_SYMBOL: unique symbol = Symbol('ap-site-editor-ready');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@ export function CreateMenuDialog(props: CreateMenuDialogProps): JSX.Element {
const slugId = useId();
const titleId = useId();
const locationId = useId();
const titleErrorId = useId();
const slugErrorId = useId();
const locationErrorId = useId();

const [slug, setSlug] = useState('');
const [title, setTitle] = useState('');
Expand Down Expand Up @@ -191,9 +194,18 @@ export function CreateMenuDialog(props: CreateMenuDialogProps): JSX.Element {
setTitle(event.target.value)
}
data-testid="ap-create-menu-title"
aria-invalid={
Boolean(errors.title) || undefined
}
aria-describedby={
errors.title !== undefined
? titleErrorId
: undefined
}
/>
{errors.title !== undefined ? (
<p
id={titleErrorId}
className="ap-site-editor__dialog-field-error"
role="alert"
>
Expand Down Expand Up @@ -221,9 +233,18 @@ export function CreateMenuDialog(props: CreateMenuDialogProps): JSX.Element {
}
placeholder={__('e.g. primary', TEXT_DOMAIN)}
data-testid="ap-create-menu-slug"
aria-invalid={
Boolean(errors.slug) || undefined
}
aria-describedby={
errors.slug !== undefined
? slugErrorId
: undefined
}
/>
{errors.slug !== undefined ? (
<p
id={slugErrorId}
className="ap-site-editor__dialog-field-error"
role="alert"
>
Expand All @@ -247,6 +268,14 @@ export function CreateMenuDialog(props: CreateMenuDialogProps): JSX.Element {
setLocation(event.target.value)
}
data-testid="ap-create-menu-location"
aria-invalid={
Boolean(errors.location) || undefined
}
aria-describedby={
errors.location !== undefined
? locationErrorId
: undefined
}
>
<option value="">
{__('— No location —', TEXT_DOMAIN)}
Expand All @@ -262,6 +291,7 @@ export function CreateMenuDialog(props: CreateMenuDialogProps): JSX.Element {
</select>
{errors.location !== undefined ? (
<p
id={locationErrorId}
className="ap-site-editor__dialog-field-error"
role="alert"
>
Expand Down
13 changes: 10 additions & 3 deletions resources/js/visual-editor/site-editor/navigation/link-picker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ export function LinkPicker(props: LinkPickerProps): JSX.Element {
const typeId = useId();
const queryId = useId();
const urlId = useId();
const searchErrorId = useId();

const typeOptions = useMemo(() => getTypeOptions(), []);
const activeType = typeOptions.find((option) => option.value === item.type);
Expand Down Expand Up @@ -187,11 +188,20 @@ export function LinkPicker(props: LinkPickerProps): JSX.Element {
onChange={(event) => setQuery(event.target.value)}
placeholder={__('Type to search…', TEXT_DOMAIN)}
data-testid="ap-nav-link-picker-query"
aria-invalid={
Boolean(searchError) || undefined
}
aria-describedby={
searchError !== null
? searchErrorId
: undefined
}
/>
</div>

{searchError !== null ? (
<p
id={searchErrorId}
className="ap-site-editor__dialog-field-error"
role="alert"
>
Expand All @@ -201,7 +211,6 @@ export function LinkPicker(props: LinkPickerProps): JSX.Element {

<ul
className="ap-nav-inspector__results"
role="listbox"
aria-label={__('Search results', TEXT_DOMAIN)}
aria-busy={isSearching}
data-testid="ap-nav-link-picker-results"
Expand All @@ -220,8 +229,6 @@ export function LinkPicker(props: LinkPickerProps): JSX.Element {
<li
key={`${result.type}-${result.id}`}
className="ap-nav-inspector__result"
role="option"
aria-selected={selected}
>
<button
type="button"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,8 @@
font-size: 14px;
padding: 4px;
border-radius: 2px;
min-width: 24px;
min-height: 24px;
}

.ap-nav-canvas__drag-handle:active {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -253,7 +253,6 @@ export function NavigationCanvas(props: NavigationCanvasProps): JSX.Element {
>
<ul
className="ap-nav-canvas__tree"
role="tree"
aria-label={__('Menu items', TEXT_DOMAIN)}
>
{visibleFlat.map((entry) => (
Expand Down Expand Up @@ -333,9 +332,6 @@ function SortableMenuItemRow(props: SortableMenuItemRowProps): JSX.Element {
ref={sortable.setNodeRef}
style={style}
className="ap-nav-canvas__item"
role="treeitem"
aria-level={renderDepth + 1}
aria-selected={selectedItemId === flat.item.localId}
data-testid={`ap-nav-canvas-item-${flat.item.localId}`}
data-dragging={sortable.isDragging}
>
Expand Down Expand Up @@ -433,7 +429,7 @@ function MenuItemRowBody(props: RowBodyProps): JSX.Element {
data-testid={`ap-nav-canvas-drag-${item.localId}`}
{...dragHandleProps}
>
{'⋮⋮'}
<span aria-hidden="true">{'⋮⋮'}</span>
</button>
) : null}
<button
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,10 @@ function EntityPanel(props: EntityPanelProps): JSX.Element {
const titleId = useId();
const slugId = useId();
const locationId = useId();
const titleErrorId = useId();
const slugErrorId = useId();
const locationErrorId = useId();
const locationsErrorId = useId();

const titleError = validationErrors?.title?.[0];
const slugError = validationErrors?.slug?.[0];
Expand All @@ -189,9 +193,14 @@ function EntityPanel(props: EntityPanelProps): JSX.Element {
onFieldsChange({ title: event.target.value })
}
data-testid="ap-nav-inspector-title"
aria-invalid={Boolean(titleError) || undefined}
aria-describedby={
titleError !== undefined ? titleErrorId : undefined
}
/>
{titleError !== undefined ? (
<p
id={titleErrorId}
className="ap-site-editor__dialog-field-error"
role="alert"
>
Expand All @@ -216,9 +225,14 @@ function EntityPanel(props: EntityPanelProps): JSX.Element {
onFieldsChange({ slug: event.target.value })
}
data-testid="ap-nav-inspector-slug"
aria-invalid={Boolean(slugError) || undefined}
aria-describedby={
slugError !== undefined ? slugErrorId : undefined
}
/>
{slugError !== undefined ? (
<p
id={slugErrorId}
className="ap-site-editor__dialog-field-error"
role="alert"
>
Expand Down Expand Up @@ -248,6 +262,17 @@ function EntityPanel(props: EntityPanelProps): JSX.Element {
}
disabled={isLocationsLoading}
data-testid="ap-nav-inspector-location"
aria-invalid={
Boolean(locationError) || Boolean(locationsError) ||
undefined
}
aria-describedby={
locationError !== undefined
? locationErrorId
: locationsError !== null
? locationsErrorId
: undefined
}
>
<option value="">
{__('— No location —', TEXT_DOMAIN)}
Expand All @@ -263,6 +288,7 @@ function EntityPanel(props: EntityPanelProps): JSX.Element {
</select>
{locationsError !== null ? (
<p
id={locationsErrorId}
className="ap-site-editor__dialog-field-error"
role="alert"
>
Expand All @@ -271,6 +297,7 @@ function EntityPanel(props: EntityPanelProps): JSX.Element {
) : null}
{locationError !== undefined ? (
<p
id={locationErrorId}
className="ap-site-editor__dialog-field-error"
role="alert"
>
Expand Down
Loading
Loading