From 811b444997dcc1178feaa78971a2912d9160ea58 Mon Sep 17 00:00:00 2001 From: Max Korsunov Date: Fri, 16 Aug 2024 16:39:59 +0200 Subject: [PATCH 1/6] feat(ui): #prax-160: implement DropdownMenu with sub components --- packages/ui/package.json | 1 + .../ui/src/DropdownMenu/checkbox-item.tsx | 26 +++++ packages/ui/src/DropdownMenu/content.tsx | 28 ++++++ .../ui/src/DropdownMenu/index.stories.tsx | 95 +++++++++++++++++++ packages/ui/src/DropdownMenu/index.tsx | 9 ++ packages/ui/src/DropdownMenu/item.tsx | 20 ++++ packages/ui/src/DropdownMenu/radio-group.tsx | 16 ++++ packages/ui/src/DropdownMenu/radio-item.tsx | 25 +++++ packages/ui/src/DropdownMenu/root.tsx | 47 +++++++++ packages/ui/src/DropdownMenu/shared.ts | 60 ++++++++++++ packages/ui/src/DropdownMenu/trigger.tsx | 19 ++++ packages/ui/src/Popover/index.tsx | 34 +------ packages/ui/src/Popover/styles.ts | 30 ++++++ pnpm-lock.yaml | 33 ++++++- 14 files changed, 411 insertions(+), 32 deletions(-) create mode 100644 packages/ui/src/DropdownMenu/checkbox-item.tsx create mode 100644 packages/ui/src/DropdownMenu/content.tsx create mode 100644 packages/ui/src/DropdownMenu/index.stories.tsx create mode 100644 packages/ui/src/DropdownMenu/index.tsx create mode 100644 packages/ui/src/DropdownMenu/item.tsx create mode 100644 packages/ui/src/DropdownMenu/radio-group.tsx create mode 100644 packages/ui/src/DropdownMenu/radio-item.tsx create mode 100644 packages/ui/src/DropdownMenu/root.tsx create mode 100644 packages/ui/src/DropdownMenu/shared.ts create mode 100644 packages/ui/src/DropdownMenu/trigger.tsx create mode 100644 packages/ui/src/Popover/styles.ts diff --git a/packages/ui/package.json b/packages/ui/package.json index 2d4658662e..21684276d4 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -33,6 +33,7 @@ "@radix-ui/react-avatar": "^1.0.4", "@radix-ui/react-checkbox": "^1.0.4", "@radix-ui/react-dialog": "1.0.5", + "@radix-ui/react-dropdown-menu": "^2.1.1", "@radix-ui/react-icons": "^1.3.0", "@radix-ui/react-navigation-menu": "^1.1.4", "@radix-ui/react-popover": "^1.0.7", diff --git a/packages/ui/src/DropdownMenu/checkbox-item.tsx b/packages/ui/src/DropdownMenu/checkbox-item.tsx new file mode 100644 index 0000000000..c0ec082ac7 --- /dev/null +++ b/packages/ui/src/DropdownMenu/checkbox-item.tsx @@ -0,0 +1,26 @@ +import * as RadixDropdownMenu from '@radix-ui/react-dropdown-menu'; +import { ReactNode } from 'react'; +import { Check } from 'lucide-react'; +import { asTransientProps } from '../utils/asTransientProps.ts'; +import { Text } from '../Text'; +import { DropdownMenuItemBase, MenuItem } from './shared.ts'; + +export interface DropdownMenuCheckboxItemProps extends DropdownMenuItemBase { + children?: ReactNode; + checked?: boolean; + onChange?: (value: boolean) => void; +} + +export const CheckboxItem = ({ children, actionType = 'default', disabled, checked, onChange }: DropdownMenuCheckboxItemProps) => { + return ( + + + + + + + {children} + + + ); +}; diff --git a/packages/ui/src/DropdownMenu/content.tsx b/packages/ui/src/DropdownMenu/content.tsx new file mode 100644 index 0000000000..43fcb67753 --- /dev/null +++ b/packages/ui/src/DropdownMenu/content.tsx @@ -0,0 +1,28 @@ +import { ReactNode } from 'react'; +import { DropdownMenuContentProps as RadixDropdownMenuContentProps } from '@radix-ui/react-dropdown-menu'; +import { useTheme } from 'styled-components'; +import * as RadixDropdownMenu from '@radix-ui/react-dropdown-menu'; +import { PopoverContent } from '../Popover/styles.ts'; + +export interface DropdownMenuContentProps { + children?: ReactNode; + side?: RadixDropdownMenuContentProps['side']; + align?: RadixDropdownMenuContentProps['align']; +} + +export const Content = ({ children, side, align }: DropdownMenuContentProps) => { + const theme = useTheme(); + + return ( + + + {children} + + + ); +}; diff --git a/packages/ui/src/DropdownMenu/index.stories.tsx b/packages/ui/src/DropdownMenu/index.stories.tsx new file mode 100644 index 0000000000..acf7bf26dd --- /dev/null +++ b/packages/ui/src/DropdownMenu/index.stories.tsx @@ -0,0 +1,95 @@ +import type { Meta, StoryObj } from '@storybook/react'; + +import { DropdownMenu } from '.'; +import { Button } from '../Button'; +import { ComponentType, useState } from 'react'; +import { Filter } from 'lucide-react'; + +const meta: Meta = { + component: DropdownMenu, + tags: ['autodocs', '!dev'], + argTypes: { + isOpen: { control: false }, + onClose: { control: false }, + }, + subcomponents: { + // Re: type coercion, see + // https://github.com/storybookjs/storybook/issues/23170#issuecomment-2241802787 + 'DropdownMenu.Content': DropdownMenu.Content as ComponentType, + 'DropdownMenu.Trigger': DropdownMenu.Trigger as ComponentType, + 'DropdownMenu.RadioGroup': DropdownMenu.RadioGroup as ComponentType, + 'DropdownMenu.RadioItem': DropdownMenu.RadioItem as ComponentType, + }, +}; +export default meta; + +type Story = StoryObj; + +export const Basic: Story = { + render: function Render() { + const [isOpen, setIsOpen] = useState(false); + + return ( + setIsOpen(false)}> + + + + + + Destructive + Accent + Unshield + Default + Disabled + + + ); + }, +}; + +export const Radio: Story = { + render: function Render() { + const [isOpen, setIsOpen] = useState(false); + const [value, setValue] = useState('1'); + + return ( + setIsOpen(false)}> + + + + + + + Destructive + Accent + Unshield + Default + Disabled + + + + ); + }, +}; + +export const Checkbox: Story = { + render: function Render() { + const [isOpen, setIsOpen] = useState(false); + + const [apple, setApple] = useState(false); + const [banana, setBanana] = useState(false); + + return ( + setIsOpen(false)}> + + + + + + Apple + Banana + + + ); + }, +}; diff --git a/packages/ui/src/DropdownMenu/index.tsx b/packages/ui/src/DropdownMenu/index.tsx new file mode 100644 index 0000000000..877c2bbab3 --- /dev/null +++ b/packages/ui/src/DropdownMenu/index.tsx @@ -0,0 +1,9 @@ +export { DropdownMenu } from './root'; + +export type { DropdownMenuProps } from './root'; +export type { DropdownMenuTriggerProps } from './trigger'; +export type { DropdownMenuContentProps } from './content'; +export type { DropdownMenuRadioGroupProps } from './radio-group'; +export type { DropdownMenuRadioItemProps } from './radio-item'; +export type { DropdownMenuCheckboxItemProps } from './checkbox-item'; +export type { DropdownMenuItemProps } from './item'; diff --git a/packages/ui/src/DropdownMenu/item.tsx b/packages/ui/src/DropdownMenu/item.tsx new file mode 100644 index 0000000000..367c9ccb6f --- /dev/null +++ b/packages/ui/src/DropdownMenu/item.tsx @@ -0,0 +1,20 @@ +import * as RadixDropdownMenu from '@radix-ui/react-dropdown-menu'; +import { ReactNode } from 'react'; +import { asTransientProps } from '../utils/asTransientProps.ts'; +import { Text } from '../Text'; +import { DropdownMenuItemBase, MenuItem } from './shared.ts'; + +export interface DropdownMenuItemProps extends DropdownMenuItemBase { + children?: ReactNode; + onSelect?: (event: Event) => void; +} + +export const Item = ({ children, actionType = 'default', disabled, onSelect }: DropdownMenuItemProps) => { + return ( + + + {children} + + + ); +}; diff --git a/packages/ui/src/DropdownMenu/radio-group.tsx b/packages/ui/src/DropdownMenu/radio-group.tsx new file mode 100644 index 0000000000..95086b18ae --- /dev/null +++ b/packages/ui/src/DropdownMenu/radio-group.tsx @@ -0,0 +1,16 @@ +import { ReactNode } from 'react'; +import * as RadixDropdownMenu from '@radix-ui/react-dropdown-menu'; + +export interface DropdownMenuRadioGroupProps { + children?: ReactNode; + value?: string; + onValueChange?: (value: string) => void; +} + +export const RadioGroup = ({ children, value, onValueChange }: DropdownMenuRadioGroupProps) => { + return ( + + {children} + + ); +}; diff --git a/packages/ui/src/DropdownMenu/radio-item.tsx b/packages/ui/src/DropdownMenu/radio-item.tsx new file mode 100644 index 0000000000..13e9913cf3 --- /dev/null +++ b/packages/ui/src/DropdownMenu/radio-item.tsx @@ -0,0 +1,25 @@ +import * as RadixDropdownMenu from '@radix-ui/react-dropdown-menu'; +import { ReactNode } from 'react'; +import { Check } from 'lucide-react'; +import { asTransientProps } from '../utils/asTransientProps.ts'; +import { Text } from '../Text'; +import { DropdownMenuItemBase, MenuItem } from './shared.ts'; + +export interface DropdownMenuRadioItemProps extends DropdownMenuItemBase { + children?: ReactNode; + value: string; +} + +export const RadioItem = ({ children, value, actionType = 'default', disabled }: DropdownMenuRadioItemProps) => { + return ( + + + + + + + {children} + + + ); +}; diff --git a/packages/ui/src/DropdownMenu/root.tsx b/packages/ui/src/DropdownMenu/root.tsx new file mode 100644 index 0000000000..32c506787e --- /dev/null +++ b/packages/ui/src/DropdownMenu/root.tsx @@ -0,0 +1,47 @@ +import { ReactNode } from 'react'; +import * as RadixDropdownMenu from '@radix-ui/react-dropdown-menu'; +import { Trigger } from './trigger.tsx'; +import { Content } from './content.tsx'; +import { RadioGroup } from './radio-group.tsx'; +import { RadioItem } from './radio-item.tsx'; +import { CheckboxItem } from './checkbox-item.tsx'; +import { Item } from './item.tsx'; + +interface ControlledDropdownMenuProps { + /** + * Whether the popover is currently open. If left `undefined`, this will be + * treated as an uncontrolled popover — that is, it will open and close based + * on user interactions rather than on state variables. + */ + isOpen: boolean; + /** + * Callback for when the user closes the popover. Should update the state + * variable being passed in via `isOpen`. If left `undefined`, users will not + * be able to close it -- that is, it will only be able to be closed programmatically + */ + onClose?: VoidFunction; +} + +interface UncontrolledDropdownMenuProps { + isOpen?: undefined; + onClose?: undefined; +} + +export type DropdownMenuProps = { + children?: ReactNode; +} & (ControlledDropdownMenuProps | UncontrolledDropdownMenuProps); + +export const DropdownMenu = ({ children, onClose, isOpen }: DropdownMenuProps) => { + return ( + onClose && !value && onClose()}> + {children} + + ); +}; + +DropdownMenu.Trigger = Trigger; +DropdownMenu.Content = Content; +DropdownMenu.RadioGroup = RadioGroup; +DropdownMenu.RadioItem = RadioItem; +DropdownMenu.CheckboxItem = CheckboxItem; +DropdownMenu.Item = Item; diff --git a/packages/ui/src/DropdownMenu/shared.ts b/packages/ui/src/DropdownMenu/shared.ts new file mode 100644 index 0000000000..93c622376b --- /dev/null +++ b/packages/ui/src/DropdownMenu/shared.ts @@ -0,0 +1,60 @@ +import styled, { DefaultTheme } from 'styled-components'; +import { ActionType } from '../utils/ActionType.ts'; + +export interface DropdownMenuItemBase { + actionType?: ActionType; + disabled?: boolean; +} + +export interface StyledItemProps { + $actionType?: ActionType; + $disabled?: boolean; +} + +const getColorByActionType = (theme: DefaultTheme, actionType: ActionType = 'default'): string => { + if (actionType === 'destructive') { + return theme.color.destructive.light; + } + return theme.color.text.primary; +}; + +const getOutlineColorByActionType = (theme: DefaultTheme, actionType: ActionType = 'default'): string => { + const map: Record = { + default: 'neutralFocusOutline', + accent: 'primaryFocusOutline', + unshield: 'unshieldFocusOutline', + destructive: 'destructiveFocusOutline', + }; + return theme.color.action[map[actionType]]; +}; + + +export const MenuItem = styled.div` + display: flex; + align-items: center; + gap: ${props => props.theme.spacing(1)}; + height: ${props => props.theme.spacing(8)}; + padding: ${props => props.theme.spacing(1)} ${props => props.theme.spacing(2)}; + + border: none; + border-radius: ${props => props.theme.borderRadius.sm}; + + color: ${props => getColorByActionType(props.theme, props.$actionType)}; + cursor: pointer; + + background-color: transparent; + transition: background-color 0.15s; + + &:focus:not(:disabled) { + background-color: ${props => props.theme.color.action.hoverOverlay}; + outline: 2px solid ${props => getOutlineColorByActionType(props.theme, props.$actionType)}; + } + + &[aria-disabled="true"] { + color: ${props => props.theme.color.text.muted}; + } + + &[aria-checked="false"], &[role="menuitem"] { + padding-left: ${props => props.theme.spacing(9)}; + } +` diff --git a/packages/ui/src/DropdownMenu/trigger.tsx b/packages/ui/src/DropdownMenu/trigger.tsx new file mode 100644 index 0000000000..7f847352c8 --- /dev/null +++ b/packages/ui/src/DropdownMenu/trigger.tsx @@ -0,0 +1,19 @@ +import { ReactNode } from 'react'; +import * as RadixDropdownMenu from '@radix-ui/react-dropdown-menu'; + +export interface DropdownMenuTriggerProps { + children: ReactNode; + /** + * Change the default rendered element for the one passed as a child, merging + * their props and behavior. + * + * Uses Radix UI's `asChild` prop under the hood. + * + * @see https://www.radix-ui.com/primitives/docs/guides/composition + */ + asChild?: boolean; +} + +export const Trigger = ({ children, asChild }: DropdownMenuTriggerProps) => ( + {children} +); diff --git a/packages/ui/src/Popover/index.tsx b/packages/ui/src/Popover/index.tsx index ebe0c37f82..4fdd67f639 100644 --- a/packages/ui/src/Popover/index.tsx +++ b/packages/ui/src/Popover/index.tsx @@ -1,36 +1,8 @@ import { ReactNode } from 'react'; import * as RadixPopover from '@radix-ui/react-popover'; import type { PopoverContentProps as RadixPopoverContentProps } from '@radix-ui/react-popover'; -import styled, { keyframes, useTheme } from 'styled-components'; - -const scaleIn = keyframes` - from { - opacity: 0; - transform: scale(0); - } - to { - opacity: 1; - transform: scale(1); - } -`; - -const RadixContent = styled.div` - display: flex; - flex-direction: column; - gap: ${props => props.theme.spacing(4)}; - - width: 240px; - max-width: 320px; - padding: ${props => props.theme.spacing(3)} ${props => props.theme.spacing(2)}; - - background: ${props => props.theme.color.other.dialogBackground}; - border: 1px solid ${props => props.theme.color.other.tonalStroke}; - border-radius: ${props => props.theme.borderRadius.sm}; - backdrop-filter: blur(${props => props.theme.blur.lg}); - - transform-origin: var(--radix-tooltip-content-transform-origin); - animation: ${scaleIn} 0.15s ease-out; -`; +import { useTheme } from 'styled-components'; +import { PopoverContent } from './styles.ts'; interface ControlledPopoverProps { /** @@ -161,7 +133,7 @@ const Content = ({ children, side, align }: PopoverContentProps) => { align={align} asChild > - {children} + {children} ); diff --git a/packages/ui/src/Popover/styles.ts b/packages/ui/src/Popover/styles.ts new file mode 100644 index 0000000000..7c4490ec34 --- /dev/null +++ b/packages/ui/src/Popover/styles.ts @@ -0,0 +1,30 @@ +import styled, { keyframes } from 'styled-components'; + +const scaleIn = keyframes` + from { + opacity: 0; + transform: scale(0); + } + to { + opacity: 1; + transform: scale(1); + } +`; + +export const PopoverContent = styled.div` + display: flex; + flex-direction: column; + gap: ${props => props.theme.spacing(4)}; + + width: 240px; + max-width: 320px; + padding: ${props => props.theme.spacing(3)} ${props => props.theme.spacing(2)}; + + background: ${props => props.theme.color.other.dialogBackground}; + border: 1px solid ${props => props.theme.color.other.tonalStroke}; + border-radius: ${props => props.theme.borderRadius.sm}; + backdrop-filter: blur(${props => props.theme.blur.lg}); + + transform-origin: var(--radix-tooltip-content-transform-origin); + animation: ${scaleIn} 0.15s ease-out; +`; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2956ba5b32..f17ffad939 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -636,6 +636,9 @@ importers: '@radix-ui/react-dialog': specifier: 1.0.5 version: 1.0.5(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-dropdown-menu': + specifier: ^2.1.1 + version: 2.1.1(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-icons': specifier: ^1.3.0 version: 1.3.0(react@18.3.1) @@ -734,7 +737,7 @@ importers: version: 3.0.1 postcss: specifier: ^8.4.38 - version: 8.4.39 + version: 7.0.39 react: specifier: ^18.3.1 version: 18.3.1 @@ -3149,6 +3152,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-dropdown-menu@2.1.1': + resolution: {integrity: sha512-y8E+x9fBq9qvteD2Zwa4397pUVhYsh9iq44b5RD5qu1GMJWBCBuVg1hMyItbc6+zH00TxGRqd9Iot4wzf3OoBQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-focus-guards@1.0.0': resolution: {integrity: sha512-UagjDk4ijOAnGu4WMUPj9ahi7/zJJqNZ9ZAiGPp7waUWJO0O1aWXi/udPphI0IUjvrhBsZJGSN66dR2dsueLWQ==} peerDependencies: @@ -14308,6 +14324,21 @@ snapshots: '@types/react': 18.3.3 '@types/react-dom': 18.3.0 + '@radix-ui/react-dropdown-menu@2.1.1(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.0 + '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.3)(react@18.3.1) + '@radix-ui/react-context': 1.1.0(@types/react@18.3.3)(react@18.3.1) + '@radix-ui/react-id': 1.1.0(@types/react@18.3.3)(react@18.3.1) + '@radix-ui/react-menu': 2.1.1(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.3.3)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.3 + '@types/react-dom': 18.3.0 + '@radix-ui/react-focus-guards@1.0.0(react@18.3.1)': dependencies: '@babel/runtime': 7.24.7 From d989e475e6af161d73f0ae96ee24bd7d05e0c229 Mon Sep 17 00:00:00 2001 From: Max Korsunov Date: Fri, 16 Aug 2024 17:39:23 +0200 Subject: [PATCH 2/6] feat(ui): #prax-160: improve dropdown components, add docs --- packages/ui/src/Button/index.tsx | 2 + packages/ui/src/Dialog/index.stories.tsx | 10 +-- .../ui/src/DropdownMenu/checkbox-item.tsx | 26 ++++++-- packages/ui/src/DropdownMenu/content.tsx | 17 +++-- .../ui/src/DropdownMenu/index.stories.tsx | 64 +++++++++++++------ packages/ui/src/DropdownMenu/item.tsx | 15 +++-- packages/ui/src/DropdownMenu/radio-group.tsx | 10 +-- packages/ui/src/DropdownMenu/radio-item.tsx | 20 ++++-- packages/ui/src/DropdownMenu/root.tsx | 49 +++++++++++++- packages/ui/src/DropdownMenu/shared.ts | 25 ++++---- packages/ui/src/DropdownMenu/trigger.tsx | 17 ++--- packages/ui/src/Popover/index.tsx | 19 ++---- .../{Popover/styles.ts => utils/popover.ts} | 0 13 files changed, 178 insertions(+), 96 deletions(-) rename packages/ui/src/{Popover/styles.ts => utils/popover.ts} (100%) diff --git a/packages/ui/src/Button/index.tsx b/packages/ui/src/Button/index.tsx index d95c84d1fd..1851bf08c5 100644 --- a/packages/ui/src/Button/index.tsx +++ b/packages/ui/src/Button/index.tsx @@ -179,6 +179,7 @@ export const Button = forwardRef( actionType = 'default', type = 'button', priority = 'primary', + ...props }, ref, ) => { @@ -200,6 +201,7 @@ export const Button = forwardRef( ? theme.borderRadius.sm : theme.borderRadius.full } + {...props} > {IconComponent && ( diff --git a/packages/ui/src/Dialog/index.stories.tsx b/packages/ui/src/Dialog/index.stories.tsx index 5f470d3214..370d4bb00b 100644 --- a/packages/ui/src/Dialog/index.stories.tsx +++ b/packages/ui/src/Dialog/index.stories.tsx @@ -2,7 +2,7 @@ import type { Meta, StoryObj } from '@storybook/react'; import { Dialog } from '.'; import { Button } from '../Button'; -import { ComponentType, useState } from 'react'; +import { ComponentType } from 'react'; import { Text } from '../Text'; import styled from 'styled-components'; import { Ban, Handshake, ThumbsUp } from 'lucide-react'; @@ -31,11 +31,11 @@ type Story = StoryObj; export const Basic: Story = { render: function Render() { - const [isOpen, setIsOpen] = useState(false); - return ( - setIsOpen(false)}> - + + + + void; } -export const CheckboxItem = ({ children, actionType = 'default', disabled, checked, onChange }: DropdownMenuCheckboxItemProps) => { +export const CheckboxItem = ({ + children, + actionType = 'default', + disabled, + checked, + onChange, +}: DropdownMenuCheckboxItemProps) => { return ( - + - + - + {children} - + ); }; diff --git a/packages/ui/src/DropdownMenu/content.tsx b/packages/ui/src/DropdownMenu/content.tsx index 43fcb67753..fb011c7741 100644 --- a/packages/ui/src/DropdownMenu/content.tsx +++ b/packages/ui/src/DropdownMenu/content.tsx @@ -1,8 +1,11 @@ import { ReactNode } from 'react'; -import { DropdownMenuContentProps as RadixDropdownMenuContentProps } from '@radix-ui/react-dropdown-menu'; import { useTheme } from 'styled-components'; -import * as RadixDropdownMenu from '@radix-ui/react-dropdown-menu'; -import { PopoverContent } from '../Popover/styles.ts'; +import { + Content as RadixDropdownMenuContent, + Portal as RadixDropdownMenuPortal, + DropdownMenuContentProps as RadixDropdownMenuContentProps, +} from '@radix-ui/react-dropdown-menu'; +import { PopoverContent } from '../utils/popover.ts'; export interface DropdownMenuContentProps { children?: ReactNode; @@ -14,15 +17,15 @@ export const Content = ({ children, side, align }: DropdownMenuContentProps) => const theme = useTheme(); return ( - - + {children} - - + + ); }; diff --git a/packages/ui/src/DropdownMenu/index.stories.tsx b/packages/ui/src/DropdownMenu/index.stories.tsx index acf7bf26dd..1523874f9f 100644 --- a/packages/ui/src/DropdownMenu/index.stories.tsx +++ b/packages/ui/src/DropdownMenu/index.stories.tsx @@ -9,8 +9,8 @@ const meta: Meta = { component: DropdownMenu, tags: ['autodocs', '!dev'], argTypes: { - isOpen: { control: false }, - onClose: { control: false }, + // isOpen: { control: false }, + // onClose: { control: false }, }, subcomponents: { // Re: type coercion, see @@ -19,6 +19,8 @@ const meta: Meta = { 'DropdownMenu.Trigger': DropdownMenu.Trigger as ComponentType, 'DropdownMenu.RadioGroup': DropdownMenu.RadioGroup as ComponentType, 'DropdownMenu.RadioItem': DropdownMenu.RadioItem as ComponentType, + 'DropdownMenu.CheckboxItem': DropdownMenu.CheckboxItem as ComponentType, + 'DropdownMenu.Item': DropdownMenu.Item as ComponentType, }, }; export default meta; @@ -27,20 +29,22 @@ type Story = StoryObj; export const Basic: Story = { render: function Render() { - const [isOpen, setIsOpen] = useState(false); - return ( - setIsOpen(false)}> - - + + + Destructive - Accent + Accent Unshield Default - Disabled + + Disabled + ); @@ -54,17 +58,29 @@ export const Radio: Story = { return ( setIsOpen(false)}> - - + + - - Destructive - Accent - Unshield - Default - Disabled + + + Destructive + + + Accent + + + Unshield + + + Default + + + Disabled + @@ -81,13 +97,19 @@ export const Checkbox: Story = { return ( setIsOpen(false)}> - - + + - Apple - Banana + + Apple + + + Banana + ); diff --git a/packages/ui/src/DropdownMenu/item.tsx b/packages/ui/src/DropdownMenu/item.tsx index 367c9ccb6f..915e012bc6 100644 --- a/packages/ui/src/DropdownMenu/item.tsx +++ b/packages/ui/src/DropdownMenu/item.tsx @@ -1,5 +1,5 @@ -import * as RadixDropdownMenu from '@radix-ui/react-dropdown-menu'; -import { ReactNode } from 'react'; +import type { ReactNode } from 'react'; +import { Item as RadixDropdownMenuItem } from '@radix-ui/react-dropdown-menu'; import { asTransientProps } from '../utils/asTransientProps.ts'; import { Text } from '../Text'; import { DropdownMenuItemBase, MenuItem } from './shared.ts'; @@ -9,12 +9,17 @@ export interface DropdownMenuItemProps extends DropdownMenuItemBase { onSelect?: (event: Event) => void; } -export const Item = ({ children, actionType = 'default', disabled, onSelect }: DropdownMenuItemProps) => { +export const Item = ({ + children, + actionType = 'default', + disabled, + onSelect, +}: DropdownMenuItemProps) => { return ( - + {children} - + ); }; diff --git a/packages/ui/src/DropdownMenu/radio-group.tsx b/packages/ui/src/DropdownMenu/radio-group.tsx index 95086b18ae..c15a3caa83 100644 --- a/packages/ui/src/DropdownMenu/radio-group.tsx +++ b/packages/ui/src/DropdownMenu/radio-group.tsx @@ -1,16 +1,16 @@ import { ReactNode } from 'react'; -import * as RadixDropdownMenu from '@radix-ui/react-dropdown-menu'; +import { RadioGroup as RadixDropdownMenuRadioGroup } from '@radix-ui/react-dropdown-menu'; export interface DropdownMenuRadioGroupProps { children?: ReactNode; value?: string; - onValueChange?: (value: string) => void; + onChange?: (value: string) => void; } -export const RadioGroup = ({ children, value, onValueChange }: DropdownMenuRadioGroupProps) => { +export const RadioGroup = ({ children, value, onChange }: DropdownMenuRadioGroupProps) => { return ( - + {children} - + ); }; diff --git a/packages/ui/src/DropdownMenu/radio-item.tsx b/packages/ui/src/DropdownMenu/radio-item.tsx index 13e9913cf3..6708900231 100644 --- a/packages/ui/src/DropdownMenu/radio-item.tsx +++ b/packages/ui/src/DropdownMenu/radio-item.tsx @@ -1,4 +1,7 @@ -import * as RadixDropdownMenu from '@radix-ui/react-dropdown-menu'; +import { + RadioItem as RadixDropdownMenuRadioItem, + ItemIndicator as RadixDropdownMenuItemIndicator, +} from '@radix-ui/react-dropdown-menu'; import { ReactNode } from 'react'; import { Check } from 'lucide-react'; import { asTransientProps } from '../utils/asTransientProps.ts'; @@ -10,16 +13,21 @@ export interface DropdownMenuRadioItemProps extends DropdownMenuItemBase { value: string; } -export const RadioItem = ({ children, value, actionType = 'default', disabled }: DropdownMenuRadioItemProps) => { +export const RadioItem = ({ + children, + value, + actionType = 'default', + disabled, +}: DropdownMenuRadioItemProps) => { return ( - + - + - + {children} - + ); }; diff --git a/packages/ui/src/DropdownMenu/root.tsx b/packages/ui/src/DropdownMenu/root.tsx index 32c506787e..1c2f12c3f4 100644 --- a/packages/ui/src/DropdownMenu/root.tsx +++ b/packages/ui/src/DropdownMenu/root.tsx @@ -1,5 +1,5 @@ import { ReactNode } from 'react'; -import * as RadixDropdownMenu from '@radix-ui/react-dropdown-menu'; +import { Root as RadixDropdownMenuRoot } from '@radix-ui/react-dropdown-menu'; import { Trigger } from './trigger.tsx'; import { Content } from './content.tsx'; import { RadioGroup } from './radio-group.tsx'; @@ -31,11 +31,54 @@ export type DropdownMenuProps = { children?: ReactNode; } & (ControlledDropdownMenuProps | UncontrolledDropdownMenuProps); +/** + * A dropdown menu with a set of subcomponents for composing complex menus. + * + * `` can be controlled or uncontrolled. If `isOpen` is not provided + * but `` is present, it will open itself. + * + * You can nest multiple components inside the ``: + * - `` as an action button in the dropdown + * - `` with `` as a group of radio buttons + * - `` as a checkbox + * + * Example: + * + * ```tsx + * const [radioValue, setRadioValue] = useState('1'); + * const [apple, setApple] = useState(false); + * const [banana, setBanana] = useState(false); + * + * + * + * + * + * + * + * Default item + * Destructive item + * + * + * Default + * Disabled + * + * + * Apple + * Banana + * + * + * ``` + */ export const DropdownMenu = ({ children, onClose, isOpen }: DropdownMenuProps) => { return ( - onClose && !value && onClose()}> + !value && onClose() : undefined} + > {children} - + ); }; diff --git a/packages/ui/src/DropdownMenu/shared.ts b/packages/ui/src/DropdownMenu/shared.ts index 93c622376b..09eff27ecf 100644 --- a/packages/ui/src/DropdownMenu/shared.ts +++ b/packages/ui/src/DropdownMenu/shared.ts @@ -18,7 +18,10 @@ const getColorByActionType = (theme: DefaultTheme, actionType: ActionType = 'def return theme.color.text.primary; }; -const getOutlineColorByActionType = (theme: DefaultTheme, actionType: ActionType = 'default'): string => { +const getOutlineColorByActionType = ( + theme: DefaultTheme, + actionType: ActionType = 'default', +): string => { const map: Record = { default: 'neutralFocusOutline', accent: 'primaryFocusOutline', @@ -28,33 +31,33 @@ const getOutlineColorByActionType = (theme: DefaultTheme, actionType: ActionType return theme.color.action[map[actionType]]; }; - export const MenuItem = styled.div` display: flex; align-items: center; gap: ${props => props.theme.spacing(1)}; height: ${props => props.theme.spacing(8)}; padding: ${props => props.theme.spacing(1)} ${props => props.theme.spacing(2)}; - + border: none; border-radius: ${props => props.theme.borderRadius.sm}; - + color: ${props => getColorByActionType(props.theme, props.$actionType)}; cursor: pointer; - + background-color: transparent; transition: background-color 0.15s; - + &:focus:not(:disabled) { background-color: ${props => props.theme.color.action.hoverOverlay}; outline: 2px solid ${props => getOutlineColorByActionType(props.theme, props.$actionType)}; } - - &[aria-disabled="true"] { + + &[aria-disabled='true'] { color: ${props => props.theme.color.text.muted}; } - - &[aria-checked="false"], &[role="menuitem"] { + + &[aria-checked='false'], + &[role='menuitem'] { padding-left: ${props => props.theme.spacing(9)}; } -` +`; diff --git a/packages/ui/src/DropdownMenu/trigger.tsx b/packages/ui/src/DropdownMenu/trigger.tsx index 7f847352c8..b056446e87 100644 --- a/packages/ui/src/DropdownMenu/trigger.tsx +++ b/packages/ui/src/DropdownMenu/trigger.tsx @@ -1,19 +1,10 @@ -import { ReactNode } from 'react'; -import * as RadixDropdownMenu from '@radix-ui/react-dropdown-menu'; +import type { ReactNode } from 'react'; +import { Trigger as RadixDropdownMenuTrigger } from '@radix-ui/react-dropdown-menu'; export interface DropdownMenuTriggerProps { children: ReactNode; - /** - * Change the default rendered element for the one passed as a child, merging - * their props and behavior. - * - * Uses Radix UI's `asChild` prop under the hood. - * - * @see https://www.radix-ui.com/primitives/docs/guides/composition - */ - asChild?: boolean; } -export const Trigger = ({ children, asChild }: DropdownMenuTriggerProps) => ( - {children} +export const Trigger = ({ children }: DropdownMenuTriggerProps) => ( + {children} ); diff --git a/packages/ui/src/Popover/index.tsx b/packages/ui/src/Popover/index.tsx index 4fdd67f639..e7c9f3c7c2 100644 --- a/packages/ui/src/Popover/index.tsx +++ b/packages/ui/src/Popover/index.tsx @@ -2,7 +2,7 @@ import { ReactNode } from 'react'; import * as RadixPopover from '@radix-ui/react-popover'; import type { PopoverContentProps as RadixPopoverContentProps } from '@radix-ui/react-popover'; import { useTheme } from 'styled-components'; -import { PopoverContent } from './styles.ts'; +import { PopoverContent } from '../utils/popover.ts'; interface ControlledPopoverProps { /** @@ -38,7 +38,7 @@ export type PopoverProps = { * * ```tsx * - * + * * * * @@ -76,7 +76,7 @@ export type PopoverProps = { * ```tsx * - * + * * * * @@ -94,19 +94,10 @@ export const Popover = ({ children, onClose, isOpen }: PopoverProps) => { export interface PopoverTriggerProps { children: ReactNode; - /** - * Change the default rendered element for the one passed as a child, merging - * their props and behavior. - * - * Uses Radix UI's `asChild` prop under the hood. - * - * @see https://www.radix-ui.com/primitives/docs/guides/composition - */ - asChild?: boolean; } -const Trigger = ({ children, asChild }: PopoverTriggerProps) => ( - {children} +const Trigger = ({ children }: PopoverTriggerProps) => ( + {children} ); Popover.Trigger = Trigger; diff --git a/packages/ui/src/Popover/styles.ts b/packages/ui/src/utils/popover.ts similarity index 100% rename from packages/ui/src/Popover/styles.ts rename to packages/ui/src/utils/popover.ts From cd5054e347052168d3e3e42871ed8c634b3da3f2 Mon Sep 17 00:00:00 2001 From: Max Korsunov Date: Fri, 16 Aug 2024 17:56:15 +0200 Subject: [PATCH 3/6] feat(ui): #prax-160: add tests --- .../ui/src/DropdownMenu/index.stories.tsx | 4 +- packages/ui/src/DropdownMenu/index.test.tsx | 91 +++++++++++++++++++ packages/ui/src/Popover/index.stories.tsx | 2 +- packages/ui/src/Popover/index.test.tsx | 8 +- packages/ui/src/utils/popover.ts | 1 - 5 files changed, 100 insertions(+), 6 deletions(-) create mode 100644 packages/ui/src/DropdownMenu/index.test.tsx diff --git a/packages/ui/src/DropdownMenu/index.stories.tsx b/packages/ui/src/DropdownMenu/index.stories.tsx index 1523874f9f..6250dbb2f5 100644 --- a/packages/ui/src/DropdownMenu/index.stories.tsx +++ b/packages/ui/src/DropdownMenu/index.stories.tsx @@ -9,8 +9,8 @@ const meta: Meta = { component: DropdownMenu, tags: ['autodocs', '!dev'], argTypes: { - // isOpen: { control: false }, - // onClose: { control: false }, + isOpen: { control: false }, + onClose: { control: false }, }, subcomponents: { // Re: type coercion, see diff --git a/packages/ui/src/DropdownMenu/index.test.tsx b/packages/ui/src/DropdownMenu/index.test.tsx new file mode 100644 index 0000000000..550e3575fa --- /dev/null +++ b/packages/ui/src/DropdownMenu/index.test.tsx @@ -0,0 +1,91 @@ +import { fireEvent, render } from '@testing-library/react'; +import { describe, expect, it, vi } from 'vitest'; +import { DropdownMenu } from '.'; +import { PenumbraUIProvider } from '../PenumbraUIProvider'; + +describe('', () => { + it('opens initially if `isOpen` is passed', () => { + const { queryByText } = render( + + + + + Content + , + { wrapper: PenumbraUIProvider }, + ); + + expect(queryByText('Content')).toBeTruthy(); + }); + + it('correctly selects radio item from the radio group', () => { + const onChange = vi.fn(); + + const { getByText } = render( + + + + + + + + Item 1 + + + Item 2 + + + + , + { wrapper: PenumbraUIProvider }, + ); + + fireEvent.click(getByText('Item 2')); + + expect(onChange).toHaveBeenCalledWith('2'); + }); + + it('correctly selects a checkbox item from the dropdown menu', () => { + const onChange = vi.fn(); + + const { getByText } = render( + + + + + + + Item + + + , + { wrapper: PenumbraUIProvider }, + ); + + fireEvent.click(getByText('Item')); + + expect(onChange).toHaveBeenCalled(); + }); + + it('correctly selects a menu item from the dropdown menu', () => { + const onChange = vi.fn(); + + const { getByText } = render( + + + + + + + Item + + + , + { wrapper: PenumbraUIProvider }, + ); + + fireEvent.click(getByText('Item')); + + expect(onChange).toHaveBeenCalled(); + }); +}); diff --git a/packages/ui/src/Popover/index.stories.tsx b/packages/ui/src/Popover/index.stories.tsx index 59b09bb2a6..5d06fb031d 100644 --- a/packages/ui/src/Popover/index.stories.tsx +++ b/packages/ui/src/Popover/index.stories.tsx @@ -39,7 +39,7 @@ export const Basic: Story = { return ( setIsOpen(false)}> - + diff --git a/packages/ui/src/Popover/index.test.tsx b/packages/ui/src/Popover/index.test.tsx index e844650635..3b71000ad1 100644 --- a/packages/ui/src/Popover/index.test.tsx +++ b/packages/ui/src/Popover/index.test.tsx @@ -7,7 +7,9 @@ describe('', () => { it('opens when trigger is clicked', () => { const { getByText, queryByText } = render( - Trigger + + + Content , { wrapper: PenumbraUIProvider }, @@ -21,7 +23,9 @@ describe('', () => { it('opens initially if `isOpen` is passed', () => { const { queryByText } = render( - Trigger + + + Content , { wrapper: PenumbraUIProvider }, diff --git a/packages/ui/src/utils/popover.ts b/packages/ui/src/utils/popover.ts index 7c4490ec34..b71bba0907 100644 --- a/packages/ui/src/utils/popover.ts +++ b/packages/ui/src/utils/popover.ts @@ -14,7 +14,6 @@ const scaleIn = keyframes` export const PopoverContent = styled.div` display: flex; flex-direction: column; - gap: ${props => props.theme.spacing(4)}; width: 240px; max-width: 320px; From ce15ee4d72ea7a634b66f74b318dab2fcb89ca95 Mon Sep 17 00:00:00 2001 From: Max Korsunov Date: Fri, 16 Aug 2024 17:57:20 +0200 Subject: [PATCH 4/6] chore: add changeset --- .changeset/honest-icons-add.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/honest-icons-add.md diff --git a/.changeset/honest-icons-add.md b/.changeset/honest-icons-add.md new file mode 100644 index 0000000000..de6778fb24 --- /dev/null +++ b/.changeset/honest-icons-add.md @@ -0,0 +1,5 @@ +--- +'@repo/ui': minor +--- + +Add DropdownMenu UI component From 217f1b2d6668a1131faff835cf1293df9c9c7335 Mon Sep 17 00:00:00 2001 From: Max Korsunov Date: Fri, 16 Aug 2024 17:57:34 +0200 Subject: [PATCH 5/6] fix: prettier --- packages/ui/src/DropdownMenu/index.test.tsx | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/packages/ui/src/DropdownMenu/index.test.tsx b/packages/ui/src/DropdownMenu/index.test.tsx index 550e3575fa..533c5535bc 100644 --- a/packages/ui/src/DropdownMenu/index.test.tsx +++ b/packages/ui/src/DropdownMenu/index.test.tsx @@ -28,12 +28,8 @@ describe('', () => { - - Item 1 - - - Item 2 - + Item 1 + Item 2 , @@ -54,9 +50,7 @@ describe('', () => { - - Item - + Item , { wrapper: PenumbraUIProvider }, @@ -76,9 +70,7 @@ describe('', () => { - - Item - + Item , { wrapper: PenumbraUIProvider }, From 656479eef405e916907ddb551e6704ff55c3020b Mon Sep 17 00:00:00 2001 From: Max Korsunov Date: Mon, 19 Aug 2024 10:52:05 +0200 Subject: [PATCH 6/6] fix(ui): after review --- packages/ui/src/Button/index.tsx | 4 +++- .../{checkbox-item.tsx => CheckboxItem.tsx} | 0 .../DropdownMenu/{content.tsx => Content.tsx} | 0 .../ui/src/DropdownMenu/{item.tsx => Item.tsx} | 0 .../{radio-group.tsx => RadioGroup.tsx} | 0 .../{radio-item.tsx => RadioItem.tsx} | 0 .../ui/src/DropdownMenu/{root.tsx => Root.tsx} | 12 ++++++------ .../DropdownMenu/{trigger.tsx => Trigger.tsx} | 0 packages/ui/src/DropdownMenu/index.tsx | 16 ++++++++-------- packages/ui/src/utils/popover.ts | 2 +- 10 files changed, 18 insertions(+), 16 deletions(-) rename packages/ui/src/DropdownMenu/{checkbox-item.tsx => CheckboxItem.tsx} (100%) rename packages/ui/src/DropdownMenu/{content.tsx => Content.tsx} (100%) rename packages/ui/src/DropdownMenu/{item.tsx => Item.tsx} (100%) rename packages/ui/src/DropdownMenu/{radio-group.tsx => RadioGroup.tsx} (100%) rename packages/ui/src/DropdownMenu/{radio-item.tsx => RadioItem.tsx} (100%) rename packages/ui/src/DropdownMenu/{root.tsx => Root.tsx} (91%) rename packages/ui/src/DropdownMenu/{trigger.tsx => Trigger.tsx} (100%) diff --git a/packages/ui/src/Button/index.tsx b/packages/ui/src/Button/index.tsx index 1851bf08c5..a211130375 100644 --- a/packages/ui/src/Button/index.tsx +++ b/packages/ui/src/Button/index.tsx @@ -179,6 +179,8 @@ export const Button = forwardRef( actionType = 'default', type = 'button', priority = 'primary', + // needed for the Radix's `asChild` prop to work correctly + // https://www.radix-ui.com/primitives/docs/guides/composition#composing-with-your-own-react-components ...props }, ref, @@ -187,6 +189,7 @@ export const Button = forwardRef( return ( ( ? theme.borderRadius.sm : theme.borderRadius.full } - {...props} > {IconComponent && ( diff --git a/packages/ui/src/DropdownMenu/checkbox-item.tsx b/packages/ui/src/DropdownMenu/CheckboxItem.tsx similarity index 100% rename from packages/ui/src/DropdownMenu/checkbox-item.tsx rename to packages/ui/src/DropdownMenu/CheckboxItem.tsx diff --git a/packages/ui/src/DropdownMenu/content.tsx b/packages/ui/src/DropdownMenu/Content.tsx similarity index 100% rename from packages/ui/src/DropdownMenu/content.tsx rename to packages/ui/src/DropdownMenu/Content.tsx diff --git a/packages/ui/src/DropdownMenu/item.tsx b/packages/ui/src/DropdownMenu/Item.tsx similarity index 100% rename from packages/ui/src/DropdownMenu/item.tsx rename to packages/ui/src/DropdownMenu/Item.tsx diff --git a/packages/ui/src/DropdownMenu/radio-group.tsx b/packages/ui/src/DropdownMenu/RadioGroup.tsx similarity index 100% rename from packages/ui/src/DropdownMenu/radio-group.tsx rename to packages/ui/src/DropdownMenu/RadioGroup.tsx diff --git a/packages/ui/src/DropdownMenu/radio-item.tsx b/packages/ui/src/DropdownMenu/RadioItem.tsx similarity index 100% rename from packages/ui/src/DropdownMenu/radio-item.tsx rename to packages/ui/src/DropdownMenu/RadioItem.tsx diff --git a/packages/ui/src/DropdownMenu/root.tsx b/packages/ui/src/DropdownMenu/Root.tsx similarity index 91% rename from packages/ui/src/DropdownMenu/root.tsx rename to packages/ui/src/DropdownMenu/Root.tsx index 1c2f12c3f4..d27a292fb7 100644 --- a/packages/ui/src/DropdownMenu/root.tsx +++ b/packages/ui/src/DropdownMenu/Root.tsx @@ -1,11 +1,11 @@ import { ReactNode } from 'react'; import { Root as RadixDropdownMenuRoot } from '@radix-ui/react-dropdown-menu'; -import { Trigger } from './trigger.tsx'; -import { Content } from './content.tsx'; -import { RadioGroup } from './radio-group.tsx'; -import { RadioItem } from './radio-item.tsx'; -import { CheckboxItem } from './checkbox-item.tsx'; -import { Item } from './item.tsx'; +import { Trigger } from './Trigger.tsx'; +import { Content } from './Content.tsx'; +import { RadioGroup } from './RadioGroup.tsx'; +import { RadioItem } from './RadioItem.tsx'; +import { CheckboxItem } from './CheckboxItem.tsx'; +import { Item } from './Item.tsx'; interface ControlledDropdownMenuProps { /** diff --git a/packages/ui/src/DropdownMenu/trigger.tsx b/packages/ui/src/DropdownMenu/Trigger.tsx similarity index 100% rename from packages/ui/src/DropdownMenu/trigger.tsx rename to packages/ui/src/DropdownMenu/Trigger.tsx diff --git a/packages/ui/src/DropdownMenu/index.tsx b/packages/ui/src/DropdownMenu/index.tsx index 877c2bbab3..d0959d4f1d 100644 --- a/packages/ui/src/DropdownMenu/index.tsx +++ b/packages/ui/src/DropdownMenu/index.tsx @@ -1,9 +1,9 @@ -export { DropdownMenu } from './root'; +export { DropdownMenu } from './Root.tsx'; -export type { DropdownMenuProps } from './root'; -export type { DropdownMenuTriggerProps } from './trigger'; -export type { DropdownMenuContentProps } from './content'; -export type { DropdownMenuRadioGroupProps } from './radio-group'; -export type { DropdownMenuRadioItemProps } from './radio-item'; -export type { DropdownMenuCheckboxItemProps } from './checkbox-item'; -export type { DropdownMenuItemProps } from './item'; +export type { DropdownMenuProps } from './Root.tsx'; +export type { DropdownMenuTriggerProps } from './Trigger.tsx'; +export type { DropdownMenuContentProps } from './Content.tsx'; +export type { DropdownMenuRadioGroupProps } from './RadioGroup.tsx'; +export type { DropdownMenuRadioItemProps } from './RadioItem.tsx'; +export type { DropdownMenuCheckboxItemProps } from './CheckboxItem.tsx'; +export type { DropdownMenuItemProps } from './Item.tsx'; diff --git a/packages/ui/src/utils/popover.ts b/packages/ui/src/utils/popover.ts index b71bba0907..7ec4cdf859 100644 --- a/packages/ui/src/utils/popover.ts +++ b/packages/ui/src/utils/popover.ts @@ -24,6 +24,6 @@ export const PopoverContent = styled.div` border-radius: ${props => props.theme.borderRadius.sm}; backdrop-filter: blur(${props => props.theme.blur.lg}); - transform-origin: var(--radix-tooltip-content-transform-origin); + transform-origin: var(--radix-popper-transform-origin); animation: ${scaleIn} 0.15s ease-out; `;