diff --git a/.changeset/stupid-rockets-clap.md b/.changeset/stupid-rockets-clap.md new file mode 100644 index 00000000..afafc082 --- /dev/null +++ b/.changeset/stupid-rockets-clap.md @@ -0,0 +1,13 @@ +--- +"@cube-dev/ui-kit": patch +--- + +Adds prop `selectionType` for `Menu` component. That stands for values `checkbox` or `radio`. + +```jsx + + Item 1 + Item 2 + +``` + diff --git a/src/components/pickers/Menu/Menu.stories.tsx b/src/components/pickers/Menu/Menu.stories.tsx index aebbdd2e..f9a360bc 100644 --- a/src/components/pickers/Menu/Menu.stories.tsx +++ b/src/components/pickers/Menu/Menu.stories.tsx @@ -199,6 +199,36 @@ export const MenuSelectableMultiple = (props) => { }); }; +export const MenuSelectableCheckboxes = (props) => { + const [selectedKeys, setSelectedKeys] = useState(['1', '2']); + const onSelectionChange = (key) => { + setSelectedKeys(key); + }; + + return MenuTemplate({ + ...props, + selectionIcon: 'checkbox', + selectionMode: 'multiple', + selectedKeys, + onSelectionChange, + }); +}; + +export const MenuSelectableRadio = (props) => { + const [selectedKeys, setSelectedKeys] = useState(['1']); + const onSelectionChange = (key) => { + setSelectedKeys(key); + }; + + return MenuTemplate({ + ...props, + selectionIcon: 'radio', + selectionMode: 'single', + selectedKeys, + onSelectionChange, + }); +}; + export const PaymentDetails = (props) => { return ( diff --git a/src/components/pickers/Menu/Menu.tsx b/src/components/pickers/Menu/Menu.tsx index 17eda332..5e54470c 100644 --- a/src/components/pickers/Menu/Menu.tsx +++ b/src/components/pickers/Menu/Menu.tsx @@ -19,12 +19,13 @@ import { import { StyledMenu, StyledMenuHeader } from './styled'; import { MenuItem } from './MenuItem'; import { MenuSection } from './MenuSection'; -import { MenuButtonProps } from './MenuButton'; +import { MenuButtonProps, MenuSelectionType } from './MenuButton'; import { useMenuContext } from './context'; export interface CubeMenuProps extends ContainerStyleProps, AriaMenuProps { + selectionIcon?: MenuSelectionType; header?: ReactNode; footer?: ReactNode; styles?: Styles; @@ -34,7 +35,7 @@ function Menu( props: CubeMenuProps, ref: DOMRef, ) { - const { header, footer } = props; + const { header, footer, selectionIcon } = props; const domRef = useDOMRef(ref); const contextProps = useMenuContext(); const completeProps = mergeProps(contextProps, props); @@ -70,6 +71,7 @@ function Menu( key={item.key} item={item} state={state} + selectionIcon={selectionIcon} onAction={completeProps.onAction} /> ); @@ -80,6 +82,7 @@ function Menu( key={item.key} item={item} state={state} + selectionIcon={selectionIcon} onAction={completeProps.onAction} /> ); diff --git a/src/components/pickers/Menu/MenuButton.tsx b/src/components/pickers/Menu/MenuButton.tsx index 9a2c0340..099a76ba 100644 --- a/src/components/pickers/Menu/MenuButton.tsx +++ b/src/components/pickers/Menu/MenuButton.tsx @@ -1,9 +1,9 @@ import { ReactNode } from 'react'; import { Button, CubeButtonProps } from '../../actions'; import { Text } from '../../content/Text'; -import { Styles } from '../../../tasty'; +import { Styles, tasty } from '../../../tasty'; import { Space } from '../../layout/Space'; -import { CheckOutlined } from '@ant-design/icons'; +import { CheckOutlined, CheckCircleOutlined } from '@ant-design/icons'; const ACTION_BUTTON: Styles = { border: { @@ -30,6 +30,8 @@ const ACTION_BUTTON: Styles = { padding: { '': '(0.75x - 1px) (1.5x - 1px)', 'selectable & !selected': + '(0.75x - 1px) (1.5x - 1px) (0.75x - 1px) (1.5x - 1px)', + 'selectionIcon & selectable & !selected': '(0.75x - 1px) (1.5x - 1px) (0.75x - 1px) (1.5x - 1px + 22px)', }, display: 'flex', @@ -45,6 +47,23 @@ const ACTION_BUTTON: Styles = { }, }; +const RadioIcon = tasty({ + styles: { + display: 'flex', + width: '1.875x', + placeContent: 'center', + + '&::before': { + display: 'block', + content: '""', + width: '1x', + height: '1x', + radius: 'round', + fill: '#current', + }, + }, +}); + const getPostfix = (postfix) => typeof postfix === 'string' ? ( @@ -54,22 +73,38 @@ const getPostfix = (postfix) => postfix ); +export type MenuSelectionType = 'checkbox' | 'radio'; + export type MenuButtonProps = { postfix: ReactNode; + selectionIcon?: MenuSelectionType; isSelectable?: boolean; disabled?: boolean; } & CubeButtonProps; +const getSelectionTypeIcon = (selectionIcon?: MenuSelectionType) => { + switch (selectionIcon) { + case 'checkbox': + return ; + case 'radio': + return ; + default: + return null; + } +}; + export function MenuButton({ children, icon, postfix, ...props }: MenuButtonProps) { - const { isSelected, isSelectable } = props; - const checkIcon = isSelectable && isSelected ? : null; + const { selectionIcon, isSelected, isSelectable } = props; + const checkIcon = + isSelectable && isSelected ? getSelectionTypeIcon(selectionIcon) : null; const mods = { ...props.mods, + selectionIcon: !!selectionIcon, selectable: isSelectable, selected: isSelected, }; diff --git a/src/components/pickers/Menu/MenuItem.tsx b/src/components/pickers/Menu/MenuItem.tsx index 4d04f744..1fbbe314 100644 --- a/src/components/pickers/Menu/MenuItem.tsx +++ b/src/components/pickers/Menu/MenuItem.tsx @@ -7,18 +7,19 @@ import { useMenuItem } from '@react-aria/menu'; import { mergeProps, ClearSlots, SlotProvider } from '../../../utils/react'; import { useMenuContext } from './context'; import { StyledMenuItem } from './styled'; -import { MenuButton } from './MenuButton'; +import { MenuButton, MenuSelectionType } from './MenuButton'; export interface MenuItemProps { item: Node; state: TreeState; + selectionIcon?: MenuSelectionType; isVirtualized?: boolean; onAction?: (key: Key) => void; } /** @private */ export function MenuItem(props: MenuItemProps) { - const { item, state, isVirtualized, onAction } = props; + const { item, state, selectionIcon, isVirtualized, onAction } = props; const { onClose, closeOnSelect } = useMenuContext(); const { rendered, key, props: itemProps } = item; @@ -48,6 +49,7 @@ export function MenuItem(props: MenuItemProps) { typeof rendered === 'string' ? (