@crystallize/design-system 1.11.3 → 1.11.5
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/CHANGELOG.md +12 -0
- package/dist/index.css +28 -9
- package/dist/index.d.ts +38 -31
- package/dist/index.js +91 -67
- package/dist/index.mjs +55 -31
- package/package.json +4 -3
- package/src/action-menu/ActionMenu.stories.tsx +12 -0
- package/src/action-menu/action-menu.css +22 -3
- package/src/action-menu/action-menu.test.tsx +53 -0
- package/src/action-menu/action-menu.tsx +26 -5
- package/src/button/button.test.tsx +22 -0
- package/src/dropdown-menu/dropdown-menu-root.tsx +16 -10
- package/src/dropdown-menu/dropdown-menu.css +1 -1
- package/src/dropdown-menu/index.ts +3 -1
- package/src/popover/index.ts +1 -0
- package/src/popover/popover-close.tsx +18 -0
- package/src/popover/popover.css +11 -0
- package/src/popover/popover.stories.tsx +48 -0
- package/src/popover/popover.tsx +45 -0
- package/src/rich-text-editor/plugins/ActionsPlugin/index.tsx +4 -2
- package/src/rich-text-editor/tests/rich-text-editor-code.test.tsx +1 -1
package/dist/index.mjs
CHANGED
|
@@ -191,8 +191,11 @@ function Card({ children, className, variant, ...delegated }) {
|
|
|
191
191
|
// src/card/index.ts
|
|
192
192
|
var cardToken = "c-card";
|
|
193
193
|
|
|
194
|
+
// src/action-menu/action-menu.tsx
|
|
195
|
+
import { cva as cva3 } from "class-variance-authority";
|
|
196
|
+
|
|
194
197
|
// src/dropdown-menu/index.ts
|
|
195
|
-
import { Portal, Separator } from "@radix-ui/react-dropdown-menu";
|
|
198
|
+
import { Portal as Portal2, Separator } from "@radix-ui/react-dropdown-menu";
|
|
196
199
|
|
|
197
200
|
// src/dropdown-menu/dropdown-menu-item.tsx
|
|
198
201
|
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
|
|
@@ -222,9 +225,10 @@ import { jsx as jsx6, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
|
222
225
|
function DropdownMenuRoot({
|
|
223
226
|
children,
|
|
224
227
|
content,
|
|
225
|
-
alignContent = "
|
|
228
|
+
alignContent = "center",
|
|
226
229
|
disabled,
|
|
227
230
|
onOpenChange,
|
|
231
|
+
container,
|
|
228
232
|
...delegated
|
|
229
233
|
}) {
|
|
230
234
|
return /* @__PURE__ */ jsxs3(DropdownMenuPrimitive3.Root, {
|
|
@@ -235,12 +239,15 @@ function DropdownMenuRoot({
|
|
|
235
239
|
asChild: true,
|
|
236
240
|
children
|
|
237
241
|
}),
|
|
238
|
-
/* @__PURE__ */ jsx6(DropdownMenuPrimitive3.
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
242
|
+
/* @__PURE__ */ jsx6(DropdownMenuPrimitive3.Portal, {
|
|
243
|
+
container,
|
|
244
|
+
children: /* @__PURE__ */ jsx6(DropdownMenuPrimitive3.Content, {
|
|
245
|
+
align: alignContent,
|
|
246
|
+
sideOffset: 5,
|
|
247
|
+
className: "c-dropdown-menu-content",
|
|
248
|
+
...delegated,
|
|
249
|
+
children: content
|
|
250
|
+
})
|
|
244
251
|
})
|
|
245
252
|
]
|
|
246
253
|
});
|
|
@@ -252,7 +259,7 @@ var DropdownMenu = {
|
|
|
252
259
|
Item: DropdownMenuItem,
|
|
253
260
|
Label: DropdownMenuLabel,
|
|
254
261
|
Separator,
|
|
255
|
-
Portal
|
|
262
|
+
Portal: Portal2
|
|
256
263
|
};
|
|
257
264
|
|
|
258
265
|
// src/action-menu/action-item.tsx
|
|
@@ -278,15 +285,28 @@ function Separator2({ className }) {
|
|
|
278
285
|
|
|
279
286
|
// src/action-menu/action-menu.tsx
|
|
280
287
|
import { jsx as jsx9, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
281
|
-
|
|
288
|
+
var buttonStyles2 = cva3("c-action-menu", {
|
|
289
|
+
variants: {
|
|
290
|
+
size: {
|
|
291
|
+
xs: "c-action-menu-xs",
|
|
292
|
+
sm: "c-action-menu-sm"
|
|
293
|
+
}
|
|
294
|
+
},
|
|
295
|
+
defaultVariants: {
|
|
296
|
+
size: "sm"
|
|
297
|
+
}
|
|
298
|
+
});
|
|
299
|
+
function ActionMenu({ children, container, tabIndex, size }) {
|
|
282
300
|
return /* @__PURE__ */ jsx9(DropdownMenu.Root, {
|
|
283
301
|
content: children,
|
|
284
302
|
alignContent: "center",
|
|
303
|
+
container,
|
|
285
304
|
children: /* @__PURE__ */ jsxs4("button", {
|
|
305
|
+
"aria-label": "more options",
|
|
306
|
+
className: buttonStyles2({ size }),
|
|
307
|
+
"data-testid": "action-menu-button",
|
|
286
308
|
tabIndex,
|
|
287
309
|
type: "button",
|
|
288
|
-
className: "c-action-menu",
|
|
289
|
-
"aria-label": "more options",
|
|
290
310
|
children: [
|
|
291
311
|
/* @__PURE__ */ jsx9("span", {
|
|
292
312
|
className: "sr-only",
|
|
@@ -310,7 +330,7 @@ ActionMenu.Separator = Separator2;
|
|
|
310
330
|
|
|
311
331
|
// src/avatar/avatar.tsx
|
|
312
332
|
import { forwardRef as forwardRef3 } from "react";
|
|
313
|
-
import { cva as
|
|
333
|
+
import { cva as cva4 } from "class-variance-authority";
|
|
314
334
|
|
|
315
335
|
// src/avatar/get-initials.ts
|
|
316
336
|
var getInitials = (name) => {
|
|
@@ -321,7 +341,7 @@ var getInitials = (name) => {
|
|
|
321
341
|
|
|
322
342
|
// src/avatar/avatar.tsx
|
|
323
343
|
import { jsx as jsx10 } from "react/jsx-runtime";
|
|
324
|
-
var avatarClassName =
|
|
344
|
+
var avatarClassName = cva4(["c-avatar"], {
|
|
325
345
|
variants: {
|
|
326
346
|
size: {
|
|
327
347
|
md: "c-avatar-md",
|
|
@@ -383,7 +403,7 @@ var destroyFns = [];
|
|
|
383
403
|
|
|
384
404
|
// src/dialog/dialog.tsx
|
|
385
405
|
import * as DialogPrimitive from "@radix-ui/react-dialog";
|
|
386
|
-
import { cva as
|
|
406
|
+
import { cva as cva5, cx as cx5 } from "class-variance-authority";
|
|
387
407
|
|
|
388
408
|
// src/iconography/add.tsx
|
|
389
409
|
import { forwardRef as forwardRef5 } from "react";
|
|
@@ -2912,7 +2932,7 @@ var IconMap = {
|
|
|
2912
2932
|
info: Icon.Info,
|
|
2913
2933
|
warning: Icon.Warning
|
|
2914
2934
|
};
|
|
2915
|
-
var dialogContentStyles =
|
|
2935
|
+
var dialogContentStyles = cva5("c-dialog", {
|
|
2916
2936
|
variants: {
|
|
2917
2937
|
withIcon: {
|
|
2918
2938
|
true: "c-dialog-with-icon"
|
|
@@ -3203,9 +3223,9 @@ function destroyAll() {
|
|
|
3203
3223
|
|
|
3204
3224
|
// src/icon-button/icon-button.tsx
|
|
3205
3225
|
import { forwardRef as forwardRef54 } from "react";
|
|
3206
|
-
import { cva as
|
|
3226
|
+
import { cva as cva6 } from "class-variance-authority";
|
|
3207
3227
|
import { jsx as jsx64 } from "react/jsx-runtime";
|
|
3208
|
-
var
|
|
3228
|
+
var buttonStyles3 = cva6(["c-icon-button"], {
|
|
3209
3229
|
variants: {
|
|
3210
3230
|
variant: {
|
|
3211
3231
|
default: "",
|
|
@@ -3229,7 +3249,7 @@ var IconButton = forwardRef54(
|
|
|
3229
3249
|
return /* @__PURE__ */ jsx64("button", {
|
|
3230
3250
|
ref,
|
|
3231
3251
|
type,
|
|
3232
|
-
className:
|
|
3252
|
+
className: buttonStyles3({ size, variant, className }),
|
|
3233
3253
|
...delegated,
|
|
3234
3254
|
children
|
|
3235
3255
|
});
|
|
@@ -3239,9 +3259,9 @@ IconButton.displayName = "Button";
|
|
|
3239
3259
|
|
|
3240
3260
|
// src/inline-radio/inline-radio.tsx
|
|
3241
3261
|
import * as RadioGroupPrimitive from "@radix-ui/react-radio-group";
|
|
3242
|
-
import { cx as cx6, cva as
|
|
3262
|
+
import { cx as cx6, cva as cva7 } from "class-variance-authority";
|
|
3243
3263
|
import { jsx as jsx65 } from "react/jsx-runtime";
|
|
3244
|
-
var inlineRadioGroupStyles =
|
|
3264
|
+
var inlineRadioGroupStyles = cva7("c-inline-radio-group", {
|
|
3245
3265
|
variants: {
|
|
3246
3266
|
size: {
|
|
3247
3267
|
xs: "c-inline-radio-group-xs",
|
|
@@ -3277,13 +3297,13 @@ var InlineRadio = {
|
|
|
3277
3297
|
|
|
3278
3298
|
// src/input-with-label/input-with-label.tsx
|
|
3279
3299
|
import { forwardRef as forwardRef58 } from "react";
|
|
3280
|
-
import { cva as
|
|
3300
|
+
import { cva as cva9, cx as cx8 } from "class-variance-authority";
|
|
3281
3301
|
|
|
3282
3302
|
// src/input/input.tsx
|
|
3283
|
-
import { cva as
|
|
3303
|
+
import { cva as cva8 } from "class-variance-authority";
|
|
3284
3304
|
import { forwardRef as forwardRef55 } from "react";
|
|
3285
3305
|
import { jsx as jsx66 } from "react/jsx-runtime";
|
|
3286
|
-
var inputStyles =
|
|
3306
|
+
var inputStyles = cva8(["c-input"], {
|
|
3287
3307
|
variants: {},
|
|
3288
3308
|
defaultVariants: {}
|
|
3289
3309
|
});
|
|
@@ -3333,7 +3353,7 @@ var Triangle = forwardRef57((delegated, ref) => {
|
|
|
3333
3353
|
|
|
3334
3354
|
// src/input-with-label/input-with-label.tsx
|
|
3335
3355
|
import { Fragment, jsx as jsx69, jsxs as jsxs54 } from "react/jsx-runtime";
|
|
3336
|
-
var inputWithLabelStyles =
|
|
3356
|
+
var inputWithLabelStyles = cva9(["c-input-with-label"], {
|
|
3337
3357
|
variants: {
|
|
3338
3358
|
variant: {
|
|
3339
3359
|
default: "",
|
|
@@ -3434,9 +3454,9 @@ SelectItem.displayName = "SelectItem";
|
|
|
3434
3454
|
// src/select/select-root.tsx
|
|
3435
3455
|
import { forwardRef as forwardRef60 } from "react";
|
|
3436
3456
|
import * as SelectPrimitives2 from "@radix-ui/react-select";
|
|
3437
|
-
import { cva as
|
|
3457
|
+
import { cva as cva10 } from "class-variance-authority";
|
|
3438
3458
|
import { jsx as jsx73, jsxs as jsxs56 } from "react/jsx-runtime";
|
|
3439
|
-
var selectTriggerStyles =
|
|
3459
|
+
var selectTriggerStyles = cva10("c-select-trigger", {
|
|
3440
3460
|
variants: {
|
|
3441
3461
|
size: {
|
|
3442
3462
|
xs: "c-select-trigger-xs",
|
|
@@ -3519,9 +3539,9 @@ var Slider = forwardRef61(({ className, transparentRange, ...delegated }, ref) =
|
|
|
3519
3539
|
});
|
|
3520
3540
|
|
|
3521
3541
|
// src/tag/tag.tsx
|
|
3522
|
-
import { cva as
|
|
3542
|
+
import { cva as cva11 } from "class-variance-authority";
|
|
3523
3543
|
import { jsx as jsx75 } from "react/jsx-runtime";
|
|
3524
|
-
var tagStyles =
|
|
3544
|
+
var tagStyles = cva11("c-tag", {
|
|
3525
3545
|
variants: {
|
|
3526
3546
|
variant: {
|
|
3527
3547
|
default: "",
|
|
@@ -3541,7 +3561,7 @@ function Tag({ children, className, variant, ...delegated }) {
|
|
|
3541
3561
|
}
|
|
3542
3562
|
|
|
3543
3563
|
// src/rich-text-editor/rich-text-editor.tsx
|
|
3544
|
-
import { useEffect as useEffect11, useRef as
|
|
3564
|
+
import { useEffect as useEffect11, useRef as useRef6, useState as useState10 } from "react";
|
|
3545
3565
|
import { AutoFocusPlugin } from "@lexical/react/LexicalAutoFocusPlugin";
|
|
3546
3566
|
import { ClearEditorPlugin } from "@lexical/react/LexicalClearEditorPlugin";
|
|
3547
3567
|
import { LexicalComposer } from "@lexical/react/LexicalComposer";
|
|
@@ -5747,6 +5767,7 @@ import {
|
|
|
5747
5767
|
} from "@lexical/utils";
|
|
5748
5768
|
|
|
5749
5769
|
// src/rich-text-editor/plugins/ActionsPlugin/index.tsx
|
|
5770
|
+
import { useRef as useRef5 } from "react";
|
|
5750
5771
|
import { CLEAR_EDITOR_COMMAND } from "lexical";
|
|
5751
5772
|
import { useLexicalComposerContext as useLexicalComposerContext10 } from "@lexical/react/LexicalComposerContext";
|
|
5752
5773
|
import { jsx as jsx87, jsxs as jsxs64 } from "react/jsx-runtime";
|
|
@@ -5764,11 +5785,14 @@ function ActionsPlugin({
|
|
|
5764
5785
|
}) {
|
|
5765
5786
|
const [editor] = useLexicalComposerContext10();
|
|
5766
5787
|
const tr = useTr();
|
|
5788
|
+
const actionMenuAnchorRef = useRef5(null);
|
|
5767
5789
|
return /* @__PURE__ */ jsxs64("div", {
|
|
5790
|
+
ref: actionMenuAnchorRef,
|
|
5768
5791
|
className: "z-50 flex items-center ",
|
|
5769
5792
|
children: [
|
|
5770
5793
|
/* @__PURE__ */ jsx87("div", {}),
|
|
5771
5794
|
/* @__PURE__ */ jsxs64(ActionMenu, {
|
|
5795
|
+
container: actionMenuAnchorRef.current,
|
|
5772
5796
|
children: [
|
|
5773
5797
|
!prepend ? null : prepend.map((actionItem) => /* @__PURE__ */ jsx87(ActionMenu.Item, {
|
|
5774
5798
|
onSelect: actionItem.action,
|
|
@@ -6665,7 +6689,7 @@ function RichTextEditorWithoutContext({
|
|
|
6665
6689
|
const [editor] = useLexicalComposerContext12();
|
|
6666
6690
|
const [floatingAnchorElem, setFloatingAnchorElem] = useState10(null);
|
|
6667
6691
|
const [isSmallWidthViewport, setIsSmallWidthViewport] = useState10(false);
|
|
6668
|
-
const firstOnChangeTriggeredRef =
|
|
6692
|
+
const firstOnChangeTriggeredRef = useRef6(!autoFocus);
|
|
6669
6693
|
const onRef = (_floatingAnchorElem) => {
|
|
6670
6694
|
if (_floatingAnchorElem !== null) {
|
|
6671
6695
|
setFloatingAnchorElem(_floatingAnchorElem);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@crystallize/design-system",
|
|
3
|
-
"version": "1.11.
|
|
3
|
+
"version": "1.11.5",
|
|
4
4
|
"types": "./dist/index.d.ts",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"module": "./dist/index.mjs",
|
|
@@ -40,6 +40,7 @@
|
|
|
40
40
|
"@radix-ui/react-checkbox": "1.0.1",
|
|
41
41
|
"@radix-ui/react-dialog": "1.0.2",
|
|
42
42
|
"@radix-ui/react-dropdown-menu": "2.0.1",
|
|
43
|
+
"@radix-ui/react-popover": "1.0.0",
|
|
43
44
|
"@radix-ui/react-progress": "^1.0.1",
|
|
44
45
|
"@radix-ui/react-radio-group": "1.1.0",
|
|
45
46
|
"@radix-ui/react-select": "1.1.2",
|
|
@@ -88,8 +89,8 @@
|
|
|
88
89
|
"tailwindcss": "^3.3.0",
|
|
89
90
|
"tsup": "^6.5.0",
|
|
90
91
|
"typescript": "^4.9.4",
|
|
91
|
-
"vite": "^4.
|
|
92
|
-
"vitest": "^0.
|
|
92
|
+
"vite": "^4.2.1",
|
|
93
|
+
"vitest": "^0.30.1",
|
|
93
94
|
"tsconfig": "0.0.0"
|
|
94
95
|
},
|
|
95
96
|
"keywords": [
|
|
@@ -23,3 +23,15 @@ export const Default: Story = {
|
|
|
23
23
|
</ActionMenu>
|
|
24
24
|
),
|
|
25
25
|
};
|
|
26
|
+
|
|
27
|
+
export const XS: Story = {
|
|
28
|
+
args: {},
|
|
29
|
+
render: () => (
|
|
30
|
+
<ActionMenu size="xs">
|
|
31
|
+
<ActionMenu.Item onSelect={() => console.warn('Download')}>Download</ActionMenu.Item>
|
|
32
|
+
<ActionMenu.Item className="danger" onSelect={() => console.warn('Delete')}>
|
|
33
|
+
Delete
|
|
34
|
+
</ActionMenu.Item>
|
|
35
|
+
</ActionMenu>
|
|
36
|
+
),
|
|
37
|
+
};
|
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
.c-action-menu {
|
|
2
|
-
@apply flex
|
|
2
|
+
@apply flex shrink-0 rotate-0 cursor-pointer flex-col items-center justify-center gap-[2px] rounded-md border-none bg-transparent -outline-offset-1 transition;
|
|
3
|
+
|
|
3
4
|
&-dot {
|
|
4
|
-
@apply
|
|
5
|
+
@apply rounded-full bg-gray;
|
|
5
6
|
}
|
|
7
|
+
|
|
6
8
|
&[data-state='open'],
|
|
7
9
|
[aria-expanded='true'] {
|
|
8
10
|
@apply flex-row gap-[3px];
|
|
9
11
|
}
|
|
12
|
+
|
|
10
13
|
&:focus-visible {
|
|
11
14
|
@apply outline outline-1 outline-inherit;
|
|
12
15
|
outline-color: currentColor;
|
|
@@ -21,8 +24,24 @@
|
|
|
21
24
|
}
|
|
22
25
|
}
|
|
23
26
|
|
|
27
|
+
.c-action-menu-xs {
|
|
28
|
+
@apply h-6 w-6 p-0.5;
|
|
29
|
+
|
|
30
|
+
.c-action-menu-dot {
|
|
31
|
+
@apply h-0.5 w-0.5;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
.c-action-menu-sm {
|
|
36
|
+
@apply h-8 w-8 p-1;
|
|
37
|
+
|
|
38
|
+
.c-action-menu-dot {
|
|
39
|
+
@apply h-1 w-1;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
24
43
|
.c-action-menu-item {
|
|
25
|
-
@apply flex cursor-pointer items-center gap-2 py-2.5
|
|
44
|
+
@apply flex cursor-pointer items-center gap-2 px-5 py-2.5 font-sans text-sm font-medium text-gray;
|
|
26
45
|
|
|
27
46
|
&:hover {
|
|
28
47
|
@apply bg-gray-50-900;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { render, screen } from '@testing-library/react';
|
|
2
|
+
import userEvent from '@testing-library/user-event';
|
|
3
|
+
|
|
4
|
+
import { ActionMenu } from './index';
|
|
5
|
+
|
|
6
|
+
describe('ActionMenu', () => {
|
|
7
|
+
it('renders correctly', () => {
|
|
8
|
+
render(
|
|
9
|
+
<ActionMenu>
|
|
10
|
+
<ActionMenu.Item onSelect={() => {}}>Delete</ActionMenu.Item>
|
|
11
|
+
</ActionMenu>,
|
|
12
|
+
);
|
|
13
|
+
|
|
14
|
+
expect(screen.getByLabelText(/more options/i)).toBeInTheDocument();
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('opens the menu and selects an option when clicking on an action item', async () => {
|
|
18
|
+
const onSelectOption = vi.fn();
|
|
19
|
+
const callbackThatShouldNotBeExecuted = vi.fn();
|
|
20
|
+
const user = userEvent.setup();
|
|
21
|
+
|
|
22
|
+
const touchableProps = { onSelect: onSelectOption, onClick: onSelectOption };
|
|
23
|
+
const touchablePropsThatShouldNotBeCalled = {
|
|
24
|
+
onSelect: callbackThatShouldNotBeExecuted,
|
|
25
|
+
onClick: callbackThatShouldNotBeExecuted,
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
render(
|
|
29
|
+
<ActionMenu>
|
|
30
|
+
<ActionMenu.Item {...touchableProps}>Add</ActionMenu.Item>
|
|
31
|
+
<ActionMenu.Item {...touchablePropsThatShouldNotBeCalled}>Delete</ActionMenu.Item>
|
|
32
|
+
</ActionMenu>,
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
const triggerButton = screen.getByTestId('action-menu-button') as HTMLButtonElement;
|
|
36
|
+
await user.click(triggerButton);
|
|
37
|
+
|
|
38
|
+
// The user should be able to see the dropdown menu
|
|
39
|
+
const dropdownMenu = await screen.getByRole('menu');
|
|
40
|
+
expect(dropdownMenu).toBeInTheDocument();
|
|
41
|
+
|
|
42
|
+
// The user should be able to see the list of options
|
|
43
|
+
const addOption = screen.getByText(/add/i);
|
|
44
|
+
expect(addOption).toBeInTheDocument();
|
|
45
|
+
|
|
46
|
+
// The user can interact with any of the options
|
|
47
|
+
await user.click(addOption);
|
|
48
|
+
|
|
49
|
+
// The following expect is failing
|
|
50
|
+
expect(onSelectOption).toBeCalledTimes(1);
|
|
51
|
+
expect(callbackThatShouldNotBeExecuted).toBeCalledTimes(0);
|
|
52
|
+
});
|
|
53
|
+
});
|
|
@@ -1,18 +1,39 @@
|
|
|
1
1
|
import type { ReactNode } from 'react';
|
|
2
|
+
import { VariantProps, cva } from 'class-variance-authority';
|
|
2
3
|
|
|
3
|
-
import { DropdownMenu } from '../dropdown-menu';
|
|
4
|
+
import { DropdownMenu, Container } from '../dropdown-menu';
|
|
4
5
|
import { Item } from './action-item';
|
|
5
6
|
import { Separator } from './action-item-separator';
|
|
6
7
|
|
|
7
|
-
type
|
|
8
|
+
type ButtonStylesProps = VariantProps<typeof buttonStyles>;
|
|
9
|
+
const buttonStyles = cva('c-action-menu', {
|
|
10
|
+
variants: {
|
|
11
|
+
size: {
|
|
12
|
+
xs: 'c-action-menu-xs',
|
|
13
|
+
sm: 'c-action-menu-sm',
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
defaultVariants: {
|
|
17
|
+
size: 'sm',
|
|
18
|
+
},
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
type ActionMenuProps = ButtonStylesProps & {
|
|
8
22
|
children: ReactNode;
|
|
23
|
+
container?: Container;
|
|
9
24
|
tabIndex?: number;
|
|
10
25
|
};
|
|
11
26
|
|
|
12
|
-
export function ActionMenu({ children, tabIndex }: ActionMenuProps) {
|
|
27
|
+
export function ActionMenu({ children, container, tabIndex, size }: ActionMenuProps) {
|
|
13
28
|
return (
|
|
14
|
-
<DropdownMenu.Root content={children} alignContent="center">
|
|
15
|
-
<button
|
|
29
|
+
<DropdownMenu.Root content={children} alignContent="center" container={container}>
|
|
30
|
+
<button
|
|
31
|
+
aria-label="more options"
|
|
32
|
+
className={buttonStyles({ size })}
|
|
33
|
+
data-testid="action-menu-button"
|
|
34
|
+
tabIndex={tabIndex}
|
|
35
|
+
type="button"
|
|
36
|
+
>
|
|
16
37
|
<span className="sr-only">Open more options</span>
|
|
17
38
|
<span className="c-action-menu-dot"></span>
|
|
18
39
|
<span className="c-action-menu-dot"></span>
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { render, screen } from '@testing-library/react';
|
|
2
|
+
import userEvent from '@testing-library/user-event';
|
|
3
|
+
|
|
4
|
+
import { Button } from './index';
|
|
5
|
+
|
|
6
|
+
describe('Button', () => {
|
|
7
|
+
it('renders correctly', () => {
|
|
8
|
+
render(<Button>Hey</Button>);
|
|
9
|
+
|
|
10
|
+
expect(screen.getByText(/Hey/i)).toBeInTheDocument();
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it('fires the onClick event', async () => {
|
|
14
|
+
const onClick = vi.fn();
|
|
15
|
+
|
|
16
|
+
render(<Button onClick={onClick}>Hey</Button>);
|
|
17
|
+
|
|
18
|
+
await userEvent.click(screen.getByRole('button'));
|
|
19
|
+
|
|
20
|
+
expect(onClick).toBeCalledTimes(1);
|
|
21
|
+
});
|
|
22
|
+
});
|
|
@@ -1,20 +1,24 @@
|
|
|
1
1
|
import type { ReactNode } from 'react';
|
|
2
2
|
import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu';
|
|
3
3
|
|
|
4
|
-
type
|
|
4
|
+
export type Container = HTMLElement | null | undefined;
|
|
5
|
+
|
|
6
|
+
export type DropdownMenuRootProps = DropdownMenuPrimitive.MenuContentProps & {
|
|
5
7
|
children: ReactNode;
|
|
6
8
|
content: ReactNode;
|
|
7
9
|
alignContent?: 'start' | 'center' | 'end';
|
|
8
10
|
disabled?: boolean;
|
|
9
11
|
onOpenChange?: (open: boolean) => void;
|
|
12
|
+
container?: Container;
|
|
10
13
|
};
|
|
11
14
|
|
|
12
15
|
export function DropdownMenuRoot({
|
|
13
16
|
children,
|
|
14
17
|
content,
|
|
15
|
-
alignContent = '
|
|
18
|
+
alignContent = 'center',
|
|
16
19
|
disabled,
|
|
17
20
|
onOpenChange,
|
|
21
|
+
container,
|
|
18
22
|
...delegated
|
|
19
23
|
}: DropdownMenuRootProps) {
|
|
20
24
|
return (
|
|
@@ -22,14 +26,16 @@ export function DropdownMenuRoot({
|
|
|
22
26
|
<DropdownMenuPrimitive.Trigger disabled={disabled} asChild>
|
|
23
27
|
{children}
|
|
24
28
|
</DropdownMenuPrimitive.Trigger>
|
|
25
|
-
<DropdownMenuPrimitive.
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
29
|
+
<DropdownMenuPrimitive.Portal container={container}>
|
|
30
|
+
<DropdownMenuPrimitive.Content
|
|
31
|
+
align={alignContent}
|
|
32
|
+
sideOffset={5}
|
|
33
|
+
className="c-dropdown-menu-content"
|
|
34
|
+
{...delegated}
|
|
35
|
+
>
|
|
36
|
+
{content}
|
|
37
|
+
</DropdownMenuPrimitive.Content>
|
|
38
|
+
</DropdownMenuPrimitive.Portal>
|
|
33
39
|
</DropdownMenuPrimitive.Root>
|
|
34
40
|
);
|
|
35
41
|
}
|
|
@@ -2,9 +2,11 @@ import { Portal, Separator } from '@radix-ui/react-dropdown-menu';
|
|
|
2
2
|
|
|
3
3
|
import { DropdownMenuItem } from './dropdown-menu-item';
|
|
4
4
|
import { DropdownMenuLabel } from './dropdown-menu-label';
|
|
5
|
-
import { DropdownMenuRoot } from './dropdown-menu-root';
|
|
5
|
+
import { DropdownMenuRoot, type DropdownMenuRootProps, type Container } from './dropdown-menu-root';
|
|
6
6
|
import './dropdown-menu.css';
|
|
7
7
|
|
|
8
|
+
export { DropdownMenuRootProps, Container };
|
|
9
|
+
|
|
8
10
|
export const DropdownMenu = {
|
|
9
11
|
Root: DropdownMenuRoot,
|
|
10
12
|
Item: DropdownMenuItem,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './popover';
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
import * as PopoverPrimitive from '@radix-ui/react-popover';
|
|
3
|
+
|
|
4
|
+
import { Icon } from '../iconography';
|
|
5
|
+
|
|
6
|
+
export type PopoverCloseProps = PopoverPrimitive.PopoverCloseProps & {
|
|
7
|
+
children?: ReactNode;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export function PopoverClose(props: PopoverCloseProps) {
|
|
11
|
+
const { asChild, children } = props;
|
|
12
|
+
|
|
13
|
+
return (
|
|
14
|
+
<PopoverPrimitive.Close asChild={asChild} className="c-popover-close">
|
|
15
|
+
{children ?? <Icon.Cancel width={16} height={16} />}
|
|
16
|
+
</PopoverPrimitive.Close>
|
|
17
|
+
);
|
|
18
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
.c-popover-content {
|
|
2
|
+
@apply rounded bg-elevate text-xs text-gray-700-200 shadow focus-visible:outline-none;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
.c-popover-close {
|
|
6
|
+
@apply absolute right-1 top-1 flex cursor-pointer items-center justify-center rounded-full border-0 bg-transparent p-1;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
.c-popover-arrow {
|
|
10
|
+
@apply fill-white;
|
|
11
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react';
|
|
2
|
+
|
|
3
|
+
import { Popover } from '.';
|
|
4
|
+
import { Icon } from '../iconography';
|
|
5
|
+
|
|
6
|
+
const meta: Meta<typeof Popover> = {
|
|
7
|
+
title: 'Components/Popover',
|
|
8
|
+
component: Popover,
|
|
9
|
+
argTypes: {
|
|
10
|
+
side: {
|
|
11
|
+
options: ['top', 'right', 'bottom', 'left'],
|
|
12
|
+
control: {
|
|
13
|
+
type: 'select',
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
align: {
|
|
17
|
+
options: ['start', 'center', 'end'],
|
|
18
|
+
control: {
|
|
19
|
+
type: 'select',
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export default meta;
|
|
26
|
+
type Story = StoryObj<typeof Popover>;
|
|
27
|
+
|
|
28
|
+
export const SimpleTemplate: Story = {
|
|
29
|
+
name: 'Simple Template',
|
|
30
|
+
render: args => (
|
|
31
|
+
<div className="flex w-1/2 justify-center pt-14">
|
|
32
|
+
<Popover {...args} content="Content goes here">
|
|
33
|
+
<Icon.Customers width={32} height={32} />
|
|
34
|
+
</Popover>
|
|
35
|
+
</div>
|
|
36
|
+
),
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export const WithCustomCloseButton: Story = {
|
|
40
|
+
name: 'With custom Close button',
|
|
41
|
+
render: () => (
|
|
42
|
+
<div className="flex w-1/2 justify-center pt-14">
|
|
43
|
+
<Popover content="Content goes here" side="left" closeButton={<button>close</button>}>
|
|
44
|
+
<Icon.Customers width={32} height={32} />
|
|
45
|
+
</Popover>
|
|
46
|
+
</div>
|
|
47
|
+
),
|
|
48
|
+
};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
import { cx } from 'class-variance-authority';
|
|
3
|
+
import * as PopoverPrimitive from '@radix-ui/react-popover';
|
|
4
|
+
|
|
5
|
+
import './popover.css';
|
|
6
|
+
import { PopoverClose } from './popover-close';
|
|
7
|
+
|
|
8
|
+
type PopoverProps = Pick<PopoverPrimitive.PopoverProps, 'open' | 'onOpenChange'> &
|
|
9
|
+
Pick<PopoverPrimitive.PortalProps, 'container'> &
|
|
10
|
+
PopoverPrimitive.PopperContentProps & {
|
|
11
|
+
children: ReactNode;
|
|
12
|
+
closeButton?: boolean | JSX.Element;
|
|
13
|
+
hasArrow?: boolean;
|
|
14
|
+
content: ReactNode;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export function Popover({
|
|
18
|
+
children,
|
|
19
|
+
closeButton,
|
|
20
|
+
content,
|
|
21
|
+
side,
|
|
22
|
+
open,
|
|
23
|
+
onOpenChange,
|
|
24
|
+
className,
|
|
25
|
+
container,
|
|
26
|
+
hasArrow = true,
|
|
27
|
+
...delegatedContent
|
|
28
|
+
}: PopoverProps) {
|
|
29
|
+
return (
|
|
30
|
+
<PopoverPrimitive.Root open={open} onOpenChange={onOpenChange}>
|
|
31
|
+
<PopoverPrimitive.Trigger asChild>{children}</PopoverPrimitive.Trigger>
|
|
32
|
+
<PopoverPrimitive.Portal container={container}>
|
|
33
|
+
<PopoverPrimitive.Content {...delegatedContent} side={side} className={cx('c-popover-content', className)}>
|
|
34
|
+
{!closeButton ? null : (
|
|
35
|
+
<PopoverClose asChild={typeof closeButton !== 'boolean'}>
|
|
36
|
+
{typeof closeButton !== 'boolean' ? closeButton : null}
|
|
37
|
+
</PopoverClose>
|
|
38
|
+
)}
|
|
39
|
+
{hasArrow && <PopoverPrimitive.Arrow className="c-popover-arrow" offset={5} />}
|
|
40
|
+
{content}
|
|
41
|
+
</PopoverPrimitive.Content>
|
|
42
|
+
</PopoverPrimitive.Portal>
|
|
43
|
+
</PopoverPrimitive.Root>
|
|
44
|
+
);
|
|
45
|
+
}
|