@duro-app/ui 4.1.0 → 4.3.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-Bj5u2Q-k.js → Table-JTgcR_Fh.js} +241 -226
- package/dist/Table-JTgcR_Fh.js.map +1 -0
- package/dist/components/DetailPanel/DetailPanel.meta.d.ts.map +1 -1
- package/dist/components/Dialog/Dialog.meta.d.ts.map +1 -1
- package/dist/components/Drawer/Drawer.meta.d.ts.map +1 -1
- package/dist/components/Menu/Menu.d.ts +2 -0
- package/dist/components/Menu/Menu.d.ts.map +1 -1
- package/dist/components/Menu/Menu.meta.d.ts.map +1 -1
- package/dist/components/Menu/Menu.stories.d.ts +1 -0
- package/dist/components/Menu/Menu.stories.d.ts.map +1 -1
- package/dist/components/Menu/styles.css.d.ts +8 -0
- package/dist/components/Menu/styles.css.d.ts.map +1 -1
- package/dist/components/Popover/Popover.d.ts +41 -0
- package/dist/components/Popover/Popover.d.ts.map +1 -0
- package/dist/components/Popover/Popover.meta.d.ts +3 -0
- package/dist/components/Popover/Popover.meta.d.ts.map +1 -0
- package/dist/components/Popover/Popover.stories.d.ts +11 -0
- package/dist/components/Popover/Popover.stories.d.ts.map +1 -0
- package/dist/components/Popover/outside.d.ts +2 -0
- package/dist/components/Popover/outside.d.ts.map +1 -0
- package/dist/components/Popover/position.d.ts +27 -0
- package/dist/components/Popover/position.d.ts.map +1 -0
- package/dist/components/Popover/styles.css.d.ts +61 -0
- package/dist/components/Popover/styles.css.d.ts.map +1 -0
- package/dist/index.css +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1645 -1381
- package/dist/index.js.map +1 -1
- package/dist/table.js +1 -1
- package/package.json +2 -2
- package/src/components/DetailPanel/DetailPanel.meta.ts +6 -0
- package/src/components/Dialog/Dialog.meta.ts +6 -0
- package/src/components/Drawer/Drawer.meta.ts +6 -0
- package/src/components/Menu/Menu.meta.ts +7 -2
- package/src/components/Menu/Menu.stories.tsx +26 -0
- package/src/components/Menu/Menu.tsx +9 -0
- package/src/components/Menu/styles.css.ts +8 -0
- package/src/components/Popover/Popover.meta.ts +69 -0
- package/src/components/Popover/Popover.stories.tsx +250 -0
- package/src/components/Popover/Popover.tsx +293 -0
- package/src/components/Popover/outside.ts +9 -0
- package/src/components/Popover/position.ts +48 -0
- package/src/components/Popover/styles.css.ts +75 -0
- package/src/components/Tooltip/Tooltip.meta.ts +1 -1
- package/src/index.ts +2 -0
- package/dist/Table-Bj5u2Q-k.js.map +0 -1
package/dist/table.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { M as F, s as xt, R as rt, H as kt, a as pt, B as bt, c as mt, C as ft, b as ht, d as yt } from "./Table-
|
|
1
|
+
import { M as F, s as xt, R as rt, H as kt, a as pt, B as bt, c as mt, C as ft, b as ht, d as yt } from "./Table-JTgcR_Fh.js";
|
|
2
2
|
import { jsx as u, jsxs as B } from "react/jsx-runtime";
|
|
3
3
|
import { c as H, m as p, d as b } from "./compiler-runtime-B2IcI8_O.js";
|
|
4
4
|
import { flexRender as E, useReactTable as it, getPaginationRowModel as St, getFilteredRowModel as Tt, getSortedRowModel as at, getCoreRowModel as st } from "@tanstack/react-table";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@duro-app/ui",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.3.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -66,7 +66,7 @@
|
|
|
66
66
|
},
|
|
67
67
|
"dependencies": {
|
|
68
68
|
"@tanstack/react-virtual": "^3.14.6",
|
|
69
|
-
"@duro-app/tokens": "^4.
|
|
69
|
+
"@duro-app/tokens": "^4.3.0"
|
|
70
70
|
},
|
|
71
71
|
"devDependencies": {
|
|
72
72
|
"@babel/preset-typescript": "^7.28.0",
|
|
@@ -23,6 +23,12 @@ export const meta: ComponentMeta = {
|
|
|
23
23
|
kind: 'contrast',
|
|
24
24
|
relationship: 'Drawer is modal with backdrop; DetailPanel is non-modal and in-flow',
|
|
25
25
|
},
|
|
26
|
+
{
|
|
27
|
+
component: 'Popover',
|
|
28
|
+
kind: 'contrast',
|
|
29
|
+
relationship:
|
|
30
|
+
'Popover is a small anchored overlay for a few controls; DetailPanel is an in-flow panel for long editing',
|
|
31
|
+
},
|
|
26
32
|
{
|
|
27
33
|
component: 'Table',
|
|
28
34
|
kind: 'composition',
|
|
@@ -30,6 +30,12 @@ export const meta: ComponentMeta = {
|
|
|
30
30
|
kind: 'contrast',
|
|
31
31
|
relationship: 'DetailPanel is non-modal and in-flow; Dialog is modal with backdrop',
|
|
32
32
|
},
|
|
33
|
+
{
|
|
34
|
+
component: 'Popover',
|
|
35
|
+
kind: 'contrast',
|
|
36
|
+
relationship:
|
|
37
|
+
'Popover is a small non-modal card anchored to an element; Dialog is a modal decision',
|
|
38
|
+
},
|
|
33
39
|
],
|
|
34
40
|
example: `<Dialog.Root open={open} onOpenChange={setOpen}>
|
|
35
41
|
<Dialog.Trigger>
|
|
@@ -30,6 +30,12 @@ export const meta: ComponentMeta = {
|
|
|
30
30
|
kind: 'contrast',
|
|
31
31
|
relationship: 'DetailPanel is non-modal and in-flow; Drawer is modal with backdrop',
|
|
32
32
|
},
|
|
33
|
+
{
|
|
34
|
+
component: 'Popover',
|
|
35
|
+
kind: 'contrast',
|
|
36
|
+
relationship:
|
|
37
|
+
'Popover is a small card anchored to an element; Drawer takes its place at phone width',
|
|
38
|
+
},
|
|
33
39
|
],
|
|
34
40
|
example: `<Drawer.Root anchor="right" open={open} onOpenChange={setOpen}>
|
|
35
41
|
<Drawer.Trigger>
|
|
@@ -8,10 +8,14 @@ export const meta: ComponentMeta = {
|
|
|
8
8
|
'Navigation links in a dropdown',
|
|
9
9
|
'Overflow menu for toolbar actions',
|
|
10
10
|
],
|
|
11
|
-
whenNotToUse: [
|
|
11
|
+
whenNotToUse: [
|
|
12
|
+
'Picking a value — use Select',
|
|
13
|
+
'Primary navigation — use SideNav or Tabs',
|
|
14
|
+
'A small form tied to the trigger — use Popover',
|
|
15
|
+
],
|
|
12
16
|
anatomy: {
|
|
13
17
|
required: ['Root', 'Trigger', 'Popup', 'Item'],
|
|
14
|
-
optional: ['LinkItem'],
|
|
18
|
+
optional: ['LinkItem', 'Separator'],
|
|
15
19
|
},
|
|
16
20
|
relatedTo: [
|
|
17
21
|
{
|
|
@@ -31,6 +35,7 @@ export const meta: ComponentMeta = {
|
|
|
31
35
|
<Menu.Popup>
|
|
32
36
|
<Menu.Item onClick={() => console.log('edit')}>Edit</Menu.Item>
|
|
33
37
|
<Menu.Item onClick={() => console.log('duplicate')}>Duplicate</Menu.Item>
|
|
38
|
+
<Menu.Separator />
|
|
34
39
|
<Menu.LinkItem href="/settings">Settings</Menu.LinkItem>
|
|
35
40
|
</Menu.Popup>
|
|
36
41
|
</Menu.Root>`,
|
|
@@ -146,6 +146,32 @@ export const WithLinks: Story = {
|
|
|
146
146
|
},
|
|
147
147
|
}
|
|
148
148
|
|
|
149
|
+
export const WithSeparator: Story = {
|
|
150
|
+
render: () => (
|
|
151
|
+
<Menu.Root>
|
|
152
|
+
<Menu.Trigger>History</Menu.Trigger>
|
|
153
|
+
<Menu.Popup>
|
|
154
|
+
<Menu.Item>Added SCADA to Operations</Menu.Item>
|
|
155
|
+
<Menu.Item>Renamed 2 capabilities</Menu.Item>
|
|
156
|
+
<Menu.Separator />
|
|
157
|
+
<Menu.Item>See full history</Menu.Item>
|
|
158
|
+
</Menu.Popup>
|
|
159
|
+
</Menu.Root>
|
|
160
|
+
),
|
|
161
|
+
play: async ({canvas, userEvent}) => {
|
|
162
|
+
await userEvent.click(canvas.getByRole('button', {name: /History/}))
|
|
163
|
+
const menu = canvas.getByRole('menu')
|
|
164
|
+
const separator = canvas.getByRole('separator')
|
|
165
|
+
await expect(separator).toHaveAttribute('aria-orientation', 'horizontal')
|
|
166
|
+
// The separator is not an item: three items, and arrows pass over it.
|
|
167
|
+
const items = canvas.getAllByRole('menuitem')
|
|
168
|
+
await expect(items.length).toBe(3)
|
|
169
|
+
await userEvent.keyboard('{ArrowDown}')
|
|
170
|
+
await userEvent.keyboard('{ArrowDown}')
|
|
171
|
+
await expect(menu).toHaveAttribute('aria-activedescendant', items[2].id)
|
|
172
|
+
},
|
|
173
|
+
}
|
|
174
|
+
|
|
149
175
|
// Misuse on purpose, so it runs as a test but stays out of the sidebar:
|
|
150
176
|
// Menu.Trigger is the button, and a Button inside it earns a dev warning.
|
|
151
177
|
export const NestedControlWarns: Story = {
|
|
@@ -159,10 +159,19 @@ function LinkItem({href, children}: LinkItemProps) {
|
|
|
159
159
|
)
|
|
160
160
|
}
|
|
161
161
|
|
|
162
|
+
// --- Separator ---
|
|
163
|
+
// A rule between groups of items. role="separator" is a valid child of a
|
|
164
|
+
// menu (WAI-ARIA menu pattern), and it is never registered as an item, so
|
|
165
|
+
// arrow keys and the active descendant pass over it.
|
|
166
|
+
function Separator() {
|
|
167
|
+
return <html.div role="separator" aria-orientation="horizontal" style={styles.separator} />
|
|
168
|
+
}
|
|
169
|
+
|
|
162
170
|
export const Menu = {
|
|
163
171
|
Root,
|
|
164
172
|
Trigger,
|
|
165
173
|
Popup,
|
|
166
174
|
Item,
|
|
167
175
|
LinkItem,
|
|
176
|
+
Separator,
|
|
168
177
|
}
|
|
@@ -80,6 +80,14 @@ export const styles = css.create({
|
|
|
80
80
|
itemHighlighted: {
|
|
81
81
|
backgroundColor: colors.bgCardHover,
|
|
82
82
|
},
|
|
83
|
+
separator: {
|
|
84
|
+
height: 0,
|
|
85
|
+
marginTop: spacing.xs,
|
|
86
|
+
marginBottom: spacing.xs,
|
|
87
|
+
borderTopWidth: 1,
|
|
88
|
+
borderTopStyle: 'solid',
|
|
89
|
+
borderTopColor: colors.border,
|
|
90
|
+
},
|
|
83
91
|
linkItem: {
|
|
84
92
|
textDecoration: 'none',
|
|
85
93
|
color: {
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import type {ComponentMeta} from '../component-meta'
|
|
2
|
+
|
|
3
|
+
export const meta: ComponentMeta = {
|
|
4
|
+
description:
|
|
5
|
+
'Non-modal anchored overlay for small interactive content. Compound component — Root is required (throws without it). Anchored to Popover.Trigger, or to a virtual anchor (a () => DOMRect) when there is no Trigger; call reposition() through a ref when the anchor moves.',
|
|
6
|
+
whenToUse: [
|
|
7
|
+
'A small form or controls tied to one element that stay open while you work in them',
|
|
8
|
+
'Anchoring to a spot that is not a button (a canvas point, a text selection) with a virtual anchor',
|
|
9
|
+
],
|
|
10
|
+
whenNotToUse: [
|
|
11
|
+
'A list of actions — use Menu',
|
|
12
|
+
'Hover-only text — use Tooltip',
|
|
13
|
+
'A blocking decision — use Dialog or ConfirmDialog',
|
|
14
|
+
'A long editor, or one that follows the selection — use DetailPanel',
|
|
15
|
+
'Phone width — use Drawer',
|
|
16
|
+
],
|
|
17
|
+
anatomy: {
|
|
18
|
+
required: ['Root', 'Popup'],
|
|
19
|
+
optional: ['Trigger', 'Close'],
|
|
20
|
+
},
|
|
21
|
+
relatedTo: [
|
|
22
|
+
{
|
|
23
|
+
component: 'Menu',
|
|
24
|
+
kind: 'contrast',
|
|
25
|
+
relationship: 'Menu opens a list of actions; Popover holds small forms and controls',
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
component: 'Tooltip',
|
|
29
|
+
kind: 'contrast',
|
|
30
|
+
relationship:
|
|
31
|
+
'Tooltip shows hover-only text; Popover holds interactive content and stays open',
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
component: 'Dialog',
|
|
35
|
+
kind: 'contrast',
|
|
36
|
+
relationship: 'Dialog is modal and blocks the page; Popover is non-modal and anchored',
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
component: 'DetailPanel',
|
|
40
|
+
kind: 'contrast',
|
|
41
|
+
relationship:
|
|
42
|
+
'DetailPanel is an in-flow panel for long editing; Popover is a small anchored overlay',
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
component: 'Drawer',
|
|
46
|
+
kind: 'contrast',
|
|
47
|
+
relationship:
|
|
48
|
+
'Drawer is the modal edge panel for phone width; Popover is anchored and non-modal',
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
component: 'Field',
|
|
52
|
+
kind: 'composition',
|
|
53
|
+
relationship: 'Place Field.Root with its control inside Popover.Popup for a labelled field',
|
|
54
|
+
},
|
|
55
|
+
],
|
|
56
|
+
example: `const ref = useRef<PopoverHandle>(null)
|
|
57
|
+
const anchor = () => new DOMRect(point.x, point.y, 0, 0)
|
|
58
|
+
|
|
59
|
+
<Popover.Root ref={ref} open={open} onOpenChange={setOpen} anchor={anchor} side="bottom" align="start">
|
|
60
|
+
<Popover.Popup label="Edit label">
|
|
61
|
+
<Field.Root>
|
|
62
|
+
<Field.Label>Label</Field.Label>
|
|
63
|
+
<Input value={label} onChange={(e) => setLabel(e.target.value)} />
|
|
64
|
+
</Field.Root>
|
|
65
|
+
<Popover.Close>Done</Popover.Close>
|
|
66
|
+
</Popover.Popup>
|
|
67
|
+
</Popover.Root>
|
|
68
|
+
// after the anchor moves: ref.current?.reposition()`,
|
|
69
|
+
}
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
import {useRef, useState} from 'react'
|
|
2
|
+
import type {Meta, StoryObj} from '@storybook/react'
|
|
3
|
+
import {expect, fn, waitFor, within} from 'storybook/test'
|
|
4
|
+
import {css, html} from 'react-strict-dom'
|
|
5
|
+
import {spacing} from '@duro-app/tokens/tokens/spacing.css'
|
|
6
|
+
import {colors} from '@duro-app/tokens/tokens/colors.css'
|
|
7
|
+
import {Popover, type PopoverHandle} from './Popover'
|
|
8
|
+
import {Button} from '../Button/Button'
|
|
9
|
+
import {Field} from '../Field/Field'
|
|
10
|
+
import {Input} from '../Input/Input'
|
|
11
|
+
import {Select} from '../Select/Select'
|
|
12
|
+
import {Stack} from '../Stack/Stack'
|
|
13
|
+
|
|
14
|
+
const meta: Meta = {
|
|
15
|
+
title: 'Components/Popover',
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export default meta
|
|
19
|
+
type Story = StoryObj
|
|
20
|
+
|
|
21
|
+
// The popup is portalled out of the story's canvas, so query the document.
|
|
22
|
+
const page = () => within(document.body)
|
|
23
|
+
|
|
24
|
+
const styles = css.create({
|
|
25
|
+
stage: {
|
|
26
|
+
position: 'relative',
|
|
27
|
+
height: 320,
|
|
28
|
+
borderWidth: 1,
|
|
29
|
+
borderStyle: 'dashed',
|
|
30
|
+
borderColor: colors.border,
|
|
31
|
+
padding: spacing.md,
|
|
32
|
+
},
|
|
33
|
+
spot: {
|
|
34
|
+
position: 'absolute',
|
|
35
|
+
top: 120,
|
|
36
|
+
left: 160,
|
|
37
|
+
width: 120,
|
|
38
|
+
padding: spacing.sm,
|
|
39
|
+
borderWidth: 1,
|
|
40
|
+
borderStyle: 'solid',
|
|
41
|
+
borderColor: colors.accent,
|
|
42
|
+
},
|
|
43
|
+
shifted: {
|
|
44
|
+
transform: 'translateX(120px)',
|
|
45
|
+
},
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
export const Default: Story = {
|
|
49
|
+
render: () => (
|
|
50
|
+
<Popover.Root>
|
|
51
|
+
<Popover.Trigger>Rename</Popover.Trigger>
|
|
52
|
+
<Popover.Popup label="Rename">
|
|
53
|
+
<Stack gap="sm">
|
|
54
|
+
<Field.Root>
|
|
55
|
+
<Field.Label>Label</Field.Label>
|
|
56
|
+
<Input defaultValue="Get started" />
|
|
57
|
+
</Field.Root>
|
|
58
|
+
<Popover.Close>Done</Popover.Close>
|
|
59
|
+
</Stack>
|
|
60
|
+
</Popover.Popup>
|
|
61
|
+
</Popover.Root>
|
|
62
|
+
),
|
|
63
|
+
play: async ({canvas, userEvent}) => {
|
|
64
|
+
const trigger = canvas.getByRole('button', {name: 'Rename'})
|
|
65
|
+
await userEvent.click(trigger)
|
|
66
|
+
await expect(trigger).toHaveAttribute('aria-expanded', 'true')
|
|
67
|
+
const dialog = page().getByRole('dialog', {name: 'Rename'})
|
|
68
|
+
await expect(dialog).toBeVisible()
|
|
69
|
+
// Focus moves to the first field.
|
|
70
|
+
await waitFor(() => expect(page().getByRole('textbox', {name: 'Label'})).toHaveFocus())
|
|
71
|
+
|
|
72
|
+
await userEvent.keyboard('{Escape}')
|
|
73
|
+
await expect(page().queryByRole('dialog')).not.toBeInTheDocument()
|
|
74
|
+
// ...and comes back to the trigger.
|
|
75
|
+
await waitFor(() => expect(trigger).toHaveFocus())
|
|
76
|
+
},
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export const CloseButton: Story = {
|
|
80
|
+
render: Default.render,
|
|
81
|
+
play: async ({canvas, userEvent}) => {
|
|
82
|
+
const trigger = canvas.getByRole('button', {name: 'Rename'})
|
|
83
|
+
await userEvent.click(trigger)
|
|
84
|
+
await userEvent.click(page().getByRole('button', {name: 'Done'}))
|
|
85
|
+
await expect(page().queryByRole('dialog')).not.toBeInTheDocument()
|
|
86
|
+
await waitFor(() => expect(trigger).toHaveFocus())
|
|
87
|
+
},
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function WithIgnored({onOpenChange}: {onOpenChange: (open: boolean) => void}) {
|
|
91
|
+
const ignored = useRef<HTMLButtonElement | null>(null)
|
|
92
|
+
return (
|
|
93
|
+
<Stack gap="md">
|
|
94
|
+
<Popover.Root onOpenChange={onOpenChange} ignore={[() => ignored.current]}>
|
|
95
|
+
<Popover.Trigger>Open</Popover.Trigger>
|
|
96
|
+
<Popover.Popup label="Item">
|
|
97
|
+
<Popover.Close>Close</Popover.Close>
|
|
98
|
+
</Popover.Popup>
|
|
99
|
+
</Popover.Root>
|
|
100
|
+
<html.button ref={ignored} type="button">
|
|
101
|
+
Grip (ignored)
|
|
102
|
+
</html.button>
|
|
103
|
+
<html.button type="button">Elsewhere</html.button>
|
|
104
|
+
</Stack>
|
|
105
|
+
)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export const OutsidePress: Story = {
|
|
109
|
+
render: () => <WithIgnored onOpenChange={fn()} />,
|
|
110
|
+
play: async ({canvas, userEvent}) => {
|
|
111
|
+
await userEvent.click(canvas.getByRole('button', {name: 'Open'}))
|
|
112
|
+
await expect(page().getByRole('dialog', {name: 'Item'})).toBeVisible()
|
|
113
|
+
|
|
114
|
+
// A press on an ignored element keeps it open.
|
|
115
|
+
await userEvent.click(canvas.getByRole('button', {name: 'Grip (ignored)'}))
|
|
116
|
+
await expect(page().getByRole('dialog', {name: 'Item'})).toBeVisible()
|
|
117
|
+
|
|
118
|
+
// A press anywhere else closes it.
|
|
119
|
+
await userEvent.click(canvas.getByRole('button', {name: 'Elsewhere'}))
|
|
120
|
+
await expect(page().queryByRole('dialog')).not.toBeInTheDocument()
|
|
121
|
+
},
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export const NestedSelectOwnsEscape: Story = {
|
|
125
|
+
render: () => (
|
|
126
|
+
<Popover.Root>
|
|
127
|
+
<Popover.Trigger>Style</Popover.Trigger>
|
|
128
|
+
<Popover.Popup label="Style">
|
|
129
|
+
<Select.Root defaultValue="primary">
|
|
130
|
+
<Select.Trigger aria-label="Variant">
|
|
131
|
+
<Select.Value />
|
|
132
|
+
<Select.Icon />
|
|
133
|
+
</Select.Trigger>
|
|
134
|
+
<Select.Popup>
|
|
135
|
+
<Select.Item value="primary">
|
|
136
|
+
<Select.ItemText>Primary</Select.ItemText>
|
|
137
|
+
</Select.Item>
|
|
138
|
+
<Select.Item value="secondary">
|
|
139
|
+
<Select.ItemText>Secondary</Select.ItemText>
|
|
140
|
+
</Select.Item>
|
|
141
|
+
</Select.Popup>
|
|
142
|
+
</Select.Root>
|
|
143
|
+
</Popover.Popup>
|
|
144
|
+
</Popover.Root>
|
|
145
|
+
),
|
|
146
|
+
play: async ({canvas, userEvent}) => {
|
|
147
|
+
await userEvent.click(canvas.getByRole('button', {name: 'Style'}))
|
|
148
|
+
const variant = page().getByRole('combobox', {name: 'Variant'})
|
|
149
|
+
await userEvent.click(variant)
|
|
150
|
+
await expect(variant).toHaveAttribute('aria-expanded', 'true')
|
|
151
|
+
|
|
152
|
+
// The first Esc closes only the Select...
|
|
153
|
+
await userEvent.keyboard('{Escape}')
|
|
154
|
+
await expect(variant).toHaveAttribute('aria-expanded', 'false')
|
|
155
|
+
await expect(page().getByRole('dialog', {name: 'Style'})).toBeVisible()
|
|
156
|
+
|
|
157
|
+
// ...the second closes the popover.
|
|
158
|
+
await userEvent.keyboard('{Escape}')
|
|
159
|
+
await expect(page().queryByRole('dialog')).not.toBeInTheDocument()
|
|
160
|
+
},
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function VirtualAnchorDemo() {
|
|
164
|
+
const spot = useRef<HTMLDivElement | null>(null)
|
|
165
|
+
const handle = useRef<PopoverHandle | null>(null)
|
|
166
|
+
const [open, setOpen] = useState(false)
|
|
167
|
+
const [shifted, setShifted] = useState(false)
|
|
168
|
+
return (
|
|
169
|
+
<Stack gap="sm">
|
|
170
|
+
<Button onClick={() => setOpen(true)}>Edit the spot</Button>
|
|
171
|
+
<html.div style={styles.stage}>
|
|
172
|
+
<html.div ref={spot} style={[styles.spot, shifted && styles.shifted]}>
|
|
173
|
+
Spot
|
|
174
|
+
</html.div>
|
|
175
|
+
</html.div>
|
|
176
|
+
<Popover.Root
|
|
177
|
+
ref={handle}
|
|
178
|
+
open={open}
|
|
179
|
+
onOpenChange={setOpen}
|
|
180
|
+
anchor={() => spot.current!.getBoundingClientRect()}
|
|
181
|
+
>
|
|
182
|
+
<Popover.Popup label="Spot">
|
|
183
|
+
<Stack gap="sm">
|
|
184
|
+
<Button
|
|
185
|
+
variant="secondary"
|
|
186
|
+
onClick={() => {
|
|
187
|
+
setShifted((s) => !s)
|
|
188
|
+
// The anchor moved without a scroll or resize: re-measure.
|
|
189
|
+
requestAnimationFrame(() => handle.current?.reposition())
|
|
190
|
+
}}
|
|
191
|
+
>
|
|
192
|
+
Move the spot
|
|
193
|
+
</Button>
|
|
194
|
+
<Popover.Close>Done</Popover.Close>
|
|
195
|
+
</Stack>
|
|
196
|
+
</Popover.Popup>
|
|
197
|
+
</Popover.Root>
|
|
198
|
+
</Stack>
|
|
199
|
+
)
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export const VirtualAnchor: Story = {
|
|
203
|
+
render: () => <VirtualAnchorDemo />,
|
|
204
|
+
play: async ({canvas, userEvent}) => {
|
|
205
|
+
await userEvent.click(canvas.getByRole('button', {name: 'Edit the spot'}))
|
|
206
|
+
const dialog = page().getByRole('dialog', {name: 'Spot'})
|
|
207
|
+
const spot = canvas.getByText('Spot')
|
|
208
|
+
await waitFor(() =>
|
|
209
|
+
expect(dialog.getBoundingClientRect().left).toBeCloseTo(spot.getBoundingClientRect().left, 0),
|
|
210
|
+
)
|
|
211
|
+
// Below the spot (side bottom).
|
|
212
|
+
await expect(dialog.getBoundingClientRect().top).toBeGreaterThanOrEqual(
|
|
213
|
+
spot.getBoundingClientRect().bottom,
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
const before = spot.getBoundingClientRect().left
|
|
217
|
+
await userEvent.click(page().getByRole('button', {name: 'Move the spot'}))
|
|
218
|
+
await waitFor(() => expect(spot.getBoundingClientRect().left).toBeGreaterThan(before))
|
|
219
|
+
await waitFor(() =>
|
|
220
|
+
expect(dialog.getBoundingClientRect().left).toBeCloseTo(spot.getBoundingClientRect().left, 0),
|
|
221
|
+
)
|
|
222
|
+
},
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function DisclosureDemo() {
|
|
226
|
+
const [expanded, setExpanded] = useState(true)
|
|
227
|
+
return (
|
|
228
|
+
<Popover.Root>
|
|
229
|
+
<Popover.Trigger>Details</Popover.Trigger>
|
|
230
|
+
<Popover.Popup label="Details">
|
|
231
|
+
<html.button type="button" aria-expanded={expanded} onClick={() => setExpanded((e) => !e)}>
|
|
232
|
+
More
|
|
233
|
+
</html.button>
|
|
234
|
+
</Popover.Popup>
|
|
235
|
+
</Popover.Root>
|
|
236
|
+
)
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
export const DisclosureDoesNotOwnEscape: Story = {
|
|
240
|
+
render: () => <DisclosureDemo />,
|
|
241
|
+
play: async ({canvas, userEvent}) => {
|
|
242
|
+
await userEvent.click(canvas.getByRole('button', {name: 'Details'}))
|
|
243
|
+
const more = page().getByRole('button', {name: 'More'})
|
|
244
|
+
await waitFor(() => expect(more).toHaveFocus())
|
|
245
|
+
await expect(more).toHaveAttribute('aria-expanded', 'true')
|
|
246
|
+
// An expanded disclosure opens no popup of its own: Esc closes the popover.
|
|
247
|
+
await userEvent.keyboard('{Escape}')
|
|
248
|
+
await expect(page().queryByRole('dialog')).not.toBeInTheDocument()
|
|
249
|
+
},
|
|
250
|
+
}
|