From 692615a055fbe0205fe9df1f20863a2f72342928 Mon Sep 17 00:00:00 2001 From: "Storm B. Heg" Date: Thu, 27 Jul 2023 13:37:48 +0200 Subject: [PATCH] Add support for options/attrs in Telepath widgets & add required/aria-describedby - This allows us to pass extra data for the widget to use in a backwards-compatible way. - FieldBlock: render 'required' and 'aria-describedby' attributes when appropriate - Ensure options passed to `render` override defaults - FieldBlock: add test proving options are constructed and passed down - Allow Telepath's widget rendering to take options - Include extra accessibility-related attributes in html output - Resolves missing required attribute on input elements for required fields - Resolves missing aria-describedby attribute on input element when the field has help text. - Partial work on #10300 --- CHANGELOG.txt | 2 + .../StreamField/blocks/FieldBlock.js | 21 +++++++++++ .../StreamField/blocks/FieldBlock.test.js | 37 +++++++++++++++++-- .../src/entrypoints/admin/telepath/widgets.js | 31 ++++++++++++++-- .../admin/telepath/widgets.test.js | 27 +++++++++++++- docs/releases/5.2.md | 3 +- 6 files changed, 112 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index b2babd0a2d09..084161aba94e 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -6,11 +6,13 @@ Changelog * Add preview-aware and page-aware fragment caching template tags, `wagtailcache` & `wagtailpagecache` (Jake Howard) * Always set help text element ID for form fields with help text in `field.html` template (Sage Abdullah) + * Fix: Ensure that StreamField's `FieldBlock`s correctly set the `required` and `aria-describedby` attributes (Storm Heg) * Maintenance: Fix snippet search test to work on non-fallback database backends (Matt Westcott) * Maintenance: Update Eslint, Prettier & Jest npm packages (LB (Ben) Johnston) * Maintenance: Add npm scripts for TypeScript checks and formatting SCSS files (LB (Ben) Johnston) * Maintenance: Run tests in parallel in some of the CI setup (Sage Abdullah) * Maintenance: Remove unused WorkflowStatus view, urlpattern, and workflow-status.js (Storm Heg) + * Maintenance: Add support for options/attrs in Telepath widgets so that attrs render on the created DOM (Storm Heg) 5.1 (01.08.2023) diff --git a/client/src/components/StreamField/blocks/FieldBlock.js b/client/src/components/StreamField/blocks/FieldBlock.js index 7262c22d9583..bddaa01aa0d4 100644 --- a/client/src/components/StreamField/blocks/FieldBlock.js +++ b/client/src/components/StreamField/blocks/FieldBlock.js @@ -39,6 +39,8 @@ export class FieldBlock { this.prefix = prefix; + const options = { attributes: this.getAttributes() }; + try { this.widget = this.blockDef.widget.render( widgetElement, @@ -46,6 +48,7 @@ export class FieldBlock { prefix, initialState, this.parentCapabilities, + options, ); } catch (e) { // eslint-disable-next-line no-console @@ -137,6 +140,24 @@ export class FieldBlock { } } + getAttributes() { + const prefix = this.prefix; + const attributes = {}; + + // If the block has help text, we should associate this with the input rendered by the widget. + // To accomplish this, we must tell the widget to render an aria-describedby attribute referring + // to the help text id in its HTML. + if (this.blockDef.meta.helpText) { + attributes['aria-describedby'] = `${prefix}-helptext`; + } + // If the block is required, we must tell the widget to render a required attribute in its HTML. + if (this.blockDef.meta.required) { + attributes.required = ''; + } + + return attributes; + } + getState() { return this.widget.getState(); } diff --git a/client/src/components/StreamField/blocks/FieldBlock.test.js b/client/src/components/StreamField/blocks/FieldBlock.test.js index adb3fb341a8d..a2db80f62c7f 100644 --- a/client/src/components/StreamField/blocks/FieldBlock.test.js +++ b/client/src/components/StreamField/blocks/FieldBlock.test.js @@ -8,7 +8,14 @@ window.comments = { }; // Define some callbacks in global scope that can be mocked in tests -let constructor = (_widgetName, _name, _id, _initialState) => {}; +let constructor = ( + _widgetName, + _name, + _id, + _initialState, + _parentCapabilities, + _options, +) => {}; let setState = (_widgetName, _state) => {}; let getState = (_widgetName) => {}; let getValue = (_widgetName) => {}; @@ -20,13 +27,19 @@ class DummyWidgetDefinition { this.throwErrorOnRender = throwErrorOnRender; } - render(placeholder, name, id, initialState) { + render(placeholder, name, id, initialState, parentCapabilities, options) { if (this.throwErrorOnRender) { throw new Error('Mock rendering error'); } const widgetName = this.widgetName; - constructor(widgetName, { name, id, initialState }); + constructor(widgetName, { + name, + id, + initialState, + parentCapabilities, + options, + }); $(placeholder).replaceWith( `

${widgetName}

`, @@ -100,6 +113,24 @@ describe('telepath: wagtail.blocks.FieldBlock', () => { name: 'the-prefix', id: 'the-prefix', initialState: 'Test initial state', + options: { + // Options should have been passed to the block definition + attributes: { + 'aria-describedby': 'the-prefix-helptext', + 'required': '', + }, + }, + parentCapabilities: new Map(), + }); + }); + + test('getAttributes() returns aria-describedby and required attributes', () => { + const attributes = boundBlock.getAttributes(); + expect(attributes).toEqual({ + // Added because FieldBlockDefinition has a helpText in its meta options + 'aria-describedby': 'the-prefix-helptext', + // Added because FieldBlockDefinition has required set in its meta options + 'required': '', }); }); diff --git a/client/src/entrypoints/admin/telepath/widgets.js b/client/src/entrypoints/admin/telepath/widgets.js index 4dfe7fa504ea..33e442cfbd47 100644 --- a/client/src/entrypoints/admin/telepath/widgets.js +++ b/client/src/entrypoints/admin/telepath/widgets.js @@ -2,12 +2,20 @@ import { gettext } from '../../../utils/gettext'; class BoundWidget { - constructor(element, name, idForLabel, initialState, parentCapabilities) { + constructor( + element, + name, + idForLabel, + initialState, + parentCapabilities, + options, + ) { var selector = ':input[name="' + name + '"]'; this.input = element.find(selector).addBack(selector); // find, including element itself this.idForLabel = idForLabel; this.setState(initialState); this.parentCapabilities = parentCapabilities || new Map(); + this.options = options; } getValue() { @@ -49,10 +57,24 @@ class Widget { boundWidgetClass = BoundWidget; - render(placeholder, name, id, initialState, parentCapabilities) { + render( + placeholder, + name, + id, + initialState, + parentCapabilities, + options = {}, + ) { var html = this.html.replace(/__NAME__/g, name).replace(/__ID__/g, id); var idForLabel = this.idPattern.replace(/__ID__/g, id); var dom = $(html); + + // Add any extra attributes we received to the HTML of the widget + if (typeof options?.attributes === 'object') { + Object.entries(options.attributes).forEach(([key, value]) => { + dom.attr(key, value); + }); + } $(placeholder).replaceWith(dom); // eslint-disable-next-line new-cap return new this.boundWidgetClass( @@ -61,6 +83,7 @@ class Widget { idForLabel, initialState, parentCapabilities, + options, ); } } @@ -349,7 +372,7 @@ class DraftailRichTextArea { this.options = options; } - render(container, name, id, initialState, parentCapabilities) { + render(container, name, id, initialState, parentCapabilities, options = {}) { const input = document.createElement('input'); input.type = 'hidden'; input.id = id; @@ -363,7 +386,7 @@ class DraftailRichTextArea { const boundDraftail = new BoundDraftailWidget( input, - this.options, + { ...this.options, ...options }, parentCapabilities, ); diff --git a/client/src/entrypoints/admin/telepath/widgets.test.js b/client/src/entrypoints/admin/telepath/widgets.test.js index 6bff475eed20..065831a64854 100644 --- a/client/src/entrypoints/admin/telepath/widgets.test.js +++ b/client/src/entrypoints/admin/telepath/widgets.test.js @@ -16,12 +16,13 @@ window.comments = { describe('telepath: wagtail.widgets.Widget', () => { let boundWidget; + let widgetDef; beforeEach(() => { // Create a placeholder to render the widget document.body.innerHTML = '
'; - const widgetDef = window.telepath.unpack({ + widgetDef = window.telepath.unpack({ _type: 'wagtail.widgets.Widget', _args: [ '', @@ -60,6 +61,30 @@ describe('telepath: wagtail.widgets.Widget', () => { boundWidget.focus(); expect(document.activeElement).toBe(document.querySelector('input')); }); + + test('it should support options with attributes', () => { + document.body.innerHTML = '
'; + boundWidget = widgetDef.render( + document.getElementById('placeholder'), + 'the-name', + 'the-id', + 'The Value', + {}, + { + attributes: { + 'maxLength': 512, + 'aria-describedby': 'some-id', + 'required': '', + }, + }, + ); + + const input = document.querySelector('input'); + + expect(input.maxLength).toBe(512); + expect(input.getAttribute('aria-describedby')).toBe('some-id'); + expect(input.required).toBe(true); + }); }); describe('telepath: wagtail.widgets.RadioSelect', () => { diff --git a/docs/releases/5.2.md b/docs/releases/5.2.md index b8e3a3a5467c..c8761bdcac00 100644 --- a/docs/releases/5.2.md +++ b/docs/releases/5.2.md @@ -19,7 +19,7 @@ depth: 1 ### Bug fixes - * ... + * Ensure that StreamField's `FieldBlock`s correctly set the `required` and `aria-describedby` attributes (Storm Heg) ### Documentation @@ -32,6 +32,7 @@ depth: 1 * Add npm scripts for TypeScript checks and formatting SCSS files (LB (Ben) Johnston) * Run tests in parallel in some of the CI setup (Sage Abdullah) * Remove unused WorkflowStatus view, urlpattern, and workflow-status.js (Storm Heg) + * Add support for options/attrs in Telepath widgets so that attrs render on the created DOM (Storm Heg) ## Upgrade considerations - changes affecting all projects