nishant-design-system
Version:
Sense UI components library
299 lines (276 loc) • 7.17 kB
Flow
// @flow strict
import * as React from 'react';
import type {ColorTypes} from '../../types/typography';
import classify from '../../utils/classify';
import {Button} from '../Button';
import type {IconProps} from '../Icon';
import {Icon} from '../Icon';
import type {TextProps} from '../Text';
import {
ButtonTextSmall,
SubTitleExtraSmall,
SubTitleMedium,
TEXT_COLORS,
} from '../Text';
import css from './SubMenu.module.css';
type ClassNames = $ReadOnly<{wrapper?: string}>;
export const SubMenuItemIcon = ({
className,
...props
}: IconProps): React.Node => (
<Icon
{...props}
size="small"
color={TEXT_COLORS.inverseSecondary}
className={classify(css.menuIcon, className)}
/>
);
SubMenuItemIcon.displayName = 'SubMenuItemIcon';
export const SubMenuItemText = ({
children,
className,
...props
}: TextProps): React.Node => (
<ButtonTextSmall
{...props}
className={classify(css.subMenuItemText, className)}
color={TEXT_COLORS.inversePrimary}
>
{children}
</ButtonTextSmall>
);
SubMenuItemText.displayName = 'SubMenuItemText';
export type SubMenuItemActionProps = {
children?: React.Node,
color?: ColorTypes,
className?: string,
...
};
export const SubMenuItemAction = ({
children,
className,
...props
}: SubMenuItemActionProps): React.Node => (
<div {...props} className={classify(css.subMenuItemAction, className)}>
{children}
</div>
);
SubMenuItemAction.displayName = 'SubMenuItemAction';
export type SubMenuItemProps = {
classNames?: ClassNames,
children?: React.Node,
selectedMenuKey?: string,
disabled?: boolean,
onChange?: (selectedMenuKey: string) => mixed,
menuKey: string,
...
};
export const SubMenuItem: React$AbstractComponent<
SubMenuItemProps,
HTMLElement,
> = React.forwardRef(
(
{
children,
selectedMenuKey,
disabled,
classNames,
onChange,
menuKey,
...props
}: SubMenuItemProps,
ref,
): React.Node => {
const onChangeHandler = () => {
if (!disabled) {
onChange && onChange(menuKey);
}
};
const onKeyDownHandler = (e) => {
if (e.key === 'Enter') {
onChangeHandler();
}
};
const selected = selectedMenuKey === menuKey;
const getNamedComp = (comp: string) => {
const childrenArray = React.Children.toArray(children);
if (childrenArray.length) {
const nodes: React.Node[] = [];
for (const child of childrenArray) {
if (child?.type?.displayName === comp) {
nodes.push(
React.cloneElement(child, {
selected,
disabled,
}),
);
}
}
return nodes.length > 1 ? nodes : nodes[0];
}
return null;
};
return (
<div
{...props}
className={classify(
css.menuItem,
{
[css.selected]: selected,
[css.disabled]: disabled,
},
classNames?.wrapper,
)}
onKeyDown={onKeyDownHandler}
onClick={onChangeHandler}
ref={ref}
tabIndex={disabled ? '-1' : 0}
>
<div className={css.menuIconName}>
{getNamedComp('SubMenuItemIcon')}
{getNamedComp('SubMenuItemText')}
</div>
{getNamedComp('SubMenuItemAction')}
</div>
);
},
);
type SubMenuGroupClassNames = $ReadOnly<{
wrapper?: string,
groupHeader?: string,
menuGroup?: string,
}>;
export type SubMenuGroupProps = {
children?: React.Node,
groupTitle?: string,
collapsible?: boolean,
disabled?: boolean,
onChange?: (value: string) => mixed,
selectedMenuKey?: string,
classNames?: SubMenuGroupClassNames,
};
export const SubMenuGroup = ({
children,
groupTitle,
collapsible = false,
disabled = false,
onChange,
selectedMenuKey,
classNames,
}: SubMenuGroupProps): React.Node => {
const [collapsed, setCollapsed] = React.useState(false);
const collapseHandler = () => {
collapsible && setCollapsed(!collapsed);
};
const onChangeHandler = (value) => {
if (onChange) {
onChange(value);
}
};
const onKeyDownHandlerHeader = (e) => {
if (e.key === 'Enter') {
collapseHandler();
}
};
const childrenWithProps = React.Children.map(children, (child) => {
if (React.isValidElement(child)) {
const {disabled: disabledChild} = child.props;
return React.cloneElement(child, {
selectedMenuKey,
onChange: onChangeHandler,
disabled: disabledChild || disabled,
});
}
return child;
});
return (
<div className={classify(css.subMenuGroupWrapper, classNames?.wrapper)}>
<div
className={classify(
css.subMenuGroupHeader,
{[css.collapsible]: collapsible},
classNames?.groupHeader,
)}
onClick={collapseHandler}
onKeyDown={onKeyDownHandlerHeader}
tabIndex={collapsible ? 0 : -1}
>
<SubTitleExtraSmall
color={TEXT_COLORS.inverseSecondary}
className={css.groupTitle}
>
{groupTitle}
</SubTitleExtraSmall>
{collapsible && (
<Icon
color={TEXT_COLORS.inverseSecondary}
name={collapsed ? 'chevron-down' : 'chevron-up'}
size="small"
/>
)}
</div>
<div
className={classify(
css.subMenuGroup,
{
[css.collapsed]: collapsible && collapsed,
},
classNames?.menuGroup,
)}
>
{childrenWithProps}
</div>
</div>
);
};
export type SubMenuProps = {
classNames?: ClassNames,
title: string,
children?: React.Node,
onClose?: () => mixed,
showClose?: boolean,
};
export const SubMenu: React$AbstractComponent<SubMenuProps, HTMLDivElement> =
React.forwardRef<SubMenuProps, HTMLDivElement>(
(
{classNames, children, title, onClose, showClose = false}: SubMenuProps,
ref,
): React.Node => {
const childrenWithProps = React.Children.map(children, (child) => {
if (React.isValidElement(child)) {
return <div className={css.menuChildWrap}>{child}</div>;
}
});
return (
<div
className={classify(css.subMenuWrapper, classNames?.wrapper)}
ref={ref}
>
<div className={css.subMenuTitleAndButton}>
<SubTitleMedium
className={css.subMenuHeader}
color={TEXT_COLORS.inversePrimary}
>
{title}
</SubTitleMedium>
{showClose && (
<Button
ariaLabel="Icon Button"
iconRightName="close"
onClick={onClose}
size="small"
type="ghost"
actionType="button"
classNames={{
wrapper: css.subMenuCloseButton,
icon: css.subMenuCloseIcon,
}}
/>
)}
</div>
{childrenWithProps}
</div>
);
},
);
SubMenu.displayName = 'SubMenu';