From 9fc55a64c7609435c45be15676490e73fdb4dab2 Mon Sep 17 00:00:00 2001 From: Sil Date: Sat, 24 Oct 2026 16:49:30 +0200 Subject: [PATCH 1/5] fix: retain menu requests before media change events --- public/r/styles/base-luma/on-media.json | 2 +- public/r/styles/base-lyra/on-media.json | 2 +- public/r/styles/base-maia/on-media.json | 2 +- public/r/styles/base-mira/on-media.json | 2 +- public/r/styles/base-nova/on-media.json | 2 +- public/r/styles/base-rhea/on-media.json | 2 +- public/r/styles/base-sera/on-media.json | 2 +- public/r/styles/base-vega/on-media.json | 2 +- src/lib/on-media.ts | 8 ++-- tests/on-media.test.mjs | 57 +++++++++++++++++++++++++ 10 files changed, 70 insertions(+), 11 deletions(-) diff --git a/public/r/styles/base-luma/on-media.json b/public/r/styles/base-luma/on-media.json index 69d81ab31..1b397d94c 100644 --- a/public/r/styles/base-luma/on-media.json +++ b/public/r/styles/base-luma/on-media.json @@ -6,7 +6,7 @@ "files": [ { "path": "src/lib/on-media.ts", - "content": "type Trigger = { root: Element; selector: string; event?: string }\n\n/** Import and mount only while the query matches; dispose on exit. */\nexport function onMedia(\n query: string,\n load: () => Promise,\n mount: (module: T) => () => void,\n trigger?: Trigger\n) {\n const media = window.matchMedia(query)\n let disposed = false\n let epoch = 0\n let cleanup: (() => void) | undefined\n let pendingClick: HTMLElement | undefined\n let pendingEvent: CustomEvent | undefined\n\n const replayClick = () => {\n const target = pendingClick\n const event = pendingEvent\n pendingClick = undefined\n pendingEvent = undefined\n if (event && trigger?.root.isConnected)\n trigger.root.dispatchEvent(\n new CustomEvent(event.type, { detail: event.detail })\n )\n else if (target?.isConnected)\n target.dispatchEvent(new MouseEvent(\"click\", { cancelable: true }))\n }\n\n const update = async () => {\n const current = ++epoch\n pendingClick = undefined\n pendingEvent = undefined\n cleanup?.()\n cleanup = undefined\n if (!media.matches || disposed) return\n try {\n const module = await load()\n if (disposed || !media.matches || current !== epoch) return\n cleanup = mount(module)\n replayClick()\n } catch (error) {\n if (!disposed && current === epoch) {\n pendingClick = undefined\n pendingEvent = undefined\n console.error(error)\n }\n }\n }\n const click = (event: Event) => {\n if (\n !trigger ||\n cleanup ||\n disposed ||\n !media.matches ||\n event.defaultPrevented\n )\n return\n if (!(event.target instanceof Element)) return\n const target = event.target.closest(trigger.selector)\n if (!target || !trigger.root.contains(target)) return\n pendingClick ??= target\n pendingEvent = undefined\n event.preventDefault()\n }\n const request = (event: Event) => {\n if (cleanup || disposed || !media.matches || event.target !== trigger?.root)\n return\n if (!(event instanceof CustomEvent)) return\n pendingEvent = event\n pendingClick = undefined\n }\n const change = () => void update()\n media.addEventListener(\"change\", change)\n trigger?.root.addEventListener(\"click\", click, true)\n if (trigger?.event) trigger.root.addEventListener(trigger.event, request)\n change()\n return () => {\n disposed = true\n epoch++\n pendingClick = undefined\n pendingEvent = undefined\n media.removeEventListener(\"change\", change)\n trigger?.root.removeEventListener(\"click\", click, true)\n if (trigger?.event) trigger.root.removeEventListener(trigger.event, request)\n cleanup?.()\n cleanup = undefined\n }\n}\n", + "content": "type Trigger = { root: Element; selector: string; event?: string }\n\n/** Import and mount only while the query matches; dispose on exit. */\nexport function onMedia(\n query: string,\n load: () => Promise,\n mount: (module: T) => () => void,\n trigger?: Trigger\n) {\n const media = window.matchMedia(query)\n let disposed = false\n let epoch = 0\n let cleanup: (() => void) | undefined\n let pendingClick: HTMLElement | undefined\n let pendingEvent: CustomEvent | undefined\n\n const replayClick = () => {\n const target = pendingClick\n const event = pendingEvent\n pendingClick = undefined\n pendingEvent = undefined\n if (event && trigger?.root.isConnected)\n trigger.root.dispatchEvent(\n new CustomEvent(event.type, { detail: event.detail })\n )\n else if (target?.isConnected)\n target.dispatchEvent(new MouseEvent(\"click\", { cancelable: true }))\n }\n\n const update = async () => {\n const current = ++epoch\n cleanup?.()\n cleanup = undefined\n if (!media.matches || disposed) {\n pendingClick = undefined\n pendingEvent = undefined\n return\n }\n try {\n const module = await load()\n if (disposed || !media.matches || current !== epoch) return\n cleanup = mount(module)\n replayClick()\n } catch (error) {\n if (!disposed && current === epoch) {\n pendingClick = undefined\n pendingEvent = undefined\n console.error(error)\n }\n }\n }\n const click = (event: Event) => {\n if (\n !trigger ||\n cleanup ||\n disposed ||\n !media.matches ||\n event.defaultPrevented\n )\n return\n if (!(event.target instanceof Element)) return\n const target = event.target.closest(trigger.selector)\n if (!target || !trigger.root.contains(target)) return\n pendingClick ??= target\n pendingEvent = undefined\n event.preventDefault()\n }\n const request = (event: Event) => {\n if (cleanup || disposed || !media.matches || event.target !== trigger?.root)\n return\n if (!(event instanceof CustomEvent)) return\n pendingEvent = event\n pendingClick = undefined\n }\n const change = () => void update()\n media.addEventListener(\"change\", change)\n trigger?.root.addEventListener(\"click\", click, true)\n if (trigger?.event) trigger.root.addEventListener(trigger.event, request)\n change()\n return () => {\n disposed = true\n epoch++\n pendingClick = undefined\n pendingEvent = undefined\n media.removeEventListener(\"change\", change)\n trigger?.root.removeEventListener(\"click\", click, true)\n if (trigger?.event) trigger.root.removeEventListener(trigger.event, request)\n cleanup?.()\n cleanup = undefined\n }\n}\n", "type": "registry:lib" } ], diff --git a/public/r/styles/base-lyra/on-media.json b/public/r/styles/base-lyra/on-media.json index 69d81ab31..1b397d94c 100644 --- a/public/r/styles/base-lyra/on-media.json +++ b/public/r/styles/base-lyra/on-media.json @@ -6,7 +6,7 @@ "files": [ { "path": "src/lib/on-media.ts", - "content": "type Trigger = { root: Element; selector: string; event?: string }\n\n/** Import and mount only while the query matches; dispose on exit. */\nexport function onMedia(\n query: string,\n load: () => Promise,\n mount: (module: T) => () => void,\n trigger?: Trigger\n) {\n const media = window.matchMedia(query)\n let disposed = false\n let epoch = 0\n let cleanup: (() => void) | undefined\n let pendingClick: HTMLElement | undefined\n let pendingEvent: CustomEvent | undefined\n\n const replayClick = () => {\n const target = pendingClick\n const event = pendingEvent\n pendingClick = undefined\n pendingEvent = undefined\n if (event && trigger?.root.isConnected)\n trigger.root.dispatchEvent(\n new CustomEvent(event.type, { detail: event.detail })\n )\n else if (target?.isConnected)\n target.dispatchEvent(new MouseEvent(\"click\", { cancelable: true }))\n }\n\n const update = async () => {\n const current = ++epoch\n pendingClick = undefined\n pendingEvent = undefined\n cleanup?.()\n cleanup = undefined\n if (!media.matches || disposed) return\n try {\n const module = await load()\n if (disposed || !media.matches || current !== epoch) return\n cleanup = mount(module)\n replayClick()\n } catch (error) {\n if (!disposed && current === epoch) {\n pendingClick = undefined\n pendingEvent = undefined\n console.error(error)\n }\n }\n }\n const click = (event: Event) => {\n if (\n !trigger ||\n cleanup ||\n disposed ||\n !media.matches ||\n event.defaultPrevented\n )\n return\n if (!(event.target instanceof Element)) return\n const target = event.target.closest(trigger.selector)\n if (!target || !trigger.root.contains(target)) return\n pendingClick ??= target\n pendingEvent = undefined\n event.preventDefault()\n }\n const request = (event: Event) => {\n if (cleanup || disposed || !media.matches || event.target !== trigger?.root)\n return\n if (!(event instanceof CustomEvent)) return\n pendingEvent = event\n pendingClick = undefined\n }\n const change = () => void update()\n media.addEventListener(\"change\", change)\n trigger?.root.addEventListener(\"click\", click, true)\n if (trigger?.event) trigger.root.addEventListener(trigger.event, request)\n change()\n return () => {\n disposed = true\n epoch++\n pendingClick = undefined\n pendingEvent = undefined\n media.removeEventListener(\"change\", change)\n trigger?.root.removeEventListener(\"click\", click, true)\n if (trigger?.event) trigger.root.removeEventListener(trigger.event, request)\n cleanup?.()\n cleanup = undefined\n }\n}\n", + "content": "type Trigger = { root: Element; selector: string; event?: string }\n\n/** Import and mount only while the query matches; dispose on exit. */\nexport function onMedia(\n query: string,\n load: () => Promise,\n mount: (module: T) => () => void,\n trigger?: Trigger\n) {\n const media = window.matchMedia(query)\n let disposed = false\n let epoch = 0\n let cleanup: (() => void) | undefined\n let pendingClick: HTMLElement | undefined\n let pendingEvent: CustomEvent | undefined\n\n const replayClick = () => {\n const target = pendingClick\n const event = pendingEvent\n pendingClick = undefined\n pendingEvent = undefined\n if (event && trigger?.root.isConnected)\n trigger.root.dispatchEvent(\n new CustomEvent(event.type, { detail: event.detail })\n )\n else if (target?.isConnected)\n target.dispatchEvent(new MouseEvent(\"click\", { cancelable: true }))\n }\n\n const update = async () => {\n const current = ++epoch\n cleanup?.()\n cleanup = undefined\n if (!media.matches || disposed) {\n pendingClick = undefined\n pendingEvent = undefined\n return\n }\n try {\n const module = await load()\n if (disposed || !media.matches || current !== epoch) return\n cleanup = mount(module)\n replayClick()\n } catch (error) {\n if (!disposed && current === epoch) {\n pendingClick = undefined\n pendingEvent = undefined\n console.error(error)\n }\n }\n }\n const click = (event: Event) => {\n if (\n !trigger ||\n cleanup ||\n disposed ||\n !media.matches ||\n event.defaultPrevented\n )\n return\n if (!(event.target instanceof Element)) return\n const target = event.target.closest(trigger.selector)\n if (!target || !trigger.root.contains(target)) return\n pendingClick ??= target\n pendingEvent = undefined\n event.preventDefault()\n }\n const request = (event: Event) => {\n if (cleanup || disposed || !media.matches || event.target !== trigger?.root)\n return\n if (!(event instanceof CustomEvent)) return\n pendingEvent = event\n pendingClick = undefined\n }\n const change = () => void update()\n media.addEventListener(\"change\", change)\n trigger?.root.addEventListener(\"click\", click, true)\n if (trigger?.event) trigger.root.addEventListener(trigger.event, request)\n change()\n return () => {\n disposed = true\n epoch++\n pendingClick = undefined\n pendingEvent = undefined\n media.removeEventListener(\"change\", change)\n trigger?.root.removeEventListener(\"click\", click, true)\n if (trigger?.event) trigger.root.removeEventListener(trigger.event, request)\n cleanup?.()\n cleanup = undefined\n }\n}\n", "type": "registry:lib" } ], diff --git a/public/r/styles/base-maia/on-media.json b/public/r/styles/base-maia/on-media.json index 69d81ab31..1b397d94c 100644 --- a/public/r/styles/base-maia/on-media.json +++ b/public/r/styles/base-maia/on-media.json @@ -6,7 +6,7 @@ "files": [ { "path": "src/lib/on-media.ts", - "content": "type Trigger = { root: Element; selector: string; event?: string }\n\n/** Import and mount only while the query matches; dispose on exit. */\nexport function onMedia(\n query: string,\n load: () => Promise,\n mount: (module: T) => () => void,\n trigger?: Trigger\n) {\n const media = window.matchMedia(query)\n let disposed = false\n let epoch = 0\n let cleanup: (() => void) | undefined\n let pendingClick: HTMLElement | undefined\n let pendingEvent: CustomEvent | undefined\n\n const replayClick = () => {\n const target = pendingClick\n const event = pendingEvent\n pendingClick = undefined\n pendingEvent = undefined\n if (event && trigger?.root.isConnected)\n trigger.root.dispatchEvent(\n new CustomEvent(event.type, { detail: event.detail })\n )\n else if (target?.isConnected)\n target.dispatchEvent(new MouseEvent(\"click\", { cancelable: true }))\n }\n\n const update = async () => {\n const current = ++epoch\n pendingClick = undefined\n pendingEvent = undefined\n cleanup?.()\n cleanup = undefined\n if (!media.matches || disposed) return\n try {\n const module = await load()\n if (disposed || !media.matches || current !== epoch) return\n cleanup = mount(module)\n replayClick()\n } catch (error) {\n if (!disposed && current === epoch) {\n pendingClick = undefined\n pendingEvent = undefined\n console.error(error)\n }\n }\n }\n const click = (event: Event) => {\n if (\n !trigger ||\n cleanup ||\n disposed ||\n !media.matches ||\n event.defaultPrevented\n )\n return\n if (!(event.target instanceof Element)) return\n const target = event.target.closest(trigger.selector)\n if (!target || !trigger.root.contains(target)) return\n pendingClick ??= target\n pendingEvent = undefined\n event.preventDefault()\n }\n const request = (event: Event) => {\n if (cleanup || disposed || !media.matches || event.target !== trigger?.root)\n return\n if (!(event instanceof CustomEvent)) return\n pendingEvent = event\n pendingClick = undefined\n }\n const change = () => void update()\n media.addEventListener(\"change\", change)\n trigger?.root.addEventListener(\"click\", click, true)\n if (trigger?.event) trigger.root.addEventListener(trigger.event, request)\n change()\n return () => {\n disposed = true\n epoch++\n pendingClick = undefined\n pendingEvent = undefined\n media.removeEventListener(\"change\", change)\n trigger?.root.removeEventListener(\"click\", click, true)\n if (trigger?.event) trigger.root.removeEventListener(trigger.event, request)\n cleanup?.()\n cleanup = undefined\n }\n}\n", + "content": "type Trigger = { root: Element; selector: string; event?: string }\n\n/** Import and mount only while the query matches; dispose on exit. */\nexport function onMedia(\n query: string,\n load: () => Promise,\n mount: (module: T) => () => void,\n trigger?: Trigger\n) {\n const media = window.matchMedia(query)\n let disposed = false\n let epoch = 0\n let cleanup: (() => void) | undefined\n let pendingClick: HTMLElement | undefined\n let pendingEvent: CustomEvent | undefined\n\n const replayClick = () => {\n const target = pendingClick\n const event = pendingEvent\n pendingClick = undefined\n pendingEvent = undefined\n if (event && trigger?.root.isConnected)\n trigger.root.dispatchEvent(\n new CustomEvent(event.type, { detail: event.detail })\n )\n else if (target?.isConnected)\n target.dispatchEvent(new MouseEvent(\"click\", { cancelable: true }))\n }\n\n const update = async () => {\n const current = ++epoch\n cleanup?.()\n cleanup = undefined\n if (!media.matches || disposed) {\n pendingClick = undefined\n pendingEvent = undefined\n return\n }\n try {\n const module = await load()\n if (disposed || !media.matches || current !== epoch) return\n cleanup = mount(module)\n replayClick()\n } catch (error) {\n if (!disposed && current === epoch) {\n pendingClick = undefined\n pendingEvent = undefined\n console.error(error)\n }\n }\n }\n const click = (event: Event) => {\n if (\n !trigger ||\n cleanup ||\n disposed ||\n !media.matches ||\n event.defaultPrevented\n )\n return\n if (!(event.target instanceof Element)) return\n const target = event.target.closest(trigger.selector)\n if (!target || !trigger.root.contains(target)) return\n pendingClick ??= target\n pendingEvent = undefined\n event.preventDefault()\n }\n const request = (event: Event) => {\n if (cleanup || disposed || !media.matches || event.target !== trigger?.root)\n return\n if (!(event instanceof CustomEvent)) return\n pendingEvent = event\n pendingClick = undefined\n }\n const change = () => void update()\n media.addEventListener(\"change\", change)\n trigger?.root.addEventListener(\"click\", click, true)\n if (trigger?.event) trigger.root.addEventListener(trigger.event, request)\n change()\n return () => {\n disposed = true\n epoch++\n pendingClick = undefined\n pendingEvent = undefined\n media.removeEventListener(\"change\", change)\n trigger?.root.removeEventListener(\"click\", click, true)\n if (trigger?.event) trigger.root.removeEventListener(trigger.event, request)\n cleanup?.()\n cleanup = undefined\n }\n}\n", "type": "registry:lib" } ], diff --git a/public/r/styles/base-mira/on-media.json b/public/r/styles/base-mira/on-media.json index 69d81ab31..1b397d94c 100644 --- a/public/r/styles/base-mira/on-media.json +++ b/public/r/styles/base-mira/on-media.json @@ -6,7 +6,7 @@ "files": [ { "path": "src/lib/on-media.ts", - "content": "type Trigger = { root: Element; selector: string; event?: string }\n\n/** Import and mount only while the query matches; dispose on exit. */\nexport function onMedia(\n query: string,\n load: () => Promise,\n mount: (module: T) => () => void,\n trigger?: Trigger\n) {\n const media = window.matchMedia(query)\n let disposed = false\n let epoch = 0\n let cleanup: (() => void) | undefined\n let pendingClick: HTMLElement | undefined\n let pendingEvent: CustomEvent | undefined\n\n const replayClick = () => {\n const target = pendingClick\n const event = pendingEvent\n pendingClick = undefined\n pendingEvent = undefined\n if (event && trigger?.root.isConnected)\n trigger.root.dispatchEvent(\n new CustomEvent(event.type, { detail: event.detail })\n )\n else if (target?.isConnected)\n target.dispatchEvent(new MouseEvent(\"click\", { cancelable: true }))\n }\n\n const update = async () => {\n const current = ++epoch\n pendingClick = undefined\n pendingEvent = undefined\n cleanup?.()\n cleanup = undefined\n if (!media.matches || disposed) return\n try {\n const module = await load()\n if (disposed || !media.matches || current !== epoch) return\n cleanup = mount(module)\n replayClick()\n } catch (error) {\n if (!disposed && current === epoch) {\n pendingClick = undefined\n pendingEvent = undefined\n console.error(error)\n }\n }\n }\n const click = (event: Event) => {\n if (\n !trigger ||\n cleanup ||\n disposed ||\n !media.matches ||\n event.defaultPrevented\n )\n return\n if (!(event.target instanceof Element)) return\n const target = event.target.closest(trigger.selector)\n if (!target || !trigger.root.contains(target)) return\n pendingClick ??= target\n pendingEvent = undefined\n event.preventDefault()\n }\n const request = (event: Event) => {\n if (cleanup || disposed || !media.matches || event.target !== trigger?.root)\n return\n if (!(event instanceof CustomEvent)) return\n pendingEvent = event\n pendingClick = undefined\n }\n const change = () => void update()\n media.addEventListener(\"change\", change)\n trigger?.root.addEventListener(\"click\", click, true)\n if (trigger?.event) trigger.root.addEventListener(trigger.event, request)\n change()\n return () => {\n disposed = true\n epoch++\n pendingClick = undefined\n pendingEvent = undefined\n media.removeEventListener(\"change\", change)\n trigger?.root.removeEventListener(\"click\", click, true)\n if (trigger?.event) trigger.root.removeEventListener(trigger.event, request)\n cleanup?.()\n cleanup = undefined\n }\n}\n", + "content": "type Trigger = { root: Element; selector: string; event?: string }\n\n/** Import and mount only while the query matches; dispose on exit. */\nexport function onMedia(\n query: string,\n load: () => Promise,\n mount: (module: T) => () => void,\n trigger?: Trigger\n) {\n const media = window.matchMedia(query)\n let disposed = false\n let epoch = 0\n let cleanup: (() => void) | undefined\n let pendingClick: HTMLElement | undefined\n let pendingEvent: CustomEvent | undefined\n\n const replayClick = () => {\n const target = pendingClick\n const event = pendingEvent\n pendingClick = undefined\n pendingEvent = undefined\n if (event && trigger?.root.isConnected)\n trigger.root.dispatchEvent(\n new CustomEvent(event.type, { detail: event.detail })\n )\n else if (target?.isConnected)\n target.dispatchEvent(new MouseEvent(\"click\", { cancelable: true }))\n }\n\n const update = async () => {\n const current = ++epoch\n cleanup?.()\n cleanup = undefined\n if (!media.matches || disposed) {\n pendingClick = undefined\n pendingEvent = undefined\n return\n }\n try {\n const module = await load()\n if (disposed || !media.matches || current !== epoch) return\n cleanup = mount(module)\n replayClick()\n } catch (error) {\n if (!disposed && current === epoch) {\n pendingClick = undefined\n pendingEvent = undefined\n console.error(error)\n }\n }\n }\n const click = (event: Event) => {\n if (\n !trigger ||\n cleanup ||\n disposed ||\n !media.matches ||\n event.defaultPrevented\n )\n return\n if (!(event.target instanceof Element)) return\n const target = event.target.closest(trigger.selector)\n if (!target || !trigger.root.contains(target)) return\n pendingClick ??= target\n pendingEvent = undefined\n event.preventDefault()\n }\n const request = (event: Event) => {\n if (cleanup || disposed || !media.matches || event.target !== trigger?.root)\n return\n if (!(event instanceof CustomEvent)) return\n pendingEvent = event\n pendingClick = undefined\n }\n const change = () => void update()\n media.addEventListener(\"change\", change)\n trigger?.root.addEventListener(\"click\", click, true)\n if (trigger?.event) trigger.root.addEventListener(trigger.event, request)\n change()\n return () => {\n disposed = true\n epoch++\n pendingClick = undefined\n pendingEvent = undefined\n media.removeEventListener(\"change\", change)\n trigger?.root.removeEventListener(\"click\", click, true)\n if (trigger?.event) trigger.root.removeEventListener(trigger.event, request)\n cleanup?.()\n cleanup = undefined\n }\n}\n", "type": "registry:lib" } ], diff --git a/public/r/styles/base-nova/on-media.json b/public/r/styles/base-nova/on-media.json index 69d81ab31..1b397d94c 100644 --- a/public/r/styles/base-nova/on-media.json +++ b/public/r/styles/base-nova/on-media.json @@ -6,7 +6,7 @@ "files": [ { "path": "src/lib/on-media.ts", - "content": "type Trigger = { root: Element; selector: string; event?: string }\n\n/** Import and mount only while the query matches; dispose on exit. */\nexport function onMedia(\n query: string,\n load: () => Promise,\n mount: (module: T) => () => void,\n trigger?: Trigger\n) {\n const media = window.matchMedia(query)\n let disposed = false\n let epoch = 0\n let cleanup: (() => void) | undefined\n let pendingClick: HTMLElement | undefined\n let pendingEvent: CustomEvent | undefined\n\n const replayClick = () => {\n const target = pendingClick\n const event = pendingEvent\n pendingClick = undefined\n pendingEvent = undefined\n if (event && trigger?.root.isConnected)\n trigger.root.dispatchEvent(\n new CustomEvent(event.type, { detail: event.detail })\n )\n else if (target?.isConnected)\n target.dispatchEvent(new MouseEvent(\"click\", { cancelable: true }))\n }\n\n const update = async () => {\n const current = ++epoch\n pendingClick = undefined\n pendingEvent = undefined\n cleanup?.()\n cleanup = undefined\n if (!media.matches || disposed) return\n try {\n const module = await load()\n if (disposed || !media.matches || current !== epoch) return\n cleanup = mount(module)\n replayClick()\n } catch (error) {\n if (!disposed && current === epoch) {\n pendingClick = undefined\n pendingEvent = undefined\n console.error(error)\n }\n }\n }\n const click = (event: Event) => {\n if (\n !trigger ||\n cleanup ||\n disposed ||\n !media.matches ||\n event.defaultPrevented\n )\n return\n if (!(event.target instanceof Element)) return\n const target = event.target.closest(trigger.selector)\n if (!target || !trigger.root.contains(target)) return\n pendingClick ??= target\n pendingEvent = undefined\n event.preventDefault()\n }\n const request = (event: Event) => {\n if (cleanup || disposed || !media.matches || event.target !== trigger?.root)\n return\n if (!(event instanceof CustomEvent)) return\n pendingEvent = event\n pendingClick = undefined\n }\n const change = () => void update()\n media.addEventListener(\"change\", change)\n trigger?.root.addEventListener(\"click\", click, true)\n if (trigger?.event) trigger.root.addEventListener(trigger.event, request)\n change()\n return () => {\n disposed = true\n epoch++\n pendingClick = undefined\n pendingEvent = undefined\n media.removeEventListener(\"change\", change)\n trigger?.root.removeEventListener(\"click\", click, true)\n if (trigger?.event) trigger.root.removeEventListener(trigger.event, request)\n cleanup?.()\n cleanup = undefined\n }\n}\n", + "content": "type Trigger = { root: Element; selector: string; event?: string }\n\n/** Import and mount only while the query matches; dispose on exit. */\nexport function onMedia(\n query: string,\n load: () => Promise,\n mount: (module: T) => () => void,\n trigger?: Trigger\n) {\n const media = window.matchMedia(query)\n let disposed = false\n let epoch = 0\n let cleanup: (() => void) | undefined\n let pendingClick: HTMLElement | undefined\n let pendingEvent: CustomEvent | undefined\n\n const replayClick = () => {\n const target = pendingClick\n const event = pendingEvent\n pendingClick = undefined\n pendingEvent = undefined\n if (event && trigger?.root.isConnected)\n trigger.root.dispatchEvent(\n new CustomEvent(event.type, { detail: event.detail })\n )\n else if (target?.isConnected)\n target.dispatchEvent(new MouseEvent(\"click\", { cancelable: true }))\n }\n\n const update = async () => {\n const current = ++epoch\n cleanup?.()\n cleanup = undefined\n if (!media.matches || disposed) {\n pendingClick = undefined\n pendingEvent = undefined\n return\n }\n try {\n const module = await load()\n if (disposed || !media.matches || current !== epoch) return\n cleanup = mount(module)\n replayClick()\n } catch (error) {\n if (!disposed && current === epoch) {\n pendingClick = undefined\n pendingEvent = undefined\n console.error(error)\n }\n }\n }\n const click = (event: Event) => {\n if (\n !trigger ||\n cleanup ||\n disposed ||\n !media.matches ||\n event.defaultPrevented\n )\n return\n if (!(event.target instanceof Element)) return\n const target = event.target.closest(trigger.selector)\n if (!target || !trigger.root.contains(target)) return\n pendingClick ??= target\n pendingEvent = undefined\n event.preventDefault()\n }\n const request = (event: Event) => {\n if (cleanup || disposed || !media.matches || event.target !== trigger?.root)\n return\n if (!(event instanceof CustomEvent)) return\n pendingEvent = event\n pendingClick = undefined\n }\n const change = () => void update()\n media.addEventListener(\"change\", change)\n trigger?.root.addEventListener(\"click\", click, true)\n if (trigger?.event) trigger.root.addEventListener(trigger.event, request)\n change()\n return () => {\n disposed = true\n epoch++\n pendingClick = undefined\n pendingEvent = undefined\n media.removeEventListener(\"change\", change)\n trigger?.root.removeEventListener(\"click\", click, true)\n if (trigger?.event) trigger.root.removeEventListener(trigger.event, request)\n cleanup?.()\n cleanup = undefined\n }\n}\n", "type": "registry:lib" } ], diff --git a/public/r/styles/base-rhea/on-media.json b/public/r/styles/base-rhea/on-media.json index 69d81ab31..1b397d94c 100644 --- a/public/r/styles/base-rhea/on-media.json +++ b/public/r/styles/base-rhea/on-media.json @@ -6,7 +6,7 @@ "files": [ { "path": "src/lib/on-media.ts", - "content": "type Trigger = { root: Element; selector: string; event?: string }\n\n/** Import and mount only while the query matches; dispose on exit. */\nexport function onMedia(\n query: string,\n load: () => Promise,\n mount: (module: T) => () => void,\n trigger?: Trigger\n) {\n const media = window.matchMedia(query)\n let disposed = false\n let epoch = 0\n let cleanup: (() => void) | undefined\n let pendingClick: HTMLElement | undefined\n let pendingEvent: CustomEvent | undefined\n\n const replayClick = () => {\n const target = pendingClick\n const event = pendingEvent\n pendingClick = undefined\n pendingEvent = undefined\n if (event && trigger?.root.isConnected)\n trigger.root.dispatchEvent(\n new CustomEvent(event.type, { detail: event.detail })\n )\n else if (target?.isConnected)\n target.dispatchEvent(new MouseEvent(\"click\", { cancelable: true }))\n }\n\n const update = async () => {\n const current = ++epoch\n pendingClick = undefined\n pendingEvent = undefined\n cleanup?.()\n cleanup = undefined\n if (!media.matches || disposed) return\n try {\n const module = await load()\n if (disposed || !media.matches || current !== epoch) return\n cleanup = mount(module)\n replayClick()\n } catch (error) {\n if (!disposed && current === epoch) {\n pendingClick = undefined\n pendingEvent = undefined\n console.error(error)\n }\n }\n }\n const click = (event: Event) => {\n if (\n !trigger ||\n cleanup ||\n disposed ||\n !media.matches ||\n event.defaultPrevented\n )\n return\n if (!(event.target instanceof Element)) return\n const target = event.target.closest(trigger.selector)\n if (!target || !trigger.root.contains(target)) return\n pendingClick ??= target\n pendingEvent = undefined\n event.preventDefault()\n }\n const request = (event: Event) => {\n if (cleanup || disposed || !media.matches || event.target !== trigger?.root)\n return\n if (!(event instanceof CustomEvent)) return\n pendingEvent = event\n pendingClick = undefined\n }\n const change = () => void update()\n media.addEventListener(\"change\", change)\n trigger?.root.addEventListener(\"click\", click, true)\n if (trigger?.event) trigger.root.addEventListener(trigger.event, request)\n change()\n return () => {\n disposed = true\n epoch++\n pendingClick = undefined\n pendingEvent = undefined\n media.removeEventListener(\"change\", change)\n trigger?.root.removeEventListener(\"click\", click, true)\n if (trigger?.event) trigger.root.removeEventListener(trigger.event, request)\n cleanup?.()\n cleanup = undefined\n }\n}\n", + "content": "type Trigger = { root: Element; selector: string; event?: string }\n\n/** Import and mount only while the query matches; dispose on exit. */\nexport function onMedia(\n query: string,\n load: () => Promise,\n mount: (module: T) => () => void,\n trigger?: Trigger\n) {\n const media = window.matchMedia(query)\n let disposed = false\n let epoch = 0\n let cleanup: (() => void) | undefined\n let pendingClick: HTMLElement | undefined\n let pendingEvent: CustomEvent | undefined\n\n const replayClick = () => {\n const target = pendingClick\n const event = pendingEvent\n pendingClick = undefined\n pendingEvent = undefined\n if (event && trigger?.root.isConnected)\n trigger.root.dispatchEvent(\n new CustomEvent(event.type, { detail: event.detail })\n )\n else if (target?.isConnected)\n target.dispatchEvent(new MouseEvent(\"click\", { cancelable: true }))\n }\n\n const update = async () => {\n const current = ++epoch\n cleanup?.()\n cleanup = undefined\n if (!media.matches || disposed) {\n pendingClick = undefined\n pendingEvent = undefined\n return\n }\n try {\n const module = await load()\n if (disposed || !media.matches || current !== epoch) return\n cleanup = mount(module)\n replayClick()\n } catch (error) {\n if (!disposed && current === epoch) {\n pendingClick = undefined\n pendingEvent = undefined\n console.error(error)\n }\n }\n }\n const click = (event: Event) => {\n if (\n !trigger ||\n cleanup ||\n disposed ||\n !media.matches ||\n event.defaultPrevented\n )\n return\n if (!(event.target instanceof Element)) return\n const target = event.target.closest(trigger.selector)\n if (!target || !trigger.root.contains(target)) return\n pendingClick ??= target\n pendingEvent = undefined\n event.preventDefault()\n }\n const request = (event: Event) => {\n if (cleanup || disposed || !media.matches || event.target !== trigger?.root)\n return\n if (!(event instanceof CustomEvent)) return\n pendingEvent = event\n pendingClick = undefined\n }\n const change = () => void update()\n media.addEventListener(\"change\", change)\n trigger?.root.addEventListener(\"click\", click, true)\n if (trigger?.event) trigger.root.addEventListener(trigger.event, request)\n change()\n return () => {\n disposed = true\n epoch++\n pendingClick = undefined\n pendingEvent = undefined\n media.removeEventListener(\"change\", change)\n trigger?.root.removeEventListener(\"click\", click, true)\n if (trigger?.event) trigger.root.removeEventListener(trigger.event, request)\n cleanup?.()\n cleanup = undefined\n }\n}\n", "type": "registry:lib" } ], diff --git a/public/r/styles/base-sera/on-media.json b/public/r/styles/base-sera/on-media.json index 69d81ab31..1b397d94c 100644 --- a/public/r/styles/base-sera/on-media.json +++ b/public/r/styles/base-sera/on-media.json @@ -6,7 +6,7 @@ "files": [ { "path": "src/lib/on-media.ts", - "content": "type Trigger = { root: Element; selector: string; event?: string }\n\n/** Import and mount only while the query matches; dispose on exit. */\nexport function onMedia(\n query: string,\n load: () => Promise,\n mount: (module: T) => () => void,\n trigger?: Trigger\n) {\n const media = window.matchMedia(query)\n let disposed = false\n let epoch = 0\n let cleanup: (() => void) | undefined\n let pendingClick: HTMLElement | undefined\n let pendingEvent: CustomEvent | undefined\n\n const replayClick = () => {\n const target = pendingClick\n const event = pendingEvent\n pendingClick = undefined\n pendingEvent = undefined\n if (event && trigger?.root.isConnected)\n trigger.root.dispatchEvent(\n new CustomEvent(event.type, { detail: event.detail })\n )\n else if (target?.isConnected)\n target.dispatchEvent(new MouseEvent(\"click\", { cancelable: true }))\n }\n\n const update = async () => {\n const current = ++epoch\n pendingClick = undefined\n pendingEvent = undefined\n cleanup?.()\n cleanup = undefined\n if (!media.matches || disposed) return\n try {\n const module = await load()\n if (disposed || !media.matches || current !== epoch) return\n cleanup = mount(module)\n replayClick()\n } catch (error) {\n if (!disposed && current === epoch) {\n pendingClick = undefined\n pendingEvent = undefined\n console.error(error)\n }\n }\n }\n const click = (event: Event) => {\n if (\n !trigger ||\n cleanup ||\n disposed ||\n !media.matches ||\n event.defaultPrevented\n )\n return\n if (!(event.target instanceof Element)) return\n const target = event.target.closest(trigger.selector)\n if (!target || !trigger.root.contains(target)) return\n pendingClick ??= target\n pendingEvent = undefined\n event.preventDefault()\n }\n const request = (event: Event) => {\n if (cleanup || disposed || !media.matches || event.target !== trigger?.root)\n return\n if (!(event instanceof CustomEvent)) return\n pendingEvent = event\n pendingClick = undefined\n }\n const change = () => void update()\n media.addEventListener(\"change\", change)\n trigger?.root.addEventListener(\"click\", click, true)\n if (trigger?.event) trigger.root.addEventListener(trigger.event, request)\n change()\n return () => {\n disposed = true\n epoch++\n pendingClick = undefined\n pendingEvent = undefined\n media.removeEventListener(\"change\", change)\n trigger?.root.removeEventListener(\"click\", click, true)\n if (trigger?.event) trigger.root.removeEventListener(trigger.event, request)\n cleanup?.()\n cleanup = undefined\n }\n}\n", + "content": "type Trigger = { root: Element; selector: string; event?: string }\n\n/** Import and mount only while the query matches; dispose on exit. */\nexport function onMedia(\n query: string,\n load: () => Promise,\n mount: (module: T) => () => void,\n trigger?: Trigger\n) {\n const media = window.matchMedia(query)\n let disposed = false\n let epoch = 0\n let cleanup: (() => void) | undefined\n let pendingClick: HTMLElement | undefined\n let pendingEvent: CustomEvent | undefined\n\n const replayClick = () => {\n const target = pendingClick\n const event = pendingEvent\n pendingClick = undefined\n pendingEvent = undefined\n if (event && trigger?.root.isConnected)\n trigger.root.dispatchEvent(\n new CustomEvent(event.type, { detail: event.detail })\n )\n else if (target?.isConnected)\n target.dispatchEvent(new MouseEvent(\"click\", { cancelable: true }))\n }\n\n const update = async () => {\n const current = ++epoch\n cleanup?.()\n cleanup = undefined\n if (!media.matches || disposed) {\n pendingClick = undefined\n pendingEvent = undefined\n return\n }\n try {\n const module = await load()\n if (disposed || !media.matches || current !== epoch) return\n cleanup = mount(module)\n replayClick()\n } catch (error) {\n if (!disposed && current === epoch) {\n pendingClick = undefined\n pendingEvent = undefined\n console.error(error)\n }\n }\n }\n const click = (event: Event) => {\n if (\n !trigger ||\n cleanup ||\n disposed ||\n !media.matches ||\n event.defaultPrevented\n )\n return\n if (!(event.target instanceof Element)) return\n const target = event.target.closest(trigger.selector)\n if (!target || !trigger.root.contains(target)) return\n pendingClick ??= target\n pendingEvent = undefined\n event.preventDefault()\n }\n const request = (event: Event) => {\n if (cleanup || disposed || !media.matches || event.target !== trigger?.root)\n return\n if (!(event instanceof CustomEvent)) return\n pendingEvent = event\n pendingClick = undefined\n }\n const change = () => void update()\n media.addEventListener(\"change\", change)\n trigger?.root.addEventListener(\"click\", click, true)\n if (trigger?.event) trigger.root.addEventListener(trigger.event, request)\n change()\n return () => {\n disposed = true\n epoch++\n pendingClick = undefined\n pendingEvent = undefined\n media.removeEventListener(\"change\", change)\n trigger?.root.removeEventListener(\"click\", click, true)\n if (trigger?.event) trigger.root.removeEventListener(trigger.event, request)\n cleanup?.()\n cleanup = undefined\n }\n}\n", "type": "registry:lib" } ], diff --git a/public/r/styles/base-vega/on-media.json b/public/r/styles/base-vega/on-media.json index 69d81ab31..1b397d94c 100644 --- a/public/r/styles/base-vega/on-media.json +++ b/public/r/styles/base-vega/on-media.json @@ -6,7 +6,7 @@ "files": [ { "path": "src/lib/on-media.ts", - "content": "type Trigger = { root: Element; selector: string; event?: string }\n\n/** Import and mount only while the query matches; dispose on exit. */\nexport function onMedia(\n query: string,\n load: () => Promise,\n mount: (module: T) => () => void,\n trigger?: Trigger\n) {\n const media = window.matchMedia(query)\n let disposed = false\n let epoch = 0\n let cleanup: (() => void) | undefined\n let pendingClick: HTMLElement | undefined\n let pendingEvent: CustomEvent | undefined\n\n const replayClick = () => {\n const target = pendingClick\n const event = pendingEvent\n pendingClick = undefined\n pendingEvent = undefined\n if (event && trigger?.root.isConnected)\n trigger.root.dispatchEvent(\n new CustomEvent(event.type, { detail: event.detail })\n )\n else if (target?.isConnected)\n target.dispatchEvent(new MouseEvent(\"click\", { cancelable: true }))\n }\n\n const update = async () => {\n const current = ++epoch\n pendingClick = undefined\n pendingEvent = undefined\n cleanup?.()\n cleanup = undefined\n if (!media.matches || disposed) return\n try {\n const module = await load()\n if (disposed || !media.matches || current !== epoch) return\n cleanup = mount(module)\n replayClick()\n } catch (error) {\n if (!disposed && current === epoch) {\n pendingClick = undefined\n pendingEvent = undefined\n console.error(error)\n }\n }\n }\n const click = (event: Event) => {\n if (\n !trigger ||\n cleanup ||\n disposed ||\n !media.matches ||\n event.defaultPrevented\n )\n return\n if (!(event.target instanceof Element)) return\n const target = event.target.closest(trigger.selector)\n if (!target || !trigger.root.contains(target)) return\n pendingClick ??= target\n pendingEvent = undefined\n event.preventDefault()\n }\n const request = (event: Event) => {\n if (cleanup || disposed || !media.matches || event.target !== trigger?.root)\n return\n if (!(event instanceof CustomEvent)) return\n pendingEvent = event\n pendingClick = undefined\n }\n const change = () => void update()\n media.addEventListener(\"change\", change)\n trigger?.root.addEventListener(\"click\", click, true)\n if (trigger?.event) trigger.root.addEventListener(trigger.event, request)\n change()\n return () => {\n disposed = true\n epoch++\n pendingClick = undefined\n pendingEvent = undefined\n media.removeEventListener(\"change\", change)\n trigger?.root.removeEventListener(\"click\", click, true)\n if (trigger?.event) trigger.root.removeEventListener(trigger.event, request)\n cleanup?.()\n cleanup = undefined\n }\n}\n", + "content": "type Trigger = { root: Element; selector: string; event?: string }\n\n/** Import and mount only while the query matches; dispose on exit. */\nexport function onMedia(\n query: string,\n load: () => Promise,\n mount: (module: T) => () => void,\n trigger?: Trigger\n) {\n const media = window.matchMedia(query)\n let disposed = false\n let epoch = 0\n let cleanup: (() => void) | undefined\n let pendingClick: HTMLElement | undefined\n let pendingEvent: CustomEvent | undefined\n\n const replayClick = () => {\n const target = pendingClick\n const event = pendingEvent\n pendingClick = undefined\n pendingEvent = undefined\n if (event && trigger?.root.isConnected)\n trigger.root.dispatchEvent(\n new CustomEvent(event.type, { detail: event.detail })\n )\n else if (target?.isConnected)\n target.dispatchEvent(new MouseEvent(\"click\", { cancelable: true }))\n }\n\n const update = async () => {\n const current = ++epoch\n cleanup?.()\n cleanup = undefined\n if (!media.matches || disposed) {\n pendingClick = undefined\n pendingEvent = undefined\n return\n }\n try {\n const module = await load()\n if (disposed || !media.matches || current !== epoch) return\n cleanup = mount(module)\n replayClick()\n } catch (error) {\n if (!disposed && current === epoch) {\n pendingClick = undefined\n pendingEvent = undefined\n console.error(error)\n }\n }\n }\n const click = (event: Event) => {\n if (\n !trigger ||\n cleanup ||\n disposed ||\n !media.matches ||\n event.defaultPrevented\n )\n return\n if (!(event.target instanceof Element)) return\n const target = event.target.closest(trigger.selector)\n if (!target || !trigger.root.contains(target)) return\n pendingClick ??= target\n pendingEvent = undefined\n event.preventDefault()\n }\n const request = (event: Event) => {\n if (cleanup || disposed || !media.matches || event.target !== trigger?.root)\n return\n if (!(event instanceof CustomEvent)) return\n pendingEvent = event\n pendingClick = undefined\n }\n const change = () => void update()\n media.addEventListener(\"change\", change)\n trigger?.root.addEventListener(\"click\", click, true)\n if (trigger?.event) trigger.root.addEventListener(trigger.event, request)\n change()\n return () => {\n disposed = true\n epoch++\n pendingClick = undefined\n pendingEvent = undefined\n media.removeEventListener(\"change\", change)\n trigger?.root.removeEventListener(\"click\", click, true)\n if (trigger?.event) trigger.root.removeEventListener(trigger.event, request)\n cleanup?.()\n cleanup = undefined\n }\n}\n", "type": "registry:lib" } ], diff --git a/src/lib/on-media.ts b/src/lib/on-media.ts index 8baf71cf2..e0f19eddc 100644 --- a/src/lib/on-media.ts +++ b/src/lib/on-media.ts @@ -29,11 +29,13 @@ export function onMedia( const update = async () => { const current = ++epoch - pendingClick = undefined - pendingEvent = undefined cleanup?.() cleanup = undefined - if (!media.matches || disposed) return + if (!media.matches || disposed) { + pendingClick = undefined + pendingEvent = undefined + return + } try { const module = await load() if (disposed || !media.matches || current !== epoch) return diff --git a/tests/on-media.test.mjs b/tests/on-media.test.mjs index 358a37671..36ab458a4 100644 --- a/tests/on-media.test.mjs +++ b/tests/on-media.test.mjs @@ -80,3 +80,60 @@ test("disposing during import prevents a later mount", async () => { assert.equal(mounts, 0) globalThis.window = previous }) + +for (const action of ["click", "request"]) + test(`retains ${action} before the media change event arrives`, async () => { + const previous = { + window: globalThis.window, + Element: globalThis.Element, + MouseEvent: globalThis.MouseEvent, + } + class Element extends EventTarget { + isConnected = true + closest() { + return this + } + contains(target) { + return target === this + } + } + const media = new EventTarget() + media.matches = false + globalThis.window = { matchMedia: () => media } + globalThis.Element = Element + globalThis.MouseEvent = Event + const root = new Element() + const received = [] + const event = action === "click" ? "click" : "dialog:set" + const receive = (event) => received.push(event.detail?.open ?? "click") + const dispose = onMedia( + "mobile", + async () => ({}), + () => { + root.addEventListener(event, receive) + return () => root.removeEventListener(event, receive) + }, + { root, selector: "button", event: "dialog:set" } + ) + try { + media.matches = true + root.dispatchEvent( + action === "click" + ? new Event("click", { cancelable: true }) + : new CustomEvent("dialog:set", { detail: { open: true } }) + ) + assert.deepEqual(received, []) + media.dispatchEvent(new Event("change")) + await Promise.resolve() + assert.deepEqual(received, [action === "click" ? "click" : true]) + media.matches = false + media.dispatchEvent(new Event("change")) + media.matches = true + media.dispatchEvent(new Event("change")) + await Promise.resolve() + assert.equal(received.length, 1) + } finally { + dispose() + Object.assign(globalThis, previous) + } + }) From 613a12ab622d875bd27bcb6a7067de39910546e4 Mon Sep 17 00:00:00 2001 From: Sil Date: Sat, 24 Oct 2026 16:58:36 +0200 Subject: [PATCH 2/5] fix: preserve media requests while query notifications settle --- public/r/styles/base-luma/on-media.json | 2 +- public/r/styles/base-lyra/on-media.json | 2 +- public/r/styles/base-maia/on-media.json | 2 +- public/r/styles/base-mira/on-media.json | 2 +- public/r/styles/base-nova/on-media.json | 2 +- public/r/styles/base-rhea/on-media.json | 2 +- public/r/styles/base-sera/on-media.json | 2 +- public/r/styles/base-vega/on-media.json | 2 +- src/lib/on-media.ts | 15 ++-- tests/browser/responsive-menu.test.mjs | 11 ++- tests/on-media.test.mjs | 113 +++++++++++++----------- 11 files changed, 83 insertions(+), 72 deletions(-) diff --git a/public/r/styles/base-luma/on-media.json b/public/r/styles/base-luma/on-media.json index 1b397d94c..28d5960f4 100644 --- a/public/r/styles/base-luma/on-media.json +++ b/public/r/styles/base-luma/on-media.json @@ -6,7 +6,7 @@ "files": [ { "path": "src/lib/on-media.ts", - "content": "type Trigger = { root: Element; selector: string; event?: string }\n\n/** Import and mount only while the query matches; dispose on exit. */\nexport function onMedia(\n query: string,\n load: () => Promise,\n mount: (module: T) => () => void,\n trigger?: Trigger\n) {\n const media = window.matchMedia(query)\n let disposed = false\n let epoch = 0\n let cleanup: (() => void) | undefined\n let pendingClick: HTMLElement | undefined\n let pendingEvent: CustomEvent | undefined\n\n const replayClick = () => {\n const target = pendingClick\n const event = pendingEvent\n pendingClick = undefined\n pendingEvent = undefined\n if (event && trigger?.root.isConnected)\n trigger.root.dispatchEvent(\n new CustomEvent(event.type, { detail: event.detail })\n )\n else if (target?.isConnected)\n target.dispatchEvent(new MouseEvent(\"click\", { cancelable: true }))\n }\n\n const update = async () => {\n const current = ++epoch\n cleanup?.()\n cleanup = undefined\n if (!media.matches || disposed) {\n pendingClick = undefined\n pendingEvent = undefined\n return\n }\n try {\n const module = await load()\n if (disposed || !media.matches || current !== epoch) return\n cleanup = mount(module)\n replayClick()\n } catch (error) {\n if (!disposed && current === epoch) {\n pendingClick = undefined\n pendingEvent = undefined\n console.error(error)\n }\n }\n }\n const click = (event: Event) => {\n if (\n !trigger ||\n cleanup ||\n disposed ||\n !media.matches ||\n event.defaultPrevented\n )\n return\n if (!(event.target instanceof Element)) return\n const target = event.target.closest(trigger.selector)\n if (!target || !trigger.root.contains(target)) return\n pendingClick ??= target\n pendingEvent = undefined\n event.preventDefault()\n }\n const request = (event: Event) => {\n if (cleanup || disposed || !media.matches || event.target !== trigger?.root)\n return\n if (!(event instanceof CustomEvent)) return\n pendingEvent = event\n pendingClick = undefined\n }\n const change = () => void update()\n media.addEventListener(\"change\", change)\n trigger?.root.addEventListener(\"click\", click, true)\n if (trigger?.event) trigger.root.addEventListener(trigger.event, request)\n change()\n return () => {\n disposed = true\n epoch++\n pendingClick = undefined\n pendingEvent = undefined\n media.removeEventListener(\"change\", change)\n trigger?.root.removeEventListener(\"click\", click, true)\n if (trigger?.event) trigger.root.removeEventListener(trigger.event, request)\n cleanup?.()\n cleanup = undefined\n }\n}\n", + "content": "type Trigger = { root: Element; selector: string; event?: string }\n\n/** Import and mount only while the query matches; dispose on exit. */\nexport function onMedia(\n query: string,\n load: () => Promise,\n mount: (module: T) => () => void,\n trigger?: Trigger\n) {\n const media = window.matchMedia(query)\n const matches = () => window.matchMedia(query).matches\n let disposed = false\n let epoch = 0\n let cleanup: (() => void) | undefined\n let pendingClick: HTMLElement | undefined\n let pendingEvent: CustomEvent | undefined\n\n const replayClick = () => {\n const target = pendingClick\n const event = pendingEvent\n pendingClick = undefined\n pendingEvent = undefined\n if (event && trigger?.root.isConnected)\n trigger.root.dispatchEvent(\n new CustomEvent(event.type, { detail: event.detail })\n )\n else if (target?.isConnected)\n target.dispatchEvent(new MouseEvent(\"click\", { cancelable: true }))\n }\n\n const update = async () => {\n const current = ++epoch\n cleanup?.()\n cleanup = undefined\n if (!matches() || disposed) {\n pendingClick = undefined\n pendingEvent = undefined\n return\n }\n try {\n const module = await load()\n if (disposed || !matches() || current !== epoch) return\n cleanup = mount(module)\n replayClick()\n } catch (error) {\n if (!disposed && current === epoch) {\n pendingClick = undefined\n pendingEvent = undefined\n console.error(error)\n }\n }\n }\n const click = (event: Event) => {\n if (!trigger || cleanup || disposed || !matches() || event.defaultPrevented)\n return\n if (!(event.target instanceof Element)) return\n const target = event.target.closest(trigger.selector)\n if (!target || !trigger.root.contains(target)) return\n pendingClick ??= target\n pendingEvent = undefined\n event.preventDefault()\n }\n const request = (event: Event) => {\n if (cleanup || disposed || !matches() || event.target !== trigger?.root)\n return\n if (!(event instanceof CustomEvent)) return\n pendingEvent = event\n pendingClick = undefined\n }\n const change = () => void update()\n media.addEventListener(\"change\", change)\n trigger?.root.addEventListener(\"click\", click, true)\n if (trigger?.event) trigger.root.addEventListener(trigger.event, request)\n change()\n return () => {\n disposed = true\n epoch++\n pendingClick = undefined\n pendingEvent = undefined\n media.removeEventListener(\"change\", change)\n trigger?.root.removeEventListener(\"click\", click, true)\n if (trigger?.event) trigger.root.removeEventListener(trigger.event, request)\n cleanup?.()\n cleanup = undefined\n }\n}\n", "type": "registry:lib" } ], diff --git a/public/r/styles/base-lyra/on-media.json b/public/r/styles/base-lyra/on-media.json index 1b397d94c..28d5960f4 100644 --- a/public/r/styles/base-lyra/on-media.json +++ b/public/r/styles/base-lyra/on-media.json @@ -6,7 +6,7 @@ "files": [ { "path": "src/lib/on-media.ts", - "content": "type Trigger = { root: Element; selector: string; event?: string }\n\n/** Import and mount only while the query matches; dispose on exit. */\nexport function onMedia(\n query: string,\n load: () => Promise,\n mount: (module: T) => () => void,\n trigger?: Trigger\n) {\n const media = window.matchMedia(query)\n let disposed = false\n let epoch = 0\n let cleanup: (() => void) | undefined\n let pendingClick: HTMLElement | undefined\n let pendingEvent: CustomEvent | undefined\n\n const replayClick = () => {\n const target = pendingClick\n const event = pendingEvent\n pendingClick = undefined\n pendingEvent = undefined\n if (event && trigger?.root.isConnected)\n trigger.root.dispatchEvent(\n new CustomEvent(event.type, { detail: event.detail })\n )\n else if (target?.isConnected)\n target.dispatchEvent(new MouseEvent(\"click\", { cancelable: true }))\n }\n\n const update = async () => {\n const current = ++epoch\n cleanup?.()\n cleanup = undefined\n if (!media.matches || disposed) {\n pendingClick = undefined\n pendingEvent = undefined\n return\n }\n try {\n const module = await load()\n if (disposed || !media.matches || current !== epoch) return\n cleanup = mount(module)\n replayClick()\n } catch (error) {\n if (!disposed && current === epoch) {\n pendingClick = undefined\n pendingEvent = undefined\n console.error(error)\n }\n }\n }\n const click = (event: Event) => {\n if (\n !trigger ||\n cleanup ||\n disposed ||\n !media.matches ||\n event.defaultPrevented\n )\n return\n if (!(event.target instanceof Element)) return\n const target = event.target.closest(trigger.selector)\n if (!target || !trigger.root.contains(target)) return\n pendingClick ??= target\n pendingEvent = undefined\n event.preventDefault()\n }\n const request = (event: Event) => {\n if (cleanup || disposed || !media.matches || event.target !== trigger?.root)\n return\n if (!(event instanceof CustomEvent)) return\n pendingEvent = event\n pendingClick = undefined\n }\n const change = () => void update()\n media.addEventListener(\"change\", change)\n trigger?.root.addEventListener(\"click\", click, true)\n if (trigger?.event) trigger.root.addEventListener(trigger.event, request)\n change()\n return () => {\n disposed = true\n epoch++\n pendingClick = undefined\n pendingEvent = undefined\n media.removeEventListener(\"change\", change)\n trigger?.root.removeEventListener(\"click\", click, true)\n if (trigger?.event) trigger.root.removeEventListener(trigger.event, request)\n cleanup?.()\n cleanup = undefined\n }\n}\n", + "content": "type Trigger = { root: Element; selector: string; event?: string }\n\n/** Import and mount only while the query matches; dispose on exit. */\nexport function onMedia(\n query: string,\n load: () => Promise,\n mount: (module: T) => () => void,\n trigger?: Trigger\n) {\n const media = window.matchMedia(query)\n const matches = () => window.matchMedia(query).matches\n let disposed = false\n let epoch = 0\n let cleanup: (() => void) | undefined\n let pendingClick: HTMLElement | undefined\n let pendingEvent: CustomEvent | undefined\n\n const replayClick = () => {\n const target = pendingClick\n const event = pendingEvent\n pendingClick = undefined\n pendingEvent = undefined\n if (event && trigger?.root.isConnected)\n trigger.root.dispatchEvent(\n new CustomEvent(event.type, { detail: event.detail })\n )\n else if (target?.isConnected)\n target.dispatchEvent(new MouseEvent(\"click\", { cancelable: true }))\n }\n\n const update = async () => {\n const current = ++epoch\n cleanup?.()\n cleanup = undefined\n if (!matches() || disposed) {\n pendingClick = undefined\n pendingEvent = undefined\n return\n }\n try {\n const module = await load()\n if (disposed || !matches() || current !== epoch) return\n cleanup = mount(module)\n replayClick()\n } catch (error) {\n if (!disposed && current === epoch) {\n pendingClick = undefined\n pendingEvent = undefined\n console.error(error)\n }\n }\n }\n const click = (event: Event) => {\n if (!trigger || cleanup || disposed || !matches() || event.defaultPrevented)\n return\n if (!(event.target instanceof Element)) return\n const target = event.target.closest(trigger.selector)\n if (!target || !trigger.root.contains(target)) return\n pendingClick ??= target\n pendingEvent = undefined\n event.preventDefault()\n }\n const request = (event: Event) => {\n if (cleanup || disposed || !matches() || event.target !== trigger?.root)\n return\n if (!(event instanceof CustomEvent)) return\n pendingEvent = event\n pendingClick = undefined\n }\n const change = () => void update()\n media.addEventListener(\"change\", change)\n trigger?.root.addEventListener(\"click\", click, true)\n if (trigger?.event) trigger.root.addEventListener(trigger.event, request)\n change()\n return () => {\n disposed = true\n epoch++\n pendingClick = undefined\n pendingEvent = undefined\n media.removeEventListener(\"change\", change)\n trigger?.root.removeEventListener(\"click\", click, true)\n if (trigger?.event) trigger.root.removeEventListener(trigger.event, request)\n cleanup?.()\n cleanup = undefined\n }\n}\n", "type": "registry:lib" } ], diff --git a/public/r/styles/base-maia/on-media.json b/public/r/styles/base-maia/on-media.json index 1b397d94c..28d5960f4 100644 --- a/public/r/styles/base-maia/on-media.json +++ b/public/r/styles/base-maia/on-media.json @@ -6,7 +6,7 @@ "files": [ { "path": "src/lib/on-media.ts", - "content": "type Trigger = { root: Element; selector: string; event?: string }\n\n/** Import and mount only while the query matches; dispose on exit. */\nexport function onMedia(\n query: string,\n load: () => Promise,\n mount: (module: T) => () => void,\n trigger?: Trigger\n) {\n const media = window.matchMedia(query)\n let disposed = false\n let epoch = 0\n let cleanup: (() => void) | undefined\n let pendingClick: HTMLElement | undefined\n let pendingEvent: CustomEvent | undefined\n\n const replayClick = () => {\n const target = pendingClick\n const event = pendingEvent\n pendingClick = undefined\n pendingEvent = undefined\n if (event && trigger?.root.isConnected)\n trigger.root.dispatchEvent(\n new CustomEvent(event.type, { detail: event.detail })\n )\n else if (target?.isConnected)\n target.dispatchEvent(new MouseEvent(\"click\", { cancelable: true }))\n }\n\n const update = async () => {\n const current = ++epoch\n cleanup?.()\n cleanup = undefined\n if (!media.matches || disposed) {\n pendingClick = undefined\n pendingEvent = undefined\n return\n }\n try {\n const module = await load()\n if (disposed || !media.matches || current !== epoch) return\n cleanup = mount(module)\n replayClick()\n } catch (error) {\n if (!disposed && current === epoch) {\n pendingClick = undefined\n pendingEvent = undefined\n console.error(error)\n }\n }\n }\n const click = (event: Event) => {\n if (\n !trigger ||\n cleanup ||\n disposed ||\n !media.matches ||\n event.defaultPrevented\n )\n return\n if (!(event.target instanceof Element)) return\n const target = event.target.closest(trigger.selector)\n if (!target || !trigger.root.contains(target)) return\n pendingClick ??= target\n pendingEvent = undefined\n event.preventDefault()\n }\n const request = (event: Event) => {\n if (cleanup || disposed || !media.matches || event.target !== trigger?.root)\n return\n if (!(event instanceof CustomEvent)) return\n pendingEvent = event\n pendingClick = undefined\n }\n const change = () => void update()\n media.addEventListener(\"change\", change)\n trigger?.root.addEventListener(\"click\", click, true)\n if (trigger?.event) trigger.root.addEventListener(trigger.event, request)\n change()\n return () => {\n disposed = true\n epoch++\n pendingClick = undefined\n pendingEvent = undefined\n media.removeEventListener(\"change\", change)\n trigger?.root.removeEventListener(\"click\", click, true)\n if (trigger?.event) trigger.root.removeEventListener(trigger.event, request)\n cleanup?.()\n cleanup = undefined\n }\n}\n", + "content": "type Trigger = { root: Element; selector: string; event?: string }\n\n/** Import and mount only while the query matches; dispose on exit. */\nexport function onMedia(\n query: string,\n load: () => Promise,\n mount: (module: T) => () => void,\n trigger?: Trigger\n) {\n const media = window.matchMedia(query)\n const matches = () => window.matchMedia(query).matches\n let disposed = false\n let epoch = 0\n let cleanup: (() => void) | undefined\n let pendingClick: HTMLElement | undefined\n let pendingEvent: CustomEvent | undefined\n\n const replayClick = () => {\n const target = pendingClick\n const event = pendingEvent\n pendingClick = undefined\n pendingEvent = undefined\n if (event && trigger?.root.isConnected)\n trigger.root.dispatchEvent(\n new CustomEvent(event.type, { detail: event.detail })\n )\n else if (target?.isConnected)\n target.dispatchEvent(new MouseEvent(\"click\", { cancelable: true }))\n }\n\n const update = async () => {\n const current = ++epoch\n cleanup?.()\n cleanup = undefined\n if (!matches() || disposed) {\n pendingClick = undefined\n pendingEvent = undefined\n return\n }\n try {\n const module = await load()\n if (disposed || !matches() || current !== epoch) return\n cleanup = mount(module)\n replayClick()\n } catch (error) {\n if (!disposed && current === epoch) {\n pendingClick = undefined\n pendingEvent = undefined\n console.error(error)\n }\n }\n }\n const click = (event: Event) => {\n if (!trigger || cleanup || disposed || !matches() || event.defaultPrevented)\n return\n if (!(event.target instanceof Element)) return\n const target = event.target.closest(trigger.selector)\n if (!target || !trigger.root.contains(target)) return\n pendingClick ??= target\n pendingEvent = undefined\n event.preventDefault()\n }\n const request = (event: Event) => {\n if (cleanup || disposed || !matches() || event.target !== trigger?.root)\n return\n if (!(event instanceof CustomEvent)) return\n pendingEvent = event\n pendingClick = undefined\n }\n const change = () => void update()\n media.addEventListener(\"change\", change)\n trigger?.root.addEventListener(\"click\", click, true)\n if (trigger?.event) trigger.root.addEventListener(trigger.event, request)\n change()\n return () => {\n disposed = true\n epoch++\n pendingClick = undefined\n pendingEvent = undefined\n media.removeEventListener(\"change\", change)\n trigger?.root.removeEventListener(\"click\", click, true)\n if (trigger?.event) trigger.root.removeEventListener(trigger.event, request)\n cleanup?.()\n cleanup = undefined\n }\n}\n", "type": "registry:lib" } ], diff --git a/public/r/styles/base-mira/on-media.json b/public/r/styles/base-mira/on-media.json index 1b397d94c..28d5960f4 100644 --- a/public/r/styles/base-mira/on-media.json +++ b/public/r/styles/base-mira/on-media.json @@ -6,7 +6,7 @@ "files": [ { "path": "src/lib/on-media.ts", - "content": "type Trigger = { root: Element; selector: string; event?: string }\n\n/** Import and mount only while the query matches; dispose on exit. */\nexport function onMedia(\n query: string,\n load: () => Promise,\n mount: (module: T) => () => void,\n trigger?: Trigger\n) {\n const media = window.matchMedia(query)\n let disposed = false\n let epoch = 0\n let cleanup: (() => void) | undefined\n let pendingClick: HTMLElement | undefined\n let pendingEvent: CustomEvent | undefined\n\n const replayClick = () => {\n const target = pendingClick\n const event = pendingEvent\n pendingClick = undefined\n pendingEvent = undefined\n if (event && trigger?.root.isConnected)\n trigger.root.dispatchEvent(\n new CustomEvent(event.type, { detail: event.detail })\n )\n else if (target?.isConnected)\n target.dispatchEvent(new MouseEvent(\"click\", { cancelable: true }))\n }\n\n const update = async () => {\n const current = ++epoch\n cleanup?.()\n cleanup = undefined\n if (!media.matches || disposed) {\n pendingClick = undefined\n pendingEvent = undefined\n return\n }\n try {\n const module = await load()\n if (disposed || !media.matches || current !== epoch) return\n cleanup = mount(module)\n replayClick()\n } catch (error) {\n if (!disposed && current === epoch) {\n pendingClick = undefined\n pendingEvent = undefined\n console.error(error)\n }\n }\n }\n const click = (event: Event) => {\n if (\n !trigger ||\n cleanup ||\n disposed ||\n !media.matches ||\n event.defaultPrevented\n )\n return\n if (!(event.target instanceof Element)) return\n const target = event.target.closest(trigger.selector)\n if (!target || !trigger.root.contains(target)) return\n pendingClick ??= target\n pendingEvent = undefined\n event.preventDefault()\n }\n const request = (event: Event) => {\n if (cleanup || disposed || !media.matches || event.target !== trigger?.root)\n return\n if (!(event instanceof CustomEvent)) return\n pendingEvent = event\n pendingClick = undefined\n }\n const change = () => void update()\n media.addEventListener(\"change\", change)\n trigger?.root.addEventListener(\"click\", click, true)\n if (trigger?.event) trigger.root.addEventListener(trigger.event, request)\n change()\n return () => {\n disposed = true\n epoch++\n pendingClick = undefined\n pendingEvent = undefined\n media.removeEventListener(\"change\", change)\n trigger?.root.removeEventListener(\"click\", click, true)\n if (trigger?.event) trigger.root.removeEventListener(trigger.event, request)\n cleanup?.()\n cleanup = undefined\n }\n}\n", + "content": "type Trigger = { root: Element; selector: string; event?: string }\n\n/** Import and mount only while the query matches; dispose on exit. */\nexport function onMedia(\n query: string,\n load: () => Promise,\n mount: (module: T) => () => void,\n trigger?: Trigger\n) {\n const media = window.matchMedia(query)\n const matches = () => window.matchMedia(query).matches\n let disposed = false\n let epoch = 0\n let cleanup: (() => void) | undefined\n let pendingClick: HTMLElement | undefined\n let pendingEvent: CustomEvent | undefined\n\n const replayClick = () => {\n const target = pendingClick\n const event = pendingEvent\n pendingClick = undefined\n pendingEvent = undefined\n if (event && trigger?.root.isConnected)\n trigger.root.dispatchEvent(\n new CustomEvent(event.type, { detail: event.detail })\n )\n else if (target?.isConnected)\n target.dispatchEvent(new MouseEvent(\"click\", { cancelable: true }))\n }\n\n const update = async () => {\n const current = ++epoch\n cleanup?.()\n cleanup = undefined\n if (!matches() || disposed) {\n pendingClick = undefined\n pendingEvent = undefined\n return\n }\n try {\n const module = await load()\n if (disposed || !matches() || current !== epoch) return\n cleanup = mount(module)\n replayClick()\n } catch (error) {\n if (!disposed && current === epoch) {\n pendingClick = undefined\n pendingEvent = undefined\n console.error(error)\n }\n }\n }\n const click = (event: Event) => {\n if (!trigger || cleanup || disposed || !matches() || event.defaultPrevented)\n return\n if (!(event.target instanceof Element)) return\n const target = event.target.closest(trigger.selector)\n if (!target || !trigger.root.contains(target)) return\n pendingClick ??= target\n pendingEvent = undefined\n event.preventDefault()\n }\n const request = (event: Event) => {\n if (cleanup || disposed || !matches() || event.target !== trigger?.root)\n return\n if (!(event instanceof CustomEvent)) return\n pendingEvent = event\n pendingClick = undefined\n }\n const change = () => void update()\n media.addEventListener(\"change\", change)\n trigger?.root.addEventListener(\"click\", click, true)\n if (trigger?.event) trigger.root.addEventListener(trigger.event, request)\n change()\n return () => {\n disposed = true\n epoch++\n pendingClick = undefined\n pendingEvent = undefined\n media.removeEventListener(\"change\", change)\n trigger?.root.removeEventListener(\"click\", click, true)\n if (trigger?.event) trigger.root.removeEventListener(trigger.event, request)\n cleanup?.()\n cleanup = undefined\n }\n}\n", "type": "registry:lib" } ], diff --git a/public/r/styles/base-nova/on-media.json b/public/r/styles/base-nova/on-media.json index 1b397d94c..28d5960f4 100644 --- a/public/r/styles/base-nova/on-media.json +++ b/public/r/styles/base-nova/on-media.json @@ -6,7 +6,7 @@ "files": [ { "path": "src/lib/on-media.ts", - "content": "type Trigger = { root: Element; selector: string; event?: string }\n\n/** Import and mount only while the query matches; dispose on exit. */\nexport function onMedia(\n query: string,\n load: () => Promise,\n mount: (module: T) => () => void,\n trigger?: Trigger\n) {\n const media = window.matchMedia(query)\n let disposed = false\n let epoch = 0\n let cleanup: (() => void) | undefined\n let pendingClick: HTMLElement | undefined\n let pendingEvent: CustomEvent | undefined\n\n const replayClick = () => {\n const target = pendingClick\n const event = pendingEvent\n pendingClick = undefined\n pendingEvent = undefined\n if (event && trigger?.root.isConnected)\n trigger.root.dispatchEvent(\n new CustomEvent(event.type, { detail: event.detail })\n )\n else if (target?.isConnected)\n target.dispatchEvent(new MouseEvent(\"click\", { cancelable: true }))\n }\n\n const update = async () => {\n const current = ++epoch\n cleanup?.()\n cleanup = undefined\n if (!media.matches || disposed) {\n pendingClick = undefined\n pendingEvent = undefined\n return\n }\n try {\n const module = await load()\n if (disposed || !media.matches || current !== epoch) return\n cleanup = mount(module)\n replayClick()\n } catch (error) {\n if (!disposed && current === epoch) {\n pendingClick = undefined\n pendingEvent = undefined\n console.error(error)\n }\n }\n }\n const click = (event: Event) => {\n if (\n !trigger ||\n cleanup ||\n disposed ||\n !media.matches ||\n event.defaultPrevented\n )\n return\n if (!(event.target instanceof Element)) return\n const target = event.target.closest(trigger.selector)\n if (!target || !trigger.root.contains(target)) return\n pendingClick ??= target\n pendingEvent = undefined\n event.preventDefault()\n }\n const request = (event: Event) => {\n if (cleanup || disposed || !media.matches || event.target !== trigger?.root)\n return\n if (!(event instanceof CustomEvent)) return\n pendingEvent = event\n pendingClick = undefined\n }\n const change = () => void update()\n media.addEventListener(\"change\", change)\n trigger?.root.addEventListener(\"click\", click, true)\n if (trigger?.event) trigger.root.addEventListener(trigger.event, request)\n change()\n return () => {\n disposed = true\n epoch++\n pendingClick = undefined\n pendingEvent = undefined\n media.removeEventListener(\"change\", change)\n trigger?.root.removeEventListener(\"click\", click, true)\n if (trigger?.event) trigger.root.removeEventListener(trigger.event, request)\n cleanup?.()\n cleanup = undefined\n }\n}\n", + "content": "type Trigger = { root: Element; selector: string; event?: string }\n\n/** Import and mount only while the query matches; dispose on exit. */\nexport function onMedia(\n query: string,\n load: () => Promise,\n mount: (module: T) => () => void,\n trigger?: Trigger\n) {\n const media = window.matchMedia(query)\n const matches = () => window.matchMedia(query).matches\n let disposed = false\n let epoch = 0\n let cleanup: (() => void) | undefined\n let pendingClick: HTMLElement | undefined\n let pendingEvent: CustomEvent | undefined\n\n const replayClick = () => {\n const target = pendingClick\n const event = pendingEvent\n pendingClick = undefined\n pendingEvent = undefined\n if (event && trigger?.root.isConnected)\n trigger.root.dispatchEvent(\n new CustomEvent(event.type, { detail: event.detail })\n )\n else if (target?.isConnected)\n target.dispatchEvent(new MouseEvent(\"click\", { cancelable: true }))\n }\n\n const update = async () => {\n const current = ++epoch\n cleanup?.()\n cleanup = undefined\n if (!matches() || disposed) {\n pendingClick = undefined\n pendingEvent = undefined\n return\n }\n try {\n const module = await load()\n if (disposed || !matches() || current !== epoch) return\n cleanup = mount(module)\n replayClick()\n } catch (error) {\n if (!disposed && current === epoch) {\n pendingClick = undefined\n pendingEvent = undefined\n console.error(error)\n }\n }\n }\n const click = (event: Event) => {\n if (!trigger || cleanup || disposed || !matches() || event.defaultPrevented)\n return\n if (!(event.target instanceof Element)) return\n const target = event.target.closest(trigger.selector)\n if (!target || !trigger.root.contains(target)) return\n pendingClick ??= target\n pendingEvent = undefined\n event.preventDefault()\n }\n const request = (event: Event) => {\n if (cleanup || disposed || !matches() || event.target !== trigger?.root)\n return\n if (!(event instanceof CustomEvent)) return\n pendingEvent = event\n pendingClick = undefined\n }\n const change = () => void update()\n media.addEventListener(\"change\", change)\n trigger?.root.addEventListener(\"click\", click, true)\n if (trigger?.event) trigger.root.addEventListener(trigger.event, request)\n change()\n return () => {\n disposed = true\n epoch++\n pendingClick = undefined\n pendingEvent = undefined\n media.removeEventListener(\"change\", change)\n trigger?.root.removeEventListener(\"click\", click, true)\n if (trigger?.event) trigger.root.removeEventListener(trigger.event, request)\n cleanup?.()\n cleanup = undefined\n }\n}\n", "type": "registry:lib" } ], diff --git a/public/r/styles/base-rhea/on-media.json b/public/r/styles/base-rhea/on-media.json index 1b397d94c..28d5960f4 100644 --- a/public/r/styles/base-rhea/on-media.json +++ b/public/r/styles/base-rhea/on-media.json @@ -6,7 +6,7 @@ "files": [ { "path": "src/lib/on-media.ts", - "content": "type Trigger = { root: Element; selector: string; event?: string }\n\n/** Import and mount only while the query matches; dispose on exit. */\nexport function onMedia(\n query: string,\n load: () => Promise,\n mount: (module: T) => () => void,\n trigger?: Trigger\n) {\n const media = window.matchMedia(query)\n let disposed = false\n let epoch = 0\n let cleanup: (() => void) | undefined\n let pendingClick: HTMLElement | undefined\n let pendingEvent: CustomEvent | undefined\n\n const replayClick = () => {\n const target = pendingClick\n const event = pendingEvent\n pendingClick = undefined\n pendingEvent = undefined\n if (event && trigger?.root.isConnected)\n trigger.root.dispatchEvent(\n new CustomEvent(event.type, { detail: event.detail })\n )\n else if (target?.isConnected)\n target.dispatchEvent(new MouseEvent(\"click\", { cancelable: true }))\n }\n\n const update = async () => {\n const current = ++epoch\n cleanup?.()\n cleanup = undefined\n if (!media.matches || disposed) {\n pendingClick = undefined\n pendingEvent = undefined\n return\n }\n try {\n const module = await load()\n if (disposed || !media.matches || current !== epoch) return\n cleanup = mount(module)\n replayClick()\n } catch (error) {\n if (!disposed && current === epoch) {\n pendingClick = undefined\n pendingEvent = undefined\n console.error(error)\n }\n }\n }\n const click = (event: Event) => {\n if (\n !trigger ||\n cleanup ||\n disposed ||\n !media.matches ||\n event.defaultPrevented\n )\n return\n if (!(event.target instanceof Element)) return\n const target = event.target.closest(trigger.selector)\n if (!target || !trigger.root.contains(target)) return\n pendingClick ??= target\n pendingEvent = undefined\n event.preventDefault()\n }\n const request = (event: Event) => {\n if (cleanup || disposed || !media.matches || event.target !== trigger?.root)\n return\n if (!(event instanceof CustomEvent)) return\n pendingEvent = event\n pendingClick = undefined\n }\n const change = () => void update()\n media.addEventListener(\"change\", change)\n trigger?.root.addEventListener(\"click\", click, true)\n if (trigger?.event) trigger.root.addEventListener(trigger.event, request)\n change()\n return () => {\n disposed = true\n epoch++\n pendingClick = undefined\n pendingEvent = undefined\n media.removeEventListener(\"change\", change)\n trigger?.root.removeEventListener(\"click\", click, true)\n if (trigger?.event) trigger.root.removeEventListener(trigger.event, request)\n cleanup?.()\n cleanup = undefined\n }\n}\n", + "content": "type Trigger = { root: Element; selector: string; event?: string }\n\n/** Import and mount only while the query matches; dispose on exit. */\nexport function onMedia(\n query: string,\n load: () => Promise,\n mount: (module: T) => () => void,\n trigger?: Trigger\n) {\n const media = window.matchMedia(query)\n const matches = () => window.matchMedia(query).matches\n let disposed = false\n let epoch = 0\n let cleanup: (() => void) | undefined\n let pendingClick: HTMLElement | undefined\n let pendingEvent: CustomEvent | undefined\n\n const replayClick = () => {\n const target = pendingClick\n const event = pendingEvent\n pendingClick = undefined\n pendingEvent = undefined\n if (event && trigger?.root.isConnected)\n trigger.root.dispatchEvent(\n new CustomEvent(event.type, { detail: event.detail })\n )\n else if (target?.isConnected)\n target.dispatchEvent(new MouseEvent(\"click\", { cancelable: true }))\n }\n\n const update = async () => {\n const current = ++epoch\n cleanup?.()\n cleanup = undefined\n if (!matches() || disposed) {\n pendingClick = undefined\n pendingEvent = undefined\n return\n }\n try {\n const module = await load()\n if (disposed || !matches() || current !== epoch) return\n cleanup = mount(module)\n replayClick()\n } catch (error) {\n if (!disposed && current === epoch) {\n pendingClick = undefined\n pendingEvent = undefined\n console.error(error)\n }\n }\n }\n const click = (event: Event) => {\n if (!trigger || cleanup || disposed || !matches() || event.defaultPrevented)\n return\n if (!(event.target instanceof Element)) return\n const target = event.target.closest(trigger.selector)\n if (!target || !trigger.root.contains(target)) return\n pendingClick ??= target\n pendingEvent = undefined\n event.preventDefault()\n }\n const request = (event: Event) => {\n if (cleanup || disposed || !matches() || event.target !== trigger?.root)\n return\n if (!(event instanceof CustomEvent)) return\n pendingEvent = event\n pendingClick = undefined\n }\n const change = () => void update()\n media.addEventListener(\"change\", change)\n trigger?.root.addEventListener(\"click\", click, true)\n if (trigger?.event) trigger.root.addEventListener(trigger.event, request)\n change()\n return () => {\n disposed = true\n epoch++\n pendingClick = undefined\n pendingEvent = undefined\n media.removeEventListener(\"change\", change)\n trigger?.root.removeEventListener(\"click\", click, true)\n if (trigger?.event) trigger.root.removeEventListener(trigger.event, request)\n cleanup?.()\n cleanup = undefined\n }\n}\n", "type": "registry:lib" } ], diff --git a/public/r/styles/base-sera/on-media.json b/public/r/styles/base-sera/on-media.json index 1b397d94c..28d5960f4 100644 --- a/public/r/styles/base-sera/on-media.json +++ b/public/r/styles/base-sera/on-media.json @@ -6,7 +6,7 @@ "files": [ { "path": "src/lib/on-media.ts", - "content": "type Trigger = { root: Element; selector: string; event?: string }\n\n/** Import and mount only while the query matches; dispose on exit. */\nexport function onMedia(\n query: string,\n load: () => Promise,\n mount: (module: T) => () => void,\n trigger?: Trigger\n) {\n const media = window.matchMedia(query)\n let disposed = false\n let epoch = 0\n let cleanup: (() => void) | undefined\n let pendingClick: HTMLElement | undefined\n let pendingEvent: CustomEvent | undefined\n\n const replayClick = () => {\n const target = pendingClick\n const event = pendingEvent\n pendingClick = undefined\n pendingEvent = undefined\n if (event && trigger?.root.isConnected)\n trigger.root.dispatchEvent(\n new CustomEvent(event.type, { detail: event.detail })\n )\n else if (target?.isConnected)\n target.dispatchEvent(new MouseEvent(\"click\", { cancelable: true }))\n }\n\n const update = async () => {\n const current = ++epoch\n cleanup?.()\n cleanup = undefined\n if (!media.matches || disposed) {\n pendingClick = undefined\n pendingEvent = undefined\n return\n }\n try {\n const module = await load()\n if (disposed || !media.matches || current !== epoch) return\n cleanup = mount(module)\n replayClick()\n } catch (error) {\n if (!disposed && current === epoch) {\n pendingClick = undefined\n pendingEvent = undefined\n console.error(error)\n }\n }\n }\n const click = (event: Event) => {\n if (\n !trigger ||\n cleanup ||\n disposed ||\n !media.matches ||\n event.defaultPrevented\n )\n return\n if (!(event.target instanceof Element)) return\n const target = event.target.closest(trigger.selector)\n if (!target || !trigger.root.contains(target)) return\n pendingClick ??= target\n pendingEvent = undefined\n event.preventDefault()\n }\n const request = (event: Event) => {\n if (cleanup || disposed || !media.matches || event.target !== trigger?.root)\n return\n if (!(event instanceof CustomEvent)) return\n pendingEvent = event\n pendingClick = undefined\n }\n const change = () => void update()\n media.addEventListener(\"change\", change)\n trigger?.root.addEventListener(\"click\", click, true)\n if (trigger?.event) trigger.root.addEventListener(trigger.event, request)\n change()\n return () => {\n disposed = true\n epoch++\n pendingClick = undefined\n pendingEvent = undefined\n media.removeEventListener(\"change\", change)\n trigger?.root.removeEventListener(\"click\", click, true)\n if (trigger?.event) trigger.root.removeEventListener(trigger.event, request)\n cleanup?.()\n cleanup = undefined\n }\n}\n", + "content": "type Trigger = { root: Element; selector: string; event?: string }\n\n/** Import and mount only while the query matches; dispose on exit. */\nexport function onMedia(\n query: string,\n load: () => Promise,\n mount: (module: T) => () => void,\n trigger?: Trigger\n) {\n const media = window.matchMedia(query)\n const matches = () => window.matchMedia(query).matches\n let disposed = false\n let epoch = 0\n let cleanup: (() => void) | undefined\n let pendingClick: HTMLElement | undefined\n let pendingEvent: CustomEvent | undefined\n\n const replayClick = () => {\n const target = pendingClick\n const event = pendingEvent\n pendingClick = undefined\n pendingEvent = undefined\n if (event && trigger?.root.isConnected)\n trigger.root.dispatchEvent(\n new CustomEvent(event.type, { detail: event.detail })\n )\n else if (target?.isConnected)\n target.dispatchEvent(new MouseEvent(\"click\", { cancelable: true }))\n }\n\n const update = async () => {\n const current = ++epoch\n cleanup?.()\n cleanup = undefined\n if (!matches() || disposed) {\n pendingClick = undefined\n pendingEvent = undefined\n return\n }\n try {\n const module = await load()\n if (disposed || !matches() || current !== epoch) return\n cleanup = mount(module)\n replayClick()\n } catch (error) {\n if (!disposed && current === epoch) {\n pendingClick = undefined\n pendingEvent = undefined\n console.error(error)\n }\n }\n }\n const click = (event: Event) => {\n if (!trigger || cleanup || disposed || !matches() || event.defaultPrevented)\n return\n if (!(event.target instanceof Element)) return\n const target = event.target.closest(trigger.selector)\n if (!target || !trigger.root.contains(target)) return\n pendingClick ??= target\n pendingEvent = undefined\n event.preventDefault()\n }\n const request = (event: Event) => {\n if (cleanup || disposed || !matches() || event.target !== trigger?.root)\n return\n if (!(event instanceof CustomEvent)) return\n pendingEvent = event\n pendingClick = undefined\n }\n const change = () => void update()\n media.addEventListener(\"change\", change)\n trigger?.root.addEventListener(\"click\", click, true)\n if (trigger?.event) trigger.root.addEventListener(trigger.event, request)\n change()\n return () => {\n disposed = true\n epoch++\n pendingClick = undefined\n pendingEvent = undefined\n media.removeEventListener(\"change\", change)\n trigger?.root.removeEventListener(\"click\", click, true)\n if (trigger?.event) trigger.root.removeEventListener(trigger.event, request)\n cleanup?.()\n cleanup = undefined\n }\n}\n", "type": "registry:lib" } ], diff --git a/public/r/styles/base-vega/on-media.json b/public/r/styles/base-vega/on-media.json index 1b397d94c..28d5960f4 100644 --- a/public/r/styles/base-vega/on-media.json +++ b/public/r/styles/base-vega/on-media.json @@ -6,7 +6,7 @@ "files": [ { "path": "src/lib/on-media.ts", - "content": "type Trigger = { root: Element; selector: string; event?: string }\n\n/** Import and mount only while the query matches; dispose on exit. */\nexport function onMedia(\n query: string,\n load: () => Promise,\n mount: (module: T) => () => void,\n trigger?: Trigger\n) {\n const media = window.matchMedia(query)\n let disposed = false\n let epoch = 0\n let cleanup: (() => void) | undefined\n let pendingClick: HTMLElement | undefined\n let pendingEvent: CustomEvent | undefined\n\n const replayClick = () => {\n const target = pendingClick\n const event = pendingEvent\n pendingClick = undefined\n pendingEvent = undefined\n if (event && trigger?.root.isConnected)\n trigger.root.dispatchEvent(\n new CustomEvent(event.type, { detail: event.detail })\n )\n else if (target?.isConnected)\n target.dispatchEvent(new MouseEvent(\"click\", { cancelable: true }))\n }\n\n const update = async () => {\n const current = ++epoch\n cleanup?.()\n cleanup = undefined\n if (!media.matches || disposed) {\n pendingClick = undefined\n pendingEvent = undefined\n return\n }\n try {\n const module = await load()\n if (disposed || !media.matches || current !== epoch) return\n cleanup = mount(module)\n replayClick()\n } catch (error) {\n if (!disposed && current === epoch) {\n pendingClick = undefined\n pendingEvent = undefined\n console.error(error)\n }\n }\n }\n const click = (event: Event) => {\n if (\n !trigger ||\n cleanup ||\n disposed ||\n !media.matches ||\n event.defaultPrevented\n )\n return\n if (!(event.target instanceof Element)) return\n const target = event.target.closest(trigger.selector)\n if (!target || !trigger.root.contains(target)) return\n pendingClick ??= target\n pendingEvent = undefined\n event.preventDefault()\n }\n const request = (event: Event) => {\n if (cleanup || disposed || !media.matches || event.target !== trigger?.root)\n return\n if (!(event instanceof CustomEvent)) return\n pendingEvent = event\n pendingClick = undefined\n }\n const change = () => void update()\n media.addEventListener(\"change\", change)\n trigger?.root.addEventListener(\"click\", click, true)\n if (trigger?.event) trigger.root.addEventListener(trigger.event, request)\n change()\n return () => {\n disposed = true\n epoch++\n pendingClick = undefined\n pendingEvent = undefined\n media.removeEventListener(\"change\", change)\n trigger?.root.removeEventListener(\"click\", click, true)\n if (trigger?.event) trigger.root.removeEventListener(trigger.event, request)\n cleanup?.()\n cleanup = undefined\n }\n}\n", + "content": "type Trigger = { root: Element; selector: string; event?: string }\n\n/** Import and mount only while the query matches; dispose on exit. */\nexport function onMedia(\n query: string,\n load: () => Promise,\n mount: (module: T) => () => void,\n trigger?: Trigger\n) {\n const media = window.matchMedia(query)\n const matches = () => window.matchMedia(query).matches\n let disposed = false\n let epoch = 0\n let cleanup: (() => void) | undefined\n let pendingClick: HTMLElement | undefined\n let pendingEvent: CustomEvent | undefined\n\n const replayClick = () => {\n const target = pendingClick\n const event = pendingEvent\n pendingClick = undefined\n pendingEvent = undefined\n if (event && trigger?.root.isConnected)\n trigger.root.dispatchEvent(\n new CustomEvent(event.type, { detail: event.detail })\n )\n else if (target?.isConnected)\n target.dispatchEvent(new MouseEvent(\"click\", { cancelable: true }))\n }\n\n const update = async () => {\n const current = ++epoch\n cleanup?.()\n cleanup = undefined\n if (!matches() || disposed) {\n pendingClick = undefined\n pendingEvent = undefined\n return\n }\n try {\n const module = await load()\n if (disposed || !matches() || current !== epoch) return\n cleanup = mount(module)\n replayClick()\n } catch (error) {\n if (!disposed && current === epoch) {\n pendingClick = undefined\n pendingEvent = undefined\n console.error(error)\n }\n }\n }\n const click = (event: Event) => {\n if (!trigger || cleanup || disposed || !matches() || event.defaultPrevented)\n return\n if (!(event.target instanceof Element)) return\n const target = event.target.closest(trigger.selector)\n if (!target || !trigger.root.contains(target)) return\n pendingClick ??= target\n pendingEvent = undefined\n event.preventDefault()\n }\n const request = (event: Event) => {\n if (cleanup || disposed || !matches() || event.target !== trigger?.root)\n return\n if (!(event instanceof CustomEvent)) return\n pendingEvent = event\n pendingClick = undefined\n }\n const change = () => void update()\n media.addEventListener(\"change\", change)\n trigger?.root.addEventListener(\"click\", click, true)\n if (trigger?.event) trigger.root.addEventListener(trigger.event, request)\n change()\n return () => {\n disposed = true\n epoch++\n pendingClick = undefined\n pendingEvent = undefined\n media.removeEventListener(\"change\", change)\n trigger?.root.removeEventListener(\"click\", click, true)\n if (trigger?.event) trigger.root.removeEventListener(trigger.event, request)\n cleanup?.()\n cleanup = undefined\n }\n}\n", "type": "registry:lib" } ], diff --git a/src/lib/on-media.ts b/src/lib/on-media.ts index e0f19eddc..3392b6bd2 100644 --- a/src/lib/on-media.ts +++ b/src/lib/on-media.ts @@ -8,6 +8,7 @@ export function onMedia( trigger?: Trigger ) { const media = window.matchMedia(query) + const matches = () => window.matchMedia(query).matches let disposed = false let epoch = 0 let cleanup: (() => void) | undefined @@ -31,14 +32,14 @@ export function onMedia( const current = ++epoch cleanup?.() cleanup = undefined - if (!media.matches || disposed) { + if (!matches() || disposed) { pendingClick = undefined pendingEvent = undefined return } try { const module = await load() - if (disposed || !media.matches || current !== epoch) return + if (disposed || !matches() || current !== epoch) return cleanup = mount(module) replayClick() } catch (error) { @@ -50,13 +51,7 @@ export function onMedia( } } const click = (event: Event) => { - if ( - !trigger || - cleanup || - disposed || - !media.matches || - event.defaultPrevented - ) + if (!trigger || cleanup || disposed || !matches() || event.defaultPrevented) return if (!(event.target instanceof Element)) return const target = event.target.closest(trigger.selector) @@ -66,7 +61,7 @@ export function onMedia( event.preventDefault() } const request = (event: Event) => { - if (cleanup || disposed || !media.matches || event.target !== trigger?.root) + if (cleanup || disposed || !matches() || event.target !== trigger?.root) return if (!(event instanceof CustomEvent)) return pendingEvent = event diff --git a/tests/browser/responsive-menu.test.mjs b/tests/browser/responsive-menu.test.mjs index a5eda8274..bd84b56a1 100644 --- a/tests/browser/responsive-menu.test.mjs +++ b/tests/browser/responsive-menu.test.mjs @@ -117,7 +117,16 @@ for (const type of [chromium, firefox, webkit]) { .getByRole("link", { name: "Product one", exact: true }) .waitFor({ state: "hidden" }) assert.equal(await navigation.isVisible(), false) - await trigger.click() + await trigger.waitFor({ state: "visible" }) + await trigger.focus() + assert.equal( + await trigger.evaluate( + (element) => element === document.activeElement + ), + true, + `header ${number} ${fold}: keyboard trigger focus` + ) + await trigger.press("Enter") await page .getByRole("dialog", { name: "Mobile navigation", exact: true }) .waitFor() diff --git a/tests/on-media.test.mjs b/tests/on-media.test.mjs index 36ab458a4..577957bdf 100644 --- a/tests/on-media.test.mjs +++ b/tests/on-media.test.mjs @@ -81,59 +81,66 @@ test("disposing during import prevents a later mount", async () => { globalThis.window = previous }) -for (const action of ["click", "request"]) - test(`retains ${action} before the media change event arrives`, async () => { - const previous = { - window: globalThis.window, - Element: globalThis.Element, - MouseEvent: globalThis.MouseEvent, - } - class Element extends EventTarget { - isConnected = true - closest() { - return this +for (const stale of [false, true]) + for (const action of ["click", "request"]) + test(`retains ${action} before the media change event arrives, stale=${stale}`, async () => { + const previous = { + window: globalThis.window, + Element: globalThis.Element, + MouseEvent: globalThis.MouseEvent, } - contains(target) { - return target === this + class Element extends EventTarget { + isConnected = true + closest() { + return this + } + contains(target) { + return target === this + } } - } - const media = new EventTarget() - media.matches = false - globalThis.window = { matchMedia: () => media } - globalThis.Element = Element - globalThis.MouseEvent = Event - const root = new Element() - const received = [] - const event = action === "click" ? "click" : "dialog:set" - const receive = (event) => received.push(event.detail?.open ?? "click") - const dispose = onMedia( - "mobile", - async () => ({}), - () => { - root.addEventListener(event, receive) - return () => root.removeEventListener(event, receive) - }, - { root, selector: "button", event: "dialog:set" } - ) - try { - media.matches = true - root.dispatchEvent( - action === "click" - ? new Event("click", { cancelable: true }) - : new CustomEvent("dialog:set", { detail: { open: true } }) - ) - assert.deepEqual(received, []) - media.dispatchEvent(new Event("change")) - await Promise.resolve() - assert.deepEqual(received, [action === "click" ? "click" : true]) + const media = new EventTarget() media.matches = false - media.dispatchEvent(new Event("change")) - media.matches = true - media.dispatchEvent(new Event("change")) - await Promise.resolve() - assert.equal(received.length, 1) - } finally { - dispose() - Object.assign(globalThis, previous) - } - }) + let current = false + let reads = 0 + globalThis.window = { + matchMedia: () => (stale && reads++ > 0 ? { matches: current } : media), + } + globalThis.Element = Element + globalThis.MouseEvent = Event + const root = new Element() + const received = [] + const event = action === "click" ? "click" : "dialog:set" + const receive = (event) => received.push(event.detail?.open ?? "click") + const dispose = onMedia( + "mobile", + async () => ({}), + () => { + root.addEventListener(event, receive) + return () => root.removeEventListener(event, receive) + }, + { root, selector: "button", event: "dialog:set" } + ) + try { + current = true + if (!stale) media.matches = true + root.dispatchEvent( + action === "click" + ? new Event("click", { cancelable: true }) + : new CustomEvent("dialog:set", { detail: { open: true } }) + ) + assert.deepEqual(received, []) + media.matches = true + media.dispatchEvent(new Event("change")) + await Promise.resolve() + assert.deepEqual(received, [action === "click" ? "click" : true]) + current = media.matches = false + media.dispatchEvent(new Event("change")) + current = media.matches = true + media.dispatchEvent(new Event("change")) + await Promise.resolve() + assert.equal(received.length, 1) + } finally { + dispose() + Object.assign(globalThis, previous) + } + }) From 24bf969d6ee39d2121936968d07f8bb81ec2718b Mon Sep 17 00:00:00 2001 From: Sil Date: Sat, 24 Oct 2026 17:08:04 +0200 Subject: [PATCH 3/5] fix: cancel pending actions when import observes media exit --- src/lib/on-media.ts | 7 ++++- tests/on-media.test.mjs | 68 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 74 insertions(+), 1 deletion(-) diff --git a/src/lib/on-media.ts b/src/lib/on-media.ts index 3392b6bd2..4c0265e82 100644 --- a/src/lib/on-media.ts +++ b/src/lib/on-media.ts @@ -39,7 +39,12 @@ export function onMedia( } try { const module = await load() - if (disposed || !matches() || current !== epoch) return + if (disposed || current !== epoch) return + if (!matches()) { + pendingClick = undefined + pendingEvent = undefined + return + } cleanup = mount(module) replayClick() } catch (error) { diff --git a/tests/on-media.test.mjs b/tests/on-media.test.mjs index 577957bdf..625db0840 100644 --- a/tests/on-media.test.mjs +++ b/tests/on-media.test.mjs @@ -144,3 +144,71 @@ for (const stale of [false, true]) Object.assign(globalThis, previous) } }) + +for (const action of ["click", "request"]) + test(`discards ${action} when import observes exit before its notification`, async () => { + const previous = { + window: globalThis.window, + Element: globalThis.Element, + MouseEvent: globalThis.MouseEvent, + } + class Element extends EventTarget { + isConnected = true + closest() { + return this + } + contains(target) { + return target === this + } + } + const media = new EventTarget() + media.matches = true + let current = true + let reads = 0 + globalThis.window = { + matchMedia: () => (reads++ === 0 ? media : { matches: current }), + } + globalThis.Element = Element + globalThis.MouseEvent = Event + const root = new Element() + const received = [] + const resolves = [] + let mounts = 0 + const event = action === "click" ? "click" : "dialog:set" + const receive = (event) => received.push(event.detail?.open ?? "click") + const activate = () => + root.dispatchEvent( + action === "click" + ? new Event("click", { cancelable: true }) + : new CustomEvent("dialog:set", { detail: { open: true } }) + ) + const dispose = onMedia( + "mobile", + () => new Promise((done) => resolves.push(done)), + () => { + mounts++ + root.addEventListener(event, receive) + return () => root.removeEventListener(event, receive) + }, + { root, selector: "button", event: "dialog:set" } + ) + try { + activate() + current = false + resolves.shift()({}) + await Promise.resolve() + assert.equal(mounts, 0) + assert.deepEqual(received, []) + current = true + media.dispatchEvent(new Event("change")) + resolves.shift()({}) + await Promise.resolve() + assert.equal(mounts, 1) + assert.deepEqual(received, []) + activate() + assert.deepEqual(received, [action === "click" ? "click" : true]) + } finally { + dispose() + Object.assign(globalThis, previous) + } + }) From 6294e74f768b253dcaab0ff626193810eb681f1e Mon Sep 17 00:00:00 2001 From: Sil Date: Sat, 24 Oct 2026 17:15:49 +0200 Subject: [PATCH 4/5] fix: cancel queued media actions on every observed exit --- src/lib/on-media.ts | 17 +++-- tests/on-media.test.mjs | 140 ++++++++++++++++++++++------------------ 2 files changed, 87 insertions(+), 70 deletions(-) diff --git a/src/lib/on-media.ts b/src/lib/on-media.ts index 4c0265e82..2e8054dfa 100644 --- a/src/lib/on-media.ts +++ b/src/lib/on-media.ts @@ -8,13 +8,21 @@ export function onMedia( trigger?: Trigger ) { const media = window.matchMedia(query) - const matches = () => window.matchMedia(query).matches let disposed = false let epoch = 0 let cleanup: (() => void) | undefined let pendingClick: HTMLElement | undefined let pendingEvent: CustomEvent | undefined + const matches = () => { + const matching = window.matchMedia(query).matches + if (!matching) { + pendingClick = undefined + pendingEvent = undefined + } + return matching + } + const replayClick = () => { const target = pendingClick const event = pendingEvent @@ -39,12 +47,7 @@ export function onMedia( } try { const module = await load() - if (disposed || current !== epoch) return - if (!matches()) { - pendingClick = undefined - pendingEvent = undefined - return - } + if (disposed || current !== epoch || !matches()) return cleanup = mount(module) replayClick() } catch (error) { diff --git a/tests/on-media.test.mjs b/tests/on-media.test.mjs index 625db0840..a1d293378 100644 --- a/tests/on-media.test.mjs +++ b/tests/on-media.test.mjs @@ -146,69 +146,83 @@ for (const stale of [false, true]) }) for (const action of ["click", "request"]) - test(`discards ${action} when import observes exit before its notification`, async () => { - const previous = { - window: globalThis.window, - Element: globalThis.Element, - MouseEvent: globalThis.MouseEvent, - } - class Element extends EventTarget { - isConnected = true - closest() { - return this + for (const observer of ["import", "click", "request"]) + test(`discards ${action} when ${observer} observes exit before its notification`, async () => { + const previous = { + window: globalThis.window, + Element: globalThis.Element, + MouseEvent: globalThis.MouseEvent, + } + class Element extends EventTarget { + isConnected = true + closest() { + return this + } + contains(target) { + return target === this + } } - contains(target) { - return target === this + const media = new EventTarget() + media.matches = true + let current = true + let reads = 0 + globalThis.window = { + matchMedia: () => (reads++ === 0 ? media : { matches: current }), } - } - const media = new EventTarget() - media.matches = true - let current = true - let reads = 0 - globalThis.window = { - matchMedia: () => (reads++ === 0 ? media : { matches: current }), - } - globalThis.Element = Element - globalThis.MouseEvent = Event - const root = new Element() - const received = [] - const resolves = [] - let mounts = 0 - const event = action === "click" ? "click" : "dialog:set" - const receive = (event) => received.push(event.detail?.open ?? "click") - const activate = () => - root.dispatchEvent( - action === "click" - ? new Event("click", { cancelable: true }) - : new CustomEvent("dialog:set", { detail: { open: true } }) + globalThis.Element = Element + globalThis.MouseEvent = Event + const root = new Element() + const received = [] + const resolves = [] + let mounts = 0 + const event = action === "click" ? "click" : "dialog:set" + const receive = (event) => received.push(event.detail?.open ?? "click") + const activate = () => + root.dispatchEvent( + action === "click" + ? new Event("click", { cancelable: true }) + : new CustomEvent("dialog:set", { detail: { open: true } }) + ) + const dispose = onMedia( + "mobile", + () => new Promise((done) => resolves.push(done)), + () => { + mounts++ + root.addEventListener(event, receive) + return () => root.removeEventListener(event, receive) + }, + { root, selector: "button", event: "dialog:set" } ) - const dispose = onMedia( - "mobile", - () => new Promise((done) => resolves.push(done)), - () => { - mounts++ - root.addEventListener(event, receive) - return () => root.removeEventListener(event, receive) - }, - { root, selector: "button", event: "dialog:set" } - ) - try { - activate() - current = false - resolves.shift()({}) - await Promise.resolve() - assert.equal(mounts, 0) - assert.deepEqual(received, []) - current = true - media.dispatchEvent(new Event("change")) - resolves.shift()({}) - await Promise.resolve() - assert.equal(mounts, 1) - assert.deepEqual(received, []) - activate() - assert.deepEqual(received, [action === "click" ? "click" : true]) - } finally { - dispose() - Object.assign(globalThis, previous) - } - }) + try { + activate() + current = false + if (observer === "import") { + resolves.shift()({}) + await Promise.resolve() + } else { + root.dispatchEvent( + observer === "click" + ? new Event("click", { cancelable: true }) + : new CustomEvent("dialog:set", { detail: { open: false } }) + ) + } + assert.equal(mounts, 0) + assert.deepEqual(received, []) + current = true + media.dispatchEvent(new Event("change")) + if (observer !== "import") { + resolves.shift()({}) + await Promise.resolve() + assert.equal(mounts, 0) + } + resolves.shift()({}) + await Promise.resolve() + assert.equal(mounts, 1) + assert.deepEqual(received, []) + activate() + assert.deepEqual(received, [action === "click" ? "click" : true]) + } finally { + dispose() + Object.assign(globalThis, previous) + } + }) From caced0785ab9ef260b6ba8ba27c49cde349e3f3a Mon Sep 17 00:00:00 2001 From: Sil Date: Sat, 24 Oct 2026 17:19:11 +0200 Subject: [PATCH 5/5] build: publish media exit cancellation in all registry styles --- public/r/styles/base-luma/on-media.json | 2 +- public/r/styles/base-lyra/on-media.json | 2 +- public/r/styles/base-maia/on-media.json | 2 +- public/r/styles/base-mira/on-media.json | 2 +- public/r/styles/base-nova/on-media.json | 2 +- public/r/styles/base-rhea/on-media.json | 2 +- public/r/styles/base-sera/on-media.json | 2 +- public/r/styles/base-vega/on-media.json | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) diff --git a/public/r/styles/base-luma/on-media.json b/public/r/styles/base-luma/on-media.json index 28d5960f4..7d8c6038c 100644 --- a/public/r/styles/base-luma/on-media.json +++ b/public/r/styles/base-luma/on-media.json @@ -6,7 +6,7 @@ "files": [ { "path": "src/lib/on-media.ts", - "content": "type Trigger = { root: Element; selector: string; event?: string }\n\n/** Import and mount only while the query matches; dispose on exit. */\nexport function onMedia(\n query: string,\n load: () => Promise,\n mount: (module: T) => () => void,\n trigger?: Trigger\n) {\n const media = window.matchMedia(query)\n const matches = () => window.matchMedia(query).matches\n let disposed = false\n let epoch = 0\n let cleanup: (() => void) | undefined\n let pendingClick: HTMLElement | undefined\n let pendingEvent: CustomEvent | undefined\n\n const replayClick = () => {\n const target = pendingClick\n const event = pendingEvent\n pendingClick = undefined\n pendingEvent = undefined\n if (event && trigger?.root.isConnected)\n trigger.root.dispatchEvent(\n new CustomEvent(event.type, { detail: event.detail })\n )\n else if (target?.isConnected)\n target.dispatchEvent(new MouseEvent(\"click\", { cancelable: true }))\n }\n\n const update = async () => {\n const current = ++epoch\n cleanup?.()\n cleanup = undefined\n if (!matches() || disposed) {\n pendingClick = undefined\n pendingEvent = undefined\n return\n }\n try {\n const module = await load()\n if (disposed || !matches() || current !== epoch) return\n cleanup = mount(module)\n replayClick()\n } catch (error) {\n if (!disposed && current === epoch) {\n pendingClick = undefined\n pendingEvent = undefined\n console.error(error)\n }\n }\n }\n const click = (event: Event) => {\n if (!trigger || cleanup || disposed || !matches() || event.defaultPrevented)\n return\n if (!(event.target instanceof Element)) return\n const target = event.target.closest(trigger.selector)\n if (!target || !trigger.root.contains(target)) return\n pendingClick ??= target\n pendingEvent = undefined\n event.preventDefault()\n }\n const request = (event: Event) => {\n if (cleanup || disposed || !matches() || event.target !== trigger?.root)\n return\n if (!(event instanceof CustomEvent)) return\n pendingEvent = event\n pendingClick = undefined\n }\n const change = () => void update()\n media.addEventListener(\"change\", change)\n trigger?.root.addEventListener(\"click\", click, true)\n if (trigger?.event) trigger.root.addEventListener(trigger.event, request)\n change()\n return () => {\n disposed = true\n epoch++\n pendingClick = undefined\n pendingEvent = undefined\n media.removeEventListener(\"change\", change)\n trigger?.root.removeEventListener(\"click\", click, true)\n if (trigger?.event) trigger.root.removeEventListener(trigger.event, request)\n cleanup?.()\n cleanup = undefined\n }\n}\n", + "content": "type Trigger = { root: Element; selector: string; event?: string }\n\n/** Import and mount only while the query matches; dispose on exit. */\nexport function onMedia(\n query: string,\n load: () => Promise,\n mount: (module: T) => () => void,\n trigger?: Trigger\n) {\n const media = window.matchMedia(query)\n let disposed = false\n let epoch = 0\n let cleanup: (() => void) | undefined\n let pendingClick: HTMLElement | undefined\n let pendingEvent: CustomEvent | undefined\n\n const matches = () => {\n const matching = window.matchMedia(query).matches\n if (!matching) {\n pendingClick = undefined\n pendingEvent = undefined\n }\n return matching\n }\n\n const replayClick = () => {\n const target = pendingClick\n const event = pendingEvent\n pendingClick = undefined\n pendingEvent = undefined\n if (event && trigger?.root.isConnected)\n trigger.root.dispatchEvent(\n new CustomEvent(event.type, { detail: event.detail })\n )\n else if (target?.isConnected)\n target.dispatchEvent(new MouseEvent(\"click\", { cancelable: true }))\n }\n\n const update = async () => {\n const current = ++epoch\n cleanup?.()\n cleanup = undefined\n if (!matches() || disposed) {\n pendingClick = undefined\n pendingEvent = undefined\n return\n }\n try {\n const module = await load()\n if (disposed || current !== epoch || !matches()) return\n cleanup = mount(module)\n replayClick()\n } catch (error) {\n if (!disposed && current === epoch) {\n pendingClick = undefined\n pendingEvent = undefined\n console.error(error)\n }\n }\n }\n const click = (event: Event) => {\n if (!trigger || cleanup || disposed || !matches() || event.defaultPrevented)\n return\n if (!(event.target instanceof Element)) return\n const target = event.target.closest(trigger.selector)\n if (!target || !trigger.root.contains(target)) return\n pendingClick ??= target\n pendingEvent = undefined\n event.preventDefault()\n }\n const request = (event: Event) => {\n if (cleanup || disposed || !matches() || event.target !== trigger?.root)\n return\n if (!(event instanceof CustomEvent)) return\n pendingEvent = event\n pendingClick = undefined\n }\n const change = () => void update()\n media.addEventListener(\"change\", change)\n trigger?.root.addEventListener(\"click\", click, true)\n if (trigger?.event) trigger.root.addEventListener(trigger.event, request)\n change()\n return () => {\n disposed = true\n epoch++\n pendingClick = undefined\n pendingEvent = undefined\n media.removeEventListener(\"change\", change)\n trigger?.root.removeEventListener(\"click\", click, true)\n if (trigger?.event) trigger.root.removeEventListener(trigger.event, request)\n cleanup?.()\n cleanup = undefined\n }\n}\n", "type": "registry:lib" } ], diff --git a/public/r/styles/base-lyra/on-media.json b/public/r/styles/base-lyra/on-media.json index 28d5960f4..7d8c6038c 100644 --- a/public/r/styles/base-lyra/on-media.json +++ b/public/r/styles/base-lyra/on-media.json @@ -6,7 +6,7 @@ "files": [ { "path": "src/lib/on-media.ts", - "content": "type Trigger = { root: Element; selector: string; event?: string }\n\n/** Import and mount only while the query matches; dispose on exit. */\nexport function onMedia(\n query: string,\n load: () => Promise,\n mount: (module: T) => () => void,\n trigger?: Trigger\n) {\n const media = window.matchMedia(query)\n const matches = () => window.matchMedia(query).matches\n let disposed = false\n let epoch = 0\n let cleanup: (() => void) | undefined\n let pendingClick: HTMLElement | undefined\n let pendingEvent: CustomEvent | undefined\n\n const replayClick = () => {\n const target = pendingClick\n const event = pendingEvent\n pendingClick = undefined\n pendingEvent = undefined\n if (event && trigger?.root.isConnected)\n trigger.root.dispatchEvent(\n new CustomEvent(event.type, { detail: event.detail })\n )\n else if (target?.isConnected)\n target.dispatchEvent(new MouseEvent(\"click\", { cancelable: true }))\n }\n\n const update = async () => {\n const current = ++epoch\n cleanup?.()\n cleanup = undefined\n if (!matches() || disposed) {\n pendingClick = undefined\n pendingEvent = undefined\n return\n }\n try {\n const module = await load()\n if (disposed || !matches() || current !== epoch) return\n cleanup = mount(module)\n replayClick()\n } catch (error) {\n if (!disposed && current === epoch) {\n pendingClick = undefined\n pendingEvent = undefined\n console.error(error)\n }\n }\n }\n const click = (event: Event) => {\n if (!trigger || cleanup || disposed || !matches() || event.defaultPrevented)\n return\n if (!(event.target instanceof Element)) return\n const target = event.target.closest(trigger.selector)\n if (!target || !trigger.root.contains(target)) return\n pendingClick ??= target\n pendingEvent = undefined\n event.preventDefault()\n }\n const request = (event: Event) => {\n if (cleanup || disposed || !matches() || event.target !== trigger?.root)\n return\n if (!(event instanceof CustomEvent)) return\n pendingEvent = event\n pendingClick = undefined\n }\n const change = () => void update()\n media.addEventListener(\"change\", change)\n trigger?.root.addEventListener(\"click\", click, true)\n if (trigger?.event) trigger.root.addEventListener(trigger.event, request)\n change()\n return () => {\n disposed = true\n epoch++\n pendingClick = undefined\n pendingEvent = undefined\n media.removeEventListener(\"change\", change)\n trigger?.root.removeEventListener(\"click\", click, true)\n if (trigger?.event) trigger.root.removeEventListener(trigger.event, request)\n cleanup?.()\n cleanup = undefined\n }\n}\n", + "content": "type Trigger = { root: Element; selector: string; event?: string }\n\n/** Import and mount only while the query matches; dispose on exit. */\nexport function onMedia(\n query: string,\n load: () => Promise,\n mount: (module: T) => () => void,\n trigger?: Trigger\n) {\n const media = window.matchMedia(query)\n let disposed = false\n let epoch = 0\n let cleanup: (() => void) | undefined\n let pendingClick: HTMLElement | undefined\n let pendingEvent: CustomEvent | undefined\n\n const matches = () => {\n const matching = window.matchMedia(query).matches\n if (!matching) {\n pendingClick = undefined\n pendingEvent = undefined\n }\n return matching\n }\n\n const replayClick = () => {\n const target = pendingClick\n const event = pendingEvent\n pendingClick = undefined\n pendingEvent = undefined\n if (event && trigger?.root.isConnected)\n trigger.root.dispatchEvent(\n new CustomEvent(event.type, { detail: event.detail })\n )\n else if (target?.isConnected)\n target.dispatchEvent(new MouseEvent(\"click\", { cancelable: true }))\n }\n\n const update = async () => {\n const current = ++epoch\n cleanup?.()\n cleanup = undefined\n if (!matches() || disposed) {\n pendingClick = undefined\n pendingEvent = undefined\n return\n }\n try {\n const module = await load()\n if (disposed || current !== epoch || !matches()) return\n cleanup = mount(module)\n replayClick()\n } catch (error) {\n if (!disposed && current === epoch) {\n pendingClick = undefined\n pendingEvent = undefined\n console.error(error)\n }\n }\n }\n const click = (event: Event) => {\n if (!trigger || cleanup || disposed || !matches() || event.defaultPrevented)\n return\n if (!(event.target instanceof Element)) return\n const target = event.target.closest(trigger.selector)\n if (!target || !trigger.root.contains(target)) return\n pendingClick ??= target\n pendingEvent = undefined\n event.preventDefault()\n }\n const request = (event: Event) => {\n if (cleanup || disposed || !matches() || event.target !== trigger?.root)\n return\n if (!(event instanceof CustomEvent)) return\n pendingEvent = event\n pendingClick = undefined\n }\n const change = () => void update()\n media.addEventListener(\"change\", change)\n trigger?.root.addEventListener(\"click\", click, true)\n if (trigger?.event) trigger.root.addEventListener(trigger.event, request)\n change()\n return () => {\n disposed = true\n epoch++\n pendingClick = undefined\n pendingEvent = undefined\n media.removeEventListener(\"change\", change)\n trigger?.root.removeEventListener(\"click\", click, true)\n if (trigger?.event) trigger.root.removeEventListener(trigger.event, request)\n cleanup?.()\n cleanup = undefined\n }\n}\n", "type": "registry:lib" } ], diff --git a/public/r/styles/base-maia/on-media.json b/public/r/styles/base-maia/on-media.json index 28d5960f4..7d8c6038c 100644 --- a/public/r/styles/base-maia/on-media.json +++ b/public/r/styles/base-maia/on-media.json @@ -6,7 +6,7 @@ "files": [ { "path": "src/lib/on-media.ts", - "content": "type Trigger = { root: Element; selector: string; event?: string }\n\n/** Import and mount only while the query matches; dispose on exit. */\nexport function onMedia(\n query: string,\n load: () => Promise,\n mount: (module: T) => () => void,\n trigger?: Trigger\n) {\n const media = window.matchMedia(query)\n const matches = () => window.matchMedia(query).matches\n let disposed = false\n let epoch = 0\n let cleanup: (() => void) | undefined\n let pendingClick: HTMLElement | undefined\n let pendingEvent: CustomEvent | undefined\n\n const replayClick = () => {\n const target = pendingClick\n const event = pendingEvent\n pendingClick = undefined\n pendingEvent = undefined\n if (event && trigger?.root.isConnected)\n trigger.root.dispatchEvent(\n new CustomEvent(event.type, { detail: event.detail })\n )\n else if (target?.isConnected)\n target.dispatchEvent(new MouseEvent(\"click\", { cancelable: true }))\n }\n\n const update = async () => {\n const current = ++epoch\n cleanup?.()\n cleanup = undefined\n if (!matches() || disposed) {\n pendingClick = undefined\n pendingEvent = undefined\n return\n }\n try {\n const module = await load()\n if (disposed || !matches() || current !== epoch) return\n cleanup = mount(module)\n replayClick()\n } catch (error) {\n if (!disposed && current === epoch) {\n pendingClick = undefined\n pendingEvent = undefined\n console.error(error)\n }\n }\n }\n const click = (event: Event) => {\n if (!trigger || cleanup || disposed || !matches() || event.defaultPrevented)\n return\n if (!(event.target instanceof Element)) return\n const target = event.target.closest(trigger.selector)\n if (!target || !trigger.root.contains(target)) return\n pendingClick ??= target\n pendingEvent = undefined\n event.preventDefault()\n }\n const request = (event: Event) => {\n if (cleanup || disposed || !matches() || event.target !== trigger?.root)\n return\n if (!(event instanceof CustomEvent)) return\n pendingEvent = event\n pendingClick = undefined\n }\n const change = () => void update()\n media.addEventListener(\"change\", change)\n trigger?.root.addEventListener(\"click\", click, true)\n if (trigger?.event) trigger.root.addEventListener(trigger.event, request)\n change()\n return () => {\n disposed = true\n epoch++\n pendingClick = undefined\n pendingEvent = undefined\n media.removeEventListener(\"change\", change)\n trigger?.root.removeEventListener(\"click\", click, true)\n if (trigger?.event) trigger.root.removeEventListener(trigger.event, request)\n cleanup?.()\n cleanup = undefined\n }\n}\n", + "content": "type Trigger = { root: Element; selector: string; event?: string }\n\n/** Import and mount only while the query matches; dispose on exit. */\nexport function onMedia(\n query: string,\n load: () => Promise,\n mount: (module: T) => () => void,\n trigger?: Trigger\n) {\n const media = window.matchMedia(query)\n let disposed = false\n let epoch = 0\n let cleanup: (() => void) | undefined\n let pendingClick: HTMLElement | undefined\n let pendingEvent: CustomEvent | undefined\n\n const matches = () => {\n const matching = window.matchMedia(query).matches\n if (!matching) {\n pendingClick = undefined\n pendingEvent = undefined\n }\n return matching\n }\n\n const replayClick = () => {\n const target = pendingClick\n const event = pendingEvent\n pendingClick = undefined\n pendingEvent = undefined\n if (event && trigger?.root.isConnected)\n trigger.root.dispatchEvent(\n new CustomEvent(event.type, { detail: event.detail })\n )\n else if (target?.isConnected)\n target.dispatchEvent(new MouseEvent(\"click\", { cancelable: true }))\n }\n\n const update = async () => {\n const current = ++epoch\n cleanup?.()\n cleanup = undefined\n if (!matches() || disposed) {\n pendingClick = undefined\n pendingEvent = undefined\n return\n }\n try {\n const module = await load()\n if (disposed || current !== epoch || !matches()) return\n cleanup = mount(module)\n replayClick()\n } catch (error) {\n if (!disposed && current === epoch) {\n pendingClick = undefined\n pendingEvent = undefined\n console.error(error)\n }\n }\n }\n const click = (event: Event) => {\n if (!trigger || cleanup || disposed || !matches() || event.defaultPrevented)\n return\n if (!(event.target instanceof Element)) return\n const target = event.target.closest(trigger.selector)\n if (!target || !trigger.root.contains(target)) return\n pendingClick ??= target\n pendingEvent = undefined\n event.preventDefault()\n }\n const request = (event: Event) => {\n if (cleanup || disposed || !matches() || event.target !== trigger?.root)\n return\n if (!(event instanceof CustomEvent)) return\n pendingEvent = event\n pendingClick = undefined\n }\n const change = () => void update()\n media.addEventListener(\"change\", change)\n trigger?.root.addEventListener(\"click\", click, true)\n if (trigger?.event) trigger.root.addEventListener(trigger.event, request)\n change()\n return () => {\n disposed = true\n epoch++\n pendingClick = undefined\n pendingEvent = undefined\n media.removeEventListener(\"change\", change)\n trigger?.root.removeEventListener(\"click\", click, true)\n if (trigger?.event) trigger.root.removeEventListener(trigger.event, request)\n cleanup?.()\n cleanup = undefined\n }\n}\n", "type": "registry:lib" } ], diff --git a/public/r/styles/base-mira/on-media.json b/public/r/styles/base-mira/on-media.json index 28d5960f4..7d8c6038c 100644 --- a/public/r/styles/base-mira/on-media.json +++ b/public/r/styles/base-mira/on-media.json @@ -6,7 +6,7 @@ "files": [ { "path": "src/lib/on-media.ts", - "content": "type Trigger = { root: Element; selector: string; event?: string }\n\n/** Import and mount only while the query matches; dispose on exit. */\nexport function onMedia(\n query: string,\n load: () => Promise,\n mount: (module: T) => () => void,\n trigger?: Trigger\n) {\n const media = window.matchMedia(query)\n const matches = () => window.matchMedia(query).matches\n let disposed = false\n let epoch = 0\n let cleanup: (() => void) | undefined\n let pendingClick: HTMLElement | undefined\n let pendingEvent: CustomEvent | undefined\n\n const replayClick = () => {\n const target = pendingClick\n const event = pendingEvent\n pendingClick = undefined\n pendingEvent = undefined\n if (event && trigger?.root.isConnected)\n trigger.root.dispatchEvent(\n new CustomEvent(event.type, { detail: event.detail })\n )\n else if (target?.isConnected)\n target.dispatchEvent(new MouseEvent(\"click\", { cancelable: true }))\n }\n\n const update = async () => {\n const current = ++epoch\n cleanup?.()\n cleanup = undefined\n if (!matches() || disposed) {\n pendingClick = undefined\n pendingEvent = undefined\n return\n }\n try {\n const module = await load()\n if (disposed || !matches() || current !== epoch) return\n cleanup = mount(module)\n replayClick()\n } catch (error) {\n if (!disposed && current === epoch) {\n pendingClick = undefined\n pendingEvent = undefined\n console.error(error)\n }\n }\n }\n const click = (event: Event) => {\n if (!trigger || cleanup || disposed || !matches() || event.defaultPrevented)\n return\n if (!(event.target instanceof Element)) return\n const target = event.target.closest(trigger.selector)\n if (!target || !trigger.root.contains(target)) return\n pendingClick ??= target\n pendingEvent = undefined\n event.preventDefault()\n }\n const request = (event: Event) => {\n if (cleanup || disposed || !matches() || event.target !== trigger?.root)\n return\n if (!(event instanceof CustomEvent)) return\n pendingEvent = event\n pendingClick = undefined\n }\n const change = () => void update()\n media.addEventListener(\"change\", change)\n trigger?.root.addEventListener(\"click\", click, true)\n if (trigger?.event) trigger.root.addEventListener(trigger.event, request)\n change()\n return () => {\n disposed = true\n epoch++\n pendingClick = undefined\n pendingEvent = undefined\n media.removeEventListener(\"change\", change)\n trigger?.root.removeEventListener(\"click\", click, true)\n if (trigger?.event) trigger.root.removeEventListener(trigger.event, request)\n cleanup?.()\n cleanup = undefined\n }\n}\n", + "content": "type Trigger = { root: Element; selector: string; event?: string }\n\n/** Import and mount only while the query matches; dispose on exit. */\nexport function onMedia(\n query: string,\n load: () => Promise,\n mount: (module: T) => () => void,\n trigger?: Trigger\n) {\n const media = window.matchMedia(query)\n let disposed = false\n let epoch = 0\n let cleanup: (() => void) | undefined\n let pendingClick: HTMLElement | undefined\n let pendingEvent: CustomEvent | undefined\n\n const matches = () => {\n const matching = window.matchMedia(query).matches\n if (!matching) {\n pendingClick = undefined\n pendingEvent = undefined\n }\n return matching\n }\n\n const replayClick = () => {\n const target = pendingClick\n const event = pendingEvent\n pendingClick = undefined\n pendingEvent = undefined\n if (event && trigger?.root.isConnected)\n trigger.root.dispatchEvent(\n new CustomEvent(event.type, { detail: event.detail })\n )\n else if (target?.isConnected)\n target.dispatchEvent(new MouseEvent(\"click\", { cancelable: true }))\n }\n\n const update = async () => {\n const current = ++epoch\n cleanup?.()\n cleanup = undefined\n if (!matches() || disposed) {\n pendingClick = undefined\n pendingEvent = undefined\n return\n }\n try {\n const module = await load()\n if (disposed || current !== epoch || !matches()) return\n cleanup = mount(module)\n replayClick()\n } catch (error) {\n if (!disposed && current === epoch) {\n pendingClick = undefined\n pendingEvent = undefined\n console.error(error)\n }\n }\n }\n const click = (event: Event) => {\n if (!trigger || cleanup || disposed || !matches() || event.defaultPrevented)\n return\n if (!(event.target instanceof Element)) return\n const target = event.target.closest(trigger.selector)\n if (!target || !trigger.root.contains(target)) return\n pendingClick ??= target\n pendingEvent = undefined\n event.preventDefault()\n }\n const request = (event: Event) => {\n if (cleanup || disposed || !matches() || event.target !== trigger?.root)\n return\n if (!(event instanceof CustomEvent)) return\n pendingEvent = event\n pendingClick = undefined\n }\n const change = () => void update()\n media.addEventListener(\"change\", change)\n trigger?.root.addEventListener(\"click\", click, true)\n if (trigger?.event) trigger.root.addEventListener(trigger.event, request)\n change()\n return () => {\n disposed = true\n epoch++\n pendingClick = undefined\n pendingEvent = undefined\n media.removeEventListener(\"change\", change)\n trigger?.root.removeEventListener(\"click\", click, true)\n if (trigger?.event) trigger.root.removeEventListener(trigger.event, request)\n cleanup?.()\n cleanup = undefined\n }\n}\n", "type": "registry:lib" } ], diff --git a/public/r/styles/base-nova/on-media.json b/public/r/styles/base-nova/on-media.json index 28d5960f4..7d8c6038c 100644 --- a/public/r/styles/base-nova/on-media.json +++ b/public/r/styles/base-nova/on-media.json @@ -6,7 +6,7 @@ "files": [ { "path": "src/lib/on-media.ts", - "content": "type Trigger = { root: Element; selector: string; event?: string }\n\n/** Import and mount only while the query matches; dispose on exit. */\nexport function onMedia(\n query: string,\n load: () => Promise,\n mount: (module: T) => () => void,\n trigger?: Trigger\n) {\n const media = window.matchMedia(query)\n const matches = () => window.matchMedia(query).matches\n let disposed = false\n let epoch = 0\n let cleanup: (() => void) | undefined\n let pendingClick: HTMLElement | undefined\n let pendingEvent: CustomEvent | undefined\n\n const replayClick = () => {\n const target = pendingClick\n const event = pendingEvent\n pendingClick = undefined\n pendingEvent = undefined\n if (event && trigger?.root.isConnected)\n trigger.root.dispatchEvent(\n new CustomEvent(event.type, { detail: event.detail })\n )\n else if (target?.isConnected)\n target.dispatchEvent(new MouseEvent(\"click\", { cancelable: true }))\n }\n\n const update = async () => {\n const current = ++epoch\n cleanup?.()\n cleanup = undefined\n if (!matches() || disposed) {\n pendingClick = undefined\n pendingEvent = undefined\n return\n }\n try {\n const module = await load()\n if (disposed || !matches() || current !== epoch) return\n cleanup = mount(module)\n replayClick()\n } catch (error) {\n if (!disposed && current === epoch) {\n pendingClick = undefined\n pendingEvent = undefined\n console.error(error)\n }\n }\n }\n const click = (event: Event) => {\n if (!trigger || cleanup || disposed || !matches() || event.defaultPrevented)\n return\n if (!(event.target instanceof Element)) return\n const target = event.target.closest(trigger.selector)\n if (!target || !trigger.root.contains(target)) return\n pendingClick ??= target\n pendingEvent = undefined\n event.preventDefault()\n }\n const request = (event: Event) => {\n if (cleanup || disposed || !matches() || event.target !== trigger?.root)\n return\n if (!(event instanceof CustomEvent)) return\n pendingEvent = event\n pendingClick = undefined\n }\n const change = () => void update()\n media.addEventListener(\"change\", change)\n trigger?.root.addEventListener(\"click\", click, true)\n if (trigger?.event) trigger.root.addEventListener(trigger.event, request)\n change()\n return () => {\n disposed = true\n epoch++\n pendingClick = undefined\n pendingEvent = undefined\n media.removeEventListener(\"change\", change)\n trigger?.root.removeEventListener(\"click\", click, true)\n if (trigger?.event) trigger.root.removeEventListener(trigger.event, request)\n cleanup?.()\n cleanup = undefined\n }\n}\n", + "content": "type Trigger = { root: Element; selector: string; event?: string }\n\n/** Import and mount only while the query matches; dispose on exit. */\nexport function onMedia(\n query: string,\n load: () => Promise,\n mount: (module: T) => () => void,\n trigger?: Trigger\n) {\n const media = window.matchMedia(query)\n let disposed = false\n let epoch = 0\n let cleanup: (() => void) | undefined\n let pendingClick: HTMLElement | undefined\n let pendingEvent: CustomEvent | undefined\n\n const matches = () => {\n const matching = window.matchMedia(query).matches\n if (!matching) {\n pendingClick = undefined\n pendingEvent = undefined\n }\n return matching\n }\n\n const replayClick = () => {\n const target = pendingClick\n const event = pendingEvent\n pendingClick = undefined\n pendingEvent = undefined\n if (event && trigger?.root.isConnected)\n trigger.root.dispatchEvent(\n new CustomEvent(event.type, { detail: event.detail })\n )\n else if (target?.isConnected)\n target.dispatchEvent(new MouseEvent(\"click\", { cancelable: true }))\n }\n\n const update = async () => {\n const current = ++epoch\n cleanup?.()\n cleanup = undefined\n if (!matches() || disposed) {\n pendingClick = undefined\n pendingEvent = undefined\n return\n }\n try {\n const module = await load()\n if (disposed || current !== epoch || !matches()) return\n cleanup = mount(module)\n replayClick()\n } catch (error) {\n if (!disposed && current === epoch) {\n pendingClick = undefined\n pendingEvent = undefined\n console.error(error)\n }\n }\n }\n const click = (event: Event) => {\n if (!trigger || cleanup || disposed || !matches() || event.defaultPrevented)\n return\n if (!(event.target instanceof Element)) return\n const target = event.target.closest(trigger.selector)\n if (!target || !trigger.root.contains(target)) return\n pendingClick ??= target\n pendingEvent = undefined\n event.preventDefault()\n }\n const request = (event: Event) => {\n if (cleanup || disposed || !matches() || event.target !== trigger?.root)\n return\n if (!(event instanceof CustomEvent)) return\n pendingEvent = event\n pendingClick = undefined\n }\n const change = () => void update()\n media.addEventListener(\"change\", change)\n trigger?.root.addEventListener(\"click\", click, true)\n if (trigger?.event) trigger.root.addEventListener(trigger.event, request)\n change()\n return () => {\n disposed = true\n epoch++\n pendingClick = undefined\n pendingEvent = undefined\n media.removeEventListener(\"change\", change)\n trigger?.root.removeEventListener(\"click\", click, true)\n if (trigger?.event) trigger.root.removeEventListener(trigger.event, request)\n cleanup?.()\n cleanup = undefined\n }\n}\n", "type": "registry:lib" } ], diff --git a/public/r/styles/base-rhea/on-media.json b/public/r/styles/base-rhea/on-media.json index 28d5960f4..7d8c6038c 100644 --- a/public/r/styles/base-rhea/on-media.json +++ b/public/r/styles/base-rhea/on-media.json @@ -6,7 +6,7 @@ "files": [ { "path": "src/lib/on-media.ts", - "content": "type Trigger = { root: Element; selector: string; event?: string }\n\n/** Import and mount only while the query matches; dispose on exit. */\nexport function onMedia(\n query: string,\n load: () => Promise,\n mount: (module: T) => () => void,\n trigger?: Trigger\n) {\n const media = window.matchMedia(query)\n const matches = () => window.matchMedia(query).matches\n let disposed = false\n let epoch = 0\n let cleanup: (() => void) | undefined\n let pendingClick: HTMLElement | undefined\n let pendingEvent: CustomEvent | undefined\n\n const replayClick = () => {\n const target = pendingClick\n const event = pendingEvent\n pendingClick = undefined\n pendingEvent = undefined\n if (event && trigger?.root.isConnected)\n trigger.root.dispatchEvent(\n new CustomEvent(event.type, { detail: event.detail })\n )\n else if (target?.isConnected)\n target.dispatchEvent(new MouseEvent(\"click\", { cancelable: true }))\n }\n\n const update = async () => {\n const current = ++epoch\n cleanup?.()\n cleanup = undefined\n if (!matches() || disposed) {\n pendingClick = undefined\n pendingEvent = undefined\n return\n }\n try {\n const module = await load()\n if (disposed || !matches() || current !== epoch) return\n cleanup = mount(module)\n replayClick()\n } catch (error) {\n if (!disposed && current === epoch) {\n pendingClick = undefined\n pendingEvent = undefined\n console.error(error)\n }\n }\n }\n const click = (event: Event) => {\n if (!trigger || cleanup || disposed || !matches() || event.defaultPrevented)\n return\n if (!(event.target instanceof Element)) return\n const target = event.target.closest(trigger.selector)\n if (!target || !trigger.root.contains(target)) return\n pendingClick ??= target\n pendingEvent = undefined\n event.preventDefault()\n }\n const request = (event: Event) => {\n if (cleanup || disposed || !matches() || event.target !== trigger?.root)\n return\n if (!(event instanceof CustomEvent)) return\n pendingEvent = event\n pendingClick = undefined\n }\n const change = () => void update()\n media.addEventListener(\"change\", change)\n trigger?.root.addEventListener(\"click\", click, true)\n if (trigger?.event) trigger.root.addEventListener(trigger.event, request)\n change()\n return () => {\n disposed = true\n epoch++\n pendingClick = undefined\n pendingEvent = undefined\n media.removeEventListener(\"change\", change)\n trigger?.root.removeEventListener(\"click\", click, true)\n if (trigger?.event) trigger.root.removeEventListener(trigger.event, request)\n cleanup?.()\n cleanup = undefined\n }\n}\n", + "content": "type Trigger = { root: Element; selector: string; event?: string }\n\n/** Import and mount only while the query matches; dispose on exit. */\nexport function onMedia(\n query: string,\n load: () => Promise,\n mount: (module: T) => () => void,\n trigger?: Trigger\n) {\n const media = window.matchMedia(query)\n let disposed = false\n let epoch = 0\n let cleanup: (() => void) | undefined\n let pendingClick: HTMLElement | undefined\n let pendingEvent: CustomEvent | undefined\n\n const matches = () => {\n const matching = window.matchMedia(query).matches\n if (!matching) {\n pendingClick = undefined\n pendingEvent = undefined\n }\n return matching\n }\n\n const replayClick = () => {\n const target = pendingClick\n const event = pendingEvent\n pendingClick = undefined\n pendingEvent = undefined\n if (event && trigger?.root.isConnected)\n trigger.root.dispatchEvent(\n new CustomEvent(event.type, { detail: event.detail })\n )\n else if (target?.isConnected)\n target.dispatchEvent(new MouseEvent(\"click\", { cancelable: true }))\n }\n\n const update = async () => {\n const current = ++epoch\n cleanup?.()\n cleanup = undefined\n if (!matches() || disposed) {\n pendingClick = undefined\n pendingEvent = undefined\n return\n }\n try {\n const module = await load()\n if (disposed || current !== epoch || !matches()) return\n cleanup = mount(module)\n replayClick()\n } catch (error) {\n if (!disposed && current === epoch) {\n pendingClick = undefined\n pendingEvent = undefined\n console.error(error)\n }\n }\n }\n const click = (event: Event) => {\n if (!trigger || cleanup || disposed || !matches() || event.defaultPrevented)\n return\n if (!(event.target instanceof Element)) return\n const target = event.target.closest(trigger.selector)\n if (!target || !trigger.root.contains(target)) return\n pendingClick ??= target\n pendingEvent = undefined\n event.preventDefault()\n }\n const request = (event: Event) => {\n if (cleanup || disposed || !matches() || event.target !== trigger?.root)\n return\n if (!(event instanceof CustomEvent)) return\n pendingEvent = event\n pendingClick = undefined\n }\n const change = () => void update()\n media.addEventListener(\"change\", change)\n trigger?.root.addEventListener(\"click\", click, true)\n if (trigger?.event) trigger.root.addEventListener(trigger.event, request)\n change()\n return () => {\n disposed = true\n epoch++\n pendingClick = undefined\n pendingEvent = undefined\n media.removeEventListener(\"change\", change)\n trigger?.root.removeEventListener(\"click\", click, true)\n if (trigger?.event) trigger.root.removeEventListener(trigger.event, request)\n cleanup?.()\n cleanup = undefined\n }\n}\n", "type": "registry:lib" } ], diff --git a/public/r/styles/base-sera/on-media.json b/public/r/styles/base-sera/on-media.json index 28d5960f4..7d8c6038c 100644 --- a/public/r/styles/base-sera/on-media.json +++ b/public/r/styles/base-sera/on-media.json @@ -6,7 +6,7 @@ "files": [ { "path": "src/lib/on-media.ts", - "content": "type Trigger = { root: Element; selector: string; event?: string }\n\n/** Import and mount only while the query matches; dispose on exit. */\nexport function onMedia(\n query: string,\n load: () => Promise,\n mount: (module: T) => () => void,\n trigger?: Trigger\n) {\n const media = window.matchMedia(query)\n const matches = () => window.matchMedia(query).matches\n let disposed = false\n let epoch = 0\n let cleanup: (() => void) | undefined\n let pendingClick: HTMLElement | undefined\n let pendingEvent: CustomEvent | undefined\n\n const replayClick = () => {\n const target = pendingClick\n const event = pendingEvent\n pendingClick = undefined\n pendingEvent = undefined\n if (event && trigger?.root.isConnected)\n trigger.root.dispatchEvent(\n new CustomEvent(event.type, { detail: event.detail })\n )\n else if (target?.isConnected)\n target.dispatchEvent(new MouseEvent(\"click\", { cancelable: true }))\n }\n\n const update = async () => {\n const current = ++epoch\n cleanup?.()\n cleanup = undefined\n if (!matches() || disposed) {\n pendingClick = undefined\n pendingEvent = undefined\n return\n }\n try {\n const module = await load()\n if (disposed || !matches() || current !== epoch) return\n cleanup = mount(module)\n replayClick()\n } catch (error) {\n if (!disposed && current === epoch) {\n pendingClick = undefined\n pendingEvent = undefined\n console.error(error)\n }\n }\n }\n const click = (event: Event) => {\n if (!trigger || cleanup || disposed || !matches() || event.defaultPrevented)\n return\n if (!(event.target instanceof Element)) return\n const target = event.target.closest(trigger.selector)\n if (!target || !trigger.root.contains(target)) return\n pendingClick ??= target\n pendingEvent = undefined\n event.preventDefault()\n }\n const request = (event: Event) => {\n if (cleanup || disposed || !matches() || event.target !== trigger?.root)\n return\n if (!(event instanceof CustomEvent)) return\n pendingEvent = event\n pendingClick = undefined\n }\n const change = () => void update()\n media.addEventListener(\"change\", change)\n trigger?.root.addEventListener(\"click\", click, true)\n if (trigger?.event) trigger.root.addEventListener(trigger.event, request)\n change()\n return () => {\n disposed = true\n epoch++\n pendingClick = undefined\n pendingEvent = undefined\n media.removeEventListener(\"change\", change)\n trigger?.root.removeEventListener(\"click\", click, true)\n if (trigger?.event) trigger.root.removeEventListener(trigger.event, request)\n cleanup?.()\n cleanup = undefined\n }\n}\n", + "content": "type Trigger = { root: Element; selector: string; event?: string }\n\n/** Import and mount only while the query matches; dispose on exit. */\nexport function onMedia(\n query: string,\n load: () => Promise,\n mount: (module: T) => () => void,\n trigger?: Trigger\n) {\n const media = window.matchMedia(query)\n let disposed = false\n let epoch = 0\n let cleanup: (() => void) | undefined\n let pendingClick: HTMLElement | undefined\n let pendingEvent: CustomEvent | undefined\n\n const matches = () => {\n const matching = window.matchMedia(query).matches\n if (!matching) {\n pendingClick = undefined\n pendingEvent = undefined\n }\n return matching\n }\n\n const replayClick = () => {\n const target = pendingClick\n const event = pendingEvent\n pendingClick = undefined\n pendingEvent = undefined\n if (event && trigger?.root.isConnected)\n trigger.root.dispatchEvent(\n new CustomEvent(event.type, { detail: event.detail })\n )\n else if (target?.isConnected)\n target.dispatchEvent(new MouseEvent(\"click\", { cancelable: true }))\n }\n\n const update = async () => {\n const current = ++epoch\n cleanup?.()\n cleanup = undefined\n if (!matches() || disposed) {\n pendingClick = undefined\n pendingEvent = undefined\n return\n }\n try {\n const module = await load()\n if (disposed || current !== epoch || !matches()) return\n cleanup = mount(module)\n replayClick()\n } catch (error) {\n if (!disposed && current === epoch) {\n pendingClick = undefined\n pendingEvent = undefined\n console.error(error)\n }\n }\n }\n const click = (event: Event) => {\n if (!trigger || cleanup || disposed || !matches() || event.defaultPrevented)\n return\n if (!(event.target instanceof Element)) return\n const target = event.target.closest(trigger.selector)\n if (!target || !trigger.root.contains(target)) return\n pendingClick ??= target\n pendingEvent = undefined\n event.preventDefault()\n }\n const request = (event: Event) => {\n if (cleanup || disposed || !matches() || event.target !== trigger?.root)\n return\n if (!(event instanceof CustomEvent)) return\n pendingEvent = event\n pendingClick = undefined\n }\n const change = () => void update()\n media.addEventListener(\"change\", change)\n trigger?.root.addEventListener(\"click\", click, true)\n if (trigger?.event) trigger.root.addEventListener(trigger.event, request)\n change()\n return () => {\n disposed = true\n epoch++\n pendingClick = undefined\n pendingEvent = undefined\n media.removeEventListener(\"change\", change)\n trigger?.root.removeEventListener(\"click\", click, true)\n if (trigger?.event) trigger.root.removeEventListener(trigger.event, request)\n cleanup?.()\n cleanup = undefined\n }\n}\n", "type": "registry:lib" } ], diff --git a/public/r/styles/base-vega/on-media.json b/public/r/styles/base-vega/on-media.json index 28d5960f4..7d8c6038c 100644 --- a/public/r/styles/base-vega/on-media.json +++ b/public/r/styles/base-vega/on-media.json @@ -6,7 +6,7 @@ "files": [ { "path": "src/lib/on-media.ts", - "content": "type Trigger = { root: Element; selector: string; event?: string }\n\n/** Import and mount only while the query matches; dispose on exit. */\nexport function onMedia(\n query: string,\n load: () => Promise,\n mount: (module: T) => () => void,\n trigger?: Trigger\n) {\n const media = window.matchMedia(query)\n const matches = () => window.matchMedia(query).matches\n let disposed = false\n let epoch = 0\n let cleanup: (() => void) | undefined\n let pendingClick: HTMLElement | undefined\n let pendingEvent: CustomEvent | undefined\n\n const replayClick = () => {\n const target = pendingClick\n const event = pendingEvent\n pendingClick = undefined\n pendingEvent = undefined\n if (event && trigger?.root.isConnected)\n trigger.root.dispatchEvent(\n new CustomEvent(event.type, { detail: event.detail })\n )\n else if (target?.isConnected)\n target.dispatchEvent(new MouseEvent(\"click\", { cancelable: true }))\n }\n\n const update = async () => {\n const current = ++epoch\n cleanup?.()\n cleanup = undefined\n if (!matches() || disposed) {\n pendingClick = undefined\n pendingEvent = undefined\n return\n }\n try {\n const module = await load()\n if (disposed || !matches() || current !== epoch) return\n cleanup = mount(module)\n replayClick()\n } catch (error) {\n if (!disposed && current === epoch) {\n pendingClick = undefined\n pendingEvent = undefined\n console.error(error)\n }\n }\n }\n const click = (event: Event) => {\n if (!trigger || cleanup || disposed || !matches() || event.defaultPrevented)\n return\n if (!(event.target instanceof Element)) return\n const target = event.target.closest(trigger.selector)\n if (!target || !trigger.root.contains(target)) return\n pendingClick ??= target\n pendingEvent = undefined\n event.preventDefault()\n }\n const request = (event: Event) => {\n if (cleanup || disposed || !matches() || event.target !== trigger?.root)\n return\n if (!(event instanceof CustomEvent)) return\n pendingEvent = event\n pendingClick = undefined\n }\n const change = () => void update()\n media.addEventListener(\"change\", change)\n trigger?.root.addEventListener(\"click\", click, true)\n if (trigger?.event) trigger.root.addEventListener(trigger.event, request)\n change()\n return () => {\n disposed = true\n epoch++\n pendingClick = undefined\n pendingEvent = undefined\n media.removeEventListener(\"change\", change)\n trigger?.root.removeEventListener(\"click\", click, true)\n if (trigger?.event) trigger.root.removeEventListener(trigger.event, request)\n cleanup?.()\n cleanup = undefined\n }\n}\n", + "content": "type Trigger = { root: Element; selector: string; event?: string }\n\n/** Import and mount only while the query matches; dispose on exit. */\nexport function onMedia(\n query: string,\n load: () => Promise,\n mount: (module: T) => () => void,\n trigger?: Trigger\n) {\n const media = window.matchMedia(query)\n let disposed = false\n let epoch = 0\n let cleanup: (() => void) | undefined\n let pendingClick: HTMLElement | undefined\n let pendingEvent: CustomEvent | undefined\n\n const matches = () => {\n const matching = window.matchMedia(query).matches\n if (!matching) {\n pendingClick = undefined\n pendingEvent = undefined\n }\n return matching\n }\n\n const replayClick = () => {\n const target = pendingClick\n const event = pendingEvent\n pendingClick = undefined\n pendingEvent = undefined\n if (event && trigger?.root.isConnected)\n trigger.root.dispatchEvent(\n new CustomEvent(event.type, { detail: event.detail })\n )\n else if (target?.isConnected)\n target.dispatchEvent(new MouseEvent(\"click\", { cancelable: true }))\n }\n\n const update = async () => {\n const current = ++epoch\n cleanup?.()\n cleanup = undefined\n if (!matches() || disposed) {\n pendingClick = undefined\n pendingEvent = undefined\n return\n }\n try {\n const module = await load()\n if (disposed || current !== epoch || !matches()) return\n cleanup = mount(module)\n replayClick()\n } catch (error) {\n if (!disposed && current === epoch) {\n pendingClick = undefined\n pendingEvent = undefined\n console.error(error)\n }\n }\n }\n const click = (event: Event) => {\n if (!trigger || cleanup || disposed || !matches() || event.defaultPrevented)\n return\n if (!(event.target instanceof Element)) return\n const target = event.target.closest(trigger.selector)\n if (!target || !trigger.root.contains(target)) return\n pendingClick ??= target\n pendingEvent = undefined\n event.preventDefault()\n }\n const request = (event: Event) => {\n if (cleanup || disposed || !matches() || event.target !== trigger?.root)\n return\n if (!(event instanceof CustomEvent)) return\n pendingEvent = event\n pendingClick = undefined\n }\n const change = () => void update()\n media.addEventListener(\"change\", change)\n trigger?.root.addEventListener(\"click\", click, true)\n if (trigger?.event) trigger.root.addEventListener(trigger.event, request)\n change()\n return () => {\n disposed = true\n epoch++\n pendingClick = undefined\n pendingEvent = undefined\n media.removeEventListener(\"change\", change)\n trigger?.root.removeEventListener(\"click\", click, true)\n if (trigger?.event) trigger.root.removeEventListener(trigger.event, request)\n cleanup?.()\n cleanup = undefined\n }\n}\n", "type": "registry:lib" } ],