From 76cfad1902d9df9e7ceffcd71a9f53f0266adb42 Mon Sep 17 00:00:00 2001 From: Dale Bustad Date: Tue, 25 Feb 2025 14:33:46 -0800 Subject: [PATCH] feat: implement remaining ClassList methods (#5233) * feat: implement remaining ClassList methods * test: getter-class-list-modify * chore: bump ci * chore: add failing fixture test for SSRv1 --- .../getter-class-list-modify/config.json | 7 +++ .../getter-class-list-modify/error-ssr.txt | 0 .../getter-class-list-modify/error.txt | 1 + .../expected-ssr.html | 4 ++ .../getter-class-list-modify/expected.html | 0 .../x/getter-class-list/getter-class-list.js | 25 +++++++++ packages/@lwc/ssr-runtime/src/class-list.ts | 52 +++++++++++-------- 7 files changed, 67 insertions(+), 22 deletions(-) create mode 100644 packages/@lwc/engine-server/src/__tests__/fixtures/getter-class-list-modify/config.json create mode 100644 packages/@lwc/engine-server/src/__tests__/fixtures/getter-class-list-modify/error-ssr.txt create mode 100644 packages/@lwc/engine-server/src/__tests__/fixtures/getter-class-list-modify/error.txt create mode 100644 packages/@lwc/engine-server/src/__tests__/fixtures/getter-class-list-modify/expected-ssr.html create mode 100644 packages/@lwc/engine-server/src/__tests__/fixtures/getter-class-list-modify/expected.html create mode 100755 packages/@lwc/engine-server/src/__tests__/fixtures/getter-class-list-modify/modules/x/getter-class-list/getter-class-list.js diff --git a/packages/@lwc/engine-server/src/__tests__/fixtures/getter-class-list-modify/config.json b/packages/@lwc/engine-server/src/__tests__/fixtures/getter-class-list-modify/config.json new file mode 100644 index 0000000000..591452b80a --- /dev/null +++ b/packages/@lwc/engine-server/src/__tests__/fixtures/getter-class-list-modify/config.json @@ -0,0 +1,7 @@ +{ + "entry": "x/getter-class-list", + "ssrFiles": { + "error": "error-ssr.txt", + "expected": "expected-ssr.html" + } +} diff --git a/packages/@lwc/engine-server/src/__tests__/fixtures/getter-class-list-modify/error-ssr.txt b/packages/@lwc/engine-server/src/__tests__/fixtures/getter-class-list-modify/error-ssr.txt new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/@lwc/engine-server/src/__tests__/fixtures/getter-class-list-modify/error.txt b/packages/@lwc/engine-server/src/__tests__/fixtures/getter-class-list-modify/error.txt new file mode 100644 index 0000000000..ea21a5aa41 --- /dev/null +++ b/packages/@lwc/engine-server/src/__tests__/fixtures/getter-class-list-modify/error.txt @@ -0,0 +1 @@ +classList.forEach is not a function \ No newline at end of file diff --git a/packages/@lwc/engine-server/src/__tests__/fixtures/getter-class-list-modify/expected-ssr.html b/packages/@lwc/engine-server/src/__tests__/fixtures/getter-class-list-modify/expected-ssr.html new file mode 100644 index 0000000000..f78beb32b5 --- /dev/null +++ b/packages/@lwc/engine-server/src/__tests__/fixtures/getter-class-list-modify/expected-ssr.html @@ -0,0 +1,4 @@ + + + \ No newline at end of file diff --git a/packages/@lwc/engine-server/src/__tests__/fixtures/getter-class-list-modify/expected.html b/packages/@lwc/engine-server/src/__tests__/fixtures/getter-class-list-modify/expected.html new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/@lwc/engine-server/src/__tests__/fixtures/getter-class-list-modify/modules/x/getter-class-list/getter-class-list.js b/packages/@lwc/engine-server/src/__tests__/fixtures/getter-class-list-modify/modules/x/getter-class-list/getter-class-list.js new file mode 100755 index 0000000000..9ef0d2d831 --- /dev/null +++ b/packages/@lwc/engine-server/src/__tests__/fixtures/getter-class-list-modify/modules/x/getter-class-list/getter-class-list.js @@ -0,0 +1,25 @@ +import { expect } from 'vitest'; +import { LightningElement } from 'lwc'; + +export default class GetterClassList extends LightningElement { + connectedCallback() { + const { classList } = this; + + classList.add('a', 'b', 'c', 'd-e', 'f', 'g', 'h', 'i'); + classList.forEach((value) => { + classList.remove(value); + }); + expect(this.getAttribute('class')).toBe(''); + expect(this.classList.length).toBe(0); + + classList.add('a', 'b', 'c', 'd-e', 'f', 'g', 'h', 'i'); + while (classList.length > 0) { + classList.remove(classList.item(0)); + } + expect(this.getAttribute('class')).toBe(''); + expect(this.classList.length).toBe(0); + + classList.add('a', 'b', 'c', 'd-e', 'f', 'g', 'h', 'i'); + expect(classList.item(3)).toBe('d-e'); + } +} diff --git a/packages/@lwc/ssr-runtime/src/class-list.ts b/packages/@lwc/ssr-runtime/src/class-list.ts index 6828f78b94..405360903a 100644 --- a/packages/@lwc/ssr-runtime/src/class-list.ts +++ b/packages/@lwc/ssr-runtime/src/class-list.ts @@ -7,8 +7,6 @@ import type { LightningElement } from './lightning-element'; -const MULTI_SPACE = /\s+/g; - // Copied from lib.dom interface DOMTokenList { readonly length: number; @@ -28,6 +26,15 @@ interface DOMTokenList { [index: number]: string; } +const MULTI_SPACE = /\s+/g; + +function parseClassName(className: string | null): string[] { + return (className ?? '') + .split(MULTI_SPACE) + .map((item) => item.trim()) + .filter(Boolean); +} + export class ClassList implements DOMTokenList { el: LightningElement; @@ -36,8 +43,7 @@ export class ClassList implements DOMTokenList { } add(...newClassNames: string[]) { - const className = this.el.className; - const set = new Set(className.split(MULTI_SPACE).filter(Boolean)); + const set = new Set(parseClassName(this.el.className)); for (const newClassName of newClassNames) { set.add(newClassName); } @@ -45,13 +51,11 @@ export class ClassList implements DOMTokenList { } contains(className: string) { - const currentClassNameStr = this.el.className; - return currentClassNameStr.split(MULTI_SPACE).includes(className); + return parseClassName(this.el.className).includes(className); } remove(...classNamesToRemove: string[]) { - const className = this.el.className; - const set = new Set(className.split(MULTI_SPACE).filter(Boolean)); + const set = new Set(parseClassName(this.el.className)); for (const newClassName of classNamesToRemove) { set.delete(newClassName); } @@ -60,8 +64,7 @@ export class ClassList implements DOMTokenList { replace(oldClassName: string, newClassName: string) { let classWasReplaced = false; - const className = this.el.className; - const listOfClasses = className.split(MULTI_SPACE).filter(Boolean) as string[]; + const listOfClasses = parseClassName(this.el.className); listOfClasses.forEach((value, idx) => { if (value === oldClassName) { classWasReplaced = true; @@ -73,8 +76,7 @@ export class ClassList implements DOMTokenList { } toggle(classNameToToggle: string, force?: boolean) { - const classNameStr = this.el.className; - const set = new Set(classNameStr.split(MULTI_SPACE).filter(Boolean)); + const set = new Set(parseClassName(this.el.className)); if (!set.has(classNameToToggle) && force !== false) { set.add(classNameToToggle); } else if (set.has(classNameToToggle) && force !== true) { @@ -93,22 +95,28 @@ export class ClassList implements DOMTokenList { } get length(): number { - const currentClassNameStr = this.el.className ?? ''; - return currentClassNameStr.split(MULTI_SPACE).length; + return parseClassName(this.el.className).length; } // Stubs to satisfy DOMTokenList interface [index: number]: never; // Can't implement arbitrary index getters without a proxy - item(_index: number): string | null { - throw new Error('Method "item" not implemented.'); - } - supports(_token: string): boolean { - throw new Error('Method "supports" not implemented.'); + + item(index: number): string | null { + return parseClassName(this.el.className ?? '')[index] ?? null; } + forEach( - _callbackfn: (value: string, key: number, parent: DOMTokenList) => void, - _thisArg?: any + callbackFn: (value: string, key: number, parent: DOMTokenList) => void, + thisArg?: any ): void { - throw new Error('Method "forEach" not implemented.'); + parseClassName(this.el.className).forEach((value, index) => + callbackFn.call(thisArg, value, index, this) + ); + } + + // This method is present on DOMTokenList but throws an error in the browser when used + // in connection with Element#classList. + supports(_token: string): boolean { + throw new TypeError('DOMTokenList has no supported tokens.'); } }