Skip to content

Commit

Permalink
feat(menu): add customization
Browse files Browse the repository at this point in the history
  • Loading branch information
andioneto committed Oct 5, 2021
1 parent 9adf3ed commit d1126c2
Show file tree
Hide file tree
Showing 13 changed files with 807 additions and 88 deletions.
6 changes: 6 additions & 0 deletions .changeset/tender-kids-brush.md
@@ -0,0 +1,6 @@
---
'@twilio-paste/menu': patch
'@twilio-paste/core': patch
---

[Menu]: Enable Component to respect element customizations set on the customization provider. Component now enables setting an element name on the underlying HTML element and checks the emotion theme object to determine whether it should merge in custom styles to the ones set by the component author.
464 changes: 464 additions & 0 deletions packages/paste-core/components/menu/__tests__/customization.spec.tsx

Large diffs are not rendered by default.

Expand Up @@ -10,6 +10,7 @@ const handleClickMock: jest.Mock = jest.fn();

const PreferencesMenu = React.forwardRef<HTMLButtonElement, MenuButtonProps>((props, ref) => {
const menu = useMenuState({baseId: 'sub-menu'});

return (
<>
<MenuButton ref={ref} {...menu} {...props} data-testid="example-submenu-trigger">
Expand Down
4 changes: 2 additions & 2 deletions packages/paste-core/components/menu/src/Menu.tsx
Expand Up @@ -25,8 +25,8 @@ const StyledMenu = React.forwardRef<HTMLDivElement, BoxElementProps>(({style, ..
);
});

const Menu = React.forwardRef<HTMLDivElement, MenuProps>((props, ref) => {
return <MenuPrimitive {...props} as={StyledMenu} ref={ref} />;
const Menu = React.forwardRef<HTMLDivElement, MenuProps>(({element = 'MENU', ...props}, ref) => {
return <MenuPrimitive {...props} element={element} as={StyledMenu} ref={ref} />;
});
Menu.displayName = 'Menu';
export {Menu};
4 changes: 2 additions & 2 deletions packages/paste-core/components/menu/src/MenuButton.tsx
Expand Up @@ -6,9 +6,9 @@ import {Button} from '@twilio-paste/button';

export type MenuButtonProps = MenuPrimitiveButtonProps & ButtonProps;

const MenuButton = React.forwardRef<HTMLButtonElement, MenuButtonProps>((props, ref) => {
const MenuButton = React.forwardRef<HTMLButtonElement, MenuButtonProps>(({element = 'MENU_BUTTON', ...props}, ref) => {
return (
<MenuPrimitiveButton {...props} as={Button} ref={ref}>
<MenuPrimitiveButton {...props} element={element} as={Button} ref={ref}>
{props.children}
</MenuPrimitiveButton>
);
Expand Down
57 changes: 33 additions & 24 deletions packages/paste-core/components/menu/src/MenuGroup.tsx
Expand Up @@ -6,30 +6,39 @@ import type {MenuItemVariant, MenuGroupProps} from './types';

export const MenuGroupContext = React.createContext<MenuItemVariant>(MenuItemVariants.DEFAULT);

const MenuGroup = React.forwardRef<HTMLDivElement, MenuGroupProps>(({label, icon, children, ...props}, ref) => {
return (
<MenuGroupContext.Provider value={MenuItemVariants.GROUP_ITEM}>
<Box {...safelySpreadBoxProps(props)} role="presentation" aria-label={label} textDecoration="none" ref={ref}>
<Box display="flex" alignItems="center" paddingX="space70" paddingY="space30" cursor="default">
{React.isValidElement(icon) ? (
<Box flexShrink={0} size="sizeIcon30">
{React.cloneElement(icon, {color: 'colorTextIcon'})}
</Box>
) : null}
<Text
as="span"
color="colorText"
role="presentation"
fontWeight="fontWeightBold"
paddingLeft={icon != null ? 'space20' : undefined}
>
{label}
</Text>
const MenuGroup = React.forwardRef<HTMLDivElement, MenuGroupProps>(
({label, icon, children, element = 'MENU_GROUP', ...props}, ref) => {
return (
<MenuGroupContext.Provider value={MenuItemVariants.GROUP_ITEM}>
<Box
{...safelySpreadBoxProps(props)}
element={element}
role="presentation"
aria-label={label}
textDecoration="none"
ref={ref}
>
<Box display="flex" alignItems="center" paddingX="space70" paddingY="space30" cursor="default">
{React.isValidElement(icon) ? (
<Box flexShrink={0} size="sizeIcon30">
{React.cloneElement(icon, {color: 'colorTextIcon'})}
</Box>
) : null}
<Text
as="span"
color="colorText"
role="presentation"
fontWeight="fontWeightBold"
paddingLeft={icon != null ? 'space20' : undefined}
>
{label}
</Text>
</Box>
{children}
</Box>
{children}
</Box>
</MenuGroupContext.Provider>
);
});
</MenuGroupContext.Provider>
);
}
);
MenuGroup.displayName = 'MenuGroup';
export {MenuGroup};
77 changes: 40 additions & 37 deletions packages/paste-core/components/menu/src/MenuItem.tsx
Expand Up @@ -6,46 +6,49 @@ import type {MenuItemProps} from './types';
import {MenuItemVariants} from './constants';
import {MenuGroupContext} from './MenuGroup';

export const StyledMenuItem = React.forwardRef<HTMLDivElement | HTMLAnchorElement, MenuItemProps>((props, ref) => {
return (
<Box
{...(props.href && secureExternalLink(props.href))}
as={props.href ? 'a' : 'button'}
{...safelySpreadBoxProps(props)}
appearance="none"
background="none"
border="none"
color={props['aria-disabled'] ? 'colorTextWeaker' : 'colorText'}
display="block"
textAlign="left"
fontFamily="fontFamilyText"
fontSize="fontSize30"
fontWeight="fontWeightNormal"
lineHeight="lineHeight30"
margin="space0"
outline="none"
paddingY="space30"
paddingX={props.variant === MenuItemVariants.GROUP_ITEM ? 'space90' : 'space70'}
textDecoration={props.tabIndex === 0 ? 'underline' : 'none'}
width="100%"
_hover={{
cursor: 'pointer',
}}
_focus={{
color: 'colorTextLink',
}}
_disabled={{cursor: 'not-allowed'}}
ref={ref}
>
{props.children}
</Box>
);
});
export const StyledMenuItem = React.forwardRef<HTMLDivElement | HTMLAnchorElement, MenuItemProps>(
({element = 'STYLED_MENU_ITEM', ...props}, ref) => {
return (
<Box
{...(props.href && secureExternalLink(props.href))}
as={props.href ? 'a' : 'button'}
{...safelySpreadBoxProps(props)}
element={element}
appearance="none"
background="none"
border="none"
color={props['aria-disabled'] ? 'colorTextWeaker' : 'colorText'}
display="block"
textAlign="left"
fontFamily="fontFamilyText"
fontSize="fontSize30"
fontWeight="fontWeightNormal"
lineHeight="lineHeight30"
margin="space0"
outline="none"
paddingY="space30"
paddingX={props.variant === MenuItemVariants.GROUP_ITEM ? 'space90' : 'space70'}
textDecoration={props.tabIndex === 0 ? 'underline' : 'none'}
width="100%"
_hover={{
cursor: 'pointer',
}}
_focus={{
color: 'colorTextLink',
}}
_disabled={{cursor: 'not-allowed'}}
ref={ref}
>
{props.children}
</Box>
);
}
);

const MenuItem = React.forwardRef<HTMLDivElement, MenuItemProps>(
({as = StyledMenuItem, variant: _variant, ...props}, ref) => {
({as = StyledMenuItem, variant: _variant, element = 'MENU_ITEM', ...props}, ref) => {
const variant = _variant || React.useContext(MenuGroupContext);
return <MenuPrimitiveItem {...props} variant={variant} as={as} ref={ref} />;
return <MenuPrimitiveItem {...props} element={element} variant={variant} as={as} ref={ref} />;
}
);
MenuItem.displayName = 'MenuItem';
Expand Down
10 changes: 6 additions & 4 deletions packages/paste-core/components/menu/src/MenuSeparator.tsx
Expand Up @@ -8,9 +8,11 @@ const StyledMenuSeparator = React.forwardRef<HTMLHRElement, SeparatorProps>((pro
return <Separator {...props} orientation="horizontal" verticalSpacing="space40" ref={ref} />;
});

const MenuSeparator = React.forwardRef<HTMLHRElement, MenuSeparatorProps>((props, ref) => {
// as prop from reakit for some reason only accepts a string of `hr` but accepts components. any prevent type errors
return <MenuPrimitiveSeparator {...props} as={StyledMenuSeparator as any} ref={ref} />;
});
const MenuSeparator = React.forwardRef<HTMLHRElement, MenuSeparatorProps>(
({element = 'MENU_SEPARATOR', ...props}, ref) => {
// as prop from reakit for some reason only accepts a string of `hr` but accepts components. any prevent type errors
return <MenuPrimitiveSeparator {...props} element={element} as={StyledMenuSeparator as any} ref={ref} />;
}
);
MenuSeparator.displayName = 'MenuSeparator';
export {MenuSeparator};
35 changes: 21 additions & 14 deletions packages/paste-core/components/menu/src/SubMenuButton.tsx
@@ -1,24 +1,31 @@
import * as React from 'react';
import type {BoxElementProps} from '@twilio-paste/box';
import type {MenuPrimitiveButtonProps} from '@twilio-paste/menu-primitive';
import {MenuPrimitiveButton} from '@twilio-paste/menu-primitive';
import {MediaObject, MediaBody, MediaFigure} from '@twilio-paste/media-object';
import {ChevronRightIcon} from '@twilio-paste/icons/esm/ChevronRightIcon';
import {StyledMenuItem} from './MenuItem';

export type SubMenuButtonProps = MenuPrimitiveButtonProps;
export type SubMenuButtonProps = MenuPrimitiveButtonProps & {element?: BoxElementProps['element']};

const SubMenuButton = React.forwardRef<HTMLButtonElement, SubMenuButtonProps>((props, ref) => {
// MenuPrimitiveButton from reakit types `as` as HTML element names, but accepts components. any prevents type errors
return (
<MenuPrimitiveButton {...props} as={StyledMenuItem as any} ref={ref}>
<MediaObject as="span" verticalAlign="center">
{props.children && <MediaBody as="span">{props.children}</MediaBody>}
<MediaFigure as="span" align="end" spacing="space20">
<ChevronRightIcon decorative size="sizeIcon30" />
</MediaFigure>
</MediaObject>
</MenuPrimitiveButton>
);
});
const SubMenuButton = React.forwardRef<HTMLButtonElement, SubMenuButtonProps>(
({element = 'SUBMENU_BUTTON', ...props}, ref) => {
// MenuPrimitiveButton from reakit types `as` as HTML element names, but accepts components. any prevents type errors
return (
<MenuPrimitiveButton {...props} as={StyledMenuItem as any} element={element} ref={ref}>
<MediaObject as="span" verticalAlign="center" element={`${element}_MEDIA_OBJECT`}>
{props.children && (
<MediaBody as="span" element={`${element}_MEDIA_BODY`}>
{props.children}
</MediaBody>
)}
<MediaFigure as="span" align="end" spacing="space20" element={`${element}_MEDIA_FIGURE`}>
<ChevronRightIcon decorative size="sizeIcon30" element={`${element}_ICON`} />
</MediaFigure>
</MediaObject>
</MenuPrimitiveButton>
);
}
);
SubMenuButton.displayName = 'SubMenuButton';
export {SubMenuButton};
4 changes: 1 addition & 3 deletions packages/paste-core/components/menu/src/index.tsx
@@ -1,6 +1,4 @@
import {useMenuPrimitiveState} from '@twilio-paste/menu-primitive';

export {useMenuPrimitiveState as useMenuState};
export * from './useMenuState';
export * from './Menu';
export * from './MenuButton';
export * from './MenuGroup';
Expand Down
7 changes: 5 additions & 2 deletions packages/paste-core/components/menu/src/types.ts
@@ -1,17 +1,19 @@
import type {ValueOf} from '@twilio-paste/types';
import type {BoxElementProps} from '@twilio-paste/box';
import type {
MenuPrimitiveItemProps,
MenuPrimitiveProps,
MenuPrimitiveSeparatorProps,
} from '@twilio-paste/menu-primitive';
import type {MenuItemVariants} from './constants';

export type MenuProps = MenuPrimitiveProps & {'aria-label': string};
export type MenuProps = MenuPrimitiveProps & {'aria-label': string; element?: BoxElementProps['element']};

export type MenuItemVariant = ValueOf<typeof MenuItemVariants>;

export interface MenuItemProps extends MenuPrimitiveItemProps {
href?: string;
element?: BoxElementProps['element'];
variant?: MenuItemVariant;
as?: any;
}
Expand All @@ -20,6 +22,7 @@ export interface MenuGroupProps {
label: string;
icon?: React.ReactNode;
children: React.ReactNode;
element?: BoxElementProps['element'];
}

export type MenuSeparatorProps = MenuPrimitiveSeparatorProps;
export type MenuSeparatorProps = MenuPrimitiveSeparatorProps & {element?: BoxElementProps['element']};
3 changes: 3 additions & 0 deletions packages/paste-core/components/menu/src/useMenuState.ts
@@ -0,0 +1,3 @@
import {useMenuPrimitiveState} from '@twilio-paste/menu-primitive';

export {useMenuPrimitiveState as useMenuState};

0 comments on commit d1126c2

Please sign in to comment.