@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/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 = "start",
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.Content, {
239
- align: alignContent,
240
- sideOffset: 5,
241
- className: "c-dropdown-menu-content",
242
- ...delegated,
243
- children: content
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
- function ActionMenu({ children, tabIndex }) {
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 cva3 } from "class-variance-authority";
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 = cva3(["c-avatar"], {
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 cva4, cx as cx5 } from "class-variance-authority";
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 = cva4("c-dialog", {
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 cva5 } from "class-variance-authority";
3226
+ import { cva as cva6 } from "class-variance-authority";
3207
3227
  import { jsx as jsx64 } from "react/jsx-runtime";
3208
- var buttonStyles2 = cva5(["c-icon-button"], {
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: buttonStyles2({ size, variant, 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 cva6 } from "class-variance-authority";
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 = cva6("c-inline-radio-group", {
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 cva8, cx as cx8 } from "class-variance-authority";
3300
+ import { cva as cva9, cx as cx8 } from "class-variance-authority";
3281
3301
 
3282
3302
  // src/input/input.tsx
3283
- import { cva as cva7 } from "class-variance-authority";
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 = cva7(["c-input"], {
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 = cva8(["c-input-with-label"], {
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 cva9 } from "class-variance-authority";
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 = cva9("c-select-trigger", {
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 cva10 } from "class-variance-authority";
3542
+ import { cva as cva11 } from "class-variance-authority";
3523
3543
  import { jsx as jsx75 } from "react/jsx-runtime";
3524
- var tagStyles = cva10("c-tag", {
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 useRef5, useState as useState10 } from "react";
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 = useRef5(!autoFocus);
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",
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.1.1",
92
- "vitest": "^0.29.1",
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 h-8 w-8 shrink-0 rotate-0 cursor-pointer flex-col items-center justify-center gap-[2px] rounded-md border-none bg-transparent p-1 -outline-offset-1 transition;
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 h-[4px] w-[4px] rounded-full bg-gray;
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 px-5 font-sans text-sm font-medium text-gray;
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 ActionMenuProps = {
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 tabIndex={tabIndex} type="button" className="c-action-menu" aria-label="more options">
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 DropdownMenuRootProps = DropdownMenuPrimitive.MenuContentProps & {
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 = 'start',
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.Content
26
- align={alignContent}
27
- sideOffset={5}
28
- className="c-dropdown-menu-content"
29
- {...delegated}
30
- >
31
- {content}
32
- </DropdownMenuPrimitive.Content>
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
  }
@@ -16,5 +16,5 @@
16
16
  @apply bg-gray-50-900 outline-none;
17
17
  }
18
18
 
19
- @apply first:rounded-tr first:rounded-tl last:rounded-br last:rounded-bl;
19
+ @apply first:rounded-tl first:rounded-tr last:rounded-bl last:rounded-br;
20
20
  }
@@ -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
+ }