Replace 2nd argument to of callbackOnCreateTemplates with an escaping function, and fix aria-label could be set to a bad value

This commit is contained in:
Xon 2024-07-31 07:40:54 +08:00
commit 1699018ad8
6 changed files with 30 additions and 17 deletions

View file

@ -21,7 +21,7 @@
* Add `removeItemButtonAlignLeft` option, to control if the remove item button is at the start or the end of the item.
* Add `removeChoice` method.
* Improve rendering performance by batching changes.
* Provide original templates via a new argument to the `callbackOnCreateTemplates` callback. #1149
* `escapeForTemplate` function is passed to the 2nd method of the `callbackOnCreateTemplates` callback.
* When `allowHtml` is false, default templates now render escaped html to `innerHtml` writing to `innerText`.
* This provides consistent rendering performance as `innerText` is quirky and slower than escaped html into `innerHtml`
@ -42,3 +42,4 @@
* Fix clearInput function did not clear the last search.
* Fix `addItemFilter` would allow empty strings as input to be added for items.
* Fix various issues with double escaping when displaying items/choices depending on allowHTML mode.
* Fix `aria-label` for placeholders was set to the string `null`

View file

@ -746,7 +746,7 @@
var singleNoSearch = new Choices('#choices-single-no-search', {
allowHTML: true,
searchEnabled: false,
removeItemButton: true,
removeItemButton: true,
choices: [
{ value: 'One', label: 'Label One' },
{ value: 'Two', label: 'Label Two', disabled: true },

View file

@ -51,7 +51,7 @@ import {
} from './lib/utils';
import { defaultState } from './reducers';
import Store from './store/store';
import templates from './templates';
import templates, { escapeForTemplate } from './templates';
import { mapInputToChoice } from './lib/choice-input';
import { ChoiceFull } from './interfaces/choice-full';
import { GroupFull } from './interfaces/group-full';
@ -2245,7 +2245,7 @@ class Choices implements Choices {
userTemplates = callbackOnCreateTemplates.call(
this,
strToEl,
defaultTemplates,
escapeForTemplate,
);
}

View file

@ -4,7 +4,6 @@ import { InputChoice } from './input-choice';
import { ClassNames } from './class-names';
import { PositionOptionsType } from './position-options-type';
import { Types } from './types';
import Templates from '../templates';
import { RecordToCompare } from '../lib/utils';
/**
@ -601,7 +600,10 @@ export interface Options {
* @default null
*/
callbackOnCreateTemplates:
| ((template: Types.StrToEl, defaultTemplates: typeof Templates) => void)
| ((
template: Types.StrToEl,
escapeForTemplate: Types.EscapeForTemplateFn,
) => void)
| null;
appendGroupInSearch: false;

View file

@ -1,7 +1,14 @@
import { StringUntrusted } from './string-untrusted';
import { StringPreEscaped } from './string-pre-escaped';
export namespace Types {
export type StrToEl = (
str: string,
) => HTMLElement | HTMLInputElement | HTMLOptionElement;
export type EscapeForTemplateFn = (
allowHTML: boolean,
s: StringUntrusted | StringPreEscaped | string,
) => string;
export type StringFunction = () => string;
export type NoticeStringFunction = (
value: string,

View file

@ -1,6 +1,7 @@
/**
* Helpers to create HTML elements used by Choices
* Can be overridden by providing `callbackOnCreateTemplates` option
* Can be overridden by providing `callbackOnCreateTemplates` option.
* `Choices.defaults.templates` allows access to the default template methods from `callbackOnCreateTemplates`
*/
import { ChoiceFull } from './interfaces/choice-full';
@ -22,7 +23,7 @@ type TemplateOptions = Record<
any
>;
const unwrapForTemplate = (
export const escapeForTemplate = (
allowHTML: boolean,
s: StringUntrusted | StringPreEscaped | string,
): string => (allowHTML ? unwrapStringForEscaped(s) : (sanitise(s) as string));
@ -94,7 +95,7 @@ const templates = {
): HTMLDivElement {
return Object.assign(document.createElement('div'), {
className: getClassNames(placeholder).join(' '),
innerHTML: unwrapForTemplate(allowHTML, value),
innerHTML: escapeForTemplate(allowHTML, value),
});
},
@ -130,12 +131,12 @@ const templates = {
if (typeof labelClass === 'string' || Array.isArray(labelClass)) {
const spanLabel = Object.assign(document.createElement('span'), {
innerHTML: unwrapForTemplate(allowHTML, label),
innerHTML: escapeForTemplate(allowHTML, label),
className: getClassNames(labelClass).join(' '),
});
div.appendChild(spanLabel);
} else {
div.innerHTML = unwrapForTemplate(allowHTML, label);
div.innerHTML = escapeForTemplate(allowHTML, label);
}
Object.assign(div.dataset, {
@ -245,7 +246,7 @@ const templates = {
div.appendChild(
Object.assign(document.createElement('div'), {
className: getClassNames(groupHeading).join(' '),
innerHTML: unwrapForTemplate(allowHTML, label),
innerHTML: escapeForTemplate(allowHTML, label),
}),
);
@ -290,19 +291,19 @@ const templates = {
if (typeof labelClass === 'string' || Array.isArray(labelClass)) {
const spanLabel = Object.assign(document.createElement('span'), {
innerHTML: unwrapForTemplate(allowHTML, label),
innerHTML: escapeForTemplate(allowHTML, label),
className: getClassNames(labelClass).join(' '),
});
spanLabel.setAttribute('aria-describedby', descId);
div.appendChild(spanLabel);
} else {
div.innerHTML = unwrapForTemplate(allowHTML, label);
div.innerHTML = escapeForTemplate(allowHTML, label);
div.setAttribute('aria-describedby', descId);
}
if (typeof labelDescription === 'string') {
const spanDesc = Object.assign(document.createElement('span'), {
innerHTML: unwrapForTemplate(allowHTML, labelDescription),
innerHTML: escapeForTemplate(allowHTML, labelDescription),
id: descId,
});
spanDesc.classList.add(...getClassNames(description));
@ -360,7 +361,9 @@ const templates = {
inp.setAttribute('role', 'textbox');
inp.setAttribute('aria-autocomplete', 'list');
inp.setAttribute('aria-label', placeholderValue);
if (placeholderValue) {
inp.setAttribute('aria-label', placeholderValue);
}
return inp;
},
@ -394,7 +397,7 @@ const templates = {
}
return Object.assign(document.createElement('div'), {
innerHTML: unwrapForTemplate(allowHTML, innerText),
innerHTML: escapeForTemplate(allowHTML, innerText),
className: classes.join(' '),
});
},