Skip to content

Commit

Permalink
feat(menu): added new menu options to make it more customizable
Browse files Browse the repository at this point in the history
Added sr (screen reader) prop, new classNames for: - menu - icon. New iconStart and iconEnd options
along with their relative positions to the button element. New menuPositionX & menuPositionY prop
for customizing menu popup location. Added descriptions for all props.
  • Loading branch information
fluid-design-io committed Aug 22, 2022
1 parent fd3a667 commit 7caa55e
Show file tree
Hide file tree
Showing 2 changed files with 162 additions and 43 deletions.
117 changes: 75 additions & 42 deletions src/lib/components/Form/AppMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,47 +5,62 @@ import { MenuProps } from '.';
import clsxm from '../../helpers/clsxm';

function AppMenu({
sr,
label,
header,
icon,
iconStart: IconStart,
iconEnd: IconEnd,
badge,
menus,
className,
buttonClassName,
labelClassName,
badgeClassName,
menuClassName,
iconClassName,
iconStartPosition = 'flex',
iconEndPosition = 'flex',
menuPositionX = 'center',
menuPositionY = 'bottom',
...props
}: MenuProps & {
className?: string;
buttonClassName?: string;
labelClassName?: string;
badgeClassName?: string;
[x: string]: any;
} & React.HTMLAttributes<HTMLDivElement>) {
}: MenuProps & React.HTMLAttributes<HTMLDivElement>) {
const iconDefaultClassName =
'h-5 w-5 flex-shrink-0 text-primary-400 group-hover:text-primary-500 dark:text-primary-400 dark:group-hover:text-primary-50';
return (
<Menu as="div" className={clsxm('relative -ml-px block', className)} {...props}>
<Menu.Button
className={clsxm(
'default-focus-visible group inline-flex items-center justify-center text-sm font-medium text-gray-700 hover:text-gray-900',
'default-focus-visible group inline-flex items-center justify-center text-sm font-medium text-primary-700 hover:text-primary-900 [-webkit-tap-highlight-color:transparent]',
buttonClassName
)}
>
{label && (
<span className={clsxm('hover:text-gray-900 dark:text-gray-200 dark:hover:text-gray-50', labelClassName)}>
{label}
</span>
)}
{icon && icon}
{badge && (
<span
className={clsxm(
'ml-1.5 rounded bg-gray-200 py-0.5 px-1.5 text-xs font-semibold tabular-nums text-gray-700',
badgeClassName
)}
>
{badge}
</span>
)}
{sr && <span className="sr-only">{sr}</span>}
<div className={clsxm('flex justify-start items-center w-full gap-2')}>
{IconStart && <IconStart className={clsxm(iconDefaultClassName, iconClassName)} />}
{iconStartPosition === 'between' && <span className="flex-grow" />}
{label && (
<span
className={clsxm(
'hover:text-primary-900 dark:text-primary-200 dark:hover:text-primary-50',
labelClassName
)}
>
{label}
</span>
)}
{iconEndPosition === 'between' && <span className="flex-grow" />}
{IconEnd && <IconEnd className={clsxm(iconDefaultClassName, iconClassName)} />}
{badge && (
<span
className={clsxm(
'ml-1.5 rounded bg-primary-200 py-0.5 px-1.5 text-xs font-semibold tabular-nums text-primary-700',
badgeClassName
)}
>
{badge}
</span>
)}
</div>
</Menu.Button>

<Transition
Expand All @@ -57,33 +72,51 @@ function AppMenu({
leaveFrom="transform opacity-100 scale-100"
leaveTo="transform opacity-0 scale-95"
>
<Menu.Items className="popover-panel menu-popover absolute right-0 z-50 w-56 divide-y divide-gray-100 dark:divide-gray-700">
<Menu.Items
className={clsxm(
'absolute z-50 min-w-full w-min divide-y divide-primary-100 dark:divide-primary-700 rounded-md bg-white shadow-2xl ring-1 ring-black ring-opacity-5 focus:outline-none dark:bg-primary-800 dark:ring-white dark:ring-opacity-5',
menuPositionX === 'center' && 'mx-auto left-1/2 -translate-x-1/2',
menuPositionX === 'start' && 'left-0 rtl:right-0',
menuPositionX === 'end' && 'right-0 rtl:left-0',
menuPositionY === 'center' && 'mt-auto top-0 bottom-0',
menuPositionY === 'top' && 'bottom-full mb-2',
menuPositionY === 'bottom' && 'top-full mt-2',
menuClassName
)}
>
{header && header}
<div className="py-1">
{menus.map((item) => (
<Menu.Item key={`menu-${item.label}`}>
{menus.map(({ icon: Icon, label, onClick, role, ...props }) => (
<Menu.Item key={`menu-${label}`}>
{({ active }) =>
item.role === 'separator' ? (
<div className="mt-1 border-t border-t-gray-100 pb-1 dark:border-t-gray-700" />
role === 'separator' ? (
<div className="mt-1 border-t border-t-primary-100 pb-1 dark:border-t-primary-700" />
) : (
<button
onClick={item.onClick}
onClick={onClick}
className={clsxm([
!item.role && active && 'bg-gray-100 text-gray-900 dark:bg-black/20 dark:text-white',
(!item.role || item.role === 'default') && 'text-gray-700 dark:text-gray-200',
item.role === 'destructive' && active && 'bg-red-100 dark:bg-red-700/20',
item.role === 'destructive' && 'text-red-600 dark:text-red-300',
item.role === 'info' && active && 'bg-blue-100 dark:bg-blue-700/20',
item.role === 'info' && 'text-blue-600 dark:text-blue-300',
item.role === 'success' && active && 'bg-green-100 dark:bg-green-700/20',
item.role === 'success' && 'text-green-600 dark:text-green-300',
((!role && active) || (role === 'default' && active)) &&
'bg-primary-100 text-primary-900 dark:bg-black/20 dark:text-white',
(!role || role === 'default') && 'text-primary-700 dark:text-primary-200',
role === 'destructive' && active && 'bg-red-100 dark:bg-red-700/20',
role === 'destructive' && 'text-red-600 dark:text-red-300',
role === 'info' && active && 'bg-blue-100 dark:bg-blue-700/20',
role === 'info' && 'text-blue-600 dark:text-blue-300',
role === 'success' && active && 'bg-green-100 dark:bg-green-700/20',
role === 'success' && 'text-green-600 dark:text-green-300',
'disabled:cursor-not-allowed disabled:opacity-50',
'block w-full px-4 py-2 text-left text-sm rtl:text-right',
'flex items-center justify-start',
'flex items-center justify-start gap-2',
])}
{...props}
>
{item.icon && <item.icon className="mr-2 h-4 w-4" />}
<span>{item.label}</span>
<span className="rtl:hidden">
{typeof Icon === 'function' ? <Icon className="h-4 w-4" /> : Icon}
</span>
<span>{label}</span>
<span className="hidden rtl:block">
{typeof Icon === 'function' ? <Icon className="h-4 w-4" /> : Icon}
</span>
</button>
)
}
Expand Down
88 changes: 87 additions & 1 deletion src/lib/components/Form/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,14 +38,100 @@ export interface DashboardNavProps {

export interface MenuProps {
label?: string;
/**
* A React component to be used as the icon for the menu item.
*/
icon?: JSX.Element;
/**
* displayed after the label
* `string
*/
badge?: string;
/**
* Header of the menu
*/
header?: JSX.Element;
/**
* sr: Screen reader text
*/
sr?: string;
/**
* iconStart: Icon to display on the left of the label
*/
iconStart?: (props: SVGProps<SVGSVGElement>) => JSX.Element;
/**
* iconEnd: Icon to display on the right of the label
*/
iconEnd?: (props: SVGProps<SVGSVGElement>) => JSX.Element;
/**
* iconStartPosition: Position of the iconStart
* `flex` (default) or `between`
* `between` will create a gap between the icon and the label
*/
iconStartPosition?: 'flex' | 'between';
/**
* iconEndPosition: Position of the iconEnd
* `flex` (default) or `between`
* `between` will create a gap between the icon and the label
*/
iconEndPosition?: 'flex' | 'between';
className?: string;
/**
* buttonClassName: Additional class name to apply to the button
* This will override the default class name if the custom class name conflicts with an existing class name
*/
buttonClassName?: string;
/**
* labelClassName: Additinal class name to apply to the label
* This will override the default class name if the custom class name conflicts with an existing class name
*/
labelClassName?: string;
/**
* badgeClassName: Additinal class name to apply to the badge
* This will override the default class name if the custom class name conflicts with an existing class name
*/
badgeClassName?: string;
/**
* menuClassName: Additinal class name to apply to the menu
* This will override the default class name if the custom class name conflicts with an existing class name
*/
menuClassName?: string;
/**
* iconClassName: Additinal class name to apply to the icon
* This will override the default class name if the custom class name conflicts with an existing class name
*/
iconClassName?: string;
/**
* menuPositionX: Horizontal position of the menu
* `center` (default) or `left` or `right`
*/
menuPositionX?: 'start' | 'center' | 'end';
/**
* menuPositionY: Vertical position of the menu
* `top` (default) or `bottom`
*/
menuPositionY?: 'top' | 'center' | 'bottom';
[x: string]: any;
/**
* menus: Array of menu items
* Each menu item should be an object with the following properties:
* `label`: The name of the menu item
* `href`: `optional` - The URL of the menu item
* `icon`: `optional` - A React component or a function that returns a React component to be used as the icon for the menu item
* `role`: `optional` - The role of the menu item
* `onClick`: `optional` - A function to be called when the menu item is clicked
* `props`: `optional` - Any additional props to be passed to the menu item
*/
menus: {
/**
* sr: Screen reader text
*/
sr?: string;
label?: string;
onClick?: () => void | Promise<boolean | void>;
icon?: (props: SVGProps<SVGSVGElement>) => JSX.Element;
icon?: (props: SVGProps<SVGSVGElement>) => JSX.Element | JSX.Element;
role?: 'separator' | 'destructive' | 'default' | 'info' | 'success' | 'warning';
[x: string]: any;
}[];
}
export interface PopoverProps {
Expand Down

0 comments on commit 7caa55e

Please sign in to comment.