srcdev-nuxt-components 9.3.6 → 9.3.7
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/skills/components/accordian-core.md +7 -7
- package/.claude/skills/components/content-docs.md +1 -0
- package/.claude/skills/components/expanding-panel.md +5 -5
- package/.claude/skills/components/navigation-items.md +1 -0
- package/.claude/skills/components/responsive-header.md +1 -0
- package/app/components/01.atoms/content-wrappers/docs-pages/ContentDocs.vue +22 -16
- package/app/components/02.molecules/expandable/accordian/AccordianCore.vue +5 -1
- package/app/components/02.molecules/expandable/accordian/tests/AccordianCore.spec.ts +22 -14
- package/app/components/02.molecules/expandable/accordian/tests/__snapshots__/AccordianCore.spec.ts.snap +42 -28
- package/app/components/02.molecules/expandable/expanding-panel/CONSUMER-STYLING.md +8 -10
- package/app/components/02.molecules/expandable/expanding-panel/ExpandingPanel.vue +15 -47
- package/app/components/02.molecules/expandable/expanding-panel/tests/__snapshots__/ExpandingPanel.spec.ts.snap +4 -12
- package/app/components/03.organisms/responsive-header/NavigationItems.vue +91 -4
- package/app/components/03.organisms/responsive-header/ResponsiveHeader.vue +5 -1
- package/app/components/03.organisms/responsive-header/tests/NavigationItems.spec.ts +2 -2
- package/app/components/03.organisms/responsive-header/tests/__snapshots__/NavigationItems.spec.ts.snap +13 -13
- package/app/components/03.organisms/responsive-header/tests/__snapshots__/ResponsiveHeader.spec.ts.snap +13 -13
- package/app/components/03.organisms/services/services-section/ServicesSection.vue +19 -0
- package/app/components/03.organisms/services/services-section/tests/__snapshots__/ServicesSection.spec.ts.snap +8 -8
- package/package.json +1 -1
|
@@ -35,7 +35,7 @@ For `itemCount="3"` the following slots exist:
|
|
|
35
35
|
| `name` | `string` | `undefined` | Shared `name` passed to every `ExpandingPanel`. When set, native `<details>` grouping means only one panel can be open at a time. Omit for independent panels. |
|
|
36
36
|
| `itemCount` | `number` | `0` | Number of `ExpandingPanel` components to render. |
|
|
37
37
|
| `animationDuration` | `number` | `300` | Expand/collapse animation duration in ms, forwarded to every panel. |
|
|
38
|
-
| `variant` | `"modern" \| "classic"` | `"
|
|
38
|
+
| `variant` | `"modern" \| "classic"` | `"classic"` | `"classic"` renders each panel as `ExpandingPanelClassic` (`grid-template-rows`-based, animates identically in every browser). `"modern"` renders `ExpandingPanel` instead (`::details-content`-based) — **known not to work correctly on WebKit (Safari/iOS)**, confirmed via real-device testing (2026-08); the animation silently fails and, worse, `contentIsOnTop` positioning never applies there. `"modern"` is opt-in for consumers who've confirmed it works for their audience, not the safe default. Applies to every panel in the group — mixing variants within one `AccordianCore` isn't supported. See [expanding-panel-classic.md](expanding-panel-classic.md) and CLAUDE.md pitfall #19. |
|
|
39
39
|
| `styleClassPassthrough` | `string \| string[]` | `[]` | Extra CSS classes applied to the root `.display-accordian` element. |
|
|
40
40
|
|
|
41
41
|
---
|
|
@@ -91,10 +91,10 @@ Passing `name="faq"` groups all panels so only one can be open at a time.
|
|
|
91
91
|
</AccordianCore>
|
|
92
92
|
```
|
|
93
93
|
|
|
94
|
-
###
|
|
94
|
+
### Modern variant (opt-in, not WebKit-safe)
|
|
95
95
|
|
|
96
96
|
```vue
|
|
97
|
-
<AccordianCore name="faq" :item-count="2" variant="
|
|
97
|
+
<AccordianCore name="faq" :item-count="2" variant="modern">
|
|
98
98
|
<template #accordian-0-summary><span>Question one?</span></template>
|
|
99
99
|
<template #accordian-0-content><p>Answer one.</p></template>
|
|
100
100
|
|
|
@@ -154,10 +154,10 @@ See [component-local-style-override.md](../component-local-style-override.md) fo
|
|
|
154
154
|
/* Geometry */
|
|
155
155
|
/* max-width: none; */ /* default is 600px — remove the width cap */
|
|
156
156
|
|
|
157
|
-
/* Panel-level overrides via the .accordian-item hook — .expanding-panel is
|
|
158
|
-
root class rendered by the default "
|
|
159
|
-
|
|
160
|
-
.accordian-item.expanding-panel {
|
|
157
|
+
/* Panel-level overrides via the .accordian-item hook — .expanding-panel-classic is
|
|
158
|
+
the root class rendered by the default "classic" variant; swap in .expanding-panel
|
|
159
|
+
if this instance uses variant="modern" */
|
|
160
|
+
.accordian-item.expanding-panel-classic {
|
|
161
161
|
/* Border */
|
|
162
162
|
/* border-block-end: 1px solid currentColor; */
|
|
163
163
|
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
| `docsPageNavItems` | `DocsNavItem[]` | `[]` | Items rendered in the `docsPageNav` panel. Panel omitted entirely when empty. |
|
|
18
18
|
| `docsNavLabel` | `string` | `"Navigation"` | Heading text for the `docsNav` panel's `#summary`. |
|
|
19
19
|
| `docsPageNavLabel` | `string` | `"On this page"` | Heading text for the `docsPageNav` panel's `#summary`. |
|
|
20
|
+
| `panelVariant` | `"modern" \| "classic"` | `"classic"` | Which panel component renders `docsNav`/`docsPageNav`: `ExpandingPanel` (`"modern"`) or `ExpandingPanelClassic` (`"classic"`, default). `"modern"` is known not to work correctly on WebKit — the mobile nav (which relies on `contentIsOnTop`) becomes unreachable there. See CLAUDE.md pitfall #19. |
|
|
20
21
|
| `styleClassPassthrough` | `string \| string[]` | `[]` | Extra CSS classes applied to the root `.content-docs` element. |
|
|
21
22
|
|
|
22
23
|
`DocsNavItem` (from `~/types/components`):
|
|
@@ -4,9 +4,9 @@
|
|
|
4
4
|
|
|
5
5
|
`ExpandingPanel` is a single expand/collapse panel built on the native `<details>`/`<summary>` element. It animates open/close by styling the `::details-content` pseudo-element (the browser's own anonymous box wrapping everything after `<summary>`) where supported, supports `v-model` for controlled state, and can be locked open with `forceOpened`. Multiple panels can be grouped into a native accordion by sharing the same `name` prop (see `AccordianCore`).
|
|
6
6
|
|
|
7
|
-
**WebKit (Safari/iOS) does not support `::details-content`** as of 2026-08, confirmed via real-device testing — Chrome's device-emulation mode cannot catch this, since it's still the Blink engine underneath regardless of which browser UI it's simulating. WebKit silently drops the whole rule block: no
|
|
7
|
+
**WebKit (Safari/iOS) does not support `::details-content`** as of 2026-08, confirmed via real-device testing — Chrome's device-emulation mode cannot catch this, since it's still the Blink engine underneath regardless of which browser UI it's simulating. WebKit silently drops the whole rule block: no animation and (more seriously) `content-is-on-top`'s `position: absolute` never applying, so an overlay panel renders in normal document flow and can break the surrounding layout. An `@supports not selector(::details-content)` CSS-only fallback was tried (2026-08-22) but didn't reliably kick in on real devices either and was reverted the same day — **do not reintroduce that approach**.
|
|
8
8
|
|
|
9
|
-
`
|
|
9
|
+
**The actual fix is component-level, not CSS-level**: every consumer that gave `ExpandingPanel` a choice of implementation now defaults to `ExpandingPanelClassic` instead — see `AccordianCore`'s `variant` prop (default `"classic"`), `ContentDocs`'s `panelVariant` prop (default `"classic"`), and `ResponsiveHeader`/`NavigationItems`'s `panelVariant` prop (default `"classic"`). `ExpandingPanel` itself is unchanged (still `::details-content`-only, no fallback) and logs a `console.warn` in dev mode on every mount warning about this. Reach for `ExpandingPanel` directly, or pass `variant`/`panelVariant: "modern"` to one of the above, only once you've confirmed it's acceptable for your actual audience — it is **not** the safe default anywhere in this library anymore. See pitfall #19 in the repo's `CLAUDE.md` for the full history.
|
|
10
10
|
|
|
11
11
|
---
|
|
12
12
|
|
|
@@ -145,9 +145,9 @@ Always supply a meaningful `name` prop when using multiple panels on the same pa
|
|
|
145
145
|
|
|
146
146
|
## Styling the content when contentIsOnTop
|
|
147
147
|
|
|
148
|
-
`background-color`, `padding`, and a shadow can be set directly on `.expanding-panel-content` (or on a wrapper inside the `#content` slot — either works)
|
|
148
|
+
`background-color`, `padding`, and a shadow can be set directly on `.expanding-panel-content` (or on a wrapper inside the `#content` slot — either works): the clipping boundary is `::details-content` itself (`overflow: clip`, animated `height`), and `.expanding-panel-content` is a *child* of that box, so its own box is correctly clipped regardless of where the styling lives.
|
|
149
149
|
|
|
150
|
-
|
|
150
|
+
If this needs to work correctly on WebKit too, use `ExpandingPanelClassic` instead (see the Overview section above) — its equivalent guidance lives in its own `CONSUMER-STYLING.md`, and its `.expanding-panel-classic-content > .inner` structure differs slightly.
|
|
151
151
|
|
|
152
152
|
---
|
|
153
153
|
|
|
@@ -213,7 +213,7 @@ See [component-local-style-override.md](../component-local-style-override.md) fo
|
|
|
213
213
|
|
|
214
214
|
## Notes
|
|
215
215
|
|
|
216
|
-
- The open/close animation targets `::details-content` (`height: 0 → auto`, `overflow: clip`)
|
|
216
|
+
- The open/close animation targets `::details-content` (`height: 0 → auto`, `overflow: clip`) — no JS height measurement needed. Requires `interpolate-size: allow-keywords` for the height transition itself (Chromium only as of 2026; other browsers with `::details-content` support still toggle correctly, just instantly). WebKit has neither, and there is no automatic fallback — use `ExpandingPanelClassic` (see the Overview section above) for anything that needs to work correctly there.
|
|
217
217
|
- `content-is-on-top` is applied to the root `.expanding-panel` element, not `.expanding-panel-content` — style overrides must scope through it, e.g. `.expanding-panel.my-panel .expanding-panel-content { ... }`.
|
|
218
218
|
- When `forceOpened` is `true`, `open` stays `true` regardless of `v-model`, but `v-model` still updates internally on clicks (useful if you later set `forceOpened` back to `false`).
|
|
219
219
|
- Group panels into a native accordion (only one open at a time) by passing the same `name` to multiple panels or use `AccordianCore` which handles this automatically.
|
|
@@ -16,6 +16,7 @@ active-route highlighting, hover indicator).
|
|
|
16
16
|
| Prop | Type | Default | Notes |
|
|
17
17
|
|------|------|---------|-------|
|
|
18
18
|
| `mainNavigationState` | `ResponsiveHeaderState` | `{ clonedNavLinks: {}, navListVisibility: {}, hasSecondNav: false }` | Shared geometry/visibility state, normally supplied by the parent `ResponsiveHeader` — not something a consumer constructs by hand outside of tests/stories. |
|
|
19
|
+
| `panelVariant` | `"modern" \| "classic"` | `"classic"` | Which panel component renders a dropdown's submenu: `ExpandingPanel` (`"modern"`) or `ExpandingPanelClassic` (`"classic"`, default). `"modern"` is known not to work correctly on WebKit. Normally set via `ResponsiveHeader`'s own `panelVariant` prop rather than directly. See CLAUDE.md pitfall #19. |
|
|
19
20
|
| `styleClassPassthrough` | `string \| string[]` | `[]` | Extra CSS classes applied to the root `.overflow-navigation-wrapper` element. |
|
|
20
21
|
|
|
21
22
|
---
|
|
@@ -106,6 +106,7 @@ buggy toggle-based version too.
|
|
|
106
106
|
| `collapseBreakpoint` | `number \| null` | `null` | A fixed pixel width below which the whole main nav collapses into the overflow burger, instead of the default per-item responsive collapse. |
|
|
107
107
|
| `collapseAtMainNavIntersection` | `boolean` | `false` | Like `collapseBreakpoint`, but the breakpoint is derived automatically from the main nav's own measured width rather than a fixed number. |
|
|
108
108
|
| `allowExpandOnGesture` | `boolean` | `true` | When `true`, hovering/focusing a dropdown summary opens it (in addition to click). When `false`, only click toggles it. |
|
|
109
|
+
| `panelVariant` | `"modern" \| "classic"` | `"classic"` | Forwarded to `NavigationItems` for its dropdown submenu panels: `ExpandingPanel` (`"modern"`) or `ExpandingPanelClassic` (`"classic"`, default). `"modern"` is known not to work correctly on WebKit. See CLAUDE.md pitfall #19. |
|
|
109
110
|
| `styleClassPassthrough` | `string \| string[]` | `[]` | Extra CSS classes applied to the root `.navigation` element. |
|
|
110
111
|
|
|
111
112
|
## Slots
|
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
<slot name="docsContent"></slot>
|
|
6
6
|
</div>
|
|
7
7
|
<div v-if="hasDocsNav" class="docs-nav">
|
|
8
|
-
<
|
|
8
|
+
<component
|
|
9
|
+
:is="panelComponent"
|
|
9
10
|
v-model="docsNavPanelOpen"
|
|
10
11
|
:name="docsNavPanelName"
|
|
11
12
|
:animation-duration="200"
|
|
@@ -37,10 +38,11 @@
|
|
|
37
38
|
</ul>
|
|
38
39
|
</nav>
|
|
39
40
|
</template>
|
|
40
|
-
</
|
|
41
|
+
</component>
|
|
41
42
|
</div>
|
|
42
43
|
<div v-if="hasDocsPageNav" class="docs-page-nav">
|
|
43
|
-
<
|
|
44
|
+
<component
|
|
45
|
+
:is="panelComponent"
|
|
44
46
|
v-model="docsPageNavPanelOpen"
|
|
45
47
|
:name="docsPageNavPanelName"
|
|
46
48
|
:animation-duration="200"
|
|
@@ -72,7 +74,7 @@
|
|
|
72
74
|
</ul>
|
|
73
75
|
</nav>
|
|
74
76
|
</template>
|
|
75
|
-
</
|
|
77
|
+
</component>
|
|
76
78
|
</div>
|
|
77
79
|
</div>
|
|
78
80
|
</component>
|
|
@@ -80,6 +82,8 @@
|
|
|
80
82
|
|
|
81
83
|
<script setup lang="ts">
|
|
82
84
|
import type { DocsNavItem } from "~/types/components";
|
|
85
|
+
import ExpandingPanel from "~/components/02.molecules/expandable/expanding-panel/ExpandingPanel.vue";
|
|
86
|
+
import ExpandingPanelClassic from "~/components/02.molecules/expandable/expanding-panel-classic/ExpandingPanelClassic.vue";
|
|
83
87
|
|
|
84
88
|
interface Props {
|
|
85
89
|
tag?: "div" | "section" | "article" | "main";
|
|
@@ -87,6 +91,7 @@ interface Props {
|
|
|
87
91
|
docsPageNavItems?: DocsNavItem[];
|
|
88
92
|
docsNavLabel?: string;
|
|
89
93
|
docsPageNavLabel?: string;
|
|
94
|
+
panelVariant?: "modern" | "classic";
|
|
90
95
|
styleClassPassthrough?: string | string[];
|
|
91
96
|
}
|
|
92
97
|
const props = withDefaults(defineProps<Props>(), {
|
|
@@ -95,9 +100,16 @@ const props = withDefaults(defineProps<Props>(), {
|
|
|
95
100
|
docsPageNavItems: () => [],
|
|
96
101
|
docsNavLabel: "Navigation",
|
|
97
102
|
docsPageNavLabel: "On this page",
|
|
103
|
+
// Defaults to "classic" — the docs/admin nav toggle relies on contentIsOnTop on mobile, and
|
|
104
|
+
// ExpandingPanel's ::details-content-based positioning silently fails on WebKit (Safari/iOS),
|
|
105
|
+
// making the nav unreachable there. See AccordianCore's own variant prop and CLAUDE.md
|
|
106
|
+
// pitfall #19 for the full mechanism. "modern" is opt-in.
|
|
107
|
+
panelVariant: "classic",
|
|
98
108
|
styleClassPassthrough: () => [],
|
|
99
109
|
});
|
|
100
110
|
|
|
111
|
+
const panelComponent = computed(() => (props.panelVariant === "modern" ? ExpandingPanel : ExpandingPanelClassic));
|
|
112
|
+
|
|
101
113
|
const activeNavItem = defineModel<string | undefined>("activeNavItem");
|
|
102
114
|
const activePageNavItem = defineModel<string | undefined>("activePageNavItem");
|
|
103
115
|
|
|
@@ -238,12 +250,9 @@ const docsPageNavPanelName = computed(() => (isMobile.value ? panelGroupId : `${
|
|
|
238
250
|
align-items: start;
|
|
239
251
|
|
|
240
252
|
@container contentDocs (width >= 1024px) {
|
|
241
|
-
.expanding-panel
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
cursor: default;
|
|
245
|
-
}
|
|
246
|
-
}
|
|
253
|
+
.expanding-panel-summary,
|
|
254
|
+
.expanding-panel-classic-summary {
|
|
255
|
+
cursor: default;
|
|
247
256
|
}
|
|
248
257
|
}
|
|
249
258
|
}
|
|
@@ -256,12 +265,9 @@ const docsPageNavPanelName = computed(() => (isMobile.value ? panelGroupId : `${
|
|
|
256
265
|
align-items: start;
|
|
257
266
|
|
|
258
267
|
@container contentDocs (width >= 768px) {
|
|
259
|
-
.expanding-panel
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
cursor: default;
|
|
263
|
-
}
|
|
264
|
-
}
|
|
268
|
+
.expanding-panel-summary,
|
|
269
|
+
.expanding-panel-classic-summary {
|
|
270
|
+
cursor: default;
|
|
265
271
|
}
|
|
266
272
|
}
|
|
267
273
|
}
|
|
@@ -34,11 +34,15 @@ interface Props {
|
|
|
34
34
|
styleClassPassthrough?: string | string[];
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
+
// Defaults to "classic" (not "modern") — ExpandingPanel's animation/positioning relies on
|
|
38
|
+
// ::details-content, which WebKit (Safari/iOS) doesn't support (see ExpandingPanel's own dev-mode
|
|
39
|
+
// warning and CLAUDE.md pitfall #19). "modern" is opt-in for consumers who've confirmed it works
|
|
40
|
+
// for their audience, not the safe default.
|
|
37
41
|
const props = withDefaults(defineProps<Props>(), {
|
|
38
42
|
name: undefined,
|
|
39
43
|
itemCount: 0,
|
|
40
44
|
animationDuration: 300,
|
|
41
|
-
variant: "
|
|
45
|
+
variant: "classic",
|
|
42
46
|
styleClassPassthrough: () => [],
|
|
43
47
|
});
|
|
44
48
|
|
|
@@ -74,11 +74,11 @@ describe("AccordianCore", () => {
|
|
|
74
74
|
|
|
75
75
|
// ─── Item count ───────────────────────────────────────────────────────────
|
|
76
76
|
|
|
77
|
-
it("renders the correct number of
|
|
77
|
+
it("renders the correct number of panel components", async () => {
|
|
78
78
|
const wrapper = await mountSuspended(AccordianCore, {
|
|
79
79
|
props: { itemCount: 4 },
|
|
80
80
|
});
|
|
81
|
-
expect(wrapper.findAll(".
|
|
81
|
+
expect(wrapper.findAll(".accordian-item")).toHaveLength(4);
|
|
82
82
|
});
|
|
83
83
|
|
|
84
84
|
it("renders no ExpandingPanel components when itemCount is 0", async () => {
|
|
@@ -95,15 +95,15 @@ describe("AccordianCore", () => {
|
|
|
95
95
|
|
|
96
96
|
// ─── variant ──────────────────────────────────────────────────────────────
|
|
97
97
|
|
|
98
|
-
it("renders
|
|
98
|
+
it("renders ExpandingPanelClassic by default (variant: classic)", async () => {
|
|
99
99
|
const wrapper = await mountSuspended(AccordianCore, {
|
|
100
100
|
props: { itemCount: 2 },
|
|
101
101
|
});
|
|
102
|
-
expect(wrapper.findAllComponents({ name: "
|
|
103
|
-
expect(wrapper.findAllComponents({ name: "
|
|
102
|
+
expect(wrapper.findAllComponents({ name: "ExpandingPanelClassic" })).toHaveLength(2);
|
|
103
|
+
expect(wrapper.findAllComponents({ name: "ExpandingPanel" })).toHaveLength(0);
|
|
104
104
|
});
|
|
105
105
|
|
|
106
|
-
it("renders ExpandingPanelClassic when variant is classic", async () => {
|
|
106
|
+
it("renders ExpandingPanelClassic when variant is classic (explicit)", async () => {
|
|
107
107
|
const wrapper = await mountSuspended(AccordianCore, {
|
|
108
108
|
props: { itemCount: 2, variant: "classic" },
|
|
109
109
|
});
|
|
@@ -111,6 +111,14 @@ describe("AccordianCore", () => {
|
|
|
111
111
|
expect(wrapper.findAllComponents({ name: "ExpandingPanel" })).toHaveLength(0);
|
|
112
112
|
});
|
|
113
113
|
|
|
114
|
+
it("renders ExpandingPanel when variant is modern", async () => {
|
|
115
|
+
const wrapper = await mountSuspended(AccordianCore, {
|
|
116
|
+
props: { itemCount: 2, variant: "modern" },
|
|
117
|
+
});
|
|
118
|
+
expect(wrapper.findAllComponents({ name: "ExpandingPanel" })).toHaveLength(2);
|
|
119
|
+
expect(wrapper.findAllComponents({ name: "ExpandingPanelClassic" })).toHaveLength(0);
|
|
120
|
+
});
|
|
121
|
+
|
|
114
122
|
it("forwards props identically to ExpandingPanelClassic when variant is classic", async () => {
|
|
115
123
|
const wrapper = await mountSuspended(AccordianCore, {
|
|
116
124
|
props: { itemCount: 2, name: "classic-accordian", animationDuration: 500, variant: "classic" },
|
|
@@ -122,11 +130,11 @@ describe("AccordianCore", () => {
|
|
|
122
130
|
});
|
|
123
131
|
});
|
|
124
132
|
|
|
125
|
-
// ─── Props forwarded to ExpandingPanel
|
|
133
|
+
// ─── Props forwarded to ExpandingPanel (variant: modern) ─────────────────
|
|
126
134
|
|
|
127
135
|
it("forwards animationDuration to every ExpandingPanel", async () => {
|
|
128
136
|
const wrapper = await mountSuspended(AccordianCore, {
|
|
129
|
-
props: { itemCount: 3, animationDuration: 500 },
|
|
137
|
+
props: { itemCount: 3, animationDuration: 500, variant: "modern" },
|
|
130
138
|
});
|
|
131
139
|
wrapper.findAllComponents({ name: "ExpandingPanel" }).forEach((panel) => {
|
|
132
140
|
expect(panel.props("animationDuration")).toBe(500);
|
|
@@ -135,7 +143,7 @@ describe("AccordianCore", () => {
|
|
|
135
143
|
|
|
136
144
|
it("forwards name to every ExpandingPanel", async () => {
|
|
137
145
|
const wrapper = await mountSuspended(AccordianCore, {
|
|
138
|
-
props: { itemCount: 2, name: "test-accordian" },
|
|
146
|
+
props: { itemCount: 2, name: "test-accordian", variant: "modern" },
|
|
139
147
|
});
|
|
140
148
|
wrapper.findAllComponents({ name: "ExpandingPanel" }).forEach((panel) => {
|
|
141
149
|
expect(panel.props("name")).toBe("test-accordian");
|
|
@@ -144,7 +152,7 @@ describe("AccordianCore", () => {
|
|
|
144
152
|
|
|
145
153
|
it("passes accordian-item styleClassPassthrough to every ExpandingPanel", async () => {
|
|
146
154
|
const wrapper = await mountSuspended(AccordianCore, {
|
|
147
|
-
props: { itemCount: 2 },
|
|
155
|
+
props: { itemCount: 2, variant: "modern" },
|
|
148
156
|
});
|
|
149
157
|
wrapper.findAllComponents({ name: "ExpandingPanel" }).forEach((panel) => {
|
|
150
158
|
expect(panel.props("styleClassPassthrough")).toEqual(["accordian-item"]);
|
|
@@ -153,11 +161,11 @@ describe("AccordianCore", () => {
|
|
|
153
161
|
|
|
154
162
|
// ─── Default prop values ──────────────────────────────────────────────────
|
|
155
163
|
|
|
156
|
-
it("defaults animationDuration to 300 on
|
|
164
|
+
it("defaults animationDuration to 300 on ExpandingPanelClassic when not provided", async () => {
|
|
157
165
|
const wrapper = await mountSuspended(AccordianCore, {
|
|
158
166
|
props: { itemCount: 1 },
|
|
159
167
|
});
|
|
160
|
-
const panel = wrapper.findComponent({ name: "
|
|
168
|
+
const panel = wrapper.findComponent({ name: "ExpandingPanelClassic" });
|
|
161
169
|
expect(panel.props("animationDuration")).toBe(300);
|
|
162
170
|
});
|
|
163
171
|
|
|
@@ -167,7 +175,7 @@ describe("AccordianCore", () => {
|
|
|
167
175
|
expect(vm.name).toBeUndefined();
|
|
168
176
|
expect(vm.itemCount).toBe(0);
|
|
169
177
|
expect(vm.animationDuration).toBe(300);
|
|
170
|
-
expect(vm.variant).toBe("
|
|
178
|
+
expect(vm.variant).toBe("classic");
|
|
171
179
|
expect(vm.styleClassPassthrough).toEqual([]);
|
|
172
180
|
});
|
|
173
181
|
|
|
@@ -241,7 +249,7 @@ describe("AccordianCore", () => {
|
|
|
241
249
|
"accordian-2-summary": "<span>Gamma</span>",
|
|
242
250
|
},
|
|
243
251
|
});
|
|
244
|
-
const panels = wrapper.findAllComponents({ name: "
|
|
252
|
+
const panels = wrapper.findAllComponents({ name: "ExpandingPanelClassic" });
|
|
245
253
|
expect(panels[0]?.text()).toContain("Alpha");
|
|
246
254
|
expect(panels[1]?.text()).toContain("Beta");
|
|
247
255
|
expect(panels[2]?.text()).toContain("Gamma");
|
|
@@ -2,78 +2,92 @@
|
|
|
2
2
|
|
|
3
3
|
exports[`AccordianCore > renders correct HTML structure with all props set 1`] = `
|
|
4
4
|
"<div class="display-accordian extra-class">
|
|
5
|
-
<div class="expanding-panel accordian-item" icon-size="medium">
|
|
6
|
-
<details class="expanding-panel-details" name="snapshot-accordian">
|
|
7
|
-
<summary id="id-snapshot-accordian-trigger" class="expanding-panel-summary" aria-controls="id-snapshot-accordian-content" aria-expanded="false">
|
|
5
|
+
<div class="expanding-panel-classic accordian-item" icon-size="medium">
|
|
6
|
+
<details class="expanding-panel-classic-details" name="snapshot-accordian">
|
|
7
|
+
<summary id="id-snapshot-accordian-trigger" class="expanding-panel-classic-summary" aria-controls="id-snapshot-accordian-content" aria-expanded="false">
|
|
8
8
|
<div class="label-wrapper"></div>
|
|
9
9
|
<div class="icon-wrapper"><span class="iconify i-bi:caret-down-fill icon" aria-hidden="true"></span></div>
|
|
10
10
|
</summary>
|
|
11
|
-
<div id="id-snapshot-accordian-content" class="expanding-panel-content" aria-labelledby="id-snapshot-accordian-trigger" role="region"><div class="inner"></div></div>
|
|
12
11
|
</details>
|
|
12
|
+
<div id="id-snapshot-accordian-content" class="expanding-panel-classic-content" aria-labelledby="id-snapshot-accordian-trigger" role="region">
|
|
13
|
+
<div class="inner"></div>
|
|
14
|
+
</div>
|
|
13
15
|
</div>
|
|
14
|
-
<div class="expanding-panel accordian-item" icon-size="medium">
|
|
15
|
-
<details class="expanding-panel-details" name="snapshot-accordian">
|
|
16
|
-
<summary id="id-snapshot-accordian-trigger" class="expanding-panel-summary" aria-controls="id-snapshot-accordian-content" aria-expanded="false">
|
|
16
|
+
<div class="expanding-panel-classic accordian-item" icon-size="medium">
|
|
17
|
+
<details class="expanding-panel-classic-details" name="snapshot-accordian">
|
|
18
|
+
<summary id="id-snapshot-accordian-trigger" class="expanding-panel-classic-summary" aria-controls="id-snapshot-accordian-content" aria-expanded="false">
|
|
17
19
|
<div class="label-wrapper"></div>
|
|
18
20
|
<div class="icon-wrapper"><span class="iconify i-bi:caret-down-fill icon" aria-hidden="true"></span></div>
|
|
19
21
|
</summary>
|
|
20
|
-
<div id="id-snapshot-accordian-content" class="expanding-panel-content" aria-labelledby="id-snapshot-accordian-trigger" role="region"><div class="inner"></div></div>
|
|
21
22
|
</details>
|
|
23
|
+
<div id="id-snapshot-accordian-content" class="expanding-panel-classic-content" aria-labelledby="id-snapshot-accordian-trigger" role="region">
|
|
24
|
+
<div class="inner"></div>
|
|
25
|
+
</div>
|
|
22
26
|
</div>
|
|
23
|
-
<div class="expanding-panel accordian-item" icon-size="medium">
|
|
24
|
-
<details class="expanding-panel-details" name="snapshot-accordian">
|
|
25
|
-
<summary id="id-snapshot-accordian-trigger" class="expanding-panel-summary" aria-controls="id-snapshot-accordian-content" aria-expanded="false">
|
|
27
|
+
<div class="expanding-panel-classic accordian-item" icon-size="medium">
|
|
28
|
+
<details class="expanding-panel-classic-details" name="snapshot-accordian">
|
|
29
|
+
<summary id="id-snapshot-accordian-trigger" class="expanding-panel-classic-summary" aria-controls="id-snapshot-accordian-content" aria-expanded="false">
|
|
26
30
|
<div class="label-wrapper"></div>
|
|
27
31
|
<div class="icon-wrapper"><span class="iconify i-bi:caret-down-fill icon" aria-hidden="true"></span></div>
|
|
28
32
|
</summary>
|
|
29
|
-
<div id="id-snapshot-accordian-content" class="expanding-panel-content" aria-labelledby="id-snapshot-accordian-trigger" role="region"><div class="inner"></div></div>
|
|
30
33
|
</details>
|
|
34
|
+
<div id="id-snapshot-accordian-content" class="expanding-panel-classic-content" aria-labelledby="id-snapshot-accordian-trigger" role="region">
|
|
35
|
+
<div class="inner"></div>
|
|
36
|
+
</div>
|
|
31
37
|
</div>
|
|
32
38
|
</div>"
|
|
33
39
|
`;
|
|
34
40
|
|
|
35
41
|
exports[`AccordianCore > renders correct HTML structure with default props 1`] = `
|
|
36
42
|
"<div class="display-accordian">
|
|
37
|
-
<div class="expanding-panel accordian-item" icon-size="medium">
|
|
38
|
-
<details class="expanding-panel-details" name="v-0-0-0">
|
|
39
|
-
<summary id="id-v-0-0-0-trigger" class="expanding-panel-summary" aria-controls="id-v-0-0-0-content" aria-expanded="false">
|
|
43
|
+
<div class="expanding-panel-classic accordian-item" icon-size="medium">
|
|
44
|
+
<details class="expanding-panel-classic-details" name="v-0-0-0">
|
|
45
|
+
<summary id="id-v-0-0-0-trigger" class="expanding-panel-classic-summary" aria-controls="id-v-0-0-0-content" aria-expanded="false">
|
|
40
46
|
<div class="label-wrapper"></div>
|
|
41
47
|
<div class="icon-wrapper"><span class="iconify i-bi:caret-down-fill icon" aria-hidden="true"></span></div>
|
|
42
48
|
</summary>
|
|
43
|
-
<div id="id-v-0-0-0-content" class="expanding-panel-content" aria-labelledby="id-v-0-0-0-trigger" role="region"><div class="inner"></div></div>
|
|
44
49
|
</details>
|
|
50
|
+
<div id="id-v-0-0-0-content" class="expanding-panel-classic-content" aria-labelledby="id-v-0-0-0-trigger" role="region">
|
|
51
|
+
<div class="inner"></div>
|
|
52
|
+
</div>
|
|
45
53
|
</div>
|
|
46
|
-
<div class="expanding-panel accordian-item" icon-size="medium">
|
|
47
|
-
<details class="expanding-panel-details" name="v-0-0-1">
|
|
48
|
-
<summary id="id-v-0-0-1-trigger" class="expanding-panel-summary" aria-controls="id-v-0-0-1-content" aria-expanded="false">
|
|
54
|
+
<div class="expanding-panel-classic accordian-item" icon-size="medium">
|
|
55
|
+
<details class="expanding-panel-classic-details" name="v-0-0-1">
|
|
56
|
+
<summary id="id-v-0-0-1-trigger" class="expanding-panel-classic-summary" aria-controls="id-v-0-0-1-content" aria-expanded="false">
|
|
49
57
|
<div class="label-wrapper"></div>
|
|
50
58
|
<div class="icon-wrapper"><span class="iconify i-bi:caret-down-fill icon" aria-hidden="true"></span></div>
|
|
51
59
|
</summary>
|
|
52
|
-
<div id="id-v-0-0-1-content" class="expanding-panel-content" aria-labelledby="id-v-0-0-1-trigger" role="region"><div class="inner"></div></div>
|
|
53
60
|
</details>
|
|
61
|
+
<div id="id-v-0-0-1-content" class="expanding-panel-classic-content" aria-labelledby="id-v-0-0-1-trigger" role="region">
|
|
62
|
+
<div class="inner"></div>
|
|
63
|
+
</div>
|
|
54
64
|
</div>
|
|
55
65
|
</div>"
|
|
56
66
|
`;
|
|
57
67
|
|
|
58
68
|
exports[`AccordianCore > renders correct HTML structure with populated slots 1`] = `
|
|
59
69
|
"<div class="display-accordian">
|
|
60
|
-
<div class="expanding-panel accordian-item" icon-size="medium">
|
|
61
|
-
<details class="expanding-panel-details" name="v-0-0-0">
|
|
62
|
-
<summary id="id-v-0-0-0-trigger" class="expanding-panel-summary" aria-controls="id-v-0-0-0-content" aria-expanded="false">
|
|
70
|
+
<div class="expanding-panel-classic accordian-item" icon-size="medium">
|
|
71
|
+
<details class="expanding-panel-classic-details" name="v-0-0-0">
|
|
72
|
+
<summary id="id-v-0-0-0-trigger" class="expanding-panel-classic-summary" aria-controls="id-v-0-0-0-content" aria-expanded="false">
|
|
63
73
|
<div class="label-wrapper"><span>Summary 0</span></div>
|
|
64
74
|
<div class="icon-wrapper"><span>Icon 0</span></div>
|
|
65
75
|
</summary>
|
|
66
|
-
<div id="id-v-0-0-0-content" class="expanding-panel-content" aria-labelledby="id-v-0-0-0-trigger" role="region"><div class="inner"><span>Content 0</span></div></div>
|
|
67
76
|
</details>
|
|
77
|
+
<div id="id-v-0-0-0-content" class="expanding-panel-classic-content" aria-labelledby="id-v-0-0-0-trigger" role="region">
|
|
78
|
+
<div class="inner"><span>Content 0</span></div>
|
|
79
|
+
</div>
|
|
68
80
|
</div>
|
|
69
|
-
<div class="expanding-panel accordian-item" icon-size="medium">
|
|
70
|
-
<details class="expanding-panel-details" name="v-0-0-1">
|
|
71
|
-
<summary id="id-v-0-0-1-trigger" class="expanding-panel-summary" aria-controls="id-v-0-0-1-content" aria-expanded="false">
|
|
81
|
+
<div class="expanding-panel-classic accordian-item" icon-size="medium">
|
|
82
|
+
<details class="expanding-panel-classic-details" name="v-0-0-1">
|
|
83
|
+
<summary id="id-v-0-0-1-trigger" class="expanding-panel-classic-summary" aria-controls="id-v-0-0-1-content" aria-expanded="false">
|
|
72
84
|
<div class="label-wrapper"><span>Summary 1</span></div>
|
|
73
85
|
<div class="icon-wrapper"><span>Icon 1</span></div>
|
|
74
86
|
</summary>
|
|
75
|
-
<div id="id-v-0-0-1-content" class="expanding-panel-content" aria-labelledby="id-v-0-0-1-trigger" role="region"><div class="inner"><span>Content 1</span></div></div>
|
|
76
87
|
</details>
|
|
88
|
+
<div id="id-v-0-0-1-content" class="expanding-panel-classic-content" aria-labelledby="id-v-0-0-1-trigger" role="region">
|
|
89
|
+
<div class="inner"><span>Content 1</span></div>
|
|
90
|
+
</div>
|
|
77
91
|
</div>
|
|
78
92
|
</div>"
|
|
79
93
|
`;
|
|
@@ -90,16 +90,14 @@ visual style, pass a modifier class:
|
|
|
90
90
|
- `--expanding-panel-content-gap` and `--expanding-panel-content-z-index` only take effect when
|
|
91
91
|
`contentIsOnTop` is `true` — they're no-ops for the default (in-flow) layout.
|
|
92
92
|
- `background-color`, `padding`, `border`, and shadow are all safe to set directly on
|
|
93
|
-
`.expanding-panel-content`.
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
detail of the collapse mechanism, not a styling hook) but styling on `.expanding-panel-content`
|
|
102
|
-
itself still works correctly, same as the `::details-content` path.
|
|
93
|
+
`.expanding-panel-content`. The open/close animation clips via `overflow: clip` on
|
|
94
|
+
`::details-content`, and `.expanding-panel-content` is a child of it, so its own box is
|
|
95
|
+
correctly clipped to the animated height while closed/closing. A wrapper inside the `#content`
|
|
96
|
+
slot works equally well if you prefer to keep the styling scoped to your own markup.
|
|
97
|
+
- **WebKit (Safari/iOS) doesn't support `::details-content` at all**, and `ExpandingPanel` has no
|
|
98
|
+
fallback for it (an `@supports`-based CSS fallback was tried and reverted 2026-08-22 — it didn't
|
|
99
|
+
reliably work). If this panel needs to work correctly on real Apple devices, use
|
|
100
|
+
`ExpandingPanelClassic` instead — see its own `CONSUMER-STYLING.md`.
|
|
103
101
|
- Don't stack multiple `contentIsOnTop` panels as direct siblings (linked via a shared `name` or
|
|
104
102
|
not). The overlay is absolutely positioned so it doesn't push the next element down — which
|
|
105
103
|
means a sibling `ExpandingPanel` placed right after it sits exactly where the overlay renders,
|
|
@@ -23,9 +23,7 @@
|
|
|
23
23
|
:aria-labelledby="`id-${name}-trigger`"
|
|
24
24
|
role="region"
|
|
25
25
|
>
|
|
26
|
-
<
|
|
27
|
-
<slot name="content"></slot>
|
|
28
|
-
</div>
|
|
26
|
+
<slot name="content"></slot>
|
|
29
27
|
</div>
|
|
30
28
|
</details>
|
|
31
29
|
</div>
|
|
@@ -66,6 +64,20 @@ onClickOutside(rootEl, () => {
|
|
|
66
64
|
});
|
|
67
65
|
|
|
68
66
|
if (import.meta.dev) {
|
|
67
|
+
// ::details-content (the animation/positioning mechanism this component relies on) is not
|
|
68
|
+
// supported in WebKit (Safari/iOS) as of 2026-08 — confirmed via real-device testing, not
|
|
69
|
+
// reproducible in Chrome's device-emulation mode since that's still the Blink engine. An
|
|
70
|
+
// earlier @supports-based fallback attempt (2026-08-22) turned out not to reliably kick in
|
|
71
|
+
// either. Prefer ExpandingPanelClassic (or a consumer's own `classic` prop, where offered —
|
|
72
|
+
// see AccordianCore) for anything that needs to work correctly on real Apple devices today.
|
|
73
|
+
console.warn(
|
|
74
|
+
`ExpandingPanel${props.name ? ` "${props.name}"` : ""}: this variant depends on ` +
|
|
75
|
+
"::details-content, which WebKit (Safari/iOS) does not support — the animation and, more " +
|
|
76
|
+
"seriously, contentIsOnTop's overlay positioning silently fail there with no console error. " +
|
|
77
|
+
"Use ExpandingPanelClassic instead if this panel needs to work correctly on real Apple " +
|
|
78
|
+
"devices."
|
|
79
|
+
);
|
|
80
|
+
|
|
69
81
|
watch(
|
|
70
82
|
() => props.contentIsOnTop,
|
|
71
83
|
(value) => {
|
|
@@ -189,50 +201,6 @@ const onDetailsToggle = (event: Event) => {
|
|
|
189
201
|
}
|
|
190
202
|
}
|
|
191
203
|
}
|
|
192
|
-
|
|
193
|
-
/* ::details-content isn't supported in WebKit (Safari/iOS) as of 2026-08 — the two rule
|
|
194
|
-
blocks above are silently dropped there entirely (no error, no animation, and crucially
|
|
195
|
-
content-is-on-top's position: absolute never applies, so the overlay renders in normal
|
|
196
|
-
document flow instead). This reproduces both using ExpandingPanelClassic's
|
|
197
|
-
grid-template-rows technique (broadly supported, including Safari), targeting the real
|
|
198
|
-
.expanding-panel-content element (and its .inner child, matching Classic's structure —
|
|
199
|
-
.inner's own overflow:hidden is what lets the grid row actually collapse to 0, since a
|
|
200
|
-
plain block child's default min-height:auto would otherwise floor it at content height)
|
|
201
|
-
instead of the pseudo-element. */
|
|
202
|
-
@supports not selector(::details-content) {
|
|
203
|
-
.expanding-panel-details {
|
|
204
|
-
.expanding-panel-content {
|
|
205
|
-
display: grid;
|
|
206
|
-
grid-template-rows: 0fr;
|
|
207
|
-
transition: grid-template-rows v-bind(animationDurationStr) ease-in-out;
|
|
208
|
-
will-change: grid-template-rows;
|
|
209
|
-
|
|
210
|
-
.inner {
|
|
211
|
-
overflow: hidden;
|
|
212
|
-
margin-top: 0;
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
&[open] .expanding-panel-content {
|
|
217
|
-
grid-template-rows: 1fr;
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
&.content-is-on-top {
|
|
223
|
-
@supports not selector(::details-content) {
|
|
224
|
-
.expanding-panel-details {
|
|
225
|
-
position: relative;
|
|
226
|
-
|
|
227
|
-
.expanding-panel-content {
|
|
228
|
-
position: absolute;
|
|
229
|
-
top: calc(100% + var(--expanding-panel-content-gap, 0px));
|
|
230
|
-
inset-inline: 0;
|
|
231
|
-
z-index: var(--expanding-panel-content-z-index, 10);
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
204
|
}
|
|
237
205
|
}
|
|
238
206
|
</style>
|
|
@@ -7,9 +7,7 @@ exports[`ExpandingPanel > renders correct HTML structure with all props set 1`]
|
|
|
7
7
|
<div class="label-wrapper"></div>
|
|
8
8
|
<div class="icon-wrapper icon-wrapper--hidden"><span class="iconify i-bi:caret-down-fill icon" aria-hidden="true"></span></div>
|
|
9
9
|
</summary>
|
|
10
|
-
<div id="id-snap-all-content" class="expanding-panel-content" aria-labelledby="id-snap-all-trigger" role="region">
|
|
11
|
-
<div class="inner"></div>
|
|
12
|
-
</div>
|
|
10
|
+
<div id="id-snap-all-content" class="expanding-panel-content" aria-labelledby="id-snap-all-trigger" role="region"></div>
|
|
13
11
|
</details>
|
|
14
12
|
</div>"
|
|
15
13
|
`;
|
|
@@ -21,9 +19,7 @@ exports[`ExpandingPanel > renders correct HTML structure with contentIsOnTop tru
|
|
|
21
19
|
<div class="label-wrapper"></div>
|
|
22
20
|
<div class="icon-wrapper"><span class="iconify i-bi:caret-down-fill icon" aria-hidden="true"></span></div>
|
|
23
21
|
</summary>
|
|
24
|
-
<div id="id-snap-content-on-top-content" class="expanding-panel-content" aria-labelledby="id-snap-content-on-top-trigger" role="region">
|
|
25
|
-
<div class="inner"></div>
|
|
26
|
-
</div>
|
|
22
|
+
<div id="id-snap-content-on-top-content" class="expanding-panel-content" aria-labelledby="id-snap-content-on-top-trigger" role="region"></div>
|
|
27
23
|
</details>
|
|
28
24
|
</div>"
|
|
29
25
|
`;
|
|
@@ -35,9 +31,7 @@ exports[`ExpandingPanel > renders correct HTML structure with default props 1`]
|
|
|
35
31
|
<div class="label-wrapper"></div>
|
|
36
32
|
<div class="icon-wrapper"><span class="iconify i-bi:caret-down-fill icon" aria-hidden="true"></span></div>
|
|
37
33
|
</summary>
|
|
38
|
-
<div id="id-snap-default-content" class="expanding-panel-content" aria-labelledby="id-snap-default-trigger" role="region">
|
|
39
|
-
<div class="inner"></div>
|
|
40
|
-
</div>
|
|
34
|
+
<div id="id-snap-default-content" class="expanding-panel-content" aria-labelledby="id-snap-default-trigger" role="region"></div>
|
|
41
35
|
</details>
|
|
42
36
|
</div>"
|
|
43
37
|
`;
|
|
@@ -50,9 +44,7 @@ exports[`ExpandingPanel > renders correct HTML structure with populated slots 1`
|
|
|
50
44
|
<div class="icon-wrapper"><span>▸</span></div>
|
|
51
45
|
</summary>
|
|
52
46
|
<div id="id-snap-slots-content" class="expanding-panel-content" aria-labelledby="id-snap-slots-trigger" role="region">
|
|
53
|
-
<
|
|
54
|
-
<p>Content text</p>
|
|
55
|
-
</div>
|
|
47
|
+
<p>Content text</p>
|
|
56
48
|
</div>
|
|
57
49
|
</details>
|
|
58
50
|
</div>"
|
|
@@ -56,7 +56,8 @@
|
|
|
56
56
|
role="none"
|
|
57
57
|
@mouseenter="hoveredItemKey = `${String(groupKey)}-${localIndex}`"
|
|
58
58
|
>
|
|
59
|
-
<
|
|
59
|
+
<component
|
|
60
|
+
:is="panelComponent"
|
|
60
61
|
v-model="panelOpenStates[`${String(groupKey)}-${localIndex}`]"
|
|
61
62
|
name="overflow-navigation-group"
|
|
62
63
|
:animation-duration="DETAILS_ANIMATION_DURATION"
|
|
@@ -96,7 +97,7 @@
|
|
|
96
97
|
<div aria-hidden="true" class="overflow-sub-nav-indicator-hovered"></div>
|
|
97
98
|
</div>
|
|
98
99
|
</template>
|
|
99
|
-
</
|
|
100
|
+
</component>
|
|
100
101
|
</li>
|
|
101
102
|
</template>
|
|
102
103
|
</ul>
|
|
@@ -107,17 +108,26 @@
|
|
|
107
108
|
|
|
108
109
|
<script setup lang="ts">
|
|
109
110
|
import type { ResponsiveHeaderState, ResponsiveHeaderNavItem } from "../../../types/components";
|
|
111
|
+
import ExpandingPanel from "../../02.molecules/expandable/expanding-panel/ExpandingPanel.vue";
|
|
112
|
+
import ExpandingPanelClassic from "../../02.molecules/expandable/expanding-panel-classic/ExpandingPanelClassic.vue";
|
|
110
113
|
|
|
111
114
|
interface Props {
|
|
112
115
|
mainNavigationState?: ResponsiveHeaderState;
|
|
116
|
+
panelVariant?: "modern" | "classic";
|
|
113
117
|
styleClassPassthrough?: string | string[];
|
|
114
118
|
}
|
|
115
119
|
|
|
116
120
|
const props = withDefaults(defineProps<Props>(), {
|
|
117
121
|
mainNavigationState: () => ({ clonedNavLinks: {}, navListVisibility: {}, hasSecondNav: false }),
|
|
122
|
+
// Defaults to "classic" — see ExpandingPanel's own dev-mode warning and CLAUDE.md pitfall #19:
|
|
123
|
+
// its ::details-content-based implementation doesn't work correctly on WebKit (Safari/iOS).
|
|
124
|
+
// "modern" is opt-in.
|
|
125
|
+
panelVariant: "classic",
|
|
118
126
|
styleClassPassthrough: () => [],
|
|
119
127
|
});
|
|
120
128
|
|
|
129
|
+
const panelComponent = computed(() => (props.panelVariant === "modern" ? ExpandingPanel : ExpandingPanelClassic));
|
|
130
|
+
|
|
121
131
|
const hoveredItemKey = ref<string | null>(null);
|
|
122
132
|
const hoveredChildKey = ref<string | null>(null);
|
|
123
133
|
|
|
@@ -231,6 +241,12 @@ watch(
|
|
|
231
241
|
}
|
|
232
242
|
|
|
233
243
|
.overflow-navigation-details {
|
|
244
|
+
/* Two variants share this styling, one full block each rather than a merged
|
|
245
|
+
selector list — their DOM shapes differ (modern nests .expanding-panel-content
|
|
246
|
+
*inside* .expanding-panel-details; classic renders .expanding-panel-classic-content
|
|
247
|
+
as a *sibling* of .expanding-panel-classic-details), so the [open] + ... sibling
|
|
248
|
+
combinator below is only structurally correct for the classic block. See
|
|
249
|
+
ExpandingPanel vs ExpandingPanelClassic's own templates. */
|
|
234
250
|
&.expanding-panel {
|
|
235
251
|
margin-block-end: 0;
|
|
236
252
|
|
|
@@ -293,6 +309,69 @@ watch(
|
|
|
293
309
|
}
|
|
294
310
|
}
|
|
295
311
|
}
|
|
312
|
+
|
|
313
|
+
&.expanding-panel-classic {
|
|
314
|
+
margin-block-end: 0;
|
|
315
|
+
|
|
316
|
+
.expanding-panel-classic-details {
|
|
317
|
+
.expanding-panel-classic-summary {
|
|
318
|
+
padding-block: var(--overflow-nav-items-padding-block, 0.8rem);
|
|
319
|
+
padding-inline: var(--overflow-nav-padding-inline, 0.8rem);
|
|
320
|
+
gap: 1rem;
|
|
321
|
+
color: var(--overflow-nav-link-color, inherit);
|
|
322
|
+
border-bottom: 0.1rem solid var(--overflow-nav-link-border-color, #efefef75);
|
|
323
|
+
|
|
324
|
+
.label-wrapper {
|
|
325
|
+
.overflow-navigation-text {
|
|
326
|
+
text-wrap: nowrap;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
.icon-wrapper {
|
|
330
|
+
padding: 0;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
&[open] {
|
|
335
|
+
.expanding-panel-classic-summary {
|
|
336
|
+
border-bottom: 0.1rem solid transparent;
|
|
337
|
+
}
|
|
338
|
+
+ .expanding-panel-classic-content {
|
|
339
|
+
border-bottom: 0.1rem solid var(--overflow-nav-link-border-color, #efefef75);
|
|
340
|
+
.overflow-navigation-sub-nav-inner {
|
|
341
|
+
margin-top: var(--overflow-nav-items-gap, 0px);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
.expanding-panel-classic-content {
|
|
348
|
+
border-bottom: 0.1rem solid transparent;
|
|
349
|
+
|
|
350
|
+
.overflow-navigation-sub-nav-inner {
|
|
351
|
+
margin-top: 0;
|
|
352
|
+
position: relative;
|
|
353
|
+
|
|
354
|
+
.overflow-navigation-sub-nav-list {
|
|
355
|
+
display: flex;
|
|
356
|
+
flex-direction: column;
|
|
357
|
+
gap: 2px;
|
|
358
|
+
|
|
359
|
+
.overflow-navigation-sub-nav-item {
|
|
360
|
+
padding-block: var(--overflow-nav-items-padding-block, 0.8rem);
|
|
361
|
+
padding-inline: var(--overflow-nav-padding-inline, 0.8rem);
|
|
362
|
+
font-size: var(--overflow-nav-sub-item-font-size, inherit);
|
|
363
|
+
color: var(--overflow-nav-sub-item-color, inherit);
|
|
364
|
+
|
|
365
|
+
.overflow-navigation-sub-nav-link {
|
|
366
|
+
display: block;
|
|
367
|
+
text-decoration: none;
|
|
368
|
+
color: inherit;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
}
|
|
296
375
|
}
|
|
297
376
|
}
|
|
298
377
|
}
|
|
@@ -325,7 +404,11 @@ watch(
|
|
|
325
404
|
.overflow-navigation-wrapper
|
|
326
405
|
.overflow-navigation-item.is-hovered
|
|
327
406
|
.overflow-navigation-details
|
|
328
|
-
.expanding-panel-summary
|
|
407
|
+
.expanding-panel-summary,
|
|
408
|
+
.overflow-navigation-wrapper
|
|
409
|
+
.overflow-navigation-item.is-hovered
|
|
410
|
+
.overflow-navigation-details
|
|
411
|
+
.expanding-panel-classic-summary {
|
|
329
412
|
anchor-name: --overflow-nav-indicator;
|
|
330
413
|
}
|
|
331
414
|
|
|
@@ -340,7 +423,11 @@ watch(
|
|
|
340
423
|
.overflow-navigation-wrapper:not(:has(.overflow-navigation-item.is-hovered))
|
|
341
424
|
.overflow-navigation-item.is-active
|
|
342
425
|
.overflow-navigation-details
|
|
343
|
-
.expanding-panel-summary
|
|
426
|
+
.expanding-panel-summary,
|
|
427
|
+
.overflow-navigation-wrapper:not(:has(.overflow-navigation-item.is-hovered))
|
|
428
|
+
.overflow-navigation-item.is-active
|
|
429
|
+
.overflow-navigation-details
|
|
430
|
+
.expanding-panel-classic-summary {
|
|
344
431
|
anchor-name: --overflow-nav-indicator;
|
|
345
432
|
}
|
|
346
433
|
|
|
@@ -100,7 +100,7 @@
|
|
|
100
100
|
/>
|
|
101
101
|
</summary>
|
|
102
102
|
<div class="overflow-details-nav" role="menu">
|
|
103
|
-
<NavigationItems :main-navigation-state="mainNavigationState" />
|
|
103
|
+
<NavigationItems :main-navigation-state="mainNavigationState" :panel-variant="panelVariant" />
|
|
104
104
|
</div>
|
|
105
105
|
</details>
|
|
106
106
|
<slot v-if="slots.secondaryNavigation" name="secondaryNavigation"></slot>
|
|
@@ -125,6 +125,7 @@ interface Props {
|
|
|
125
125
|
collapseAtMainNavIntersection?: boolean;
|
|
126
126
|
styleClassPassthrough?: string | string[];
|
|
127
127
|
allowExpandOnGesture?: boolean;
|
|
128
|
+
panelVariant?: "modern" | "classic";
|
|
128
129
|
}
|
|
129
130
|
|
|
130
131
|
const props = withDefaults(defineProps<Props>(), {
|
|
@@ -138,6 +139,9 @@ const props = withDefaults(defineProps<Props>(), {
|
|
|
138
139
|
collapseAtMainNavIntersection: false,
|
|
139
140
|
styleClassPassthrough: () => [],
|
|
140
141
|
allowExpandOnGesture: true,
|
|
142
|
+
// Forwarded to NavigationItems' overflow submenu panels — see its own default for why
|
|
143
|
+
// "classic" is the safe default (CLAUDE.md pitfall #19).
|
|
144
|
+
panelVariant: "classic",
|
|
141
145
|
});
|
|
142
146
|
|
|
143
147
|
const collapseNavigationBelowWidth = computed(
|
|
@@ -138,10 +138,10 @@ describe("NavigationItems", () => {
|
|
|
138
138
|
const wrapper = await mountSuspended(NavigationItems, {
|
|
139
139
|
props: { mainNavigationState },
|
|
140
140
|
});
|
|
141
|
-
const details = wrapper.find(".expanding-panel-details");
|
|
141
|
+
const details = wrapper.find(".expanding-panel-classic-details");
|
|
142
142
|
expect(details.attributes("open")).toBeUndefined();
|
|
143
143
|
|
|
144
|
-
await wrapper.find(".expanding-panel-summary").trigger("click");
|
|
144
|
+
await wrapper.find(".expanding-panel-classic-summary").trigger("click");
|
|
145
145
|
await nextTick();
|
|
146
146
|
expect(details.attributes("open")).toBe("");
|
|
147
147
|
});
|
|
@@ -6,24 +6,24 @@ exports[`NavigationItems > renders correct HTML structure with default props 1`]
|
|
|
6
6
|
<li class="overflow-navigation-item is-active" style="--_main-navigation-item-width: 0px;" data-group-key="firstNav" data-local-index="0" role="none"><a href="/" class="overflow-navigation-link" role="menuitem"><span class="overflow-navigation-text">Home</span></a></li>
|
|
7
7
|
<li class="overflow-navigation-item visible" style="--_main-navigation-item-width: 0px;" data-group-key="firstNav" data-local-index="1" role="none"><a href="/about" class="overflow-navigation-link" role="menuitem"><span class="overflow-navigation-text">About</span></a></li>
|
|
8
8
|
<li class="overflow-navigation-item visible" style="--_main-navigation-item-width: 0px;" data-group-key="firstNav" data-local-index="2" role="none">
|
|
9
|
-
<div class="expanding-panel overflow-navigation-details">
|
|
10
|
-
<details class="expanding-panel-details" name="overflow-navigation-group">
|
|
11
|
-
<summary id="id-overflow-navigation-group-trigger" class="expanding-panel-summary" aria-controls="id-overflow-navigation-group-content" aria-expanded="false">
|
|
9
|
+
<div class="expanding-panel-classic overflow-navigation-details">
|
|
10
|
+
<details class="expanding-panel-classic-details" name="overflow-navigation-group">
|
|
11
|
+
<summary id="id-overflow-navigation-group-trigger" class="expanding-panel-classic-summary" aria-controls="id-overflow-navigation-group-content" aria-expanded="false">
|
|
12
12
|
<div class="label-wrapper"><span class="overflow-navigation-text" aria-label="UI Components submenu" role="menuitem" aria-haspopup="true">UI Components</span></div>
|
|
13
13
|
<div class="icon-wrapper"><span class="iconify i-mdi:chevron-down icon" aria-hidden="true"></span></div>
|
|
14
14
|
</summary>
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
</
|
|
22
|
-
|
|
23
|
-
|
|
15
|
+
</details>
|
|
16
|
+
<div id="id-overflow-navigation-group-content" class="expanding-panel-classic-content" aria-labelledby="id-overflow-navigation-group-trigger" role="region">
|
|
17
|
+
<div class="inner">
|
|
18
|
+
<div class="overflow-navigation-sub-nav-inner">
|
|
19
|
+
<ul class="overflow-navigation-sub-nav-list">
|
|
20
|
+
<li class="overflow-navigation-sub-nav-item"><a href="/forms/examples/buttons" class="overflow-navigation-sub-nav-link" role="menuitem"><span class="overflow-navigation-sub-nav-text">Buttons</span></a></li>
|
|
21
|
+
<li class="overflow-navigation-sub-nav-item"><a href="/ui/tabs" class="overflow-navigation-sub-nav-link" role="menuitem"><span class="overflow-navigation-sub-nav-text">Tabs</span></a></li>
|
|
22
|
+
</ul>
|
|
23
|
+
<div aria-hidden="true" class="overflow-sub-nav-indicator-hovered"></div>
|
|
24
24
|
</div>
|
|
25
25
|
</div>
|
|
26
|
-
</
|
|
26
|
+
</div>
|
|
27
27
|
</div>
|
|
28
28
|
</li>
|
|
29
29
|
</ul>
|
|
@@ -39,24 +39,24 @@ exports[`ResponsiveHeader > renders correct HTML structure with default props 1`
|
|
|
39
39
|
<li class="overflow-navigation-item visible is-active" style="--_main-navigation-item-width: 0px;" data-group-key="firstNav" data-local-index="0" role="none"><a href="/" class="overflow-navigation-link" role="menuitem"><span class="overflow-navigation-text">Home</span></a></li>
|
|
40
40
|
<li class="overflow-navigation-item visible" style="--_main-navigation-item-width: 0px;" data-group-key="firstNav" data-local-index="1" role="none"><a href="/about" class="overflow-navigation-link" role="menuitem"><span class="overflow-navigation-text">About</span></a></li>
|
|
41
41
|
<li class="overflow-navigation-item visible" style="--_main-navigation-item-width: 0px;" data-group-key="firstNav" data-local-index="2" role="none">
|
|
42
|
-
<div class="expanding-panel overflow-navigation-details">
|
|
43
|
-
<details class="expanding-panel-details" name="overflow-navigation-group">
|
|
44
|
-
<summary id="id-overflow-navigation-group-trigger" class="expanding-panel-summary" aria-controls="id-overflow-navigation-group-content" aria-expanded="false">
|
|
42
|
+
<div class="expanding-panel-classic overflow-navigation-details">
|
|
43
|
+
<details class="expanding-panel-classic-details" name="overflow-navigation-group">
|
|
44
|
+
<summary id="id-overflow-navigation-group-trigger" class="expanding-panel-classic-summary" aria-controls="id-overflow-navigation-group-content" aria-expanded="false">
|
|
45
45
|
<div class="label-wrapper"><span class="overflow-navigation-text" aria-label="UI Components submenu" role="menuitem" aria-haspopup="true">UI Components</span></div>
|
|
46
46
|
<div class="icon-wrapper"><span class="iconify i-mdi:chevron-down icon" aria-hidden="true"></span></div>
|
|
47
47
|
</summary>
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
</
|
|
55
|
-
|
|
56
|
-
|
|
48
|
+
</details>
|
|
49
|
+
<div id="id-overflow-navigation-group-content" class="expanding-panel-classic-content" aria-labelledby="id-overflow-navigation-group-trigger" role="region">
|
|
50
|
+
<div class="inner">
|
|
51
|
+
<div class="overflow-navigation-sub-nav-inner">
|
|
52
|
+
<ul class="overflow-navigation-sub-nav-list">
|
|
53
|
+
<li class="overflow-navigation-sub-nav-item"><a href="/forms/examples/buttons" class="overflow-navigation-sub-nav-link" role="menuitem"><span class="overflow-navigation-sub-nav-text">Buttons</span></a></li>
|
|
54
|
+
<li class="overflow-navigation-sub-nav-item"><a href="/ui/tabs" class="overflow-navigation-sub-nav-link" role="menuitem"><span class="overflow-navigation-sub-nav-text">Tabs</span></a></li>
|
|
55
|
+
</ul>
|
|
56
|
+
<div aria-hidden="true" class="overflow-sub-nav-indicator-hovered"></div>
|
|
57
57
|
</div>
|
|
58
58
|
</div>
|
|
59
|
-
</
|
|
59
|
+
</div>
|
|
60
60
|
</div>
|
|
61
61
|
</li>
|
|
62
62
|
</ul>
|
|
@@ -318,6 +318,25 @@ watch(
|
|
|
318
318
|
}
|
|
319
319
|
}
|
|
320
320
|
}
|
|
321
|
+
|
|
322
|
+
.accordian-item.expanding-panel-classic {
|
|
323
|
+
border-block-end: 1px solid var(--services-section-faq-divider-color, currentColor);
|
|
324
|
+
opacity: var(--services-section-faq-divider-opacity, 0.7);
|
|
325
|
+
|
|
326
|
+
&:first-child {
|
|
327
|
+
border-block-start: 1px solid var(--services-section-faq-divider-color, currentColor);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
.expanding-panel-classic-details .expanding-panel-classic-summary {
|
|
331
|
+
padding-block: 1.2rem;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
.expanding-panel-classic-content .inner {
|
|
335
|
+
.services-section__faq-answer {
|
|
336
|
+
line-height: var(--services-section-faq-answer-line-height, 1.6);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
}
|
|
321
340
|
}
|
|
322
341
|
}
|
|
323
342
|
}
|
|
@@ -61,18 +61,18 @@ exports[`ServicesSection > renders correct HTML structure 1`] = `
|
|
|
61
61
|
<!--v-if--><span class="text-block-0 normal">Frequently Asked Questions</span>
|
|
62
62
|
</h2>
|
|
63
63
|
<div class="display-accordian services-section__faq" id="faq">
|
|
64
|
-
<div class="expanding-panel accordian-item" icon-size="medium">
|
|
65
|
-
<details class="expanding-panel-details" name="faq-v-0-0-0">
|
|
66
|
-
<summary id="id-faq-v-0-0-0-trigger" class="expanding-panel-summary" aria-controls="id-faq-v-0-0-0-content" aria-expanded="false">
|
|
64
|
+
<div class="expanding-panel-classic accordian-item" icon-size="medium">
|
|
65
|
+
<details class="expanding-panel-classic-details" name="faq-v-0-0-0">
|
|
66
|
+
<summary id="id-faq-v-0-0-0-trigger" class="expanding-panel-classic-summary" aria-controls="id-faq-v-0-0-0-content" aria-expanded="false">
|
|
67
67
|
<div class="label-wrapper">FAQ question?</div>
|
|
68
68
|
<div class="icon-wrapper"><span class="iconify i-mdi:chevron-down" aria-hidden="true"></span></div>
|
|
69
69
|
</summary>
|
|
70
|
-
<div id="id-faq-v-0-0-0-content" class="expanding-panel-content" aria-labelledby="id-faq-v-0-0-0-trigger" role="region">
|
|
71
|
-
<div class="inner">
|
|
72
|
-
<p class="services-section__faq-answer">FAQ answer.</p>
|
|
73
|
-
</div>
|
|
74
|
-
</div>
|
|
75
70
|
</details>
|
|
71
|
+
<div id="id-faq-v-0-0-0-content" class="expanding-panel-classic-content" aria-labelledby="id-faq-v-0-0-0-trigger" role="region">
|
|
72
|
+
<div class="inner">
|
|
73
|
+
<p class="services-section__faq-answer">FAQ answer.</p>
|
|
74
|
+
</div>
|
|
75
|
+
</div>
|
|
76
76
|
</div>
|
|
77
77
|
</div>
|
|
78
78
|
<div class="glass-panel services-section__glass-panel p-24">
|