@featherds/dock
Advanced tools
| import { describe, it, beforeEach, afterEach, expect, vi } from "vitest"; | ||
| import { mount } from "@vue/test-utils"; | ||
| import { nextTick } from "vue"; | ||
| import FeatherDock from "./FeatherDock.vue"; | ||
| describe("FeatherDock", () => { | ||
| let wrapper: any = null; | ||
| beforeEach(() => { | ||
| wrapper = mount(FeatherDock as unknown as any, { | ||
| props: { | ||
| id: "test-dock", | ||
| modelValue: false, | ||
| }, | ||
| }); | ||
| }); | ||
| afterEach(() => { | ||
| vi.restoreAllMocks(); | ||
| vi.clearAllMocks(); | ||
| if (wrapper) wrapper.unmount(); | ||
| wrapper = null; | ||
| }); | ||
| it('reflects the open property to the "open" attribute (via aria-expanded/class)', async () => { | ||
| if (!wrapper) throw new Error("wrapper not mounted"); | ||
| // set open -> toggle button aria-expanded should be "true" and root should have dock-open class | ||
| await wrapper.setProps({ modelValue: true }); | ||
| await nextTick(); | ||
| const toggle = wrapper.find('[data-ref-id="feather-dock-toggle"]'); | ||
| expect(toggle.exists()).toBe(true); | ||
| expect(toggle.attributes("aria-expanded")).toBe("true"); | ||
| expect(wrapper.classes()).toContain("dock-open"); | ||
| // unset open -> aria-expanded should be "false" and class removed | ||
| await wrapper.setProps({ modelValue: false }); | ||
| await nextTick(); | ||
| expect(toggle.attributes("aria-expanded")).toBe("false"); | ||
| expect(wrapper.classes()).not.toContain("dock-open"); | ||
| }); | ||
| it("emits update events with the new state when toggled", async () => { | ||
| if (!wrapper) throw new Error("wrapper not mounted"); | ||
| const toggle = wrapper.find('[data-ref-id="feather-dock-toggle"]'); | ||
| expect(toggle.exists()).toBe(true); | ||
| // ensure starting state | ||
| await wrapper.setProps({ modelValue: false }); | ||
| await nextTick(); | ||
| // click the toggle button | ||
| await toggle.trigger("click"); | ||
| await nextTick(); | ||
| // component should emit update:modelValue with true and update:dock-expanded | ||
| expect(wrapper.emitted("update:modelValue")?.[0]?.[0]).toBe(true); | ||
| expect(wrapper.emitted("update:dock-expanded")).toBeTruthy(); | ||
| // aria and classes should reflect new state | ||
| expect(toggle.attributes("aria-expanded")).toBe("true"); | ||
| expect(wrapper.classes()).toContain("dock-open"); | ||
| }); | ||
| it("updates internal state when external prop changes without emitting update events", async () => { | ||
| if (!wrapper) throw new Error("wrapper not mounted"); | ||
| // Ensure no emits initially | ||
| expect(wrapper.emitted("update:modelValue")).toBeUndefined(); | ||
| // Change the prop externally and expect the component to reflect the change | ||
| await wrapper.setProps({ modelValue: true }); | ||
| await nextTick(); | ||
| const toggle = wrapper.find('[data-ref-id="feather-dock-toggle"]'); | ||
| expect(toggle.attributes("aria-expanded")).toBe("true"); | ||
| // Changing the prop should not cause the component to emit an update back to the parent | ||
| expect(wrapper.emitted("update:modelValue")).toBeUndefined(); | ||
| }); | ||
| it("computes and exposes expanded/collapsed widths and dockWidth reacts to modelValue", async () => { | ||
| // mount with explicit width props | ||
| const w = mount(FeatherDock as unknown as any, { | ||
| props: { | ||
| id: "width-dock", | ||
| modelValue: true, | ||
| expandedWidth: "420px", | ||
| collapsedWidth: "48px", | ||
| }, | ||
| }); | ||
| // expandedWidthPx / collapsedWidthPx should reflect the px values | ||
| expect(w.vm.expandedWidthPx).toBe("420px"); | ||
| expect(w.vm.collapsedWidthPx).toBe("48px"); | ||
| // dockWidth should reflect expandedWidth when modelValue=true | ||
| expect(w.vm.dockWidth).toBe("420px"); | ||
| // collapse and assert dockWidth changes | ||
| await w.setProps({ modelValue: false }); | ||
| await nextTick(); | ||
| expect(w.vm.dockWidth).toBe("48px"); | ||
| w.unmount(); | ||
| }); | ||
| it("closes on Escape key and focuses the toggle button", async () => { | ||
| // mount open | ||
| const w = mount(FeatherDock as unknown as any, { | ||
| attachTo: document.body, | ||
| props: { id: "kbd-dock", modelValue: true }, | ||
| }); | ||
| // Wait for DOM to be updated and for the component to mount into document | ||
| await nextTick(); | ||
| // The component's handler queries the document by id, so spy the actual DOM element | ||
| const docToggleEl = document.querySelector( | ||
| `#kbd-dock .feather-dock-toggle` | ||
| ) as HTMLElement | null; | ||
| expect(docToggleEl).toBeTruthy(); | ||
| const focusSpy = vi.fn(); | ||
| if (docToggleEl) docToggleEl.focus = focusSpy as unknown as () => void; | ||
| // dispatch Escape keydown on document | ||
| document.dispatchEvent(new KeyboardEvent("keydown", { key: "Escape" })); | ||
| await nextTick(); | ||
| // component should emit closing events | ||
| expect(w.emitted("update:modelValue")?.[0]?.[0]).toBe(false); | ||
| expect(w.emitted("update:dock-collapsed")).toBeTruthy(); | ||
| // focus should have been called on the toggle button | ||
| expect(focusSpy).toHaveBeenCalled(); | ||
| w.unmount(); | ||
| }); | ||
| }); |
+8
-4
@@ -29,4 +29,4 @@ import { defineComponent, useCssVars, computed, ref, watch, provide, inject, readonly, onMounted, onUnmounted, createBlock, openBlock, resolveDynamicComponent, normalizeClass, withCtx, createVNode, createElementVNode, unref, renderSlot, createTextVNode } from "vue"; | ||
| useCssVars((_ctx) => ({ | ||
| "12c90f18": dockWidth.value, | ||
| "7e7aacd6": dockConfig.value.isOpen ? "cubic-bezier(0, 0.8, 0.4, 1)" : "cubic-beziercubic-bezier(0, 0.8, 0.4, 1)" | ||
| "13ea2f12": dockWidth.value, | ||
| "f3bd3be2": dockConfig.value.isOpen ? "cubic-bezier(0, 0.8, 0.4, 1)" : "cubic-beziercubic-bezier(0, 0.8, 0.4, 1)" | ||
| })); | ||
@@ -36,2 +36,3 @@ const props = __props; | ||
| const dockContentRef = ref(void 0); | ||
| const toggleHovering = ref(false); | ||
| const isDockOpen = ref(props.modelValue); | ||
@@ -165,2 +166,3 @@ const pushedSelectorPadding = ref(""); | ||
| provide("dockConfig", readonly(dockConfig)); | ||
| provide("toggleHovering", readonly(toggleHovering)); | ||
| onMounted(() => { | ||
@@ -218,2 +220,4 @@ if (props.pushedSelector) { | ||
| onClick: toggleDock, | ||
| onMouseenter: _cache[0] || (_cache[0] = ($event) => toggleHovering.value = true), | ||
| onMouseleave: _cache[1] || (_cache[1] = ($event) => toggleHovering.value = false), | ||
| "aria-expanded": dockConfig.value.isOpen, | ||
@@ -240,3 +244,3 @@ "aria-label": dockConfig.value.isOpen ? _ctx.labels.collapse : _ctx.labels.expand, | ||
| renderSlot(_ctx.$slots, "docked", {}, () => [ | ||
| _cache[0] || (_cache[0] = createElementVNode("div", { class: "custom-content" }, [ | ||
| _cache[2] || (_cache[2] = createElementVNode("div", { class: "custom-content" }, [ | ||
| createElementVNode("h2", null, "Custom Dock Content"), | ||
@@ -268,5 +272,5 @@ createElementVNode("p", null, 'Add your content here using the "docked" slot.'), | ||
| }; | ||
| const FeatherDock = /* @__PURE__ */ _export_sfc(_sfc_main, [["__scopeId", "data-v-c6b62f8b"]]); | ||
| const FeatherDock = /* @__PURE__ */ _export_sfc(_sfc_main, [["__scopeId", "data-v-4407d75f"]]); | ||
| export { | ||
| FeatherDock | ||
| }; |
+20
-20
@@ -31,5 +31,5 @@ | ||
| } | ||
| .feather-dock[data-v-c6b62f8b] { | ||
| --feather-dock-width: var(--12c90f18); | ||
| --feather-dock-toggle-timing-fn: var(--7e7aacd6); | ||
| .feather-dock[data-v-4407d75f] { | ||
| --feather-dock-width: var(--13ea2f12); | ||
| --feather-dock-toggle-timing-fn: var(--f3bd3be2); | ||
| position: fixed; | ||
@@ -52,6 +52,6 @@ inset: var(--feather-dock-header-offset) 0 0 0; | ||
| } | ||
| .feather-dock.right[data-v-c6b62f8b] { | ||
| .feather-dock.right[data-v-4407d75f] { | ||
| inset: 0 0 0 auto; | ||
| } | ||
| .feather-dock.right .feather-dock-toggle[data-v-c6b62f8b] { | ||
| .feather-dock.right .feather-dock-toggle[data-v-4407d75f] { | ||
| transform: rotate(180deg); | ||
@@ -63,7 +63,7 @@ right: calc(var(--feather-dock-width) - 1rem); | ||
| } | ||
| .feather-dock.dock-closed[data-v-c6b62f8b] { | ||
| .feather-dock.dock-closed[data-v-4407d75f] { | ||
| scrollbar-width: none; | ||
| scrollbar-color: transparent transparent; | ||
| } | ||
| .feather-dock.dock-closed > .feather-dock-toggle[data-v-c6b62f8b] { | ||
| .feather-dock.dock-closed > .feather-dock-toggle[data-v-4407d75f] { | ||
| position: absolute; | ||
@@ -73,6 +73,6 @@ left: calc(var(--feather-dock-width) / 2 - 1.5rem); | ||
| } | ||
| .feather-dock.dock-closed > .feather-dock-toggle[data-v-c6b62f8b] { | ||
| .feather-dock.dock-closed > .feather-dock-toggle[data-v-4407d75f] { | ||
| top: var(--feather-dock-toggle-top); | ||
| } | ||
| .feather-dock > .feather-dock-toggle[data-v-c6b62f8b] { | ||
| .feather-dock > .feather-dock-toggle[data-v-4407d75f] { | ||
| position: fixed; | ||
@@ -91,29 +91,29 @@ top: calc(var(--feather-dock-toggle-top) + var(--feather-dock-header-offset)); | ||
| } | ||
| .feather-dock > .feather-dock-toggle .ripple[data-v-c6b62f8b] { | ||
| .feather-dock > .feather-dock-toggle .ripple[data-v-4407d75f] { | ||
| background-color: var(--feather-state-color-on-neutral); | ||
| opacity: var(--feather-state-opacity-pressed-on-neutral); | ||
| } | ||
| .feather-dock > .feather-dock-toggle.selected[data-v-c6b62f8b], | ||
| .feather-dock > .feather-dock-toggle .selected[data-v-c6b62f8b] { | ||
| .feather-dock > .feather-dock-toggle.selected[data-v-4407d75f], | ||
| .feather-dock > .feather-dock-toggle .selected[data-v-4407d75f] { | ||
| background: linear-gradient(rgba(var(--feather-state-color-on-neutral-r), var(--feather-state-color-on-neutral-g), var(--feather-state-color-on-neutral-b), var(--feather-state-opacity-selected-on-neutral)), rgba(var(--feather-state-color-on-neutral-r), var(--feather-state-color-on-neutral-g), var(--feather-state-color-on-neutral-b), var(--feather-state-opacity-selected-on-neutral))), linear-gradient(rgba(0, 0, 0, 0), rgba(0, 0, 0, 0)); | ||
| } | ||
| .feather-dock > .feather-dock-toggle.hover[data-v-c6b62f8b]:hover, .feather-dock > .feather-dock-toggle:hover .hover[data-v-c6b62f8b] { | ||
| .feather-dock > .feather-dock-toggle.hover[data-v-4407d75f]:hover, .feather-dock > .feather-dock-toggle:hover .hover[data-v-4407d75f] { | ||
| background: linear-gradient(rgba(var(--feather-state-color-on-neutral-r), var(--feather-state-color-on-neutral-g), var(--feather-state-color-on-neutral-b), var(--feather-state-opacity-hover-on-neutral)), rgba(var(--feather-state-color-on-neutral-r), var(--feather-state-color-on-neutral-g), var(--feather-state-color-on-neutral-b), var(--feather-state-opacity-hover-on-neutral))), linear-gradient(rgba(0, 0, 0, 0), rgba(0, 0, 0, 0)); | ||
| } | ||
| .feather-dock > .feather-dock-toggle.hover:hover.selected[data-v-c6b62f8b], .feather-dock > .feather-dock-toggle:hover .hover.selected[data-v-c6b62f8b] { | ||
| .feather-dock > .feather-dock-toggle.hover:hover.selected[data-v-4407d75f], .feather-dock > .feather-dock-toggle:hover .hover.selected[data-v-4407d75f] { | ||
| background: linear-gradient(rgba(var(--feather-state-color-on-neutral-r), var(--feather-state-color-on-neutral-g), var(--feather-state-color-on-neutral-b), var(--feather-state-opacity-selected-on-neutral)), rgba(var(--feather-state-color-on-neutral-r), var(--feather-state-color-on-neutral-g), var(--feather-state-color-on-neutral-b), var(--feather-state-opacity-selected-on-neutral))), linear-gradient(rgba(var(--feather-state-color-on-neutral-r), var(--feather-state-color-on-neutral-g), var(--feather-state-color-on-neutral-b), var(--feather-state-opacity-hover-on-neutral)), rgba(var(--feather-state-color-on-neutral-r), var(--feather-state-color-on-neutral-g), var(--feather-state-color-on-neutral-b), var(--feather-state-opacity-hover-on-neutral))), linear-gradient(rgba(0, 0, 0, 0), rgba(0, 0, 0, 0)); | ||
| } | ||
| .feather-dock > .feather-dock-toggle:focus.focus[data-v-c6b62f8b], .feather-dock > .feather-dock-toggle:focus .focus[data-v-c6b62f8b], .feather-dock > .feather-dock-toggle.focused.focus[data-v-c6b62f8b], .feather-dock > .feather-dock-toggle.focused .focus[data-v-c6b62f8b] { | ||
| .feather-dock > .feather-dock-toggle:focus.focus[data-v-4407d75f], .feather-dock > .feather-dock-toggle:focus .focus[data-v-4407d75f], .feather-dock > .feather-dock-toggle.focused.focus[data-v-4407d75f], .feather-dock > .feather-dock-toggle.focused .focus[data-v-4407d75f] { | ||
| background: linear-gradient(rgba(var(--feather-state-color-on-neutral-r), var(--feather-state-color-on-neutral-g), var(--feather-state-color-on-neutral-b), var(--feather-state-opacity-focus-on-neutral)), rgba(var(--feather-state-color-on-neutral-r), var(--feather-state-color-on-neutral-g), var(--feather-state-color-on-neutral-b), var(--feather-state-opacity-focus-on-neutral))), linear-gradient(rgba(0, 0, 0, 0), rgba(0, 0, 0, 0)); | ||
| } | ||
| .feather-dock > .feather-dock-toggle:focus.focus.selected[data-v-c6b62f8b], .feather-dock > .feather-dock-toggle:focus .focus.selected[data-v-c6b62f8b], .feather-dock > .feather-dock-toggle.focused.focus.selected[data-v-c6b62f8b], .feather-dock > .feather-dock-toggle.focused .focus.selected[data-v-c6b62f8b] { | ||
| .feather-dock > .feather-dock-toggle:focus.focus.selected[data-v-4407d75f], .feather-dock > .feather-dock-toggle:focus .focus.selected[data-v-4407d75f], .feather-dock > .feather-dock-toggle.focused.focus.selected[data-v-4407d75f], .feather-dock > .feather-dock-toggle.focused .focus.selected[data-v-4407d75f] { | ||
| background: linear-gradient(rgba(var(--feather-state-color-on-neutral-r), var(--feather-state-color-on-neutral-g), var(--feather-state-color-on-neutral-b), var(--feather-state-opacity-selected-on-neutral)), rgba(var(--feather-state-color-on-neutral-r), var(--feather-state-color-on-neutral-g), var(--feather-state-color-on-neutral-b), var(--feather-state-opacity-selected-on-neutral))), linear-gradient(rgba(var(--feather-state-color-on-neutral-r), var(--feather-state-color-on-neutral-g), var(--feather-state-color-on-neutral-b), var(--feather-state-opacity-focus-on-neutral)), rgba(var(--feather-state-color-on-neutral-r), var(--feather-state-color-on-neutral-g), var(--feather-state-color-on-neutral-b), var(--feather-state-opacity-focus-on-neutral))), linear-gradient(rgba(0, 0, 0, 0), rgba(0, 0, 0, 0)); | ||
| } | ||
| .feather-dock > .feather-dock-toggle:hover:focus .focus.hover[data-v-c6b62f8b], .feather-dock > .feather-dock-toggle:hover:focus.focus.hover[data-v-c6b62f8b], .feather-dock > .feather-dock-toggle:hover.focused .focus.hover[data-v-c6b62f8b], .feather-dock > .feather-dock-toggle:hover.focused.focus.hover[data-v-c6b62f8b] { | ||
| .feather-dock > .feather-dock-toggle:hover:focus .focus.hover[data-v-4407d75f], .feather-dock > .feather-dock-toggle:hover:focus.focus.hover[data-v-4407d75f], .feather-dock > .feather-dock-toggle:hover.focused .focus.hover[data-v-4407d75f], .feather-dock > .feather-dock-toggle:hover.focused.focus.hover[data-v-4407d75f] { | ||
| background: linear-gradient(rgba(var(--feather-state-color-on-neutral-r), var(--feather-state-color-on-neutral-g), var(--feather-state-color-on-neutral-b), var(--feather-state-opacity-hover-on-neutral)), rgba(var(--feather-state-color-on-neutral-r), var(--feather-state-color-on-neutral-g), var(--feather-state-color-on-neutral-b), var(--feather-state-opacity-hover-on-neutral))), linear-gradient(rgba(var(--feather-state-color-on-neutral-r), var(--feather-state-color-on-neutral-g), var(--feather-state-color-on-neutral-b), var(--feather-state-opacity-focus-on-neutral)), rgba(var(--feather-state-color-on-neutral-r), var(--feather-state-color-on-neutral-g), var(--feather-state-color-on-neutral-b), var(--feather-state-opacity-focus-on-neutral))), linear-gradient(rgba(0, 0, 0, 0), rgba(0, 0, 0, 0)); | ||
| } | ||
| .feather-dock > .feather-dock-toggle:hover:focus .focus.hover.selected[data-v-c6b62f8b], .feather-dock > .feather-dock-toggle:hover:focus.focus.hover.selected[data-v-c6b62f8b], .feather-dock > .feather-dock-toggle:hover.focused .focus.hover.selected[data-v-c6b62f8b], .feather-dock > .feather-dock-toggle:hover.focused.focus.hover.selected[data-v-c6b62f8b] { | ||
| .feather-dock > .feather-dock-toggle:hover:focus .focus.hover.selected[data-v-4407d75f], .feather-dock > .feather-dock-toggle:hover:focus.focus.hover.selected[data-v-4407d75f], .feather-dock > .feather-dock-toggle:hover.focused .focus.hover.selected[data-v-4407d75f], .feather-dock > .feather-dock-toggle:hover.focused.focus.hover.selected[data-v-4407d75f] { | ||
| background: linear-gradient(rgba(var(--feather-state-color-on-neutral-r), var(--feather-state-color-on-neutral-g), var(--feather-state-color-on-neutral-b), var(--feather-state-opacity-selected-on-neutral)), rgba(var(--feather-state-color-on-neutral-r), var(--feather-state-color-on-neutral-g), var(--feather-state-color-on-neutral-b), var(--feather-state-opacity-selected-on-neutral))), linear-gradient(rgba(var(--feather-state-color-on-neutral-r), var(--feather-state-color-on-neutral-g), var(--feather-state-color-on-neutral-b), var(--feather-state-opacity-hover-on-neutral)), rgba(var(--feather-state-color-on-neutral-r), var(--feather-state-color-on-neutral-g), var(--feather-state-color-on-neutral-b), var(--feather-state-opacity-hover-on-neutral))), linear-gradient(rgba(var(--feather-state-color-on-neutral-r), var(--feather-state-color-on-neutral-g), var(--feather-state-color-on-neutral-b), var(--feather-state-opacity-focus-on-neutral)), rgba(var(--feather-state-color-on-neutral-r), var(--feather-state-color-on-neutral-g), var(--feather-state-color-on-neutral-b), var(--feather-state-opacity-focus-on-neutral))), linear-gradient(rgba(0, 0, 0, 0), rgba(0, 0, 0, 0)); | ||
| } | ||
| .feather-dock > .feather-dock-content[data-v-c6b62f8b] { | ||
| .feather-dock > .feather-dock-content[data-v-4407d75f] { | ||
| position: relative; | ||
@@ -129,5 +129,5 @@ height: 100%; | ||
| } | ||
| .feather-dock > .feather-dock-content .custom-content[data-v-c6b62f8b] { | ||
| .feather-dock > .feather-dock-content .custom-content[data-v-4407d75f] { | ||
| text-align: center; | ||
| padding: 1rem; | ||
| } |
+4
-4
| { | ||
| "name": "@featherds/dock", | ||
| "version": "0.12.41", | ||
| "version": "0.12.42", | ||
| "publishConfig": { | ||
@@ -12,4 +12,4 @@ "access": "public" | ||
| "dependencies": { | ||
| "@featherds/icon": "^0.12.41", | ||
| "@featherds/styles": "^0.12.41", | ||
| "@featherds/icon": "^0.12.42", | ||
| "@featherds/styles": "^0.12.42", | ||
| "vue": "^3.5.13" | ||
@@ -22,3 +22,3 @@ }, | ||
| "types": "./src/index.d.ts", | ||
| "gitHead": "339b2892b574bcb01022eb9c7edde9a86895fa4d" | ||
| "gitHead": "e807ef35c30f4d9ef4a08ac173dfd50e3f2004d7" | ||
| } |
@@ -10,2 +10,4 @@ <template> | ||
| @click="toggleDock" | ||
| @mouseenter="toggleHovering = true" | ||
| @mouseleave="toggleHovering = false" | ||
| :aria-expanded="dockConfig.isOpen" | ||
@@ -82,2 +84,4 @@ :aria-label="dockConfig.isOpen ? labels.collapse : labels.expand" | ||
| const toggleHovering = ref(false); | ||
| // use composable's ref as source of truth; its .value is a Ref<boolean> | ||
@@ -284,2 +288,3 @@ const isDockOpen = ref<boolean>(props.modelValue); | ||
| provide("dockConfig", readonly(dockConfig)); //readonly | ||
| provide("toggleHovering", readonly(toggleHovering)); | ||
@@ -286,0 +291,0 @@ onMounted(() => { |
53611
11.01%10
11.11%567
25.44%Updated
Updated