-
Notifications
You must be signed in to change notification settings - Fork 16
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* feat(ui): #prax-160: implement DropdownMenu with sub components * feat(ui): #prax-160: improve dropdown components, add docs * feat(ui): #prax-160: add tests * chore: add changeset * fix: prettier * fix(ui): after review
- Loading branch information
Showing
20 changed files
with
607 additions
and
53 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
--- | ||
'@repo/ui': minor | ||
--- | ||
|
||
Add DropdownMenu UI component |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
import { | ||
CheckboxItem as RadixDropdownMenuCheckboxItem, | ||
ItemIndicator as RadixDropdownMenuItemIndicator, | ||
} 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 ( | ||
<RadixDropdownMenuCheckboxItem | ||
checked={checked} | ||
disabled={disabled} | ||
asChild | ||
onCheckedChange={onChange} | ||
> | ||
<MenuItem {...asTransientProps({ actionType, disabled })}> | ||
<RadixDropdownMenuItemIndicator> | ||
<Check /> | ||
</RadixDropdownMenuItemIndicator> | ||
|
||
<Text small>{children}</Text> | ||
</MenuItem> | ||
</RadixDropdownMenuCheckboxItem> | ||
); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
import { ReactNode } from 'react'; | ||
import { useTheme } from 'styled-components'; | ||
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; | ||
side?: RadixDropdownMenuContentProps['side']; | ||
align?: RadixDropdownMenuContentProps['align']; | ||
} | ||
|
||
export const Content = ({ children, side, align }: DropdownMenuContentProps) => { | ||
const theme = useTheme(); | ||
|
||
return ( | ||
<RadixDropdownMenuPortal> | ||
<RadixDropdownMenuContent | ||
sideOffset={theme.spacing(1, 'number')} | ||
side={side} | ||
align={align} | ||
asChild | ||
> | ||
<PopoverContent>{children}</PopoverContent> | ||
</RadixDropdownMenuContent> | ||
</RadixDropdownMenuPortal> | ||
); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
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'; | ||
|
||
export interface DropdownMenuItemProps extends DropdownMenuItemBase { | ||
children?: ReactNode; | ||
onSelect?: (event: Event) => void; | ||
} | ||
|
||
export const Item = ({ | ||
children, | ||
actionType = 'default', | ||
disabled, | ||
onSelect, | ||
}: DropdownMenuItemProps) => { | ||
return ( | ||
<RadixDropdownMenuItem disabled={disabled} asChild onSelect={onSelect}> | ||
<MenuItem {...asTransientProps({ actionType, disabled })}> | ||
<Text small>{children}</Text> | ||
</MenuItem> | ||
</RadixDropdownMenuItem> | ||
); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
import { ReactNode } from 'react'; | ||
import { RadioGroup as RadixDropdownMenuRadioGroup } from '@radix-ui/react-dropdown-menu'; | ||
|
||
export interface DropdownMenuRadioGroupProps { | ||
children?: ReactNode; | ||
value?: string; | ||
onChange?: (value: string) => void; | ||
} | ||
|
||
export const RadioGroup = ({ children, value, onChange }: DropdownMenuRadioGroupProps) => { | ||
return ( | ||
<RadixDropdownMenuRadioGroup value={value} onValueChange={onChange}> | ||
{children} | ||
</RadixDropdownMenuRadioGroup> | ||
); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
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'; | ||
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 ( | ||
<RadixDropdownMenuRadioItem value={value} disabled={disabled} asChild> | ||
<MenuItem {...asTransientProps({ actionType, disabled })}> | ||
<RadixDropdownMenuItemIndicator> | ||
<Check /> | ||
</RadixDropdownMenuItemIndicator> | ||
|
||
<Text small>{children}</Text> | ||
</MenuItem> | ||
</RadixDropdownMenuRadioItem> | ||
); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,90 @@ | ||
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 './RadioGroup.tsx'; | ||
import { RadioItem } from './RadioItem.tsx'; | ||
import { CheckboxItem } from './CheckboxItem.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); | ||
|
||
/** | ||
* A dropdown menu with a set of subcomponents for composing complex menus. | ||
* | ||
* `<DropdownMenu>` can be controlled or uncontrolled. If `isOpen` is not provided | ||
* but `<DropdownMenu.Trigger>` is present, it will open itself. | ||
* | ||
* You can nest multiple components inside the `<DropdownMenu.Content>`: | ||
* - `<DropdownMenu.Item>` as an action button in the dropdown | ||
* - `<DropdownMenu.RadioGroup>` with `<DropdownMenu.RadioItem>` as a group of radio buttons | ||
* - `<DropdownMenu.CheckboxItem>` as a checkbox | ||
* | ||
* Example: | ||
* | ||
* ```tsx | ||
* const [radioValue, setRadioValue] = useState('1'); | ||
* const [apple, setApple] = useState(false); | ||
* const [banana, setBanana] = useState(false); | ||
* | ||
* <DropdownMenu> | ||
* <DropdownMenu.Trigger> | ||
* <Button iconOnly icon={Filter}> | ||
* Filter | ||
* </Button> | ||
* </DropdownMenu.Trigger> | ||
* | ||
* <DropdownMenu.Content> | ||
* <DropdownMenu.Item>Default item</DropdownMenu.Item> | ||
* <DropdownMenu.Item actionType='destructive'>Destructive item</DropdownMenu.Item> | ||
* | ||
* <DropdownMenu.RadioGroup value={value} onValueChange={setValue}> | ||
* <DropdownMenu.RadioItem value='4'>Default</DropdownMenu.RadioItem> | ||
* <DropdownMenu.RadioItem value='5' disabled>Disabled</DropdownMenu.RadioItem> | ||
* </DropdownMenu.RadioGroup> | ||
* | ||
* <DropdownMenu.CheckboxItem checked={apple} onChange={setApple}>Apple</DropdownMenu.CheckboxItem> | ||
* <DropdownMenu.CheckboxItem checked={banana} onChange={setBanana}>Banana</DropdownMenu.CheckboxItem> | ||
* </DropdownMenu.Content> | ||
* </DropdownMenu> | ||
* ``` | ||
*/ | ||
export const DropdownMenu = ({ children, onClose, isOpen }: DropdownMenuProps) => { | ||
return ( | ||
<RadixDropdownMenuRoot | ||
open={isOpen} | ||
onOpenChange={onClose ? value => !value && onClose() : undefined} | ||
> | ||
{children} | ||
</RadixDropdownMenuRoot> | ||
); | ||
}; | ||
|
||
DropdownMenu.Trigger = Trigger; | ||
DropdownMenu.Content = Content; | ||
DropdownMenu.RadioGroup = RadioGroup; | ||
DropdownMenu.RadioItem = RadioItem; | ||
DropdownMenu.CheckboxItem = CheckboxItem; | ||
DropdownMenu.Item = Item; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
import type { ReactNode } from 'react'; | ||
import { Trigger as RadixDropdownMenuTrigger } from '@radix-ui/react-dropdown-menu'; | ||
|
||
export interface DropdownMenuTriggerProps { | ||
children: ReactNode; | ||
} | ||
|
||
export const Trigger = ({ children }: DropdownMenuTriggerProps) => ( | ||
<RadixDropdownMenuTrigger asChild>{children}</RadixDropdownMenuTrigger> | ||
); |
Oops, something went wrong.