@pnx-mixtape/mxds 0.1.0-rc.5 → 0.1.0-rc.6
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/DESIGN.md +1 -1
- package/README.md +1 -1
- package/dist/build/accordion.entry.js +1 -1
- package/dist/build/base.css +1 -1
- package/dist/build/chunks/{Accordion-D2wp8hBG.js → Accordion-Bqe8vTNR.js} +1 -1
- package/dist/build/chunks/{AccordionMobile-D3sTeiM5.js → AccordionMobile-D517oXYZ.js} +1 -1
- package/dist/build/chunks/{ClosableAlert-yy84gae6.js → ClosableAlert-CZ114JsL.js} +1 -1
- package/dist/build/chunks/{Dialog-DWoXW-7Z.js → Dialog-DoYcGqhW.js} +1 -1
- package/dist/build/chunks/{FeedbackForm-BochXEDx.js → FeedbackForm-BsL7fv8s.js} +1 -1
- package/dist/build/chunks/{Gallery-Rkfv7Mbb.js → Gallery-DqJPujX5.js} +2 -2
- package/dist/build/chunks/{GlobalToggle-CzlkpLpc.js → GlobalToggle-7H3F1Wvc.js} +1 -1
- package/dist/build/chunks/{InPageNavigation-D1Kk-rCm.js → InPageNavigation-D7m3IN89.js} +1 -1
- package/dist/build/chunks/{Navigation-CDQnN61h.js → Navigation-CRModT40.js} +13 -18
- package/dist/build/chunks/{Sticky-DoVndENm.js → Sticky-C1H8MNqK.js} +1 -1
- package/dist/build/chunks/{Tabs-BDlxCDNh.js → Tabs-BJD3kABL.js} +1 -1
- package/dist/build/chunks/{Translate-bmM38j6z.js → Translate-yAgySwet.js} +1 -1
- package/dist/build/chunks/{utilities-Dd-08SW6.js → utilities-B9mNRs7V.js} +1 -1
- package/dist/build/dialog.entry.js +1 -1
- package/dist/build/elements.js +13 -13
- package/dist/build/feedback-form.entry.js +1 -1
- package/dist/build/filters.entry.js +3 -3
- package/dist/build/form.css +1 -1
- package/dist/build/gallery.entry.js +1 -1
- package/dist/build/global-alert.entry.js +1 -1
- package/dist/build/header.entry.js +1 -1
- package/dist/build/in-page-navigation.entry.js +1 -1
- package/dist/build/navigation.css +1 -1
- package/dist/build/navigation.entry.js +1 -1
- package/dist/build/react.js +230 -209
- package/dist/build/side-navigation.css +1 -1
- package/dist/build/sidebar.css +1 -1
- package/dist/build/sticky.entry.js +1 -1
- package/dist/build/tabs.entry.js +1 -1
- package/dist/build/translate.entry.js +1 -1
- package/dist/types/Component/Navigation/Elements/Navigation.d.ts +2 -3
- package/dist/types/Component/Navigation/Elements/Navigation.d.ts.map +1 -1
- package/dist/types/Component/SideNavigation/Components/SideNavigationTree.d.ts +3 -3
- package/dist/types/Component/SideNavigation/Components/SideNavigationTree.d.ts.map +1 -1
- package/dist/types/Component/SideNavigation/SideNavigation.d.ts +3 -1
- package/dist/types/Component/SideNavigation/SideNavigation.d.ts.map +1 -1
- package/dist/types/tokens.d.ts +1 -0
- package/dist/types/tokens.d.ts.map +1 -1
- package/docs/CSSGuidelines.md +2 -0
- package/package.json +5 -5
- package/src/Atom/Link/_links.css +1 -0
- package/src/Component/Navigation/Elements/Navigation.ts +25 -29
- package/src/Component/Navigation/README.md +11 -10
- package/src/Component/Navigation/__snapshots__/Navigation.stories.ts.snap +8 -8
- package/src/Component/Navigation/_navigation-dropdown.css +9 -11
- package/src/Component/Navigation/_navigation-mega.css +5 -11
- package/src/Component/Navigation/_navigation.css +5 -3
- package/src/Component/Navigation/menu.twig +1 -1
- package/src/Component/Navigation/navigation.twig +0 -1
- package/src/Component/SideNavigation/Components/SideNavigationTree.tsx +68 -24
- package/src/Component/SideNavigation/README.md +78 -32
- package/src/Component/SideNavigation/SideNavigation.stories.ts +41 -2
- package/src/Component/SideNavigation/SideNavigation.stories.tsx +31 -0
- package/src/Component/SideNavigation/SideNavigation.tsx +13 -3
- package/src/Component/SideNavigation/__snapshots__/SideNavigation.stories.ts.snap +87 -3
- package/src/Component/SideNavigation/__snapshots__/SideNavigation.stories.tsx.snap +2 -0
- package/src/Component/SideNavigation/side-navigation.css +3 -3
- package/src/Component/SideNavigation/side-navigation.twig +10 -3
- package/src/Component/Translate/README.md +4 -2
- package/src/Component/Translate/Translate.stories.ts +2 -2
- package/src/Component/Translate/Translate.stories.tsx +2 -2
- package/src/Component/Translate/Translate.tsx +1 -1
- package/src/Component/Translate/__snapshots__/Translate.stories.ts.snap +1 -1
- package/src/Component/Translate/__snapshots__/Translate.stories.tsx.snap +1 -1
- package/src/Component/Translate/translate.twig +1 -1
- package/src/Form/form.css +17 -19
- package/src/Introduction.mdx +1 -1
- package/src/Layout/Footer/__snapshots__/Footer.stories.ts.snap +3 -3
- package/src/Layout/Header/__snapshots__/Header.stories.ts.snap +6 -6
- package/src/Layout/Sidebar/sidebar.css +10 -3
- package/src/Utility/Elements/google-translate-widget.ts +1 -1
- package/src/_custom-media.css +2 -0
- package/src/tokens.js +1 -0
|
@@ -2,18 +2,26 @@
|
|
|
2
2
|
|
|
3
3
|
A vertical, multi-level navigation rendered as a `<nav>` landmark with a parent page link at the top and a nested list of sibling/child pages below. Designed to live in the aside of content layouts to expose a section's local page tree. Extends the base `.mx-nav` styles from the Navigation component.
|
|
4
4
|
|
|
5
|
+
By default every level is rendered open. Set `collapsible` to wrap the nav in `<mx-nav openActive>`, which turns each sub-list into a click-to-expand disclosure and opens the branch containing the current page.
|
|
6
|
+
|
|
5
7
|
## CSS Import
|
|
6
8
|
|
|
7
9
|
```css
|
|
8
10
|
@import "@pnx-mixtape/mxds/src/Component/SideNavigation/side-navigation.css";
|
|
9
11
|
```
|
|
10
12
|
|
|
11
|
-
Depends on the Navigation component's base styles (for `.mx-nav
|
|
13
|
+
Depends on the Navigation component's base styles (for `.mx-nav`, sub-list level classes and, when collapsible, the `.mx-nav--collapsible` toggle/transition styles):
|
|
12
14
|
|
|
13
15
|
```css
|
|
14
16
|
@import "@pnx-mixtape/mxds/src/Component/Navigation/navigation.css";
|
|
15
17
|
```
|
|
16
18
|
|
|
19
|
+
## JS Import (collapsible only)
|
|
20
|
+
|
|
21
|
+
```js
|
|
22
|
+
import "@pnx-mixtape/mxds/src/Component/Navigation/Elements/Navigation"
|
|
23
|
+
```
|
|
24
|
+
|
|
17
25
|
---
|
|
18
26
|
|
|
19
27
|
## Twig
|
|
@@ -24,6 +32,7 @@ Depends on the Navigation component's base styles (for `.mx-nav` and sub-list le
|
|
|
24
32
|
title: 'Section navigation',
|
|
25
33
|
parent: parent_link,
|
|
26
34
|
items: menu_tree,
|
|
35
|
+
collapsible: true,
|
|
27
36
|
} %}
|
|
28
37
|
```
|
|
29
38
|
|
|
@@ -31,13 +40,14 @@ Depends on the Navigation component's base styles (for `.mx-nav` and sub-list le
|
|
|
31
40
|
|
|
32
41
|
**side-navigation.twig props** (`SideNavigation` type from `ids-shape`):
|
|
33
42
|
|
|
34
|
-
| Prop
|
|
35
|
-
|
|
|
36
|
-
| `id`
|
|
37
|
-
| `title`
|
|
38
|
-
| `parent`
|
|
39
|
-
| `items`
|
|
40
|
-
| `
|
|
43
|
+
| Prop | Type | Description |
|
|
44
|
+
| ------------- | ------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
45
|
+
| `id` | `string` | Written to `.mx-side-nav__parent` and used as the `<nav>`'s `aria-labelledby` fallback when `title` is empty |
|
|
46
|
+
| `title` | `string` | `aria-label` for the `<nav>` landmark. Provide a meaningful value so assistive tech can distinguish this nav from the site's primary nav |
|
|
47
|
+
| `parent` | `Link` | Pre-rendered Link atom representing the current section's parent page, shown above the list |
|
|
48
|
+
| `items` | `MenuTree[]` | Recursive menu tree — see shape below |
|
|
49
|
+
| `collapsible` | `boolean` | Optional. Wraps the `<nav>` in `<mx-nav openActive>` and adds `mx-nav--collapsible` so sub-lists collapse behind their toggle buttons. Not part of the `ids-shape` `SideNavigation` type |
|
|
50
|
+
| `attributes` | `Attribute` | Optional Drupal-style attributes object; the template merges `mx-nav mx-side-nav` onto its classes |
|
|
41
51
|
|
|
42
52
|
**`MenuTree` shape** (from `@pnx-mixtape/ids-shape`):
|
|
43
53
|
|
|
@@ -98,13 +108,43 @@ Rules for the markup:
|
|
|
98
108
|
- The top list is `mx-nav__level-1`, children are `mx-nav__level-2`, grandchildren are `mx-nav__level-3`. Each level adds more inline-start indent.
|
|
99
109
|
- Any `<li>` that contains a nested `<ul>` must carry `mx-nav__has-subnav`.
|
|
100
110
|
- The current page's `<a>` carries `aria-current="page"` — CSS uses this to bold the label and apply the active background.
|
|
101
|
-
- Sub-lists are **not** collapsed or inert
|
|
111
|
+
- Sub-lists are **not** collapsed or inert by default — every level is rendered open.
|
|
112
|
+
|
|
113
|
+
### Collapsible
|
|
114
|
+
|
|
115
|
+
```html
|
|
116
|
+
<mx-nav openActive>
|
|
117
|
+
<nav class="mx-nav mx-side-nav mx-nav--collapsible" aria-label="Section navigation">
|
|
118
|
+
<div class="mx-side-nav__parent" id="section-nav">
|
|
119
|
+
<a href="/about">About</a>
|
|
120
|
+
</div>
|
|
121
|
+
<ul class="mx-nav__level-1">
|
|
122
|
+
<li class="mx-nav__has-subnav">
|
|
123
|
+
<a href="/about/what-we-do">What we do</a>
|
|
124
|
+
<button class="mx-nav__toggle mx-icon mx-icon--chevron-down mx-icon--only" type="button">
|
|
125
|
+
Sub-navigation
|
|
126
|
+
</button>
|
|
127
|
+
<ul class="mx-nav__level-2">
|
|
128
|
+
<li>
|
|
129
|
+
<a href="/about/what-we-do/events" aria-current="page">Events</a>
|
|
130
|
+
</li>
|
|
131
|
+
</ul>
|
|
132
|
+
</li>
|
|
133
|
+
</ul>
|
|
134
|
+
</nav>
|
|
135
|
+
</mx-nav>
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
- Sub-lists are rendered open in the markup (no `inert`), so the full tree is available without JS.
|
|
139
|
+
- On connect, `<mx-nav>` wires each `.mx-nav__toggle` to its sibling `<ul>` (`aria-controls`, `aria-expanded`, `inert`), labels each sub-list by its parent link, then (because of `openActive`) re-opens any sub-list whose parent link or descendants carry `aria-current="page"` or `.is-active`.
|
|
140
|
+
- Sub-lists not on the current page's trail start collapsed.
|
|
141
|
+
- Toggles are hidden until `<mx-nav>` is defined (`mx-nav:not(:defined) .mx-nav__toggle`).
|
|
102
142
|
|
|
103
143
|
---
|
|
104
144
|
|
|
105
145
|
## React
|
|
106
146
|
|
|
107
|
-
Uses a composition pattern — always use `SideNavigationTree` inside `SideNavigation`.
|
|
147
|
+
Uses a composition pattern — always use `SideNavigationTree` inside `SideNavigation`. `SideNavigationTree` reads `collapsible` from `SideNavigation` via context, so it is only set once.
|
|
108
148
|
|
|
109
149
|
```tsx
|
|
110
150
|
import { SideNavigation, SideNavigationTree, Link } from "@pnx-mixtape/mxds/react"
|
|
@@ -126,6 +166,7 @@ const items: MenuTree[] = [
|
|
|
126
166
|
id="section-nav"
|
|
127
167
|
title="Section navigation"
|
|
128
168
|
parent={<Link to="/about">About</Link>}
|
|
169
|
+
collapsible
|
|
129
170
|
>
|
|
130
171
|
<SideNavigationTree items={items} />
|
|
131
172
|
</SideNavigation>
|
|
@@ -133,18 +174,20 @@ const items: MenuTree[] = [
|
|
|
133
174
|
|
|
134
175
|
**`SideNavigation` Props:**
|
|
135
176
|
|
|
136
|
-
| Prop
|
|
137
|
-
|
|
|
138
|
-
| `parent`
|
|
139
|
-
| `title`
|
|
140
|
-
| `id`
|
|
141
|
-
| `
|
|
177
|
+
| Prop | Type | Required | Description |
|
|
178
|
+
| ------------- | ----------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
179
|
+
| `parent` | `ReactNode` | No | Link/element shown above the list inside `.mx-side-nav__parent` |
|
|
180
|
+
| `title` | `string` | No | `aria-label` for the `<nav>` landmark. When omitted, falls back to `aria-labelledby` pointing at the parent wrapper |
|
|
181
|
+
| `id` | `string` | No | `id` on `.mx-side-nav__parent`, also used as the `aria-labelledby` target when `title` is not set. Auto-generated if omitted |
|
|
182
|
+
| `collapsible` | `boolean` | No | Adds `mx-nav--collapsible` and makes every nested `SideNavigationTree` sub-list a disclosure (`aria-expanded`/`aria-controls` on the toggle, `inert` when closed). Branches containing the current item start open. Default `false` |
|
|
183
|
+
| `className` | `string` | No | Extra class on the `<nav>` |
|
|
142
184
|
|
|
143
185
|
**`SideNavigationTree` Props:**
|
|
144
186
|
|
|
145
|
-
| Prop
|
|
146
|
-
|
|
|
147
|
-
| `items`
|
|
187
|
+
| Prop | Type | Required | Description |
|
|
188
|
+
| ---------- | --------------- | -------- | ----------------------------------------------------------------------------- |
|
|
189
|
+
| `items` | `MenuTree[]` | Yes | Recursive menu tree — rendered as nested `<ul class="mx-nav__level-N">` lists |
|
|
190
|
+
| `...props` | `ul` attributes | No | Spread onto the `<ul>` |
|
|
148
191
|
|
|
149
192
|
**`MenuTree` shape** (from `@pnx-mixtape/ids-shape`):
|
|
150
193
|
|
|
@@ -159,17 +202,18 @@ const items: MenuTree[] = [
|
|
|
159
202
|
|
|
160
203
|
## Modifiers
|
|
161
204
|
|
|
162
|
-
SideNavigation uses the base `.mx-nav` class for list reset and the component-specific classes below.
|
|
205
|
+
SideNavigation uses the base `.mx-nav` class for list reset and the component-specific classes below. The only variant is `mx-nav--collapsible`.
|
|
163
206
|
|
|
164
|
-
| Class | Applied by | Visual intent
|
|
165
|
-
| --------------------------------------------------------- | -------------------------------------- |
|
|
166
|
-
| `mx-nav` | Always (shared with Navigation) | Base list reset — no bullets, vertical layout
|
|
167
|
-
| `mx-side-nav` | Always | Side-nav specific styling: per-item dividers, indent ramp across levels, current-page background
|
|
168
|
-
| `mx-side-nav__parent` | Always | Parent-page wrapper above the list — bold label, thicker bottom border
|
|
169
|
-
| `mx-
|
|
170
|
-
| `mx-
|
|
171
|
-
| `mx-
|
|
172
|
-
| `
|
|
207
|
+
| Class | Applied by | Visual intent |
|
|
208
|
+
| --------------------------------------------------------- | -------------------------------------- | ---------------------------------------------------------------------------------------------------------------- |
|
|
209
|
+
| `mx-nav` | Always (shared with Navigation) | Base list reset — no bullets, vertical layout |
|
|
210
|
+
| `mx-side-nav` | Always | Side-nav specific styling: per-item dividers, indent ramp across levels, current-page background |
|
|
211
|
+
| `mx-side-nav__parent` | Always | Parent-page wrapper above the list — bold label, thicker bottom border |
|
|
212
|
+
| `mx-nav--collapsible` | Twig `collapsible: true` | Shows the sub-list toggles and applies the collapsible nav's `inert`/transition styles |
|
|
213
|
+
| `mx-nav__level-1` / `mx-nav__level-2` / `mx-nav__level-3` | Structural | Sub-list nesting level — each level increases `--indent` |
|
|
214
|
+
| `mx-nav__has-subnav` | Structural | Applied to any `<li>` that contains a child `<ul>` |
|
|
215
|
+
| `mx-nav__toggle` | Structural (inherited from Navigation) | Hidden (`display: none`) unless the nav is `mx-nav--collapsible`, where it opens and closes its sibling sub-list |
|
|
216
|
+
| `is-active` | Authoring | Alternative to `aria-current="page"` for marking the current item — styled identically |
|
|
173
217
|
|
|
174
218
|
---
|
|
175
219
|
|
|
@@ -180,8 +224,10 @@ SideNavigation uses the base `.mx-nav` class for list reset and the component-sp
|
|
|
180
224
|
- Uses semantic nested `<ul>` / `<li>` lists, so assistive tech announces list depth and item count at each level.
|
|
181
225
|
- The current page is marked with `aria-current="page"` on the `<a>`. Style-only bolding is a visual reinforcement — the ARIA attribute is what assistive tech announces. Avoid relying on the background/weight alone.
|
|
182
226
|
- Active-trail ancestors (parent items on the path to the current page) are not given `aria-current` — only the exact current page. Apply the `is-active` class if you want to visually mark ancestors with the same active styling.
|
|
183
|
-
- Keyboard navigation: every item is a native `<a>`, so tab order follows source order
|
|
184
|
-
- The `.mx-nav__toggle` button is
|
|
227
|
+
- Keyboard navigation: every item is a native `<a>`, so tab order follows source order. In the default variant sub-lists are always visible, so there is no expand/collapse interaction to manage.
|
|
228
|
+
- The `.mx-nav__toggle` button is hidden via CSS in the default variant.
|
|
229
|
+
- Collapsible (Twig and React): each toggle is a native `<button>` with `aria-expanded` and `aria-controls`; collapsed sub-lists are `inert`, so they drop out of the tab order and the accessibility tree. In Twig, `<mx-nav>` also adds the shared `Keyboard` arrow-key handling between links; React has no equivalent. `Escape` does not close sub-lists in either — that is only wired up for `flyout` navs.
|
|
230
|
+
- Collapsible: every toggle's accessible name is "Sub-navigation". Screen-reader users get the context from the preceding link, but the name alone doesn't say which section it opens.
|
|
185
231
|
|
|
186
232
|
---
|
|
187
233
|
|
|
@@ -205,7 +251,7 @@ SideNavigation uses the base `.mx-nav` class for list reset and the component-sp
|
|
|
205
251
|
}
|
|
206
252
|
```
|
|
207
253
|
|
|
208
|
-
Level 2 and level 3 sub-lists override `--indent` to step the text further in — at narrow widths with `--spacing-s` / `--spacing-m`, and at the `--
|
|
254
|
+
Level 2 and level 3 sub-lists override `--indent` to step the text further in — at narrow widths with `--spacing-s` / `--spacing-m`, and at the `--sidebar-up` breakpoint with `--spacing-m` / `--spacing-l`.
|
|
209
255
|
|
|
210
256
|
---
|
|
211
257
|
|
|
@@ -1,11 +1,17 @@
|
|
|
1
1
|
import { Meta, StoryObj } from "@storybook/html-vite"
|
|
2
|
+
import { expect } from "storybook/test"
|
|
2
3
|
import Component from "./side-navigation.twig"
|
|
3
4
|
import Link from "../../Atom/Link/link.twig"
|
|
4
5
|
import "../Navigation/navigation.css"
|
|
5
6
|
import "./side-navigation.css"
|
|
7
|
+
import "../Navigation/Elements/Navigation"
|
|
6
8
|
import { SideNavigation as NavigationType } from "@pnx-mixtape/ids-shape"
|
|
7
9
|
|
|
8
|
-
|
|
10
|
+
type SideNavigationType = NavigationType & {
|
|
11
|
+
collapsible?: boolean
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const meta: Meta<SideNavigationType> = {
|
|
9
15
|
tags: ["autodocs", "ids-mvp"],
|
|
10
16
|
component: Component,
|
|
11
17
|
args: {
|
|
@@ -128,10 +134,43 @@ const meta: Meta<NavigationType> = {
|
|
|
128
134
|
required: true,
|
|
129
135
|
},
|
|
130
136
|
},
|
|
137
|
+
collapsible: {
|
|
138
|
+
type: "boolean",
|
|
139
|
+
},
|
|
131
140
|
},
|
|
132
141
|
}
|
|
133
142
|
|
|
134
143
|
export default meta
|
|
135
|
-
type Story = StoryObj<
|
|
144
|
+
type Story = StoryObj<SideNavigationType>
|
|
136
145
|
|
|
137
146
|
export const SideNavigation: Story = {}
|
|
147
|
+
|
|
148
|
+
export const Collapsible: Story = {
|
|
149
|
+
args: {
|
|
150
|
+
collapsible: true,
|
|
151
|
+
},
|
|
152
|
+
play: async ({ canvas, userEvent, step }) => {
|
|
153
|
+
const item = canvas.getByRole("link", { name: "What we do" }).closest("li")
|
|
154
|
+
const trigger = item.querySelector(":scope > .mx-nav__toggle")
|
|
155
|
+
const list = item.querySelector(":scope > ul")
|
|
156
|
+
|
|
157
|
+
await step("Opens the active trail", async () => {
|
|
158
|
+
await expect(trigger).toHaveAttribute("aria-expanded", "true")
|
|
159
|
+
await expect(trigger).toHaveAttribute("aria-controls", list.id)
|
|
160
|
+
await expect(list).not.toHaveAttribute("inert")
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
await step("Closes on click", async () => {
|
|
164
|
+
await userEvent.click(trigger)
|
|
165
|
+
await expect(trigger).toHaveAttribute("aria-expanded", "false")
|
|
166
|
+
await expect(list).toHaveAttribute("inert")
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
await step("Reopens on click", async () => {
|
|
170
|
+
await userEvent.click(trigger)
|
|
171
|
+
await expect(trigger).toHaveAttribute("aria-expanded", "true")
|
|
172
|
+
await expect(list).not.toHaveAttribute("inert")
|
|
173
|
+
;(trigger as HTMLElement).blur()
|
|
174
|
+
})
|
|
175
|
+
},
|
|
176
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Meta, StoryObj } from "@storybook/react-vite"
|
|
2
|
+
import { expect } from "storybook/test"
|
|
2
3
|
import Component from "./SideNavigation"
|
|
3
4
|
import SideNavigationTree from "./Components/SideNavigationTree"
|
|
4
5
|
import "../Navigation/navigation.css"
|
|
@@ -66,3 +67,33 @@ const meta: Meta<typeof Component> = {
|
|
|
66
67
|
export default meta
|
|
67
68
|
|
|
68
69
|
export const SideNavigation: StoryObj<typeof meta> = {}
|
|
70
|
+
|
|
71
|
+
export const Collapsible: StoryObj<typeof meta> = {
|
|
72
|
+
args: {
|
|
73
|
+
collapsible: true,
|
|
74
|
+
},
|
|
75
|
+
play: async ({ canvas, userEvent, step }) => {
|
|
76
|
+
const item = canvas.getByRole("link", { name: "What we do" }).closest("li")
|
|
77
|
+
const trigger = item.querySelector(":scope > .mx-nav__toggle")
|
|
78
|
+
const list = item.querySelector(":scope > ul")
|
|
79
|
+
|
|
80
|
+
await step("Opens the active trail", async () => {
|
|
81
|
+
await expect(trigger).toHaveAttribute("aria-expanded", "true")
|
|
82
|
+
await expect(trigger).toHaveAttribute("aria-controls", list.id)
|
|
83
|
+
await expect(list).not.toHaveAttribute("inert")
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
await step("Closes on click", async () => {
|
|
87
|
+
await userEvent.click(trigger)
|
|
88
|
+
await expect(trigger).toHaveAttribute("aria-expanded", "false")
|
|
89
|
+
await expect(list).toHaveAttribute("inert")
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
await step("Reopens on click", async () => {
|
|
93
|
+
await userEvent.click(trigger)
|
|
94
|
+
await expect(trigger).toHaveAttribute("aria-expanded", "true")
|
|
95
|
+
await expect(list).not.toHaveAttribute("inert")
|
|
96
|
+
;(trigger as HTMLElement).blur()
|
|
97
|
+
})
|
|
98
|
+
},
|
|
99
|
+
}
|
|
@@ -1,18 +1,23 @@
|
|
|
1
|
-
import { JSX, ReactNode, PropsWithChildren, useId } from "react"
|
|
1
|
+
import { JSX, ReactNode, PropsWithChildren, createContext, useId } from "react"
|
|
2
2
|
import classNames from "classnames"
|
|
3
3
|
|
|
4
4
|
type SideNavigationProps = PropsWithChildren & {
|
|
5
5
|
parent?: ReactNode
|
|
6
6
|
title?: string
|
|
7
7
|
id?: string
|
|
8
|
+
collapsible?: boolean
|
|
8
9
|
className?: string
|
|
9
10
|
}
|
|
10
11
|
|
|
12
|
+
// Whether sub-lists in a nested SideNavigationTree can be collapsed.
|
|
13
|
+
export const SideNavigationContext = createContext<boolean>(false)
|
|
14
|
+
|
|
11
15
|
const SideNavigation = ({
|
|
12
16
|
children,
|
|
13
17
|
parent,
|
|
14
18
|
title,
|
|
15
19
|
id,
|
|
20
|
+
collapsible = false,
|
|
16
21
|
className,
|
|
17
22
|
}: SideNavigationProps): JSX.Element => {
|
|
18
23
|
const autoId = useId()
|
|
@@ -20,13 +25,18 @@ const SideNavigation = ({
|
|
|
20
25
|
|
|
21
26
|
return (
|
|
22
27
|
<nav
|
|
23
|
-
className={classNames(
|
|
28
|
+
className={classNames(
|
|
29
|
+
"mx-nav",
|
|
30
|
+
"mx-side-nav",
|
|
31
|
+
{ "mx-nav--collapsible": collapsible },
|
|
32
|
+
className,
|
|
33
|
+
)}
|
|
24
34
|
{...(title ? { "aria-label": title } : { "aria-labelledby": navId })}
|
|
25
35
|
>
|
|
26
36
|
<div className="mx-side-nav__parent" id={navId}>
|
|
27
37
|
{parent}
|
|
28
38
|
</div>
|
|
29
|
-
{children}
|
|
39
|
+
<SideNavigationContext value={collapsible}>{children}</SideNavigationContext>
|
|
30
40
|
</nav>
|
|
31
41
|
)
|
|
32
42
|
}
|
|
@@ -1,9 +1,93 @@
|
|
|
1
1
|
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
|
2
2
|
|
|
3
|
+
exports[`Collapsible 1`] = `
|
|
4
|
+
"
|
|
5
|
+
|
|
6
|
+
<mx-nav openactive="">
|
|
7
|
+
<nav class="mx-nav mx-side-nav mx-nav--collapsible" aria-label="Sidebar nav">
|
|
8
|
+
<div class="mx-side-nav__parent" id="sidenav-story">
|
|
9
|
+
|
|
10
|
+
<a href="#">
|
|
11
|
+
|
|
12
|
+
About
|
|
13
|
+
</a>
|
|
14
|
+
|
|
15
|
+
</div>
|
|
16
|
+
<ul class="mx-nav__level-1">
|
|
17
|
+
<li>
|
|
18
|
+
<a href="#">
|
|
19
|
+
News
|
|
20
|
+
</a>
|
|
21
|
+
</li>
|
|
22
|
+
<li class="mx-nav__has-subnav">
|
|
23
|
+
<a href="#" id="unique-2">
|
|
24
|
+
What we do
|
|
25
|
+
</a>
|
|
26
|
+
<button class="mx-nav__toggle mx-icon mx-icon--chevron-down mx-icon--only" type="button" aria-expanded="true" aria-controls="unique-0">Sub-navigation</button>
|
|
27
|
+
<ul class="mx-nav__level-2" id="unique-0" aria-labelledby="unique-1">
|
|
28
|
+
<li>
|
|
29
|
+
<a href="#">
|
|
30
|
+
Events
|
|
31
|
+
</a>
|
|
32
|
+
</li>
|
|
33
|
+
<li class="mx-nav__has-subnav">
|
|
34
|
+
<a href="#" id="unique-3">
|
|
35
|
+
Who we are
|
|
36
|
+
</a>
|
|
37
|
+
<button class="mx-nav__toggle mx-icon mx-icon--chevron-down mx-icon--only" type="button" aria-expanded="true" aria-controls="unique-2">Sub-navigation</button>
|
|
38
|
+
<ul class="mx-nav__level-3" id="unique-1" aria-labelledby="unique-3">
|
|
39
|
+
<li>
|
|
40
|
+
<a href="#">
|
|
41
|
+
Our team
|
|
42
|
+
</a>
|
|
43
|
+
</li>
|
|
44
|
+
<li>
|
|
45
|
+
<a href="#" aria-current="page">
|
|
46
|
+
Our philosophy
|
|
47
|
+
</a>
|
|
48
|
+
</li>
|
|
49
|
+
<li>
|
|
50
|
+
<a href="#">
|
|
51
|
+
Why music?
|
|
52
|
+
</a>
|
|
53
|
+
</li>
|
|
54
|
+
</ul>
|
|
55
|
+
|
|
56
|
+
</li>
|
|
57
|
+
<li>
|
|
58
|
+
<a href="#">
|
|
59
|
+
Join us
|
|
60
|
+
</a>
|
|
61
|
+
</li>
|
|
62
|
+
<li>
|
|
63
|
+
<a href="#">
|
|
64
|
+
Careers
|
|
65
|
+
</a>
|
|
66
|
+
</li>
|
|
67
|
+
</ul>
|
|
68
|
+
|
|
69
|
+
</li>
|
|
70
|
+
<li>
|
|
71
|
+
<a href="#">
|
|
72
|
+
Contact
|
|
73
|
+
</a>
|
|
74
|
+
</li>
|
|
75
|
+
<li>
|
|
76
|
+
<a href="#">
|
|
77
|
+
Resources
|
|
78
|
+
</a>
|
|
79
|
+
</li>
|
|
80
|
+
</ul>
|
|
81
|
+
|
|
82
|
+
</nav>
|
|
83
|
+
</mx-nav>
|
|
84
|
+
"
|
|
85
|
+
`;
|
|
86
|
+
|
|
3
87
|
exports[`Side Navigation 1`] = `
|
|
4
88
|
"
|
|
5
89
|
|
|
6
|
-
<nav class="mx-nav mx-side-nav" aria-label="Sidebar nav"
|
|
90
|
+
<nav class="mx-nav mx-side-nav" aria-label="Sidebar nav">
|
|
7
91
|
<div class="mx-side-nav__parent" id="sidenav-story">
|
|
8
92
|
|
|
9
93
|
<a href="#">
|
|
@@ -22,7 +106,7 @@ exports[`Side Navigation 1`] = `
|
|
|
22
106
|
<a href="#">
|
|
23
107
|
What we do
|
|
24
108
|
</a>
|
|
25
|
-
<button class="mx-nav__toggle mx-icon mx-icon--chevron-down mx-icon--only">Sub-navigation</button>
|
|
109
|
+
<button class="mx-nav__toggle mx-icon mx-icon--chevron-down mx-icon--only" type="button">Sub-navigation</button>
|
|
26
110
|
<ul class="mx-nav__level-2">
|
|
27
111
|
<li>
|
|
28
112
|
<a href="#">
|
|
@@ -33,7 +117,7 @@ exports[`Side Navigation 1`] = `
|
|
|
33
117
|
<a href="#">
|
|
34
118
|
Who we are
|
|
35
119
|
</a>
|
|
36
|
-
<button class="mx-nav__toggle mx-icon mx-icon--chevron-down mx-icon--only">Sub-navigation</button>
|
|
120
|
+
<button class="mx-nav__toggle mx-icon mx-icon--chevron-down mx-icon--only" type="button">Sub-navigation</button>
|
|
37
121
|
<ul class="mx-nav__level-3">
|
|
38
122
|
<li>
|
|
39
123
|
<a href="#">
|
|
@@ -1,3 +1,5 @@
|
|
|
1
1
|
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
|
2
2
|
|
|
3
|
+
exports[`Collapsible 1`] = `"<nav class="mx-nav mx-side-nav mx-nav--collapsible" aria-label="Sidebar nav"><div class="mx-side-nav__parent" id="sidenav-story"><a class="mx-link" href="#">About</a></div><ul class="mx-nav__level-1"><li><a href="#" class="mx-link">News</a></li><li class="mx-nav__has-subnav"><a href="#" class="mx-link" id="_r_17_">What we do</a><button class="mx-nav__toggle mx-icon mx-icon--chevron-down mx-icon--only" type="button" aria-expanded="true" aria-controls="_r_16_">Sub-navigation</button><ul class="mx-nav__level-2" id="_r_16_" aria-labelledby="_r_17_"><li><a href="#" class="mx-link">Events</a></li><li class="mx-nav__has-subnav"><a href="#" class="mx-link" id="_r_1d_">Who we are</a><button class="mx-nav__toggle mx-icon mx-icon--chevron-down mx-icon--only" type="button" aria-expanded="true" aria-controls="_r_1c_">Sub-navigation</button><ul class="mx-nav__level-3" id="_r_1c_" aria-labelledby="_r_1d_"><li><a href="#" class="mx-link">Our team</a></li><li><a href="#" class="mx-link" aria-current="page">Our philosophy</a></li><li><a href="#" class="mx-link">Why music?</a></li></ul></li><li><a href="#" class="mx-link">Join us</a></li><li><a href="#" class="mx-link">Careers</a></li></ul></li><li><a href="#" class="mx-link">Contact</a></li><li><a href="#" class="mx-link">Resources</a></li></ul></nav>"`;
|
|
4
|
+
|
|
3
5
|
exports[`Side Navigation 1`] = `"<nav class="mx-nav mx-side-nav" aria-label="Sidebar nav"><div class="mx-side-nav__parent" id="sidenav-story"><a class="mx-link" href="#">About</a></div><ul class="mx-nav__level-1"><li><a href="#" class="mx-link">News</a></li><li class="mx-nav__has-subnav"><a href="#" class="mx-link">What we do</a><button class="mx-nav__toggle mx-icon mx-icon--chevron-down mx-icon--only" type="button">Sub-navigation</button><ul class="mx-nav__level-2"><li><a href="#" class="mx-link">Events</a></li><li class="mx-nav__has-subnav"><a href="#" class="mx-link">Who we are</a><button class="mx-nav__toggle mx-icon mx-icon--chevron-down mx-icon--only" type="button">Sub-navigation</button><ul class="mx-nav__level-3"><li><a href="#" class="mx-link">Our team</a></li><li><a href="#" class="mx-link" aria-current="page">Our philosophy</a></li><li><a href="#" class="mx-link">Why music?</a></li></ul></li><li><a href="#" class="mx-link">Join us</a></li><li><a href="#" class="mx-link">Careers</a></li></ul></li><li><a href="#" class="mx-link">Contact</a></li><li><a href="#" class="mx-link">Resources</a></li></ul></nav>"`;
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
& .mx-nav__level-2 > li > a {
|
|
42
42
|
--indent: var(--spacing-s);
|
|
43
43
|
|
|
44
|
-
@media (--
|
|
44
|
+
@media (--sidebar-up) {
|
|
45
45
|
--indent: var(--spacing-m);
|
|
46
46
|
}
|
|
47
47
|
}
|
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
& .mx-nav__level-3 > li > a {
|
|
50
50
|
--indent: var(--spacing-m);
|
|
51
51
|
|
|
52
|
-
@media (--
|
|
52
|
+
@media (--sidebar-up) {
|
|
53
53
|
--indent: var(--spacing-l);
|
|
54
54
|
}
|
|
55
55
|
}
|
|
@@ -62,7 +62,7 @@
|
|
|
62
62
|
color: var(--nav-active-foreground, inherit);
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
-
|
|
65
|
+
&:not(.mx-nav--collapsible) .mx-nav__toggle {
|
|
66
66
|
display: none;
|
|
67
67
|
}
|
|
68
68
|
}
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
{% set baseClass = 'mx-nav' %}
|
|
3
3
|
{% set classes = [
|
|
4
4
|
baseClass,
|
|
5
|
-
'mx-side-nav'
|
|
5
|
+
'mx-side-nav',
|
|
6
|
+
collapsible ? 'mx-nav--collapsible' : null,
|
|
6
7
|
] %}
|
|
7
8
|
{% set attributes = (attributes ?? create_attribute()).addClass(classes) %}
|
|
8
9
|
|
|
@@ -19,7 +20,7 @@
|
|
|
19
20
|
{{ item.link.title }}
|
|
20
21
|
{% endif %}
|
|
21
22
|
{% if item.items is not empty %}
|
|
22
|
-
<button class="mx-nav__toggle mx-icon mx-icon--chevron-down mx-icon--only">Sub-navigation</button>
|
|
23
|
+
<button class="mx-nav__toggle mx-icon mx-icon--chevron-down mx-icon--only" type="button">Sub-navigation</button>
|
|
23
24
|
{{ nav.menutree(item.items, level + 1, setInert, baseClass) }}
|
|
24
25
|
{% endif %}
|
|
25
26
|
</li>
|
|
@@ -27,9 +28,15 @@
|
|
|
27
28
|
</ul>
|
|
28
29
|
{% endmacro %}
|
|
29
30
|
|
|
30
|
-
|
|
31
|
+
{% if collapsible %}
|
|
32
|
+
<mx-nav openActive>
|
|
33
|
+
{% endif %}
|
|
34
|
+
<nav{{ attributes }}{% if title is not empty %} aria-label="{{ title }}"{% else %} aria-labelledby="{{ id }}"{% endif %}>
|
|
31
35
|
<div class="mx-side-nav__parent" id="{{ id }}">
|
|
32
36
|
{{ parent }}
|
|
33
37
|
</div>
|
|
34
38
|
{{ nav.menutree(items, 0, false, baseClass) }}
|
|
35
39
|
</nav>
|
|
40
|
+
{% if collapsible %}
|
|
41
|
+
</mx-nav>
|
|
42
|
+
{% endif %}
|
|
@@ -81,7 +81,7 @@ import "@pnx-mixtape/mxds/src/Component/Translate/Elements/Translate"
|
|
|
81
81
|
popovertarget="translate"
|
|
82
82
|
style="anchor-name: --translate"
|
|
83
83
|
data-label="Language"
|
|
84
|
-
aria-label="Language"
|
|
84
|
+
aria-label="Language, EN"
|
|
85
85
|
>
|
|
86
86
|
<span class="mx-icon mx-icon--globe"></span>
|
|
87
87
|
<span class="mx-translate__code">EN</span>
|
|
@@ -230,8 +230,10 @@ No BEM modifier classes.
|
|
|
230
230
|
## Accessibility
|
|
231
231
|
|
|
232
232
|
- By default, the trigger's accessible name (`aria-label`) updates to reflect the active language
|
|
233
|
-
(e.g. `"Language: Français"`) rather than staying static — pass `updateTriggerLabel={false}` to
|
|
233
|
+
(e.g. `"Language: Français, FR"`) rather than staying static — pass `updateTriggerLabel={false}` to
|
|
234
234
|
keep it static instead, which some sites prefer.
|
|
235
|
+
- The accessible name always includes the visible language code (e.g. `EN`) so it satisfies WCAG
|
|
236
|
+
2.5.3 Label in Name — speech-input users can activate the trigger by saying what they see.
|
|
235
237
|
- Each language link gets `lang`/`hreflang` set to its own code, so screen readers pronounce each
|
|
236
238
|
language name using that language's own phonetics.
|
|
237
239
|
- The active language gets `aria-current="true"`, cleared from the previously active one.
|
|
@@ -44,7 +44,7 @@ export const Translate: Story = {
|
|
|
44
44
|
// (and a fresh view of the story) always starts from the real default state.
|
|
45
45
|
window.localStorage.removeItem("googtrans")
|
|
46
46
|
|
|
47
|
-
const trigger = canvas.getByRole("button", { name:
|
|
47
|
+
const trigger = canvas.getByRole("button", { name: /^Language\b/ })
|
|
48
48
|
const menu = canvasElement.querySelector("#translate") as HTMLElement
|
|
49
49
|
const french = await canvas.findByRole("menuitem", { name: "Français", hidden: true })
|
|
50
50
|
// The notice is created via JS on first use (not pre-rendered) and inserted at the
|
|
@@ -71,7 +71,7 @@ export const Translate: Story = {
|
|
|
71
71
|
async () => {
|
|
72
72
|
await userEvent.click(french)
|
|
73
73
|
await waitFor(() => expect(french).toHaveAttribute("aria-current", "true"))
|
|
74
|
-
await expect(trigger).toHaveAttribute("aria-label", "Language: Français")
|
|
74
|
+
await expect(trigger).toHaveAttribute("aria-label", "Language: Français, FR")
|
|
75
75
|
await waitFor(() => expect(findNotice()).not.toHaveAttribute("hidden"))
|
|
76
76
|
},
|
|
77
77
|
)
|
|
@@ -35,7 +35,7 @@ export const Translate: StoryObj<typeof meta> = {
|
|
|
35
35
|
// (and a fresh view of the story) always starts from the real default state.
|
|
36
36
|
window.localStorage.removeItem("googtrans")
|
|
37
37
|
|
|
38
|
-
const trigger = canvas.getByRole("button", { name:
|
|
38
|
+
const trigger = canvas.getByRole("button", { name: /^Language\b/ })
|
|
39
39
|
const menu = canvasElement.querySelector("#translate") as HTMLElement
|
|
40
40
|
const french = await canvas.findByRole("menuitem", { name: "Français", hidden: true })
|
|
41
41
|
// The notice is created via JS on first use (not pre-rendered) and inserted at the
|
|
@@ -62,7 +62,7 @@ export const Translate: StoryObj<typeof meta> = {
|
|
|
62
62
|
async () => {
|
|
63
63
|
await userEvent.click(french)
|
|
64
64
|
await waitFor(() => expect(french).toHaveAttribute("aria-current", "true"))
|
|
65
|
-
await expect(trigger).toHaveAttribute("aria-label", "Language: Français")
|
|
65
|
+
await expect(trigger).toHaveAttribute("aria-label", "Language: Français, FR")
|
|
66
66
|
await waitFor(() => expect(findNotice()).not.toHaveAttribute("hidden"))
|
|
67
67
|
},
|
|
68
68
|
)
|
|
@@ -186,7 +186,7 @@ const Translate = ({
|
|
|
186
186
|
popoverTarget={rootId}
|
|
187
187
|
style={triggerStyle}
|
|
188
188
|
data-label={label}
|
|
189
|
-
aria-label={label}
|
|
189
|
+
aria-label={`${label}, ${defaultLanguage.toUpperCase()}`}
|
|
190
190
|
>
|
|
191
191
|
<Icon icon={Icons.GLOBE} />
|
|
192
192
|
<span className="mx-translate__code">{defaultLanguage.toUpperCase()}</span>
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
exports[`Translate 1`] = `
|
|
4
4
|
"
|
|
5
5
|
<mx-translate class="mx-translate" closeonclick="" data-default-language="en" data-update-trigger-label="true" data-notice-content="Our website uses an automatic service to translate content. These translations should be used as a guide only.">
|
|
6
|
-
<button id="unique-0" type="button" class="mx-button mx-button--light mx-button--small mx-translate__trigger mx-popover__trigger notranslate" popovertarget="translate" style="anchor-name: --translate" data-label="Language" aria-label="Language: English">
|
|
6
|
+
<button id="unique-0" type="button" class="mx-button mx-button--light mx-button--small mx-translate__trigger mx-popover__trigger notranslate" popovertarget="translate" style="anchor-name: --translate" data-label="Language" aria-label="Language: English, EN">
|
|
7
7
|
|
|
8
8
|
<span class="mx-icon mx-icon--globe"></span>
|
|
9
9
|
<span class="mx-translate__code">EN</span>
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
|
2
2
|
|
|
3
|
-
exports[`Translate 1`] = `"<div class="mx-translate"><button type="button" id="translate-target" class="mx-translate__trigger mx-popover__trigger notranslate" popovertarget="translate" data-label="Language" aria-label="Language: English" style="anchor-name: --translate;"><span class="mx-icon mx-icon--globe"></span><span class="mx-translate__code">EN</span></button><div id="translate" class="mx-popover mx-translate__menu notranslate" aria-describedby="translate-target" aria-labelledby="translate-target" data-placement="bottom-start" popover="" role="menu" style="position-anchor: --translate;"><span data-focus-scope-start="true" hidden=""></span><a class="mx-translate__item" href="?hl=en" lang="en" hreflang="en" role="menuitem" data-language="en" aria-current="true">English</a><a class="mx-translate__item" href="?hl=fr" lang="fr" hreflang="fr" role="menuitem" data-language="fr">Français</a><a class="mx-translate__item" href="?hl=es" lang="es" hreflang="es" role="menuitem" data-language="es">Español</a><span data-focus-scope-end="true" hidden=""></span></div></div>"`;
|
|
3
|
+
exports[`Translate 1`] = `"<div class="mx-translate"><button type="button" id="translate-target" class="mx-translate__trigger mx-popover__trigger notranslate" popovertarget="translate" data-label="Language" aria-label="Language: English, EN" style="anchor-name: --translate;"><span class="mx-icon mx-icon--globe"></span><span class="mx-translate__code">EN</span></button><div id="translate" class="mx-popover mx-translate__menu notranslate" aria-describedby="translate-target" aria-labelledby="translate-target" data-placement="bottom-start" popover="" role="menu" style="position-anchor: --translate;"><span data-focus-scope-start="true" hidden=""></span><a class="mx-translate__item" href="?hl=en" lang="en" hreflang="en" role="menuitem" data-language="en" aria-current="true">English</a><a class="mx-translate__item" href="?hl=fr" lang="fr" hreflang="fr" role="menuitem" data-language="fr">Français</a><a class="mx-translate__item" href="?hl=es" lang="es" hreflang="es" role="menuitem" data-language="es">Español</a><span data-focus-scope-end="true" hidden=""></span></div></div>"`;
|