Skip to content

Commit

Permalink
Add support for options/attrs in Telepath widgets & add required/aria…
Browse files Browse the repository at this point in the history
…-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 wagtail#10300
  • Loading branch information
Stormheg authored and lb- committed Aug 4, 2023
1 parent ebbd5d0 commit 2c43ddb
Show file tree
Hide file tree
Showing 6 changed files with 112 additions and 9 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
21 changes: 21 additions & 0 deletions client/src/components/StreamField/blocks/FieldBlock.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,13 +39,16 @@ export class FieldBlock {

this.prefix = prefix;

const options = { attributes: this.getAttributes() };

try {
this.widget = this.blockDef.widget.render(
widgetElement,
prefix,
prefix,
initialState,
this.parentCapabilities,
options,
);
} catch (e) {
// eslint-disable-next-line no-console
Expand Down Expand Up @@ -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();
}
Expand Down
37 changes: 34 additions & 3 deletions client/src/components/StreamField/blocks/FieldBlock.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {};
Expand All @@ -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(
`<p name="${name}" id="${id}">${widgetName}</p>`,
Expand Down Expand Up @@ -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': '',
});
});

Expand Down
31 changes: 27 additions & 4 deletions client/src/entrypoints/admin/telepath/widgets.js
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down Expand Up @@ -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(
Expand All @@ -61,6 +83,7 @@ class Widget {
idForLabel,
initialState,
parentCapabilities,
options,
);
}
}
Expand Down Expand Up @@ -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;
Expand All @@ -363,7 +386,7 @@ class DraftailRichTextArea {

const boundDraftail = new BoundDraftailWidget(
input,
this.options,
{ ...this.options, ...options },
parentCapabilities,
);

Expand Down
27 changes: 26 additions & 1 deletion client/src/entrypoints/admin/telepath/widgets.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = '<div id="placeholder"></div>';

const widgetDef = window.telepath.unpack({
widgetDef = window.telepath.unpack({
_type: 'wagtail.widgets.Widget',
_args: [
'<input type="text" name="__NAME__" maxlength="255" id="__ID__">',
Expand Down Expand Up @@ -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 = '<div id="placeholder"></div>';
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', () => {
Expand Down
3 changes: 2 additions & 1 deletion docs/releases/5.2.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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
Expand Down

0 comments on commit 2c43ddb

Please sign in to comment.