-
-
Notifications
You must be signed in to change notification settings - Fork 29
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
breaking: deprecate
Readable
and useActiveElement
-> `ActiveEleme…
…nt` (#193) Co-authored-by: Hunter Johnston <[email protected]>
- Loading branch information
Showing
18 changed files
with
454 additions
and
321 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
--- | ||
"runed": minor | ||
--- | ||
|
||
breaking: deprecate `Readable` in favor of `createSubscriber` from Svelte |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
--- | ||
"runed": minor | ||
--- | ||
|
||
breaking: replace `useActiveElement` with `ActiveElement` for custom `DocumentOrShadowRoot` options |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,125 @@ | ||
/* eslint-disable @typescript-eslint/no-explicit-any */ | ||
import { describe, it, expect, beforeEach } from "vitest"; | ||
import { getActiveElement } from "./dom.js"; | ||
|
||
describe("getActiveElement", () => { | ||
class MockShadowRoot implements Partial<ShadowRoot> { | ||
private _activeElement: Element | null = null; | ||
|
||
get activeElement() { | ||
return this._activeElement; | ||
} | ||
|
||
setActiveElement(element: Element | null | MockElement) { | ||
this._activeElement = element as unknown as Element; | ||
} | ||
} | ||
|
||
class MockElement implements Partial<HTMLElement> { | ||
private _shadowRoot: MockShadowRoot | null = null; | ||
|
||
constructor(public tagName: string = "DIV") {} | ||
|
||
get shadowRoot() { | ||
return this._shadowRoot as unknown as ShadowRoot; | ||
} | ||
|
||
attachShadow(_: ShadowRootInit) { | ||
this._shadowRoot = new MockShadowRoot(); | ||
return this._shadowRoot as unknown as ShadowRoot; | ||
} | ||
} | ||
|
||
class MockDocument implements Partial<DocumentOrShadowRoot> { | ||
private _activeElement: Element | null = null; | ||
|
||
get activeElement() { | ||
return this._activeElement; | ||
} | ||
|
||
setActiveElement(element: Element | null | MockElement) { | ||
this._activeElement = element as unknown as Element; | ||
} | ||
} | ||
|
||
let mockDocument: MockDocument; | ||
|
||
beforeEach(() => { | ||
mockDocument = new MockDocument(); | ||
}); | ||
|
||
it("returns null when document has no active element", () => { | ||
expect(getActiveElement(mockDocument as any)).toBeNull(); | ||
}); | ||
|
||
it("returns the active element when it has no shadow root", () => { | ||
const mockElement = new MockElement(); | ||
mockDocument.setActiveElement(mockElement); | ||
|
||
expect(getActiveElement(mockDocument as any)).toBe(mockElement); | ||
}); | ||
|
||
it("returns the active element within a shadow root", () => { | ||
const shadowElement = new MockElement("BUTTON"); | ||
const hostElement = new MockElement(); | ||
|
||
hostElement.attachShadow({ mode: "open" }); | ||
(hostElement.shadowRoot as unknown as MockShadowRoot).setActiveElement(shadowElement); | ||
mockDocument.setActiveElement(hostElement); | ||
|
||
expect(getActiveElement(mockDocument as any)).toBe(shadowElement); | ||
}); | ||
|
||
it("returns the deepest active element in nested shadow roots", () => { | ||
const deepestElement = new MockElement("INPUT"); | ||
const middleElement = new MockElement(); | ||
const topElement = new MockElement(); | ||
|
||
middleElement.attachShadow({ mode: "open" }); | ||
topElement.attachShadow({ mode: "open" }); | ||
|
||
(middleElement.shadowRoot as unknown as MockShadowRoot).setActiveElement(deepestElement); | ||
(topElement.shadowRoot as unknown as MockShadowRoot).setActiveElement(middleElement); | ||
mockDocument.setActiveElement(topElement); | ||
|
||
expect(getActiveElement(mockDocument as any)).toBe(deepestElement); | ||
}); | ||
|
||
it("handles when inner shadow root has no active element", () => { | ||
const hostElement = new MockElement(); | ||
hostElement.attachShadow({ mode: "open" }); | ||
mockDocument.setActiveElement(hostElement); | ||
|
||
expect(getActiveElement(mockDocument as any)).toBeNull(); | ||
}); | ||
|
||
it("works when starting from a shadow root instead of document", () => { | ||
const innerElement = new MockElement("SPAN"); | ||
const mockShadowRoot = new MockShadowRoot(); | ||
mockShadowRoot.setActiveElement(innerElement); | ||
|
||
expect(getActiveElement(mockShadowRoot as any)).toBe(innerElement); | ||
}); | ||
|
||
it("handles nested shadow roots when starting from middle shadow root", () => { | ||
const deepestElement = new MockElement("BUTTON"); | ||
const middleElement = new MockElement(); | ||
|
||
middleElement.attachShadow({ mode: "open" }); | ||
(middleElement.shadowRoot as unknown as MockShadowRoot).setActiveElement(deepestElement); | ||
|
||
const mockShadowRoot = new MockShadowRoot(); | ||
mockShadowRoot.setActiveElement(middleElement); | ||
|
||
expect(getActiveElement(mockShadowRoot as any)).toBe(deepestElement); | ||
}); | ||
|
||
it("breaks infinite loops if activeElement references itself", () => { | ||
const element = new MockElement(); | ||
element.attachShadow({ mode: "open" }); | ||
(element.shadowRoot as unknown as MockShadowRoot).setActiveElement(element); | ||
mockDocument.setActiveElement(element); | ||
|
||
expect(getActiveElement(mockDocument as any)).toBe(element); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,31 +1,26 @@ | ||
import { flushSync } from "svelte"; | ||
import { test, vi } from "vitest"; | ||
|
||
export function testWithEffect(name: string, fn: () => void | Promise<void>) { | ||
test(name, async () => { | ||
let promise: void | Promise<void>; | ||
const cleanup = $effect.root(() => { | ||
promise = fn(); | ||
}); | ||
export function testWithEffect(name: string, fn: () => void | Promise<void>): void { | ||
test(name, () => effectRootScope(fn)); | ||
} | ||
|
||
try { | ||
await promise!; | ||
} finally { | ||
cleanup(); | ||
} | ||
export function effectRootScope(fn: () => void | Promise<void>): void | Promise<void> { | ||
let promise!: void | Promise<void>; | ||
const cleanup = $effect.root(() => { | ||
promise = fn(); | ||
}); | ||
|
||
if (promise instanceof Promise) { | ||
return promise.finally(cleanup); | ||
} else { | ||
cleanup(); | ||
} | ||
} | ||
|
||
export function vitestSetTimeoutWrapper(fn: () => void, timeout: number) { | ||
setTimeout(async () => { | ||
export function vitestSetTimeoutWrapper(fn: () => void, timeout: number): void { | ||
setTimeout(() => { | ||
fn(); | ||
}, timeout + 1); | ||
|
||
vi.advanceTimersByTime(timeout); | ||
} | ||
|
||
export function focus(node: HTMLElement | null | undefined) { | ||
if (node) { | ||
flushSync(() => node.focus()); | ||
} | ||
} |
22 changes: 9 additions & 13 deletions
22
packages/runed/src/lib/utilities/IsFocusWithin/IsFocusWithin.svelte.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,29 +1,25 @@ | ||
import { extract } from "../extract/extract.svelte.js"; | ||
import type { MaybeElementGetter } from "$lib/internal/types.js"; | ||
import { | ||
type ConfigurableDocumentOrShadowRoot, | ||
type ConfigurableWindow, | ||
} from "$lib/internal/configurable-globals.js"; | ||
import { useActiveElement } from "../useActiveElement/useActiveElement.svelte.js"; | ||
import { ActiveElement, type ActiveElementOptions } from "../activeElement/activeElement.svelte.js"; | ||
import { extract } from "../extract/extract.svelte.js"; | ||
|
||
type IsFocusWithinOptions = ConfigurableDocumentOrShadowRoot & ConfigurableWindow; | ||
export interface IsFocusWithinOptions extends ActiveElementOptions {} | ||
|
||
/** | ||
* Tracks whether the focus is within a target element. | ||
* @see {@link https://runed.dev/docs/utilities/is-focus-within} | ||
*/ | ||
export class IsFocusWithin { | ||
#node: MaybeElementGetter; | ||
#target = $derived.by(() => extract(this.#node)); | ||
#activeElement: ReturnType<typeof useActiveElement>; | ||
readonly #node: MaybeElementGetter; | ||
readonly #activeElement: ActiveElement; | ||
|
||
constructor(node: MaybeElementGetter, options: IsFocusWithinOptions = {}) { | ||
this.#node = node; | ||
this.#activeElement = useActiveElement({ document: options.document, window: options.window }); | ||
this.#activeElement = new ActiveElement(options); | ||
} | ||
|
||
readonly current = $derived.by(() => { | ||
if (!this.#target || !this.#activeElement.current) return false; | ||
return this.#target.contains(this.#activeElement.current); | ||
const node = extract(this.#node); | ||
if (node == null) return false; | ||
return node.contains(this.#activeElement.current); | ||
}); | ||
} |
Oops, something went wrong.