@duro-app/ui 5.4.0 → 5.6.0
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/dist/{Table-Cm5cCnZD.js → Table-B1sWL3GA.js} +668 -650
- package/dist/Table-B1sWL3GA.js.map +1 -0
- package/dist/components/Breadcrumb/Breadcrumb.d.ts +37 -0
- package/dist/components/Breadcrumb/Breadcrumb.d.ts.map +1 -0
- package/dist/components/Breadcrumb/Breadcrumb.meta.d.ts +3 -0
- package/dist/components/Breadcrumb/Breadcrumb.meta.d.ts.map +1 -0
- package/dist/components/Breadcrumb/Breadcrumb.stories.d.ts +18 -0
- package/dist/components/Breadcrumb/Breadcrumb.stories.d.ts.map +1 -0
- package/dist/components/Breadcrumb/styles.css.d.ts +46 -0
- package/dist/components/Breadcrumb/styles.css.d.ts.map +1 -0
- package/dist/components/Button/Button.d.ts +2 -1
- package/dist/components/Button/Button.d.ts.map +1 -1
- package/dist/components/Button/styles.css.d.ts +1 -0
- package/dist/components/Button/styles.css.d.ts.map +1 -1
- package/dist/components/ButtonGroup/ButtonGroup.stories.d.ts +14 -0
- package/dist/components/ButtonGroup/ButtonGroup.stories.d.ts.map +1 -1
- package/dist/components/Inline/Inline.meta.d.ts.map +1 -1
- package/dist/components/LinkButton/LinkButton.d.ts +8 -1
- package/dist/components/LinkButton/LinkButton.d.ts.map +1 -1
- package/dist/components/LinkButton/LinkButton.stories.d.ts +9 -0
- package/dist/components/LinkButton/LinkButton.stories.d.ts.map +1 -1
- package/dist/components/LinkButton/styles.css.d.ts +1 -0
- package/dist/components/LinkButton/styles.css.d.ts.map +1 -1
- package/dist/components/Menu/Menu.d.ts +4 -1
- package/dist/components/Menu/Menu.d.ts.map +1 -1
- package/dist/components/Menu/styles.css.d.ts +14 -0
- package/dist/components/Menu/styles.css.d.ts.map +1 -1
- package/dist/components/PageNav/PageNav.d.ts +30 -0
- package/dist/components/PageNav/PageNav.d.ts.map +1 -0
- package/dist/components/PageNav/PageNav.meta.d.ts +3 -0
- package/dist/components/PageNav/PageNav.meta.d.ts.map +1 -0
- package/dist/components/PageNav/PageNav.stories.d.ts +16 -0
- package/dist/components/PageNav/PageNav.stories.d.ts.map +1 -0
- package/dist/components/PageNav/styles.css.d.ts +51 -0
- package/dist/components/PageNav/styles.css.d.ts.map +1 -0
- package/dist/components/Popover/Popover.d.ts +14 -2
- package/dist/components/Popover/Popover.d.ts.map +1 -1
- package/dist/components/Popover/Popover.meta.d.ts.map +1 -1
- package/dist/components/Popover/PopoverLayerContext.d.ts +3 -0
- package/dist/components/Popover/PopoverLayerContext.d.ts.map +1 -1
- package/dist/components/Popover/PopoverRaised.stories.d.ts +28 -0
- package/dist/components/Popover/PopoverRaised.stories.d.ts.map +1 -0
- package/dist/components/Popover/styles.css.d.ts +15 -0
- package/dist/components/Popover/styles.css.d.ts.map +1 -1
- package/dist/components/Select/Select.d.ts +4 -1
- package/dist/components/Select/Select.d.ts.map +1 -1
- package/dist/components/Select/styles.css.d.ts +9 -0
- package/dist/components/Select/styles.css.d.ts.map +1 -1
- package/dist/components/TableOfContents/TableOfContents.d.ts +53 -0
- package/dist/components/TableOfContents/TableOfContents.d.ts.map +1 -0
- package/dist/components/TableOfContents/TableOfContents.meta.d.ts +3 -0
- package/dist/components/TableOfContents/TableOfContents.meta.d.ts.map +1 -0
- package/dist/components/TableOfContents/TableOfContents.stories.d.ts +21 -0
- package/dist/components/TableOfContents/TableOfContents.stories.d.ts.map +1 -0
- package/dist/components/TableOfContents/TableOfContentsContext.d.ts +11 -0
- package/dist/components/TableOfContents/TableOfContentsContext.d.ts.map +1 -0
- package/dist/components/TableOfContents/styles.css.d.ts +114 -0
- package/dist/components/TableOfContents/styles.css.d.ts.map +1 -0
- package/dist/components/TextLink/TextLink.d.ts +8 -1
- package/dist/components/TextLink/TextLink.d.ts.map +1 -1
- package/dist/components/TextLink/TextLink.stories.d.ts +9 -0
- package/dist/components/TextLink/TextLink.stories.d.ts.map +1 -1
- package/dist/components/Toggle/Toggle.d.ts +2 -1
- package/dist/components/Toggle/Toggle.d.ts.map +1 -1
- package/dist/components/Toggle/styles.css.d.ts +1 -0
- package/dist/components/Toggle/styles.css.d.ts.map +1 -1
- package/dist/components/Tree/Tree.d.ts +13 -1
- package/dist/components/Tree/Tree.d.ts.map +1 -1
- package/dist/components/Tree/Tree.meta.d.ts.map +1 -1
- package/dist/components/Tree/Tree.stories.d.ts +9 -0
- package/dist/components/Tree/Tree.stories.d.ts.map +1 -1
- package/dist/components/Tree/styles.css.d.ts +5 -0
- package/dist/components/Tree/styles.css.d.ts.map +1 -1
- package/dist/docs/clickLink.d.ts +4 -0
- package/dist/docs/clickLink.d.ts.map +1 -0
- package/dist/docs/coarsePointer.d.ts +15 -0
- package/dist/docs/coarsePointer.d.ts.map +1 -0
- package/dist/docs/themedSurface.d.ts +8 -0
- package/dist/docs/themedSurface.d.ts.map +1 -0
- package/dist/index.css +1 -1
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2718 -2212
- package/dist/index.js.map +1 -1
- package/dist/shared/ModalContext.d.ts +7 -0
- package/dist/shared/ModalContext.d.ts.map +1 -1
- package/dist/shared/exports.typecheck.d.ts +9 -0
- package/dist/shared/exports.typecheck.d.ts.map +1 -0
- package/dist/shared/navigate.d.ts +25 -0
- package/dist/shared/navigate.d.ts.map +1 -0
- package/dist/shared/types.d.ts +2 -0
- package/dist/shared/types.d.ts.map +1 -1
- package/dist/table.js +1 -1
- package/package.json +2 -2
- package/src/components/Breadcrumb/Breadcrumb.meta.ts +37 -0
- package/src/components/Breadcrumb/Breadcrumb.stories.tsx +110 -0
- package/src/components/Breadcrumb/Breadcrumb.tsx +95 -0
- package/src/components/Breadcrumb/styles.css.ts +76 -0
- package/src/components/Button/Button.tsx +2 -1
- package/src/components/Button/styles.css.ts +3 -0
- package/src/components/ButtonGroup/ButtonGroup.stories.tsx +172 -14
- package/src/components/Inline/Inline.meta.ts +2 -1
- package/src/components/LinkButton/LinkButton.meta.ts +1 -1
- package/src/components/LinkButton/LinkButton.stories.tsx +42 -1
- package/src/components/LinkButton/LinkButton.tsx +9 -0
- package/src/components/LinkButton/styles.css.ts +4 -0
- package/src/components/Menu/Menu.meta.ts +1 -1
- package/src/components/Menu/Menu.tsx +17 -2
- package/src/components/Menu/styles.css.ts +18 -0
- package/src/components/PageNav/PageNav.meta.ts +35 -0
- package/src/components/PageNav/PageNav.stories.tsx +146 -0
- package/src/components/PageNav/PageNav.tsx +93 -0
- package/src/components/PageNav/styles.css.ts +100 -0
- package/src/components/Popover/Popover.meta.ts +2 -1
- package/src/components/Popover/Popover.tsx +29 -4
- package/src/components/Popover/PopoverLayerContext.ts +3 -0
- package/src/components/Popover/PopoverRaised.stories.tsx +334 -0
- package/src/components/Popover/styles.css.ts +21 -0
- package/src/components/Select/Select.meta.ts +1 -1
- package/src/components/Select/Select.tsx +5 -1
- package/src/components/Select/styles.css.ts +12 -0
- package/src/components/TableOfContents/TableOfContents.meta.ts +41 -0
- package/src/components/TableOfContents/TableOfContents.stories.tsx +222 -0
- package/src/components/TableOfContents/TableOfContents.tsx +173 -0
- package/src/components/TableOfContents/TableOfContentsContext.ts +20 -0
- package/src/components/TableOfContents/styles.css.ts +147 -0
- package/src/components/TextLink/TextLink.meta.ts +1 -1
- package/src/components/TextLink/TextLink.stories.tsx +42 -1
- package/src/components/TextLink/TextLink.tsx +9 -0
- package/src/components/Toggle/Toggle.tsx +2 -1
- package/src/components/Toggle/styles.css.ts +2 -0
- package/src/components/Tree/Tree.meta.ts +2 -1
- package/src/components/Tree/Tree.stories.tsx +93 -1
- package/src/components/Tree/Tree.tsx +69 -27
- package/src/components/Tree/styles.css.ts +6 -0
- package/src/docs/clickLink.ts +24 -0
- package/src/docs/coarsePointer.ts +42 -0
- package/src/docs/themedSurface.tsx +23 -0
- package/src/index.ts +11 -0
- package/src/shared/ModalContext.ts +8 -1
- package/src/shared/exports.typecheck.ts +16 -0
- package/src/shared/linkParts.typecheck.tsx +47 -0
- package/src/shared/navigate.ts +62 -0
- package/src/shared/types.ts +3 -0
- package/dist/Table-Cm5cCnZD.js.map +0 -1
- package/dist/shared/length.typecheck.d.ts +0 -31
- package/dist/shared/length.typecheck.d.ts.map +0 -1
|
@@ -1,9 +1,21 @@
|
|
|
1
1
|
import {useState} from 'react'
|
|
2
2
|
import type {Meta, StoryObj} from '@storybook/react'
|
|
3
|
-
import {expect, userEvent, within} from 'storybook/test'
|
|
4
|
-
import {
|
|
3
|
+
import {expect, userEvent, waitFor, within} from 'storybook/test'
|
|
4
|
+
import {
|
|
5
|
+
BORDERS_PX,
|
|
6
|
+
FONT_SIZE_REM,
|
|
7
|
+
LAYERS,
|
|
8
|
+
RADII_PX,
|
|
9
|
+
SIZES_PX,
|
|
10
|
+
SPACING_PX,
|
|
11
|
+
} from '@duro-app/tokens/keys'
|
|
12
|
+
import {withCoarsePointer, withFinePointer} from '../../docs/coarsePointer'
|
|
5
13
|
import {ButtonGroup} from './ButtonGroup'
|
|
14
|
+
import type {ControlSize} from '../../shared/types'
|
|
6
15
|
import {Button} from '../Button/Button'
|
|
16
|
+
import {Icon} from '../Icon/Icon'
|
|
17
|
+
import {Inline} from '../Inline/Inline'
|
|
18
|
+
import {Menu} from '../Menu/Menu'
|
|
7
19
|
import {Popover} from '../Popover/Popover'
|
|
8
20
|
import {Select} from '../Select/Select'
|
|
9
21
|
import {Stack} from '../Stack/Stack'
|
|
@@ -146,11 +158,11 @@ async function expectJoined(controls: Element[]) {
|
|
|
146
158
|
}
|
|
147
159
|
}
|
|
148
160
|
|
|
149
|
-
function TextStyleSelects({named = false}: {named?: boolean}) {
|
|
161
|
+
function TextStyleSelects({named = false, size}: {named?: boolean; size?: ControlSize}) {
|
|
150
162
|
return (
|
|
151
163
|
<ButtonGroup attached aria-label="Text style">
|
|
152
164
|
<Select.Root name={named ? 'block' : undefined} defaultValue="normal">
|
|
153
|
-
<Select.Trigger aria-label="Block type">
|
|
165
|
+
<Select.Trigger aria-label="Block type" size={size}>
|
|
154
166
|
<Select.Value />
|
|
155
167
|
<Select.Icon />
|
|
156
168
|
</Select.Trigger>
|
|
@@ -160,7 +172,7 @@ function TextStyleSelects({named = false}: {named?: boolean}) {
|
|
|
160
172
|
</Select.Popup>
|
|
161
173
|
</Select.Root>
|
|
162
174
|
<Select.Root name={named ? 'font' : undefined} defaultValue="arial">
|
|
163
|
-
<Select.Trigger aria-label="Font">
|
|
175
|
+
<Select.Trigger aria-label="Font" size={size}>
|
|
164
176
|
<Select.Value />
|
|
165
177
|
<Select.Icon />
|
|
166
178
|
</Select.Trigger>
|
|
@@ -170,7 +182,7 @@ function TextStyleSelects({named = false}: {named?: boolean}) {
|
|
|
170
182
|
</Select.Popup>
|
|
171
183
|
</Select.Root>
|
|
172
184
|
<Select.Root name={named ? 'size' : undefined} defaultValue="15">
|
|
173
|
-
<Select.Trigger aria-label="Font size">
|
|
185
|
+
<Select.Trigger aria-label="Font size" size={size}>
|
|
174
186
|
<Select.Value />
|
|
175
187
|
<Select.Icon />
|
|
176
188
|
</Select.Trigger>
|
|
@@ -242,25 +254,37 @@ export const AttachedFocusMiddle: Story = {
|
|
|
242
254
|
},
|
|
243
255
|
}
|
|
244
256
|
|
|
245
|
-
function FormatGroup() {
|
|
257
|
+
function FormatGroup({size}: {size?: ControlSize}) {
|
|
246
258
|
const [pressed, setPressed] = useState<Record<string, boolean>>({u: true})
|
|
247
259
|
const toggle = (key: string) => (next: boolean) => setPressed((p) => ({...p, [key]: next}))
|
|
248
260
|
return (
|
|
249
261
|
<ButtonGroup attached aria-label="Format">
|
|
250
|
-
<Toggle aria-label="Bold" pressed={!!pressed.b} onPressedChange={toggle('b')}>
|
|
262
|
+
<Toggle aria-label="Bold" size={size} pressed={!!pressed.b} onPressedChange={toggle('b')}>
|
|
251
263
|
B
|
|
252
264
|
</Toggle>
|
|
253
|
-
<Toggle aria-label="Italic" pressed={!!pressed.i} onPressedChange={toggle('i')}>
|
|
265
|
+
<Toggle aria-label="Italic" size={size} pressed={!!pressed.i} onPressedChange={toggle('i')}>
|
|
254
266
|
I
|
|
255
267
|
</Toggle>
|
|
256
|
-
<Toggle
|
|
268
|
+
<Toggle
|
|
269
|
+
aria-label="Underline"
|
|
270
|
+
size={size}
|
|
271
|
+
pressed={!!pressed.u}
|
|
272
|
+
onPressedChange={toggle('u')}
|
|
273
|
+
>
|
|
257
274
|
U
|
|
258
275
|
</Toggle>
|
|
259
|
-
<Toggle
|
|
276
|
+
<Toggle
|
|
277
|
+
aria-label="Code"
|
|
278
|
+
size={size}
|
|
279
|
+
pressed={!!pressed.code}
|
|
280
|
+
onPressedChange={toggle('code')}
|
|
281
|
+
>
|
|
260
282
|
{'<>'}
|
|
261
283
|
</Toggle>
|
|
262
284
|
<Popover.Root>
|
|
263
|
-
<Popover.Trigger aria-label="Link"
|
|
285
|
+
<Popover.Trigger aria-label="Link" size={size}>
|
|
286
|
+
↗
|
|
287
|
+
</Popover.Trigger>
|
|
264
288
|
<Popover.Popup label="Link">
|
|
265
289
|
<Stack gap="sm">
|
|
266
290
|
<Text>Paste a URL.</Text>
|
|
@@ -268,10 +292,10 @@ function FormatGroup() {
|
|
|
268
292
|
</Stack>
|
|
269
293
|
</Popover.Popup>
|
|
270
294
|
</Popover.Root>
|
|
271
|
-
<Button variant="secondary" aria-label="Clear formatting">
|
|
295
|
+
<Button variant="secondary" size={size} aria-label="Clear formatting">
|
|
272
296
|
T
|
|
273
297
|
</Button>
|
|
274
|
-
<Button variant="secondary" aria-label="Highlight">
|
|
298
|
+
<Button variant="secondary" size={size} aria-label="Highlight">
|
|
275
299
|
H
|
|
276
300
|
</Button>
|
|
277
301
|
</ButtonGroup>
|
|
@@ -372,3 +396,137 @@ export const BothPressedFocusSecond: Story = {
|
|
|
372
396
|
await expect(drawnAt(bold, b.left + b.width / 2, ringPixels(b).bottom)).toBe(true)
|
|
373
397
|
},
|
|
374
398
|
}
|
|
399
|
+
|
|
400
|
+
// --- Small triggers (5.6) ---
|
|
401
|
+
|
|
402
|
+
/** fontSizeXs in px, from the root font size. */
|
|
403
|
+
const smallFontPx = () =>
|
|
404
|
+
`${parseFloat(getComputedStyle(document.documentElement).fontSize) * FONT_SIZE_REM.fontSizeXs}px`
|
|
405
|
+
|
|
406
|
+
/** [Normal | Arial | 15px] at size="small": joined like the default size. */
|
|
407
|
+
export const AttachedSelectsSmall: Story = {
|
|
408
|
+
render: () => <TextStyleSelects size="small" />,
|
|
409
|
+
play: async ({canvas}) => {
|
|
410
|
+
const triggers = canvas.getAllByRole('combobox')
|
|
411
|
+
await expectJoined(triggers)
|
|
412
|
+
for (const el of triggers) {
|
|
413
|
+
await expect(getComputedStyle(el).fontSize).toBe(smallFontPx())
|
|
414
|
+
}
|
|
415
|
+
},
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/** The mixed format group at size="small": Toggles, the Popover trigger and
|
|
419
|
+
* the Buttons stay joined. */
|
|
420
|
+
export const AttachedMixedSmall: Story = {
|
|
421
|
+
render: () => <FormatGroup size="small" />,
|
|
422
|
+
play: async ({canvas}) => {
|
|
423
|
+
const group = canvas.getByRole('group', {name: 'Format'})
|
|
424
|
+
await expectJoined(within(group).getAllByRole('button'))
|
|
425
|
+
},
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
function SmallControlsRow() {
|
|
429
|
+
return (
|
|
430
|
+
<Inline gap="sm" align="center">
|
|
431
|
+
<Toggle size="small" aria-label="Bold">
|
|
432
|
+
B
|
|
433
|
+
</Toggle>
|
|
434
|
+
<Toggle size="small" aria-label="Search">
|
|
435
|
+
<Icon name="search" size="md" />
|
|
436
|
+
</Toggle>
|
|
437
|
+
<Button size="small" variant="secondary">
|
|
438
|
+
Clear
|
|
439
|
+
</Button>
|
|
440
|
+
<Select.Root defaultValue="normal">
|
|
441
|
+
<Select.Trigger size="small" aria-label="Block type">
|
|
442
|
+
<Select.Value />
|
|
443
|
+
<Select.Icon />
|
|
444
|
+
</Select.Trigger>
|
|
445
|
+
<Select.Popup>
|
|
446
|
+
<Select.Item value="normal">Normal</Select.Item>
|
|
447
|
+
<Select.Item value="h1">Heading 1</Select.Item>
|
|
448
|
+
</Select.Popup>
|
|
449
|
+
</Select.Root>
|
|
450
|
+
<Menu.Root>
|
|
451
|
+
<Menu.Trigger size="small">Insert</Menu.Trigger>
|
|
452
|
+
<Menu.Popup>
|
|
453
|
+
<Menu.Item onClick={() => undefined}>Image</Menu.Item>
|
|
454
|
+
</Menu.Popup>
|
|
455
|
+
</Menu.Root>
|
|
456
|
+
<Popover.Root>
|
|
457
|
+
<Popover.Trigger size="small" aria-label="Text colour">
|
|
458
|
+
A
|
|
459
|
+
</Popover.Trigger>
|
|
460
|
+
<Popover.Popup label="Text colour">
|
|
461
|
+
<Text>Pick a colour.</Text>
|
|
462
|
+
</Popover.Popup>
|
|
463
|
+
</Popover.Root>
|
|
464
|
+
<Menu.Root>
|
|
465
|
+
<Menu.Trigger size="small" variant="ghost" aria-label="More">
|
|
466
|
+
<Icon name="menu" size="sm" />
|
|
467
|
+
</Menu.Trigger>
|
|
468
|
+
<Menu.Popup>
|
|
469
|
+
<Menu.Item onClick={() => undefined}>Settings</Menu.Item>
|
|
470
|
+
</Menu.Popup>
|
|
471
|
+
</Menu.Root>
|
|
472
|
+
</Inline>
|
|
473
|
+
)
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
/** Small Toggles (text and an 18px icon), a small Button and small Select,
|
|
477
|
+
* Menu and Popover triggers in a plain Inline (no attached group to stretch
|
|
478
|
+
* them): every one is controlSm tall whatever it holds, with Toggle's
|
|
479
|
+
* padding and font. The ghost Menu trigger is the small icon-button size. */
|
|
480
|
+
export const SmallTriggersRow: Story = {
|
|
481
|
+
render: () => <SmallControlsRow />,
|
|
482
|
+
play: async ({canvas}) => {
|
|
483
|
+
await withFinePointer()
|
|
484
|
+
const controls = [
|
|
485
|
+
canvas.getByRole('button', {name: 'Bold'}),
|
|
486
|
+
canvas.getByRole('button', {name: 'Search'}),
|
|
487
|
+
canvas.getByRole('button', {name: 'Clear'}),
|
|
488
|
+
]
|
|
489
|
+
const triggers = [
|
|
490
|
+
canvas.getByRole('combobox', {name: 'Block type'}),
|
|
491
|
+
canvas.getByRole('button', {name: 'Insert'}),
|
|
492
|
+
canvas.getByRole('button', {name: 'Text colour'}),
|
|
493
|
+
]
|
|
494
|
+
for (const el of [...controls, ...triggers]) {
|
|
495
|
+
await expect(el.offsetHeight).toBe(SIZES_PX.controlSm)
|
|
496
|
+
}
|
|
497
|
+
for (const el of triggers) {
|
|
498
|
+
const cs = getComputedStyle(el)
|
|
499
|
+
await expect(cs.paddingTop).toBe(`${SPACING_PX.xs}px`)
|
|
500
|
+
await expect(cs.paddingLeft).toBe(`${SPACING_PX.sm}px`)
|
|
501
|
+
await expect(cs.fontSize).toBe(smallFontPx())
|
|
502
|
+
}
|
|
503
|
+
const ghost = canvas.getByRole('button', {name: 'More'})
|
|
504
|
+
await expect(ghost.offsetHeight).toBe(SIZES_PX.iconButtonSm)
|
|
505
|
+
await expect(ghost.offsetWidth).toBe(SIZES_PX.iconButtonSm)
|
|
506
|
+
await expect(getComputedStyle(ghost).fontSize).toBe(smallFontPx())
|
|
507
|
+
},
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
/** The same row under a coarse pointer: Toggle and the three triggers reach
|
|
511
|
+
* touchTarget, the ghost Menu too. Button has no touch size at any size, so the small Button stays
|
|
512
|
+
* controlSm (the 5.5 plan's addendum). */
|
|
513
|
+
export const SmallTriggersRowCoarsePointer: Story = {
|
|
514
|
+
render: () => <SmallControlsRow />,
|
|
515
|
+
play: async ({canvas}) => {
|
|
516
|
+
const touch = [
|
|
517
|
+
canvas.getByRole('button', {name: 'Bold'}),
|
|
518
|
+
canvas.getByRole('button', {name: 'Search'}),
|
|
519
|
+
canvas.getByRole('combobox', {name: 'Block type'}),
|
|
520
|
+
canvas.getByRole('button', {name: 'Insert'}),
|
|
521
|
+
canvas.getByRole('button', {name: 'Text colour'}),
|
|
522
|
+
canvas.getByRole('button', {name: 'More'}),
|
|
523
|
+
]
|
|
524
|
+
const button = canvas.getByRole('button', {name: 'Clear'})
|
|
525
|
+
await withCoarsePointer(async () => {
|
|
526
|
+
for (const el of touch) {
|
|
527
|
+
await waitFor(() => expect(el.offsetHeight).toBe(SIZES_PX.touchTarget))
|
|
528
|
+
}
|
|
529
|
+
await expect(button.offsetHeight).toBe(SIZES_PX.controlSm)
|
|
530
|
+
})
|
|
531
|
+
},
|
|
532
|
+
}
|
|
@@ -5,11 +5,12 @@ export const meta: ComponentMeta = {
|
|
|
5
5
|
whenToUse: [
|
|
6
6
|
'Toolbar or button group that must stay on one row',
|
|
7
7
|
'Horizontal alignment of a known, small number of items',
|
|
8
|
-
'Icon + text pairs,
|
|
8
|
+
'Icon + text pairs, action rows',
|
|
9
9
|
],
|
|
10
10
|
whenNotToUse: [
|
|
11
11
|
'Items that should wrap to the next line — use Cluster',
|
|
12
12
|
'Vertical stacking — use Stack',
|
|
13
|
+
'A breadcrumb trail — use Breadcrumb (a nav landmark with aria-current)',
|
|
13
14
|
],
|
|
14
15
|
relatedTo: [
|
|
15
16
|
{
|
|
@@ -2,7 +2,7 @@ import type {ComponentMeta} from '../component-meta'
|
|
|
2
2
|
|
|
3
3
|
export const meta: ComponentMeta = {
|
|
4
4
|
description:
|
|
5
|
-
'Button-styled hyperlink. Renders an <a> tag, not a <button>. Use for navigation, not actions.',
|
|
5
|
+
'Button-styled hyperlink. Renders an <a> tag, not a <button>. Use for navigation, not actions. onNavigate(href, event) hands a plain primary click to a client router (the app calls event.preventDefault(), then navigates); modified clicks keep the browser default.',
|
|
6
6
|
whenToUse: [
|
|
7
7
|
'Navigation that should look like a button (e.g., "Get started", "View all")',
|
|
8
8
|
'Links to external pages with button styling',
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import type {Meta, StoryObj} from '@storybook/react'
|
|
2
|
-
import {expect} from 'storybook/test'
|
|
2
|
+
import {expect, fn} from 'storybook/test'
|
|
3
3
|
import {css, html} from 'react-strict-dom'
|
|
4
4
|
import {LinkButton} from './LinkButton'
|
|
5
|
+
import {clickLink} from '../../docs/clickLink'
|
|
6
|
+
import {onThemeSurface} from '../../docs/themedSurface'
|
|
7
|
+
import type {OnNavigate} from '../../shared/navigate'
|
|
5
8
|
import {spacing} from '@duro-app/tokens/tokens/spacing.css'
|
|
6
9
|
|
|
7
10
|
const meta: Meta<typeof LinkButton> = {
|
|
@@ -90,3 +93,41 @@ export const AllVariants: Story = {
|
|
|
90
93
|
await expect(links.length).toBe(4)
|
|
91
94
|
},
|
|
92
95
|
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Client-side navigation: `onNavigate` gets a plain primary click (the app
|
|
99
|
+
* calls `event.preventDefault()`, then its router's navigate); a modified
|
|
100
|
+
* click, or a link with `target`, keeps the browser default.
|
|
101
|
+
*/
|
|
102
|
+
export const ClientNavigation: StoryObj<{onNavigate: OnNavigate}> = {
|
|
103
|
+
parameters: {a11y: {test: 'error'}},
|
|
104
|
+
decorators: [onThemeSurface],
|
|
105
|
+
args: {onNavigate: fn()},
|
|
106
|
+
render: (args) => (
|
|
107
|
+
<>
|
|
108
|
+
<LinkButton href="#billing" onNavigate={args.onNavigate}>
|
|
109
|
+
Get started
|
|
110
|
+
</LinkButton>{' '}
|
|
111
|
+
<LinkButton href="#help" target="_blank" onNavigate={args.onNavigate}>
|
|
112
|
+
Help
|
|
113
|
+
</LinkButton>
|
|
114
|
+
</>
|
|
115
|
+
),
|
|
116
|
+
play: async ({args, canvas}) => {
|
|
117
|
+
const onNavigate = args.onNavigate as unknown as ReturnType<typeof fn>
|
|
118
|
+
onNavigate.mockImplementation((_href: string, event: {preventDefault(): void}) =>
|
|
119
|
+
event.preventDefault(),
|
|
120
|
+
)
|
|
121
|
+
const link = canvas.getByRole('link', {name: 'Get started'})
|
|
122
|
+
await expect(clickLink(link).defaultPrevented).toBe(true)
|
|
123
|
+
await expect(onNavigate).toHaveBeenCalledTimes(1)
|
|
124
|
+
await expect(onNavigate.mock.calls[0][0]).toBe('#billing')
|
|
125
|
+
for (const init of [{metaKey: true}, {ctrlKey: true}, {shiftKey: true}, {altKey: true}]) {
|
|
126
|
+
await expect(clickLink(link, init).defaultPrevented).toBe(false)
|
|
127
|
+
}
|
|
128
|
+
// a target opens where it says, not through the router
|
|
129
|
+
const help = canvas.getByRole('link', {name: 'Help'})
|
|
130
|
+
await expect(clickLink(help).defaultPrevented).toBe(false)
|
|
131
|
+
await expect(onNavigate).toHaveBeenCalledTimes(1)
|
|
132
|
+
},
|
|
133
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type {ReactNode} from 'react'
|
|
2
2
|
import {html} from 'react-strict-dom'
|
|
3
3
|
import {styles} from './styles.css'
|
|
4
|
+
import {linkClickHandler, type OnNavigate} from '../../shared/navigate'
|
|
4
5
|
|
|
5
6
|
export type LinkButtonVariant = 'primary' | 'secondary'
|
|
6
7
|
export type LinkButtonSize = 'default' | 'small'
|
|
@@ -12,6 +13,12 @@ interface LinkButtonProps {
|
|
|
12
13
|
fullWidth?: boolean
|
|
13
14
|
target?: '_blank' | '_self'
|
|
14
15
|
rel?: string
|
|
16
|
+
/**
|
|
17
|
+
* Client-side navigation: called for a plain primary click (no modifier
|
|
18
|
+
* key, no `target`); the browser default runs for every other click. Call
|
|
19
|
+
* `event.preventDefault()`, then your router's navigate.
|
|
20
|
+
*/
|
|
21
|
+
onNavigate?: OnNavigate
|
|
15
22
|
children: ReactNode
|
|
16
23
|
}
|
|
17
24
|
|
|
@@ -27,6 +34,7 @@ export function LinkButton({
|
|
|
27
34
|
fullWidth = false,
|
|
28
35
|
target,
|
|
29
36
|
rel,
|
|
37
|
+
onNavigate,
|
|
30
38
|
children,
|
|
31
39
|
}: LinkButtonProps) {
|
|
32
40
|
return (
|
|
@@ -34,6 +42,7 @@ export function LinkButton({
|
|
|
34
42
|
href={href}
|
|
35
43
|
target={target}
|
|
36
44
|
rel={rel}
|
|
45
|
+
onClick={linkClickHandler(href, onNavigate, target)}
|
|
37
46
|
style={[styles.base, sizeMap[size], styles[variant], fullWidth && styles.fullWidth]}
|
|
38
47
|
>
|
|
39
48
|
{children}
|
|
@@ -4,6 +4,7 @@ import {spacing, radii} from '@duro-app/tokens/tokens/spacing.css'
|
|
|
4
4
|
import {typography} from '@duro-app/tokens/tokens/typography.css'
|
|
5
5
|
import {duration, easing} from '@duro-app/tokens/tokens/motion.css'
|
|
6
6
|
import {borders} from '@duro-app/tokens/tokens/borders.css'
|
|
7
|
+
import {sizes} from '@duro-app/tokens/tokens/sizes.css'
|
|
7
8
|
|
|
8
9
|
export const styles = css.create({
|
|
9
10
|
base: {
|
|
@@ -34,6 +35,9 @@ export const styles = css.create({
|
|
|
34
35
|
paddingLeft: spacing.sm,
|
|
35
36
|
paddingRight: spacing.sm,
|
|
36
37
|
fontSize: typography.fontSizeXs,
|
|
38
|
+
|
|
39
|
+
// Matches Button: every small control is at least controlSm tall (5.6).
|
|
40
|
+
minHeight: sizes.controlSm,
|
|
37
41
|
},
|
|
38
42
|
primary: {
|
|
39
43
|
backgroundColor: {
|
|
@@ -2,7 +2,7 @@ import type {ComponentMeta} from '../component-meta'
|
|
|
2
2
|
|
|
3
3
|
export const meta: ComponentMeta = {
|
|
4
4
|
description:
|
|
5
|
-
'Dropdown action menu. Triggers actions (not value selection). Compound component — Root is required (throws without it). Menu.Trigger renders the button itself: give it the label (text, an Icon), never a Button; an icon-only trigger takes aria-label, and variant="ghost" suits toolbars. The popup is portalled and placed against the trigger (never clipped by a sticky or overflow ancestor), capped at sizes.listMaxH with scrolling, and takes focus on open (WAI-ARIA menu button); Escape closes only the menu, not an enclosing Popover or Dialog.',
|
|
5
|
+
'Dropdown action menu. Triggers actions (not value selection). Compound component — Root is required (throws without it). Menu.Trigger renders the button itself: give it the label (text, an Icon), never a Button; an icon-only trigger takes aria-label, and variant="ghost" suits toolbars; size="small" matches small Toggles and Buttons (a small ghost trigger is iconButtonSm). The popup is portalled and placed against the trigger (never clipped by a sticky or overflow ancestor), capped at sizes.listMaxH with scrolling, and takes focus on open (WAI-ARIA menu button); Escape closes only the menu, not an enclosing Popover or Dialog.',
|
|
6
6
|
whenToUse: [
|
|
7
7
|
'Context menu or "more actions" dropdown',
|
|
8
8
|
'Navigation links in a dropdown',
|
|
@@ -15,6 +15,7 @@ import {styles} from './styles.css'
|
|
|
15
15
|
import {MenuContext, useMenu} from './MenuContext'
|
|
16
16
|
import {useMenuRoot} from './useMenuRoot'
|
|
17
17
|
import {devWarnOnce} from '../../shared/devWarnOnce'
|
|
18
|
+
import type {ControlSize} from '../../shared/types'
|
|
18
19
|
import {usePortalMount} from '../ThemeProvider/ThemeProvider'
|
|
19
20
|
import {computePopoverPosition} from '../Popover/position'
|
|
20
21
|
import {usePopoverLayer} from '../Popover/PopoverLayerContext'
|
|
@@ -58,10 +59,18 @@ interface TriggerProps {
|
|
|
58
59
|
'aria-label'?: string
|
|
59
60
|
/** `ghost`: no border or fill until hover, for toolbars. Default: `default`. */
|
|
60
61
|
variant?: MenuTriggerVariant
|
|
62
|
+
/** `small` matches a small Toggle or Button beside it; a small ghost trigger is `iconButtonSm`. Default: `default`. */
|
|
63
|
+
size?: ControlSize
|
|
61
64
|
ref?: Ref<HTMLButtonElement>
|
|
62
65
|
}
|
|
63
66
|
|
|
64
|
-
function Trigger({
|
|
67
|
+
function Trigger({
|
|
68
|
+
children,
|
|
69
|
+
'aria-label': ariaLabel,
|
|
70
|
+
variant = 'default',
|
|
71
|
+
size = 'default',
|
|
72
|
+
ref,
|
|
73
|
+
}: TriggerProps) {
|
|
65
74
|
const {open, toggle, menuId, triggerId, triggerRef} = useMenu()
|
|
66
75
|
const grouped = useGroupedControl<HTMLButtonElement>()
|
|
67
76
|
const groupedRef = grouped.ref
|
|
@@ -99,7 +108,13 @@ function Trigger({children, 'aria-label': ariaLabel, variant = 'default', ref}:
|
|
|
99
108
|
aria-expanded={open}
|
|
100
109
|
aria-haspopup="menu"
|
|
101
110
|
aria-controls={open ? menuId : undefined}
|
|
102
|
-
style={[
|
|
111
|
+
style={[
|
|
112
|
+
styles.trigger,
|
|
113
|
+
variant === 'ghost'
|
|
114
|
+
? [styles.triggerGhost, size === 'small' && styles.triggerGhostSmall]
|
|
115
|
+
: size === 'small' && styles.triggerSmall,
|
|
116
|
+
grouped.style,
|
|
117
|
+
]}
|
|
103
118
|
>
|
|
104
119
|
{children}
|
|
105
120
|
</html.button>
|
|
@@ -47,6 +47,24 @@ export const styles = css.create({
|
|
|
47
47
|
paddingRight: spacing.xs,
|
|
48
48
|
borderColor: 'transparent',
|
|
49
49
|
},
|
|
50
|
+
// size="small": Toggle sizeSmall's values, as longhands so they win over the
|
|
51
|
+
// trigger's own longhands. Every small control is at least controlSm tall,
|
|
52
|
+
// whatever it holds, and the touch target under a coarse pointer.
|
|
53
|
+
triggerSmall: {
|
|
54
|
+
gap: spacing.xs,
|
|
55
|
+
paddingTop: spacing.xs,
|
|
56
|
+
paddingBottom: spacing.xs,
|
|
57
|
+
paddingLeft: spacing.sm,
|
|
58
|
+
paddingRight: spacing.sm,
|
|
59
|
+
fontSize: typography.fontSizeXs,
|
|
60
|
+
minHeight: {default: sizes.controlSm, '@media (pointer: coarse)': sizes.touchTarget},
|
|
61
|
+
},
|
|
62
|
+
// A small ghost trigger keeps the ghost's xs padding at the small icon-button size.
|
|
63
|
+
triggerGhostSmall: {
|
|
64
|
+
fontSize: typography.fontSizeXs,
|
|
65
|
+
minWidth: sizes.iconButtonSm,
|
|
66
|
+
minHeight: {default: sizes.iconButtonSm, '@media (pointer: coarse)': sizes.touchTarget},
|
|
67
|
+
},
|
|
50
68
|
popup: {
|
|
51
69
|
position: 'fixed',
|
|
52
70
|
top: 0,
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type {ComponentMeta} from '../component-meta'
|
|
2
|
+
|
|
3
|
+
export const meta: ComponentMeta = {
|
|
4
|
+
description:
|
|
5
|
+
'The previous and next pages in reading order, at the foot of a page: a labelled nav with up to two link cards (a direction label over the page title, both from props). A missing side keeps its slot, so Next stays right-aligned; the cards stack below the xs container width. Links take href and an optional onNavigate(href, event) (plain clicks only). Compound component — Root is required.',
|
|
6
|
+
whenToUse: [
|
|
7
|
+
'Docs or a guide read in order — the foot of each page',
|
|
8
|
+
'A sequence of steps where each step is its own page',
|
|
9
|
+
],
|
|
10
|
+
whenNotToUse: [
|
|
11
|
+
'Numbered result pages of a table — use Pagination from @duro-app/ui/table',
|
|
12
|
+
'Going up the hierarchy — use Breadcrumb',
|
|
13
|
+
],
|
|
14
|
+
anatomy: {
|
|
15
|
+
required: ['Root'],
|
|
16
|
+
optional: ['Prev', 'Next'],
|
|
17
|
+
},
|
|
18
|
+
relatedTo: [
|
|
19
|
+
{
|
|
20
|
+
component: 'Breadcrumb',
|
|
21
|
+
kind: 'contrast',
|
|
22
|
+
relationship: 'PageNav goes along the reading order; Breadcrumb goes up the hierarchy',
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
component: 'Card',
|
|
26
|
+
kind: 'contrast',
|
|
27
|
+
relationship:
|
|
28
|
+
'PageNav draws its own link cards; do not wrap a Card in a link to fake a prev/next pair',
|
|
29
|
+
},
|
|
30
|
+
],
|
|
31
|
+
example: `<PageNav.Root aria-label="Pages">
|
|
32
|
+
<PageNav.Prev href="/docs/architecture/clusters" label="Previous" title="Clusters and bootstrap" onNavigate={go} />
|
|
33
|
+
<PageNav.Next href="/docs/architecture/secrets" label="Next" title="Secrets and Vault" onNavigate={go} />
|
|
34
|
+
</PageNav.Root>`,
|
|
35
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import {css, html} from 'react-strict-dom'
|
|
2
|
+
import type {Meta, StoryObj} from '@storybook/react'
|
|
3
|
+
import {expect, fn} from 'storybook/test'
|
|
4
|
+
import {sizes} from '@duro-app/tokens/tokens/sizes.css'
|
|
5
|
+
import {PageNav} from './PageNav'
|
|
6
|
+
import {clickLink} from '../../docs/clickLink'
|
|
7
|
+
import {onThemeSurface} from '../../docs/themedSurface'
|
|
8
|
+
import type {OnNavigate} from '../../shared/navigate'
|
|
9
|
+
|
|
10
|
+
const meta: Meta = {
|
|
11
|
+
title: 'Components/PageNav',
|
|
12
|
+
parameters: {a11y: {test: 'error'}},
|
|
13
|
+
decorators: [onThemeSurface],
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export default meta
|
|
17
|
+
type Story = StoryObj
|
|
18
|
+
|
|
19
|
+
const styles = css.create({
|
|
20
|
+
column: {width: sizes.panelLg},
|
|
21
|
+
narrow: {width: sizes.panelSm},
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
function Pages(props: {prev?: boolean; next?: boolean; onNavigate?: OnNavigate}) {
|
|
25
|
+
const {prev = true, next = true, onNavigate} = props
|
|
26
|
+
return (
|
|
27
|
+
<PageNav.Root aria-label="Pages">
|
|
28
|
+
{prev ? (
|
|
29
|
+
<PageNav.Prev
|
|
30
|
+
href="#clusters"
|
|
31
|
+
label="Previous"
|
|
32
|
+
title="Clusters and bootstrap"
|
|
33
|
+
onNavigate={onNavigate}
|
|
34
|
+
/>
|
|
35
|
+
) : null}
|
|
36
|
+
{next ? (
|
|
37
|
+
<PageNav.Next
|
|
38
|
+
href="#secrets"
|
|
39
|
+
label="Next"
|
|
40
|
+
title="Secrets and Vault"
|
|
41
|
+
onNavigate={onNavigate}
|
|
42
|
+
/>
|
|
43
|
+
) : null}
|
|
44
|
+
</PageNav.Root>
|
|
45
|
+
)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export const Default: Story = {
|
|
49
|
+
render: () => (
|
|
50
|
+
<html.div style={styles.column}>
|
|
51
|
+
<Pages />
|
|
52
|
+
</html.div>
|
|
53
|
+
),
|
|
54
|
+
play: async ({canvas, userEvent}) => {
|
|
55
|
+
await expect(canvas.getByRole('navigation', {name: 'Pages'})).toBeInTheDocument()
|
|
56
|
+
// the arrows are decorative: the name is the label and the title
|
|
57
|
+
const prev = canvas.getByRole('link', {name: 'Previous Clusters and bootstrap'})
|
|
58
|
+
const next = canvas.getByRole('link', {name: 'Next Secrets and Vault'})
|
|
59
|
+
// side by side, Next on the right
|
|
60
|
+
await expect(next.getBoundingClientRect().left).toBeGreaterThan(
|
|
61
|
+
prev.getBoundingClientRect().right,
|
|
62
|
+
)
|
|
63
|
+
await userEvent.tab()
|
|
64
|
+
await expect(prev).toHaveFocus()
|
|
65
|
+
await userEvent.tab()
|
|
66
|
+
await expect(next).toHaveFocus()
|
|
67
|
+
},
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** The first page of a section: no Prev, and Next keeps the right half. */
|
|
71
|
+
export const OnlyNext: Story = {
|
|
72
|
+
render: () => (
|
|
73
|
+
<html.div style={styles.column}>
|
|
74
|
+
<Pages prev={false} />
|
|
75
|
+
</html.div>
|
|
76
|
+
),
|
|
77
|
+
play: async ({canvas}) => {
|
|
78
|
+
const nav = canvas.getByRole('navigation', {name: 'Pages'})
|
|
79
|
+
const next = canvas.getByRole('link', {name: 'Next Secrets and Vault'})
|
|
80
|
+
const navBox = nav.getBoundingClientRect()
|
|
81
|
+
const nextBox = next.getBoundingClientRect()
|
|
82
|
+
await expect(nextBox.left).toBeGreaterThan(navBox.left + navBox.width / 3)
|
|
83
|
+
await expect(Math.round(nextBox.right)).toBe(Math.round(navBox.right))
|
|
84
|
+
await expect(getComputedStyle(next).textAlign).toBe('right')
|
|
85
|
+
},
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** The last page of a section: no Next, and Prev keeps the left half. */
|
|
89
|
+
export const OnlyPrev: Story = {
|
|
90
|
+
render: () => (
|
|
91
|
+
<html.div style={styles.column}>
|
|
92
|
+
<Pages next={false} />
|
|
93
|
+
</html.div>
|
|
94
|
+
),
|
|
95
|
+
play: async ({canvas}) => {
|
|
96
|
+
const navBox = canvas.getByRole('navigation', {name: 'Pages'}).getBoundingClientRect()
|
|
97
|
+
const prevBox = canvas
|
|
98
|
+
.getByRole('link', {name: 'Previous Clusters and bootstrap'})
|
|
99
|
+
.getBoundingClientRect()
|
|
100
|
+
await expect(canvas.getAllByRole('link')).toHaveLength(1)
|
|
101
|
+
await expect(canvas.queryByRole('link', {name: /Next/})).toBeNull()
|
|
102
|
+
await expect(Math.round(prevBox.left)).toBe(Math.round(navBox.left))
|
|
103
|
+
await expect(prevBox.right).toBeLessThan(navBox.left + navBox.width / 2)
|
|
104
|
+
},
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Below the xs container width the cards stack, Prev first. */
|
|
108
|
+
export const Stacked: Story = {
|
|
109
|
+
render: () => (
|
|
110
|
+
<html.div style={styles.narrow}>
|
|
111
|
+
<Pages />
|
|
112
|
+
</html.div>
|
|
113
|
+
),
|
|
114
|
+
play: async ({canvas}) => {
|
|
115
|
+
const prev = canvas.getByRole('link', {name: 'Previous Clusters and bootstrap'})
|
|
116
|
+
const next = canvas.getByRole('link', {name: 'Next Secrets and Vault'})
|
|
117
|
+
await expect(next.getBoundingClientRect().top).toBeGreaterThanOrEqual(
|
|
118
|
+
prev.getBoundingClientRect().bottom,
|
|
119
|
+
)
|
|
120
|
+
await expect(Math.round(next.getBoundingClientRect().left)).toBe(
|
|
121
|
+
Math.round(prev.getBoundingClientRect().left),
|
|
122
|
+
)
|
|
123
|
+
},
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export const ClientNavigation: StoryObj<{onNavigate: OnNavigate}> = {
|
|
127
|
+
args: {onNavigate: fn()},
|
|
128
|
+
render: (args) => (
|
|
129
|
+
<html.div style={styles.column}>
|
|
130
|
+
<Pages onNavigate={args.onNavigate} />
|
|
131
|
+
</html.div>
|
|
132
|
+
),
|
|
133
|
+
play: async ({args, canvas}) => {
|
|
134
|
+
const onNavigate = args.onNavigate as unknown as ReturnType<typeof fn>
|
|
135
|
+
onNavigate.mockImplementation((_href: string, event: {preventDefault(): void}) =>
|
|
136
|
+
event.preventDefault(),
|
|
137
|
+
)
|
|
138
|
+
const next = canvas.getByRole('link', {name: 'Next Secrets and Vault'})
|
|
139
|
+
await expect(clickLink(next).defaultPrevented).toBe(true)
|
|
140
|
+
await expect(onNavigate).toHaveBeenCalledTimes(1)
|
|
141
|
+
await expect(onNavigate.mock.calls[0][0]).toBe('#secrets')
|
|
142
|
+
await expect(clickLink(next, {metaKey: true}).defaultPrevented).toBe(false)
|
|
143
|
+
await expect(clickLink(next, {altKey: true}).defaultPrevented).toBe(false)
|
|
144
|
+
await expect(onNavigate).toHaveBeenCalledTimes(1)
|
|
145
|
+
},
|
|
146
|
+
}
|