Use helper function for add/remove classes, standardize adding/setting classnames for elements. Also shaves ~180 bytes off the compressed bundle

This commit is contained in:
Xon 2024-08-30 04:19:57 +08:00
commit 1e741fbdd9
7 changed files with 74 additions and 55 deletions

View file

@ -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)

View file

@ -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<HTMLElement>(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,

View file

@ -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;
}

View file

@ -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;

View file

@ -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<T extends HTMLInputElement | HTMLSelectElement> {
@ -36,7 +36,7 @@ export default class WrappedElement<T extends HTMLInputElement | HTMLSelectEleme
conceal(): void {
const el = this.element;
// Hide passed input
el.classList.add(...getClassNames(this.classNames.input));
addClassesToElement(el, this.classNames.input);
el.hidden = true;
// Remove element from tab index
@ -55,7 +55,7 @@ export default class WrappedElement<T extends HTMLInputElement | HTMLSelectEleme
reveal(): void {
const el = this.element;
// Reinstate passed element
el.classList.remove(...getClassNames(this.classNames.input));
removeClassesFromElement(el, this.classNames.input);
el.hidden = false;
el.removeAttribute('tabindex');

View file

@ -202,6 +202,14 @@ export const getClassNamesSelector = (option: string | Array<string> | null): st
return `.${option}`;
};
export const addClassesToElement = (element: HTMLElement, className: Array<string> | string): void => {
element.classList.add(...getClassNames(className));
};
export const removeClassesFromElement = (element: HTMLElement, className: Array<string> | 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);
}
};

View file

@ -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 = '';