diff --git a/README.md b/README.md index 055dc31..89f930a 100644 --- a/README.md +++ b/README.md @@ -20,14 +20,22 @@ Either bucket can live on either track. Credits are optional metadata any module | Module | Bucket | Track | Adopted from | |---|---|---|---| -| Media Library: disable infinite scroll by default | Feature | Practical | WordPress core | -| Heading-order validation | Experiment | TBD | — | +| Media Library: add view options | Feature | Practical | WordPress core | | Block Validation Framework | Experiment | Core-track | validation-api (Troy Chaplin) | | Core block accessibility rules | Feature | Practical | validation-api-core-blocks (Troy Chaplin) | | Validation settings | Feature | Practical | validation-api-settings (Troy Chaplin) | ## Block validation subsystem +### Terminology + +Two words do a lot of work in this subsystem, so to be precise: **validation** names the mechanism, **accessibility** names the subject a particular rule set tests. + +- **Block Validation Framework** — the Experiment module that provides the mechanism. It is a named subsystem, so it takes title case; the other module labels are descriptive and use sentence case. +- **Validation API** — the developer-facing interface that framework exposes: the `validation_api_register_*()` functions, the `editor.validateBlock` / `editor.validateMeta` / `editor.validateEditor` JS filters, the `validation_api_check_level` filter, and `GET /wp-validation/v1/checks`. This is why the code prefix is `validation_api_`. +- **Validation check** — a single registered rule, whatever its subject. +- **Core block accessibility rules** — one consumer of the framework: WCAG-oriented checks for core blocks. Accessibility is what they test; validation is how they run. + Three of the first-party modules compose into a full block-editor validation subsystem: 1. **Block Validation Framework** — a plugin API (`validation_api_register_block_check()`, `validation_api_register_meta_check()`, `validation_api_register_editor_check()`), an `accessibility-lab/validation` data store, real-time debounced validation, publish locking, a Validation sidebar, and REST introspection at `GET /wp-validation/v1/checks`. diff --git a/includes/Modules/Experiments/BlockValidation/Global_Functions.php b/includes/Modules/Experiments/BlockValidation/Global_Functions.php index 1d5d532..0390ebd 100644 --- a/includes/Modules/Experiments/BlockValidation/Global_Functions.php +++ b/includes/Modules/Experiments/BlockValidation/Global_Functions.php @@ -1,6 +1,6 @@ register_editor_checks(); } + /** + * Registers image block accessibility checks. + */ private function register_image_checks(): void { validation_api_register_block_check( 'core/image', @@ -181,6 +223,9 @@ private function register_image_checks(): void { } } + /** + * Registers button block accessibility checks. + */ private function register_button_checks(): void { validation_api_register_block_check( 'core/button', @@ -208,6 +253,9 @@ private function register_button_checks(): void { ); } + /** + * Registers table block accessibility checks. + */ private function register_table_checks(): void { validation_api_register_block_check( 'core/table', @@ -223,6 +271,9 @@ private function register_table_checks(): void { ); } + /** + * Registers heading block accessibility checks. + */ private function register_heading_checks(): void { validation_api_register_block_check( 'core/heading', @@ -238,6 +289,9 @@ private function register_heading_checks(): void { ); } + /** + * Registers editor accessibility checks. + */ private function register_editor_checks(): void { foreach ( array( 'post', 'page' ) as $post_type ) { validation_api_register_editor_check( diff --git a/src/editor/core-block-rules/button.ts b/src/editor/core-block-rules/button.ts index 62120ae..3a28a80 100644 --- a/src/editor/core-block-rules/button.ts +++ b/src/editor/core-block-rules/button.ts @@ -12,6 +12,8 @@ function stripHtml( html: string ): string { * Minimal URL sanity check. A full Public Suffix List (PSL) integration is * planned as a follow-up — for now we accept any parseable http(s), mailto:, * tel:, or root-relative URL. + * + * @param raw URL as entered in the block's link control. */ function isPlausibleUrl( raw: string ): boolean { const value = raw.trim(); @@ -38,7 +40,12 @@ function isPlausibleUrl( raw: string ): boolean { addFilter( 'editor.validateBlock', 'accessibility-lab-core-blocks/button', - ( isValid: boolean, blockType: string, attributes: Record< string, unknown >, checkName: string ) => { + ( + isValid: boolean, + blockType: string, + attributes: Record< string, unknown >, + checkName: string + ) => { if ( blockType !== 'core/button' ) { return isValid; } diff --git a/src/editor/core-block-rules/gallery.ts b/src/editor/core-block-rules/gallery.ts index 6e2f800..27a0289 100644 --- a/src/editor/core-block-rules/gallery.ts +++ b/src/editor/core-block-rules/gallery.ts @@ -3,8 +3,7 @@ * by iterating the gallery's inner image blocks. */ -import { addFilter } from '@wordpress/hooks'; -import { applyFilters } from '@wordpress/hooks'; +import { addFilter, applyFilters } from '@wordpress/hooks'; type GalleryAttributes = { images?: Array< Record< string, unknown > >; @@ -20,7 +19,13 @@ const CHECK_MAP: Record< string, string > = { addFilter( 'editor.validateBlock', 'accessibility-lab-core-blocks/gallery', - ( isValid: boolean, blockType: string, attributes: GalleryAttributes, checkName: string, rule: unknown ) => { + ( + isValid: boolean, + blockType: string, + attributes: GalleryAttributes, + checkName: string, + rule: unknown + ) => { if ( blockType !== 'core/gallery' ) { return isValid; } diff --git a/src/editor/core-block-rules/heading.ts b/src/editor/core-block-rules/heading.ts index e5c58fd..cb8f7ab 100644 --- a/src/editor/core-block-rules/heading.ts +++ b/src/editor/core-block-rules/heading.ts @@ -35,14 +35,25 @@ function flattenBlocks( blocks: Block[] ): Block[] { addFilter( 'editor.validateBlock', 'accessibility-lab-core-blocks/heading', - ( isValid: boolean, blockType: string, attributes: { level?: number }, checkName: string, _rule: unknown, /* extra */ ) => { - if ( blockType !== 'core/heading' || checkName !== 'check_heading_rank' ) { + ( + isValid: boolean, + blockType: string, + attributes: { level?: number }, + checkName: string + ) => { + if ( + blockType !== 'core/heading' || + checkName !== 'check_heading_rank' + ) { return isValid; } - const level = Number( attributes.level ?? 2 ); // Locate previous heading in document order. const blocks = flattenBlocks( - ( select( blockEditorStore ) as unknown as { getBlocks: () => Block[] } ).getBlocks() + ( + select( blockEditorStore ) as unknown as { + getBlocks: () => Block[]; + } + ).getBlocks() ); const headings = blocks.filter( ( b ) => b.name === 'core/heading' ); // We don't have clientId in the filter args; approximate by finding the @@ -52,7 +63,10 @@ addFilter( if ( headings.length === 0 ) { return true; } - const firstHeadingLevel = Number( headings[ 0 ].attributes?.level ?? 2 ); + const level = Number( attributes.level ?? 2 ); + const firstHeadingLevel = Number( + headings[ 0 ].attributes?.level ?? 2 + ); if ( level === firstHeadingLevel ) { return firstHeadingLevel <= 2; } diff --git a/src/editor/core-block-rules/image.ts b/src/editor/core-block-rules/image.ts index bd401f2..5a4c424 100644 --- a/src/editor/core-block-rules/image.ts +++ b/src/editor/core-block-rules/image.ts @@ -15,21 +15,23 @@ function stripHtml( html: string ): string { addFilter( 'editor.validateBlock', 'accessibility-lab-core-blocks/image', - ( isValid: boolean, blockType: string, attributes: Record< string, unknown >, checkName: string ) => { + ( + isValid: boolean, + blockType: string, + attributes: Record< string, unknown >, + checkName: string + ) => { if ( blockType !== 'core/image' ) { return isValid; } const alt = String( attributes.alt ?? '' ).trim(); - const caption = stripHtml( String( attributes.caption ?? '' ) ); const isDecorative = Boolean( attributes.isDecorative ); const hasImage = Boolean( attributes.url || attributes.id ); switch ( checkName ) { case 'check_image_alt_text': - if ( ! hasImage || isDecorative ) { - return true; - } - return alt.length > 0; + // Passes with no image yet, a decorative image, or non-empty alt text. + return ! hasImage || isDecorative || alt.length > 0; case 'check_image_alt_text_length': if ( ! alt ) { @@ -37,11 +39,13 @@ addFilter( } return alt.length <= 125; - case 'check_image_alt_caption_match': + case 'check_image_alt_caption_match': { + const caption = stripHtml( String( attributes.caption ?? '' ) ); if ( ! alt || ! caption ) { return true; } return alt.toLowerCase() !== caption.toLowerCase(); + } case 'check_image_alt_text_patterns': if ( ! alt || isDecorative ) { diff --git a/src/editor/core-block-rules/post-title.ts b/src/editor/core-block-rules/post-title.ts index 7e4d280..3cf337a 100644 --- a/src/editor/core-block-rules/post-title.ts +++ b/src/editor/core-block-rules/post-title.ts @@ -10,13 +10,16 @@ addFilter( 'editor.validateEditor', 'accessibility-lab-core-blocks/post-title', ( isValid: boolean, checkName: string ) => { - if ( checkName !== 'post_title_required_post' && checkName !== 'post_title_required_page' ) { + if ( + checkName !== 'post_title_required_post' && + checkName !== 'post_title_required_page' + ) { return isValid; } - const currentPost = ( select( editorStore ) as unknown as { + const currentPost = select( editorStore ) as unknown as { getCurrentPost: () => { type?: string }; getEditedPostAttribute: ( name: string ) => unknown; - } ); + }; const postType = currentPost.getCurrentPost().type; if ( checkName === 'post_title_required_post' && postType !== 'post' ) { return true; @@ -24,7 +27,9 @@ addFilter( if ( checkName === 'post_title_required_page' && postType !== 'page' ) { return true; } - const title = String( currentPost.getEditedPostAttribute( 'title' ) ?? '' ).trim(); + const title = String( + currentPost.getEditedPostAttribute( 'title' ) ?? '' + ).trim(); return title.length > 0; } ); diff --git a/src/editor/core-block-rules/table.ts b/src/editor/core-block-rules/table.ts index 9801cc5..60478c8 100644 --- a/src/editor/core-block-rules/table.ts +++ b/src/editor/core-block-rules/table.ts @@ -15,7 +15,12 @@ type TableAttributes = { addFilter( 'editor.validateBlock', 'accessibility-lab-core-blocks/table', - ( isValid: boolean, blockType: string, attributes: TableAttributes, checkName: string ) => { + ( + isValid: boolean, + blockType: string, + attributes: TableAttributes, + checkName: string + ) => { if ( blockType !== 'core/table' ) { return isValid; } @@ -24,7 +29,8 @@ addFilter( return isValid; } - const hasHead = Array.isArray( attributes.head ) && attributes.head.length > 0; + const hasHead = + Array.isArray( attributes.head ) && attributes.head.length > 0; if ( hasHead ) { return true; }