@estiva-app/ui 0.9.0 → 0.10.1
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/Avatar.d.ts +13 -1
- package/dist/Avatar.d.ts.map +1 -1
- package/dist/AvatarGroup.d.ts.map +1 -1
- package/dist/Banner.d.ts +16 -1
- package/dist/Banner.d.ts.map +1 -1
- package/dist/Breadcrumb.d.ts +9 -0
- package/dist/Breadcrumb.d.ts.map +1 -1
- package/dist/Button.d.ts +12 -2
- package/dist/Button.d.ts.map +1 -1
- package/dist/Checkbox.d.ts +2 -0
- package/dist/Checkbox.d.ts.map +1 -1
- package/dist/Chip.d.ts +3 -2
- package/dist/Chip.d.ts.map +1 -1
- package/dist/ChipInput.d.ts +3 -1
- package/dist/ChipInput.d.ts.map +1 -1
- package/dist/ConfirmDialog.d.ts +5 -0
- package/dist/ConfirmDialog.d.ts.map +1 -1
- package/dist/DialogShell.d.ts +27 -3
- package/dist/DialogShell.d.ts.map +1 -1
- package/dist/EditableText.d.ts +6 -0
- package/dist/EditableText.d.ts.map +1 -1
- package/dist/Field.d.ts +42 -25
- package/dist/Field.d.ts.map +1 -1
- package/dist/IconButton.d.ts +3 -2
- package/dist/IconButton.d.ts.map +1 -1
- package/dist/IdentityMenu.d.ts +25 -13
- package/dist/IdentityMenu.d.ts.map +1 -1
- package/dist/Menu.d.ts +65 -82
- package/dist/Menu.d.ts.map +1 -1
- package/dist/PersonTrigger.d.ts.map +1 -1
- package/dist/Popover.d.ts +86 -0
- package/dist/Popover.d.ts.map +1 -0
- package/dist/PreviewCard.d.ts +39 -0
- package/dist/PreviewCard.d.ts.map +1 -0
- package/dist/Reaction.d.ts.map +1 -1
- package/dist/ReactionPicker.d.ts +55 -0
- package/dist/ReactionPicker.d.ts.map +1 -0
- package/dist/SearchInput.d.ts +5 -0
- package/dist/SearchInput.d.ts.map +1 -1
- package/dist/Select.d.ts +26 -16
- package/dist/Select.d.ts.map +1 -1
- package/dist/Tabs.d.ts +11 -1
- package/dist/Tabs.d.ts.map +1 -1
- package/dist/TextInput.d.ts +7 -1
- package/dist/TextInput.d.ts.map +1 -1
- package/dist/Textarea.d.ts +8 -1
- package/dist/Textarea.d.ts.map +1 -1
- package/dist/Toolbar.d.ts +95 -0
- package/dist/Toolbar.d.ts.map +1 -0
- package/dist/Tooltip.d.ts +36 -10
- package/dist/Tooltip.d.ts.map +1 -1
- package/dist/cn.d.ts.map +1 -1
- package/dist/fit.d.ts +7 -72
- package/dist/fit.d.ts.map +1 -1
- package/dist/index.d.ts +6 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +888 -763
- package/dist/index.js.map +4 -4
- package/dist/triggerDisabled.d.ts +10 -0
- package/dist/triggerDisabled.d.ts.map +1 -0
- package/package.json +1 -1
- package/src/Avatar.mdx +17 -0
- package/src/Avatar.name.test.tsx +99 -0
- package/src/Avatar.tsx +25 -3
- package/src/AvatarGroup.tsx +4 -1
- package/src/Banner.mdx +10 -2
- package/src/Banner.stories.tsx +29 -0
- package/src/Banner.test.tsx +60 -0
- package/src/Banner.tsx +35 -3
- package/src/Breadcrumb.mdx +5 -2
- package/src/Breadcrumb.test.tsx +44 -0
- package/src/Breadcrumb.tsx +10 -2
- package/src/Button.compose.test.tsx +119 -0
- package/src/Button.mdx +7 -5
- package/src/Button.test.tsx +2 -1
- package/src/Button.tsx +21 -5
- package/src/Checkbox.tsx +3 -0
- package/src/Chip.tsx +3 -2
- package/src/ChipInput.tsx +4 -0
- package/src/ConfirmDialog.mdx +20 -1
- package/src/ConfirmDialog.test.tsx +109 -0
- package/src/ConfirmDialog.tsx +6 -0
- package/src/DialogShell.mdx +18 -4
- package/src/DialogShell.stories.tsx +56 -6
- package/src/DialogShell.test.tsx +184 -0
- package/src/DialogShell.tsx +108 -39
- package/src/EditableText.mdx +6 -1
- package/src/EditableText.test.tsx +156 -0
- package/src/EditableText.tsx +23 -5
- package/src/Field.mdx +23 -3
- package/src/Field.stories.tsx +35 -0
- package/src/Field.test.tsx +144 -26
- package/src/Field.tsx +94 -59
- package/src/IconButton.mdx +6 -2
- package/src/IconButton.test.tsx +5 -2
- package/src/IconButton.tsx +12 -6
- package/src/IdentityMenu.mdx +29 -6
- package/src/IdentityMenu.stories.tsx +41 -6
- package/src/IdentityMenu.tsx +74 -47
- package/src/Kbd.stories.tsx +3 -3
- package/src/Menu.mdx +88 -75
- package/src/Menu.stories.tsx +120 -52
- package/src/Menu.test.tsx +315 -0
- package/src/Menu.tsx +347 -251
- package/src/MenuItem.stories.tsx +135 -98
- package/src/NavItem.mdx +1 -1
- package/src/PersonTrigger.mdx +20 -14
- package/src/PersonTrigger.tsx +23 -1
- package/src/Popover.mdx +130 -0
- package/src/Popover.stories.tsx +222 -0
- package/src/Popover.test.tsx +188 -0
- package/src/Popover.tsx +156 -0
- package/src/PreviewCard.mdx +98 -0
- package/src/PreviewCard.stories.tsx +168 -0
- package/src/PreviewCard.test.tsx +83 -0
- package/src/PreviewCard.tsx +91 -0
- package/src/Rail.mdx +63 -0
- package/src/Rail.stories.tsx +81 -0
- package/src/Reaction.mdx +4 -2
- package/src/Reaction.tsx +4 -2
- package/src/ReactionPicker.mdx +85 -0
- package/src/ReactionPicker.stories.tsx +120 -0
- package/src/ReactionPicker.test.tsx +118 -0
- package/src/ReactionPicker.tsx +88 -0
- package/src/SearchInput.mdx +6 -2
- package/src/SearchInput.tsx +7 -1
- package/src/Select.mdx +16 -5
- package/src/Select.test.tsx +157 -0
- package/src/Select.tsx +89 -214
- package/src/Sidebar.mdx +6 -2
- package/src/Tabs.mdx +5 -0
- package/src/Tabs.test.tsx +15 -0
- package/src/Tabs.tsx +17 -2
- package/src/TextInput.mdx +8 -2
- package/src/TextInput.tsx +10 -7
- package/src/Textarea.mdx +6 -2
- package/src/Textarea.tsx +14 -9
- package/src/Toolbar.mdx +104 -0
- package/src/Toolbar.stories.tsx +142 -0
- package/src/Toolbar.test.tsx +253 -0
- package/src/Toolbar.tsx +158 -0
- package/src/Tooltip.mdx +43 -5
- package/src/Tooltip.stories.tsx +26 -0
- package/src/Tooltip.test.tsx +195 -0
- package/src/Tooltip.tsx +150 -48
- package/src/cn.ts +1 -1
- package/src/fit.test.ts +101 -0
- package/src/fit.ts +19 -63
- package/src/index.ts +6 -3
- package/src/triggerDisabled.ts +13 -0
- package/stories/Choosing.mdx +8 -3
- package/tailwind-preset.js +7 -0
- package/src/Menu.fit.test.ts +0 -90
- package/src/Select.fit.test.ts +0 -101
package/src/MenuItem.stories.tsx
CHANGED
|
@@ -1,98 +1,135 @@
|
|
|
1
|
-
import type { Meta, StoryObj } from '@storybook/react-vite'
|
|
2
|
-
import { IconCopy, IconPin, IconTrash } from '@tabler/icons-react'
|
|
3
|
-
import { Avatar } from './Avatar'
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
},
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
export
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
export const
|
|
48
|
-
args: { label: '
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
/**
|
|
52
|
-
export const
|
|
53
|
-
args: {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
),
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
/**
|
|
89
|
-
export const
|
|
90
|
-
args: {
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react-vite'
|
|
2
|
+
import { IconCopy, IconPin, IconTrash } from '@tabler/icons-react'
|
|
3
|
+
import { Avatar } from './Avatar'
|
|
4
|
+
import { useRef, useState } from 'react'
|
|
5
|
+
import { Button } from './Button'
|
|
6
|
+
import { Divider } from './Divider'
|
|
7
|
+
import { EnterHint, Menu, MenuItem, MenuPanel, MenuSub } from './Menu'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* One row of a menu. Anatomy: `leading` (a 16px icon or an Avatar) · label
|
|
11
|
+
* with an optional `description` line · one thing at the right edge —
|
|
12
|
+
* `trailing` (any hint), else `shortcut` (the kbd chip), else the `submenu`
|
|
13
|
+
* chevron. Stories sit on a `MenuPanel` — the menu's surface, drawn without
|
|
14
|
+
* its behaviour — so the row is seen where it lives. A live menu portals and
|
|
15
|
+
* places itself, so it cannot stand in the page (Katerina, D25); **Menu →
|
|
16
|
+
* FromATrigger** is where the keyboard and the placement are.
|
|
17
|
+
*/
|
|
18
|
+
const meta = {
|
|
19
|
+
title: 'Overlays/MenuItem',
|
|
20
|
+
component: MenuItem,
|
|
21
|
+
decorators: [
|
|
22
|
+
(Story) => (
|
|
23
|
+
<div className="flex min-h-[120px] w-full items-center justify-center">
|
|
24
|
+
<MenuPanel className="w-72">
|
|
25
|
+
<Story />
|
|
26
|
+
</MenuPanel>
|
|
27
|
+
</div>
|
|
28
|
+
),
|
|
29
|
+
],
|
|
30
|
+
args: { label: 'Rename', onClick: () => {} },
|
|
31
|
+
argTypes: {
|
|
32
|
+
onClick: { control: false },
|
|
33
|
+
leading: { control: false },
|
|
34
|
+
trailing: { control: false },
|
|
35
|
+
},
|
|
36
|
+
} satisfies Meta<typeof MenuItem>
|
|
37
|
+
|
|
38
|
+
export default meta
|
|
39
|
+
type Story = StoryObj<typeof meta>
|
|
40
|
+
|
|
41
|
+
export const Default: Story = {}
|
|
42
|
+
|
|
43
|
+
export const WithAnIcon: Story = {
|
|
44
|
+
args: { label: 'Pin', leading: <IconPin size={16} stroke={1.5} className="text-text-secondary" /> },
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export const WithAShortcut: Story = {
|
|
48
|
+
args: { label: 'Copy link', leading: <IconCopy size={16} stroke={1.5} className="text-text-secondary" />, shortcut: 'Ctrl+C' },
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** The row opens another menu. */
|
|
52
|
+
export const Submenu: Story = {
|
|
53
|
+
args: { label: 'Move to…', submenu: true },
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* The submenu, working. The story above draws the row at rest — the chevron is
|
|
58
|
+
* the whole affordance — but a row that opens another menu can only be tried
|
|
59
|
+
* inside a real one, so here is one.
|
|
60
|
+
*
|
|
61
|
+
* Hover **Move to…**, or arrow onto it and press →. The panel opens beside the
|
|
62
|
+
* row, flips to the other side at a screen edge, and stays open while you
|
|
63
|
+
* cross the diagonal into it.
|
|
64
|
+
*
|
|
65
|
+
* From the keyboard: → opens it and leaves the highlight on the row, ↓ steps
|
|
66
|
+
* into it, and ← closes it and puts the highlight back on the row. Measured in
|
|
67
|
+
* that order — → then ← alone does nothing, because focus has not entered yet.
|
|
68
|
+
*/
|
|
69
|
+
export const SubmenuLive: Story = {
|
|
70
|
+
parameters: { controls: { disable: true } },
|
|
71
|
+
decorators: [(Story) => <Story />],
|
|
72
|
+
render: () => (
|
|
73
|
+
<div className="flex min-h-[220px] w-full items-start justify-center pt-4">
|
|
74
|
+
<Menu trigger={<Button variant="outlined">Open the menu</Button>}>
|
|
75
|
+
<MenuItem label="Rename" onClick={() => {}} />
|
|
76
|
+
<MenuSub label="Move to…">
|
|
77
|
+
<MenuItem label="Item one" onClick={() => {}} />
|
|
78
|
+
<MenuItem label="Item two" onClick={() => {}} />
|
|
79
|
+
<MenuItem label="Item three" onClick={() => {}} />
|
|
80
|
+
</MenuSub>
|
|
81
|
+
<Divider className="my-1" />
|
|
82
|
+
<MenuItem label="Delete" destructive onClick={() => {}} />
|
|
83
|
+
</Menu>
|
|
84
|
+
</div>
|
|
85
|
+
),
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** A person as a row — the face, the name, a second line, a trailing hint. Peek's mention rows. */
|
|
89
|
+
export const APerson: Story = {
|
|
90
|
+
args: {
|
|
91
|
+
label: 'Ana Duarte',
|
|
92
|
+
description: 'Product designer',
|
|
93
|
+
leading: <Avatar name="Ana Duarte" size={32} />,
|
|
94
|
+
trailing: <EnterHint />,
|
|
95
|
+
},
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** The submenu's chosen value. */
|
|
99
|
+
export const Selected: Story = {
|
|
100
|
+
args: { label: 'Item one', selected: true },
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export const Destructive: Story = {
|
|
104
|
+
args: { label: 'Delete', destructive: true, leading: <IconTrash size={16} stroke={1.5} className="text-error-default" /> },
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Every shape on one canvas. */
|
|
108
|
+
export const AllVariants: Story = {
|
|
109
|
+
parameters: { controls: { disable: true } },
|
|
110
|
+
render: () => (
|
|
111
|
+
<>
|
|
112
|
+
<MenuItem label="Bare" onClick={() => {}} />
|
|
113
|
+
<MenuItem label="With an icon" leading={<IconPin size={16} stroke={1.5} className="text-text-secondary" />} onClick={() => {}} />
|
|
114
|
+
<MenuItem label="With a shortcut" shortcut="Ctrl+K" onClick={() => {}} />
|
|
115
|
+
<MenuItem label="Icon and shortcut" leading={<IconCopy size={16} stroke={1.5} className="text-text-secondary" />} shortcut="Ctrl+C" onClick={() => {}} />
|
|
116
|
+
<MenuItem label="Opens another menu" submenu onClick={() => {}} />
|
|
117
|
+
<MenuItem label="Ana Duarte" description="Product designer" leading={<Avatar name="Ana Duarte" size={32} />} trailing={<EnterHint />} onClick={() => {}} />
|
|
118
|
+
<MenuItem label="The chosen value" selected onClick={() => {}} />
|
|
119
|
+
<MenuItem label="Destructive" destructive leading={<IconTrash size={16} stroke={1.5} className="text-error-default" />} onClick={() => {}} />
|
|
120
|
+
<MenuItem label="A very long label that runs out of room and truncates" onClick={() => {}} />
|
|
121
|
+
</>
|
|
122
|
+
),
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** The tall picker row — 48px, px-3, a 32px face or tile, the description line, and the hint while highlighted. TopicMenu, MentionMenu, the files menu and the launcher all draw this row. */
|
|
126
|
+
export const Tall: Story = {
|
|
127
|
+
args: {
|
|
128
|
+
size: 'tall',
|
|
129
|
+
label: 'Ana Duarte',
|
|
130
|
+
description: 'Product designer',
|
|
131
|
+
leading: <Avatar name="Ana Duarte" size={32} />,
|
|
132
|
+
selected: true,
|
|
133
|
+
trailing: <EnterHint />,
|
|
134
|
+
},
|
|
135
|
+
}
|
package/src/NavItem.mdx
CHANGED
package/src/PersonTrigger.mdx
CHANGED
|
@@ -29,23 +29,29 @@ fill, because the face fills the whole control.
|
|
|
29
29
|
## How
|
|
30
30
|
|
|
31
31
|
```tsx
|
|
32
|
-
import { PersonTrigger } from '@estiva-app/ui'
|
|
32
|
+
import { Menu, PersonTrigger } from '@estiva-app/ui'
|
|
33
33
|
|
|
34
|
-
<PersonTrigger name={me.name} picture={me.picture}
|
|
35
|
-
|
|
34
|
+
<Menu trigger={<PersonTrigger name={me.name} picture={me.picture} />}>
|
|
35
|
+
…
|
|
36
|
+
</Menu>
|
|
36
37
|
```
|
|
37
38
|
|
|
38
|
-
-
|
|
39
|
-
`aria-expanded
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
39
|
+
- **Hand it to the menu and write nothing else.** Since stage 4 a `Menu` owns
|
|
40
|
+
its trigger, so the open state, the toggle, `aria-expanded`, the placement
|
|
41
|
+
and the focus return are all its. The `onMouseDown` + `stopPropagation`
|
|
42
|
+
guard this page used to show is gone with the trap it worked around.
|
|
43
|
+
- `open` is still there for a caller driving something that is not a `Menu`;
|
|
44
|
+
it holds the row's hover fill and feeds `aria-expanded`. Under a `Menu` it
|
|
45
|
+
is unnecessary — the trigger carries `data-popup-open`, which holds the
|
|
46
|
+
same fill.
|
|
47
|
+
- **The accessible name comes from the person, in both shapes.** The row is
|
|
48
|
+
named by its visible name, and the `compact` face — which has no visible
|
|
49
|
+
text — takes the same name through an `aria-label` of its own, so nothing
|
|
50
|
+
is owed. Until 2026-09-08 that face announced **"AD"**: the initials, read
|
|
51
|
+
as text. Pass an `aria-label` only to name what it *opens* rather than who
|
|
52
|
+
it shows; `IdentityMenu`'s compact shape says "Account menu".
|
|
53
|
+
- It is a native `<button>` on Base UI's Button: it takes a `ref` and every
|
|
54
|
+
native button prop.
|
|
49
55
|
|
|
50
56
|
## Keys
|
|
51
57
|
|
package/src/PersonTrigger.tsx
CHANGED
|
@@ -35,7 +35,24 @@ export function PersonTrigger({ name, picture, fallback, size, open = false, com
|
|
|
35
35
|
type="button"
|
|
36
36
|
aria-haspopup="menu"
|
|
37
37
|
aria-expanded={open}
|
|
38
|
-
|
|
38
|
+
/*
|
|
39
|
+
The face alone is an icon-only control, so it owes a name. It has
|
|
40
|
+
one already — the person — and taking it from there means no caller
|
|
41
|
+
can forget: measured 2026-09-08, this button announced **"AD"**, the
|
|
42
|
+
initials, with no `aria-label` anywhere. A caller that wants to name
|
|
43
|
+
the destination rather than the person still can, and `IdentityMenu`
|
|
44
|
+
does ("Account menu").
|
|
45
|
+
*/
|
|
46
|
+
aria-label={props['aria-label'] ?? name ?? fallback}
|
|
47
|
+
/*
|
|
48
|
+
No `focus:outline-none` here any more. It removed the browser's ring
|
|
49
|
+
and put nothing in its place, so this control — the account trigger
|
|
50
|
+
in Peek's top bar — had **no visible focus at all** (measured
|
|
51
|
+
2026-09-08: `outline: solid 2px rgba(0,0,0,0)`, no shadow). Left
|
|
52
|
+
alone it wears the same ring `Button`, `IconButton` and a `Tab`
|
|
53
|
+
wear, which is the ring the rest of the package already relies on.
|
|
54
|
+
*/
|
|
55
|
+
className={cn('cursor-pointer rounded-full', className)}
|
|
39
56
|
{...props}
|
|
40
57
|
>
|
|
41
58
|
<Avatar name={name} src={picture} size={size ?? 36} />
|
|
@@ -50,6 +67,11 @@ export function PersonTrigger({ name, picture, fallback, size, open = false, com
|
|
|
50
67
|
className={cn(
|
|
51
68
|
'flex h-8 cursor-pointer items-center gap-1.5 rounded-md pl-1.5 pr-1.5 text-body-2 text-text-primary transition-colors hover:bg-bg-hover',
|
|
52
69
|
open && 'bg-bg-hover',
|
|
70
|
+
// Under a `Menu` the open state is Base UI's, not a prop: the trigger
|
|
71
|
+
// carries `data-popup-open` while its menu is up, and sets its own
|
|
72
|
+
// `aria-expanded`. Both spellings hold the fill, so this works whether
|
|
73
|
+
// the caller drives it or the menu does.
|
|
74
|
+
'data-[popup-open]:bg-bg-hover',
|
|
53
75
|
className,
|
|
54
76
|
)}
|
|
55
77
|
{...props}
|
package/src/Popover.mdx
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { Meta, Canvas, Controls } from '@storybook/addon-docs/blocks'
|
|
2
|
+
import * as PopoverStories from './Popover.stories'
|
|
3
|
+
|
|
4
|
+
<Meta of={PopoverStories} />
|
|
5
|
+
|
|
6
|
+
# Popover
|
|
7
|
+
|
|
8
|
+
A floating panel from a trigger: the same elevated surface a **Menu** draws,
|
|
9
|
+
with none of a menu's semantics. Its contents are contents — Tab walks them
|
|
10
|
+
in order, nothing is a menu item, and typing goes where you are typing.
|
|
11
|
+
|
|
12
|
+
<Canvas of={PopoverStories.Default} />
|
|
13
|
+
|
|
14
|
+
## When
|
|
15
|
+
|
|
16
|
+
- A small form beside its trigger: rename, a date, a link address.
|
|
17
|
+
- A row of controls over a text selection — a formatting toolbar.
|
|
18
|
+
- A panel of settings or toggles.
|
|
19
|
+
- Anything you would have reached for `Menu` for because you wanted the box.
|
|
20
|
+
|
|
21
|
+
<Canvas of={PopoverStories.AToolbar} />
|
|
22
|
+
|
|
23
|
+
## When not
|
|
24
|
+
|
|
25
|
+
- A list of actions → **Menu**. That is what the arrow keys, the typeahead
|
|
26
|
+
and `role="menuitem"` are for, and a Popover has none of them.
|
|
27
|
+
- One value from a known set → **Select**.
|
|
28
|
+
- A word for a control → **WithTooltip** — it cannot be pointed at, which
|
|
29
|
+
is the difference.
|
|
30
|
+
- More of a thing on hover → **PreviewCard**.
|
|
31
|
+
- A title, a body and a footer, with the page behind it out of reach →
|
|
32
|
+
**DialogShell**.
|
|
33
|
+
|
|
34
|
+
## How
|
|
35
|
+
|
|
36
|
+
The panel owns the trigger, exactly as `Menu` does: hand it the control and the
|
|
37
|
+
contents, and write no open state, no placement and no dismiss.
|
|
38
|
+
|
|
39
|
+
```tsx
|
|
40
|
+
import { Popover } from '@estiva-app/ui'
|
|
41
|
+
|
|
42
|
+
<Popover trigger={<Button variant="outlined">Rename</Button>} ariaLabel="Rename this">
|
|
43
|
+
<TextInput value={name} onChange={onChange} aria-label="Name" />
|
|
44
|
+
</Popover>
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
- **`trigger`** is any element that forwards its ref and spreads its props —
|
|
48
|
+
this package's `Button`, `IconButton` and `PersonTrigger` all do.
|
|
49
|
+
- **Focus lands on the first thing in the panel** when it opens — the field, in
|
|
50
|
+
the panel this component exists for — and goes back to the trigger when it
|
|
51
|
+
closes. Nothing is written to get that.
|
|
52
|
+
- **`actionsRef`** gives `close()`, for the Cancel and Save a form panel ends
|
|
53
|
+
with. Unlike a menu row, a control inside a panel does not close it by being
|
|
54
|
+
pressed: it is content, and content may be used more than once.
|
|
55
|
+
- **`align="right"`** hangs the panel's right edge from the trigger's.
|
|
56
|
+
- **`side` is a preference, not a promise.** It says which side to try; Base UI
|
|
57
|
+
measures the room and flips when there is none — which is the reason the
|
|
58
|
+
placement is its job rather than arithmetic of ours. **A panel holding a
|
|
59
|
+
`Toolbar` asks for `top`**: a strip of controls acts on what is under it, so
|
|
60
|
+
it stands over that rather than on top of it.
|
|
61
|
+
- **`open` / `onOpenChange`** are there for a caller that must know or must
|
|
62
|
+
force it. Leave them off and the panel keeps its own state.
|
|
63
|
+
- `ariaLabel` names the panel. A panel with a visible heading can point at it
|
|
64
|
+
with `aria-labelledby` instead.
|
|
65
|
+
- Width, padding and internal rhythm are yours, through `className` — the
|
|
66
|
+
panel is a surface, not a layout.
|
|
67
|
+
|
|
68
|
+
<Canvas of={PopoverStories.FromATrigger} />
|
|
69
|
+
|
|
70
|
+
### A panel with no trigger
|
|
71
|
+
|
|
72
|
+
A toolbar over a text selection hangs from the selection, and there is no
|
|
73
|
+
control that opened it. That is the one case with nothing for Base UI to watch,
|
|
74
|
+
so it is the one case the caller drives:
|
|
75
|
+
|
|
76
|
+
```tsx
|
|
77
|
+
<Popover
|
|
78
|
+
anchor={selectionRect}
|
|
79
|
+
open={selectionRect !== null}
|
|
80
|
+
onOpenChange={(open) => !open && setSelectionRect(null)}
|
|
81
|
+
ariaLabel="Formatting"
|
|
82
|
+
>
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
- **`anchor`** takes an element or a rect. A new rect moves the panel, so
|
|
86
|
+
re-measuring as the selection changes is all the tracking needed.
|
|
87
|
+
- **It does not take focus**, on purpose: the person is still in the text, and
|
|
88
|
+
a toolbar that moved the caret out of it would end the edit it exists to
|
|
89
|
+
serve. `finalFocus` is therefore only for a panel something inside it *did*
|
|
90
|
+
take focus into — a field the person clicked.
|
|
91
|
+
- **Render it always and toggle `open`.** Not `{rect && <Popover…>}`: mounting
|
|
92
|
+
the panel only while it is open takes Base UI's state machine with it, and
|
|
93
|
+
its exits go too — measured, Escape and re-opening both became intermittent.
|
|
94
|
+
- **Two traps that come with this shape**, both measured rather than reasoned
|
|
95
|
+
about, and both belonging to the caller:
|
|
96
|
+
- **Escape must not reopen what it just closed.** The keydown closes the
|
|
97
|
+
panel; if focus is on the element that reads the selection, the same key's
|
|
98
|
+
*keyup* re-reads a selection that is still perfectly alive. Ignore Escape
|
|
99
|
+
in that handler.
|
|
100
|
+
- **A press in the text is a press outside the panel.** It closes it, and the
|
|
101
|
+
same gesture's new selection opens it again. That is right, but it means
|
|
102
|
+
the panel is remade rather than moved.
|
|
103
|
+
|
|
104
|
+
**The gap, stated: an anchored panel cannot be reached by keyboard.** There is
|
|
105
|
+
no trigger to Tab from and it does not take focus, so everything in one must be
|
|
106
|
+
reachable another way — a shortcut, a menu. A panel that is the only route to
|
|
107
|
+
something belongs on a `trigger`.
|
|
108
|
+
|
|
109
|
+
<Canvas of={PopoverStories.FromASelection} />
|
|
110
|
+
|
|
111
|
+
### When the side it asks for has no room
|
|
112
|
+
|
|
113
|
+
<Canvas of={PopoverStories.FlippedForRoom} />
|
|
114
|
+
|
|
115
|
+
## Keys
|
|
116
|
+
|
|
117
|
+
| Input | What happens |
|
|
118
|
+
|---|---|
|
|
119
|
+
| click / Enter / Space on the trigger | opens, and focus moves to the first thing inside |
|
|
120
|
+
| Tab / Shift-Tab | walks the contents, in order |
|
|
121
|
+
| Escape | closes, and gives focus back to the trigger |
|
|
122
|
+
| a press outside | closes |
|
|
123
|
+
| a second press of the trigger | closes — it is the trigger's own toggle, not a press outside |
|
|
124
|
+
|
|
125
|
+
An anchored panel has the same two exits — Escape and a press outside — but no
|
|
126
|
+
trigger row: it is not opened from the keyboard and does not take focus.
|
|
127
|
+
|
|
128
|
+
## Props
|
|
129
|
+
|
|
130
|
+
<Controls of={PopoverStories.FromATrigger} />
|