diff --git a/public/r/styles/base-luma/on-media.json b/public/r/styles/base-luma/on-media.json index 69d81ab31..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 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 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 69d81ab31..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 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 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 69d81ab31..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 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 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 69d81ab31..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 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 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 69d81ab31..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 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 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 69d81ab31..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 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 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 69d81ab31..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 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 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 69d81ab31..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 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 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/src/lib/on-media.ts b/src/lib/on-media.ts index 8baf71cf2..2e8054dfa 100644 --- a/src/lib/on-media.ts +++ b/src/lib/on-media.ts @@ -14,6 +14,15 @@ export function onMedia( 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 @@ -29,14 +38,16 @@ export function onMedia( const update = async () => { const current = ++epoch - pendingClick = undefined - pendingEvent = undefined cleanup?.() cleanup = undefined - if (!media.matches || disposed) return + if (!matches() || disposed) { + pendingClick = undefined + pendingEvent = undefined + return + } try { const module = await load() - if (disposed || !media.matches || current !== epoch) return + if (disposed || current !== epoch || !matches()) return cleanup = mount(module) replayClick() } catch (error) { @@ -48,13 +59,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) @@ -64,7 +69,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 358a37671..a1d293378 100644 --- a/tests/on-media.test.mjs +++ b/tests/on-media.test.mjs @@ -80,3 +80,149 @@ test("disposing during import prevents a later mount", async () => { assert.equal(mounts, 0) globalThis.window = previous }) + +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, + } + class Element extends EventTarget { + isConnected = true + closest() { + return this + } + contains(target) { + return target === this + } + } + const media = new EventTarget() + media.matches = false + 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) + } + }) + +for (const action of ["click", "request"]) + 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 + } + } + 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 + 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) + } + })