@czi-sds/mcp 0.16.0 → 0.17.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/README.md +2 -1
- package/data/component-docs/Accordion.md +291 -0
- package/data/component-docs/Autocomplete.md +525 -0
- package/data/component-docs/Banner.md +231 -0
- package/data/component-docs/Button.md +224 -0
- package/data/component-docs/ButtonDropdown.md +256 -0
- package/data/component-docs/ButtonGroup.md +320 -0
- package/data/component-docs/ButtonToggle.md +397 -0
- package/data/component-docs/Callout.md +268 -0
- package/data/component-docs/CellBasic.md +122 -0
- package/data/component-docs/CellComponent.md +63 -0
- package/data/component-docs/CellHeader.md +128 -0
- package/data/component-docs/ComplexFilter.md +294 -0
- package/data/component-docs/ContentCard.md +406 -0
- package/data/component-docs/Dialog.md +651 -0
- package/data/component-docs/Dropdown.md +155 -0
- package/data/component-docs/DropdownMenu.md +949 -0
- package/data/component-docs/HeatmapChart.md +782 -0
- package/data/component-docs/Hero.md +363 -0
- package/data/component-docs/Icon.md +283 -0
- package/data/component-docs/InputCheckbox.md +205 -0
- package/data/component-docs/InputDropdown.md +482 -0
- package/data/component-docs/InputRadio.md +173 -0
- package/data/component-docs/InputSearch.md +81 -0
- package/data/component-docs/InputSlider.md +189 -0
- package/data/component-docs/InputText.md +110 -0
- package/data/component-docs/InputToggle.md +141 -0
- package/data/component-docs/IntentMessage.md +293 -0
- package/data/component-docs/Legend.md +442 -0
- package/data/component-docs/Link.md +23 -0
- package/data/component-docs/List.md +300 -0
- package/data/component-docs/LoadingIndicator.md +198 -0
- package/data/component-docs/Menu.md +298 -0
- package/data/component-docs/MenuItem.md +100 -0
- package/data/component-docs/NavigationFooter.md +165 -0
- package/data/component-docs/NavigationHeader.md +500 -0
- package/data/component-docs/NavigationJumpTo.md +414 -0
- package/data/component-docs/Notification.md +288 -0
- package/data/component-docs/Pagination.md +67 -0
- package/data/component-docs/Panel.md +636 -0
- package/data/component-docs/Popover.md +448 -0
- package/data/component-docs/PreComposedTable.md +2947 -0
- package/data/component-docs/SegmentedControl.md +396 -0
- package/data/component-docs/StackedBarChart.md +475 -0
- package/data/component-docs/Table.md +2000 -0
- package/data/component-docs/TableHeader.md +53 -0
- package/data/component-docs/TableRow.md +74 -0
- package/data/component-docs/Tabs.md +455 -0
- package/data/component-docs/Tag.md +647 -0
- package/data/component-docs/TagFilter.md +131 -0
- package/data/component-docs/Tooltip.md +561 -0
- package/data/component-docs/TooltipCondensed.md +203 -0
- package/data/component-docs/TooltipTable.md +145 -0
- package/data/component-docs/index.json +262 -0
- package/data/component-list.json +2 -5
- package/data/component-props/Accordion.json +31 -11
- package/data/component-props/Alert.json +3 -3
- package/data/component-props/Autocomplete.json +150 -15
- package/data/component-props/Banner.json +31 -17
- package/data/component-props/Button.json +45 -20
- package/data/component-props/ButtonDropdown.json +36 -17
- package/data/component-props/ButtonDropdownLegacy.json +11 -11
- package/data/component-props/ButtonGroup.json +32 -11
- package/data/component-props/ButtonIcon.json +10 -10
- package/data/component-props/ButtonLegacy.json +19 -19
- package/data/component-props/ButtonToggle.json +32 -19
- package/data/component-props/ButtonToggleLegacy.json +11 -11
- package/data/component-props/Callout.json +58 -17
- package/data/component-props/CellBasic.json +94 -41
- package/data/component-props/CellComponent.json +23 -7
- package/data/component-props/CellHeader.json +62 -19
- package/data/component-props/ComplexFilter.json +52 -21
- package/data/component-props/ContentCard.json +70 -47
- package/data/component-props/Dialog.json +47 -14
- package/data/component-props/Dropdown.json +68 -31
- package/data/component-props/DropdownMenu.json +128 -23
- package/data/component-props/HeatmapChart.json +88 -12
- package/data/component-props/Hero.json +80 -39
- package/data/component-props/Icon.json +20 -9
- package/data/component-props/InputCheckbox.json +36 -14
- package/data/component-props/InputDropdown.json +75 -27
- package/data/component-props/InputRadio.json +31 -13
- package/data/component-props/InputSearch.json +41 -18
- package/data/component-props/InputSlider.json +46 -19
- package/data/component-props/InputText.json +26 -19
- package/data/component-props/InputToggle.json +26 -9
- package/data/component-props/IntentMessage.json +38 -9
- package/data/component-props/Link.json +13 -7
- package/data/component-props/LoadingIndicator.json +11 -3
- package/data/component-props/MenuItem.json +38 -19
- package/data/component-props/MenuSelect.json +8 -8
- package/data/component-props/NavigationFooter.json +49 -19
- package/data/component-props/NavigationHeader.json +150 -37
- package/data/component-props/NavigationJumpTo.json +33 -7
- package/data/component-props/Notification.json +39 -21
- package/data/component-props/Pagination.json +39 -11
- package/data/component-props/Panel.json +62 -9
- package/data/component-props/Popover.json +80 -7
- package/data/component-props/PreComposedTable.json +99 -27
- package/data/component-props/SegmentedControl.json +42 -3
- package/data/component-props/StackedBarChart.json +116 -42
- package/data/component-props/TableRow.json +38 -19
- package/data/component-props/Tabs.json +66 -9
- package/data/component-props/Tag.json +34 -17
- package/data/component-props/TagFilter.json +10 -3
- package/data/component-props/Tooltip.json +49 -18
- package/data/component-props/TooltipCondensed.json +18 -9
- package/data/component-props/TooltipTable.json +17 -7
- package/data/resources/{zeroheight-rules.md → documentation-rules.md} +43 -30
- package/data/resources/figma-rules.md +2 -2
- package/dist/stdio.js +112 -208
- package/package.json +9 -5
- package/data/component-props/ButtonDropdownV2.json +0 -34
- package/data/component-props/ButtonNew.json +0 -47
- package/data/component-props/ButtonToggleV2.json +0 -44
- package/data/component-props/ButtonV2.json +0 -43
- package/data/zeroheight-cache.json +0 -10259
- package/data/zeroheight-exports/Accordion.md +0 -774
- package/data/zeroheight-exports/Banner.md +0 -691
- package/data/zeroheight-exports/Borders.md +0 -117
- package/data/zeroheight-exports/Buttons.md +0 -1854
- package/data/zeroheight-exports/Callout.md +0 -931
- package/data/zeroheight-exports/Colors.md +0 -1755
- package/data/zeroheight-exports/Content-Card.md +0 -309
- package/data/zeroheight-exports/Contributing-to-SDS.md +0 -98
- package/data/zeroheight-exports/Control-Inputs.md +0 -2818
- package/data/zeroheight-exports/Corners.md +0 -104
- package/data/zeroheight-exports/Cover-page.md +0 -3
- package/data/zeroheight-exports/Dialog.md +0 -1367
- package/data/zeroheight-exports/Downloads.md +0 -236
- package/data/zeroheight-exports/Drop-Shadows.md +0 -79
- package/data/zeroheight-exports/Dropdown-Input.md +0 -1776
- package/data/zeroheight-exports/Dropdown-Menus.md +0 -913
- package/data/zeroheight-exports/Element-Status-Tracker.md +0 -267
- package/data/zeroheight-exports/Field-Inputs.md +0 -1405
- package/data/zeroheight-exports/Filters.md +0 -1111
- package/data/zeroheight-exports/Getting-Started.md +0 -1157
- package/data/zeroheight-exports/Hero.md +0 -414
- package/data/zeroheight-exports/Icons.md +0 -2067
- package/data/zeroheight-exports/Intent.md +0 -86
- package/data/zeroheight-exports/Introduction-to-Bases.md +0 -35
- package/data/zeroheight-exports/Introduction-to-Cells.md +0 -35
- package/data/zeroheight-exports/Introduction-to-DNA.md +0 -35
- package/data/zeroheight-exports/Introduction-to-Genes.md +0 -35
- package/data/zeroheight-exports/Introduction.md +0 -115
- package/data/zeroheight-exports/Link.md +0 -432
- package/data/zeroheight-exports/Lists.md +0 -986
- package/data/zeroheight-exports/Loading-Indicator.md +0 -41
- package/data/zeroheight-exports/Navigation.md +0 -1190
- package/data/zeroheight-exports/Notification.md +0 -1038
- package/data/zeroheight-exports/Panel.md +0 -1030
- package/data/zeroheight-exports/README.md +0 -122
- package/data/zeroheight-exports/Responsive-Design.md +0 -330
- package/data/zeroheight-exports/Search-Input.md +0 -91
- package/data/zeroheight-exports/Search.md +0 -822
- package/data/zeroheight-exports/Segmented-Control.md +0 -609
- package/data/zeroheight-exports/Spacing.md +0 -297
- package/data/zeroheight-exports/Table.md +0 -5345
- package/data/zeroheight-exports/Tabs.md +0 -1324
- package/data/zeroheight-exports/Tags.md +0 -1545
- package/data/zeroheight-exports/Text-Link.md +0 -268
- package/data/zeroheight-exports/Theming.md +0 -362
- package/data/zeroheight-exports/Tooltips.md +0 -2015
- package/data/zeroheight-exports/Typography.md +0 -800
|
@@ -0,0 +1,448 @@
|
|
|
1
|
+
# Popover
|
|
2
|
+
|
|
3
|
+
## Source Code
|
|
4
|
+
|
|
5
|
+
The component's source code in the SDS codebase can be found [here](https://github.com/chanzuckerberg/sci-components/blob/main/packages/components/src/core/Popover/index.tsx).
|
|
6
|
+
|
|
7
|
+
## Import
|
|
8
|
+
|
|
9
|
+
**React TypeScript**
|
|
10
|
+
|
|
11
|
+
```tsx
|
|
12
|
+
import { Popover } from "@czi-sds/components";
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Code examples
|
|
16
|
+
|
|
17
|
+
### **Default Popover**
|
|
18
|
+
|
|
19
|
+
The least a popover needs: an anchor kept in state, an `open` derived from it, and an `onClose` that clears it. It opens below the trigger, with the 8px gap the SDS defaults provide.
|
|
20
|
+
|
|
21
|
+
**Example: DefaultPopover**
|
|
22
|
+
|
|
23
|
+
```tsx
|
|
24
|
+
// The least a popover needs: the trigger records itself as the anchor, `open` is
|
|
25
|
+
// derived from whether there is one, and `onClose` clears it. The popover owns no
|
|
26
|
+
// open state of its own, so nothing closes it unless you do.
|
|
27
|
+
//
|
|
28
|
+
// anchorEl has to be state rather than a ref, because the popover measures the
|
|
29
|
+
// anchor as it renders and so needs a render to happen once the element is known.
|
|
30
|
+
//
|
|
31
|
+
// The paper pads itself by 6px and 12px, which is why the text below is written
|
|
32
|
+
// straight onto it. onClose fires for a backdrop click and for Escape; the second
|
|
33
|
+
// argument says which, and is ignored here because both should close.
|
|
34
|
+
|
|
35
|
+
import { useState } from "react";
|
|
36
|
+
import { Button, Popover } from "@czi-sds/components";
|
|
37
|
+
|
|
38
|
+
function App() {
|
|
39
|
+
const [anchorEl, setAnchorEl] = useState<HTMLButtonElement | null>(null);
|
|
40
|
+
const open = Boolean(anchorEl);
|
|
41
|
+
|
|
42
|
+
return (
|
|
43
|
+
<div className="app">
|
|
44
|
+
<Button
|
|
45
|
+
aria-describedby={open ? "default-popover" : undefined}
|
|
46
|
+
onClick={(event) => setAnchorEl(event.currentTarget)}
|
|
47
|
+
sdsStyle="outline"
|
|
48
|
+
sdsType="primary"
|
|
49
|
+
>
|
|
50
|
+
Open Popover
|
|
51
|
+
</Button>
|
|
52
|
+
|
|
53
|
+
<Popover
|
|
54
|
+
anchorEl={anchorEl}
|
|
55
|
+
id="default-popover"
|
|
56
|
+
onClose={() => setAnchorEl(null)}
|
|
57
|
+
open={open}
|
|
58
|
+
>
|
|
59
|
+
Sequencing finished at 14:02.
|
|
60
|
+
</Popover>
|
|
61
|
+
</div>
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export default App;
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### **Positioning**
|
|
69
|
+
|
|
70
|
+
`anchorOrigin` picks the point on the trigger and `transformOrigin` the point on the popover that meets it. Because SDS's defaults are replaced rather than merged, each of these pairs writes the gap back in as a negative number.
|
|
71
|
+
|
|
72
|
+
**Example: PopoverPositioning**
|
|
73
|
+
|
|
74
|
+
```tsx
|
|
75
|
+
// anchorOrigin names the point on the trigger, transformOrigin the point on the
|
|
76
|
+
// popover that meets it. A string names a corner or an edge; a number is measured
|
|
77
|
+
// in pixels and subtracted from the anchor point, which is how a gap is made.
|
|
78
|
+
//
|
|
79
|
+
// SDS already sets both to open the popover 8px below the left of its trigger. The
|
|
80
|
+
// defaults are spread before your props, so each one is replaced whole rather than
|
|
81
|
+
// merged: every pair below has to write its own gap back in as a negative number.
|
|
82
|
+
|
|
83
|
+
import { useState } from "react";
|
|
84
|
+
import {
|
|
85
|
+
Button,
|
|
86
|
+
Popover,
|
|
87
|
+
getSpaces,
|
|
88
|
+
type CommonThemeProps,
|
|
89
|
+
type PopoverProps,
|
|
90
|
+
} from "@czi-sds/components";
|
|
91
|
+
import styled from "@emotion/styled";
|
|
92
|
+
|
|
93
|
+
const GAP = 8;
|
|
94
|
+
|
|
95
|
+
interface Placement {
|
|
96
|
+
anchorOrigin: PopoverProps["anchorOrigin"];
|
|
97
|
+
description: string;
|
|
98
|
+
label: string;
|
|
99
|
+
transformOrigin: PopoverProps["transformOrigin"];
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const PLACEMENTS: Placement[] = [
|
|
103
|
+
{
|
|
104
|
+
anchorOrigin: { horizontal: "left", vertical: "bottom" },
|
|
105
|
+
description: "Below, left aligned",
|
|
106
|
+
label: "Default",
|
|
107
|
+
transformOrigin: { horizontal: 0, vertical: -GAP },
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
anchorOrigin: { horizontal: "right", vertical: "bottom" },
|
|
111
|
+
description: "Below, right aligned",
|
|
112
|
+
label: "Right aligned",
|
|
113
|
+
transformOrigin: { horizontal: "right", vertical: -GAP },
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
anchorOrigin: { horizontal: "right", vertical: "top" },
|
|
117
|
+
description: "Beside the trigger",
|
|
118
|
+
label: "To the side",
|
|
119
|
+
transformOrigin: { horizontal: -GAP, vertical: "top" },
|
|
120
|
+
},
|
|
121
|
+
];
|
|
122
|
+
|
|
123
|
+
const Stage = styled.div<CommonThemeProps>`
|
|
124
|
+
${(props) => {
|
|
125
|
+
const spaces = getSpaces(props);
|
|
126
|
+
|
|
127
|
+
return `
|
|
128
|
+
display: flex;
|
|
129
|
+
flex-wrap: wrap;
|
|
130
|
+
gap: ${spaces?.l}px;
|
|
131
|
+
`;
|
|
132
|
+
}}
|
|
133
|
+
`;
|
|
134
|
+
|
|
135
|
+
function App() {
|
|
136
|
+
const [openIndex, setOpenIndex] = useState<number | null>(null);
|
|
137
|
+
const [anchorEl, setAnchorEl] = useState<HTMLButtonElement | null>(null);
|
|
138
|
+
|
|
139
|
+
const close = () => {
|
|
140
|
+
setOpenIndex(null);
|
|
141
|
+
setAnchorEl(null);
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
return (
|
|
145
|
+
<div className="app">
|
|
146
|
+
<Stage>
|
|
147
|
+
{PLACEMENTS.map((placement, index) => (
|
|
148
|
+
<div key={placement.label}>
|
|
149
|
+
<Button
|
|
150
|
+
onClick={(event) => {
|
|
151
|
+
setAnchorEl(event.currentTarget);
|
|
152
|
+
setOpenIndex(index);
|
|
153
|
+
}}
|
|
154
|
+
sdsStyle="outline"
|
|
155
|
+
sdsType="primary"
|
|
156
|
+
>
|
|
157
|
+
{placement.label}
|
|
158
|
+
</Button>
|
|
159
|
+
|
|
160
|
+
<Popover
|
|
161
|
+
anchorEl={anchorEl}
|
|
162
|
+
anchorOrigin={placement.anchorOrigin}
|
|
163
|
+
onClose={close}
|
|
164
|
+
open={openIndex === index}
|
|
165
|
+
transformOrigin={placement.transformOrigin}
|
|
166
|
+
>
|
|
167
|
+
{placement.description}
|
|
168
|
+
</Popover>
|
|
169
|
+
</div>
|
|
170
|
+
))}
|
|
171
|
+
</Stage>
|
|
172
|
+
</div>
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export default App;
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
### **Popover with rich content**
|
|
180
|
+
|
|
181
|
+
Anything React can render goes on the paper. A popover holding more than a sentence wants a width, which is set through `slotProps.paper`, and a little more room than the paper's 6px and 12px, which the content adds itself.
|
|
182
|
+
|
|
183
|
+
**Example: PopoverWithRichContent**
|
|
184
|
+
|
|
185
|
+
```tsx
|
|
186
|
+
// A popover is only a surface, so anything React can render can go on it: a
|
|
187
|
+
// heading, body copy, and the buttons that act on what it describes.
|
|
188
|
+
//
|
|
189
|
+
// Two things have to be decided for content this size. The width, because a
|
|
190
|
+
// popover is otherwise as wide as its content: slotProps.paper is the way in, and
|
|
191
|
+
// sx works there because SDS sets no width of its own. And the room around it,
|
|
192
|
+
// which the content adds: the paper's 6px and 12px come from a selector on the
|
|
193
|
+
// popover's root that outranks an sx on the paper, so they are there to build on
|
|
194
|
+
// rather than to replace.
|
|
195
|
+
//
|
|
196
|
+
// Focus is trapped while the popover is open, so the two buttons are reachable by
|
|
197
|
+
// keyboard and Escape closes the whole thing.
|
|
198
|
+
|
|
199
|
+
import { useState } from "react";
|
|
200
|
+
import {
|
|
201
|
+
Button,
|
|
202
|
+
Popover,
|
|
203
|
+
fontBodyS,
|
|
204
|
+
fontHeaderS,
|
|
205
|
+
getSemanticColors,
|
|
206
|
+
getSpaces,
|
|
207
|
+
type CommonThemeProps,
|
|
208
|
+
} from "@czi-sds/components";
|
|
209
|
+
import styled from "@emotion/styled";
|
|
210
|
+
|
|
211
|
+
const Content = styled.div<CommonThemeProps>`
|
|
212
|
+
${(props) => {
|
|
213
|
+
const spaces = getSpaces(props);
|
|
214
|
+
|
|
215
|
+
return `
|
|
216
|
+
display: flex;
|
|
217
|
+
flex-direction: column;
|
|
218
|
+
gap: ${spaces?.s}px;
|
|
219
|
+
padding: ${spaces?.xs}px 0;
|
|
220
|
+
`;
|
|
221
|
+
}}
|
|
222
|
+
`;
|
|
223
|
+
|
|
224
|
+
const Title = styled.p`
|
|
225
|
+
${fontHeaderS}
|
|
226
|
+
margin: 0;
|
|
227
|
+
`;
|
|
228
|
+
|
|
229
|
+
const Body = styled.p<CommonThemeProps>`
|
|
230
|
+
${fontBodyS}
|
|
231
|
+
|
|
232
|
+
${(props) => `
|
|
233
|
+
color: ${getSemanticColors(props)?.base?.textSecondary};
|
|
234
|
+
margin: 0;
|
|
235
|
+
`}
|
|
236
|
+
`;
|
|
237
|
+
|
|
238
|
+
const Actions = styled.div<CommonThemeProps>`
|
|
239
|
+
${(props) => `
|
|
240
|
+
display: flex;
|
|
241
|
+
gap: ${getSpaces(props)?.s}px;
|
|
242
|
+
`}
|
|
243
|
+
`;
|
|
244
|
+
|
|
245
|
+
function App() {
|
|
246
|
+
const [anchorEl, setAnchorEl] = useState<HTMLButtonElement | null>(null);
|
|
247
|
+
const open = Boolean(anchorEl);
|
|
248
|
+
const close = () => setAnchorEl(null);
|
|
249
|
+
|
|
250
|
+
return (
|
|
251
|
+
<div className="app">
|
|
252
|
+
<Button
|
|
253
|
+
aria-describedby={open ? "rich-popover" : undefined}
|
|
254
|
+
onClick={(event) => setAnchorEl(event.currentTarget)}
|
|
255
|
+
sdsStyle="outline"
|
|
256
|
+
sdsType="primary"
|
|
257
|
+
>
|
|
258
|
+
Discard run
|
|
259
|
+
</Button>
|
|
260
|
+
|
|
261
|
+
<Popover
|
|
262
|
+
anchorEl={anchorEl}
|
|
263
|
+
id="rich-popover"
|
|
264
|
+
onClose={close}
|
|
265
|
+
open={open}
|
|
266
|
+
slotProps={{ paper: { sx: { maxWidth: 320 } } }}
|
|
267
|
+
>
|
|
268
|
+
<Content>
|
|
269
|
+
<Title>Discard this run?</Title>
|
|
270
|
+
<Body>
|
|
271
|
+
The 240 samples queued behind it will be released, and the results
|
|
272
|
+
already written will be kept.
|
|
273
|
+
</Body>
|
|
274
|
+
<Actions>
|
|
275
|
+
<Button onClick={close} sdsStyle="solid" sdsType="destructive">
|
|
276
|
+
Discard
|
|
277
|
+
</Button>
|
|
278
|
+
<Button onClick={close} sdsStyle="minimal" sdsType="secondary">
|
|
279
|
+
Cancel
|
|
280
|
+
</Button>
|
|
281
|
+
</Actions>
|
|
282
|
+
</Content>
|
|
283
|
+
</Popover>
|
|
284
|
+
</div>
|
|
285
|
+
);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
export default App;
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
### **Opening at the cursor**
|
|
292
|
+
|
|
293
|
+
With `anchorReference` set to `"anchorPosition"` the popover is placed at a point rather than against an element, which is what a right-click menu needs. The vertical offset in the SDS `transformOrigin` applies here too, so the popover sits 8px below the cursor.
|
|
294
|
+
|
|
295
|
+
**Example: PopoverAtCursorPosition**
|
|
296
|
+
|
|
297
|
+
```tsx
|
|
298
|
+
// anchorReference="anchorPosition" positions the popover against a point instead
|
|
299
|
+
// of an element, which is what a right-click menu needs: there is no anchor to
|
|
300
|
+
// measure, only where the pointer was.
|
|
301
|
+
//
|
|
302
|
+
// anchorPosition is read in viewport coordinates, so clientX and clientY go
|
|
303
|
+
// straight in: no scroll offset, and no bounding rect. Everything else works as
|
|
304
|
+
// before, including the 8px the SDS transformOrigin subtracts, which is why the
|
|
305
|
+
// popover opens just below the cursor rather than under it.
|
|
306
|
+
//
|
|
307
|
+
// The browser's own context menu has to be prevented, and the surface is given a
|
|
308
|
+
// tabIndex and a keyboard route of its own, since a right-click is not something
|
|
309
|
+
// every person can perform.
|
|
310
|
+
|
|
311
|
+
import { useState, type MouseEvent } from "react";
|
|
312
|
+
import {
|
|
313
|
+
Popover,
|
|
314
|
+
fontBodyS,
|
|
315
|
+
getSemanticColors,
|
|
316
|
+
getSpaces,
|
|
317
|
+
type CommonThemeProps,
|
|
318
|
+
} from "@czi-sds/components";
|
|
319
|
+
import styled from "@emotion/styled";
|
|
320
|
+
|
|
321
|
+
interface Point {
|
|
322
|
+
left: number;
|
|
323
|
+
top: number;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
const Surface = styled.div<CommonThemeProps>`
|
|
327
|
+
${fontBodyS}
|
|
328
|
+
|
|
329
|
+
${(props) => {
|
|
330
|
+
const semanticColors = getSemanticColors(props);
|
|
331
|
+
const spaces = getSpaces(props);
|
|
332
|
+
|
|
333
|
+
return `
|
|
334
|
+
align-items: center;
|
|
335
|
+
background-color: ${semanticColors?.base?.backgroundSecondary};
|
|
336
|
+
border: 1px dashed ${semanticColors?.base?.divider};
|
|
337
|
+
border-radius: 6px;
|
|
338
|
+
color: ${semanticColors?.base?.textSecondary};
|
|
339
|
+
display: flex;
|
|
340
|
+
justify-content: center;
|
|
341
|
+
min-height: 120px;
|
|
342
|
+
padding: ${spaces?.l}px;
|
|
343
|
+
text-align: center;
|
|
344
|
+
`;
|
|
345
|
+
}}
|
|
346
|
+
`;
|
|
347
|
+
|
|
348
|
+
function App() {
|
|
349
|
+
const [point, setPoint] = useState<Point | null>(null);
|
|
350
|
+
|
|
351
|
+
const openAt = (event: MouseEvent<HTMLDivElement>) => {
|
|
352
|
+
event.preventDefault();
|
|
353
|
+
setPoint({ left: event.clientX, top: event.clientY });
|
|
354
|
+
};
|
|
355
|
+
|
|
356
|
+
return (
|
|
357
|
+
<div className="app">
|
|
358
|
+
<Surface
|
|
359
|
+
onContextMenu={openAt}
|
|
360
|
+
onKeyDown={(event) => {
|
|
361
|
+
if (event.key === "Enter" || event.key === " ") {
|
|
362
|
+
event.preventDefault();
|
|
363
|
+
const box = event.currentTarget.getBoundingClientRect();
|
|
364
|
+
setPoint({ left: box.left + box.width / 2, top: box.top });
|
|
365
|
+
}
|
|
366
|
+
}}
|
|
367
|
+
role="button"
|
|
368
|
+
tabIndex={0}
|
|
369
|
+
>
|
|
370
|
+
Right-click anywhere in here, or focus it and press Enter
|
|
371
|
+
</Surface>
|
|
372
|
+
|
|
373
|
+
<Popover
|
|
374
|
+
anchorPosition={point ?? undefined}
|
|
375
|
+
anchorReference="anchorPosition"
|
|
376
|
+
onClose={() => setPoint(null)}
|
|
377
|
+
open={point !== null}
|
|
378
|
+
>
|
|
379
|
+
Opened at the cursor
|
|
380
|
+
</Popover>
|
|
381
|
+
</div>
|
|
382
|
+
);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
export default App;
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
## SDS vs MUI
|
|
389
|
+
|
|
390
|
+
SDS Popover is a thin wrapper around MUI's Popover. `PopoverProps` is MUI's own type re-exported unchanged, so there are no sds-prefixed props and everything in the MUI documentation applies. What SDS contributes is two positioning defaults and the styling of the paper the content sits on:
|
|
391
|
+
|
|
392
|
+
- `anchorOrigin`: set to `{ vertical: "bottom", horizontal: "left" }`, where MUI's default is `"top"`/`"left"`. An SDS popover therefore opens below its anchor and aligned to its left edge, rather than on top of it.
|
|
393
|
+
|
|
394
|
+
- `transformOrigin`: set to `{ vertical: -8, horizontal: 0 }`, taken from the `s` space token. A number is subtracted from the anchor point rather than naming a corner, so a negative eight reads as an 8px gap between the anchor and the popover.
|
|
395
|
+
|
|
396
|
+
- **Both defaults are spread before your props,** which means each one is replaced whole rather than merged. Passing a `transformOrigin` of your own drops the 8px gap unless you write it back in, and the same is true of `anchorOrigin`.
|
|
397
|
+
|
|
398
|
+
- **The paper is restyled:** the surface colour comes from the theme's `surfacePrimary`, corners are the `l` radius (6px), the shadow is `m`, and there is a hairline in `borderSecondary` at 15% opacity. SDS also sets `background-image: none`, which removes the tint MUI's Paper paints over surfaces in dark mode.
|
|
399
|
+
|
|
400
|
+
- **The hairline is an outline, not a border.** It is drawn outside the paper's box, so it never adds to the popover's size or shifts the content inside it.
|
|
401
|
+
|
|
402
|
+
- **elevation has no visible effect.** SDS sets the shadow on the paper with enough specificity to win over MUI's elevation styles, so every popover carries the same one.
|
|
403
|
+
|
|
404
|
+
- **The paper already has padding** of 6px vertically and 12px horizontally, and it is best treated as fixed: SDS sets it through a descendant selector on the popover's root, which outranks an `sx` on the paper slot. Content that needs more room should add its own padding rather than try to replace what is there.
|
|
405
|
+
|
|
406
|
+
## MUI Documentation
|
|
407
|
+
|
|
408
|
+
Documentation for the underlying MUI component can be found [here](https://mui.com/material-ui/react-popover/), and its full API [here](https://mui.com/material-ui/api/popover/).
|
|
409
|
+
|
|
410
|
+
## Behavior and accessibility
|
|
411
|
+
|
|
412
|
+
- A Popover is built on MUI's Modal. It renders in a portal at the end of the document, traps focus while it is open, hides the rest of the page from assistive technology, and locks the page's scroll. It is a modal surface that happens to be anchored, not a floating layer over a page that carries on as normal.
|
|
413
|
+
|
|
414
|
+
- The backdrop is invisible but present, so a click anywhere outside the popover is caught by it rather than by whatever is underneath. `onClose` is called with the event and a reason: `"backdropClick"` or `"escapeKeyDown"`. Closing is left to you, since the open state is yours.
|
|
415
|
+
|
|
416
|
+
- **anchorEl has to be state, not a ref.** The popover measures the anchor while rendering, so it needs a render to happen once the element is known. Storing the trigger in a ref leaves the popover with nothing to measure on the first open.
|
|
417
|
+
|
|
418
|
+
- Nothing at all is rendered while `open` is `false`, so the content is mounted fresh on each open and its state starts over. Pass `keepMounted` to keep it in the document instead, worth it when the content is expensive to build or has to stay findable by an in-page search.
|
|
419
|
+
|
|
420
|
+
- The position is worked out when the popover opens and again on window resize; it does not follow its anchor. Page scroll is locked while it is open, so this is usually enough, but an anchor inside a box that scrolls on its own can be scrolled away from its popover.
|
|
421
|
+
|
|
422
|
+
- A popover that would fall off the screen is pulled back inside, leaving `marginThreshold` (16px) between it and the edge. Unlike Tooltip, it does not flip to the other side of its anchor.
|
|
423
|
+
|
|
424
|
+
- Give the popover an `id` and point the trigger at it with `aria-describedby` while it is open, so the popover is announced as the description of the control that opened it.
|
|
425
|
+
|
|
426
|
+
- Popover is the plain anchored surface the richer overlays are built from, and it is often not the component to reach for directly. Use Tooltip for a short hint on hover, DropdownMenu or Menu for a list of choices, Dialog for a task that should interrupt the page, and Popover for anchored content that none of those describe.
|
|
427
|
+
|
|
428
|
+
## Props
|
|
429
|
+
|
|
430
|
+
Popover takes MUI's props and adds none of its own. The ones needed to get a popover working, and those whose SDS defaults differ from MUI's, are listed below. See the MUI documentation for the rest.
|
|
431
|
+
|
|
432
|
+
| Name | Type | Default | Description |
|
|
433
|
+
| -------------------- | -------------------------------------------------------------------------------------------------------------- | -------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
434
|
+
| `open` | `boolean` | - | Required. Whether the popover is shown. Usually derived from whether an anchor has been recorded. |
|
|
435
|
+
| `anchorEl` | `HTMLElement \| (() => HTMLElement) \| null` | - | The element the popover is positioned against. Keep it in state so that setting it causes the render the popover needs in order to measure it. |
|
|
436
|
+
| `onClose` | `(event, reason) => void` | - | Called on backdrop click and on Escape, with `"backdropClick"` or `"escapeKeyDown"` as the reason. Closing the popover is up to you. |
|
|
437
|
+
| `children` | `ReactNode` | - | The content, placed straight onto the paper, which has already padded itself by 6px and 12px. |
|
|
438
|
+
| `anchorOrigin` | `{ vertical: "top" \| "center" \| "bottom" \| number,` `horizontal: "left" \| "center" \| "right" \| number }` | `{ vertical: "bottom", horizontal: "left" }` | The point on the anchor the popover attaches to. SDS changes MUI's vertical default from `"top"` to `"bottom"`. |
|
|
439
|
+
| `transformOrigin` | `{ vertical: "top" \| "center" \| "bottom" \| number,` `horizontal: "left" \| "center" \| "right" \| number }` | `{ vertical: -8, horizontal: 0 }` | The point on the popover that meets the anchor's. The negative eight is what produces the gap; passing your own value replaces it. |
|
|
440
|
+
| `anchorReference` | `"anchorEl" \| "anchorPosition" \| "none"` | `"anchorEl"` | Whether to position against an element, a point on the page, or nothing at all. `"anchorPosition"` is how a popover opens at the cursor. |
|
|
441
|
+
| `anchorPosition` | `{ top: number, left: number }` | - | The point to open at, in viewport coordinates. Read only when `anchorReference` is `"anchorPosition"`. |
|
|
442
|
+
| `slotProps` | `{ paper, backdrop, root, transition }` | - | Props for the parts the popover is made of. `slotProps.paper` is where a width or a max height belongs; the padding there is SDS's and outranks an `sx` on the slot. |
|
|
443
|
+
| `marginThreshold` | `number` | `16` | How much room is kept between the popover and the edge of the window when it has to be pulled back on screen. |
|
|
444
|
+
| `keepMounted` | `boolean` | `false` | Keeps the content in the document while closed, preserving its state at the cost of rendering it up front. |
|
|
445
|
+
| `disableScrollLock` | `boolean` | `false` | Lets the page scroll while the popover is open. The popover stays where it was placed, so the anchor scrolls out from under it. |
|
|
446
|
+
| `disablePortal` | `boolean` | `false` | Renders the popover where it is written rather than at the end of the document, which puts it back within reach of an ancestor's overflow. |
|
|
447
|
+
| `elevation` | `number` | `8` | MUI's prop. It has no visible effect, since SDS sets the paper's shadow itself. |
|
|
448
|
+
| `transitionDuration` | `number \| { appear, enter, exit } \| "auto"` | `"auto"` | How long the grow transition runs. `"auto"` scales it to the size of the popover. |
|