kopia lustrzana https://github.com/wagtail/wagtail
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 #10300pull/6406/head
rodzic
ebbd5d0767
commit
2c43ddbbd3
|
@ -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)
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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(
|
||||
`<p name="${name}" id="${id}">${widgetName}</p>`,
|
||||
|
@ -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': '',
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -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,
|
||||
);
|
||||
|
||||
|
|
|
@ -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__">',
|
||||
|
@ -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', () => {
|
||||
|
|
|
@ -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
|
||||
|
|
Ładowanie…
Reference in New Issue