From 6bf4c6d99658671d8f2244dd12feea33b7b851dc Mon Sep 17 00:00:00 2001 From: Hunter Johnston Date: Wed, 18 Dec 2024 21:24:36 -0500 Subject: [PATCH 1/3] accordion done --- packages/tests/package.json | 4 +- .../accordion-multi-test-controlled.svelte | 2 +- .../accordion-single-test-controlled.svelte | 2 +- .../src/tests/accordion/accordion.test.ts | 43 ++++++++++--------- .../tests/src/tests/dialog/dialog.test.ts | 6 +-- 5 files changed, 30 insertions(+), 27 deletions(-) diff --git a/packages/tests/package.json b/packages/tests/package.json index 5fe483fa1..e032aba94 100644 --- a/packages/tests/package.json +++ b/packages/tests/package.json @@ -7,7 +7,8 @@ "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", "sync": "svelte-kit sync", - "test": "pnpm sync && vitest" + "test": "pnpm sync && vitest", + "test:ui": "pnpm sync && vitest --ui" }, "devDependencies": { "@sveltejs/adapter-auto": "^3.3.1", @@ -21,6 +22,7 @@ "@types/node": "^20.17.6", "@types/resize-observer-browser": "^0.1.11", "@types/testing-library__jest-dom": "^5.14.9", + "@vitest/ui": "^2.1.8", "jest-axe": "^9.0.0", "jsdom": "^24.1.3", "resize-observer-polyfill": "^1.5.1", diff --git a/packages/tests/src/tests/accordion/accordion-multi-test-controlled.svelte b/packages/tests/src/tests/accordion/accordion-multi-test-controlled.svelte index 85844cbc7..2684569c1 100644 --- a/packages/tests/src/tests/accordion/accordion-multi-test-controlled.svelte +++ b/packages/tests/src/tests/accordion/accordion-multi-test-controlled.svelte @@ -25,7 +25,7 @@ {value} - + - + {#each items as { value, title, disabled, content, level }} diff --git a/packages/tests/src/tests/accordion/accordion.test.ts b/packages/tests/src/tests/accordion/accordion.test.ts index afbc188d8..2f35088fa 100644 --- a/packages/tests/src/tests/accordion/accordion.test.ts +++ b/packages/tests/src/tests/accordion/accordion.test.ts @@ -22,6 +22,13 @@ export type Item = { const kbd = getTestKbd(); const items: Item[] = [ + { + value: "item-0", + title: "Item 0", + content: "Content 0", + disabled: false, + level: 3, + }, { value: "item-1", title: "Item 1", @@ -43,17 +50,10 @@ const items: Item[] = [ disabled: false, level: 3, }, - { - value: "item-4", - title: "Item 4", - content: "Content 4", - disabled: false, - level: 3, - }, ]; const itemsWithDisabled = items.map((item) => { - if (item.value === "item-2") { + if (item.value === "item-1") { return { ...item, disabled: true }; } return item; @@ -301,6 +301,7 @@ describe("accordion - single", () => { const triggers = items.map((item) => getByTestId(`${item.value}-trigger`)); await user.click(triggers[0] as HTMLElement); + expect(triggers[0]).toHaveFocus(); await user.keyboard(kbd.ARROW_DOWN); expect(triggers[1]).not.toHaveFocus(); @@ -326,17 +327,17 @@ describe("accordion - single", () => { it("should update the `bind:value` prop when the value changes", async () => { const user = setupUserEvents(); const { getByTestId } = render(AccordionSingleTestControlledSvelte as any, { items }); - const trigger = getByTestId("item-1-trigger"); + const trigger = getByTestId("item-0-trigger"); const value = getByTestId("value"); expect(value).toHaveTextContent(""); await user.click(trigger); - expect(value).toHaveTextContent("item-1"); + expect(value).toHaveTextContent("item-0"); }); - it('should handle programatic changes to the "value" prop', async () => { + it('should handle programmatic changes to the "value" prop', async () => { const user = setupUserEvents(); const { getByTestId } = render(AccordionSingleTestControlledSvelte as any, { items }); const updateButton = getByTestId("update-value"); @@ -344,12 +345,12 @@ describe("accordion - single", () => { expect(value).toHaveTextContent(""); - const itemTwoItem = getByTestId("item-2-item"); - expect(itemTwoItem).toHaveAttribute("data-state", "closed"); + const itemOneItem = getByTestId("item-1-item"); + expect(itemOneItem).toHaveAttribute("data-state", "closed"); await user.click(updateButton); - expect(value).toHaveTextContent("item-2"); - expect(itemTwoItem).toHaveAttribute("data-state", "open"); + expect(value).toHaveTextContent("item-1"); + expect(itemOneItem).toHaveAttribute("data-state", "open"); }); }); @@ -579,17 +580,17 @@ describe("accordion - multiple", () => { const { getByTestId, queryByTestId } = render(AccordionMultiTestControlled as any, { items, }); - const trigger = getByTestId("item-1-trigger"); + const trigger = getByTestId("item-0-trigger"); const value = getByTestId("value"); expect(value).toHaveTextContent(""); await user.click(trigger); - expect(queryByTestId("value")).toHaveTextContent("item-1"); + expect(queryByTestId("value")).toHaveTextContent("item-0"); }); - it('should handle programatic changes to the "value" prop', async () => { + it('should handle programmatic changes to the "value" prop', async () => { const user = setupUserEvents(); const { getByTestId, queryByTestId } = render(AccordionMultiTestControlled as any, { items, @@ -599,9 +600,9 @@ describe("accordion - multiple", () => { expect(value).toHaveTextContent(""); - const itemTwoItem = getByTestId("item-2-item"); - expect(itemTwoItem).toHaveAttribute("data-state", "closed"); + const itemOneItem = getByTestId("item-1-item"); + expect(itemOneItem).toHaveAttribute("data-state", "closed"); await user.click(updateButton); - expect(itemTwoItem).toHaveAttribute("data-state", "open"); + expect(itemOneItem).toHaveAttribute("data-state", "open"); }); }); diff --git a/packages/tests/src/tests/dialog/dialog.test.ts b/packages/tests/src/tests/dialog/dialog.test.ts index 35cb2ee38..ddea75ca7 100644 --- a/packages/tests/src/tests/dialog/dialog.test.ts +++ b/packages/tests/src/tests/dialog/dialog.test.ts @@ -9,7 +9,7 @@ import { userEvent } from "@testing-library/user-event"; import { axe } from "jest-axe"; import { describe, it } from "vitest"; import { tick } from "svelte"; -import { getTestKbd, sleep } from "../utils.js"; +import { getTestKbd, setupUserEvents, sleep } from "../utils.js"; import DialogTest, { type DialogTestProps } from "./dialog-test.svelte"; const kbd = getTestKbd(); @@ -29,7 +29,7 @@ async function expectIsOpen( } function setup(props: DialogTestProps = {}) { - const user = userEvent.setup({ pointerEventsCheck: 0 }); + const user = setupUserEvents(); const returned = render(DialogTest, { ...props }); const trigger = returned.getByTestId("trigger"); @@ -44,7 +44,7 @@ async function open(props: DialogTestProps = {}) { const { getByTestId, queryByTestId, user, trigger } = setup(props); const content = queryByTestId("content"); expect(content).toBeNull(); - await user.click(trigger); + await user.pointerDownUp(trigger); const contentAfter = getByTestId("content"); expect(contentAfter).not.toBeNull(); return { getByTestId, queryByTestId, user }; From a036998fc5cd1e75b4af867c4201efef8d85c335 Mon Sep 17 00:00:00 2001 From: Hunter Johnston Date: Wed, 18 Dec 2024 21:29:21 -0500 Subject: [PATCH 2/3] dialog done --- .../src/tests/alert-dialog/alert-dialog.test.ts | 16 ++++++++-------- packages/tests/src/tests/dialog/dialog.test.ts | 5 +++-- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/packages/tests/src/tests/alert-dialog/alert-dialog.test.ts b/packages/tests/src/tests/alert-dialog/alert-dialog.test.ts index 49d1036c1..56800158c 100644 --- a/packages/tests/src/tests/alert-dialog/alert-dialog.test.ts +++ b/packages/tests/src/tests/alert-dialog/alert-dialog.test.ts @@ -9,7 +9,7 @@ import { userEvent } from "@testing-library/user-event"; import { axe } from "jest-axe"; import { describe, it } from "vitest"; import type { Component } from "svelte"; -import { getTestKbd, sleep } from "../utils.js"; +import { getTestKbd, setupUserEvents, sleep } from "../utils.js"; import AlertDialogTest, { type AlertDialogTestProps } from "./alert-dialog-test.svelte"; import AlertDialogForceMountTest from "./alert-dialog-force-mount-test.svelte"; @@ -30,7 +30,7 @@ async function expectIsOpen( } function setup(props: AlertDialogTestProps = {}, component: Component = AlertDialogTest) { - const user = userEvent.setup({ pointerEventsCheck: 0 }); + const user = setupUserEvents(); const returned = render(component, { ...props }); const trigger = returned.getByTestId("trigger"); @@ -45,7 +45,7 @@ async function open(props: AlertDialogTestProps = {}) { const { getByTestId, queryByTestId, user, trigger } = setup(props); const content = queryByTestId("content"); expect(content).toBeNull(); - await user.click(trigger); + await user.pointerDownUp(trigger); const contentAfter = getByTestId("content"); expect(contentAfter).not.toBeNull(); const cancel = getByTestId("cancel"); @@ -104,7 +104,7 @@ describe("alert dialog", () => { expect(initContent).toBeNull(); const trigger = getByTestId("trigger"); - await user.click(trigger); + await user.pointerDownUp(trigger); const overlay = getByTestId("overlay"); expect(overlay).toBeInTheDocument(); @@ -121,7 +121,7 @@ describe("alert dialog", () => { it("should close when the cancel button is clicked", async () => { const { getByTestId, queryByTestId, user } = await open(); const cancel = getByTestId("cancel"); - await user.click(cancel); + await user.pointerDownUp(cancel); expectIsClosed(queryByTestId); }); @@ -138,7 +138,7 @@ describe("alert dialog", () => { await sleep(100); const overlay = getByTestId("overlay"); - await user.click(overlay); + await user.pointerDownUp(overlay); await sleep(25); const contentAfter2 = queryByTestId("content"); @@ -176,7 +176,7 @@ describe("alert dialog", () => { it("should not close when content is clicked", async () => { const { user, getByTestId, queryByTestId } = await open(); const content = getByTestId("content"); - await user.click(content); + await user.pointerDownUp(content); await expectIsOpen(queryByTestId); }); @@ -186,7 +186,7 @@ describe("alert dialog", () => { const trigger = getByTestId("trigger"); const binding = getByTestId("binding"); expect(binding).toHaveTextContent("false"); - await user.click(trigger); + await user.pointerDownUp(trigger); expect(binding).toHaveTextContent("true"); await user.keyboard(kbd.ESCAPE); expect(binding).toHaveTextContent("false"); diff --git a/packages/tests/src/tests/dialog/dialog.test.ts b/packages/tests/src/tests/dialog/dialog.test.ts index ddea75ca7..89dbe77eb 100644 --- a/packages/tests/src/tests/dialog/dialog.test.ts +++ b/packages/tests/src/tests/dialog/dialog.test.ts @@ -45,6 +45,7 @@ async function open(props: DialogTestProps = {}) { const content = queryByTestId("content"); expect(content).toBeNull(); await user.pointerDownUp(trigger); + await tick(); const contentAfter = getByTestId("content"); expect(contentAfter).not.toBeNull(); return { getByTestId, queryByTestId, user }; @@ -153,14 +154,14 @@ describe("dialog", () => { const trigger = getByTestId("trigger"); const binding = getByTestId("binding"); expect(binding).toHaveTextContent("false"); - await user.click(trigger); + await user.pointerDownUp(trigger); expect(binding).toHaveTextContent("true"); await user.keyboard(kbd.ESCAPE); expect(binding).toHaveTextContent("false"); const toggle = getByTestId("toggle"); expectIsClosed(queryByTestId); - await user.click(toggle); + await user.pointerDownUp(toggle); await expectIsOpen(queryByTestId); }); From 95a528e9e3da5ce2cefc7d2c6daeeae754823308 Mon Sep 17 00:00:00 2001 From: Hunter Johnston Date: Wed, 18 Dec 2024 21:56:24 -0500 Subject: [PATCH 3/3] chore: cleanup events --- .changeset/yellow-lies-vanish.md | 5 + .../lib/bits/accordion/accordion.svelte.ts | 26 +-- .../bits/collapsible/collapsible.svelte.ts | 22 +-- .../src/lib/bits/dialog/dialog.svelte.ts | 6 +- .../lib/bits/pagination/pagination.svelte.ts | 38 +---- .../components/popover-content-static.svelte | 4 +- .../popover/components/popover-content.svelte | 6 +- .../src/lib/bits/popover/popover.svelte.ts | 35 ++-- .../src/tests/accordion/accordion.test.ts | 144 +++++++++------- .../tests/src/tests/popover/popover.test.ts | 10 +- pnpm-lock.yaml | 158 ++++++++++-------- 11 files changed, 230 insertions(+), 224 deletions(-) create mode 100644 .changeset/yellow-lies-vanish.md diff --git a/.changeset/yellow-lies-vanish.md b/.changeset/yellow-lies-vanish.md new file mode 100644 index 000000000..a16cf5120 --- /dev/null +++ b/.changeset/yellow-lies-vanish.md @@ -0,0 +1,5 @@ +--- +"bits-ui": patch +--- + +revert to `onclick` events for most components except where it makes sense (like menus, select, etc.) diff --git a/packages/bits-ui/src/lib/bits/accordion/accordion.svelte.ts b/packages/bits-ui/src/lib/bits/accordion/accordion.svelte.ts index ed05e4978..ee26bf4be 100644 --- a/packages/bits-ui/src/lib/bits/accordion/accordion.svelte.ts +++ b/packages/bits-ui/src/lib/bits/accordion/accordion.svelte.ts @@ -1,6 +1,7 @@ import { afterTick, useRefById } from "svelte-toolbelt"; +import { watch } from "runed"; import type { Box, ReadableBoxedValues, WritableBoxedValues } from "$lib/internal/box.svelte.js"; -import type { BitsKeyboardEvent, BitsPointerEvent, WithRefProps } from "$lib/internal/types.js"; +import type { BitsKeyboardEvent, BitsMouseEvent, WithRefProps } from "$lib/internal/types.js"; import { getAriaDisabled, getAriaExpanded, @@ -213,9 +214,8 @@ class AccordionTriggerState { this.#root = itemState.root; this.#id = props.id; this.#ref = props.ref; - this.onpointerdown = this.onpointerdown.bind(this); - this.onpointerup = this.onpointerup.bind(this); this.onkeydown = this.onkeydown.bind(this); + this.onclick = this.onclick.bind(this); useRefById({ id: props.id, @@ -223,20 +223,12 @@ class AccordionTriggerState { }); } - onpointerdown(e: BitsPointerEvent) { + onclick(e: BitsMouseEvent) { if (this.#isDisabled) return; - if (e.pointerType === "touch" || e.button !== 0) return e.preventDefault(); + if (e.button !== 0) return e.preventDefault(); this.#itemState.updateValue(); } - onpointerup(e: BitsPointerEvent) { - if (this.#isDisabled) return; - if (e.pointerType === "touch") { - e.preventDefault(); - this.#itemState.updateValue(); - } - } - onkeydown(e: BitsKeyboardEvent) { if (this.#isDisabled) return; if (e.key === kbd.SPACE || e.key === kbd.ENTER) { @@ -261,8 +253,7 @@ class AccordionTriggerState { [ACCORDION_TRIGGER_ATTR]: "", tabindex: 0, // - onpointerdown: this.onpointerdown, - onpointerup: this.onpointerup, + onclick: this.onclick, onkeydown: this.onkeydown, }) as const ); @@ -311,11 +302,8 @@ class AccordionContentState { }; }); - $effect(() => { - this.present; - const node = this.#ref.current; + watch([() => this.present, () => this.#ref.current], ([_, node]) => { if (!node) return; - afterTick(() => { if (!this.#ref.current) return; // get the dimensions of the element diff --git a/packages/bits-ui/src/lib/bits/collapsible/collapsible.svelte.ts b/packages/bits-ui/src/lib/bits/collapsible/collapsible.svelte.ts index 3d3ff25ae..bce1b4836 100644 --- a/packages/bits-ui/src/lib/bits/collapsible/collapsible.svelte.ts +++ b/packages/bits-ui/src/lib/bits/collapsible/collapsible.svelte.ts @@ -3,7 +3,7 @@ import type { ReadableBoxedValues, WritableBoxedValues } from "$lib/internal/box import { getAriaExpanded, getDataDisabled, getDataOpenClosed } from "$lib/internal/attrs.js"; import { createContext } from "$lib/internal/create-context.js"; import { kbd } from "$lib/internal/kbd.js"; -import type { BitsKeyboardEvent, BitsPointerEvent } from "$lib/internal/types.js"; +import type { BitsKeyboardEvent, BitsMouseEvent, BitsPointerEvent } from "$lib/internal/types.js"; const COLLAPSIBLE_ROOT_ATTR = "data-collapsible-root"; const COLLAPSIBLE_CONTENT_ATTR = "data-collapsible-content"; @@ -176,8 +176,7 @@ class CollapsibleTriggerState { this.#ref = props.ref; this.#disabled = props.disabled; - this.onpointerdown = this.onpointerdown.bind(this); - this.onpointerup = this.onpointerup.bind(this); + this.onclick = this.onclick.bind(this); this.onkeydown = this.onkeydown.bind(this); useRefById({ @@ -186,23 +185,17 @@ class CollapsibleTriggerState { }); } - onpointerdown(e: BitsPointerEvent) { + onclick(e: BitsMouseEvent) { if (this.#isDisabled) return; - if (e.pointerType === "touch" || e.button !== 0) return e.preventDefault(); + if (e.button !== 0) return e.preventDefault(); this.#root.toggleOpen(); } - onpointerup(e: BitsPointerEvent) { - if (this.#isDisabled) return; - if (e.pointerType === "touch") { - e.preventDefault(); - this.#root.toggleOpen(); - } - } - onkeydown(e: BitsKeyboardEvent) { if (this.#isDisabled) return; + if (e.key === kbd.SPACE || e.key === kbd.ENTER) { + e.preventDefault(); this.#root.toggleOpen(); } } @@ -219,8 +212,7 @@ class CollapsibleTriggerState { "data-disabled": getDataDisabled(this.#isDisabled), [COLLAPSIBLE_TRIGGER_ATTR]: "", // - onpointerdown: this.onpointerdown, - onpointerup: this.onpointerup, + onclick: this.onclick, onkeydown: this.onkeydown, }) as const ); diff --git a/packages/bits-ui/src/lib/bits/dialog/dialog.svelte.ts b/packages/bits-ui/src/lib/bits/dialog/dialog.svelte.ts index aecbcc859..f89d79a6a 100644 --- a/packages/bits-ui/src/lib/bits/dialog/dialog.svelte.ts +++ b/packages/bits-ui/src/lib/bits/dialog/dialog.svelte.ts @@ -108,13 +108,10 @@ class DialogTriggerState { onpointerdown = (e: BitsPointerEvent) => { if (this.#disabled.current) return; - if (e.pointerType === "touch") return e.preventDefault(); if (e.button > 0) return; // by default, it will attempt to focus this trigger on pointerdown // since this also opens the dialog we want to prevent that behavior e.preventDefault(); - - this.#root.handleOpen(); }; onkeydown = (e: BitsKeyboardEvent) => { @@ -180,10 +177,9 @@ class DialogCloseState { onpointerdown(e: BitsPointerEvent) { if (this.#disabled.current) return; - if (e.pointerType === "touch") return e.preventDefault(); if (e.button > 0) return; // by default, it will attempt to focus this trigger on pointerdown - // since this also opens the dialog we want to prevent that behavior + // since this also closes the dialog and restores focus we want to prevent that behavior e.preventDefault(); this.#root.handleClose(); diff --git a/packages/bits-ui/src/lib/bits/pagination/pagination.svelte.ts b/packages/bits-ui/src/lib/bits/pagination/pagination.svelte.ts index 3d4a95d58..f14921dc9 100644 --- a/packages/bits-ui/src/lib/bits/pagination/pagination.svelte.ts +++ b/packages/bits-ui/src/lib/bits/pagination/pagination.svelte.ts @@ -1,6 +1,6 @@ import { useRefById } from "svelte-toolbelt"; import type { Page, PageItem } from "./types.js"; -import type { BitsKeyboardEvent, BitsPointerEvent, WithRefProps } from "$lib/internal/types.js"; +import type { BitsKeyboardEvent, BitsMouseEvent, WithRefProps } from "$lib/internal/types.js"; import type { ReadableBoxedValues, WritableBoxedValues } from "$lib/internal/box.svelte.js"; import { getDataOrientation } from "$lib/internal/attrs.js"; import { getElemDirection } from "$lib/internal/locale.js"; @@ -143,25 +143,16 @@ class PaginationPageState { ref: this.#ref, }); - this.onpointerdown = this.onpointerdown.bind(this); - this.onpointerup = this.onpointerup.bind(this); + this.onclick = this.onclick.bind(this); this.onkeydown = this.onkeydown.bind(this); } - onpointerdown(e: BitsPointerEvent) { + onclick(e: BitsMouseEvent) { if (this.#disabled.current) return; - if (e.pointerType === "touch") return e.preventDefault(); + if (e.button !== 0) return; this.#root.setPage(this.page.current.value); } - onpointerup(e: BitsPointerEvent) { - if (this.#disabled.current) return; - if (e.pointerType === "touch") { - e.preventDefault(); - this.#root.setPage(this.page.current.value); - } - } - onkeydown(e: BitsKeyboardEvent) { if (e.key === kbd.SPACE || e.key === kbd.ENTER) { e.preventDefault(); @@ -180,8 +171,7 @@ class PaginationPageState { "data-selected": this.#isSelected ? "" : undefined, [PAGE_ATTR]: "", // - onpointerdown: this.onpointerdown, - onpointerup: this.onpointerup, + onclick: this.onclick, onkeydown: this.onkeydown, }) as const ); @@ -217,8 +207,7 @@ class PaginationButtonState { ref: this.#ref, }); - this.onpointerdown = this.onpointerdown.bind(this); - this.onpointerup = this.onpointerup.bind(this); + this.onclick = this.onclick.bind(this); this.onkeydown = this.onkeydown.bind(this); } @@ -233,20 +222,12 @@ class PaginationButtonState { return false; }); - onpointerdown(e: BitsPointerEvent) { + onclick(e: BitsMouseEvent) { if (this.#disabled.current) return; - if (e.pointerType === "touch") return e.preventDefault(); + if (e.button !== 0) return; this.#action(); } - onpointerup(e: BitsPointerEvent) { - if (this.#disabled.current) return; - if (e.pointerType === "touch") { - e.preventDefault(); - this.#action(); - } - } - onkeydown(e: BitsKeyboardEvent) { if (e.key === kbd.SPACE || e.key === kbd.ENTER) { e.preventDefault(); @@ -264,8 +245,7 @@ class PaginationButtonState { [NEXT_ATTR]: this.type === "next" ? "" : undefined, disabled: this.#isDisabled, // - onpointerdown: this.onpointerdown, - onpointerup: this.onpointerup, + onclick: this.onclick, onkeydown: this.onkeydown, }) as const ); diff --git a/packages/bits-ui/src/lib/bits/popover/components/popover-content-static.svelte b/packages/bits-ui/src/lib/bits/popover/components/popover-content-static.svelte index ea7ca6ca8..353357137 100644 --- a/packages/bits-ui/src/lib/bits/popover/components/popover-content-static.svelte +++ b/packages/bits-ui/src/lib/bits/popover/components/popover-content-static.svelte @@ -35,13 +35,13 @@ function handleInteractOutside(e: PointerEvent) { onInteractOutside(e); if (e.defaultPrevented) return; - contentState.root.close(); + contentState.root.handleClose(); } function handleEscapeKeydown(e: KeyboardEvent) { onEscapeKeydown(e); if (e.defaultPrevented) return; - contentState.root.close(); + contentState.root.handleClose(); } function handleCloseAutoFocus(e: Event) { diff --git a/packages/bits-ui/src/lib/bits/popover/components/popover-content.svelte b/packages/bits-ui/src/lib/bits/popover/components/popover-content.svelte index 69be94d7d..7813fa1e4 100644 --- a/packages/bits-ui/src/lib/bits/popover/components/popover-content.svelte +++ b/packages/bits-ui/src/lib/bits/popover/components/popover-content.svelte @@ -7,6 +7,7 @@ import { useId } from "$lib/internal/use-id.js"; import { getFloatingContentCSSVars } from "$lib/internal/floating-svelte/floating-utils.svelte.js"; import PopperLayerForceMount from "$lib/bits/utilities/popper-layer/popper-layer-force-mount.svelte"; + import { isHTMLElement } from "$lib/internal/is.js"; let { child, @@ -35,13 +36,14 @@ function handleInteractOutside(e: PointerEvent) { onInteractOutside(e); if (e.defaultPrevented) return; - contentState.root.close(); + if (isHTMLElement(e.target) && e.target.closest("[data-popover-trigger")) return; + contentState.root.handleClose(); } function handleEscapeKeydown(e: KeyboardEvent) { onEscapeKeydown(e); if (e.defaultPrevented) return; - contentState.root.close(); + contentState.root.handleClose(); } function handleCloseAutoFocus(e: Event) { diff --git a/packages/bits-ui/src/lib/bits/popover/popover.svelte.ts b/packages/bits-ui/src/lib/bits/popover/popover.svelte.ts index 0135ed6a2..bdff2895a 100644 --- a/packages/bits-ui/src/lib/bits/popover/popover.svelte.ts +++ b/packages/bits-ui/src/lib/bits/popover/popover.svelte.ts @@ -1,9 +1,14 @@ -import { type ReadableBoxedValues, useRefById } from "svelte-toolbelt"; +import { type ReadableBoxedValues, afterSleep, afterTick, useRefById } from "svelte-toolbelt"; import type { WritableBoxedValues } from "$lib/internal/box.svelte.js"; import { kbd } from "$lib/internal/kbd.js"; import { getAriaExpanded, getDataOpenClosed } from "$lib/internal/attrs.js"; import { createContext } from "$lib/internal/create-context.js"; -import type { BitsKeyboardEvent, BitsPointerEvent, WithRefProps } from "$lib/internal/types.js"; +import type { + BitsKeyboardEvent, + BitsMouseEvent, + BitsPointerEvent, + WithRefProps, +} from "$lib/internal/types.js"; type PopoverRootStateProps = WritableBoxedValues<{ open: boolean; @@ -23,7 +28,7 @@ class PopoverRootState { this.open.current = !this.open.current; } - close() { + handleClose() { if (!this.open.current) return; this.open.current = false; } @@ -51,23 +56,23 @@ class PopoverTriggerState { }, }); + this.onclick = this.onclick.bind(this); this.onpointerdown = this.onpointerdown.bind(this); - this.onpointerup = this.onpointerup.bind(this); this.onkeydown = this.onkeydown.bind(this); } - onpointerdown(e: BitsPointerEvent) { + onclick(e: BitsMouseEvent) { if (this.#disabled.current) return; - if (e.pointerType === "touch" || e.button !== 0) return e.preventDefault(); + if (e.button !== 0) return; this.#root.toggleOpen(); } - onpointerup(e: BitsPointerEvent) { + onpointerdown(e: BitsPointerEvent) { if (this.#disabled.current) return; - if (e.pointerType === "touch") { - e.preventDefault(); - this.#root.toggleOpen(); - } + if (e.button !== 0) return; + // We prevent default to prevent focus from moving to the trigger + // since this action will open the popover and focus will move to the content + e.preventDefault(); } onkeydown(e: BitsKeyboardEvent) { @@ -97,7 +102,7 @@ class PopoverTriggerState { // onpointerdown: this.onpointerdown, onkeydown: this.onkeydown, - onpointerup: this.onpointerup, + onclick: this.onclick, }) as const ); } @@ -158,14 +163,14 @@ class PopoverCloseState { this.onkeydown = this.onkeydown.bind(this); } - onclick(e: BitsPointerEvent) { - this.#root.close(); + onclick(_: BitsPointerEvent) { + this.#root.handleClose(); } onkeydown(e: BitsKeyboardEvent) { if (!(e.key === kbd.ENTER || e.key === kbd.SPACE)) return; e.preventDefault(); - this.#root.close(); + this.#root.handleClose(); } props = $derived.by( diff --git a/packages/tests/src/tests/accordion/accordion.test.ts b/packages/tests/src/tests/accordion/accordion.test.ts index 2f35088fa..a0898b1c2 100644 --- a/packages/tests/src/tests/accordion/accordion.test.ts +++ b/packages/tests/src/tests/accordion/accordion.test.ts @@ -2,7 +2,7 @@ import { render, waitFor } from "@testing-library/svelte/svelte5"; import { axe } from "jest-axe"; import { describe, it } from "vitest"; -import { tick } from "svelte"; +import { type ComponentProps, tick } from "svelte"; import { getTestKbd, setupUserEvents, sleep } from "../utils.js"; import AccordionSingleTest from "./accordion-single-test.svelte"; import AccordionMultiTest from "./accordion-multi-test.svelte"; @@ -59,9 +59,46 @@ const itemsWithDisabled = items.map((item) => { return item; }); +function setupSingle(props: ComponentProps = { items }) { + const user = setupUserEvents(); + const returned = render(AccordionSingleTest, { ...props }); + const itemEls = items.map((item) => returned.getByTestId(`${item.value}-item`)); + const triggerEls = items.map((item) => returned.getByTestId(`${item.value}-trigger`)); + return { + user, + itemEls, + triggerEls, + ...returned, + }; +} + +function expectOpen(...itemEls: HTMLElement[]) { + for (const itemEl of itemEls) { + expect(itemEl).toHaveAttribute("data-state", "open"); + } +} + +function expectClosed(...itemEls: HTMLElement[]) { + for (const itemEl of itemEls) { + expect(itemEl).toHaveAttribute("data-state", "closed"); + } +} + +function expectDisabled(...triggerEls: HTMLElement[]) { + for (const triggerEl of triggerEls) { + expect(triggerEl).toHaveAttribute("data-disabled"); + } +} + +function expectNotDisabled(...triggerEls: HTMLElement[]) { + for (const triggerEl of triggerEls) { + expect(triggerEl).not.toHaveAttribute("data-disabled"); + } +} + describe("accordion - single", () => { it("should have no accessibility violations", async () => { - const { container } = render(AccordionSingleTest as any, { items }); + const { container } = setupSingle(); expect(await axe(container)).toHaveNoViolations(); }); @@ -81,22 +118,15 @@ describe("accordion - single", () => { it("should have expected data attributes", async () => { const user = setupUserEvents(); - const { getByTestId } = render(AccordionSingleTest as any, { items: itemsWithDisabled }); - const itemEls = items.map((item) => getByTestId(`${item.value}-item`)); - const triggerEls = items.map((item) => getByTestId(`${item.value}-trigger`)); + const { itemEls, triggerEls } = setupSingle({ items: itemsWithDisabled }); - expect(itemEls[0]).toHaveAttribute("data-state", "closed"); - expect(itemEls[0]).not.toHaveAttribute("data-disabled"); - expect(triggerEls[0]).toHaveAttribute("data-state", "closed"); - expect(triggerEls[0]).not.toHaveAttribute("data-disabled"); + expectClosed(itemEls[0], triggerEls[0]); + expectNotDisabled(itemEls[0], triggerEls[0]); await user.click(triggerEls[0] as HTMLElement); await tick(); - expect(itemEls[0]).toHaveAttribute("data-state", "open"); - expect(triggerEls[0]).toHaveAttribute("data-state", "open"); - - expect(itemEls[1]).toHaveAttribute("data-disabled"); - expect(triggerEls[1]).toHaveAttribute("data-disabled"); + expectOpen(itemEls[0], triggerEls[0]); + expectDisabled(itemEls[1], triggerEls[1]); }); it("should forceMount the content when `forceMount` is true", async () => { @@ -143,16 +173,16 @@ describe("accordion - single", () => { const triggerEls = items.map((item) => getByTestId(`${item.value}-trigger`)); await user.click(triggerEls[0] as HTMLElement); - expect(triggerEls[0]).not.toHaveAttribute("data-state", "open"); - expect(triggerEls[0]).toHaveAttribute("data-disabled"); + expectClosed(triggerEls[0]); + expectDisabled(triggerEls[0]); await user.click(triggerEls[1] as HTMLElement); - expect(triggerEls[1]).not.toHaveAttribute("data-state", "open"); - expect(triggerEls[1]).toHaveAttribute("data-disabled"); + expectClosed(triggerEls[1]); + expectDisabled(triggerEls[1]); await user.click(triggerEls[2] as HTMLElement); - expect(triggerEls[2]).not.toHaveAttribute("data-state", "open"); - expect(triggerEls[2]).toHaveAttribute("data-disabled"); + expectClosed(triggerEls[2]); + expectDisabled(triggerEls[2]); }); it("should display content when an item is expanded", async () => { @@ -163,12 +193,12 @@ describe("accordion - single", () => { const trigger = getByTestId(`${item.value}-trigger`); const content = getByTestId(`${item.value}-content`); const itemEl = getByTestId(`${item.value}-item`); - expect(itemEl).toHaveAttribute("data-state", "closed"); - expect(itemEl).toHaveAttribute("data-state", "closed"); + expectClosed(itemEl, trigger); expect(content).not.toBeVisible(); await user.click(trigger); const contentAfter = getByTestId(`${item.value}-content`); expect(contentAfter).toHaveTextContent(item.content); + expectOpen(itemEl, trigger); expect(itemEl).toHaveAttribute("data-state", "open"); } }); @@ -181,13 +211,12 @@ describe("accordion - single", () => { const trigger = getByTestId(`${item.value}-trigger`); const content = getByTestId(`${item.value}-content`); const itemEl = getByTestId(`${item.value}-item`); - expect(itemEl).toHaveAttribute("data-state", "closed"); - expect(itemEl).toHaveAttribute("data-state", "closed"); + expectClosed(itemEl, trigger); expect(content).not.toBeVisible(); await user.click(trigger); const contentAfter = getByTestId(`${item.value}-content`); expect(contentAfter).toHaveTextContent(item.content); - expect(itemEl).toHaveAttribute("data-state", "open"); + expectOpen(itemEl, trigger); } const openItems = Array.from( document.querySelectorAll("[data-state='open'][data-accordion-item]") @@ -205,14 +234,13 @@ describe("accordion - single", () => { const trigger = getByTestId(`${item.value}-trigger`); const content = getByTestId(`${item.value}-content`); const itemEl = getByTestId(`${item.value}-item`); - expect(itemEl).toHaveAttribute("data-state", "closed"); - expect(itemEl).toHaveAttribute("data-state", "closed"); + expectClosed(itemEl, trigger); expect(content).not.toBeVisible(); trigger.focus(); await user.keyboard(kbd.ENTER); const contentAfter = getByTestId(`${item.value}-content`); expect(contentAfter).toHaveTextContent(item.content); - expect(itemEl).toHaveAttribute("data-state", "open"); + expectOpen(itemEl, trigger); } }); @@ -226,14 +254,13 @@ describe("accordion - single", () => { const trigger = getByTestId(`${item.value}-trigger`); const content = getByTestId(`${item.value}-content`); const itemEl = getByTestId(`${item.value}-item`); - expect(itemEl).toHaveAttribute("data-state", "closed"); - expect(itemEl).toHaveAttribute("data-state", "closed"); + expectClosed(itemEl, trigger); expect(content).not.toBeVisible(); trigger.focus(); await user.keyboard(kbd.SPACE); const contentAfter = getByTestId(`${item.value}-content`); expect(contentAfter).toHaveTextContent(item.content); - expect(itemEl).toHaveAttribute("data-state", "open"); + expectOpen(itemEl, trigger); } }); @@ -346,11 +373,11 @@ describe("accordion - single", () => { expect(value).toHaveTextContent(""); const itemOneItem = getByTestId("item-1-item"); - expect(itemOneItem).toHaveAttribute("data-state", "closed"); + expectClosed(itemOneItem); await user.click(updateButton); expect(value).toHaveTextContent("item-1"); - expect(itemOneItem).toHaveAttribute("data-state", "open"); + expectOpen(itemOneItem); }); }); @@ -370,17 +397,12 @@ describe("accordion - multiple", () => { const itemEls = items.map((item) => getByTestId(`${item.value}-item`)); const triggerEls = items.map((item) => getByTestId(`${item.value}-trigger`)); - expect(itemEls[0]).toHaveAttribute("data-state", "closed"); - expect(itemEls[0]).not.toHaveAttribute("data-disabled"); - expect(triggerEls[0]).toHaveAttribute("data-state", "closed"); - expect(triggerEls[0]).not.toHaveAttribute("data-disabled"); + expectClosed(itemEls[0], triggerEls[0]); + expectNotDisabled(itemEls[0], triggerEls[0]); await user.click(triggerEls[0] as HTMLElement); - await waitFor(() => expect(triggerEls[0]).toHaveAttribute("data-state", "open")); - expect(itemEls[0]).toHaveAttribute("data-state", "open"); - - expect(itemEls[1]).toHaveAttribute("data-disabled"); - expect(triggerEls[1]).toHaveAttribute("data-disabled"); + expectOpen(itemEls[0], triggerEls[0]); + expectDisabled(itemEls[1], triggerEls[1]); }); it("should disable everything when the `disabled` prop is true", async () => { @@ -392,16 +414,16 @@ describe("accordion - multiple", () => { const triggerEls = items.map((item) => getByTestId(`${item.value}-trigger`)); await user.click(triggerEls[0] as HTMLElement); - expect(triggerEls[0]).not.toHaveAttribute("data-state", "open"); - expect(triggerEls[0]).toHaveAttribute("data-disabled"); + expectClosed(triggerEls[0]); + expectDisabled(triggerEls[0]); await user.click(triggerEls[1] as HTMLElement); - expect(triggerEls[1]).not.toHaveAttribute("data-state", "open"); - expect(triggerEls[1]).toHaveAttribute("data-disabled"); + expectClosed(triggerEls[1]); + expectDisabled(triggerEls[1]); await user.click(triggerEls[2] as HTMLElement); - expect(triggerEls[2]).not.toHaveAttribute("data-state", "open"); - expect(triggerEls[2]).toHaveAttribute("data-disabled"); + expectClosed(triggerEls[2]); + expectDisabled(triggerEls[2]); }); it("should display content when an item is expanded", async () => { @@ -412,14 +434,12 @@ describe("accordion - multiple", () => { const trigger = getByTestId(`${item.value}-trigger`); const content = getByTestId(`${item.value}-content`); const itemEl = getByTestId(`${item.value}-item`); - expect(itemEl).toHaveAttribute("data-state", "closed"); - expect(itemEl).toHaveAttribute("data-state", "closed"); + expectClosed(itemEl, trigger); expect(content).not.toBeVisible(); await user.click(trigger); const contentAfter = getByTestId(`${item.value}-content`); expect(contentAfter).toHaveTextContent(item.content); - - await waitFor(() => expect(itemEl).toHaveAttribute("data-state", "open")); + expectOpen(itemEl, trigger); } }); @@ -433,12 +453,12 @@ describe("accordion - multiple", () => { const trigger = getByTestId(`${item.value}-trigger`); const content = getByTestId(`${item.value}-content`); const itemEl = getByTestId(`${item.value}-item`); - expect(itemEl).toHaveAttribute("data-state", "closed"); + expectClosed(itemEl, trigger); expect(content).not.toBeVisible(); await user.click(trigger); const contentAfter = getByTestId(`${item.value}-content`); expect(contentAfter).toHaveTextContent(item.content); - expect(itemEl).toHaveAttribute("data-state", "open"); + expectOpen(itemEl, trigger); } const openItems = Array.from( document.querySelectorAll("[data-state='open'][data-accordion-item]") @@ -456,14 +476,13 @@ describe("accordion - multiple", () => { const trigger = getByTestId(`${item.value}-trigger`); const content = getByTestId(`${item.value}-content`); const itemEl = getByTestId(`${item.value}-item`); - expect(itemEl).toHaveAttribute("data-state", "closed"); - expect(itemEl).toHaveAttribute("data-state", "closed"); + expectClosed(itemEl, trigger); expect(content).not.toBeVisible(); trigger.focus(); await user.keyboard(kbd.ENTER); const contentAfter = getByTestId(`${item.value}-content`); expect(contentAfter).toHaveTextContent(item.content); - expect(itemEl).toHaveAttribute("data-state", "open"); + expectOpen(itemEl, trigger); } }); @@ -477,15 +496,14 @@ describe("accordion - multiple", () => { const trigger = getByTestId(`${item.value}-trigger`); const content = getByTestId(`${item.value}-content`); const itemEl = getByTestId(`${item.value}-item`); - expect(itemEl).toHaveAttribute("data-state", "closed"); - expect(itemEl).toHaveAttribute("data-state", "closed"); + expectClosed(itemEl, trigger); expect(content).not.toBeVisible(); trigger.focus(); await user.keyboard(kbd.SPACE); await sleep(19); const contentAfter = getByTestId(`${item.value}-content`); expect(contentAfter).toHaveTextContent(item.content); - expect(itemEl).toHaveAttribute("data-state", "open"); + expectOpen(itemEl, trigger); } }); @@ -592,7 +610,7 @@ describe("accordion - multiple", () => { it('should handle programmatic changes to the "value" prop', async () => { const user = setupUserEvents(); - const { getByTestId, queryByTestId } = render(AccordionMultiTestControlled as any, { + const { getByTestId } = render(AccordionMultiTestControlled as any, { items, }); const updateButton = getByTestId("update-value"); @@ -601,8 +619,8 @@ describe("accordion - multiple", () => { expect(value).toHaveTextContent(""); const itemOneItem = getByTestId("item-1-item"); - expect(itemOneItem).toHaveAttribute("data-state", "closed"); + expectClosed(itemOneItem); await user.click(updateButton); - expect(itemOneItem).toHaveAttribute("data-state", "open"); + expectOpen(itemOneItem); }); }); diff --git a/packages/tests/src/tests/popover/popover.test.ts b/packages/tests/src/tests/popover/popover.test.ts index bee26dcb4..b5cdea46b 100644 --- a/packages/tests/src/tests/popover/popover.test.ts +++ b/packages/tests/src/tests/popover/popover.test.ts @@ -1,7 +1,7 @@ import { render, waitFor } from "@testing-library/svelte/svelte5"; import { axe } from "jest-axe"; import { describe, it, vi } from "vitest"; -import { type Component, tick } from "svelte"; +import type { Component } from "svelte"; import { getTestKbd, setupUserEvents } from "../utils.js"; import PopoverTest, { type PopoverTestProps } from "./popover-test.svelte"; import PopoverForceMountTest, { @@ -29,7 +29,7 @@ async function open(props: PopoverTestProps = {}, openWith: "click" | (string & const { trigger, getByTestId, queryByTestId, user, getContent, ...returned } = setup(props); expect(getContent()).toBeNull(); if (openWith === "click") { - await user.click(trigger); + await user.pointerDownUp(trigger); } else { trigger.focus(); await user.keyboard(openWith); @@ -88,7 +88,7 @@ describe("popover", () => { it("should close when the close button is clicked", async () => { const { user, getContent, getByTestId } = await open(); const close = getByTestId("close"); - await user.click(close); + await user.pointerDownUp(close); expect(getContent()).toBeNull(); }); @@ -151,7 +151,7 @@ describe("popover", () => { }, }); const outside = getByTestId("outside"); - await user.click(outside); + await user.pointerDownUp(outside); expect(getContent()).not.toBeNull(); }); @@ -184,7 +184,7 @@ describe("popover", () => { PopoverForceMountTest ); expect(queryByTestId("content")).toBeNull(); - await user.click(trigger); + await user.pointerDownUp(trigger); const content = getByTestId("content"); expect(content).toBeVisible(); }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8b1a5ac4c..79a74926e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -13,7 +13,7 @@ importers: version: 2.27.9 '@huntabyte/eslint-config': specifier: ^0.3.2 - version: 0.3.2(@vue/compiler-sfc@3.4.31)(eslint-plugin-svelte@2.46.0(eslint@9.14.0(jiti@1.21.6))(svelte@5.1.16))(eslint@9.14.0(jiti@1.21.6))(svelte-eslint-parser@0.41.1(svelte@5.1.16))(svelte@5.1.16)(typescript@5.6.3)(vitest@2.1.5(@types/node@22.9.0)(jsdom@24.1.3)(terser@5.36.0)) + version: 0.3.2(@vue/compiler-sfc@3.4.31)(eslint-plugin-svelte@2.46.0(eslint@9.14.0(jiti@1.21.6))(svelte@5.1.16))(eslint@9.14.0(jiti@1.21.6))(svelte-eslint-parser@0.41.1(svelte@5.1.16))(svelte@5.1.16)(typescript@5.6.3)(vitest@2.1.5) '@huntabyte/eslint-plugin': specifier: ^0.1.0 version: 0.1.0(eslint@9.14.0(jiti@1.21.6)) @@ -110,7 +110,7 @@ importers: version: 5.4.11(@types/node@20.17.6)(terser@5.36.0) vitest: specifier: ^2.1.5 - version: 2.1.5(@types/node@20.17.6)(jsdom@24.1.3)(terser@5.36.0) + version: 2.1.5(@types/node@20.17.6)(@vitest/ui@2.1.8)(jsdom@24.1.3)(terser@5.36.0) packages/tests: dependencies: @@ -138,7 +138,7 @@ importers: version: 6.6.3 '@testing-library/svelte': specifier: ^5.2.4 - version: 5.2.4(svelte@5.1.16)(vite@5.4.11(@types/node@20.17.6)(terser@5.36.0))(vitest@2.1.5(@types/node@20.17.6)(jsdom@24.1.3)(terser@5.36.0)) + version: 5.2.4(svelte@5.1.16)(vite@5.4.11(@types/node@20.17.6)(terser@5.36.0))(vitest@2.1.5(@types/node@20.17.6)(@vitest/ui@2.1.8)(jsdom@24.1.3)(terser@5.36.0)) '@testing-library/user-event': specifier: ^14.5.2 version: 14.5.2(@testing-library/dom@10.4.0) @@ -154,6 +154,9 @@ importers: '@types/testing-library__jest-dom': specifier: ^5.14.9 version: 5.14.9 + '@vitest/ui': + specifier: ^2.1.8 + version: 2.1.8(vitest@2.1.5) jest-axe: specifier: ^9.0.0 version: 9.0.0 @@ -177,7 +180,7 @@ importers: version: 5.4.11(@types/node@20.17.6)(terser@5.36.0) vitest: specifier: ^2.1.5 - version: 2.1.5(@types/node@20.17.6)(jsdom@24.1.3)(terser@5.36.0) + version: 2.1.5(@types/node@20.17.6)(@vitest/ui@2.1.8)(jsdom@24.1.3)(terser@5.36.0) sites/docs: dependencies: @@ -310,7 +313,7 @@ importers: version: 5.0.0 velite: specifier: ^0.2.1 - version: 0.2.1(acorn@8.12.1) + version: 0.2.1(acorn@8.14.0) vite: specifier: ^5.4.11 version: 5.4.11(@types/node@20.17.6)(terser@5.36.0) @@ -1716,6 +1719,9 @@ packages: '@vitest/pretty-format@2.1.5': resolution: {integrity: sha512-4ZOwtk2bqG5Y6xRGHcveZVr+6txkH7M2e+nPFd6guSoN638v/1XQ0K06eOpi0ptVU/2tW/pIU4IoPotY/GZ9fw==} + '@vitest/pretty-format@2.1.8': + resolution: {integrity: sha512-9HiSZ9zpqNLKlbIDRWOnAWqgcA7xu+8YxXSekhr0Ykab7PAYFkhkwoqVArPOtJhPmYeE2YHgKZlj3CP36z2AJQ==} + '@vitest/runner@2.1.5': resolution: {integrity: sha512-pKHKy3uaUdh7X6p1pxOkgkVAFW7r2I818vHDthYLvUyjRfkKOU6P45PztOch4DZarWQne+VOaIMwA/erSSpB9g==} @@ -1725,9 +1731,17 @@ packages: '@vitest/spy@2.1.5': resolution: {integrity: sha512-aWZF3P0r3w6DiYTVskOYuhBc7EMc3jvn1TkBg8ttylFFRqNN2XGD7V5a4aQdk6QiUzZQ4klNBSpCLJgWNdIiNw==} + '@vitest/ui@2.1.8': + resolution: {integrity: sha512-5zPJ1fs0ixSVSs5+5V2XJjXLmNzjugHRyV11RqxYVR+oMcogZ9qTuSfKW+OcTV0JeFNznI83BNylzH6SSNJ1+w==} + peerDependencies: + vitest: 2.1.8 + '@vitest/utils@2.1.5': resolution: {integrity: sha512-yfj6Yrp0Vesw2cwJbP+cl04OC+IHFsuQsrsJBL9pyGeQXE56v1UAOQco+SR55Vf1nQzfV0QJg1Qum7AaWUwwYg==} + '@vitest/utils@2.1.8': + resolution: {integrity: sha512-dwSoui6djdwbfFmIgbIjX2ZhIoG7Ex/+xpxyiEgIGzjliY8xGkcpITKTlp6B4MgtGkF2ilvm97cPM96XZaAgcA==} + '@vue/compiler-core@3.4.31': resolution: {integrity: sha512-skOiodXWTV3DxfDhB4rOf3OGalpITLlgCeOwb+Y9GJpfQ8ErigdBUHomBzvG78JoVE8MJoQsb+qhZiHfKeNeEg==} @@ -2592,6 +2606,17 @@ packages: picomatch: optional: true + fdir@6.4.2: + resolution: {integrity: sha512-KnhMXsKSPZlAhp7+IjUkRZKPb4fUyccpDrdFXbi4QL1qkmFh9kVY09Yox+n4MaOb3lHZ1Tv829C3oaaXoMYPDQ==} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + fflate@0.8.2: + resolution: {integrity: sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==} + file-entry-cache@8.0.0: resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} engines: {node: '>=16.0.0'} @@ -4283,6 +4308,10 @@ packages: tinyexec@0.3.1: resolution: {integrity: sha512-WiCJLEECkO18gwqIp6+hJg0//p23HXp4S+gGtAKu3mI2F2/sXC4FvHvXvB0zJVVaTPhx1/tOwdbRsa1sOBIKqQ==} + tinyglobby@0.2.10: + resolution: {integrity: sha512-Zc+8eJlFMvgatPZTl6A9L/yht8QqdmUNtURHaKZLmKBE12hNPSrqNkUp2cs3M/UKmNVVAMFQYSjYIVHDjW5zew==} + engines: {node: '>=12.0.0'} + tinypool@1.0.2: resolution: {integrity: sha512-al6n+QEANGFOMf/dmUMsuS5/r9B06uwlyNjZZql/zv8J7ybHCgoihBNORZCY2mzUuAnomQa2JdhyHKzZxPCrFA==} engines: {node: ^18.0.0 || >=20.0.0} @@ -4693,7 +4722,7 @@ snapshots: '@jridgewell/gen-mapping': 0.3.5 '@jridgewell/trace-mapping': 0.3.25 - '@antfu/eslint-config@2.22.0(@vue/compiler-sfc@3.4.31)(eslint-plugin-svelte@2.46.0(eslint@9.14.0(jiti@1.21.6))(svelte@5.1.16))(eslint@9.14.0(jiti@1.21.6))(svelte-eslint-parser@0.41.1(svelte@5.1.16))(svelte@5.1.16)(typescript@5.6.3)(vitest@2.1.5(@types/node@22.9.0)(jsdom@24.1.3)(terser@5.36.0))': + '@antfu/eslint-config@2.22.0(@vue/compiler-sfc@3.4.31)(eslint-plugin-svelte@2.46.0(eslint@9.14.0(jiti@1.21.6))(svelte@5.1.16))(eslint@9.14.0(jiti@1.21.6))(svelte-eslint-parser@0.41.1(svelte@5.1.16))(svelte@5.1.16)(typescript@5.6.3)(vitest@2.1.5)': dependencies: '@antfu/install-pkg': 0.3.3 '@clack/prompts': 0.7.0 @@ -4718,7 +4747,7 @@ snapshots: eslint-plugin-toml: 0.11.1(eslint@9.14.0(jiti@1.21.6)) eslint-plugin-unicorn: 54.0.0(eslint@9.14.0(jiti@1.21.6)) eslint-plugin-unused-imports: 4.0.0(@typescript-eslint/eslint-plugin@8.0.0-alpha.40(@typescript-eslint/parser@8.0.0-alpha.40(eslint@9.14.0(jiti@1.21.6))(typescript@5.6.3))(eslint@9.14.0(jiti@1.21.6))(typescript@5.6.3))(eslint@9.14.0(jiti@1.21.6)) - eslint-plugin-vitest: 0.5.4(@typescript-eslint/eslint-plugin@8.0.0-alpha.40(@typescript-eslint/parser@8.0.0-alpha.40(eslint@9.14.0(jiti@1.21.6))(typescript@5.6.3))(eslint@9.14.0(jiti@1.21.6))(typescript@5.6.3))(eslint@9.14.0(jiti@1.21.6))(typescript@5.6.3)(vitest@2.1.5(@types/node@22.9.0)(jsdom@24.1.3)(terser@5.36.0)) + eslint-plugin-vitest: 0.5.4(@typescript-eslint/eslint-plugin@8.0.0-alpha.40(@typescript-eslint/parser@8.0.0-alpha.40(eslint@9.14.0(jiti@1.21.6))(typescript@5.6.3))(eslint@9.14.0(jiti@1.21.6))(typescript@5.6.3))(eslint@9.14.0(jiti@1.21.6))(typescript@5.6.3)(vitest@2.1.5) eslint-plugin-vue: 9.27.0(eslint@9.14.0(jiti@1.21.6)) eslint-plugin-yml: 1.14.0(eslint@9.14.0(jiti@1.21.6)) eslint-processor-vue-blocks: 0.1.2(@vue/compiler-sfc@3.4.31)(eslint@9.14.0(jiti@1.21.6)) @@ -5272,9 +5301,9 @@ snapshots: '@humanwhocodes/retry@0.4.1': {} - '@huntabyte/eslint-config@0.3.2(@vue/compiler-sfc@3.4.31)(eslint-plugin-svelte@2.46.0(eslint@9.14.0(jiti@1.21.6))(svelte@5.1.16))(eslint@9.14.0(jiti@1.21.6))(svelte-eslint-parser@0.41.1(svelte@5.1.16))(svelte@5.1.16)(typescript@5.6.3)(vitest@2.1.5(@types/node@22.9.0)(jsdom@24.1.3)(terser@5.36.0))': + '@huntabyte/eslint-config@0.3.2(@vue/compiler-sfc@3.4.31)(eslint-plugin-svelte@2.46.0(eslint@9.14.0(jiti@1.21.6))(svelte@5.1.16))(eslint@9.14.0(jiti@1.21.6))(svelte-eslint-parser@0.41.1(svelte@5.1.16))(svelte@5.1.16)(typescript@5.6.3)(vitest@2.1.5)': dependencies: - '@antfu/eslint-config': 2.22.0(@vue/compiler-sfc@3.4.31)(eslint-plugin-svelte@2.46.0(eslint@9.14.0(jiti@1.21.6))(svelte@5.1.16))(eslint@9.14.0(jiti@1.21.6))(svelte-eslint-parser@0.41.1(svelte@5.1.16))(svelte@5.1.16)(typescript@5.6.3)(vitest@2.1.5(@types/node@22.9.0)(jsdom@24.1.3)(terser@5.36.0)) + '@antfu/eslint-config': 2.22.0(@vue/compiler-sfc@3.4.31)(eslint-plugin-svelte@2.46.0(eslint@9.14.0(jiti@1.21.6))(svelte@5.1.16))(eslint@9.14.0(jiti@1.21.6))(svelte-eslint-parser@0.41.1(svelte@5.1.16))(svelte@5.1.16)(typescript@5.6.3)(vitest@2.1.5) '@antfu/install-pkg': 0.3.3 '@clack/prompts': 0.7.0 '@huntabyte/eslint-plugin': 0.1.0(eslint@9.14.0(jiti@1.21.6)) @@ -5465,7 +5494,7 @@ snapshots: globby: 11.1.0 read-yaml-file: 1.1.0 - '@mdx-js/mdx@3.1.0(acorn@8.12.1)': + '@mdx-js/mdx@3.1.0(acorn@8.14.0)': dependencies: '@types/estree': 1.0.6 '@types/estree-jsx': 1.0.5 @@ -5479,7 +5508,7 @@ snapshots: hast-util-to-jsx-runtime: 2.3.0 markdown-extensions: 2.0.0 recma-build-jsx: 1.0.0 - recma-jsx: 1.0.0(acorn@8.12.1) + recma-jsx: 1.0.0(acorn@8.14.0) recma-stringify: 1.0.0 rehype-recma: 1.0.0 remark-mdx: 3.0.1 @@ -5769,13 +5798,13 @@ snapshots: lodash: 4.17.21 redent: 3.0.0 - '@testing-library/svelte@5.2.4(svelte@5.1.16)(vite@5.4.11(@types/node@20.17.6)(terser@5.36.0))(vitest@2.1.5(@types/node@20.17.6)(jsdom@24.1.3)(terser@5.36.0))': + '@testing-library/svelte@5.2.4(svelte@5.1.16)(vite@5.4.11(@types/node@20.17.6)(terser@5.36.0))(vitest@2.1.5(@types/node@20.17.6)(@vitest/ui@2.1.8)(jsdom@24.1.3)(terser@5.36.0))': dependencies: '@testing-library/dom': 10.4.0 svelte: 5.1.16 optionalDependencies: vite: 5.4.11(@types/node@20.17.6)(terser@5.36.0) - vitest: 2.1.5(@types/node@20.17.6)(jsdom@24.1.3)(terser@5.36.0) + vitest: 2.1.5(@types/node@20.17.6)(@vitest/ui@2.1.8)(jsdom@24.1.3)(terser@5.36.0) '@testing-library/user-event@14.5.2(@testing-library/dom@10.4.0)': dependencies: @@ -6087,7 +6116,7 @@ snapshots: chai: 5.1.2 tinyrainbow: 1.2.0 - '@vitest/mocker@2.1.5(vite@5.4.11(@types/node@20.17.6)(terser@5.36.0))': + '@vitest/mocker@2.1.5(vite@5.4.11)': dependencies: '@vitest/spy': 2.1.5 estree-walker: 3.0.3 @@ -6095,16 +6124,11 @@ snapshots: optionalDependencies: vite: 5.4.11(@types/node@20.17.6)(terser@5.36.0) - '@vitest/mocker@2.1.5(vite@5.4.11(@types/node@22.9.0)(terser@5.36.0))': + '@vitest/pretty-format@2.1.5': dependencies: - '@vitest/spy': 2.1.5 - estree-walker: 3.0.3 - magic-string: 0.30.12 - optionalDependencies: - vite: 5.4.11(@types/node@22.9.0)(terser@5.36.0) - optional: true + tinyrainbow: 1.2.0 - '@vitest/pretty-format@2.1.5': + '@vitest/pretty-format@2.1.8': dependencies: tinyrainbow: 1.2.0 @@ -6123,12 +6147,29 @@ snapshots: dependencies: tinyspy: 3.0.2 + '@vitest/ui@2.1.8(vitest@2.1.5)': + dependencies: + '@vitest/utils': 2.1.8 + fflate: 0.8.2 + flatted: 3.3.1 + pathe: 1.1.2 + sirv: 3.0.0 + tinyglobby: 0.2.10 + tinyrainbow: 1.2.0 + vitest: 2.1.5(@types/node@20.17.6)(@vitest/ui@2.1.8)(jsdom@24.1.3)(terser@5.36.0) + '@vitest/utils@2.1.5': dependencies: '@vitest/pretty-format': 2.1.5 loupe: 3.1.2 tinyrainbow: 1.2.0 + '@vitest/utils@2.1.8': + dependencies: + '@vitest/pretty-format': 2.1.8 + loupe: 3.1.2 + tinyrainbow: 1.2.0 + '@vue/compiler-core@3.4.31': dependencies: '@babel/parser': 7.26.2 @@ -6889,13 +6930,13 @@ snapshots: optionalDependencies: '@typescript-eslint/eslint-plugin': 8.0.0-alpha.40(@typescript-eslint/parser@7.16.0(eslint@9.14.0(jiti@1.21.6))(typescript@5.6.3))(eslint@9.14.0(jiti@1.21.6))(typescript@5.6.3) - eslint-plugin-vitest@0.5.4(@typescript-eslint/eslint-plugin@8.0.0-alpha.40(@typescript-eslint/parser@8.0.0-alpha.40(eslint@9.14.0(jiti@1.21.6))(typescript@5.6.3))(eslint@9.14.0(jiti@1.21.6))(typescript@5.6.3))(eslint@9.14.0(jiti@1.21.6))(typescript@5.6.3)(vitest@2.1.5(@types/node@22.9.0)(jsdom@24.1.3)(terser@5.36.0)): + eslint-plugin-vitest@0.5.4(@typescript-eslint/eslint-plugin@8.0.0-alpha.40(@typescript-eslint/parser@8.0.0-alpha.40(eslint@9.14.0(jiti@1.21.6))(typescript@5.6.3))(eslint@9.14.0(jiti@1.21.6))(typescript@5.6.3))(eslint@9.14.0(jiti@1.21.6))(typescript@5.6.3)(vitest@2.1.5): dependencies: '@typescript-eslint/utils': 7.16.0(eslint@9.14.0(jiti@1.21.6))(typescript@5.6.3) eslint: 9.14.0(jiti@1.21.6) optionalDependencies: '@typescript-eslint/eslint-plugin': 8.0.0-alpha.40(@typescript-eslint/parser@7.16.0(eslint@9.14.0(jiti@1.21.6))(typescript@5.6.3))(eslint@9.14.0(jiti@1.21.6))(typescript@5.6.3) - vitest: 2.1.5(@types/node@22.9.0)(jsdom@24.1.3)(terser@5.36.0) + vitest: 2.1.5 transitivePeerDependencies: - supports-color - typescript @@ -7102,6 +7143,12 @@ snapshots: optionalDependencies: picomatch: 4.0.2 + fdir@6.4.2(picomatch@4.0.2): + optionalDependencies: + picomatch: 4.0.2 + + fflate@0.8.2: {} + file-entry-cache@8.0.0: dependencies: flat-cache: 4.0.1 @@ -8633,9 +8680,9 @@ snapshots: estree-util-build-jsx: 3.0.1 vfile: 6.0.1 - recma-jsx@1.0.0(acorn@8.12.1): + recma-jsx@1.0.0(acorn@8.14.0): dependencies: - acorn-jsx: 5.3.2(acorn@8.12.1) + acorn-jsx: 5.3.2(acorn@8.14.0) estree-util-to-js: 2.0.0 recma-parse: 1.0.0 recma-stringify: 1.0.0 @@ -9216,6 +9263,11 @@ snapshots: tinyexec@0.3.1: {} + tinyglobby@0.2.10: + dependencies: + fdir: 6.4.2(picomatch@4.0.2) + picomatch: 4.0.2 + tinypool@1.0.2: {} tinyrainbow@1.2.0: {} @@ -9367,9 +9419,9 @@ snapshots: spdx-correct: 3.2.0 spdx-expression-parse: 3.0.1 - velite@0.2.1(acorn@8.12.1): + velite@0.2.1(acorn@8.14.0): dependencies: - '@mdx-js/mdx': 3.1.0(acorn@8.12.1) + '@mdx-js/mdx': 3.1.0(acorn@8.14.0) esbuild: 0.24.0 sharp: 0.33.5 terser: 5.36.0 @@ -9411,25 +9463,6 @@ snapshots: - supports-color - terser - vite-node@2.1.5(@types/node@22.9.0)(terser@5.36.0): - dependencies: - cac: 6.7.14 - debug: 4.3.7 - es-module-lexer: 1.5.4 - pathe: 1.1.2 - vite: 5.4.11(@types/node@22.9.0)(terser@5.36.0) - transitivePeerDependencies: - - '@types/node' - - less - - lightningcss - - sass - - sass-embedded - - stylus - - sugarss - - supports-color - - terser - optional: true - vite@5.4.11(@types/node@20.17.6)(terser@5.36.0): dependencies: esbuild: 0.21.5 @@ -9440,25 +9473,14 @@ snapshots: fsevents: 2.3.3 terser: 5.36.0 - vite@5.4.11(@types/node@22.9.0)(terser@5.36.0): - dependencies: - esbuild: 0.21.5 - postcss: 8.4.49 - rollup: 4.26.0 - optionalDependencies: - '@types/node': 22.9.0 - fsevents: 2.3.3 - terser: 5.36.0 - optional: true - vitefu@1.0.2(vite@5.4.11(@types/node@20.17.6)(terser@5.36.0)): optionalDependencies: vite: 5.4.11(@types/node@20.17.6)(terser@5.36.0) - vitest@2.1.5(@types/node@20.17.6)(jsdom@24.1.3)(terser@5.36.0): + vitest@2.1.5: dependencies: '@vitest/expect': 2.1.5 - '@vitest/mocker': 2.1.5(vite@5.4.11(@types/node@20.17.6)(terser@5.36.0)) + '@vitest/mocker': 2.1.5(vite@5.4.11) '@vitest/pretty-format': 2.1.5 '@vitest/runner': 2.1.5 '@vitest/snapshot': 2.1.5 @@ -9477,9 +9499,6 @@ snapshots: vite: 5.4.11(@types/node@20.17.6)(terser@5.36.0) vite-node: 2.1.5(@types/node@20.17.6)(terser@5.36.0) why-is-node-running: 2.3.0 - optionalDependencies: - '@types/node': 20.17.6 - jsdom: 24.1.3 transitivePeerDependencies: - less - lightningcss @@ -9490,11 +9509,12 @@ snapshots: - sugarss - supports-color - terser + optional: true - vitest@2.1.5(@types/node@22.9.0)(jsdom@24.1.3)(terser@5.36.0): + vitest@2.1.5(@types/node@20.17.6)(@vitest/ui@2.1.8)(jsdom@24.1.3)(terser@5.36.0): dependencies: '@vitest/expect': 2.1.5 - '@vitest/mocker': 2.1.5(vite@5.4.11(@types/node@22.9.0)(terser@5.36.0)) + '@vitest/mocker': 2.1.5(vite@5.4.11) '@vitest/pretty-format': 2.1.5 '@vitest/runner': 2.1.5 '@vitest/snapshot': 2.1.5 @@ -9510,11 +9530,12 @@ snapshots: tinyexec: 0.3.1 tinypool: 1.0.2 tinyrainbow: 1.2.0 - vite: 5.4.11(@types/node@22.9.0)(terser@5.36.0) - vite-node: 2.1.5(@types/node@22.9.0)(terser@5.36.0) + vite: 5.4.11(@types/node@20.17.6)(terser@5.36.0) + vite-node: 2.1.5(@types/node@20.17.6)(terser@5.36.0) why-is-node-running: 2.3.0 optionalDependencies: - '@types/node': 22.9.0 + '@types/node': 20.17.6 + '@vitest/ui': 2.1.8(vitest@2.1.5) jsdom: 24.1.3 transitivePeerDependencies: - less @@ -9526,7 +9547,6 @@ snapshots: - sugarss - supports-color - terser - optional: true vue-eslint-parser@9.4.3(eslint@9.14.0(jiti@1.21.6)): dependencies: