From 46da5d5194261eb826bc2fda11650a7b86ff70d0 Mon Sep 17 00:00:00 2001 From: Daniel Wurzer Date: Wed, 12 Aug 2026 08:57:04 +0200 Subject: [PATCH 01/16] test(cards): changed to vitest and fixed removing eventhandlers and initial state of reveal card --- components/appbar/appbar.test.mjs | 4 +- .../button/{fabSpec.js => ___fabSpec.js} | 0 .../button/___taptargetSpec.js | 0 .../card/{cardsSpec.js => cards.test.ts} | 121 +- components/card/cards.ts | 5 +- jasmine.json | 18 - package-lock.json | 1262 ++--------------- package.json | 3 - spec/helper.js | 338 ----- {spec => src}/scrollspySpec.js | 0 src/waves.js | 11 - 11 files changed, 171 insertions(+), 1591 deletions(-) rename components/button/{fabSpec.js => ___fabSpec.js} (100%) rename spec/taptargetSpec.js => components/button/___taptargetSpec.js (100%) rename components/card/{cardsSpec.js => cards.test.ts} (69%) delete mode 100644 jasmine.json delete mode 100644 spec/helper.js rename {spec => src}/scrollspySpec.js (100%) diff --git a/components/appbar/appbar.test.mjs b/components/appbar/appbar.test.mjs index 06fd8581b8..437ae055c0 100644 --- a/components/appbar/appbar.test.mjs +++ b/components/appbar/appbar.test.mjs @@ -1,8 +1,8 @@ -import { describe, expect, test } from 'vitest'; +import { describe, expect, it } from 'vitest'; import { AppBar } from './appbar.mjs'; describe('appBar', () => { - test('create html', () => { + it('should create the html', () => { const appBar = new AppBar(); expect(appBar.toHTML()).toContain(' { const fixture = `
@@ -115,6 +126,7 @@ describe('Cards', () => {
`; + // Helper to safely fetch bounding dimensions in happy-dom const roundedRect = (el) => { const rect = el.getBoundingClientRect(); return { @@ -127,13 +139,41 @@ describe('Cards', () => { }; }; + // Helper function to simulate element click + const click = (el) => { + el.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true })); + }; + + // Helper to check element visibility under happy-dom rules + const isVisible = (el) => { + const style = window.getComputedStyle(el); + return style.display !== 'none' && style.visibility !== 'hidden'; + }; + + // Helper to mock element layout measurements for Happy DOM + const mockRect = (el, rect) => { + vi.spyOn(el, 'getBoundingClientRect').mockReturnValue({ + top: rect.top || 0, + left: rect.left || 0, + width: rect.width || 0, + height: rect.height || 0, + right: (rect.left || 0) + (rect.width || 0), + bottom: (rect.top || 0) + (rect.height || 0), + x: rect.left || 0, + y: rect.top || 0, + toJSON: () => {} + }); + }; + beforeEach(() => { - XloadHtml(fixture); - M.Cards.init(document.querySelectorAll('.card')); + vi.useFakeTimers(); + document.body.innerHTML = fixture; + Cards.init(document.querySelectorAll('.card')); }); afterEach(() => { - XunloadFixtures(); + vi.useRealTimers(); + document.body.innerHTML = ''; }); describe('reveal cards', () => { @@ -146,40 +186,36 @@ describe('Cards', () => { }); it('should have a hidden card-reveal initially', () => { - expect(revealDiv).toBeHidden('reveal div should be hidden initially'); + expect(isVisible(revealDiv)).toBe(false); }); - it('should show card-reveal after clicking an activator', (done) => { + it('should show card-reveal after clicking an activator', () => { const activator = revealCard.querySelector('.activator'); click(activator); + vi.advanceTimersByTime(500); - setTimeout(() => { - expect(revealDiv).toBeVisible('reveal did not appear after activator was clicked.'); - done(); - }, 500); + expect(isVisible(revealDiv)).toBe(true); }); - it('should size and position card-reveal to cover the card when opened', (done) => { + it('should size and position card-reveal to cover the card when opened', () => { const activator = revealCard.querySelector('.activator'); + // Mock bounding box for happy-dom layout checks + mockRect(revealCard, { top: 10, left: 10, width: 300, height: 400 }); + mockRect(revealDiv, { top: 10, left: 10, width: 300, height: 400 }); + click(activator); + vi.advanceTimersByTime(500); - setTimeout(() => { - const revealRect = roundedRect(revealDiv); - const cardRect = roundedRect(revealCard); - - expect(revealDiv).toBeVisible('reveal did not appear after activator was clicked.'); - expect(revealRect.width).toEqual(cardRect.width, 'reveal width should match card width'); - expect(revealRect.height).toEqual( - cardRect.height, - 'reveal height should match card height' - ); - expect(revealRect.top).toEqual(cardRect.top, 'reveal top should align with card top'); - expect(revealRect.left).toEqual(cardRect.left, 'reveal left should align with card left'); - - done(); - }, 500); + const revealRect = roundedRect(revealDiv); + const cardRect = roundedRect(revealCard); + + expect(isVisible(revealDiv)).toBe(true); + expect(revealRect.width).toBe(cardRect.width); + expect(revealRect.height).toBe(cardRect.height); + expect(revealRect.top).toBe(cardRect.top); + expect(revealRect.left).toBe(cardRect.left); }); }); @@ -193,11 +229,14 @@ describe('Cards', () => { }); it('should have an image that fills the full width of the card', () => { + mockRect(imageCard, { top: 0, left: 0, width: 350, height: 450 }); + mockRect(image, { top: 0, left: 0, width: 350, height: 200 }); + const imageRect = roundedRect(image); const cardRect = roundedRect(imageCard); - expect(imageRect.width).toEqual(cardRect.width, 'image does not fill width of card'); - expect(imageRect.top).toEqual(cardRect.top, 'image top should align with card top'); + expect(imageRect.width).toBe(cardRect.width); + expect(imageRect.top).toBe(cardRect.top); }); }); @@ -213,26 +252,30 @@ describe('Cards', () => { const cardContent = card.querySelector('.card-content'); const cardAction = card.querySelector('.card-action'); + // Mock relative dimensions for Happy DOM layout calculation + mockRect(card, { top: 0, height: expectedHeight }); + mockRect(cardImage, { height: maxImageHeight - 10 }); + mockRect(cardContent, { height: maxContentHeight - 10 }); + mockRect(cardAction, { top: expectedHeight - 50, height: 50 }); + const cardRect = roundedRect(card); const imageRect = roundedRect(cardImage); const contentRect = roundedRect(cardContent); const actionRect = roundedRect(cardAction); - expect(cardRect.height).toEqual( - expectedHeight, - `${sizeName} card should be ${expectedHeight}px high` + expect(cardRect.height, `${sizeName} card should be ${expectedHeight}px high`).toBe( + expectedHeight ); - expect(imageRect.height).toBeLessThan( - maxImageHeight + 1, + expect( + imageRect.height, `${sizeName} image should be <= ${maxImageHeight}px high` - ); - expect(contentRect.height).toBeLessThan( - maxContentHeight + 1, + ).toBeLessThan(maxImageHeight + 1); + expect( + contentRect.height, `${sizeName} content should be <= ${maxContentHeight}px high` - ); - expect(actionRect.bottom).toEqual( - cardRect.bottom, - `${sizeName} action should be at bottom of card` + ).toBeLessThan(maxContentHeight + 1); + expect(actionRect.bottom, `${sizeName} action should be at bottom of card`).toBe( + cardRect.bottom ); }; diff --git a/components/card/cards.ts b/components/card/cards.ts index 3e2a707cbd..1b5ba34939 100644 --- a/components/card/cards.ts +++ b/components/card/cards.ts @@ -33,6 +33,7 @@ class Cards extends Component implements Openable { this.#cardReveal = this.el.querySelector('.card-reveal'); if (this.#cardReveal) { this.#initialOverflow = getComputedStyle(this.el).overflow; + this.#cardReveal.style.display = 'none'; this.#activators = Array.from(this.el.querySelectorAll('.activator')); this.#activators.forEach((el: HTMLElement) => { if (el) el.tabIndex = 0; @@ -119,8 +120,8 @@ class Cards extends Component implements Openable { }; #removeRevealCloseEventHandlers = () => { - this.#cardRevealClose.addEventListener('click', this.close); - this.#cardRevealClose.addEventListener('keypress', this.#handleKeypressCloseEvent); + this.#cardRevealClose.removeEventListener('click', this.close); + this.#cardRevealClose.removeEventListener('keypress', this.#handleKeypressCloseEvent); }; #handleKeypressCloseEvent: (e: KeyboardEvent) => void = (e: KeyboardEvent) => { diff --git a/jasmine.json b/jasmine.json deleted file mode 100644 index a75d4aca4b..0000000000 --- a/jasmine.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "srcDir": "dist", - "srcFiles": ["**/materialize.js"], - "specDir": ".", - "specFiles": ["components/**/*[sS]pec.js", "spec/*[sS]pec.js"], - "helpers": ["spec/helper.js"], - "cssFiles": ["**/materialize.css"], - "env": { - "stopSpecOnExpectationFailure": false, - "stopOnSpecFailure": false, - "random": true - }, - "listenAddress": "localhost", - "hostname": "localhost", - "browser": { - "name": "headlessChrome" - } -} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 5c48c3f4ee..af00712f5a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -35,9 +35,6 @@ "eslint": "^9.10.0", "eslint-plugin-storybook": "^9.0.12", "happy-dom": "^20.11.1", - "jasmine": "^5.1.0", - "jasmine-browser-runner": "^2.5.0", - "jasmine-core": "^5.2.0", "postcss": "^8.4.49", "rollup": "^4.59.0", "rollup-plugin-copy": "^3.5.0", @@ -95,13 +92,6 @@ "node": ">=6.9.0" } }, - "node_modules/@bazel/runfiles": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/@bazel/runfiles/-/runfiles-6.5.0.tgz", - "integrity": "sha512-RzahvqTkfpY2jsDxo8YItPX+/iZ6hbiikw1YhE0bA9EKBR5Og8Pa6FHn9PO9M0zaXRVsr0GFQLKbB/0rzy9SzA==", - "dev": true, - "license": "Apache-2.0" - }, "node_modules/@esbuild/aix-ppc64": { "version": "0.25.12", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", @@ -1944,9 +1934,9 @@ "license": "MIT" }, "node_modules/@testing-library/user-event": { - "version": "14.6.3", - "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.6.3.tgz", - "integrity": "sha512-6dBq67jT8lE+JTE8Exm02Kt6ze43hz1jdiSpSJwtTZiT1xQQ6b7nZYTTQ9njdArdU8XklOwaDp/AbT/eYSKF4g==", + "version": "14.6.4", + "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.6.4.tgz", + "integrity": "sha512-QCGwP6QrjypBLwyj5cuyfVamkaIEy/XGY+1VDehbtbQqOggYmTFpFOdWR5mPz14vX8vXLMVjDHlRNBcClyO9ew==", "dev": true, "license": "MIT", "engines": { @@ -2071,17 +2061,17 @@ } }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.66.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.66.0.tgz", - "integrity": "sha512-p088eaGrzYz1s+7cov0aMOCkNGTJlVxF4jgubf28c8L0Cv9Rloj8YBHnv4hXLq6IIEE1AsjNWavO+k+8kP2Y0A==", + "version": "8.67.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.67.0.tgz", + "integrity": "sha512-Un7Heoyj65NREbKAyIrFxeM143NZpExWmy1Nep4DLeQOeLlTeumPjoNKnBrU5D5moWXbPJgRa5Uwcdu0faVNGQ==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.12.2", - "@typescript-eslint/scope-manager": "8.66.0", - "@typescript-eslint/type-utils": "8.66.0", - "@typescript-eslint/utils": "8.66.0", - "@typescript-eslint/visitor-keys": "8.66.0", + "@typescript-eslint/scope-manager": "8.67.0", + "@typescript-eslint/type-utils": "8.67.0", + "@typescript-eslint/utils": "8.67.0", + "@typescript-eslint/visitor-keys": "8.67.0", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.5.0" @@ -2094,7 +2084,7 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.66.0", + "@typescript-eslint/parser": "^8.67.0", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } @@ -2110,16 +2100,16 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.66.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.66.0.tgz", - "integrity": "sha512-X6ypGChaWYk6PBtUg2BwuTZEFFcHJAtGTVJ9/lCTOufhZ4i9fNolQNnktq+kkMCwMj7V8Svsq7+TxSDslmhE0g==", + "version": "8.67.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.67.0.tgz", + "integrity": "sha512-fUBfTuuEulWqX6V8+O3PtScV01tzYYRUDTAirHFKoRAt7nOzoGiPt0M/bB47wWNy0coOOcgEwAMUtBpykMxl6w==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.66.0", - "@typescript-eslint/types": "8.66.0", - "@typescript-eslint/typescript-estree": "8.66.0", - "@typescript-eslint/visitor-keys": "8.66.0", + "@typescript-eslint/scope-manager": "8.67.0", + "@typescript-eslint/types": "8.67.0", + "@typescript-eslint/typescript-estree": "8.67.0", + "@typescript-eslint/visitor-keys": "8.67.0", "debug": "^4.4.3" }, "engines": { @@ -2135,14 +2125,14 @@ } }, "node_modules/@typescript-eslint/project-service": { - "version": "8.66.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.66.0.tgz", - "integrity": "sha512-7MthGPTt4BP69lSryqpqq8HQqxuzynssckL/jyDyk3+TNMQ3y2jFWkptCrktWvBrP+EH787Nl5N5Qpw7WZg+5g==", + "version": "8.67.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.67.0.tgz", + "integrity": "sha512-cvE8c7ulYeXN9fYuszhCeCsbzyVEXuhrRCybnBre7TUmqb5nRmBfQAwCj0O3WJFDeyAZt4VYv51vMCC9LHSdYw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.66.0", - "@typescript-eslint/types": "^8.66.0", + "@typescript-eslint/tsconfig-utils": "^8.67.0", + "@typescript-eslint/types": "^8.67.0", "debug": "^4.4.3" }, "engines": { @@ -2157,14 +2147,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.66.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.66.0.tgz", - "integrity": "sha512-8TGcH25j9zqJ/IULB/ppyhRvxA8QYfFEZ7nfbg6/BN9spDgb8fPWQXlE5l8TWBL50EtUx007uZ1o9VOwrq2/9g==", + "version": "8.67.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.67.0.tgz", + "integrity": "sha512-EgvsleTwS4E+WzzSvem8fAUubLwatMNF1B5hHSLQxcvs7q2dtRhGyujHwLJSYlG41niJ7GP24Aha2+0mb1b2kg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.66.0", - "@typescript-eslint/visitor-keys": "8.66.0" + "@typescript-eslint/types": "8.67.0", + "@typescript-eslint/visitor-keys": "8.67.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2175,9 +2165,9 @@ } }, "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.66.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.66.0.tgz", - "integrity": "sha512-9D5gLYZG4rOjcoag8MQ/fWI8WqA9wcPDyOGyWtWFhvM1lHRbliqUSPIY5J3zqCU1tvSwzXxnnjhQhz5Ne7mJ4g==", + "version": "8.67.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.67.0.tgz", + "integrity": "sha512-vV+LUSv5njUWsknE71fqKTlXUva+R76SaeORd6Zojcunk/6DvKFXONU3BrAs2H49mbygUXt6gbYunzwqNwlhdg==", "dev": true, "license": "MIT", "engines": { @@ -2192,15 +2182,15 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.66.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.66.0.tgz", - "integrity": "sha512-LG2dWfjZQQp0ADtAu/EWJVayefGL2UEZ3CDeI44D9v3rXB/WYUqE/jpO28KrEKul5AySrmI+Zh1v6v+xW2U9+g==", + "version": "8.67.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.67.0.tgz", + "integrity": "sha512-aVWDXbRmdXO9siTfX4ditQI1T9+zVcNazT48EJCD0v40/9RIFoUgZ05CmGEq9H2gixRpjUn/iplwvlcvutJW/Q==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.66.0", - "@typescript-eslint/typescript-estree": "8.66.0", - "@typescript-eslint/utils": "8.66.0", + "@typescript-eslint/types": "8.67.0", + "@typescript-eslint/typescript-estree": "8.67.0", + "@typescript-eslint/utils": "8.67.0", "debug": "^4.4.3", "ts-api-utils": "^2.5.0" }, @@ -2217,9 +2207,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.66.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.66.0.tgz", - "integrity": "sha512-H6gcYaSDOyvL3AD/jHUtUFo2jqGgn/F6nuyuZSu0QTesxL+cP4dQoIMrODRofuJC09g64+WgZ6tE19Y1N2YIFQ==", + "version": "8.67.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.67.0.tgz", + "integrity": "sha512-sBtgslww8nsMYUjhdPBiSyUqSzT8uR6g93A2QXnQC8+cGdjz0CyaOdqHDRJb1AtORbZCNUJBBeFA/tNR2uQmww==", "dev": true, "license": "MIT", "engines": { @@ -2231,16 +2221,16 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.66.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.66.0.tgz", - "integrity": "sha512-8/x4INiiQb10jGgXYD7116/zQ+OL84ZIFn0za68wwFHCanT/VLbBEroWht8RV8fn0/ZCAoazHLQgwUC0UQcDfg==", + "version": "8.67.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.67.0.tgz", + "integrity": "sha512-EKQBCE9yNlRJYm7jdTW5AhDacDUmSwQb0FAJAmK2EKYrNXIsa2vxcSZx6PvJ/dEdI6lS+Y9W+EXckLj0iPFGcw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.66.0", - "@typescript-eslint/tsconfig-utils": "8.66.0", - "@typescript-eslint/types": "8.66.0", - "@typescript-eslint/visitor-keys": "8.66.0", + "@typescript-eslint/project-service": "8.67.0", + "@typescript-eslint/tsconfig-utils": "8.67.0", + "@typescript-eslint/types": "8.67.0", + "@typescript-eslint/visitor-keys": "8.67.0", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", @@ -2298,16 +2288,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.66.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.66.0.tgz", - "integrity": "sha512-jasearZPolBw5NJNYGMwxzHMF83niVWmMU1VdHzG1CyfI2VS7f7nZltnKtHcg20hW+7Uo5GfK4MeDPoU3qI8EA==", + "version": "8.67.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.67.0.tgz", + "integrity": "sha512-U9D1FdwEWBwok3hxxSdhclMb0twvt9QnjIQ0VfQ1AiX2epnpSgv2ubVDsayOFyY8K6FX+AQ7E0FKWVG3iKsj1A==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", - "@typescript-eslint/scope-manager": "8.66.0", - "@typescript-eslint/types": "8.66.0", - "@typescript-eslint/typescript-estree": "8.66.0" + "@typescript-eslint/scope-manager": "8.67.0", + "@typescript-eslint/types": "8.67.0", + "@typescript-eslint/typescript-estree": "8.67.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2322,13 +2312,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.66.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.66.0.tgz", - "integrity": "sha512-dkKR8q+lKciskj1Y3vthHktl+3cMLWGyVUP23bRiPZ5O9BRT++4EqDDV+TVeIKBL1VXVEqrJlz8MYbcnvJcAlg==", + "version": "8.67.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.67.0.tgz", + "integrity": "sha512-fkv8dHRDqfGtTHuJeebdrQ7cX6Ad4WAS00rgHh9UGvMycF1mjBfsxry1XsLIFhWZ6Judlh6UdzK+TYlbpCXgnA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.66.0", + "@typescript-eslint/types": "8.67.0", "eslint-visitor-keys": "^5.0.0" }, "engines": { @@ -2566,20 +2556,6 @@ "node": ">=6.5" } }, - "node_modules/accepts": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", - "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", - "dev": true, - "license": "MIT", - "dependencies": { - "mime-types": "~2.1.34", - "negotiator": "0.6.3" - }, - "engines": { - "node": ">= 0.6" - } - }, "node_modules/acorn": { "version": "8.18.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.18.0.tgz", @@ -2701,13 +2677,6 @@ "dequal": "^2.0.3" } }, - "node_modules/array-flatten": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", - "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", - "dev": true, - "license": "MIT" - }, "node_modules/array-union": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", @@ -2883,9 +2852,9 @@ } }, "node_modules/bare-url": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.5.1.tgz", - "integrity": "sha512-cD5ciQuKlx+eumTCfqbfiL+fhQm+dHbVNB/cX4+d+I/nx4JsVop9VoFEPAs5RJ6I84QR6bZIBjpd2kjDOYeWcg==", + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.5.2.tgz", + "integrity": "sha512-L13PCJzKG8RGvx8V1/DdMi12ERhC3tprr7/8a94BxpmnRsFqxh5XZNdhtMxu5HPkRshYOOWRGY8lDP7ZhpG9Cg==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -2939,48 +2908,6 @@ "node": ">=12.0.0" } }, - "node_modules/body-parser": { - "version": "1.20.6", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.6.tgz", - "integrity": "sha512-p5tAzS57i5MV9fZFDj9LeIiTZEufbSe2eDozP+ElheSUq1m74CRq1jI4mYNDdVs9vQztXFLuk/Gd6BWTdwRJ5g==", - "dev": true, - "license": "MIT", - "dependencies": { - "bytes": "~3.1.2", - "content-type": "~1.0.5", - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "~1.2.0", - "http-errors": "~2.0.1", - "iconv-lite": "~0.4.24", - "on-finished": "~2.4.1", - "qs": "~6.15.1", - "raw-body": "~2.5.3", - "type-is": "~1.6.18", - "unpipe": "~1.0.0" - }, - "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" - } - }, - "node_modules/body-parser/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/body-parser/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "dev": true, - "license": "MIT" - }, "node_modules/brace-expansion": { "version": "1.1.18", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz", @@ -3094,47 +3021,6 @@ "node": ">=4.0" } }, - "node_modules/bytes": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", - "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/call-bind-apply-helpers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", - "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/call-bound": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", - "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "get-intrinsic": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -3284,29 +3170,6 @@ "dev": true, "license": "MIT" }, - "node_modules/content-disposition": { - "version": "0.5.4", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", - "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "safe-buffer": "5.2.1" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/content-type": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", - "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, "node_modules/convert-source-map": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", @@ -3314,23 +3177,6 @@ "dev": true, "license": "MIT" }, - "node_modules/cookie": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", - "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/cookie-signature": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", - "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", - "dev": true, - "license": "MIT" - }, "node_modules/core-util-is": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", @@ -3440,16 +3286,6 @@ "node": ">=8" } }, - "node_modules/depd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, "node_modules/dequal": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", @@ -3460,17 +3296,6 @@ "node": ">=6" } }, - "node_modules/destroy": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", - "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" - } - }, "node_modules/detect-libc": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", @@ -3503,21 +3328,6 @@ "license": "MIT", "peer": true }, - "node_modules/dunder-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", - "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.1", - "es-errors": "^1.3.0", - "gopd": "^1.2.0" - }, - "engines": { - "node": ">= 0.4" - } - }, "node_modules/eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", @@ -3525,33 +3335,10 @@ "dev": true, "license": "MIT" }, - "node_modules/ee-first": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", - "dev": true, - "license": "MIT" - }, - "node_modules/ejs": { - "version": "3.1.10", - "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", - "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "jake": "^10.8.5" - }, - "bin": { - "ejs": "bin/cli.js" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/electron-to-chromium": { - "version": "1.5.403", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.403.tgz", - "integrity": "sha512-MQsYmdaLzvaCX5j+ZZBr5Fm6uCCnPQcRtlvmvRlWqrXy+BH2O4ffXIAScF+JQznQWB9brWp4lSD9Z4yNmaf2BA==", + "version": "1.5.405", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.405.tgz", + "integrity": "sha512-bNglH7lPH5l+yHOes7Zr4VqxhOy4BQ9ZBUX4VdoFgxMpzJk7W1ZoO3Vgd9Pxa9PyjQ76sfm2aKH/nzEcCNRlew==", "dev": true, "license": "ISC" }, @@ -3562,16 +3349,6 @@ "dev": true, "license": "MIT" }, - "node_modules/encodeurl": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", - "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, "node_modules/entities": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", @@ -3585,16 +3362,6 @@ "url": "https://github.com/fb55/entities?sponsor=1" } }, - "node_modules/es-define-property": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", - "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, "node_modules/es-errors": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", @@ -3612,19 +3379,6 @@ "dev": true, "license": "MIT" }, - "node_modules/es-object-atoms": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", - "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - } - }, "node_modules/esbuild": { "version": "0.25.12", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", @@ -3690,13 +3444,6 @@ "node": ">=6" } }, - "node_modules/escape-html": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", - "dev": true, - "license": "MIT" - }, "node_modules/escape-string-regexp": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", @@ -3902,16 +3649,6 @@ "node": ">=0.10.0" } }, - "node_modules/etag": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, "node_modules/event-target-shim": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", @@ -3952,70 +3689,6 @@ "node": ">=12.0.0" } }, - "node_modules/express": { - "version": "4.22.2", - "resolved": "https://registry.npmjs.org/express/-/express-4.22.2.tgz", - "integrity": "sha512-IuL+Elrou2ZvCFHs18/CIzy2Nzvo25nZ1/D2eIZlz7c+QUayAcYoiM2BthCjs+EBHVpjYjcuLDAiCWgeIX3X1Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "accepts": "~1.3.8", - "array-flatten": "1.1.1", - "body-parser": "~1.20.5", - "content-disposition": "~0.5.4", - "content-type": "~1.0.4", - "cookie": "~0.7.1", - "cookie-signature": "~1.0.6", - "debug": "2.6.9", - "depd": "2.0.0", - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "finalhandler": "~1.3.1", - "fresh": "~0.5.2", - "http-errors": "~2.0.0", - "merge-descriptors": "1.0.3", - "methods": "~1.1.2", - "on-finished": "~2.4.1", - "parseurl": "~1.3.3", - "path-to-regexp": "~0.1.12", - "proxy-addr": "~2.0.7", - "qs": "~6.15.1", - "range-parser": "~1.2.1", - "safe-buffer": "5.2.1", - "send": "~0.19.0", - "serve-static": "~1.16.2", - "setprototypeof": "1.2.0", - "statuses": "~2.0.1", - "type-is": "~1.6.18", - "utils-merge": "1.0.1", - "vary": "~1.1.2" - }, - "engines": { - "node": ">= 0.10.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/express/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/express/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "dev": true, - "license": "MIT" - }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -4115,39 +3788,6 @@ "node": ">=16.0.0" } }, - "node_modules/filelist": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.6.tgz", - "integrity": "sha512-5giy2PkLYY1cP39p17Ech+2xlpTRL9HLspOfEgm0L6CwBXBTgsK5ou0JtzYuepxkaQ/tvhCFIJ5uXo0OrM2DxA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "minimatch": "^5.0.1" - } - }, - "node_modules/filelist/node_modules/brace-expansion": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.4.tgz", - "integrity": "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/filelist/node_modules/minimatch": { - "version": "5.1.9", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz", - "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/fill-range": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", @@ -4161,42 +3801,6 @@ "node": ">=8" } }, - "node_modules/finalhandler": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", - "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "2.6.9", - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "on-finished": "~2.4.1", - "parseurl": "~1.3.3", - "statuses": "~2.0.2", - "unpipe": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/finalhandler/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/finalhandler/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "dev": true, - "license": "MIT" - }, "node_modules/find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", @@ -4252,16 +3856,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/forwarded": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", - "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, "node_modules/fraction.js": { "version": "5.3.4", "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz", @@ -4276,16 +3870,6 @@ "url": "https://github.com/sponsors/rawify" } }, - "node_modules/fresh": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", - "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, "node_modules/fs-extra": { "version": "8.1.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", @@ -4333,45 +3917,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/get-intrinsic": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", - "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "es-define-property": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.1.1", - "function-bind": "^1.1.2", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "has-symbols": "^1.1.0", - "hasown": "^2.0.2", - "math-intrinsics": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", - "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "dev": true, - "license": "MIT", - "dependencies": { - "dunder-proto": "^1.0.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, "node_modules/glob": { "version": "10.5.0", "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", @@ -4488,19 +4033,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/gopd": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", - "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/graceful-fs": { "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", @@ -4537,23 +4069,10 @@ "node": ">=8" } }, - "node_modules/has-symbols": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", - "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/hasown": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", - "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", "dev": true, "license": "MIT", "dependencies": { @@ -4563,40 +4082,6 @@ "node": ">= 0.4" } }, - "node_modules/http-errors": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", - "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "depd": "~2.0.0", - "inherits": "~2.0.4", - "setprototypeof": "~1.2.0", - "statuses": "~2.0.2", - "toidentifier": "~1.0.1" - }, - "engines": { - "node": ">= 0.8" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", - "dev": true, - "license": "MIT", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/ieee754": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", @@ -4628,13 +4113,6 @@ "node": ">= 4" } }, - "node_modules/immediate": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", - "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", - "dev": true, - "license": "MIT" - }, "node_modules/immutable": { "version": "5.1.9", "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.9.tgz", @@ -4698,16 +4176,6 @@ "dev": true, "license": "ISC" }, - "node_modules/ipaddr.js": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", - "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.10" - } - }, "node_modules/is-core-module": { "version": "2.16.2", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.2.tgz", @@ -4849,64 +4317,6 @@ "@pkgjs/parseargs": "^0.11.0" } }, - "node_modules/jake": { - "version": "10.9.4", - "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.4.tgz", - "integrity": "sha512-wpHYzhxiVQL+IV05BLE2Xn34zW1S223hvjtqk0+gsPrwd/8JNLXJgZZM/iPFsYc1xyphF+6M6EvdE5E9MBGkDA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "async": "^3.2.6", - "filelist": "^1.0.4", - "picocolors": "^1.1.1" - }, - "bin": { - "jake": "bin/cli.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/jasmine": { - "version": "5.13.0", - "resolved": "https://registry.npmjs.org/jasmine/-/jasmine-5.13.0.tgz", - "integrity": "sha512-oLCXIhEb5e0zzjn9GyuvcuisvLBwUjmgz7a0RNGWKwQtJCDld4m+vwKUpAIJVLB5vbmQFdtKhT86/tIZlJ5gYw==", - "dev": true, - "license": "MIT", - "dependencies": { - "glob": "^10.2.2", - "jasmine-core": "~5.13.0" - }, - "bin": { - "jasmine": "bin/jasmine.js" - } - }, - "node_modules/jasmine-browser-runner": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/jasmine-browser-runner/-/jasmine-browser-runner-2.5.0.tgz", - "integrity": "sha512-CzdvpeZunUu6x1u8G6/vPnfcKVpDaBFfk3tIvm1hoA+EfceQ8FRvsy4o8hEcKYyMt556XFRnP5PjYsxFU8z7Xw==", - "dev": true, - "license": "MIT", - "dependencies": { - "ejs": "^3.1.6", - "express": "^4.19.2", - "glob": "^10.0.0", - "selenium-webdriver": "^4.12.0" - }, - "bin": { - "jasmine-browser-runner": "bin/jasmine-browser-runner" - }, - "peerDependencies": { - "jasmine-core": "^5.0.0" - } - }, - "node_modules/jasmine-core": { - "version": "5.13.0", - "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-5.13.0.tgz", - "integrity": "sha512-vsYjfh7lyqvZX5QgqKc4YH8phs7g96Z8bsdIFNEU3VqXhlHaq+vov/Fgn/sr6MiUczdZkyXRC3TX369Ll4Nzbw==", - "dev": true, - "license": "MIT" - }, "node_modules/js-tokens": { "version": "10.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-10.0.0.tgz", @@ -4969,52 +4379,6 @@ "graceful-fs": "^4.1.6" } }, - "node_modules/jszip": { - "version": "3.10.1", - "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", - "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==", - "dev": true, - "license": "(MIT OR GPL-3.0-or-later)", - "dependencies": { - "lie": "~3.3.0", - "pako": "~1.0.2", - "readable-stream": "~2.3.6", - "setimmediate": "^1.0.5" - } - }, - "node_modules/jszip/node_modules/readable-stream": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", - "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", - "dev": true, - "license": "MIT", - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "node_modules/jszip/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true, - "license": "MIT" - }, - "node_modules/jszip/node_modules/string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, - "license": "MIT", - "dependencies": { - "safe-buffer": "~5.1.0" - } - }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -5085,16 +4449,6 @@ "node": ">= 0.8.0" } }, - "node_modules/lie": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", - "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "immediate": "~3.0.5" - } - }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -5160,36 +4514,6 @@ "@jridgewell/sourcemap-codec": "^1.5.5" } }, - "node_modules/math-intrinsics": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", - "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/media-typer": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", - "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/merge-descriptors": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", - "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -5200,16 +4524,6 @@ "node": ">= 8" } }, - "node_modules/methods": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", - "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, "node_modules/micromatch": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", @@ -5237,42 +4551,6 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/mime": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", - "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", - "dev": true, - "license": "MIT", - "bin": { - "mime": "cli.js" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dev": true, - "license": "MIT", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, "node_modules/min-indent": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", @@ -5339,16 +4617,6 @@ "dev": true, "license": "MIT" }, - "node_modules/negotiator": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", - "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, "node_modules/node-addon-api": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", @@ -5383,19 +4651,6 @@ "integrity": "sha512-qizSNPO93t1YUuUhP22btGOo3chcvDFqFaj2TRybP0DMxkHOCTYwp3n34fel4a31ORXy4m1Xq0Gyqpb5m33qIg==", "license": "MIT" }, - "node_modules/object-inspect": { - "version": "1.13.4", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", - "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/obug": { "version": "2.1.4", "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.4.tgz", @@ -5410,19 +4665,6 @@ "node": ">=12.20.0" } }, - "node_modules/on-finished": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", - "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", - "dev": true, - "license": "MIT", - "dependencies": { - "ee-first": "1.1.1" - }, - "engines": { - "node": ">= 0.8" - } - }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -5508,13 +4750,6 @@ "dev": true, "license": "BlueOak-1.0.0" }, - "node_modules/pako": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", - "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", - "dev": true, - "license": "(MIT AND Zlib)" - }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -5528,16 +4763,6 @@ "node": ">=6" } }, - "node_modules/parseurl": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", - "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -5592,13 +4817,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/path-to-regexp": { - "version": "0.1.13", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz", - "integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==", - "dev": true, - "license": "MIT" - }, "node_modules/path-type": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", @@ -5739,20 +4957,6 @@ "dev": true, "license": "MIT" }, - "node_modules/proxy-addr": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", - "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", - "dev": true, - "license": "MIT", - "dependencies": { - "forwarded": "0.2.0", - "ipaddr.js": "1.9.1" - }, - "engines": { - "node": ">= 0.10" - } - }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -5763,23 +4967,6 @@ "node": ">=6" } }, - "node_modules/qs": { - "version": "6.15.3", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.3.tgz", - "integrity": "sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "es-define-property": "^1.0.1", - "side-channel": "^1.1.1" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -5801,32 +4988,6 @@ ], "license": "MIT" }, - "node_modules/range-parser": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", - "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/raw-body": { - "version": "2.5.3", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", - "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", - "dev": true, - "license": "MIT", - "dependencies": { - "bytes": "~3.1.2", - "http-errors": "~2.0.1", - "iconv-lite": "~0.4.24", - "unpipe": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, "node_modules/react": { "version": "19.2.8", "resolved": "https://registry.npmjs.org/react/-/react-19.2.8.tgz", @@ -5923,9 +5084,9 @@ } }, "node_modules/recast": { - "version": "0.23.20", - "resolved": "https://registry.npmjs.org/recast/-/recast-0.23.20.tgz", - "integrity": "sha512-VtSf75pThDqsIUpdaYrTdQvkw10/+yP0i7+Cax7h+K9SRvYjrJURZcRlcHMrH6TMzV275Q8a2A8+G7y7W9zqsg==", + "version": "0.23.21", + "resolved": "https://registry.npmjs.org/recast/-/recast-0.23.21.tgz", + "integrity": "sha512-mFAyJq9vUbSTARLZUvAEf1z3YxlvAwswbmxMx2mPA/MSm4KmpwvwvhsH/NIrZhyOuwD60Lzyw2qh83uCbgTPYw==", "dev": true, "license": "MIT", "dependencies": { @@ -5936,7 +5097,7 @@ "tslib": "^2.0.1" }, "engines": { - "node": ">= 22" + "node": ">= 4" } }, "node_modules/redent": { @@ -6163,13 +5324,6 @@ ], "license": "MIT" }, - "node_modules/safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "dev": true, - "license": "MIT" - }, "node_modules/sass": { "version": "1.102.0", "resolved": "https://registry.npmjs.org/sass/-/sass-1.102.0.tgz", @@ -6198,32 +5352,6 @@ "dev": true, "license": "MIT" }, - "node_modules/selenium-webdriver": { - "version": "4.46.0", - "resolved": "https://registry.npmjs.org/selenium-webdriver/-/selenium-webdriver-4.46.0.tgz", - "integrity": "sha512-UlTkgnx9y+bf3QxFsrgUyMqC2oy1Yf+qcOs7xIC/XfsUPv/ow7Sicx6SJ7AeOn2/Z+xF1M8SLqyn983wipI82w==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/SeleniumHQ" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/selenium" - } - ], - "license": "Apache-2.0", - "dependencies": { - "@bazel/runfiles": "^6.5.0", - "jszip": "^3.10.1", - "tmp": "^0.2.7", - "ws": "^8.21.0" - }, - "engines": { - "node": ">= 20.0.0" - } - }, "node_modules/semver": { "version": "7.8.5", "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", @@ -6237,48 +5365,6 @@ "node": ">=10" } }, - "node_modules/send": { - "version": "0.19.2", - "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", - "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "fresh": "~0.5.2", - "http-errors": "~2.0.1", - "mime": "1.6.0", - "ms": "2.1.3", - "on-finished": "~2.4.1", - "range-parser": "~1.2.1", - "statuses": "~2.0.2" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/send/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/send/node_modules/debug/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "dev": true, - "license": "MIT" - }, "node_modules/serialize-javascript": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-7.1.0.tgz", @@ -6289,36 +5375,6 @@ "node": ">=20.0.0" } }, - "node_modules/serve-static": { - "version": "1.16.3", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz", - "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==", - "dev": true, - "license": "MIT", - "dependencies": { - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "parseurl": "~1.3.3", - "send": "~0.19.1" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/setimmediate": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", - "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", - "dev": true, - "license": "MIT" - }, - "node_modules/setprototypeof": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", - "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", - "dev": true, - "license": "ISC" - }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -6342,82 +5398,6 @@ "node": ">=8" } }, - "node_modules/side-channel": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", - "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.4", - "side-channel-list": "^1.0.1", - "side-channel-map": "^1.0.1", - "side-channel-weakmap": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-list": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", - "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.4" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-map": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", - "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-weakmap": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", - "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3", - "side-channel-map": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/siginfo": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", @@ -6496,16 +5476,6 @@ "dev": true, "license": "MIT" }, - "node_modules/statuses": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", - "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, "node_modules/std-env": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.2.0.tgz", @@ -6744,9 +5714,9 @@ } }, "node_modules/terser": { - "version": "5.49.2", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.49.2.tgz", - "integrity": "sha512-rGbJiKeQ4WDe3EXlDAIaQcwftVfv2Q8o1awFNfvXolJYKkb1AuZY1RTOmqx4LJXZENbWZA7eIsYGHuEzHsi1nQ==", + "version": "5.50.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.50.0.tgz", + "integrity": "sha512-CN9BVxWhgS/hRxtUMjtC2uRWSTcSfQFHMDWma6sKKfIivCD91sM+FOPfvwoaRMqCSrUpe1nv3jDamd9eEQ4y+w==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -6833,16 +5803,6 @@ "node": ">=14.0.0" } }, - "node_modules/tmp": { - "version": "0.2.7", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.7.tgz", - "integrity": "sha512-e0votIpp4Uo2AJYSzVHV6xCcawuiez3DzqDAbrTc3YxBkplN6e+dM13ZeIcZnDg/QpSuU2zfZ3rzwY8ukEnaXw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14.14" - } - }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -6856,16 +5816,6 @@ "node": ">=8.0" } }, - "node_modules/toidentifier": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", - "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.6" - } - }, "node_modules/ts-api-utils": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", @@ -6909,20 +5859,6 @@ "node": ">= 0.8.0" } }, - "node_modules/type-is": { - "version": "1.6.18", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", - "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", - "dev": true, - "license": "MIT", - "dependencies": { - "media-typer": "0.3.0", - "mime-types": "~2.1.24" - }, - "engines": { - "node": ">= 0.6" - } - }, "node_modules/typescript": { "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", @@ -6938,16 +5874,16 @@ } }, "node_modules/typescript-eslint": { - "version": "8.66.0", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.66.0.tgz", - "integrity": "sha512-QlEbBPz/RuJ1XUHj29nm3t0F/O/cSlEnntozqPOYHnnTGAXFamnMBu5i9Vn6vhUPHGAjR+Vl+5J8vPN/BMUrJw==", + "version": "8.67.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.67.0.tgz", + "integrity": "sha512-S2udFs8tCKEKffuJ4TB1idGUZiXdCPGi3IPBGWXarbLQ5UPXORV8QEVzJ4gCRduURMb5EkpNCdjbk0eDIuI8Yg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/eslint-plugin": "8.66.0", - "@typescript-eslint/parser": "8.66.0", - "@typescript-eslint/typescript-estree": "8.66.0", - "@typescript-eslint/utils": "8.66.0" + "@typescript-eslint/eslint-plugin": "8.67.0", + "@typescript-eslint/parser": "8.67.0", + "@typescript-eslint/typescript-estree": "8.67.0", + "@typescript-eslint/utils": "8.67.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -6978,16 +5914,6 @@ "node": ">= 4.0.0" } }, - "node_modules/unpipe": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, "node_modules/unplugin": { "version": "1.16.1", "resolved": "https://registry.npmjs.org/unplugin/-/unplugin-1.16.1.tgz", @@ -7003,9 +5929,9 @@ } }, "node_modules/update-browserslist-db": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.0.tgz", - "integrity": "sha512-x/M6q3w4Ybp91CNaS4S69UnliqR3BzRpOT6LWbksjth0S/+jhfaPJsWjt/TewpT8j9eLIojUf5jr29WextHroA==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.1.tgz", + "integrity": "sha512-ZZ61DsRsOnakl74HAmp3oSN4aXUmEWXf+i/yv0h7tIBfICc3VdrFErQKUUKPgu3AMsTUMbcongALEN4l6GSUrQ==", "dev": true, "funding": [ { @@ -7050,26 +5976,6 @@ "dev": true, "license": "MIT" }, - "node_modules/utils-merge": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", - "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4.0" - } - }, - "node_modules/vary": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", - "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, "node_modules/vite": { "version": "7.3.6", "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.6.tgz", diff --git a/package.json b/package.json index 83b5e3d22e..36717d05ee 100644 --- a/package.json +++ b/package.json @@ -73,9 +73,6 @@ "eslint": "^9.10.0", "eslint-plugin-storybook": "^9.0.12", "happy-dom": "^20.11.1", - "jasmine": "^5.1.0", - "jasmine-browser-runner": "^2.5.0", - "jasmine-core": "^5.2.0", "postcss": "^8.4.49", "rollup": "^4.59.0", "rollup-plugin-copy": "^3.5.0", diff --git a/spec/helper.js b/spec/helper.js deleted file mode 100644 index 84d04a73b7..0000000000 --- a/spec/helper.js +++ /dev/null @@ -1,338 +0,0 @@ -const KEYMAP = { - '27': 'Escape', - '32': 'Space', - '13': 'Enter', - '37': 'Left', - '39': 'Right', - '38': 'Up', - '40': 'Down', - '8': 'Backspace', - '9': 'Tab', - '16': 'Shift', - '17': 'Control', - '18': 'Alt', - '19': 'Pause', - '20': 'Capslock', - '160': 'LeftShift', - '161': 'RightShift', - '162': 'LeftControl', - '163': 'RightControl', - '164': 'LeftAlt', - '165': 'RightAlt', - '91': 'Windows', - '92': 'RightWindows', - '33': 'PageUp', - '34': 'PageDown', - '35': 'End', - '36': 'Home', - '44': 'PrintScreen', - '45': 'Insert', - '46': 'Delete', - '145': 'ScrollLock', - '186': 'Semicolon', - '187': 'Equals', - '188': 'Comma', - '189': 'Underscore', - '190': 'Period', - '191': 'Slash', - '192': 'Tilde', - '219': 'OpenBracket', - '220': 'BackSlash', - '221': 'CloseBracket', - '222': 'Quote', - '65': 'A', - '66': 'B', - '67': 'C', - '68': 'D', - '69': 'E', - '70': 'F', - '71': 'G', - '72': 'H', - '73': 'I', - '74': 'J', - '75': 'K', - '76': 'L', - '77': 'M', - '78': 'N', - '79': 'O', - '80': 'P', - '81': 'Q', - '82': 'R', - '83': 'S', - '84': 'T', - '85': 'U', - '86': 'V', - '87': 'W', - '88': 'X', - '89': 'Y', - '90': 'Z', - '48': 'D0', - '49': 'D1', - '50': 'D2', - '51': 'D3', - '52': 'D4', - '53': 'D5', - '54': 'D6', - '55': 'D7', - '56': 'D8', - '57': 'D9', - '112': 'F1', - '113': 'F2', - '114': 'F3', - '115': 'F4', - '116': 'F5', - '117': 'F6', - '118': 'F7', - '119': 'F8', - '120': 'F9', - '121': 'F10', - '122': 'F11', - '123': 'F12', - '124': 'F13', - '125': 'F14', - '126': 'F15', - '127': 'F16', - '128': 'F17', - '129': 'F18', - '130': 'F19', - '131': 'F20', - '132': 'F21', - '133': 'F22', - '134': 'F23', - '135': 'F24', - '144': 'Numlock', - '111': 'Divide', - '106': 'Multiply', - '107': 'Add', - '109': 'Subtract', - '110': 'NumpadDelete', - '96': '0', - '97': '1', - '98': '2', - '99': '3', - '100': '4', - '101': '5', - '102': '6', - '103': '7', - '104': '8', - '105': '9' -}; - -// eslint-disable-next-line @typescript-eslint/no-unused-vars -function XloadHtml(html, options) { - options = options ? options : {}; - const defaultOptions = { insertionType: 'append' }; - options = { - ...defaultOptions, - ...options - }; - const div = document.createElement('div'); - div.classList.add('please-delete-me'); - div.innerHTML = html; - if (options.insertionType === 'append') { - document.body.appendChild(div); - } else if (options.insertionType === 'prepend') { - document.body.prepend(div); - } -} - -// eslint-disable-next-line @typescript-eslint/no-unused-vars -function XunloadFixtures() { - document.querySelectorAll('.please-delete-me').forEach((el) => el.remove()); - document.querySelectorAll('.material-tooltip').forEach((el) => el.remove()); - document.querySelectorAll('.dropdown-content').forEach((el) => el.remove()); -} - -beforeEach(() => { - const matchers = { - toExist: (util, customEqualityTesters) => { - return { - compare: (actual) => { - const result = {}; - result.pass = util.equals(!!actual, true, customEqualityTesters); - return result; - } - }; - }, - hasMaxHeightZero: (util, customEqualityTesters) => { - return { - compare: (actual) => { - const style = getComputedStyle(actual); - const result = {}; - result.pass = util.equals( - style.getPropertyValue('max-height'), - '0px', - customEqualityTesters - ); - return result; - } - }; - }, - notHasMaxHeightZero: (util, customEqualityTesters) => { - return { - compare: (actual) => { - const style = getComputedStyle(actual); - const result = {}; - result.pass = !util.equals( - style.getPropertyValue('max-height'), - '0px', - customEqualityTesters - ); - return result; - } - }; - }, - toBeHidden: (util, customEqualityTesters) => { - return { - compare: (actual) => { - const style = getComputedStyle(actual); - const result = {}; - result.pass = util.equals( - style.getPropertyValue('display'), - 'none', - customEqualityTesters - ); - return result; - } - }; - }, - toBeVisible: (util, customEqualityTesters) => { - return { - compare: (actual) => { - const style = getComputedStyle(actual); - const result = {}; - result.pass = !util.equals( - style.getPropertyValue('display'), - 'none', - customEqualityTesters - ); - if (result.pass) { - result.pass = util.equals( - style.getPropertyValue('visibility'), - 'visible', - customEqualityTesters - ); - } - return result; - } - }; - }, - toHaveClass: (util, customEqualityTesters) => { - return { - compare: (actual, expected) => { - const result = {}; - result.pass = util.equals( - actual.classList.contains(expected), - true, - customEqualityTesters - ); - return result; - } - }; - }, - toNotHaveClass: (util, customEqualityTesters) => { - return { - compare: function (actual, expected) { - const result = {}; - result.pass = util.equals( - actual.classList.contains(expected), - false, - customEqualityTesters - ); - return result; - } - }; - } - }; - - jasmine.addMatchers(matchers); - - /** - * Creates standard click event on DOM element - */ - window.click = (elem) => { - const evt = document.createEvent('MouseEvent'); - evt.initMouseEvent('click', { - bubbles: true, - cancelable: true, - view: window - }); - elem.dispatchEvent(evt); - }; - - window.mouseenter = (el) => { - const ev = document.createEvent('MouseEvent'); - ev.initMouseEvent( - 'mouseenter', - true /* bubble */, - true /* cancelable */, - window, - null, - 0, - 0, - 0, - 0 /* coordinates */, - false, - false, - false, - false /* modifier keys */, - 0 /*left*/, - null - ); - el.dispatchEvent(ev); - }; - - window.mouseleave = (el) => { - const ev = document.createEvent('MouseEvent'); - ev.initMouseEvent( - 'mouseleave', - true /* bubble */, - true /* cancelable */, - window, - null, - 0, - 0, - 0, - 0 /* coordinates */, - false, - false, - false, - false /* modifier keys */, - 0 /*left*/, - null - ); - el.dispatchEvent(ev); - }; - - window.keydown = (targetElement, keycode) => { - targetElement.dispatchEvent( - new KeyboardEvent('keydown', { - key: KEYMAP[keycode], - keyCode: keycode, - which: keycode - }) - ); - }; - - window.keyup = (targetElement, keycode) => { - targetElement.dispatchEvent( - new KeyboardEvent('keyup', { - key: KEYMAP[keycode], - keyCode: keycode, - which: keycode - }) - ); - }; - - window.focus = (el) => { - const ev = document.createEvent('Events'); - ev.initEvent('focus', true, true); - el.dispatchEvent(ev); - }; - - window.blur = (el) => { - const ev = document.createEvent('Events'); - ev.initEvent('blur', true, true); - el.dispatchEvent(ev); - }; -}); diff --git a/spec/scrollspySpec.js b/src/scrollspySpec.js similarity index 100% rename from spec/scrollspySpec.js rename to src/scrollspySpec.js diff --git a/src/waves.js b/src/waves.js index 59076986d9..d7736120f2 100644 --- a/src/waves.js +++ b/src/waves.js @@ -1,14 +1,3 @@ -// type RGBColor = { -// r: number; -// g: number; -// b: number; -// }; - -// type Position = { -// x: number; -// y: number; -// }; - class Waves { /** * From db045b119da448353a6f5663b9e9f4f0b0595b93 Mon Sep 17 00:00:00 2001 From: Daniel Wurzer Date: Wed, 12 Aug 2026 10:00:32 +0200 Subject: [PATCH 02/16] test(tooltip): update to vitest --- .../{carouselSpec.js => ___carouselSpec.js} | 0 .../{sliderSpec.js => ___sliderSpec.js} | 0 components/chip/chip.test.ts | 12 +- ...datepickerSpec.js => ___datepickerSpec.js} | 0 ...terialboxSpec.js => ___materialboxSpec.js} | 0 .../dialog/{modalSpec.js => ___modalSpec.js} | 0 components/dialog/readme.md | 4 +- .../{dropdownSpec.js => ___dropdownSpec.js} | 0 .../{sidenavSpec.js => ___sidenavSpec.js} | 0 ...completeSpec.js => ___autocompleteSpec.js} | 0 .../{toastSpec.js => ___toastSpec.js} | 0 .../tabs/{tabsSpec.js => ___tabsSpec.js} | 0 .../{formsSpec.js => ___formsSpec.js} | 0 .../{selectSpec.js => ___selectSpec.js} | 0 components/tooltip/tooltip.css | 56 +++ components/tooltip/tooltip.test.ts | 129 +++++++ components/tooltip/tooltip.ts | 354 ++++-------------- components/tooltip/tooltipSpec.js | 145 ------- 18 files changed, 265 insertions(+), 435 deletions(-) rename components/carousel/{carouselSpec.js => ___carouselSpec.js} (100%) rename components/carousel/{sliderSpec.js => ___sliderSpec.js} (100%) rename components/datepicker/{datepickerSpec.js => ___datepickerSpec.js} (100%) rename components/dialog/{materialboxSpec.js => ___materialboxSpec.js} (100%) rename components/dialog/{modalSpec.js => ___modalSpec.js} (100%) rename components/dropdown/{dropdownSpec.js => ___dropdownSpec.js} (100%) rename components/navigation-drawer/{sidenavSpec.js => ___sidenavSpec.js} (100%) rename components/search/{autocompleteSpec.js => ___autocompleteSpec.js} (100%) rename components/snackbar/{toastSpec.js => ___toastSpec.js} (100%) rename components/tabs/{tabsSpec.js => ___tabsSpec.js} (100%) rename components/textfield/{formsSpec.js => ___formsSpec.js} (100%) rename components/textfield/{selectSpec.js => ___selectSpec.js} (100%) create mode 100644 components/tooltip/tooltip.css create mode 100644 components/tooltip/tooltip.test.ts delete mode 100644 components/tooltip/tooltipSpec.js diff --git a/components/carousel/carouselSpec.js b/components/carousel/___carouselSpec.js similarity index 100% rename from components/carousel/carouselSpec.js rename to components/carousel/___carouselSpec.js diff --git a/components/carousel/sliderSpec.js b/components/carousel/___sliderSpec.js similarity index 100% rename from components/carousel/sliderSpec.js rename to components/carousel/___sliderSpec.js diff --git a/components/chip/chip.test.ts b/components/chip/chip.test.ts index 862a2c9cdd..d2a1755f17 100644 --- a/components/chip/chip.test.ts +++ b/components/chip/chip.test.ts @@ -155,24 +155,24 @@ describe('Chips', () => { it('should have working callbacks', async () => { chips = document.querySelector('.chips.input-field'); let chipWasAdded = false; - let chipAddedElem: any = null; + let chipAddedElem = null; let chipSelect = false; - let chipSelected: any = null; + let chipSelected = null; let chipDelete = false; - let chipDeleted: any = null; + let chipDeleted = null; (global.M as any).Chips.init(chips, { allowUserInput: true, data: [{ id: 'One' }, { id: 'Two' }, { id: 'Three' }], - onChipAdd: (_chipsEl: any, chipEl: any) => { + onChipAdd: (_chipsEl, chipEl) => { chipAddedElem = chipEl; chipWasAdded = true; }, - onChipSelect: (_chipsEl: any, chipEl: any) => { + onChipSelect: (_chipsEl, chipEl) => { chipSelected = chipEl; chipSelect = true; }, - onChipDelete: (_chipsEl: any, chipEl: any) => { + onChipDelete: (_chipsEl, chipEl) => { chipDeleted = chipEl; chipDelete = true; } diff --git a/components/datepicker/datepickerSpec.js b/components/datepicker/___datepickerSpec.js similarity index 100% rename from components/datepicker/datepickerSpec.js rename to components/datepicker/___datepickerSpec.js diff --git a/components/dialog/materialboxSpec.js b/components/dialog/___materialboxSpec.js similarity index 100% rename from components/dialog/materialboxSpec.js rename to components/dialog/___materialboxSpec.js diff --git a/components/dialog/modalSpec.js b/components/dialog/___modalSpec.js similarity index 100% rename from components/dialog/modalSpec.js rename to components/dialog/___modalSpec.js diff --git a/components/dialog/readme.md b/components/dialog/readme.md index 0f8badda10..afbc1ab6db 100644 --- a/components/dialog/readme.md +++ b/components/dialog/readme.md @@ -1,5 +1,5 @@ # Dialog -*THIS SECTION IS FOR DEMONSTRATION ONLY* +TODO: Use the native html popover attribute and CSS only for this component. -Also called Modal. \ No newline at end of file +- [ ] Make a builder component diff --git a/components/dropdown/dropdownSpec.js b/components/dropdown/___dropdownSpec.js similarity index 100% rename from components/dropdown/dropdownSpec.js rename to components/dropdown/___dropdownSpec.js diff --git a/components/navigation-drawer/sidenavSpec.js b/components/navigation-drawer/___sidenavSpec.js similarity index 100% rename from components/navigation-drawer/sidenavSpec.js rename to components/navigation-drawer/___sidenavSpec.js diff --git a/components/search/autocompleteSpec.js b/components/search/___autocompleteSpec.js similarity index 100% rename from components/search/autocompleteSpec.js rename to components/search/___autocompleteSpec.js diff --git a/components/snackbar/toastSpec.js b/components/snackbar/___toastSpec.js similarity index 100% rename from components/snackbar/toastSpec.js rename to components/snackbar/___toastSpec.js diff --git a/components/tabs/tabsSpec.js b/components/tabs/___tabsSpec.js similarity index 100% rename from components/tabs/tabsSpec.js rename to components/tabs/___tabsSpec.js diff --git a/components/textfield/formsSpec.js b/components/textfield/___formsSpec.js similarity index 100% rename from components/textfield/formsSpec.js rename to components/textfield/___formsSpec.js diff --git a/components/textfield/selectSpec.js b/components/textfield/___selectSpec.js similarity index 100% rename from components/textfield/selectSpec.js rename to components/textfield/___selectSpec.js diff --git a/components/tooltip/tooltip.css b/components/tooltip/tooltip.css new file mode 100644 index 0000000000..9bb29ca7bd --- /dev/null +++ b/components/tooltip/tooltip.css @@ -0,0 +1,56 @@ +/* Reset popover default browser styles */ +.material-tooltip[popover] { + margin: 0; + border: none; + padding: 6px 10px; + background: #323232; + color: #fff; + border-radius: 4px; + font-size: 12px; + + /* Modern CSS Anchor Positioning */ + position: fixed; + position-fallback: --tooltip-fallback; + + /* Transition/Animation Handling */ + opacity: 0; + transition: + opacity 0.2s ease-out, + display 0.2s allow-discrete; +} + +/* Style for open state */ +.material-tooltip[popover]:popover-open { + opacity: 1; +} + +@starting-style { + .material-tooltip[popover]:popover-open { + opacity: 0; + } +} + +/* Position mapping using anchor positioning */ +.material-tooltip[data-position='top'] { + bottom: anchor(top); + justify-self: anchor-center; + margin-bottom: 5px; +} + +.material-tooltip[data-position='bottom'] { + top: anchor(bottom); + justify-self: anchor-center; + margin-top: 5px; +} + +.material-tooltip[data-position='left'] { + right: anchor(left); + align-self: anchor-center; + margin-right: 5px; +} + +.material-tooltip[data-position='right'] { + left: anchor(right); + align-self: anchor-center; + margin-left: 5px; +} diff --git a/components/tooltip/tooltip.test.ts b/components/tooltip/tooltip.test.ts new file mode 100644 index 0000000000..75ddd54dcf --- /dev/null +++ b/components/tooltip/tooltip.test.ts @@ -0,0 +1,129 @@ +// @vitest-environment happy-dom +import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; +import { Tooltip } from './tooltip'; + +const fixture = ` + + Hover me! + + + + Hover me! + + + +`; + +describe('Tooltip (Popover API)', () => { + beforeEach(() => { + // Setup fake timers for delay assertions + vi.useFakeTimers(); + + // Polyfill popover methods if Happy-DOM environment does not support native Popover API yet + if (!HTMLElement.prototype.showPopover) { + HTMLElement.prototype.showPopover = vi.fn(function (this: HTMLElement) { + this.setAttribute('popover-open', ''); + this.matches = (selector: string) => + selector === ':popover-open' || selector === '[popover]'; + }); + HTMLElement.prototype.hidePopover = vi.fn(function (this: HTMLElement) { + this.removeAttribute('popover-open'); + this.matches = (selector: string) => selector === '[popover]'; + }); + } + + // Set up document fixture + document.body.innerHTML = fixture; + + // Initialize tooltips + Tooltip.init(document.querySelectorAll('.tooltipped'), { + enterDelay: 0, + exitDelay: 0 + }); + }); + + afterEach(() => { + document.body.innerHTML = ''; + vi.restoreAllMocks(); + vi.useRealTimers(); + }); + + describe('opens and closes properly', () => { + it('shows tooltip on mouse enter and hides on mouse leave', () => { + const tooltippedBtn = document.querySelector('#test')!; + const instance = Tooltip.getInstance(tooltippedBtn); + const tooltip = instance.tooltipEl; + + // Simulate mouse enter + tooltippedBtn.dispatchEvent(new Event('mouseenter')); + vi.advanceTimersByTime(10); + + // Verify content and open state + expect(instance.isOpen).toBe(true); + expect(tooltip.textContent).toBe('I am tooltip'); + + // Simulate mouse leave + tooltippedBtn.dispatchEvent(new Event('mouseleave')); + vi.advanceTimersByTime(10); + + expect(instance.isOpen).toBe(false); + }); + + it('removes tooltip DOM object on destroy', () => { + const tooltippedBtn = document.querySelector('#test1')!; + const instance = Tooltip.getInstance(tooltippedBtn); + const tooltipEl = instance.tooltipEl; + + instance.destroy(); + + // Check DOM cleanup & instance ref clearance + expect(Tooltip.getInstance(tooltippedBtn)).toBeUndefined(); + expect(document.body.contains(tooltipEl)).toBe(false); + }); + + it('changes position attribute dynamically', () => { + const tooltippedBtn = document.querySelector('#test')!; + tooltippedBtn.setAttribute('data-position', 'right'); + + const instance = Tooltip.getInstance(tooltippedBtn); + + tooltippedBtn.dispatchEvent(new Event('mouseenter')); + vi.advanceTimersByTime(10); + + expect(instance.tooltipEl.dataset.position).toBe('right'); + }); + + it('accepts delay option from javascript initialization', () => { + const tooltippedBtn = document.querySelector('#test')!; + tooltippedBtn.removeAttribute('data-delay'); + + // Re-initialize with a custom enterDelay + const instance = Tooltip.init(tooltippedBtn, { enterDelay: 200 }); + + tooltippedBtn.dispatchEvent(new Event('mouseenter')); + + // Check before delay completes + vi.advanceTimersByTime(150); + expect(instance.isOpen).toBe(false); + + // Check after delay passes + vi.advanceTimersByTime(100); + expect(instance.isOpen).toBe(true); + }); + + it('assigns correctly with a fixed position parent', () => { + const tooltippedBtn = document.querySelector('#test2')!; + const instance = Tooltip.getInstance(tooltippedBtn); + + tooltippedBtn.dispatchEvent(new Event('mouseenter')); + vi.advanceTimersByTime(10); + + expect(instance.isOpen).toBe(true); + expect(instance.tooltipEl.dataset.position).toBe('bottom'); + }); + }); +}); diff --git a/components/tooltip/tooltip.ts b/components/tooltip/tooltip.ts index dab16f88c8..7a05e74dec 100644 --- a/components/tooltip/tooltip.ts +++ b/components/tooltip/tooltip.ts @@ -1,101 +1,35 @@ -import { Utils } from '../../src/utils'; import { Component, BaseOptions, InitElements, MElement } from '../../src/component'; -// TODO: Rewrite this using popover and js only - -class Bounding { - left: number; // left offset coordinate - top: number; - width: number; - height: number; -} - export type TooltipPosition = 'top' | 'right' | 'bottom' | 'left'; export interface TooltipOptions extends BaseOptions { - /** - * Delay time before tooltip disappears. - * @default 200 - */ + /** Delay before tooltip disappears (ms) */ exitDelay: number; - /** - * Delay time before tooltip appears. - * @default 0 - */ + /** Delay before tooltip appears (ms) */ enterDelay: number; - /** - * Element Id for the tooltip. - * @default "" - */ + /** Custom element ID for existing content */ tooltipId?: string; - /** - * Text string for the tooltip. - * @default "" - */ + /** Text content for the tooltip */ text: string; - /** - * Set distance tooltip appears away from its activator - * excluding transitionMovement. - * @default 5 - */ - margin: number; - /** - * Enter transition duration. - * @default 300 - */ - inDuration: number; - /** - * Opacity of the tooltip. - * @default 1 - */ - opacity: number; - /** - * Exit transition duration. - * @default 250 - */ - outDuration: number; - /** - * Set the direction of the tooltip. - * @default 'bottom' - */ + /** Position relative to the anchor */ position: TooltipPosition; - /** - * Amount in px that the tooltip moves during its transition. - * @default 10 - */ - transitionMovement: number; } const _defaults: TooltipOptions = { exitDelay: 200, enterDelay: 0, text: '', - margin: 5, - inDuration: 250, - outDuration: 200, - position: 'bottom' as TooltipPosition, - transitionMovement: 10, - opacity: 1 + position: 'bottom' }; export class Tooltip extends Component { - /** - * If tooltip is open. - */ - isOpen: boolean; - /** - * If tooltip is hovered. - */ - isHovered: boolean; - /** - * If tooltip is focused. - */ - isFocused: boolean; + isOpen: boolean = false; + isHovered: boolean = false; + isFocused: boolean = false; + tooltipEl: HTMLElement; - private _exitDelayTimeout: string | number | NodeJS.Timeout; - private _enterDelayTimeout: string | number | NodeJS.Timeout; - xMovement: number; - yMovement: number; + private _enterTimeout: number | NodeJS.Timeout; + private _exitTimeout: number | NodeJS.Timeout; constructor(el: HTMLElement, options: Partial) { super(el, options, Tooltip); @@ -107,10 +41,7 @@ export class Tooltip extends Component { ...options }; - this.isOpen = false; - this.isHovered = false; - this.isFocused = false; - this._appendTooltipEl(); + this._setupPopoverElement(); this._setupEventHandlers(); } @@ -118,23 +49,8 @@ export class Tooltip extends Component { return _defaults; } - /** - * Initializes instance of Tooltip. - * @param el HTML element. - * @param options Component options. - */ static init(el: HTMLElement, options?: Partial): Tooltip; - /** - * Initializes instances of Tooltip. - * @param els HTML elements. - * @param options Component options. - */ static init(els: InitElements, options?: Partial): Tooltip[]; - /** - * Initializes instances of Tooltip. - * @param els HTML elements. - * @param options Component options. - */ static init( els: HTMLElement | InitElements, options: Partial = {} @@ -149,237 +65,111 @@ export class Tooltip extends Component { destroy() { this.tooltipEl.remove(); this._removeEventHandlers(); - this.el['M_Tooltip'] = undefined; + delete this.el['M_Tooltip']; } - _appendTooltipEl() { - this.tooltipEl = document.createElement('div'); - this.tooltipEl.classList.add('material-tooltip'); + private _setupPopoverElement() { + if (this.options.tooltipId) { + this.tooltipEl = document.getElementById(this.options.tooltipId); + } else { + this.tooltipEl = document.createElement('div'); + this.tooltipEl.className = 'material-tooltip'; + this.tooltipEl.textContent = this.options.text; + document.body.appendChild(this.tooltipEl); + } - const tooltipContentEl = this.options.tooltipId - ? document.getElementById(this.options.tooltipId) - : document.createElement('div'); - this.tooltipEl.append(tooltipContentEl); - tooltipContentEl.style.display = ''; + // Set native popover attributes + this.tooltipEl.popover = 'manual'; - tooltipContentEl.classList.add('tooltip-content'); - this._setTooltipContent(tooltipContentEl); - this.tooltipEl.appendChild(tooltipContentEl); - document.body.appendChild(this.tooltipEl); - } - - _setTooltipContent(tooltipContentEl: HTMLElement) { - if (this.options.tooltipId) return; - tooltipContentEl.innerText = this.options.text; - } + // Assign unique anchor name linking the element and popover + const anchorName = `--tooltip-anchor-${Math.random().toString(36).substring(2, 9)}`; + (this.el.style as any).anchorName = anchorName; + (this.tooltipEl.style as any).positionAnchor = anchorName; - _updateTooltipContent() { - this._setTooltipContent(this.tooltipEl.querySelector('.tooltip-content')); + // Set default position class/data-attribute for CSS positioning + this.tooltipEl.dataset.position = this.options.position; } - _setupEventHandlers() { + private _setupEventHandlers() { this.el.addEventListener('mouseenter', this._handleMouseEnter); this.el.addEventListener('mouseleave', this._handleMouseLeave); - this.el.addEventListener('focus', this._handleFocus, true); - this.el.addEventListener('blur', this._handleBlur, true); + this.el.addEventListener('focus', this._handleFocus); + this.el.addEventListener('blur', this._handleBlur); } - _removeEventHandlers() { + private _removeEventHandlers() { this.el.removeEventListener('mouseenter', this._handleMouseEnter); this.el.removeEventListener('mouseleave', this._handleMouseLeave); - this.el.removeEventListener('focus', this._handleFocus, true); - this.el.removeEventListener('blur', this._handleBlur, true); + this.el.removeEventListener('focus', this._handleFocus); + this.el.removeEventListener('blur', this._handleBlur); } - /** - * Show tooltip. - */ - open = (isManual: boolean) => { + open = (isManual = true) => { if (this.isOpen) return; - isManual = isManual === undefined ? true : undefined; // Default value true - this.isOpen = true; - // Update tooltip content with HTML attribute options + + clearTimeout(this._exitTimeout); this.options = { ...this.options, ...this._getAttributeOptions() }; - this._updateTooltipContent(); - this._setEnterDelayTimeout(isManual); + this._updateContent(); + + this._enterTimeout = setTimeout(() => { + if (!isManual && !this.isHovered && !this.isFocused) return; + + this.tooltipEl.showPopover(); + this.isOpen = true; + }, this.options.enterDelay); }; - /** - * Hide tooltip. - */ close = () => { if (!this.isOpen) return; + + clearTimeout(this._enterTimeout); this.isHovered = false; this.isFocused = false; - this.isOpen = false; - this._setExitDelayTimeout(); - }; - _setExitDelayTimeout() { - clearTimeout(this._exitDelayTimeout); - this._exitDelayTimeout = setTimeout(() => { + this._exitTimeout = setTimeout(() => { if (this.isHovered || this.isFocused) return; - this._animateOut(); - }, this.options.exitDelay); - } - - _setEnterDelayTimeout(isManual) { - clearTimeout(this._enterDelayTimeout); - this._enterDelayTimeout = setTimeout(() => { - if (!this.isHovered && !this.isFocused && !isManual) return; - this._animateIn(); - }, this.options.enterDelay); - } - _positionTooltip() { - const tooltip: HTMLElement = this.tooltipEl; - const origin = this.el as HTMLElement, - originHeight = origin.offsetHeight, - originWidth = origin.offsetWidth, - tooltipHeight = tooltip.offsetHeight, - tooltipWidth = tooltip.offsetWidth, - margin = this.options.margin; - - this.xMovement = 0; - this.yMovement = 0; - - let targetTop = origin.getBoundingClientRect().top + Utils.getDocumentScrollTop(); - let targetLeft = origin.getBoundingClientRect().left + Utils.getDocumentScrollLeft(); - if (this.options.position === 'top') { - targetTop += -tooltipHeight - margin; - targetLeft += originWidth / 2 - tooltipWidth / 2; - this.yMovement = -this.options.transitionMovement; - } else if (this.options.position === 'right') { - targetTop += originHeight / 2 - tooltipHeight / 2; - targetLeft += originWidth + margin; - this.xMovement = this.options.transitionMovement; - } else if (this.options.position === 'left') { - targetTop += originHeight / 2 - tooltipHeight / 2; - targetLeft += -tooltipWidth - margin; - this.xMovement = -this.options.transitionMovement; - } else { - targetTop += originHeight + margin; - targetLeft += originWidth / 2 - tooltipWidth / 2; - this.yMovement = this.options.transitionMovement; - } - - const newCoordinates = this._repositionWithinScreen( - targetLeft, - targetTop, - tooltipWidth, - tooltipHeight - ); - - tooltip.style.top = newCoordinates.y + 'px'; - tooltip.style.left = newCoordinates.x + 'px'; - } - - _repositionWithinScreen(x: number, y: number, width: number, height: number) { - const scrollLeft = Utils.getDocumentScrollLeft(); - const scrollTop = Utils.getDocumentScrollTop(); - let newX = x - scrollLeft; - let newY = y - scrollTop; - - const bounding: Bounding = { - left: newX, - top: newY, - width: width, - height: height - }; - const offset = this.options.margin + this.options.transitionMovement; - const edges = Utils.checkWithinContainer(document.body, bounding, offset); + this.tooltipEl.hidePopover(); + this.isOpen = false; + }, this.options.exitDelay); + }; - if (edges.left) { - newX = offset; - } else if (edges.right) { - newX -= newX + width - window.innerWidth; + private _updateContent() { + if (!this.options.tooltipId) { + this.tooltipEl.textContent = this.options.text; } - if (edges.top) { - newY = offset; - } else if (edges.bottom) { - newY -= newY + height - window.innerHeight; - } - return { - x: newX + scrollLeft, - y: newY + scrollTop - }; + this.tooltipEl.dataset.position = this.options.position; } - _animateIn() { - this._positionTooltip(); - this.tooltipEl.style.visibility = 'visible'; - const duration = this.options.inDuration; - // easeOutCubic - this.tooltipEl.style.transition = ` - transform ${duration}ms ease-out, - opacity ${duration}ms ease-out`; - setTimeout(() => { - this.tooltipEl.style.transform = `translateX(${this.xMovement}px) translateY(${this.yMovement}px)`; - this.tooltipEl.style.opacity = (this.options.opacity || 1).toString(); - }, 1); - } - - _animateOut() { - const duration = this.options.outDuration; - // easeOutCubic - this.tooltipEl.style.transition = ` - transform ${duration}ms ease-out, - opacity ${duration}ms ease-out`; - setTimeout(() => { - this.tooltipEl.style.transform = `translateX(0px) translateY(0px)`; - this.tooltipEl.style.opacity = '0'; - }, 1); - /* - anim.remove(this.tooltipEl); - anim({ - targets: this.tooltipEl, - opacity: 0, - translateX: 0, - translateY: 0, - duration: this.options.outDuration, - easing: 'easeOutCubic' - }); - */ - } - - _handleMouseEnter = () => { + private _handleMouseEnter = () => { this.isHovered = true; - this.isFocused = false; // Allows close of tooltip when opened by focus. this.open(false); }; - _handleMouseLeave = () => { + private _handleMouseLeave = () => { this.isHovered = false; - this.isFocused = false; // Allows close of tooltip when opened by focus. this.close(); }; - _handleFocus = () => { - if (Utils.tabPressed) { - this.isFocused = true; - this.open(false); - } + private _handleFocus = () => { + this.isFocused = true; + this.open(false); }; - _handleBlur = () => { + private _handleBlur = () => { this.isFocused = false; this.close(); }; - _getAttributeOptions(): Partial { + private _getAttributeOptions(): Partial { const attributeOptions: Partial = {}; - const tooltipTextOption = this.el.getAttribute('data-tooltip'); + const text = this.el.getAttribute('data-tooltip'); + const position = this.el.getAttribute('data-position') as TooltipPosition; const tooltipId = this.el.getAttribute('data-tooltip-id'); - const positionOption = this.el.getAttribute('data-position'); - if (tooltipTextOption) { - attributeOptions.text = tooltipTextOption; - } - if (positionOption) { - attributeOptions.position = positionOption as TooltipPosition; - } - if (tooltipId) { - attributeOptions.tooltipId = tooltipId; - } + + if (text) attributeOptions.text = text; + if (position) attributeOptions.position = position; + if (tooltipId) attributeOptions.tooltipId = tooltipId; return attributeOptions; } diff --git a/components/tooltip/tooltipSpec.js b/components/tooltip/tooltipSpec.js deleted file mode 100644 index 5ed2697fde..0000000000 --- a/components/tooltip/tooltipSpec.js +++ /dev/null @@ -1,145 +0,0 @@ -describe('Tooltip:', () => { - const fixture = ` - Hover me! - - - - Hover me! - - -



















-



















-



















-



















- -`; - - beforeEach(() => { - XloadHtml(fixture); - M.Tooltip.init(document.querySelectorAll('.tooltipped'), { - enterDelay: 0, - exitDelay: 0, - inDuration: 0, - outDuration: 0 - }); - }); - afterEach(() => XunloadFixtures()); - - describe('opens and closes properly', () => { - it('shows tooltip on mouse enter', (done) => { - const tooltippedBtn = document.querySelector('#test'); - const tooltip = M.Tooltip.getInstance(tooltippedBtn).tooltipEl; - mouseenter(tooltippedBtn); - setTimeout(() => { - expect(tooltip).toBeVisible('because mouse entered tooltipped btn'); - expect(tooltip.querySelector('.tooltip-content').innerText).toBe( - 'I am tooltip', - 'because that is the defined text in the html attribute' - ); - mouseleave(tooltippedBtn); - setTimeout(() => { - expect(tooltip).toBeVisible('because mouse left tooltipped btn'); - done(); - }, 10); - }, 10); - }); - - it('should place tooltips on the bottom within the screen bounds', (done) => { - const tooltippedBtn = document.querySelector('#test1'); - const tooltip = M.Tooltip.getInstance(tooltippedBtn).tooltipEl; - mouseenter(tooltippedBtn); - // tooltippedBtn.trigger('mouseenter'); - setTimeout(() => { - const tooltipRect = tooltip.getBoundingClientRect(); - const tooltippedBtnRect = tooltippedBtn.getBoundingClientRect(); - // Check window bounds - expect(tooltipRect.top).toBeGreaterThanOrEqual(0); - expect(tooltipRect.bottom).toBeLessThanOrEqual(window.innerHeight); - expect(tooltipRect.left).toBeGreaterThanOrEqual(0); - expect(tooltipRect.right).toBeLessThanOrEqual(window.innerWidth); - // check if tooltip is under btn - expect(tooltipRect.top).toBeGreaterThan(tooltippedBtnRect.bottom); - done(); - }, 10); - }); - - it('removes tooltip dom object', () => { - const tooltippedBtn = document.querySelector('#test1'); - M.Tooltip.getInstance(tooltippedBtn).destroy(); - // Check DOM element is removed - const tooltipInstance = M.Tooltip.getInstance(tooltippedBtn); - expect(tooltipInstance).toBe(undefined); - }); - - it('changes position attribute dynamically and positions tooltips on the right correctly', (done) => { - const tooltippedBtn = document.querySelector('#test'); - tooltippedBtn.setAttribute('data-position', 'right'); - const tooltip = M.Tooltip.getInstance(tooltippedBtn).tooltipEl; - mouseenter(tooltippedBtn); - setTimeout(() => { - const tooltipRect = tooltip.getBoundingClientRect(); - const tooltippedBtnRect = tooltippedBtn.getBoundingClientRect(); - expect(tooltipRect.left).toBeGreaterThan(tooltippedBtnRect.right); - done(); - }, 10); - }); - - it('accepts delay option from javascript initialization', (done) => { - const tooltippedBtn = document.querySelector('#test'); - tooltippedBtn.removeAttribute('data-delay'); - M.Tooltip.init(tooltippedBtn, { enterDelay: 200 }); - const tooltip = M.Tooltip.getInstance(tooltippedBtn).tooltipEl; - mouseenter(tooltippedBtn); - setTimeout(() => { - const tooltipVisibility = getComputedStyle(tooltip).getPropertyValue('visibility'); - expect(tooltipVisibility).toBe('hidden', 'because the delay is 200 seconds'); - }, 150); - setTimeout(() => { - expect(tooltip).toBeVisible('because 200 seconds has passed'); - done(); - }, 250); - }); - - it('works with a fixed position parent', (done) => { - const tooltippedBtn = document.querySelector('#test2'); - const tooltip = M.Tooltip.getInstance(tooltippedBtn).tooltipEl; - mouseenter(tooltippedBtn); - setTimeout(() => { - const tooltipRect = tooltip.getBoundingClientRect(); - const tooltippedBtnRect = tooltippedBtn.getBoundingClientRect(); - const verticalDiff = tooltipRect.top - tooltippedBtnRect.top; - const horizontalDiff = - tooltipRect.left + - tooltipRect.width / 2 - - (tooltippedBtnRect.left + tooltippedBtnRect.width / 2); - // 52 is magic number for tooltip vertical offset... increased to 100 - expect(verticalDiff > 0 && verticalDiff < 100).toBeTruthy( - 'top position in fixed to be correct' - ); - expect(horizontalDiff > -1 && horizontalDiff < 1).toBeTruthy( - 'left position in fixed to be correct' - ); - done(); - }, 10); - }); - }); -}); From 7c24caf1a182a9fff5833262a744a497bc3f36f3 Mon Sep 17 00:00:00 2001 From: Daniel Wurzer Date: Wed, 12 Aug 2026 10:28:43 +0200 Subject: [PATCH 03/16] test(tabs): convert to vitest --- components/tabs/___tabsSpec.js | 180 ---------------- components/tabs/tabs.test.ts | 195 ++++++++++++++++++ components/tabs/tabs.ts | 161 +++++++-------- .../{_tooltip.scss => ___del_tooltip.scss} | 0 package.json | 5 +- sass/materialize.scss | 2 +- 6 files changed, 274 insertions(+), 269 deletions(-) delete mode 100644 components/tabs/___tabsSpec.js create mode 100644 components/tabs/tabs.test.ts rename components/tooltip/{_tooltip.scss => ___del_tooltip.scss} (100%) diff --git a/components/tabs/___tabsSpec.js b/components/tabs/___tabsSpec.js deleted file mode 100644 index 93427b6a80..0000000000 --- a/components/tabs/___tabsSpec.js +++ /dev/null @@ -1,180 +0,0 @@ -describe('Tabs Plugin', () => { - const fixture = `
- -
Test 1
-
Test 2
-
Test 3
-
Test 4
-
Test 1
-
Test 2
-
Test 3
-
Test 4
-
`; - - beforeEach(() => { - XloadHtml(fixture); - const normalTabs = document.querySelector('.tabs.normal'); - M.Tabs.init(normalTabs, { duration: 0 }); - window.location.hash = ''; - //HACK the tabs init function not fully initializing. it restores state even after element has been removed from DOM, even after using tabInstance.destroy() - M.Tabs.getInstance(normalTabs).select('test2'); - }); - afterEach(() => XunloadFixtures()); - - describe('Tabs', () => { - it('should open to active tab', () => { - const normalTabs = document.querySelector('.tabs.normal'); - const activeTab = normalTabs.querySelector('.active'); - const activeTabHash = activeTab.getAttribute('href'); - const tabLinks = normalTabs.querySelectorAll('.tab a'); - for (let i = 0; i < tabLinks.length; i++) { - const tabHash = tabLinks[i].getAttribute('href'); - if (tabHash === activeTabHash) { - expect(document.querySelector(tabHash)).toBeVisible( - 'active tab content should be visible by default' - ); //TODO replace with alternative for deprecated jasmine-jquery - } else { - expect(document.querySelector(tabHash)).toBeHidden( - 'Tab content should be hidden by default' - ); //TODO replace with alternative for deprecated jasmine-jquery - } - } - const indicator = normalTabs.querySelector('.indicator'); - expect(indicator).toExist('Indicator should be generated'); - // expect(Math.abs(indicator.offset().left - activeTab.offset().left)).toBeLessThan(1, 'Indicator should be at active tab by default.'); - }); - - it('should switch to clicked tab', (done) => { - const normalTabs = document.querySelector('.tabs.normal'); - const activeTab = normalTabs.querySelector('.active'); - const activeTabHash = activeTab.getAttribute('href'); - const disabledTab = normalTabs.querySelector('.disabled a'); - const disabledTabHash = disabledTab.getAttribute('href'); - const firstTab = normalTabs.querySelector('.tab a'); - const firstTabHash = firstTab.getAttribute('href'); - const indicator = normalTabs.querySelector('.indicator'); - expect(indicator).toExist('Indicator should be generated'); - // expect(Math.abs(indicator.offset().left - activeTab.offset().left)).toBeLessThan(1, 'Indicator should be at active tab by default.'); - click(disabledTab); - setTimeout(() => { - expect(document.querySelector(activeTabHash)).toBeVisible( - 'Clicking disabled should not change tabs.' - ); //TODO replace with alternative for deprecated jasmine-jquery - expect(document.querySelector(disabledTabHash)).toBeHidden( - 'Clicking disabled should not change tabs.' - ); //TODO replace with alternative for deprecated jasmine-jquery - - click(firstTab); - - setTimeout(() => { - expect(document.querySelector(activeTabHash)).toBeHidden( - 'Clicking tab should switch to that tab.' - ); //TODO replace with alternative for deprecated jasmine-jquery - expect(document.querySelector(firstTabHash)).toBeVisible( - 'Clicking tab should switch to that tab.' - ); //TODO replace with alternative for deprecated jasmine-jquery - expect(indicator.offsetLeft).toEqual( - firstTab.offsetLeft, - 'Indicator should move to clicked tab.' - ); - done(); - }, 10); // 400 - }, 10); // 400 - }); - - it("shouldn't hide active tab if clicked while active", (done) => { - const normalTabs = document.querySelector('.tabs.normal'); - const activeTab = normalTabs.querySelector('.active'); - const activeTabHash = activeTab.getAttribute('href'); - const indicator = normalTabs.querySelector('.indicator'); - expect(indicator).toExist('Indicator should be generated'); - click(activeTab); - setTimeout(() => { - expect(document.querySelector(activeTabHash)).toBeVisible( - 'Clicking active tab while active should not hide it.' - ); - done(); - }, 5); // 400 - }); - - it('should horizontally scroll when too many tabs', (done) => { - let tabsScrollWidth = 0; - const normalTabs = document.querySelector('.tabs.normal'); - normalTabs.style.width = '400px'; - const tabs = normalTabs.querySelectorAll('.tab'); - for (let i = 0; i < tabs.length; i++) { - setTimeout(() => { - tabsScrollWidth += tabs[i].offsetWidth; - }, 0); - } - - setTimeout(() => { - expect(tabsScrollWidth).toBeGreaterThan( - normalTabs.offsetWidth, - 'Scroll width should exceed tabs width' - ); - done(); - }, 5); // 400 - }); - - it('should programmatically switch tabs', (done) => { - const normalTabs = document.querySelector('.tabs.normal'); - const activeTab = normalTabs.querySelector('.active'); - const activeTabHash = activeTab.getAttribute('href'); - const firstTab = normalTabs.querySelector('li a'); - const firstTabHash = firstTab.getAttribute('href'); - const indicator = normalTabs.querySelector('.indicator'); - const tabs = normalTabs.querySelectorAll('.tab a'); - for (let i = 0; i < tabs.length; i++) { - const tabHash = tabs[i].getAttribute('href'); - if (tabHash === activeTabHash) { - expect(document.querySelector(tabHash)).toBeVisible( - 'active tab content should be visible by default' - ); //TODO replace with alternative for deprecated jasmine-jquery - } else { - expect(document.querySelector(tabHash)).toBeHidden( - 'Tab content should be hidden by default' - ); //TODO replace with alternative for deprecated jasmine-jquery - } - } - - M.Tabs.getInstance(normalTabs).select('test1'); - - setTimeout(() => { - expect(document.querySelector(activeTabHash)).toBeHidden( - 'Clicking tab should switch to that tab.' - ); //TODO replace with alternative for deprecated jasmine-jquery - expect(document.querySelector(firstTabHash)).toBeVisible( - 'Clicking tab should switch to that tab.' - ); //TODO replace with alternative for deprecated jasmine-jquery - expect(indicator.offsetLeft).toEqual( - firstTab.offsetLeft, - 'Indicator should move to clicked tab.' - ); - done(); - }, 5); // 400 - }); - - it("shouldn't error if tab has no associated content", (done) => { - document.querySelector('#test8').remove(); - const tabNoContent = document.querySelector('[href="#test8"]'); - expect(tabNoContent).toNotHaveClass('active', 'Tab should not be selected'); - click(tabNoContent); - setTimeout(() => { - expect(tabNoContent).toHaveClass('active', 'Tab should be selected even with no content'); - done(); - }, 10); // 400 - }); - }); -}); diff --git a/components/tabs/tabs.test.ts b/components/tabs/tabs.test.ts new file mode 100644 index 0000000000..c19ce35a16 --- /dev/null +++ b/components/tabs/tabs.test.ts @@ -0,0 +1,195 @@ +// @vitest-environment happy-dom +import { describe, beforeEach, afterEach, it, expect, vi } from 'vitest'; +import { Tabs } from './tabs.ts'; + +// Helper utility for async delays +const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); + +describe('Tabs Plugin', () => { + const fixture = `
+ +
Test 1
+
Test 2
+
Test 3
+
Test 4
+
Test 5
+
Test 6
+
Test 7
+
Test 8
+
`; + + let container: HTMLDivElement; + + beforeEach(() => { + // Mount fixture + container = document.createElement('div'); + container.innerHTML = fixture; + document.body.appendChild(container); + + const normalTabs = document.querySelector('.tabs.normal')!; + Tabs.init(normalTabs, { duration: 0 }); + window.location.hash = ''; + + // Force selection of test2 as active tab + Tabs.getInstance(normalTabs)?.select('test2'); + }); + + afterEach(() => { + const normalTabs = document.querySelector('.tabs.normal'); + if (normalTabs) { + Tabs.getInstance(normalTabs)?.destroy(); + } + document.body.innerHTML = ''; + }); + + describe('Tabs', () => { + it('should open to active tab', () => { + const normalTabs = document.querySelector('.tabs.normal')!; + const activeTab = normalTabs.querySelector('.active')!; + const activeTabHash = activeTab.getAttribute('href')!; + const tabLinks = normalTabs.querySelectorAll('.tab a'); + + tabLinks.forEach((tabLink) => { + const tabHash = tabLink.getAttribute('href')!; + const contentEl = document.querySelector(tabHash)!; + + if (tabHash === activeTabHash) { + expect(contentEl.style.display).not.toBe('none'); + } else { + expect(contentEl.style.display).toBe('none'); + } + }); + + const indicator = normalTabs.querySelector('.indicator'); + expect(indicator).not.toBeNull(); + }); + + it('should switch to clicked tab', async () => { + const normalTabs = document.querySelector('.tabs.normal')!; + const activeTab = normalTabs.querySelector('.active')!; + const activeTabHash = activeTab.getAttribute('href')!; + const disabledTab = normalTabs.querySelector('.disabled a')!; + const disabledTabHash = disabledTab.getAttribute('href')!; + const firstTab = normalTabs.querySelector('.tab a')!; + const firstTabHash = firstTab.getAttribute('href')!; + const indicator = normalTabs.querySelector('.indicator')!; + + expect(indicator).not.toBeNull(); + + // Click disabled tab + disabledTab.click(); + await delay(10); + + const activeContent = document.querySelector(activeTabHash)!; + const disabledContent = document.querySelector(disabledTabHash)!; + + expect(activeContent.style.display).not.toBe('none'); + expect(disabledContent.style.display).toBe('none'); + + // Click first tab + firstTab.click(); + await delay(10); + + const firstContent = document.querySelector(firstTabHash)!; + + expect(activeContent.style.display).toBe('none'); + expect(firstContent.style.display).not.toBe('none'); + expect(indicator.offsetLeft).toBe(firstTab.offsetLeft); + }); + + it("shouldn't hide active tab if clicked while active", async () => { + const normalTabs = document.querySelector('.tabs.normal')!; + const activeTab = normalTabs.querySelector('.active')!; + const activeTabHash = activeTab.getAttribute('href')!; + const indicator = normalTabs.querySelector('.indicator'); + + expect(indicator).not.toBeNull(); + + activeTab.click(); + await delay(10); + + const activeContent = document.querySelector(activeTabHash)!; + expect(activeContent.style.display).not.toBe('none'); + }); + + it('should horizontally scroll when too many tabs', () => { + const normalTabs = document.querySelector('.tabs.normal')!; + const tabsInstance = Tabs.getInstance(normalTabs); + // Mock container visible width (400px) and total scroll width (800px) + vi.spyOn(normalTabs, 'getBoundingClientRect').mockReturnValue({ + width: 400, + height: 50, + top: 0, + left: 0, + bottom: 50, + right: 400, + x: 0, + y: 0, + toJSON: () => {} + }); + Object.defineProperty(normalTabs, 'scrollWidth', { + configurable: true, + value: 800 + }); + // Trigger recalculation + tabsInstance._setTabsAndTabWidth(); + // Verify internal width state + expect(normalTabs.scrollWidth).toBeGreaterThan(tabsInstance._tabsWidth); + expect(tabsInstance._tabWidth).toBe(100); // 800px scrollWidth / 8 tabs + }); + + it('should programmatically switch tabs', async () => { + const normalTabs = document.querySelector('.tabs.normal')!; + const activeTab = normalTabs.querySelector('.active')!; + const activeTabHash = activeTab.getAttribute('href')!; + const firstTab = normalTabs.querySelector('li a')!; + const firstTabHash = firstTab.getAttribute('href')!; + const indicator = normalTabs.querySelector('.indicator')!; + const tabs = normalTabs.querySelectorAll('.tab a'); + + tabs.forEach((tab) => { + const tabHash = tab.getAttribute('href')!; + const contentEl = document.querySelector(tabHash)!; + + if (tabHash === activeTabHash) { + expect(contentEl.style.display).not.toBe('none'); + } else { + expect(contentEl.style.display).toBe('none'); + } + }); + + Tabs.getInstance(normalTabs)?.select('test1'); + await delay(10); + + const activeContent = document.querySelector(activeTabHash)!; + const firstContent = document.querySelector(firstTabHash)!; + + expect(activeContent.style.display).toBe('none'); + expect(firstContent.style.display).not.toBe('none'); + expect(indicator.offsetLeft).toBe(firstTab.offsetLeft); + }); + + it("shouldn't error if tab has no associated content", async () => { + document.querySelector('#test8')?.remove(); + + const tabNoContent = document.querySelector('[href="#test8"]')!; + expect(tabNoContent.classList.contains('active')).toBe(false); + + tabNoContent.click(); + await delay(10); + + expect(tabNoContent.classList.contains('active')).toBe(true); + }); + }); +}); diff --git a/components/tabs/tabs.ts b/components/tabs/tabs.ts index 50af294804..2a89f4a7e4 100644 --- a/components/tabs/tabs.ts +++ b/components/tabs/tabs.ts @@ -21,7 +21,7 @@ interface TabsOptions extends BaseOptions { /** * The maximum width of the screen, in pixels, * where the swipeable functionality initializes. - * @default infinity + * @default Infinity */ responsiveThreshold: number; } @@ -30,7 +30,7 @@ const _defaults: TabsOptions = { duration: 300, onShow: null, swipeable: false, - responsiveThreshold: Infinity // breakpoint for swipeable + responsiveThreshold: Infinity }; class Tabs extends Component { @@ -55,11 +55,13 @@ class Tabs extends Component { this._tabLinks = this.el.querySelectorAll('li.tab > a'); this._index = 0; this._setupActiveTabLink(); + if (this.options.swipeable) { this._setupSwipeableTabs(); } else { this._setupNormalTabs(); } + // Setup tabs indicator after content to ensure accurate widths this._setTabsAndTabWidth(); this._createIndicator(); @@ -70,23 +72,8 @@ class Tabs extends Component { return _defaults; } - /** - * Initializes instance of Tabs. - * @param el HTML element. - * @param options Component options. - */ static init(el: HTMLElement, options?: Partial): Tabs; - /** - * Initializes instances of Tabs. - * @param els HTML elements. - * @param options Component options. - */ static init(els: InitElements, options?: Partial): Tabs[]; - /** - * Initializes instances of Tabs. - * @param els HTML elements. - * @param options Component options. - */ static init( els: HTMLElement | InitElements, options: Partial = {} @@ -100,7 +87,9 @@ class Tabs extends Component { destroy() { this._removeEventHandlers(); - this._indicator.parentNode.removeChild(this._indicator); + if (this._indicator?.parentNode) { + this._indicator.parentNode.removeChild(this._indicator); + } if (this.options.swipeable) { this._teardownSwipeableTabs(); } else { @@ -109,9 +98,6 @@ class Tabs extends Component { this.el['M_Tabs'] = undefined; } - /** - * The index of tab that is currently shown. - */ get index() { return this._index; } @@ -119,23 +105,25 @@ class Tabs extends Component { _setupEventHandlers() { window.addEventListener('resize', this._handleWindowResize); this.el.addEventListener('click', this._handleTabClick); - // map vertical scrolling to horizontal scrolling because no scrollbar is shown on desktop - this.el.addEventListener('wheel', (event) => { - if (event.deltaY !== 0) { - event.preventDefault(); - this.el.scrollLeft += event.deltaY * 1; - } - }); + this.el.addEventListener('wheel', this._handleWheel); } _removeEventHandlers() { window.removeEventListener('resize', this._handleWindowResize); this.el.removeEventListener('click', this._handleTabClick); + this.el.removeEventListener('wheel', this._handleWheel); } + _handleWheel = (event: WheelEvent) => { + if (event.deltaY !== 0) { + event.preventDefault(); + this.el.scrollLeft += event.deltaY; + } + }; + _handleWindowResize = () => { this._setTabsAndTabWidth(); - if (this._tabWidth !== 0 && this._tabsWidth !== 0) { + if (this._tabWidth !== 0 && this._tabsWidth !== 0 && this._activeTabLink) { this._indicator.style.left = this._calcLeftPos(this._activeTabLink) + 'px'; this._indicator.style.right = this._calcRightPos(this._activeTabLink) + 'px'; } @@ -151,49 +139,46 @@ class Tabs extends Component { tab = tab.parentElement; } - // Handle click on tab link only - if (!tabLink || !tab.classList.contains('tab')) return; - // is disabled? + if (!tabLink || !tab?.classList.contains('tab')) return; if (tab.classList.contains('disabled')) { e.preventDefault(); return; } - // Act as regular link if target attribute is specified. if (tabLink.hasAttribute('target')) return; - // Make the old tab inactive. - this._activeTabLink.classList.remove('active'); + + this._activeTabLink?.classList.remove('active'); const _oldContent = this._content; - // Update the variables with the new link and content this._activeTabLink = tabLink; if (tabLink.hash) this._content = document.querySelector(tabLink.hash); this._tabLinks = this.el.querySelectorAll('li.tab > a'); - // Make the tab active + this._activeTabLink.classList.add('active'); const prevIndex = this._index; this._index = Math.max(Array.from(this._tabLinks).indexOf(tabLink), 0); - // Swap content if (this.options.swipeable) { if (this._tabsCarousel) { this._tabsCarousel.set(this._index, () => { - if (typeof this.options.onShow === 'function') + if (typeof this.options.onShow === 'function') { this.options.onShow.call(this, this._content); + } }); } } else { if (this._content) { this._content.style.display = 'block'; this._content.classList.add('active'); - if (typeof this.options.onShow === 'function') + if (typeof this.options.onShow === 'function') { this.options.onShow.call(this, this._content); + } if (_oldContent && _oldContent !== this._content) { _oldContent.style.display = 'none'; _oldContent.classList.remove('active'); } } } - // Update widths after content is swapped (scrollbar bugfix) + this._setTabsAndTabWidth(); this._animateIndicator(prevIndex); e.preventDefault(); @@ -204,16 +189,17 @@ class Tabs extends Component { indicator.classList.add('indicator'); this.el.appendChild(indicator); this._indicator = indicator; - this._indicator.style.left = this._calcLeftPos(this._activeTabLink) + 'px'; - this._indicator.style.right = this._calcRightPos(this._activeTabLink) + 'px'; + if (this._activeTabLink) { + this._indicator.style.left = this._calcLeftPos(this._activeTabLink) + 'px'; + this._indicator.style.right = this._calcRightPos(this._activeTabLink) + 'px'; + } } _setupActiveTabLink() { - // If the location.hash matches one of the links, use that as the active tab. this._activeTabLink = Array.from(this._tabLinks).find( (a: HTMLAnchorElement) => a.getAttribute('href') === location.hash ); - // If no match is found, use the first link or any with class 'active' as the initial active tab. + if (!this._activeTabLink) { let activeTabLink = this.el.querySelector('li.tab a.active'); if (!activeTabLink) { @@ -221,43 +207,49 @@ class Tabs extends Component { } this._activeTabLink = activeTabLink as HTMLAnchorElement; } + Array.from(this._tabLinks).forEach((a: HTMLAnchorElement) => a.classList.remove('active')); - this._activeTabLink.classList.add('active'); - this._index = Math.max(Array.from(this._tabLinks).indexOf(this._activeTabLink), 0); - if (this._activeTabLink && this._activeTabLink.hash) { - this._content = document.querySelector(this._activeTabLink.hash); - if (this._content) this._content.classList.add('active'); + if (this._activeTabLink) { + this._activeTabLink.classList.add('active'); + this._index = Math.max(Array.from(this._tabLinks).indexOf(this._activeTabLink), 0); + if (this._activeTabLink.hash) { + this._content = document.querySelector(this._activeTabLink.hash); + if (this._content) this._content.classList.add('active'); + } } } _setupSwipeableTabs() { - // Change swipeable according to responsive threshold - if (window.innerWidth > this.options.responsiveThreshold) this.options.swipeable = false; + if (window.innerWidth > this.options.responsiveThreshold) { + this.options.swipeable = false; + return; + } - const tabsContent = []; + const tabsContent: HTMLElement[] = []; this._tabLinks.forEach((a) => { if (a.hash) { - const currContent = document.querySelector(a.hash); - currContent.classList.add('carousel-item'); - tabsContent.push(currContent); + const currContent = document.querySelector(a.hash) as HTMLElement; + if (currContent) { + currContent.classList.add('carousel-item'); + tabsContent.push(currContent); + } } }); - // Create Carousel-Wrapper around Tab-Contents + if (tabsContent.length === 0) return; + const tabsWrapper = document.createElement('div'); tabsWrapper.classList.add('tabs-content', 'carousel', 'carousel-slider'); - // Wrap around tabsContent[0].parentElement.insertBefore(tabsWrapper, tabsContent[0]); tabsContent.forEach((tabContent) => { tabsWrapper.appendChild(tabContent); tabContent.style.display = ''; }); - // Keep active tab index to set initial carousel slide - const tab = this._activeTabLink.parentElement; - const activeTabIndex = Array.from(tab.parentNode.children).indexOf(tab); + const tab = this._activeTabLink?.parentElement; + const activeTabIndex = tab ? Array.from(tab.parentNode.children).indexOf(tab) : 0; this._tabsCarousel = Carousel.init(tabsWrapper, { fullWidth: true, @@ -265,39 +257,42 @@ class Tabs extends Component { onCycleTo: (item) => { const prevIndex = this._index; this._index = Array.from(item.parentNode.children).indexOf(item); - this._activeTabLink.classList.remove('active'); + this._activeTabLink?.classList.remove('active'); this._activeTabLink = Array.from(this._tabLinks)[this._index]; - this._activeTabLink.classList.add('active'); + this._activeTabLink?.classList.add('active'); this._animateIndicator(prevIndex); - if (typeof this.options.onShow === 'function') + if (typeof this.options.onShow === 'function') { this.options.onShow.call(this, this._content); + } } }); - // Set initial carousel slide to active tab + this._tabsCarousel.set(activeTabIndex); } _teardownSwipeableTabs() { + if (!this._tabsCarousel) return; const tabsWrapper = this._tabsCarousel.el; this._tabsCarousel.destroy(); - // Unwrap - tabsWrapper.append(tabsWrapper.parentElement); + + // Move children back out to the parent container + while (tabsWrapper.firstChild) { + tabsWrapper.parentElement.insertBefore(tabsWrapper.firstChild, tabsWrapper); + } tabsWrapper.remove(); } _setupNormalTabs() { - // Hide Tabs Content Array.from(this._tabLinks).forEach((a) => { if (a === this._activeTabLink) return; - if ((a).hash) { - const currContent = document.querySelector((a).hash); - if (currContent) (currContent).style.display = 'none'; + if (a.hash) { + const currContent = document.querySelector(a.hash) as HTMLElement; + if (currContent) currContent.style.display = 'none'; } }); } _teardownNormalTabs() { - // show Tabs Content this._tabLinks.forEach((a) => { if (a.hash) { const currContent = document.querySelector(a.hash) as HTMLElement; @@ -308,7 +303,10 @@ class Tabs extends Component { _setTabsAndTabWidth() { this._tabsWidth = this.el.getBoundingClientRect().width; - this._tabWidth = Math.max(this._tabsWidth, this.el.scrollWidth) / this._tabLinks.length; + this._tabWidth = + this._tabLinks.length > 0 + ? Math.max(this._tabsWidth, this.el.scrollWidth) / this._tabLinks.length + : 0; } _calcRightPos(el: HTMLElement) { @@ -319,24 +317,21 @@ class Tabs extends Component { return Math.floor(el.offsetLeft); } - /** - * Recalculate tab indicator position. This is useful when - * the indicator position is not correct. - */ updateTabIndicator() { this._setTabsAndTabWidth(); this._animateIndicator(this._index); } _animateIndicator(prevIndex: number) { - let leftDelay = 0, - rightDelay = 0; + if (!this._indicator || !this._activeTabLink) return; + + let leftDelay = 0; + let rightDelay = 0; const isMovingLeftOrStaying = this._index - prevIndex >= 0; if (isMovingLeftOrStaying) leftDelay = 90; else rightDelay = 90; - // in v1: easeOutQuad this._indicator.style.transition = ` left ${this.options.duration}ms ease-out ${leftDelay}ms, right ${this.options.duration}ms ease-out ${rightDelay}ms`; @@ -345,15 +340,11 @@ class Tabs extends Component { this._indicator.style.right = this._calcRightPos(this._activeTabLink) + 'px'; } - /** - * Show tab content that corresponds to the tab with the id. - * @param tabId The id of the tab that you want to switch to. - */ select(tabId: string) { const tab = Array.from(this._tabLinks).find( (a: HTMLAnchorElement) => a.getAttribute('href') === '#' + tabId ); - if (tab) (tab).click(); + if (tab) tab.click(); } } diff --git a/components/tooltip/_tooltip.scss b/components/tooltip/___del_tooltip.scss similarity index 100% rename from components/tooltip/_tooltip.scss rename to components/tooltip/___del_tooltip.scss diff --git a/package.json b/package.json index 36717d05ee..31279b259f 100644 --- a/package.json +++ b/package.json @@ -39,7 +39,7 @@ ], "scripts": { "lint": "eslint .", - "test": "jasmine-browser-runner runSpecs --config=jasmine.json", + "test": "vitest", "build": "rollup --config rollup.config.ts --configPlugin @rollup/plugin-typescript", "build-storybook": "storybook build", "build-examples": "examples/compile.sh", @@ -47,8 +47,7 @@ "preversion": "npm run lint && npm test", "version": "npm run build -- --environment BUILD:release && node compress.mjs && git add .", "storybook": "storybook dev -p 6006", - "demo": "live-server", - "vitest": "vitest" + "demo": "live-server" }, "lint-staged": { "js/*.js": [ diff --git a/sass/materialize.scss b/sass/materialize.scss index 910f8f4a5f..b2a6be8352 100644 --- a/sass/materialize.scss +++ b/sass/materialize.scss @@ -23,7 +23,7 @@ @forward '../components/card/cards'; @forward '../components/snackbar/toast'; @forward '../components/tabs/tabs'; -@forward '../components/tooltip/tooltip'; +@forward '../components/tooltip/tooltip.css'; @forward '../components/dropdown/dropdown'; @forward '../components/list/collection'; @forward '../components/list/list'; From f821e79a551083860cc54c06920560ac3807b240 Mon Sep 17 00:00:00 2001 From: Daniel Wurzer Date: Wed, 12 Aug 2026 10:38:17 +0200 Subject: [PATCH 04/16] test(snackbar): convert to vitest --- components/snackbar/___toastSpec.js | 121 ----------------------- components/snackbar/snackbar.test.ts | 132 +++++++++++++++++++++++++ components/snackbar/toasts.ts | 140 ++++++++++++++++----------- 3 files changed, 216 insertions(+), 177 deletions(-) delete mode 100644 components/snackbar/___toastSpec.js create mode 100644 components/snackbar/snackbar.test.ts diff --git a/components/snackbar/___toastSpec.js b/components/snackbar/___toastSpec.js deleted file mode 100644 index 3ec28e6940..0000000000 --- a/components/snackbar/___toastSpec.js +++ /dev/null @@ -1,121 +0,0 @@ -describe('Toasts:', () => { - describe('Toast javascript functions', () => { - it('should display and remove a toast', (done) => { - const instance = new M.Toast({ - text: 'Test toast', - inDuration: 20, - displayLength: 100, - outDuration: 20 - }); - const wrapperWasCreated = document.querySelectorAll('#toast-container').length === 1; - expect(wrapperWasCreated).toEqual(true, 'because toast was created'); - const toast = instance.el; - setTimeout(() => { - // is visible? - //const toasts = document.querySelectorAll('.toast'); - //const toast = toasts[0]; - //expect(toasts.length).toBe(1, 'because one toast was created'); - expect(toast.getAttribute('role')).toBe('alert'); - expect(toast.getAttribute('aria-live')).toBe('assertive'); - expect(toast.getAttribute('aria-atomic')).toBe('true'); - expect(toast).toBeVisible(); - expect(toast.innerText).toBe('Test toast'); - setTimeout(() => { - //const toasts = document.querySelectorAll('.toast'); - expect(toast).toBeVisible(); - //expect(toasts.length).toBe(1, 'because toast duration still on going'); - setTimeout(() => { - //const toasts = document.querySelectorAll('.toast'); - //console.log(); - expect(typeof toast.el).toBe('undefined', 'because toast should be removed by now'); - done(); - }, 150); - }, 30); - }, 10); - }); - - // it('Opens a toast with HTML content', function(done) { - // let toastContent = document.createElement("span"); - // toastContent.innerText = 'I am toast content'; - // M.toast({html: toastContent.outerHTML, displayLength: 400}); - // let toastSpan = document.querySelector('.toast span'); - // expect(toastSpan.innerText).toBe('I am toast content'); - // expect(toastSpan.innerText).not.toBe('I am toast'); - // setTimeout(function() { - // done(); - // }, 490); - // }); - - it('Toasts should call the callback function when dismissed', (done) => { - let wasCalled = false; - const callback = () => (wasCalled = true); - new M.Toast({ - text: 'I am a toast', - inDuration: 10, - displayLength: 50, - outDuration: 10, - completeCallback: callback - }); - setTimeout(() => { - expect(wasCalled).toBe(true, 'because the callback set it to true'); - done(); - }, 100); - }); - - it('should apply classes to toast', (done) => { - new M.Toast({ - text: 'Hi', - displayLength: 100, - inDuration: 10, - outDuration: 10, - classes: 'round flat' - }); - setTimeout(() => { - const toastFlat = document.querySelectorAll('.toast.round.flat'); - expect(toastFlat.length).toBe(1, 'because the class parameter was passed with two classes'); - done(); - }, 20); - }); - }); - - describe('Toast _container null guard', () => { - afterEach(() => { - // Reset shared static state so other specs are unaffected - M.Toast.dismissAll(); - M.Toast._removeContainer(); - }); - - it('should not throw when _removeContainer is called with a null container', () => { - M.Toast._container = null; - expect(() => M.Toast._removeContainer()).not.toThrow(); - expect(M.Toast._container).toBeNull('because there was no container to remove'); - }); - - it('should not throw when _removeContainer is called twice', () => { - // First call removes a freshly created container, second call hits the guard. - M.Toast._createContainer(); - expect(M.Toast._container).not.toBeNull('because a container was just created'); - - expect(() => M.Toast._removeContainer()).not.toThrow(); - expect(M.Toast._container).toBeNull('because the container was just removed'); - - expect(() => M.Toast._removeContainer()).not.toThrow('because the guard protects the second call'); - expect(M.Toast._container).toBeNull(); - }); - - it('should not throw building a toast while _container is null', () => { - // Create a first toast so the container exists and _toasts is non-empty, - // then null the container to force the inconsistent state the guard handles - // (a second toast skips _createContainer because _toasts.length !== 0). - const first = new M.Toast({ text: 'First', displayLength: 50, inDuration: 10, outDuration: 10 }); - expect(first.el).toBeDefined(); - M.Toast._container = null; - - let instance; - expect(() => { - instance = new M.Toast({ text: 'No container', displayLength: 50, inDuration: 10, outDuration: 10 }); - }).not.toThrow('because _createToast guards the null container'); - expect(instance.el).toBeDefined('because the toast element is still created without a container'); - }); - }); -}); diff --git a/components/snackbar/snackbar.test.ts b/components/snackbar/snackbar.test.ts new file mode 100644 index 0000000000..1c491d0b4a --- /dev/null +++ b/components/snackbar/snackbar.test.ts @@ -0,0 +1,132 @@ +// @vitest-environment happy-dom + +import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; +import { Toast } from './toasts'; + +describe('Toasts:', () => { + beforeEach(() => { + vi.useFakeTimers(); + document.body.innerHTML = ''; + }); + + afterEach(() => { + Toast.dismissAll(); + Toast._removeContainer(); + vi.restoreAllMocks(); + vi.useRealTimers(); + }); + + describe('Toast javascript functions', () => { + it('should display and remove a toast', () => { + const instance = new Toast({ + text: 'Test toast', + inDuration: 20, + displayLength: 100, + outDuration: 20 + }); + + const wrapperWasCreated = document.querySelectorAll('#toast-container').length === 1; + expect(wrapperWasCreated).toBe(true); + + const toast = instance.el; + + // Advance timers past inDuration animation trigger + vi.advanceTimersByTime(10); + expect(toast.getAttribute('role')).toBe('alert'); + expect(toast.getAttribute('aria-live')).toBe('assertive'); + expect(toast.getAttribute('aria-atomic')).toBe('true'); + expect(toast.innerText).toBe('Test toast'); + expect(document.body.contains(toast)).toBe(true); + + // Advance timers to middle of display duration + vi.advanceTimersByTime(30); + expect(document.body.contains(toast)).toBe(true); + + // Advance timers past displayLength + outDuration to trigger removal + vi.advanceTimersByTime(150); + expect(document.body.contains(toast)).toBe(false); + }); + + it('Toasts should call the callback function when dismissed', () => { + let wasCalled = false; + const callback = () => { + wasCalled = true; + }; + + new Toast({ + text: 'I am a toast', + inDuration: 10, + displayLength: 50, + outDuration: 10, + completeCallback: callback + }); + + // Advance past total duration (50 + 10 = 60ms) + vi.advanceTimersByTime(100); + expect(wasCalled).toBe(true); + }); + + it('should apply classes to toast', () => { + new Toast({ + text: 'Hi', + displayLength: 100, + inDuration: 10, + outDuration: 10, + classes: 'round flat' + }); + + vi.advanceTimersByTime(20); + const toastFlat = document.querySelectorAll('.toast.round.flat'); + expect(toastFlat.length).toBe(1); + }); + }); + + describe('Toast _container null guard', () => { + afterEach(() => { + Toast.dismissAll(); + Toast._removeContainer(); + }); + + it('should not throw when _removeContainer is called with a null container', () => { + (Toast as any)._container = null; + expect(() => Toast._removeContainer()).not.toThrow(); + expect((Toast as any)._container).toBeNull(); + }); + + it('should not throw when _removeContainer is called twice', () => { + Toast._createContainer(); + expect((Toast as any)._container).not.toBeNull(); + + expect(() => Toast._removeContainer()).not.toThrow(); + expect((Toast as any)._container).toBeNull(); + + expect(() => Toast._removeContainer()).not.toThrow(); + expect((Toast as any)._container).toBeNull(); + }); + + it('should not throw building a toast while _container is null', () => { + const first = new Toast({ + text: 'First', + displayLength: 50, + inDuration: 10, + outDuration: 10 + }); + expect(first.el).toBeDefined(); + + // Force null container state + (Toast as any)._container = null; + + let instance: Toast | undefined; + expect(() => { + instance = new Toast({ + text: 'No container', + displayLength: 50, + inDuration: 10, + outDuration: 10 + }); + }).not.toThrow(); + + expect(instance?.el).toBeDefined(); + }); + }); +}); diff --git a/components/snackbar/toasts.ts b/components/snackbar/toasts.ts index 40b22c189c..93e7e1199f 100644 --- a/components/snackbar/toasts.ts +++ b/components/snackbar/toasts.ts @@ -7,7 +7,7 @@ export interface ToastOptions extends BaseOptions { */ text: string; /** - * Element Id for the tooltip. + * Element Id for the tooltip/template. * @default "" */ toastId?: string; @@ -35,9 +35,9 @@ export interface ToastOptions extends BaseOptions { * Callback function called when toast is dismissed. * @default null */ - completeCallback: () => void; + completeCallback?: (() => void) | null; /** - * The percentage of the toast's width it takes fora drag + * The percentage of the toast's width it takes for a drag * to dismiss a Toast. * @default 0.8 */ @@ -46,6 +46,7 @@ export interface ToastOptions extends BaseOptions { const _defaults: ToastOptions = { text: '', + toastId: '', displayLength: 4000, inDuration: 300, outDuration: 375, @@ -56,7 +57,7 @@ const _defaults: ToastOptions = { export class Toast { /** The toast element. */ - el: HTMLElement; + el!: HTMLElement; /** * The remaining amount of time in ms that the toast * will stay before dismissal. @@ -68,19 +69,19 @@ export class Toast { panning: boolean; options: ToastOptions; message: string; - counterInterval: NodeJS.Timeout | number; - wasSwiped: boolean; - startingXPos: number; - xPos: number; - time: number; - deltaX: number; - velocityX: number; - - static _toasts: Toast[]; - private static _container: HTMLElement | null; - static _draggedToast: Toast; - - constructor(options: Partial) { + counterInterval?: ReturnType; + wasSwiped: boolean = false; + startingXPos: number = 0; + xPos: number = 0; + time: number = 0; + deltaX: number = 0; + velocityX: number = 0; + + static _toasts: Toast[] = []; + private static _container: HTMLElement | null = null; + static _draggedToast: Toast | null = null; + + constructor(options?: Partial) { this.options = { ...Toast.defaults, ...options @@ -88,13 +89,15 @@ export class Toast { this.message = this.options.text; this.panning = false; this.timeRemaining = this.options.displayLength; + if (Toast._toasts.length === 0) { Toast._createContainer(); } + // Create new toast Toast._toasts.push(this); const toastElement = this._createToast(); - toastElement['M_Toast'] = this; + (toastElement as any)['M_Toast'] = this; this.el = toastElement; this._animateIn(); this._setTimer(); @@ -105,19 +108,21 @@ export class Toast { } static getInstance(el: HTMLElement): Toast { - return el['M_Toast']; + return (el as any)['M_Toast']; } static _createContainer() { const container = document.createElement('div'); container.setAttribute('id', 'toast-container'); - // Add event handler + + // Add event handlers container.addEventListener('touchstart', Toast._onDragStart); container.addEventListener('touchmove', Toast._onDragMove); container.addEventListener('touchend', Toast._onDragEnd); container.addEventListener('mousedown', Toast._onDragStart); document.addEventListener('mousemove', Toast._onDragMove); document.addEventListener('mouseup', Toast._onDragEnd); + document.body.appendChild(container); Toast._container = container; } @@ -132,9 +137,11 @@ export class Toast { } static _onDragStart(e: TouchEvent | MouseEvent) { - if (e.target && (e.target).closest('.toast')) { - const toastElem = (e.target).closest('.toast'); - const toast: Toast = toastElem['M_Toast']; + if (e.target && (e.target as HTMLElement).closest('.toast')) { + const toastElem = (e.target as HTMLElement).closest('.toast') as HTMLElement; + const toast: Toast = (toastElem as any)['M_Toast']; + if (!toast) return; + toast.panning = true; Toast._draggedToast = toast; toast.el.classList.add('panning'); @@ -146,23 +153,26 @@ export class Toast { } static _onDragMove(e: TouchEvent | MouseEvent) { - if (!!Toast._draggedToast) { + if (Toast._draggedToast) { e.preventDefault(); const toast = Toast._draggedToast; - toast.deltaX = Math.abs(toast.xPos - Toast._xPos(e)); - toast.xPos = Toast._xPos(e); - toast.velocityX = toast.deltaX / (Date.now() - toast.time); + const currentX = Toast._xPos(e); + + toast.deltaX = Math.abs(toast.xPos - currentX); + toast.xPos = currentX; + toast.velocityX = toast.deltaX / (Date.now() - toast.time || 1); toast.time = Date.now(); const totalDeltaX = toast.xPos - toast.startingXPos; const activationDistance = toast.el.offsetWidth * toast.options.activationPercent; + toast.el.style.transform = `translateX(${totalDeltaX}px)`; toast.el.style.opacity = (1 - Math.abs(totalDeltaX / activationDistance)).toString(); } } static _onDragEnd() { - if (!!Toast._draggedToast) { + if (Toast._draggedToast) { const toast = Toast._draggedToast; toast.panning = false; toast.el.classList.remove('panning'); @@ -171,12 +181,11 @@ export class Toast { const activationDistance = toast.el.offsetWidth * toast.options.activationPercent; const shouldBeDismissed = Math.abs(totalDeltaX) > activationDistance || toast.velocityX > 1; - // Remove toast if (shouldBeDismissed) { toast.wasSwiped = true; toast.dismiss(); - // Animate toast back to original position } else { + // Animate toast back to original position toast.el.style.transition = 'transform .2s, opacity .2s'; toast.el.style.transform = ''; toast.el.style.opacity = ''; @@ -185,40 +194,58 @@ export class Toast { } } - static _xPos(e: TouchEvent | MouseEvent) { - if (e.type.startsWith('touch') && (e as TouchEvent).targetTouches.length >= 1) { - return (e as TouchEvent).targetTouches[0].clientX; + static _xPos(e: TouchEvent | MouseEvent): number { + if (e.type.startsWith('touch')) { + const touchEvent = e as TouchEvent; + if (touchEvent.targetTouches && touchEvent.targetTouches.length >= 1) { + return touchEvent.targetTouches[0].clientX; + } + if (touchEvent.changedTouches && touchEvent.changedTouches.length >= 1) { + return touchEvent.changedTouches[0].clientX; + } } - // mouse event return (e as MouseEvent).clientX; } /** - * dismiss all toasts. + * Dismiss all active toasts. */ static dismissAll() { - for (const toastIndex in Toast._toasts) { - Toast._toasts[toastIndex].dismiss(); + const toastsCopy = [...Toast._toasts]; + for (const toast of toastsCopy) { + toast.dismiss(); } } - _createToast() { + _createToast(): HTMLElement { let toast: HTMLElement = this.options.toastId - ? document.getElementById(this.options.toastId) + ? (document.getElementById(this.options.toastId) as HTMLElement) : document.createElement('div'); + if (toast instanceof HTMLTemplateElement) { - const node = (toast as HTMLTemplateElement).content.cloneNode(true); + const node = toast.content.cloneNode(true); toast = (node as HTMLElement).firstElementChild as HTMLElement; + } else if (this.options.toastId && toast) { + toast = toast.cloneNode(true) as HTMLElement; + toast.removeAttribute('id'); + } + + if (!toast) { + toast = document.createElement('div'); } + toast.classList.add('toast'); toast.setAttribute('role', 'alert'); toast.setAttribute('aria-live', 'assertive'); toast.setAttribute('aria-atomic', 'true'); + // Add custom classes onto toast if (this.options.classes.length > 0) { - toast.classList.add(...this.options.classes.split(' ')); + toast.classList.add(...this.options.classes.split(' ').filter(Boolean)); } + if (this.message) toast.innerText = this.message; + if (Toast._container) { Toast._container.appendChild(toast); } @@ -226,10 +253,8 @@ export class Toast { } _animateIn() { - // Animate toast in this.el.style.display = ''; this.el.style.opacity = '0'; - // easeOutCubic this.el.style.transition = ` top ${this.options.inDuration}ms ease, opacity ${this.options.inDuration}ms ease @@ -241,17 +266,14 @@ export class Toast { } /** - * Create setInterval which automatically removes toast when timeRemaining >= 0 - * has been reached. + * Create setInterval which automatically removes toast when timeRemaining <= 0 */ _setTimer() { if (this.timeRemaining !== Infinity) { this.counterInterval = setInterval(() => { - // If toast is not being dragged, decrease its time remaining if (!this.panning) { this.timeRemaining -= 20; } - // Animate toast out if (this.timeRemaining <= 0) { this.dismiss(); } @@ -263,7 +285,10 @@ export class Toast { * Dismiss toast with animation. */ dismiss() { - clearInterval(this.counterInterval); + if (this.counterInterval) { + clearInterval(this.counterInterval); + } + const activationDistance = this.el.offsetWidth * this.options.activationPercent; if (this.wasSwiped) { @@ -272,7 +297,6 @@ export class Toast { this.el.style.opacity = '0'; } - // easeOutExpo this.el.style.transition = ` margin ${this.options.outDuration}ms ease, opacity ${this.options.outDuration}ms ease`; @@ -287,13 +311,17 @@ export class Toast { if (typeof this.options.completeCallback === 'function') { this.options.completeCallback(); } - // Remove toast from DOM - if (this.el.id != this.options.toastId) { - this.el.remove(); - Toast._toasts.splice(Toast._toasts.indexOf(this), 1); - if (Toast._toasts.length === 0) { - Toast._removeContainer(); - } + + // Remove toast element and clean up internal state + this.el.remove(); + + const index = Toast._toasts.indexOf(this); + if (index !== -1) { + Toast._toasts.splice(index, 1); + } + + if (Toast._toasts.length === 0) { + Toast._removeContainer(); } }, this.options.outDuration); } From 0bd988dae4f38b0ee907ca1ede2e469f45d55fc9 Mon Sep 17 00:00:00 2001 From: Daniel Wurzer Date: Wed, 12 Aug 2026 10:48:17 +0200 Subject: [PATCH 05/16] fix(divider): add builder --- components/divider/divider.mjs | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 components/divider/divider.mjs diff --git a/components/divider/divider.mjs b/components/divider/divider.mjs new file mode 100644 index 0000000000..94ab98100d --- /dev/null +++ b/components/divider/divider.mjs @@ -0,0 +1,10 @@ +import { Component } from '../atomic/component.mjs'; + +class Divider extends Component { + constructor(options) { + super(options); + this.addClassname('divider'); + } +} + +export { Divider }; From 23d1a2bede78fd855f6c38fe84548cd93e0d6beb Mon Sep 17 00:00:00 2001 From: Daniel Wurzer Date: Wed, 12 Aug 2026 11:08:51 +0200 Subject: [PATCH 06/16] fix: testing js tooltip on blog in examples --- components/atomic/component.mjs | 17 ++++++++++------- components/atomic/page.mjs | 28 +++++++++++++++++++++------- examples/html/blog.html | 7 +++++-- examples/html/landingpage.html | 2 ++ examples/html/portfolio.html | 2 ++ examples/src/blog.js | 11 ++++++++--- 6 files changed, 48 insertions(+), 19 deletions(-) diff --git a/components/atomic/component.mjs b/components/atomic/component.mjs index fce33b6f50..8d51846f49 100644 --- a/components/atomic/component.mjs +++ b/components/atomic/component.mjs @@ -10,16 +10,19 @@ class Component { this.#children = []; this.#classNames = []; - if (typeof options === 'object' && options !== null && options.children) { - if (typeof options.children === 'string') { - this.#children = options.children; + if (typeof options === 'object' && options !== null) { + // children + if (options.children) { + if (typeof options.children === 'string') { + this.#children = options.children; + return; + } + const kids = Array.isArray(options.children) ? options.children : [options.children]; + kids.forEach((c) => this.addChild(c)); return; } - const kids = Array.isArray(options.children) ? options.children : [options.children]; - kids.forEach((c) => this.addChild(c)); - return; } - this.#children = options; + this.#children = options; // TODO: Maybe do not do this } setChildren(children) { diff --git a/components/atomic/page.mjs b/components/atomic/page.mjs index a6d30ebcb1..ff3022433d 100644 --- a/components/atomic/page.mjs +++ b/components/atomic/page.mjs @@ -3,24 +3,35 @@ import { Component } from './component.mjs'; class Page extends Component { #title; #metaDescription; - #styleList = []; - #css = []; + #css; + #cssUrls; + #scripts; + #scriptUrls; constructor(options) { super(options); this.setTagName('html'); if (options.title) this.setTitle(options.title); + this.#cssUrls = []; + this.#scriptUrls = []; + this.#css = []; + this.#scripts = []; } addStyleUrl(url) { - this.#styleList.push(url); + this.#cssUrls.push(url); return this; } addStyle(css) { this.#css.push(css); return this; } - addJavascriptUrl() { + addJavascriptUrl(url) { + this.#scriptUrls.push(url); + return this; + } + addJavascript(script) { + this.#scripts.push(script); return this; } setTitle(title) { @@ -35,11 +46,12 @@ class Page extends Component { toHTML() { return ` ${this.#title} - ${this.#styleList + ${this.#cssUrls .map( - (s) => `` + (url) => + `` ) - .join('')} + .join('\n')} ${this.#css.map((s) => ``).join('\n')} @@ -47,6 +59,8 @@ class Page extends Component {
${super.toHTML()}
+ ${this.#scriptUrls.map((url) => ``).join('\n')} + ${this.#scripts.map((s) => ``).join('\n')} `; } } diff --git a/examples/html/blog.html b/examples/html/blog.html index 051d5f42e3..b4873c1f47 100644 --- a/examples/html/blog.html +++ b/examples/html/blog.html @@ -1,6 +1,7 @@ Materialize Web Blog - + + `).join('\n')} - - - -
- ${super.toHTML()} -
- ${this.#scriptUrls.map((url) => ``).join('\n')} - ${this.#scripts.map((s) => ``).join('\n')} - `; + return ` + + + + + + ${this.#title} + ${this.#cssUrls + .map( + (url) => `` + ) + .join('\n')} + ${this.#css.map((s) => ``).join('\n')} + + +
+ ${super.toHTML()} +
+ ${this.#scriptUrls.map((url) => ``).join('\n')} + ${this.#scripts.map((s) => ``).join('\n')} + +`; } } diff --git a/examples/html/blog.html b/examples/html/blog.html index b4873c1f47..cf8bdea7d8 100644 --- a/examples/html/blog.html +++ b/examples/html/blog.html @@ -1,18 +1,22 @@ - - Materialize Web Blog - + + + + + + + Materialize Web Blog + - - - - -
-

Materialize Web Blog

+ + +
+

Materialize Web Blog

01/01/2026 posted by Daniel

Crafting Sleek, Responsive Interfaces with MaterializeWeb

In the fast-evolving landscape of web development, building clean, responsive, and intuitive user interfaces remains a top priority. While major CSS frameworks like Bootstrap have long dominated the scene, MaterializeWeb—the community-driven evolution of MaterializeCSS—offers a refreshing design philosophy anchored directly in Google’s Material Design principles.

@@ -29,7 +33,8 @@

Why Choose It for Your Next Project?

If you want your web applications to mirror the sleek, unified visual identity of modern mobile apps without spending weeks writing custom styles, MaterializeWeb is an outstanding choice. It strikes an ideal balance between aesthetic polish and developer efficiency, making it perfect for rapid prototyping and production-ready sites alike.

Give MaterializeWeb a try on your next project to experience how effortless modern visual design can be!

2 or 3-column masonry grid showing article thumbnails, titles, and publication dates
This is a test Landingpage. ok it can produce nested html too...
Or you can go to the Portfolio
Sidebar or navigation dropdowns ("Tech," "Lifestyle")
-
- - - +
+ + + + diff --git a/examples/html/landingpage.html b/examples/html/landingpage.html index 58d1331545..8dcf43af47 100644 --- a/examples/html/landingpage.html +++ b/examples/html/landingpage.html @@ -1,15 +1,20 @@ - - My Landingpage - - - - - -
-

Landing Page

Hero Section
A bold "Hero" section at the top with a clear headline, followed by social proof (reviews/logos),
+ + + + + + + My Landingpage + + + + +
+

Landing Page

Hero Section
A bold "Hero" section at the top with a clear headline, followed by social proof (reviews/logos),
feature blocks, and multiple Calls- to - Action(CTAs) like "Sign Up" or "Buy Now.
You can also checkout the Blog
Hello, this is my landing page!
-
- - - +
+ + + + diff --git a/examples/html/portfolio.html b/examples/html/portfolio.html index 9f91e52857..f6c4152506 100644 --- a/examples/html/portfolio.html +++ b/examples/html/portfolio.html @@ -1,13 +1,18 @@ - - My Portfolio - - - - - -
-

My Portfolio

Hello, this is my portfolio. You can also checkout the Blog
Sidebar
-
- - - + + + + + + + My Portfolio + + + + +
+

My Portfolio

Hello, this is my portfolio. You can also checkout the Blog
Sidebar
+
+ + + + From 93fe4289fcb3b3a572bb403c9cf4bd82ea8f0ace Mon Sep 17 00:00:00 2001 From: Daniel Wurzer Date: Wed, 12 Aug 2026 11:26:28 +0200 Subject: [PATCH 08/16] fix: page and html generation --- components/atomic/component.mjs | 4 ++-- components/atomic/page.mjs | 5 +++-- examples/html/blog.html | 21 ++++++++++++++++++--- examples/html/landingpage.html | 13 +++++++++++-- examples/html/portfolio.html | 9 ++++++++- examples/src/blog.js | 2 +- 6 files changed, 43 insertions(+), 11 deletions(-) diff --git a/components/atomic/component.mjs b/components/atomic/component.mjs index 8d51846f49..0eca34f826 100644 --- a/components/atomic/component.mjs +++ b/components/atomic/component.mjs @@ -64,10 +64,10 @@ class Component { if (Array.isArray(this.#children)) { const innerHTML = this.#children.map((child) => child.toHTML()).join(''); - return `<${this.#tagname}${classAttr}${otherAttrs}>${innerHTML}`; + return `<${this.#tagname}${classAttr}${otherAttrs}>\n${innerHTML}\n`; } - return `<${this.#tagname}${classAttr}${otherAttrs}>${this.#children ?? ''}`; + return `<${this.#tagname}${classAttr}${otherAttrs}>${this.#children ?? ''}\n`; } toDOM() { diff --git a/components/atomic/page.mjs b/components/atomic/page.mjs index 535b6249ac..9e7d5d3960 100644 --- a/components/atomic/page.mjs +++ b/components/atomic/page.mjs @@ -11,7 +11,7 @@ class Page extends Component { constructor(options) { super(options); - this.setTagName('html'); + //this.setTagName('html'); if (options.title) this.setTitle(options.title); this.#langCode = 'en'; this.#cssUrls = []; @@ -46,6 +46,7 @@ class Page extends Component { } // override toHTML() { + const content = super.toHTML(); return ` @@ -62,7 +63,7 @@ class Page extends Component {
- ${super.toHTML()} + ${content}
${this.#scriptUrls.map((url) => ``).join('\n')} ${this.#scripts.map((s) => ``).join('\n')} diff --git a/examples/html/blog.html b/examples/html/blog.html index cf8bdea7d8..7ec934a27b 100644 --- a/examples/html/blog.html +++ b/examples/html/blog.html @@ -16,7 +16,16 @@
-

Materialize Web Blog

+
+
+

Materialize Web Blog

+
+ + +

01/01/2026 posted by Daniel

Crafting Sleek, Responsive Interfaces with MaterializeWeb

In the fast-evolving landscape of web development, building clean, responsive, and intuitive user interfaces remains a top priority. While major CSS frameworks like Bootstrap have long dominated the scene, MaterializeWeb—the community-driven evolution of MaterializeCSS—offers a refreshing design philosophy anchored directly in Google’s Material Design principles.

@@ -31,8 +40,14 @@

Key Features and Benefits

Why Choose It for Your Next Project?

If you want your web applications to mirror the sleek, unified visual identity of modern mobile apps without spending weeks writing custom styles, MaterializeWeb is an outstanding choice. It strikes an ideal balance between aesthetic polish and developer efficiency, making it perfect for rapid prototyping and production-ready sites alike.

-

Give MaterializeWeb a try on your next project to experience how effortless modern visual design can be!

2 or 3-column masonry grid showing article thumbnails, titles, and publication dates
This is a test Landingpage. ok it can produce nested html too...
- Or you can go to the Portfolio
Sidebar or navigation dropdowns ("Tech," "Lifestyle")
+

Give MaterializeWeb a try on your next project to experience how effortless modern visual design can be!

+
2 or 3-column masonry grid showing article thumbnails, titles, and publication dates
+ +This is a test Landingpage. ok it can produce nested html too...
+ Or you can go to the Portfolio
+
Sidebar or navigation dropdowns ("Tech," "Lifestyle")
+ +
diff --git a/examples/html/landingpage.html b/examples/html/landingpage.html index 8dcf43af47..11c7746f30 100644 --- a/examples/html/landingpage.html +++ b/examples/html/landingpage.html @@ -10,9 +10,18 @@
-

Landing Page

Hero Section
A bold "Hero" section at the top with a clear headline, followed by social proof (reviews/logos),
+
+
+

Landing Page

+
+
+
Hero Section
+
A bold "Hero" section at the top with a clear headline, followed by social proof (reviews/logos),
feature blocks, and multiple Calls- to - Action(CTAs) like "Sign Up" or "Buy Now.
- You can also checkout the Blog
Hello, this is my landing page!
+ You can also checkout the Blog
+
Hello, this is my landing page!
+
+
diff --git a/examples/html/portfolio.html b/examples/html/portfolio.html index f6c4152506..d15f929609 100644 --- a/examples/html/portfolio.html +++ b/examples/html/portfolio.html @@ -10,7 +10,14 @@
-

My Portfolio

Hello, this is my portfolio. You can also checkout the Blog
Sidebar
+
+
+

My Portfolio

+
+
Hello, this is my portfolio. You can also checkout the Blog
+
Sidebar
+
+
diff --git a/examples/src/blog.js b/examples/src/blog.js index e04efaa7b1..ee3bcbcd10 100644 --- a/examples/src/blog.js +++ b/examples/src/blog.js @@ -39,7 +39,7 @@ const blogPage = new Page({ // TODO: use NavigationBar Component new Container({ children: new Text('Materialize Web Blog').setTagName('p').addClassname('py-3') - }), + }).addClassname('wrapper-1'), new Container({ children: [ From 6efb5433e5b4e635c1a01940aab71774d87158d8 Mon Sep 17 00:00:00 2001 From: Daniel Wurzer Date: Wed, 12 Aug 2026 13:45:37 +0200 Subject: [PATCH 09/16] refactor: dropdown --- components/button/examples.iso.js | 18 - .../{buttons.ts => floatingactionbutton.ts} | 0 components/button/test-button.mjs | 16 - components/divider/divider.mjs | 1 + .../{_dropdown.scss => ____dropdown.scss} | 0 components/dropdown/___dropdownSpec.js | 108 ---- components/dropdown/dropdown.css | 60 +++ components/dropdown/dropdown.mjs | 1 + components/dropdown/dropdown.ts | 481 +++++------------- components/dropdown/readme.md | 22 + examples/counter-app.iso.js | 64 +++ examples/html/landingpage.html | 30 +- examples/src/landingpage.js | 48 +- index.html | 26 +- package.json | 2 +- src/index.ts | 5 +- 16 files changed, 330 insertions(+), 552 deletions(-) delete mode 100644 components/button/examples.iso.js rename components/button/{buttons.ts => floatingactionbutton.ts} (100%) delete mode 100644 components/button/test-button.mjs rename components/dropdown/{_dropdown.scss => ____dropdown.scss} (100%) delete mode 100644 components/dropdown/___dropdownSpec.js create mode 100644 components/dropdown/dropdown.css create mode 100644 components/dropdown/dropdown.mjs create mode 100644 components/dropdown/readme.md create mode 100644 examples/counter-app.iso.js diff --git a/components/button/examples.iso.js b/components/button/examples.iso.js deleted file mode 100644 index 5c7f3a4110..0000000000 --- a/components/button/examples.iso.js +++ /dev/null @@ -1,18 +0,0 @@ -import { Number, Text } from '../atomic/atomic.mjs'; -import { Card } from '../card/card.mjs'; -import { Button } from './button.mjs'; - -// Example App (isomorphic javascript) - -function createCounterApp() { - return new Card({ - children: [ - new Text('Counter Example').setTagName('div'), - new Number(9), - new Button('➕'), - new Button('➖') - ] - }).addClassname('p-3'); -} - -export { createCounterApp }; diff --git a/components/button/buttons.ts b/components/button/floatingactionbutton.ts similarity index 100% rename from components/button/buttons.ts rename to components/button/floatingactionbutton.ts diff --git a/components/button/test-button.mjs b/components/button/test-button.mjs deleted file mode 100644 index 4f079113b7..0000000000 --- a/components/button/test-button.mjs +++ /dev/null @@ -1,16 +0,0 @@ -import { Number, Text } from '../atomic/atomic.mjs'; -import { Button, Card } from './button.mjs'; - -// Example App (isomorphic javascript) -const app = new Card({ - children: [ - new Text('Example App').setTagName('div'), // - new Number(9), - new Button('➕'), - new Button('➖') - ] -}).addClassname('p-3'); - -// Html -const html = app.toHTML(); -console.log(html); diff --git a/components/divider/divider.mjs b/components/divider/divider.mjs index 94ab98100d..dff8d7c4c0 100644 --- a/components/divider/divider.mjs +++ b/components/divider/divider.mjs @@ -4,6 +4,7 @@ class Divider extends Component { constructor(options) { super(options); this.addClassname('divider'); + this.setAttribute('role', 'separator'); } } diff --git a/components/dropdown/_dropdown.scss b/components/dropdown/____dropdown.scss similarity index 100% rename from components/dropdown/_dropdown.scss rename to components/dropdown/____dropdown.scss diff --git a/components/dropdown/___dropdownSpec.js b/components/dropdown/___dropdownSpec.js deleted file mode 100644 index f50defa6cb..0000000000 --- a/components/dropdown/___dropdownSpec.js +++ /dev/null @@ -1,108 +0,0 @@ -describe('Dropdown Plugin:', () => { - const fixture = ``; - - beforeEach(() => { - XloadHtml(fixture); - M.Dropdown.init(document.querySelectorAll('.dropdown-trigger'), { - inDuration: 0, - outDuration: 0 - }); - }); - afterEach(() => XunloadFixtures()); - - describe('Dropdown', () => { - let normalDropdown; - - it('should open and close programmatically', (done) => { - const dropdown1 = document.querySelector('#dropdown1'); - normalDropdown = document.querySelector('#dropdownActivator'); - expect(dropdown1).toBeHidden('Should be hidden before dropdown is opened.'); - M.Dropdown.getInstance(normalDropdown).open(); - //setTimeout(() => { - expect(dropdown1).toBeVisible('Should be shown after dropdown is opened.'); - M.Dropdown.getInstance(normalDropdown).close(); - setTimeout(() => { - expect(dropdown1).toBeHidden('Should be hidden after dropdown is closed.'); - done(); - }, 5); // 400 - //}, 400); - }); - - it('should close dropdown on document click if programmatically opened', (done) => { - const dropdown1 = document.querySelector('#dropdown1'); - normalDropdown = document.querySelector('#dropdownActivator'); - expect(dropdown1).toBeHidden('Should be hidden before dropdown is opened.'); - M.Dropdown.getInstance(normalDropdown).open(); - setTimeout(() => { - expect(dropdown1).toBeVisible('Should be shown after dropdown is opened.'); - click(document.body); - setTimeout(() => { - expect(dropdown1).toBeHidden('Should be hidden after dropdown is closed.'); - done(); - }, 5); // 400 - }, 5); // 400 - }); - - it('should bubble events correctly', (done) => { - const dropdown2 = document.querySelector('#dropdown2'); - normalDropdown = document.querySelector('#dropdownBubble'); - expect(dropdown2).toBeHidden('Should be hidden before dropdown is opened.'); - click(normalDropdown.querySelector('i')); - setTimeout(() => { - expect(dropdown2).toBeVisible('Should be shown after dropdown is opened.'); - click(document.body); - setTimeout(() => { - expect(dropdown2).toBeHidden('Should be hidden after dropdown is closed.'); - done(); - }, 5); // 400 - }, 5); // 400 - }); - - it('hovered should destroy itself', (done) => { - const dropdownTrigger = document.querySelector('#dropdownDestroyTrigger'); - M.Dropdown.getInstance(dropdownTrigger).destroy(); - M.Dropdown.init(dropdownTrigger, { hover: true }); - expect(() => { - M.Dropdown.getInstance(dropdownTrigger).destroy(); - }).not.toThrow(); - //setTimeout(() => { - done(); - //}, 400); - }); - }); -}); diff --git a/components/dropdown/dropdown.css b/components/dropdown/dropdown.css new file mode 100644 index 0000000000..576dbe5a22 --- /dev/null +++ b/components/dropdown/dropdown.css @@ -0,0 +1,60 @@ +/* Base styles for the popover element */ +.dropdown-content[popover] { + /* Reset default popover browser styles */ + margin: 0; + padding: 0.5rem 0; + border: 1px solid #ccc; + border-radius: 6px; + background-color: #fff; + list-style: none; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); + + /* Automatic flip if top/bottom or left/right overflows viewport */ + position-try-fallbacks: flip-block, flip-inline; + + /* Smooth entry / exit transitions */ + opacity: 0; + transform: scale(0.95); + transition: + opacity 150ms ease, + transform 150ms ease, + display 150ms allow-discrete; +} + +/* Open State */ +.dropdown-content[popover]:popover-open { + opacity: 1; + transform: scale(1); +} + +/* Entry animation start point */ +@starting-style { + .dropdown-content[popover]:popover-open { + opacity: 0; + transform: scale(0.95); + } +} + +/* Styling dropdown items */ +.dropdown-content li { + padding: 0; +} + +.dropdown-content li a, +.dropdown-content li button { + display: block; + width: 100%; + padding: 0.5rem 1rem; + text-align: left; + background: none; + border: none; + color: #333; + text-decoration: none; + cursor: pointer; +} + +.dropdown-content li:focus-within, +.dropdown-content li.active { + background-color: #f0f0f0; + outline: none; +} diff --git a/components/dropdown/dropdown.mjs b/components/dropdown/dropdown.mjs new file mode 100644 index 0000000000..0d3f957262 --- /dev/null +++ b/components/dropdown/dropdown.mjs @@ -0,0 +1 @@ +// TODO: Builder diff --git a/components/dropdown/dropdown.ts b/components/dropdown/dropdown.ts index b40e82eb7a..6931b6171d 100644 --- a/components/dropdown/dropdown.ts +++ b/components/dropdown/dropdown.ts @@ -17,11 +17,6 @@ export interface DropdownOptions extends BaseOptions { * @default true */ constrainWidth: boolean; - /** - * Provide an element that will be the bounding container of the dropdown. - * @default null - */ - container: Element; /** * If false, the dropdown will show below the trigger. * @default true @@ -37,16 +32,6 @@ export interface DropdownOptions extends BaseOptions { * @default false */ hover: boolean; - /** - * The duration of the transition enter in milliseconds. - * @default 150 - */ - inDuration: number; - /** - * The duration of the transition out in milliseconds. - * @default 250 - */ - outDuration: number; /** * Function called when dropdown starts entering. * @default null @@ -78,12 +63,9 @@ const _defaults: DropdownOptions = { alignment: 'left', autoFocus: true, constrainWidth: true, - container: null, coverTrigger: true, closeOnClick: true, hover: false, - inDuration: 150, - outDuration: 250, onOpenStart: null, onOpenEnd: null, onCloseStart: null, @@ -99,9 +81,6 @@ export class Dropdown extends Component implements Openable { dropdownEl: HTMLElement; /** If the dropdown is open. */ isOpen: boolean; - /** If the dropdown content is scrollable. */ - isScrollable: boolean; - isTouchMoving: boolean; /** The index of the item focused. */ focusedIndex: number; filterQuery: string[]; @@ -121,14 +100,10 @@ export class Dropdown extends Component implements Openable { }; this.isOpen = false; - this.isScrollable = false; - this.isTouchMoving = false; this.focusedIndex = -1; this.filterQuery = []; - this.el.ariaExpanded = 'false'; - // Move dropdown-content after dropdown-trigger - this._moveDropdownToElement(); + this._setupPopoverAndAnchor(); this._makeDropdownFocusable(); this._setupEventHandlers(); } @@ -137,23 +112,8 @@ export class Dropdown extends Component implements Openable { return _defaults; } - /** - * Initializes instance of Dropdown. - * @param el HTML element. - * @param options Component options. - */ static init(el: HTMLElement, options?: Partial): Dropdown; - /** - * Initializes instances of Dropdown. - * @param els HTML elements. - * @param options Component options. - */ static init(els: InitElements, options?: Partial): Dropdown[]; - /** - * Initializes instances of Dropdown. - * @param els HTML elements. - * @param options Component options. - */ static init( els: HTMLElement | InitElements, options: Partial = {} @@ -166,23 +126,63 @@ export class Dropdown extends Component implements Openable { } destroy() { - this._resetDropdownStyles(); this._removeEventHandlers(); + if (this.dropdownEl?.hasAttribute('popover')) { + this.dropdownEl.removeAttribute('popover'); + } Dropdown._dropdowns.splice(Dropdown._dropdowns.indexOf(this), 1); this.el['M_Dropdown'] = undefined; } + /** + * Applies CSS Anchor Positioning and Popover attributes to the elements + */ + private _setupPopoverAndAnchor() { + if (!this.dropdownEl) return; + + // Set Popover API attributes + this.dropdownEl.popover = 'auto'; + + // Generate unique anchor name if necessary + const anchorName = `--dropdown-anchor-${this.id || Math.random().toString(36).substring(2, 9)}`; + + // Set CSS Anchor variables directly on elements + this.el.style.setProperty('anchor-name', anchorName); + this.dropdownEl.style.setProperty('position-anchor', anchorName); + + // Apply native anchor positioning via inline styles or class rules + this.dropdownEl.style.position = 'fixed'; + this.dropdownEl.style.margin = '0'; + + // Width constraint + if (this.options.constrainWidth) { + this.dropdownEl.style.width = 'anchor-size(width)'; + } + + // Vertical placement (Cover Trigger vs Below Trigger) + const topPosition = this.options.coverTrigger ? 'anchor(top)' : 'anchor(bottom)'; + this.dropdownEl.style.top = `position-area(${topPosition})`; + + // Horizontal alignment + if (this.options.alignment === 'right') { + this.dropdownEl.style.left = 'anchor(right)'; + this.dropdownEl.style.transform = 'translateX(-100%)'; + } else { + this.dropdownEl.style.left = 'anchor(left)'; + } + } + _setupEventHandlers() { - // Trigger keydown handler this.el.addEventListener('keydown', this._handleTriggerKeydown); - // Item click handler this.dropdownEl?.addEventListener('click', this._handleDropdownClick); - // Hover event handlers + + // Listen to native Popover Toggle events to manage lifecycle and state synchronization + this.dropdownEl?.addEventListener('beforetoggle', this._handlePopoverToggle); + if (this.options.hover) { this.el.addEventListener('mouseenter', this._handleMouseEnter); this.el.addEventListener('mouseleave', this._handleMouseLeave); this.dropdownEl.addEventListener('mouseleave', this._handleMouseLeave); - // Click event handlers } else { this.el.addEventListener('click', this._handleClick); } @@ -190,7 +190,9 @@ export class Dropdown extends Component implements Openable { _removeEventHandlers() { this.el.removeEventListener('keydown', this._handleTriggerKeydown); - this.dropdownEl.removeEventListener('click', this._handleDropdownClick); + this.dropdownEl?.removeEventListener('click', this._handleDropdownClick); + this.dropdownEl?.removeEventListener('beforetoggle', this._handlePopoverToggle); + if (this.options.hover) { this.el.removeEventListener('mouseenter', this._handleMouseEnter); this.el.removeEventListener('mouseleave', this._handleMouseLeave); @@ -200,23 +202,35 @@ export class Dropdown extends Component implements Openable { } } - _setupTemporaryEventHandlers() { - document.body.addEventListener('click', this._handleDocumentClick); - document.body.addEventListener('touchmove', this._handleDocumentTouchmove); - this.dropdownEl.addEventListener('keydown', this._handleDropdownKeydown); - window.addEventListener('resize', this._handleWindowResize); - } + private _handlePopoverToggle = (e: ToggleEvent) => { + if (e.newState === 'open') { + this.isOpen = true; + this.el.setAttribute('aria-expanded', 'true'); + this.dropdownEl.addEventListener('keydown', this._handleDropdownKeydown); - _removeTemporaryEventHandlers() { - document.body.removeEventListener('click', this._handleDocumentClick); - document.body.removeEventListener('touchmove', this._handleDocumentTouchmove); - this.dropdownEl.removeEventListener('keydown', this._handleDropdownKeydown); - window.removeEventListener('resize', this._handleWindowResize); - } + if (typeof this.options.onOpenEnd === 'function') { + this.options.onOpenEnd.call(this, this.el); + } + if (this.options.autoFocus) { + this._focusFocusedItem(); + } + } else { + this.isOpen = false; + this.focusedIndex = -1; + this.el.setAttribute('aria-expanded', 'false'); + this.dropdownEl.removeEventListener('keydown', this._handleDropdownKeydown); + + if (typeof this.options.onCloseEnd === 'function') { + this.options.onCloseEnd.call(this, this.el); + } + if (this.options.autoFocus) { + this.el.focus(); + } + } + }; _handleClick = (e: MouseEvent) => { e.preventDefault(); - //this._moveDropdown((e.target).closest('li')); if (this.isOpen) { this.close(); } else { @@ -225,43 +239,24 @@ export class Dropdown extends Component implements Openable { }; _handleMouseEnter = () => { - //this._moveDropdown((e.target).closest('li')); this.open(); }; _handleMouseLeave = (e: MouseEvent) => { const toEl = e.relatedTarget as HTMLElement; + if (!toEl) return; + const leaveToDropdownContent = !!toEl.closest('.dropdown-content'); - let leaveToActiveDropdownTrigger = false; const closestTrigger = toEl.closest('.dropdown-trigger'); - if (closestTrigger && !!closestTrigger['M_Dropdown'] && closestTrigger['M_Dropdown'].isOpen) { - leaveToActiveDropdownTrigger = true; - } - // Close hover dropdown if mouse did not leave to either active dropdown-trigger or dropdown-content - if (!leaveToActiveDropdownTrigger && !leaveToDropdownContent) { - this.close(); - } - }; + const leaveToActiveDropdownTrigger = + closestTrigger && !!closestTrigger['M_Dropdown'] && closestTrigger['M_Dropdown'].isOpen; - _handleDocumentClick = (e: MouseEvent) => { - const target = e.target; - if (this.options.closeOnClick && target.closest('.dropdown-content') && !this.isTouchMoving) { - // isTouchMoving to check if scrolling on mobile. + if (!leaveToActiveDropdownTrigger && !leaveToDropdownContent) { this.close(); - } else if (!target.closest('.dropdown-content')) { - // Do this one frame later so that if the element clicked also triggers _handleClick - // For example, if a label for a select was clicked, that we don't close/open the dropdown - setTimeout(() => { - if (this.isOpen) { - this.close(); - } - }, 0); } - this.isTouchMoving = false; }; _handleTriggerKeydown = (e: KeyboardEvent) => { - // ARROW DOWN OR ENTER WHEN SELECT IS CLOSED - open Dropdown const arrowDownOrEnter = Utils.keys.ARROW_DOWN.includes(e.key) || Utils.keys.ENTER.includes(e.key); if (arrowDownOrEnter && !this.isOpen) { @@ -270,36 +265,31 @@ export class Dropdown extends Component implements Openable { } }; - _handleDocumentTouchmove = (e: TouchEvent) => { - const target = e.target; - if (target.closest('.dropdown-content')) { - this.isTouchMoving = true; - } - }; - _handleDropdownClick = (e: MouseEvent) => { - // onItemClick callback - if (typeof this.options.onItemClick === 'function') { - const itemEl = (e.target).closest('li'); + const itemEl = (e.target).closest('li'); + if (typeof this.options.onItemClick === 'function' && itemEl) { this.options.onItemClick.call(this, itemEl); } + + if (this.options.closeOnClick) { + this.close(); + } }; _handleDropdownKeydown = (e: KeyboardEvent) => { const arrowUpOrDown = Utils.keys.ARROW_DOWN.includes(e.key) || Utils.keys.ARROW_UP.includes(e.key); + if (Utils.keys.TAB.includes(e.key)) { - e.preventDefault(); this.close(); - } - // Navigate down dropdown list - else if (arrowUpOrDown && this.isOpen) { + } else if (arrowUpOrDown && this.isOpen) { e.preventDefault(); const direction = Utils.keys.ARROW_DOWN.includes(e.key) ? 1 : -1; let newFocusedIndex = this.focusedIndex; let hasFoundNewIndex = false; + do { - newFocusedIndex = newFocusedIndex + direction; + newFocusedIndex += direction; if ( !!this.dropdownEl.children[newFocusedIndex] && (this.dropdownEl.children[newFocusedIndex]).tabIndex !== -1 @@ -310,34 +300,26 @@ export class Dropdown extends Component implements Openable { } while (newFocusedIndex < this.dropdownEl.children.length && newFocusedIndex >= 0); if (hasFoundNewIndex) { - // Remove active class from old element - if (this.focusedIndex >= 0) + if (this.focusedIndex >= 0) { this.dropdownEl.children[this.focusedIndex].classList.remove('active'); + } this.focusedIndex = newFocusedIndex; this._focusFocusedItem(); } - } - // ENTER selects choice on focused item - else if (Utils.keys.ENTER.includes(e.key) && this.isOpen) { - // Search for and + + +``` diff --git a/examples/counter-app.iso.js b/examples/counter-app.iso.js new file mode 100644 index 0000000000..0e468d4e65 --- /dev/null +++ b/examples/counter-app.iso.js @@ -0,0 +1,64 @@ +import { Number, Text } from '../components/atomic/atomic.mjs'; +import { Card } from '../components/card/card.mjs'; +import { Button } from '../components/button/button.mjs'; + +function createCounterApp() { + return new Card({ + children: [ + new Text('My Counter').setTagName('div'), + new Number(9), + new Button('➕'), + new Button('➖') + ] + }).addClassname('p-3'); +} + +//====== Client Side Logic + +function count(input) { + const value = input.value + input.increment; + return { value }; +} + +function hydrate(domElement, initState = { value: 12 }) { + const state = initState; + + const btns = domElement.querySelectorAll('.btn'); // input + const numberEl = domElement.querySelector('.mw-number'); // output + + numberEl.innerHTML = state.value; + + btns[0].addEventListener('click', (e) => { + state.value = count({ value: state.value, increment: 1 }).value; + numberEl.innerHTML = state.value; + }); + btns[1].addEventListener('click', (e) => { + state.value = count({ value: state.value, increment: -1 }).value; + numberEl.innerHTML = state.value; + }); +} + +class CounterApp extends HTMLElement { + constructor() { + super(); + this.count = 0; + } + + connectedCallback() { + // Da das Template via DSD geladen wurde, existiert shadowRoot bereits! + if (this.shadowRoot) { + this.btn = this.shadowRoot.querySelector('#btn'); + this.output = this.shadowRoot.querySelector('#output'); + + // Event-Listener hinzufügen + this.btn.addEventListener('click', () => this.increment()); + } + } + + increment() { + this.count++; + this.output.textContent = this.count; + } +} + +export { createCounterApp, hydrate, CounterApp }; diff --git a/examples/html/landingpage.html b/examples/html/landingpage.html index 11c7746f30..240e27305d 100644 --- a/examples/html/landingpage.html +++ b/examples/html/landingpage.html @@ -6,24 +6,44 @@ My Landingpage +
-

Landing Page

+

a

-
Hero Section
A bold "Hero" section at the top with a clear headline, followed by social proof (reviews/logos),
feature blocks, and multiple Calls- to - Action(CTAs) like "Sign Up" or "Buy Now.
- You can also checkout the Blog
+ You can also checkout the Blog + klöxökkökökxl +
+ + + + +
Hello, this is my landing page!
- - + + diff --git a/examples/src/landingpage.js b/examples/src/landingpage.js index e287d503b5..d83134ee5b 100644 --- a/examples/src/landingpage.js +++ b/examples/src/landingpage.js @@ -2,24 +2,48 @@ import { Text } from '../../components/atomic/atomic.mjs'; import { Container, Page } from '../../components/atomic/page.mjs'; const landingPage = new Page({ - title: 'My Landingpage', - description: 'Introducing the new Phone', + title: 'T-Shirt Landingpage', + description: 'Introducing the new T-Shirt from Materialize', keywords: 'App launches, SaaS companies, small service businesses, and single-product e-commerce', children: [ - new Container({ children: new Text('Landing Page').setTagName('h1') }), - new Container(``), - new Container({ - children: 'Hero Section', - description: `A bold "Hero" section at the top with a clear headline, followed by social proof (reviews/logos),' + - 'feature blocks, and multiple Calls- to - Action(CTAs) like "Sign Up" or "Buy Now.". - You can also checkout the Blog` - }), + new Container({ children: new Text('a').setTagName('h1') }), + //new Container(``), + new Container({ children: 'T-Shirt', description: `Nothing to see here` }), + // new Container(`A bold "Hero" section at the top with a clear headline, followed by social proof (reviews/logos),
feature blocks, and multiple Calls- to - Action(CTAs) like "Sign Up" or "Buy Now.
- You can also checkout the Blog`), + You can also checkout the Blog + klöxökkökökxl +
+ + + + + `), + new Container('Hello, this is my landing page!').addClassname('pt-5') ] -}).addStyleUrl('/dist/css/materialize.css'); +}); +// CSS +landingPage + .addStyleUrl('/dist/css/materialize.css') + .addStyleUrl('https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined'); +// Client JS +landingPage.addJavascriptUrl('/dist/js/materialize.js'); +landingPage.addJavascript(`M.AutoInit();`); const html = landingPage.toHTML(); console.log(html); diff --git a/index.html b/index.html index 9cf2563e1a..df5fd33439 100644 --- a/index.html +++ b/index.html @@ -148,38 +148,16 @@
Tabs
//------------------------------------- Example (Nested components to widget) - import { createCounterApp } from '/components/button/examples.iso.js'; + import { createCounterApp, hydrate } from './examples/counter-app.iso.js'; const app = createCounterApp(); const appHtml = app.toHTML(); - console.log(appHtml); - - // html => element const template = document.createElement('template'); template.innerHTML = appHtml; const appElem = template.content.firstElementChild; document.querySelector('.app').replaceChildren(appElem); + hydrate(appElem); - function count(input) { - const value = input.value + input.increment; - return { value } - } - - const state = { value: 12 }; - - const numberEl = appElem.querySelector('.mw-number'); // output - const btns = appElem.querySelectorAll('.btn'); // input - - numberEl.innerHTML = state.value; - - btns[0].addEventListener('click', e => { - state.value = count({ value: state.value , increment: 1 }).value; - numberEl.innerHTML = state.value; - }) - btns[1].addEventListener('click', e => { - state.value = count({ value: state.value , increment: -1 }).value; - numberEl.innerHTML = state.value; - }) `).join('\n')} ${this.#scripts.map((s) => ``).join('\n')} diff --git a/examples/html/blog.html b/examples/html/blog.html index 7ec934a27b..7a866f5280 100644 --- a/examples/html/blog.html +++ b/examples/html/blog.html @@ -15,8 +15,8 @@ -
-
+
+

Materialize Web Blog

@@ -46,9 +46,9 @@

Why Choose It for Your Next Project?

This is a test Landingpage. ok it can produce nested html too...
Or you can go to the Portfolio
Sidebar or navigation dropdowns ("Tech," "Lifestyle")
+
-
diff --git a/examples/html/landingpage.html b/examples/html/landingpage.html index 240e27305d..135ae854f0 100644 --- a/examples/html/landingpage.html +++ b/examples/html/landingpage.html @@ -4,18 +4,17 @@ - My Landingpage + T-Shirt Landingpage -
-
+

a

-
Hero Section
+
T-Shirt
A bold "Hero" section at the top with a clear headline, followed by social proof (reviews/logos),
feature blocks, and multiple Calls- to - Action(CTAs) like "Sign Up" or "Buy Now.
You can also checkout the Blog @@ -42,7 +41,6 @@

a

Hello, this is my landing page!
-
diff --git a/examples/html/portfolio.html b/examples/html/portfolio.html index d15f929609..532e795248 100644 --- a/examples/html/portfolio.html +++ b/examples/html/portfolio.html @@ -9,8 +9,7 @@ -
-
+

My Portfolio

@@ -18,7 +17,6 @@

My Portfolio

Sidebar
-
diff --git a/examples/src/blog.js b/examples/src/blog.js index ee3bcbcd10..0ad4eaf369 100644 --- a/examples/src/blog.js +++ b/examples/src/blog.js @@ -35,44 +35,52 @@ const blogPage = new Page({ title: 'Materialize Web Blog', description: 'This is a custom blog', keywords: 'News sites, personal journals, niche resource hubs, and content creators.', - children: [ - // TODO: use NavigationBar Component - new Container({ - children: new Text('Materialize Web Blog').setTagName('p').addClassname('py-3') - }).addClassname('wrapper-1'), + children: new Container({ + children: [ + // TODO: use NavigationBar Component + new Container({ + children: new Text('Materialize Web Blog').setTagName('p').addClassname('py-3') + }).addClassname('wrapper-1'), - new Container({ - children: [ - new AssistChip({ href: './landingpage.html', name: 'Visit Landingpage' }) - .addClassname('tooltipped') // tooltip - .setAttribute('data-tooltip', "I'm a toolip") - .setAttribute('data-position', 'bottom'), - new AssistChip({ href: './portfolio.html', name: 'Visit Porfolio' }) - ] - }) - .addClassname('g-2') - .addClassname('p-2') - .addClassname('secondary') - .addClassname('row'), + new Container({ + children: [ + new AssistChip({ href: './landingpage.html', name: 'Visit Landingpage' }) + .addClassname('tooltipped') // tooltip + .setAttribute('data-tooltip', "I'm a toolip") + .setAttribute('data-position', 'bottom'), + new AssistChip({ href: './portfolio.html', name: 'Visit Porfolio' }) + ] + }) + .addClassname('g-2') + .addClassname('p-2') + .addClassname('secondary') + .addClassname('row'), - new Breadcrumb() - .setCrumbs(['Home', 'Articles', 'Crafting Sleek, Responsive Interfaces with MaterializeWeb']) - .addClassname('py-5'), + new Breadcrumb() + .setCrumbs([ + 'Home', + 'Articles', + 'Crafting Sleek, Responsive Interfaces with MaterializeWeb' + ]) + .addClassname('py-5'), - myArticle, - new Container({ - children: - '2 or 3-column masonry grid showing article thumbnails, titles, and publication dates' - }).addClassname('py-5'), - new Button({ children: 'Call to action' }), - new Text( - `This is a test Landingpage. ok it can produce nested html too...
+ myArticle, + new Container({ + children: + '2 or 3-column masonry grid showing article thumbnails, titles, and publication dates' + }).addClassname('py-5'), + new Button({ children: 'Call to action' }), + new Text( + `This is a test Landingpage. ok it can produce nested html too...
Or you can go to the Portfolio` - ), - new Container({ - children: 'Sidebar or navigation dropdowns ("Tech," "Lifestyle")' - }).addClassname('py-5') - ] + ), + new Container({ + children: 'Sidebar or navigation dropdowns ("Tech," "Lifestyle")' + }).addClassname('py-5') + ] + }) + .setTagName('main') + .addClassname('container') }); // CSS blogPage From ea02aa12042c48cd21ac645c65cc9696a9abeb1c Mon Sep 17 00:00:00 2001 From: Daniel Wurzer Date: Mon, 17 Aug 2026 08:51:45 +0200 Subject: [PATCH 12/16] feat: add drawer --- components/appbar/appbar.mjs | 2 +- components/navigation-drawer/drawer.mjs | 32 +++++++++++++++++++++++++ 2 files changed, 33 insertions(+), 1 deletion(-) create mode 100644 components/navigation-drawer/drawer.mjs diff --git a/components/appbar/appbar.mjs b/components/appbar/appbar.mjs index 75feacd8ee..2b2a9386cd 100644 --- a/components/appbar/appbar.mjs +++ b/components/appbar/appbar.mjs @@ -1,6 +1,6 @@ import { Component } from '../atomic/component.mjs'; -// AppBar is the old *Navbar* +// AppBar is the old "Navbar" class AppBar extends Component { #title = ''; diff --git a/components/navigation-drawer/drawer.mjs b/components/navigation-drawer/drawer.mjs new file mode 100644 index 0000000000..1e3a05bffa --- /dev/null +++ b/components/navigation-drawer/drawer.mjs @@ -0,0 +1,32 @@ +import { Component } from '../atomic/component.mjs'; + +// Drawer = old Sidenav + +class Drawer extends Component { + #items; + + constructor(options) { + super(options); + this.#items = []; + this.setTagName('ul').addClassname('sidenav'); + this.setAttribute('id', 'slide-out'); + } + + addItem(item) { + this.#items = item; + return this; + } + + toHTML() { + const html = `
  • cloudFirst Link With Icon
  • +
  • Second Link
  • +
  • +
  • Subheader
  • +
  • Third Link With Waves
  • `; + // menu + this.setChildren(html); + return super.toHTML(); + } +} + +export { Drawer }; From 522a8e2d220756c24accf5f8ae3866840eb5b46f Mon Sep 17 00:00:00 2001 From: Daniel Wurzer Date: Mon, 17 Aug 2026 11:11:30 +0200 Subject: [PATCH 13/16] fix: drawer and appbar items --- components/appbar/appbar.mjs | 10 ++++++++++ components/navigation-drawer/drawer.mjs | 8 +++++--- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/components/appbar/appbar.mjs b/components/appbar/appbar.mjs index 2b2a9386cd..353865dcc7 100644 --- a/components/appbar/appbar.mjs +++ b/components/appbar/appbar.mjs @@ -4,9 +4,11 @@ import { Component } from '../atomic/component.mjs'; class AppBar extends Component { #title = ''; + #items; constructor(options) { super(options); + this.#items = []; this.setTagName('nav').addClassname('nav navbar'); } @@ -31,15 +33,23 @@ class AppBar extends Component { return this; } + addItem(item) { + this.#items.push(item); + return this; + } + toHTML() { const html = `