diff --git a/src/lib/__tests__/utils.js b/src/lib/__tests__/utils.js index f0750f9..116c650 100644 --- a/src/lib/__tests__/utils.js +++ b/src/lib/__tests__/utils.js @@ -35,3 +35,9 @@ export function initializeAlpine() { export function createMockCustomEventListener() { return vi.fn((e) => [e.target, e.detail]); } + +export const scopes = { + CUSTOM_EVENTS: "custom events", + KEYBOARD_NAVIGATION: "keyboard navigation", + JAVASCRIPT_METHODS: "javascript methods", +}; diff --git a/src/lib/__tests__/x-accordion.test.js b/src/lib/__tests__/x-accordion.test.js index f25a224..07c5bfe 100644 --- a/src/lib/__tests__/x-accordion.test.js +++ b/src/lib/__tests__/x-accordion.test.js @@ -1,7 +1,12 @@ import { expect, describe, beforeAll, beforeEach, test } from "vitest"; import { fireEvent, screen, waitFor } from "@testing-library/dom"; -import { createMockCustomEventListener, html, initializeAlpine } from "./utils"; +import { + createMockCustomEventListener, + html, + initializeAlpine, + scopes, +} from "./utils"; describe("x-accordion", () => { beforeAll(initializeAlpine); @@ -85,7 +90,7 @@ describe("x-accordion", () => { }); }); - describe("custom events", () => { + describe(scopes.CUSTOM_EVENTS, () => { test("should indicate item is open", async () => { const item = screen.getByTestId("item-1"); const trigger = screen.getByTestId("trigger-1"); @@ -132,7 +137,7 @@ describe("x-accordion", () => { }); }); - describe("keyboard navigation", () => { + describe(scopes.KEYBOARD_NAVIGATION, () => { let trigger1, trigger2; beforeEach(() => { @@ -194,6 +199,42 @@ describe("x-accordion", () => { }); }); }); + + describe(scopes.JAVASCRIPT_METHODS, () => { + test("(item.toolbelt.toggle) should open and close an item", async () => { + const item = screen.getByTestId("item-1"); + const trigger = screen.getByTestId("trigger-1"); + const content = screen.getByTestId("content-1"); + + item.toolbelt.toggle(); + + await waitFor(() => { + expectItemToBeOpen({ item, trigger, content }, true); + item.toolbelt.toggle(); + }); + + await waitFor(() => { + expectItemToBeOpen({ item, trigger, content }, false); + }); + }); + + test("(item.toolbelt.toggle) should open and close an item when override is given", async () => { + const item = screen.getByTestId("item-1"); + const trigger = screen.getByTestId("trigger-1"); + const content = screen.getByTestId("content-1"); + + item.toolbelt.toggle(true); + + await waitFor(() => { + expectItemToBeOpen({ item, trigger, content }, true); + item.toolbelt.toggle(false); + }); + + await waitFor(() => { + expectItemToBeOpen({ item, trigger, content }, false); + }); + }); + }); }); describe("(x-accordion.single) single item only configuration", () => { @@ -253,7 +294,7 @@ describe("x-accordion", () => { }); }); - describe("custom events", () => { + describe(scopes.CUSTOM_EVENTS, () => { test("should trigger separate events for opened and closed items.", async () => { const item1 = screen.getByTestId("item-1"); const trigger1 = screen.getByTestId("trigger-1"); @@ -311,7 +352,7 @@ describe("x-accordion", () => { `; }); - describe("keyboard navigation", () => { + describe(scopes.KEYBOARD_NAVIGATION, () => { let trigger1, trigger2; beforeEach(() => { @@ -356,7 +397,7 @@ describe("x-accordion", () => { `; }); - describe("keyboard navigation", () => { + describe(scopes.KEYBOARD_NAVIGATION, () => { let trigger1, trigger2; beforeEach(() => { diff --git a/src/lib/__tests__/x-checkbox.test.js b/src/lib/__tests__/x-checkbox.test.js index 01a258b..45ffae1 100644 --- a/src/lib/__tests__/x-checkbox.test.js +++ b/src/lib/__tests__/x-checkbox.test.js @@ -1,7 +1,12 @@ import { expect, describe, beforeAll, beforeEach, test } from "vitest"; import { fireEvent, screen, waitFor } from "@testing-library/dom"; -import { createMockCustomEventListener, html, initializeAlpine } from "./utils"; +import { + createMockCustomEventListener, + html, + initializeAlpine, + scopes, +} from "./utils"; describe("x-checkbox", () => { beforeAll(initializeAlpine); @@ -97,7 +102,7 @@ describe("x-checkbox", () => { }); }); - describe("custom events", () => { + describe(scopes.CUSTOM_EVENTS, () => { test("should indicate checkbox is open", async () => { const root = screen.getByTestId("root"); const indicator = screen.getByTestId("indicator"); diff --git a/src/lib/__tests__/x-dialog.test.js b/src/lib/__tests__/x-dialog.test.js index e25031a..c93a6c7 100644 --- a/src/lib/__tests__/x-dialog.test.js +++ b/src/lib/__tests__/x-dialog.test.js @@ -1,7 +1,12 @@ import { expect, describe, beforeAll, beforeEach, test } from "vitest"; import { fireEvent, screen, waitFor } from "@testing-library/dom"; -import { createMockCustomEventListener, html, initializeAlpine } from "./utils"; +import { + createMockCustomEventListener, + html, + initializeAlpine, + scopes, +} from "./utils"; describe("x-dialog", () => { beforeAll(initializeAlpine); @@ -117,7 +122,7 @@ describe("x-dialog", () => { }); }); - describe("custom events", () => { + describe(scopes.CUSTOM_EVENTS, () => { test("should indicate dialog is open", async () => { const listener = createMockCustomEventListener(); diff --git a/src/lib/__tests__/x-flyout.test.js b/src/lib/__tests__/x-flyout.test.js index 4812eab..7accfc2 100644 --- a/src/lib/__tests__/x-flyout.test.js +++ b/src/lib/__tests__/x-flyout.test.js @@ -1,7 +1,12 @@ import { expect, describe, beforeAll, beforeEach, test } from "vitest"; import { fireEvent, screen, waitFor } from "@testing-library/dom"; -import { createMockCustomEventListener, html, initializeAlpine } from "./utils"; +import { + createMockCustomEventListener, + html, + initializeAlpine, + scopes, +} from "./utils"; describe("x-flyout", () => { beforeAll(initializeAlpine); @@ -90,7 +95,7 @@ describe("x-flyout", () => { }); }); - describe("custom events", () => { + describe(scopes.CUSTOM_EVENTS, () => { test("should indicate flyout is open", async () => { const listener = createMockCustomEventListener(); diff --git a/src/lib/__tests__/x-tabs.test.js b/src/lib/__tests__/x-tabs.test.js index c644fa8..3d4d172 100644 --- a/src/lib/__tests__/x-tabs.test.js +++ b/src/lib/__tests__/x-tabs.test.js @@ -1,7 +1,12 @@ import { expect, describe, beforeAll, beforeEach, test } from "vitest"; import { fireEvent, screen, waitFor } from "@testing-library/dom"; -import { createMockCustomEventListener, html, initializeAlpine } from "./utils"; +import { + createMockCustomEventListener, + html, + initializeAlpine, + scopes, +} from "./utils"; describe("x-tabs", () => { beforeAll(initializeAlpine); @@ -78,7 +83,7 @@ describe("x-tabs", () => { }); }); - describe("custom events", () => { + describe(scopes.CUSTOM_EVENTS, () => { test("should indicate tab is open", async () => { const listener = createMockCustomEventListener(); @@ -108,7 +113,7 @@ describe("x-tabs", () => { }); }); - describe("keyboard navigation", () => { + describe(scopes.KEYBOARD_NAVIGATION, () => { test("pressing right arrow should move focus to the next tab", async () => { fireEvent.keyDown(tab1, { key: "ArrowRight" }); @@ -179,7 +184,7 @@ describe("x-tabs", () => { tab2 = screen.getByTestId("tab2"); }); - describe("keyboard navigation", () => { + describe(scopes.KEYBOARD_NAVIGATION, () => { test("pressing right arrow on the last tab should loop", async () => { fireEvent.keyDown(tab2, { key: "ArrowRight" }); @@ -220,7 +225,7 @@ describe("x-tabs", () => { panel2 = screen.getByTestId("panel2"); }); - describe("keyboard navigation", () => { + describe(scopes.KEYBOARD_NAVIGATION, () => { test("pressing right arrow should automatically open the next tab", async () => { fireEvent.keyDown(tab1, { key: "ArrowRight" }); @@ -281,7 +286,7 @@ describe("x-tabs", () => { panel2 = screen.getByTestId("panel2"); }); - describe("keyboard navigation", () => { + describe(scopes.KEYBOARD_NAVIGATION, () => { test("pressing down arrow should move focus to the next tab", async () => { fireEvent.keyDown(tab1, { key: "ArrowDown" }); diff --git a/src/lib/toolbelt/x-accordion.js b/src/lib/toolbelt/x-accordion.js index 3c28c0f..f435cb0 100644 --- a/src/lib/toolbelt/x-accordion.js +++ b/src/lib/toolbelt/x-accordion.js @@ -43,15 +43,18 @@ function handleRoot(el, Alpine, config) { triggers: [], openItems: new Set(), - toggleItem(el) { - if (config.type === "single" && this.openItems.has(el)) { + toggleItem(el, open) { + const shouldOpen = + open === undefined ? !this.openItems.has(el) : open; + + if (config.type === "single" && !shouldOpen) { this.openItems.delete(el); - } else if (config.type === "single" && !this.openItems.has(el)) { + } else if (config.type === "single" && shouldOpen) { this.openItems.clear(); this.openItems.add(el); - } else if (config.type === "multiple" && this.openItems.has(el)) { + } else if (config.type === "multiple" && !shouldOpen) { this.openItems.delete(el); - } else if (config.type === "multiple" && !this.openItems.has(el)) { + } else if (config.type === "multiple" && shouldOpen) { this.openItems.add(el); } @@ -101,6 +104,12 @@ function handleItem(el, Alpine, config) { if (config.open) { this.openItems.add(el); } + + el.toolbelt = { + toggle: (open) => { + this.toggleItem(this.__item, open); + }, + }; }, "x-id"() {