srcdev-nuxt-components 9.2.9 → 9.2.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/settings.json +14 -0
- package/.claude/skills/components/content-docs.md +16 -1
- package/.claude/skills/components/expanding-panel.md +9 -1
- package/app/assets/styles/setup/01.config/_head.css +5 -7
- package/app/components/01.atoms/content-wrappers/docs-pages/ContentDocs.vue +16 -21
- package/app/components/01.atoms/content-wrappers/docs-pages/tests/ContentDocs.spec.ts +32 -0
- package/app/components/02.molecules/expandable/expanding-panel/ExpandingPanel.vue +16 -4
- package/app/components/02.molecules/expandable/expanding-panel/tests/ExpandingPanel.spec.ts +47 -0
- package/package.json +1 -1
package/.claude/settings.json
CHANGED
|
@@ -106,5 +106,19 @@
|
|
|
106
106
|
"/Users/simoncornforth/websites/nuxt-components/.claude/skills/components",
|
|
107
107
|
"/Users/simoncornforth/websites/nuxt-components/.claude/skills"
|
|
108
108
|
]
|
|
109
|
+
},
|
|
110
|
+
"hooks": {
|
|
111
|
+
"PostToolUse": [
|
|
112
|
+
{
|
|
113
|
+
"matcher": "Write|Edit",
|
|
114
|
+
"hooks": [
|
|
115
|
+
{
|
|
116
|
+
"type": "command",
|
|
117
|
+
"command": "f=$(jq -r '.tool_input.file_path // empty'); if [[ \"$f\" == */app/components/*.vue && \"$f\" != */tests/* && \"$f\" != */stories/* ]]; then name=$(basename \"$f\" .vue); skill=$(printf '%s' \"$name\" | sed -E 's/([a-z0-9])([A-Z])/\\1-\\2/g; s/([A-Z]+)([A-Z][a-z])/\\1-\\2/g' | tr '[:upper:]' '[:lower:]'); jq -n --arg msg \"Component file $f was edited/written. Per Claude.md Development Workflow step 7, check whether .claude/skills/components/$skill.md needs updating to reflect this change (props/slots/models/defaults/behaviour).\" '{hookSpecificOutput:{hookEventName:\"PostToolUse\",additionalContext:$msg}}'; fi",
|
|
118
|
+
"timeout": 15
|
|
119
|
+
}
|
|
120
|
+
]
|
|
121
|
+
}
|
|
122
|
+
]
|
|
109
123
|
}
|
|
110
124
|
}
|
|
@@ -36,7 +36,7 @@ interface DocsNavItem {
|
|
|
36
36
|
| `v-model:activeNavItem` | `string \| undefined` | `undefined` | The `to` of the currently-active `docsNav` item. Updates automatically on click; bind externally (e.g. to route matching) to control it. |
|
|
37
37
|
| `v-model:activePageNavItem` | `string \| undefined` | `undefined` | Same, for `docsPageNav`. |
|
|
38
38
|
|
|
39
|
-
Open/expanded state of the two panels is **not** exposed as a model — it's driven entirely by the container-width breakpoint logic (see below), not user-controllable.
|
|
39
|
+
Open/expanded state of the two panels is **not** exposed as a model to `ContentDocs`' own consumers — externally it's driven entirely by the container-width breakpoint logic (see below), not user-controllable. Internally, each panel is still a controlled `ExpandingPanel` (`v-model`, not left uncontrolled) so it can be closed programmatically — see "Mobile overlay dismissal" below.
|
|
40
40
|
|
|
41
41
|
---
|
|
42
42
|
|
|
@@ -93,6 +93,21 @@ Widths are measured on the component's own root element (`useContainerBreakpoint
|
|
|
93
93
|
| 768–1023px (tablet) | collapsible, closed by default | **forced open**, no toggle icon |
|
|
94
94
|
| ≥ 1024px (desktop) | **forced open**, no toggle icon | **forced open**, no toggle icon |
|
|
95
95
|
|
|
96
|
+
Below 1024px (i.e. whenever a panel isn't `forceOpened`), it's also `contentIsOnTop` — an overlay, not in-flow — since a collapsible nav panel pushing page content down/up as it opens reads as janky on mobile/tablet. See "Mobile overlay dismissal" below for how these are closed.
|
|
97
|
+
|
|
98
|
+
### Mobile overlay dismissal
|
|
99
|
+
|
|
100
|
+
Below 1024px, `docsNav`/`docsPageNav` are `contentIsOnTop` `ExpandingPanel`s (see the breakpoint table above), so they need an explicit way to close again beyond just toggling the summary:
|
|
101
|
+
|
|
102
|
+
- **Click outside** — inherited for free from `ExpandingPanel`'s own `contentIsOnTop` behaviour (see `expanding-panel.md`), no extra wiring in `ContentDocs` itself.
|
|
103
|
+
- **Clicking a nav link inside the open panel** — `ContentDocs` does not leave either `ExpandingPanel` uncontrolled: it holds its own `docsNavPanelOpen`/`docsPageNavPanelOpen` refs, bound via `v-model`, and each nav link's click handler sets its panel's ref to `false` in addition to updating `activeNavItem`/`activePageNavItem`. Without this, selecting a link would leave the overlay open on top of the page it just navigated to.
|
|
104
|
+
|
|
105
|
+
Both are no-ops at desktop/tablet where a panel is `forceOpened` — `open` there is `forceOpened || isPanelOpen`, so `forceOpened` wins regardless of what these refs are set to.
|
|
106
|
+
|
|
107
|
+
### DOM order vs visual order (accessibility)
|
|
108
|
+
|
|
109
|
+
In the template, `docsContent` is placed **before** `docsNav`/`docsPageNav` — the reverse of their left-to-right visual position at desktop. This is deliberate: `docsNav`/`docsPageNav` each render an `<h3>` heading (`docsNavLabel`/`docsPageNavLabel`), and if they came first in the DOM, a screen-reader user navigating by heading would hit one of those h3s *before* the consuming page's own `h1` inside `docsContent` — a broken heading outline. Visual position is unaffected because layout comes from `grid-template-areas` (see the `<style>` block), not source order. The trade-off: default Tab order now reaches main content before the side nav — an accepted consequence, not a regression to "fix" back by reverting the order.
|
|
110
|
+
|
|
96
111
|
### Why the two panels share a `name` only on mobile
|
|
97
112
|
|
|
98
113
|
Both panels are `ExpandingPanel`s using the native `<details name="...">` grouping feature, which makes same-named panels mutually exclusive (browser force-closes one when the other opens). That's the wanted behaviour on mobile (accordion — only one open at a time), but at tablet/desktop both panels must be open **simultaneously** — a shared name there would make the browser silently force-close one of them the moment both try to be open. So the component computes distinct names (`"docsNav"` / `"docsPageNav"`) once past mobile, and a shared name (`"docsPanelGroup"`) only while mobile.
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
| `name` | `string` | `useId()` | Identifies the panel. Used in ARIA attributes (`id-{name}-trigger`, `id-{name}-content`). If omitted, a unique id is generated automatically. |
|
|
14
14
|
| `animationDuration` | `number` | `400` | Expand/collapse transition duration in milliseconds. Pass `0` to disable animation. |
|
|
15
15
|
| `forceOpened` | `boolean` | `false` | When `true`, the panel is always open. The toggle icon is hidden and clicks do not close the panel. |
|
|
16
|
-
| `contentIsOnTop` | `boolean` | `false` | When `true`, the content region is taken out of flow and absolutely positioned directly below the summary, raised above surrounding page content via `z-index` — instead of pushing layout down when it opens. Applies a `content-is-on-top` class to the root `.expanding-panel` element (not the content div). |
|
|
16
|
+
| `contentIsOnTop` | `boolean` | `false` | When `true`, the content region is taken out of flow and absolutely positioned directly below the summary, raised above surrounding page content via `z-index` — instead of pushing layout down when it opens. Applies a `content-is-on-top` class to the root `.expanding-panel` element (not the content div). Also enables click-outside-to-close (see below) — not applied when `forceOpened` is `true`. |
|
|
17
17
|
| `styleClassPassthrough` | `string \| string[]` | `[]` | Extra CSS classes applied to the root `.expanding-panel` element. |
|
|
18
18
|
|
|
19
19
|
## Model
|
|
@@ -157,6 +157,14 @@ Why: `.expanding-panel-content` collapses via `grid-template-rows: 0fr → 1fr`,
|
|
|
157
157
|
|
|
158
158
|
---
|
|
159
159
|
|
|
160
|
+
## Click-outside-to-close (contentIsOnTop only)
|
|
161
|
+
|
|
162
|
+
When `contentIsOnTop` is `true` and the panel is open, clicking anywhere outside the panel's root element closes it (via `@vueuse/core`'s `onClickOutside`, sets `v-model` to `false`) — matching the dismissal behaviour of a native `<select>` or dropdown menu, since that's what an overlay panel functionally is. This does **not** apply to ordinary in-flow panels (`contentIsOnTop: false`, the default) — an inline accordion staying open when you click elsewhere on the page is expected, not a bug. It also never applies when `forceOpened` is `true`, regardless of `contentIsOnTop` — a forced-open panel isn't dismissible by any interaction.
|
|
163
|
+
|
|
164
|
+
If you also want the panel to close when something *inside* it is activated (e.g. a nav link) — clicking outside doesn't cover that case — set `v-model` to `false` from that element's own click handler, same as any other controlled usage. See `ContentDocs`' `docsNav`/`docsPageNav` panels for a real example: both are `contentIsOnTop` on mobile, and each nav link closes its panel on click in addition to relying on click-outside.
|
|
165
|
+
|
|
166
|
+
---
|
|
167
|
+
|
|
160
168
|
## CSS Token Customization
|
|
161
169
|
|
|
162
170
|
All `--expanding-panel-*` tokens can be overridden at global, page, or instance scope. See `CONSUMER-STYLING.md` in the component directory for full token documentation and examples.
|
|
@@ -24,7 +24,6 @@ html {
|
|
|
24
24
|
color: var(--colour-text-default);
|
|
25
25
|
font-family: var(--font-family);
|
|
26
26
|
font-size: var(--step-4);
|
|
27
|
-
/* min-height: 100dvh; */
|
|
28
27
|
transition:
|
|
29
28
|
background-color 0.4s ease,
|
|
30
29
|
color 0.4s ease;
|
|
@@ -33,12 +32,11 @@ html {
|
|
|
33
32
|
|
|
34
33
|
#__nuxt {
|
|
35
34
|
height: 100%;
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
}
|
|
35
|
+
.page-layout {
|
|
36
|
+
min-block-size: 100svh;
|
|
37
|
+
display: grid;
|
|
38
|
+
grid-template-rows: auto 1fr auto;
|
|
39
|
+
align-items: start;
|
|
42
40
|
}
|
|
43
41
|
}
|
|
44
42
|
}
|
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<component :is="tag" ref="rootEl" class="content-docs" :class="[elementClasses]">
|
|
3
3
|
<div class="content-docs-inner">
|
|
4
|
+
<div v-if="hasDocsContent" class="docs-content">
|
|
5
|
+
<slot name="docsContent"></slot>
|
|
6
|
+
</div>
|
|
4
7
|
<div v-if="hasDocsNav" class="docs-nav">
|
|
5
8
|
<ExpandingPanel
|
|
9
|
+
v-model="docsNavPanelOpen"
|
|
6
10
|
:name="docsNavPanelName"
|
|
7
11
|
:animation-duration="200"
|
|
8
12
|
:force-opened="docsNavForceOpen"
|
|
@@ -21,7 +25,10 @@
|
|
|
21
25
|
class="docs-nav-link"
|
|
22
26
|
:class="{ 'is-active': item.to === activeNavItem }"
|
|
23
27
|
:aria-current="item.to === activeNavItem ? 'page' : undefined"
|
|
24
|
-
@click="
|
|
28
|
+
@click="
|
|
29
|
+
activeNavItem = item.to;
|
|
30
|
+
docsNavPanelOpen = false;
|
|
31
|
+
"
|
|
25
32
|
>
|
|
26
33
|
<Icon v-if="item.icon" :name="item.icon" class="docs-nav-link-icon" aria-hidden="true" />
|
|
27
34
|
<span class="docs-nav-link-label">{{ item.label }}</span>
|
|
@@ -32,11 +39,9 @@
|
|
|
32
39
|
</template>
|
|
33
40
|
</ExpandingPanel>
|
|
34
41
|
</div>
|
|
35
|
-
<div v-if="hasDocsContent" class="docs-content">
|
|
36
|
-
<slot name="docsContent"></slot>
|
|
37
|
-
</div>
|
|
38
42
|
<div v-if="hasDocsPageNav" class="docs-page-nav">
|
|
39
43
|
<ExpandingPanel
|
|
44
|
+
v-model="docsPageNavPanelOpen"
|
|
40
45
|
:name="docsPageNavPanelName"
|
|
41
46
|
:animation-duration="200"
|
|
42
47
|
:force-opened="docsPageNavForceOpen"
|
|
@@ -55,7 +60,10 @@
|
|
|
55
60
|
class="docs-page-nav-link"
|
|
56
61
|
:class="{ 'is-active': item.to === activePageNavItem }"
|
|
57
62
|
:aria-current="item.to === activePageNavItem ? 'page' : undefined"
|
|
58
|
-
@click="
|
|
63
|
+
@click="
|
|
64
|
+
activePageNavItem = item.to;
|
|
65
|
+
docsPageNavPanelOpen = false;
|
|
66
|
+
"
|
|
59
67
|
>
|
|
60
68
|
<Icon v-if="item.icon" :name="item.icon" class="docs-page-nav-link-icon" aria-hidden="true" />
|
|
61
69
|
<span class="docs-page-nav-link-label">{{ item.label }}</span>
|
|
@@ -121,9 +129,9 @@ const docsNavOnTop = computed(() => !isDesktop.value);
|
|
|
121
129
|
const docsPageNavForceOpen = computed(() => !isMobile.value);
|
|
122
130
|
const docsPageNavOnTop = computed(() => isMobile.value);
|
|
123
131
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
132
|
+
const docsNavPanelOpen = ref(false);
|
|
133
|
+
const docsPageNavPanelOpen = ref(false);
|
|
134
|
+
|
|
127
135
|
const panelGroupId = useId();
|
|
128
136
|
const docsNavPanelName = computed(() => (isMobile.value ? panelGroupId : `${panelGroupId}-docsNav`));
|
|
129
137
|
const docsPageNavPanelName = computed(() => (isMobile.value ? panelGroupId : `${panelGroupId}-docsPageNav`));
|
|
@@ -132,7 +140,6 @@ const docsPageNavPanelName = computed(() => (isMobile.value ? panelGroupId : `${
|
|
|
132
140
|
<style lang="css">
|
|
133
141
|
@layer components {
|
|
134
142
|
.content-docs {
|
|
135
|
-
/* Shared defaults — theme both docsNav and docsPageNav at once */
|
|
136
143
|
--_heading-font-size: var(--content-docs-heading-font-size, 1.4rem);
|
|
137
144
|
--_heading-font-weight: var(--content-docs-heading-font-weight, 700);
|
|
138
145
|
--_heading-color: var(--content-docs-heading-color, light-dark(var(--slate-10), var(--slate-01)));
|
|
@@ -146,8 +153,6 @@ const docsPageNavPanelName = computed(() => (isMobile.value ? panelGroupId : `${
|
|
|
146
153
|
--_panel-padding-inline: var(--content-docs-panel-padding-inline, 0.8rem);
|
|
147
154
|
--_panel-border-radius: var(--content-docs-panel-border-radius, 0.5rem);
|
|
148
155
|
|
|
149
|
-
/* Fixed-width grid tracks — the docsNav/docsPageNav columns at tablet/desktop.
|
|
150
|
-
docsPageNav gets a narrower track at tablet since docsNav is full-width there. */
|
|
151
156
|
--_nav-column-width: var(--content-docs-nav-column-width, 23rem);
|
|
152
157
|
--_page-nav-column-width: var(--content-docs-page-nav-column-width, 22rem);
|
|
153
158
|
--_page-nav-column-width-tablet: var(--content-docs-page-nav-column-width-tablet, 20rem);
|
|
@@ -164,7 +169,6 @@ const docsPageNavPanelName = computed(() => (isMobile.value ? panelGroupId : `${
|
|
|
164
169
|
--_link-active-bg: var(--content-docs-link-active-bg, light-dark(var(--green-01), var(--green-09)));
|
|
165
170
|
--_link-active-color: var(--content-docs-link-active-color, light-dark(var(--green-10), var(--green-04)));
|
|
166
171
|
|
|
167
|
-
/* Per-side overrides — each falls back to the shared token above */
|
|
168
172
|
--_nav-heading-font-size: var(--content-docs-nav-heading-font-size, var(--_heading-font-size));
|
|
169
173
|
--_nav-heading-font-weight: var(--content-docs-nav-heading-font-weight, var(--_heading-font-weight));
|
|
170
174
|
--_nav-heading-color: var(--content-docs-nav-heading-color, var(--_heading-color));
|
|
@@ -313,15 +317,6 @@ const docsPageNavPanelName = computed(() => (isMobile.value ? panelGroupId : `${
|
|
|
313
317
|
}
|
|
314
318
|
}
|
|
315
319
|
|
|
316
|
-
/* Grid (not flex) so the label column stays aligned at a fixed offset whether or
|
|
317
|
-
not an item has an icon — an icon-less item's label still starts in column 2,
|
|
318
|
-
instead of collapsing back to column 1 like it would with flex.
|
|
319
|
-
|
|
320
|
-
Icon at the end: set --docs-nav-link-icon-order to rtl. This mirrors which
|
|
321
|
-
physical side each grid column renders on without touching column sizing —
|
|
322
|
-
swapping grid-column values directly would leave the label squeezed into the
|
|
323
|
-
icon-sized track instead. The label/icon direction resets further down undo
|
|
324
|
-
the mirroring for their own content so text and icon glyphs don't visually flip. */
|
|
325
320
|
.docs-nav-link {
|
|
326
321
|
display: grid;
|
|
327
322
|
grid-template-columns: var(--docs-nav-link-icon-size, 1.6rem) 1fr;
|
|
@@ -74,6 +74,16 @@ describe("ContentDocs", () => {
|
|
|
74
74
|
expect(wrapper.find('[data-testid="body"]').text()).toBe("Body");
|
|
75
75
|
});
|
|
76
76
|
|
|
77
|
+
it("places docsContent before docsNav and docsPageNav in DOM order, so a consumer's h1 is reachable before either nav panel's h3 heading (visual position is unaffected — grid-template-areas places all three, not source order)", async () => {
|
|
78
|
+
const wrapper = await mountSuspended(ContentDocs, {
|
|
79
|
+
props: { docsNavItems: navItems, docsPageNavItems: pageNavItems },
|
|
80
|
+
slots: { docsContent: "<h1>Title</h1>" },
|
|
81
|
+
});
|
|
82
|
+
const inner = wrapper.find(".content-docs-inner").element;
|
|
83
|
+
const children = [...inner.children].map((el) => el.className);
|
|
84
|
+
expect(children).toEqual(["docs-content", "docs-nav", "docs-page-nav"]);
|
|
85
|
+
});
|
|
86
|
+
|
|
77
87
|
// ─── labels ─────────────────────────────────────────────────────────────
|
|
78
88
|
|
|
79
89
|
it("defaults docsNavLabel to 'Navigation' and docsPageNavLabel to 'On this page'", async () => {
|
|
@@ -142,6 +152,28 @@ describe("ContentDocs", () => {
|
|
|
142
152
|
expect(wrapper.emitted("update:activePageNavItem")?.[0]).toEqual(["/one#overview"]);
|
|
143
153
|
});
|
|
144
154
|
|
|
155
|
+
// ─── closing on link click (mobile overlay behaviour) ─────────────────────
|
|
156
|
+
|
|
157
|
+
it("closes the docsNav panel when a link inside it is clicked", async () => {
|
|
158
|
+
const wrapper = await mountSuspended(ContentDocs, { props: { docsNavItems: navItems } });
|
|
159
|
+
|
|
160
|
+
await wrapper.find(".docs-nav summary").trigger("click");
|
|
161
|
+
expect(wrapper.find(".docs-nav details").attributes("open")).toBeDefined();
|
|
162
|
+
|
|
163
|
+
await wrapper.find(".docs-nav").findAll("a")[0]?.trigger("click");
|
|
164
|
+
expect(wrapper.find(".docs-nav details").attributes("open")).toBeUndefined();
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it("closes the docsPageNav panel when a link inside it is clicked", async () => {
|
|
168
|
+
const wrapper = await mountSuspended(ContentDocs, { props: { docsPageNavItems: pageNavItems } });
|
|
169
|
+
|
|
170
|
+
await wrapper.find(".docs-page-nav summary").trigger("click");
|
|
171
|
+
expect(wrapper.find(".docs-page-nav details").attributes("open")).toBeDefined();
|
|
172
|
+
|
|
173
|
+
await wrapper.find(".docs-page-nav").find("a").trigger("click");
|
|
174
|
+
expect(wrapper.find(".docs-page-nav details").attributes("open")).toBeUndefined();
|
|
175
|
+
});
|
|
176
|
+
|
|
145
177
|
// ─── container-width-driven forceOpened ────────────────────────────────────
|
|
146
178
|
|
|
147
179
|
describe("breakpoint-driven forceOpened", () => {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<div class="expanding-panel" :class="[elementClasses, { 'content-is-on-top': contentIsOnTop }]">
|
|
2
|
+
<div ref="rootEl" class="expanding-panel" :class="[elementClasses, { 'content-is-on-top': contentIsOnTop }]">
|
|
3
3
|
<details class="expanding-panel-details" :name :open @toggle="onDetailsToggle">
|
|
4
4
|
<summary
|
|
5
5
|
:id="`id-${name}-trigger`"
|
|
@@ -32,6 +32,8 @@
|
|
|
32
32
|
</template>
|
|
33
33
|
|
|
34
34
|
<script setup lang="ts">
|
|
35
|
+
import { onClickOutside } from "@vueuse/core";
|
|
36
|
+
|
|
35
37
|
interface Props {
|
|
36
38
|
name?: string;
|
|
37
39
|
animationDuration?: number;
|
|
@@ -56,6 +58,17 @@ const open = computed(() => props.forceOpened || isPanelOpen.value);
|
|
|
56
58
|
|
|
57
59
|
const { elementClasses } = useStyleClassPassthrough(props.styleClassPassthrough);
|
|
58
60
|
|
|
61
|
+
// contentIsOnTop panels behave like a dropdown/overlay (e.g. ContentDocs' mobile nav) rather
|
|
62
|
+
// than an inline accordion — clicking outside is the expected way to dismiss those, matching
|
|
63
|
+
// native <select>/menu behaviour. Not wanted for ordinary accordions (forceOpened is always
|
|
64
|
+
// false here too — a forceOpened panel isn't meant to be dismissible by any interaction).
|
|
65
|
+
const rootEl = ref<HTMLElement | null>(null);
|
|
66
|
+
onClickOutside(rootEl, () => {
|
|
67
|
+
if (props.contentIsOnTop && !props.forceOpened && isPanelOpen.value) {
|
|
68
|
+
isPanelOpen.value = false;
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
|
|
59
72
|
if (import.meta.dev) {
|
|
60
73
|
watch(
|
|
61
74
|
() => props.contentIsOnTop,
|
|
@@ -97,10 +110,9 @@ const onDetailsToggle = (event: Event) => {
|
|
|
97
110
|
cursor: pointer;
|
|
98
111
|
}
|
|
99
112
|
.expanding-panel-summary {
|
|
100
|
-
display:
|
|
113
|
+
display: grid;
|
|
101
114
|
align-items: center;
|
|
102
|
-
|
|
103
|
-
flex-direction: row;
|
|
115
|
+
grid-template-columns: 1fr auto;
|
|
104
116
|
gap: var(--expanding-panel-summary-gap, 1rem);
|
|
105
117
|
list-style: none;
|
|
106
118
|
user-select: none;
|
|
@@ -427,6 +427,53 @@ describe("ExpandingPanel", () => {
|
|
|
427
427
|
expect(vm.isPanelOpen).toBe(true);
|
|
428
428
|
});
|
|
429
429
|
|
|
430
|
+
it("closes an open contentIsOnTop panel on outside click", async () => {
|
|
431
|
+
const wrapper = await mountSuspended(ExpandingPanel, {
|
|
432
|
+
props: { name: "content-on-top-outside-click", contentIsOnTop: true },
|
|
433
|
+
attachTo: document.body,
|
|
434
|
+
});
|
|
435
|
+
const vm = wrapper.vm as unknown as ExpandingPanelInstance;
|
|
436
|
+
|
|
437
|
+
await wrapper.find("summary").trigger("click");
|
|
438
|
+
expect(vm.isPanelOpen).toBe(true);
|
|
439
|
+
|
|
440
|
+
document.body.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
|
441
|
+
await nextTick();
|
|
442
|
+
|
|
443
|
+
expect(vm.isPanelOpen).toBe(false);
|
|
444
|
+
wrapper.unmount();
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
it("does not close on outside click when contentIsOnTop is false (ordinary accordion)", async () => {
|
|
448
|
+
const wrapper = await mountSuspended(ExpandingPanel, {
|
|
449
|
+
props: { name: "content-on-top-outside-click-inline" },
|
|
450
|
+
attachTo: document.body,
|
|
451
|
+
});
|
|
452
|
+
const vm = wrapper.vm as unknown as ExpandingPanelInstance;
|
|
453
|
+
|
|
454
|
+
await wrapper.find("summary").trigger("click");
|
|
455
|
+
expect(vm.isPanelOpen).toBe(true);
|
|
456
|
+
|
|
457
|
+
document.body.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
|
458
|
+
await nextTick();
|
|
459
|
+
|
|
460
|
+
expect(vm.isPanelOpen).toBe(true);
|
|
461
|
+
wrapper.unmount();
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
it("does not close a forceOpened contentIsOnTop panel on outside click", async () => {
|
|
465
|
+
const wrapper = await mountSuspended(ExpandingPanel, {
|
|
466
|
+
props: { name: "content-on-top-outside-click-forced", contentIsOnTop: true, forceOpened: true },
|
|
467
|
+
attachTo: document.body,
|
|
468
|
+
});
|
|
469
|
+
|
|
470
|
+
document.body.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
|
471
|
+
await nextTick();
|
|
472
|
+
|
|
473
|
+
expect(wrapper.find("details").attributes("open")).toBeDefined();
|
|
474
|
+
wrapper.unmount();
|
|
475
|
+
});
|
|
476
|
+
|
|
430
477
|
// ─── name prop / useId fallback ───────────────────────────────────────────
|
|
431
478
|
|
|
432
479
|
it("uses the provided name in ARIA attributes", async () => {
|