diff --git a/CHANGELOG.md b/CHANGELOG.md index 3a1a1594..a52e9dd7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,8 @@ * Fix the rendered item list was not cleared when `clearStore` was called. This impacted the on-form-reset and `refresh` features. ### Chore -* Add e2e test for 'form reset' and 'on paste & search' +* Add e2e test for 'form reset' and 'on paste & search'. +* Cleanup adding classes to generated elements. ## [11.0.0] (2024-08-28) diff --git a/src/scripts/choices.ts b/src/scripts/choices.ts index a259b393..07cfbe83 100644 --- a/src/scripts/choices.ts +++ b/src/scripts/choices.ts @@ -9,13 +9,14 @@ import { InputGroup } from './interfaces/input-group'; import { Options, ObjectsInConfig } from './interfaces/options'; import { StateChangeSet } from './interfaces/state'; import { + addClassesToElement, diff, escapeForTemplate, generateId, getAdjacentEl, - getClassNames, getClassNamesSelector, isScrolledIntoView, + removeClassesFromElement, resolveNoticeFunction, resolveStringFunction, sanitise, @@ -1995,14 +1996,14 @@ class Choices { } let passedEl = el; - const highlightedState = getClassNames(this.config.classNames.highlightedState); + const { highlightedState } = this.config.classNames; const highlightedChoices = Array.from( this.dropdown.element.querySelectorAll(getClassNamesSelector(highlightedState)), ); // Remove any highlighted choices highlightedChoices.forEach((choice) => { - choice.classList.remove(...highlightedState); + removeClassesFromElement(choice, highlightedState); choice.setAttribute('aria-selected', 'false'); }); @@ -2023,7 +2024,7 @@ class Choices { } } - passedEl.classList.add(...highlightedState); + addClassesToElement(passedEl, highlightedState); passedEl.setAttribute('aria-selected', 'true'); this.passedElement.triggerEvent(EventType.highlightChoice, { el: passedEl, diff --git a/src/scripts/components/container.ts b/src/scripts/components/container.ts index fb72b852..e8ca4246 100644 --- a/src/scripts/components/container.ts +++ b/src/scripts/components/container.ts @@ -1,4 +1,4 @@ -import { getClassNames } from '../lib/utils'; +import { addClassesToElement, removeClassesFromElement } from '../lib/utils'; import { ClassNames } from '../interfaces/class-names'; import { PositionOptionsType } from '../interfaces/position-options-type'; import { PassedElementType, PassedElementTypes } from '../interfaces/passed-element-type'; @@ -69,39 +69,39 @@ export default class Container { } open(dropdownPos: number, dropdownHeight: number): void { - this.element.classList.add(...getClassNames(this.classNames.openState)); + addClassesToElement(this.element, this.classNames.openState); this.element.setAttribute('aria-expanded', 'true'); this.isOpen = true; if (this.shouldFlip(dropdownPos, dropdownHeight)) { - this.element.classList.add(...getClassNames(this.classNames.flippedState)); + addClassesToElement(this.element, this.classNames.flippedState); this.isFlipped = true; } } close(): void { - this.element.classList.remove(...getClassNames(this.classNames.openState)); + removeClassesFromElement(this.element, this.classNames.openState); this.element.setAttribute('aria-expanded', 'false'); this.removeActiveDescendant(); this.isOpen = false; // A dropdown flips if it does not have space within the page if (this.isFlipped) { - this.element.classList.remove(...getClassNames(this.classNames.flippedState)); + removeClassesFromElement(this.element, this.classNames.flippedState); this.isFlipped = false; } } addFocusState(): void { - this.element.classList.add(...getClassNames(this.classNames.focusState)); + addClassesToElement(this.element, this.classNames.focusState); } removeFocusState(): void { - this.element.classList.remove(...getClassNames(this.classNames.focusState)); + removeClassesFromElement(this.element, this.classNames.focusState); } enable(): void { - this.element.classList.remove(...getClassNames(this.classNames.disabledState)); + removeClassesFromElement(this.element, this.classNames.disabledState); this.element.removeAttribute('aria-disabled'); if (this.type === PassedElementTypes.SelectOne) { this.element.setAttribute('tabindex', '0'); @@ -110,7 +110,7 @@ export default class Container { } disable(): void { - this.element.classList.add(...getClassNames(this.classNames.disabledState)); + addClassesToElement(this.element, this.classNames.disabledState); this.element.setAttribute('aria-disabled', 'true'); if (this.type === PassedElementTypes.SelectOne) { this.element.setAttribute('tabindex', '-1'); @@ -144,13 +144,13 @@ export default class Container { } addLoadingState(): void { - this.element.classList.add(...getClassNames(this.classNames.loadingState)); + addClassesToElement(this.element, this.classNames.loadingState); this.element.setAttribute('aria-busy', 'true'); this.isLoading = true; } removeLoadingState(): void { - this.element.classList.remove(...getClassNames(this.classNames.loadingState)); + removeClassesFromElement(this.element, this.classNames.loadingState); this.element.removeAttribute('aria-busy'); this.isLoading = false; } diff --git a/src/scripts/components/dropdown.ts b/src/scripts/components/dropdown.ts index b7519b33..ce8ac73f 100644 --- a/src/scripts/components/dropdown.ts +++ b/src/scripts/components/dropdown.ts @@ -1,6 +1,6 @@ import { ClassNames } from '../interfaces/class-names'; import { PassedElementType } from '../interfaces/passed-element-type'; -import { getClassNames } from '../lib/utils'; +import { addClassesToElement, removeClassesFromElement } from '../lib/utils'; export default class Dropdown { element: HTMLElement; @@ -30,7 +30,7 @@ export default class Dropdown { * Show dropdown to user by adding active state class */ show(): this { - this.element.classList.add(...getClassNames(this.classNames.activeState)); + addClassesToElement(this.element, this.classNames.activeState); this.element.setAttribute('aria-expanded', 'true'); this.isActive = true; @@ -41,7 +41,7 @@ export default class Dropdown { * Hide dropdown from user */ hide(): this { - this.element.classList.remove(...getClassNames(this.classNames.activeState)); + removeClassesFromElement(this.element, this.classNames.activeState); this.element.setAttribute('aria-expanded', 'false'); this.isActive = false; diff --git a/src/scripts/components/wrapped-element.ts b/src/scripts/components/wrapped-element.ts index 9d5c4293..f2f1837b 100644 --- a/src/scripts/components/wrapped-element.ts +++ b/src/scripts/components/wrapped-element.ts @@ -1,6 +1,6 @@ import { ClassNames } from '../interfaces/class-names'; import { EventTypes } from '../interfaces/event-type'; -import { dispatchEvent, getClassNames } from '../lib/utils'; +import { addClassesToElement, dispatchEvent, removeClassesFromElement } from '../lib/utils'; import { EventMap } from '../interfaces'; export default class WrappedElement { @@ -36,7 +36,7 @@ export default class WrappedElement | null): st return `.${option}`; }; +export const addClassesToElement = (element: HTMLElement, className: Array | string): void => { + element.classList.add(...getClassNames(className)); +}; + +export const removeClassesFromElement = (element: HTMLElement, className: Array | string): void => { + element.classList.remove(...getClassNames(className)); +}; + export const parseCustomProperties = (customProperties?: string): object | string => { if (typeof customProperties !== 'undefined') { try { @@ -217,7 +225,7 @@ export const parseCustomProperties = (customProperties?: string): object | strin export const updateClassList = (item: ChoiceFull, add: string | string[], remove: string | string[]): void => { const { itemEl } = item; if (itemEl) { - itemEl.classList.remove(...getClassNames(remove)); - itemEl.classList.add(...getClassNames(add)); + removeClassesFromElement(itemEl, remove); + addClassesToElement(itemEl, add); } }; diff --git a/src/scripts/templates.ts b/src/scripts/templates.ts index e3d5a14e..e5d82b40 100644 --- a/src/scripts/templates.ts +++ b/src/scripts/templates.ts @@ -14,6 +14,8 @@ import { resolveNoticeFunction, setElementHtml, escapeForTemplate, + addClassesToElement, + removeClassesFromElement, } from './lib/utils'; import { NoticeType, NoticeTypes, TemplateOptions, Templates as TemplatesInterface } from './interfaces/templates'; import { StringUntrusted } from './interfaces/string-untrusted'; @@ -69,7 +71,7 @@ const templates: TemplatesInterface = { labelId: string, ): HTMLDivElement { const div = document.createElement('div'); - div.className = getClassNames(containerOuter).join(' '); + addClassesToElement(div, containerOuter); div.dataset.type = passedElementType; @@ -102,7 +104,7 @@ const templates: TemplatesInterface = { containerInner({ classNames: { containerInner } }: TemplateOptions): HTMLDivElement { const div = document.createElement('div'); - div.className = getClassNames(containerInner).join(' '); + addClassesToElement(div, containerInner); return div; }, @@ -112,7 +114,8 @@ const templates: TemplatesInterface = { isSelectOneElement: boolean, ): HTMLDivElement { const div = document.createElement('div'); - div.className = `${getClassNames(list).join(' ')} ${isSelectOneElement ? getClassNames(listSingle).join(' ') : getClassNames(listItems).join(' ')}`; + addClassesToElement(div, list); + addClassesToElement(div, isSelectOneElement ? listSingle : listItems); if (this._isSelectElement && searchEnabled) { div.setAttribute('role', 'listbox'); @@ -126,7 +129,7 @@ const templates: TemplatesInterface = { value: StringPreEscaped | string, ): HTMLDivElement { const div = document.createElement('div'); - div.className = getClassNames(placeholder).join(' '); + addClassesToElement(div, placeholder); setElementHtml(div, allowHTML, value); return div; @@ -145,12 +148,12 @@ const templates: TemplatesInterface = { ): HTMLDivElement { const rawValue = unwrapStringForRaw(choice.value); const div = document.createElement('div'); - div.className = getClassNames(item).join(' '); + addClassesToElement(div, item); if (choice.labelClass) { const spanLabel = document.createElement('span'); setElementHtml(spanLabel, allowHTML, choice.label); - spanLabel.className = getClassNames(choice.labelClass).join(' '); + addClassesToElement(spanLabel, choice.labelClass); div.appendChild(spanLabel); } else { setElementHtml(div, allowHTML, choice.label); @@ -171,21 +174,21 @@ const templates: TemplatesInterface = { } if (choice.placeholder) { - div.classList.add(...getClassNames(placeholder)); + addClassesToElement(div, placeholder); div.dataset.placeholder = ''; } - div.classList.add(...(choice.highlighted ? getClassNames(highlightedState) : getClassNames(itemSelectable))); + addClassesToElement(div, choice.highlighted ? highlightedState : itemSelectable); if (removeItemButton) { if (choice.disabled) { - div.classList.remove(...getClassNames(itemSelectable)); + removeClassesFromElement(div, itemSelectable); } div.dataset.deletable = ''; const removeButton = document.createElement('button'); removeButton.type = 'button'; - removeButton.className = getClassNames(button).join(' '); + addClassesToElement(removeButton, button); setElementHtml(removeButton, true, resolveNoticeFunction(removeItemIconText, choice.value)); const REMOVE_ITEM_LABEL = resolveNoticeFunction(removeItemLabelText, choice.value); @@ -205,7 +208,7 @@ const templates: TemplatesInterface = { choiceList({ classNames: { list } }: TemplateOptions, isSelectOneElement: boolean): HTMLDivElement { const div = document.createElement('div'); - div.className = getClassNames(list).join(' '); + addClassesToElement(div, list); if (!isSelectOneElement) { div.setAttribute('aria-multiselectable', 'true'); @@ -221,7 +224,10 @@ const templates: TemplatesInterface = { ): HTMLDivElement { const rawLabel = unwrapStringForRaw(label); const div = document.createElement('div'); - div.className = `${getClassNames(group).join(' ')} ${disabled ? getClassNames(itemDisabled).join(' ') : ''}`; + addClassesToElement(div, group); + if (disabled) { + addClassesToElement(div, itemDisabled); + } div.setAttribute('role', 'group'); @@ -234,7 +240,7 @@ const templates: TemplatesInterface = { } const heading = document.createElement('div'); - heading.className = getClassNames(groupHeading).join(' '); + addClassesToElement(heading, groupHeading); setElementHtml(heading, allowHTML, label || ''); div.appendChild(heading); @@ -255,7 +261,8 @@ const templates: TemplatesInterface = { const rawValue = unwrapStringForRaw(choice.value); const div = document.createElement('div'); div.id = choice.elementId as string; - div.className = `${getClassNames(item).join(' ')} ${getClassNames(itemChoice).join(' ')}`; + addClassesToElement(div, item); + addClassesToElement(div, itemChoice); if (groupName && typeof label === 'string') { label = escapeForTemplate(allowHTML, label); @@ -268,7 +275,7 @@ const templates: TemplatesInterface = { if (choice.labelClass) { const spanLabel = document.createElement('span'); setElementHtml(spanLabel, allowHTML, label); - spanLabel.className = getClassNames(choice.labelClass).join(' '); + addClassesToElement(spanLabel, choice.labelClass); describedBy = spanLabel; div.appendChild(spanLabel); } else { @@ -281,16 +288,16 @@ const templates: TemplatesInterface = { const spanDesc = document.createElement('span'); setElementHtml(spanDesc, allowHTML, choice.labelDescription); spanDesc.id = descId; - spanDesc.classList.add(...getClassNames(description)); + addClassesToElement(spanDesc, description); div.appendChild(spanDesc); } if (choice.selected) { - div.classList.add(...getClassNames(selectedState)); + addClassesToElement(div, selectedState); } if (choice.placeholder) { - div.classList.add(...getClassNames(placeholder)); + addClassesToElement(div, placeholder); } div.setAttribute('role', choice.groupId ? 'treeitem' : 'option'); @@ -305,11 +312,11 @@ const templates: TemplatesInterface = { assignCustomProperties(div, choice, false); if (choice.disabled) { - div.classList.add(...getClassNames(itemDisabled)); + addClassesToElement(div, itemDisabled); div.dataset.choiceDisabled = ''; div.setAttribute('aria-disabled', 'true'); } else { - div.classList.add(...getClassNames(itemSelectable)); + addClassesToElement(div, itemSelectable); div.dataset.choiceSelectable = ''; } @@ -322,7 +329,8 @@ const templates: TemplatesInterface = { ): HTMLInputElement { const inp = document.createElement('input'); inp.type = 'search'; - inp.className = `${getClassNames(input).join(' ')} ${getClassNames(inputCloned).join(' ')}`; + addClassesToElement(inp, input); + addClassesToElement(inp, inputCloned); inp.autocomplete = 'off'; inp.autocapitalize = 'off'; inp.spellcheck = false; @@ -341,8 +349,8 @@ const templates: TemplatesInterface = { dropdown({ classNames: { list, listDropdown } }: TemplateOptions): HTMLDivElement { const div = document.createElement('div'); - div.classList.add(...getClassNames(list)); - div.classList.add(...getClassNames(listDropdown)); + addClassesToElement(div, list); + addClassesToElement(div, listDropdown); div.setAttribute('aria-expanded', 'false'); return div; @@ -353,25 +361,26 @@ const templates: TemplatesInterface = { innerHTML: string, type: NoticeType = NoticeTypes.generic, ): HTMLDivElement { - const classes = [...getClassNames(item), ...getClassNames(itemChoice), ...getClassNames(noticeItem)]; + const notice = document.createElement('div'); + setElementHtml(notice, true, innerHTML); + + addClassesToElement(notice, item); + addClassesToElement(notice, itemChoice); + addClassesToElement(notice, noticeItem); // eslint-disable-next-line default-case switch (type) { case NoticeTypes.addChoice: - classes.push(...getClassNames(addChoice)); + addClassesToElement(notice, addChoice); break; case NoticeTypes.noResults: - classes.push(...getClassNames(noResults)); + addClassesToElement(notice, noResults); break; case NoticeTypes.noChoices: - classes.push(...getClassNames(noChoices)); + addClassesToElement(notice, noChoices); break; } - const notice = document.createElement('div'); - setElementHtml(notice, true, innerHTML); - notice.className = classes.join(' '); - if (type === NoticeTypes.addChoice) { notice.dataset.choiceSelectable = ''; notice.dataset.choice = '';