diff --git a/CHANGELOG.md b/CHANGELOG.md index e63d30e5..02d466f1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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` diff --git a/public/index.html b/public/index.html index 9ebf19fe..0fe8b469 100644 --- a/public/index.html +++ b/public/index.html @@ -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 }, diff --git a/src/scripts/choices.ts b/src/scripts/choices.ts index b986b148..12e1d3a2 100644 --- a/src/scripts/choices.ts +++ b/src/scripts/choices.ts @@ -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, ); } diff --git a/src/scripts/interfaces/options.ts b/src/scripts/interfaces/options.ts index 8bc309da..5b761d61 100644 --- a/src/scripts/interfaces/options.ts +++ b/src/scripts/interfaces/options.ts @@ -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; diff --git a/src/scripts/interfaces/types.ts b/src/scripts/interfaces/types.ts index dd1e2429..8097798a 100644 --- a/src/scripts/interfaces/types.ts +++ b/src/scripts/interfaces/types.ts @@ -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, diff --git a/src/scripts/templates.ts b/src/scripts/templates.ts index 4c4a96ed..070aed64 100644 --- a/src/scripts/templates.ts +++ b/src/scripts/templates.ts @@ -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(' '), }); },