diff --git a/.changeset/feat-dropdownExpandableMenu.md b/.changeset/feat-dropdownExpandableMenu.md new file mode 100644 index 000000000..a5e90546d --- /dev/null +++ b/.changeset/feat-dropdownExpandableMenu.md @@ -0,0 +1,5 @@ +--- +'react-magma-dom': minor +--- + +feat(DropdownExpandableMenu): A new menu item display for the Dropdown component which enables expandable lists by one level. diff --git a/packages/react-magma-dom/src/components/Accordion/AccordionButton.tsx b/packages/react-magma-dom/src/components/Accordion/AccordionButton.tsx index 3d9b353a3..0ac8a7702 100644 --- a/packages/react-magma-dom/src/components/Accordion/AccordionButton.tsx +++ b/packages/react-magma-dom/src/components/Accordion/AccordionButton.tsx @@ -20,6 +20,11 @@ import { transparentize } from 'polished'; export interface AccordionButtonProps extends UseAccordionButtonProps, React.HTMLAttributes { + /** + * For use in components repurposing Accordion with custom keyboard navigation with it's elements. + * @internal + */ + customOnKeyDown?: () => void; /** * @internal */ @@ -90,7 +95,13 @@ export const AccordionButton = React.forwardRef< HTMLButtonElement, AccordionButtonProps >((props, forwardedRef) => { - const { children, testId, isInverse: isInverseProp, ...rest } = props; + const { + children, + testId, + customOnKeyDown, + isInverse: isInverseProp, + ...rest + } = props; const theme = React.useContext(ThemeContext); const isInverse = useIsInverse(isInverseProp); @@ -126,7 +137,7 @@ export const AccordionButton = React.forwardRef< isExpanded={isExpanded} isInverse={isInverse} onClick={handleClick} - onKeyDown={handleKeyDown} + onKeyDown={customOnKeyDown ? customOnKeyDown : handleKeyDown} ref={ref} theme={theme} > diff --git a/packages/react-magma-dom/src/components/Dropdown/Dropdown.stories.tsx b/packages/react-magma-dom/src/components/Dropdown/Dropdown.stories.tsx index 4d2886609..a4d90dd53 100644 --- a/packages/react-magma-dom/src/components/Dropdown/Dropdown.stories.tsx +++ b/packages/react-magma-dom/src/components/Dropdown/Dropdown.stories.tsx @@ -17,10 +17,21 @@ import { Card, CardBody } from '../Card'; import { Input } from '../Input'; import { Checkbox } from '../Checkbox'; import { PasswordInput } from '../PasswordInput'; -import { SettingsIcon, MenuIcon } from 'react-magma-icons'; +import { + LocalPizzaIcon, + LunchDiningIcon, + MenuIcon, + RestaurantMenuIcon, + SettingsIcon, +} from 'react-magma-icons'; import { Story, Meta } from '@storybook/react/types-6-0'; import { Paragraph, Spacer } from '../..'; import { ButtonGroup } from '../ButtonGroup'; +import { DropdownExpandableMenuButton } from './DropdownExpandableMenuButton'; +import { DropdownExpandableMenuItem } from './DropdownExpandableMenuItem'; +import { DropdownExpandableMenuListItem } from './DropdownExpandableMenuListItem'; +import { DropdownExpandableMenuGroup } from './DropdownExpandableMenuGroup'; +import { DropdownExpandableMenuPanel } from './DropdownExpandableMenuPanel'; const Template: Story = args => (
@@ -407,3 +418,146 @@ export const NoItems = args => { ); }; + +export const ExpandableItems = args => { + return ( + + Expandable Items Dropdown + + + + Pasta + + + Fresh + + + Processed + + + + + + Prosciutto + + + + Domestic + + + Speck + + + + + + + ); +}; + +export const ExpandableItemsWithIcons = args => { + return ( + + Expandable Items Dropdown + + + + }> + Longer title area breaking lines within the + DropdownExpandableMenuButton component + + + + Fresh + + + Processed + + + + + }> + Prosciutto + + + + Domestic + + + Speck + + + + + + }>Pizza + + + ); +}; + +export const ExpandableItemsWithIconsAndConsoleWarning = args => { + return ( + + Expandable Items Dropdown + + + + Pasta + + + Fresh + + + Processed + + + + + }> + Prosciutto + + + + Domestic + + + Speck + + + + }> + Pasta + + + + Fresh + + + Processed + + + + + }> + Prosciutto + + + + Domestic + + + Speck + + + + + + + + + }>Pizza + + + ); +}; diff --git a/packages/react-magma-dom/src/components/Dropdown/Dropdown.test.js b/packages/react-magma-dom/src/components/Dropdown/Dropdown.test.js index a5528bb1e..1ae4a3048 100644 --- a/packages/react-magma-dom/src/components/Dropdown/Dropdown.test.js +++ b/packages/react-magma-dom/src/components/Dropdown/Dropdown.test.js @@ -1,19 +1,27 @@ import React from 'react'; import { AsteriskIcon } from 'react-magma-icons'; import { Dropdown } from '.'; -import { DropdownContent } from './DropdownContent'; -import { DropdownDivider } from './DropdownDivider'; -import { DropdownHeader } from './DropdownHeader'; -import { DropdownMenuItem } from './DropdownMenuItem'; -import { DropdownMenuGroup } from './DropdownMenuGroup'; -import { DropdownSplitButton } from './DropdownSplitButton'; -import { DropdownButton } from './DropdownButton'; -import { DropdownMenuNavItem } from './DropdownMenuNavItem'; +import { + DropdownContent, + DropdownDivider, + DropdownHeader, + DropdownMenuItem, + DropdownMenuGroup, + DropdownSplitButton, + DropdownButton, + DropdownMenuNavItem, + DropdownExpandableMenuGroup, + DropdownExpandableMenuItem, + DropdownExpandableMenuListItem, + DropdownExpandableMenuButton, + DropdownExpandableMenuPanel, +} from './'; import { Modal } from '../Modal'; import { magma } from '../../theme/magma'; +import { RestaurantMenuIcon } from 'react-magma-icons'; import { transparentize } from 'polished'; -import { act, render, fireEvent } from '@testing-library/react'; +import { act, render, fireEvent, getByTestId } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; describe('Dropdown', () => { @@ -843,4 +851,443 @@ describe('Dropdown', () => { expect(queryByText('Menu Item 1')).not.toBeInTheDocument(); expect(queryByText('Menu Item 2')).toBeInTheDocument(); }); + + describe('dropdown with expandable menu', () => { + const expandableGroupId = 'expandable group'; + const expandableItemId = 'expandable item'; + const expandableButtonId = 'expandable button'; + const expandablePanelId = 'expandable panel'; + const expandablePanelTwoId = 'expandable panel two'; + + it('should render an expandable menu group', () => { + const { getByTestId } = render( + + Expandable Items Dropdown + + + + + Pasta + + + + + + ); + expect(getByTestId(expandableGroupId)).toBeInTheDocument(); + expect(getByTestId(expandableItemId)).toBeInTheDocument(); + expect(getByTestId(expandableButtonId)).toBeInTheDocument(); + }); + + it('should render an expandable menu group with icons', () => { + const { getByText } = render( + + Expandable Items Dropdown + + + + }> + Pasta + + + + + + ); + expect(getByText('Pasta').querySelector('svg')).toBeInTheDocument(); + }); + + it('should render an expanded panel of menu items when the DropdownExpandableMenuButton is clicked', () => { + const { getByTestId, getByText } = render( + + Expandable Items Dropdown + + + + + Pasta + + + + Fresh + + + Processed + + + + + + + ); + + fireEvent.click(getByText('Pasta')); + + expect(getByTestId(expandablePanelId)).toBeInTheDocument(); + }); + + it('should close an expanded panel of menu items when the DropdownExpandableMenuButton is clicked', () => { + const { getByTestId, getByText } = render( + + Expandable Items Dropdown + + + + + Pasta + + + + Fresh + + + Processed + + + + + + + ); + + fireEvent.click(getByText('Pasta')); + + expect(getByTestId(expandablePanelId)).toBeInTheDocument(); + + fireEvent.click(getByText('Pasta')); + + expect(getByText('Fresh')).not.toBeVisible(); + }); + + it('should have a default expanded item set by the user with defaultIndex', () => { + const { getByTestId, getByText, queryByTestId } = render( + + Expandable Items Dropdown + + + + + Pasta + + + + Fresh + + + Processed + + + + + + + Bacon + + + + Fresh + + + Processed + + + + + + + ); + + fireEvent.click(getByText('Expandable Items Dropdown')); + + expect(getByTestId(expandablePanelId)).toBeInTheDocument(); + + expect(queryByTestId(expandablePanelTwoId)).not.toBeInTheDocument(); + }); + + it('should have multiple open menu items when isMulti is true', () => { + const { getByTestId, getByText } = render( + + Expandable Items Dropdown + + + + + Pasta + + + + Fresh + + + Processed Stuff + + + + + + + Bacon + + + + Fresh + + + Processed + + + + + + + ); + + fireEvent.click(getByText('Expandable Items Dropdown')); + + fireEvent.click(getByText('Pasta')); + + expect(getByTestId(expandablePanelId)).toBeInTheDocument(); + + fireEvent.click(getByText('Bacon')); + + expect(getByTestId(expandablePanelId)).toBeInTheDocument(); + expect(getByTestId(expandablePanelTwoId)).toBeInTheDocument(); + }); + + it('should only allow one open menu item when isMulti is false', () => { + const { getByTestId, getByText, queryByTestId } = render( + + Expandable Items Dropdown + + + + + Pasta + + + + Fresh + + + Processed Stuff + + + + + + + Bacon + + + + Fresh + + + Processed + + + + + + + ); + + fireEvent.click(getByText('Expandable Items Dropdown')); + + fireEvent.click(getByText('Pasta')); + + expect(getByTestId(expandablePanelId)).toBeInTheDocument(); + + expect(queryByTestId(expandablePanelTwoId)).not.toBeInTheDocument(); + + fireEvent.click(getByText('Bacon')); + + expect(getByTestId(expandablePanelTwoId)).toBeInTheDocument(); + + expect(queryByTestId(expandablePanelId)).not.toBeVisible(); + }); + + describe('dropdown with expandable menu styling', () => { + it(`DropdownExpandableMenuPanel items should have additional padding if DropdownExpandableMenuButton has an icon`, () => { + const { getByText } = render( + + Expandable Items Dropdown + + + + }> + Pasta + + + + Fresh + + + Processed + + + + + + + ); + fireEvent.click(getByText('Pasta')); + + expect(getByText('Fresh')).toHaveStyleRule( + 'padding', + `${magma.spaceScale.spacing03} ${magma.spaceScale.spacing05} ${magma.spaceScale.spacing03} 72px` + ); + }); + + it(`DropdownExpandableMenuPanel items should have standard padding if DropdownExpandableMenuButton doesn't have an icon`, () => { + const { getByText } = render( + + Expandable Items Dropdown + + + + + Pasta + + + + Fresh + + + Processed + + + + + + + ); + fireEvent.click(getByText('Pasta')); + + expect(getByText('Fresh')).toHaveStyleRule( + 'padding', + `${magma.spaceScale.spacing03} ${magma.spaceScale.spacing05} ${magma.spaceScale.spacing03} ${magma.spaceScale.spacing08}` + ); + }); + + it(`DropdownExpandableMenuPanel items should have additional padding if DropdownExpandableMenuButton has an icon and a text only menu item`, () => { + const { getByTestId, getByText } = render( + + Expandable Items Dropdown + + + + + Pasta + + + + Fresh + + + Processed + + + + + } + testId={`${expandableButtonId}-2`} + > + Prosciutto + + + + Domestic + + + Speck + + + + + + + ); + fireEvent.click(getByText('Pasta')); + fireEvent.click(getByText('Prosciutto')); + + expect(getByTestId(expandableButtonId)).toHaveStyleRule( + 'padding', + `${magma.spaceScale.spacing03} ${magma.spaceScale.spacing05} ${magma.spaceScale.spacing03} ${magma.spaceScale.spacing11}` + ); + + expect(getByTestId(`${expandableButtonId}-2`)).toHaveStyleRule( + 'padding', + `${magma.spaceScale.spacing03} ${magma.spaceScale.spacing05}` + ); + + expect(getByText('Fresh')).toHaveStyleRule( + 'padding', + `${magma.spaceScale.spacing03} ${magma.spaceScale.spacing05} ${magma.spaceScale.spacing03} 72px` + ); + expect(getByText('Domestic')).toHaveStyleRule( + 'padding', + `${magma.spaceScale.spacing03} ${magma.spaceScale.spacing05} ${magma.spaceScale.spacing03} 72px` + ); + }); + + it('should fire the customOnKeyDown function if used', () => { + const onChangeMock = jest.fn(); + const { getByTestId } = render( + + Expandable Items Dropdown + + + + + Pasta + + + + Fresh + + + Processed + + + + + + + ); + + const dropdownExpandableButton = getByTestId(expandableButtonId); + + fireEvent.keyDown(dropdownExpandableButton, { key: 'Enter' }); + + expect(onChangeMock).toHaveBeenCalledTimes(1); + }); + + it(`should support isInverse mode`, () => { + const { getByTestId } = render( + + Expandable Items Dropdown + + + + + ); + + expect(getByTestId(expandableGroupId)).toHaveStyleRule( + 'background', + 'transparent' + ); + expect(getByTestId(expandableGroupId)).toHaveStyleRule( + 'color', + magma.colors.neutral100 + ); + }); + }); + }); }); diff --git a/packages/react-magma-dom/src/components/Dropdown/DropdownContent.tsx b/packages/react-magma-dom/src/components/Dropdown/DropdownContent.tsx index 89abaff7b..d3e4df9a4 100644 --- a/packages/react-magma-dom/src/components/Dropdown/DropdownContent.tsx +++ b/packages/react-magma-dom/src/components/Dropdown/DropdownContent.tsx @@ -117,14 +117,20 @@ export const DropdownContent = React.forwardRef< let hasItemChildren = false; + // For Expandable Dropdowns that don't require a max-height + let hasExpandableItems = false; + React.Children.forEach(children, (child: any) => { if ( child?.type?.displayName === 'DropdownMenuItem' || child?.type?.displayName === 'DropdownMenuGroup' ) { hasItemChildren = true; - return; } + if (child.type?.displayName === 'DropdownExpandableMenuGroup') { + hasExpandableItems = true; + } + return; }); return ( @@ -137,6 +143,11 @@ export const DropdownContent = React.forwardRef< isOpen={context.isOpen} maxHeight={context.maxHeight} ref={ref} + style={ + hasExpandableItems + ? { maxHeight: 'inherit', overflow: 'hidden' } + : props.style + } tabIndex={-1} testId={testId || 'dropdownContent'} theme={theme} diff --git a/packages/react-magma-dom/src/components/Dropdown/DropdownExpandableMenuButton.tsx b/packages/react-magma-dom/src/components/Dropdown/DropdownExpandableMenuButton.tsx new file mode 100644 index 000000000..5aacb6455 --- /dev/null +++ b/packages/react-magma-dom/src/components/Dropdown/DropdownExpandableMenuButton.tsx @@ -0,0 +1,96 @@ +import * as React from 'react'; +import styled from '../../theme/styled'; +import { AccordionButton, AccordionButtonProps } from '../Accordion'; +import { IconWrapper, menuBackground } from './DropdownMenuItem'; +import { IconProps } from 'react-magma-icons'; +import { ThemeContext } from '../../theme/ThemeContext'; +import { DropdownContext } from './Dropdown'; +import { DropdownExpandableMenuGroupContext } from './DropdownExpandableMenuGroup'; +import { useForkedRef } from '../../utils'; + +export interface DropdownExpandableMenuButtonProps + extends AccordionButtonProps { + disabled?: boolean; + icon?: React.ReactElement; + testId?: string; +} + +const StyledAccordionButton = styled(AccordionButton)<{ + disabled?: boolean; + expandableMenuButtonHasIcon?: boolean; + icon?: React.ReactElement; +}>` + font-weight: 400; + overflow-wrap: anywhere; + padding: ${props => + !props.icon && props.expandableMenuButtonHasIcon + ? `${props.theme.spaceScale.spacing03} ${props.theme.spaceScale.spacing05} ${props.theme.spaceScale.spacing03} ${props.theme.spaceScale.spacing11}` + : `${props.theme.spaceScale.spacing03} ${props.theme.spaceScale.spacing05}`}; + margin: 0; + border-top: 0; + &:hover, + &:focus { + background: ${menuBackground}; + } + > span { + display: flex; + } +`; + +const StyledIconWrapper = styled(IconWrapper)` + justify-content: center; +`; + +export const DropdownExpandableMenuButton = React.forwardRef< + HTMLDivElement, + DropdownExpandableMenuButtonProps +>((props, forwardedRef) => { + const { children, disabled, customOnKeyDown, icon, testId, ...other } = props; + + const theme = React.useContext(ThemeContext); + const context = React.useContext(DropdownContext); + const expandableMenuGroupContext = React.useContext( + DropdownExpandableMenuGroupContext + ); + + const ownRef = React.useRef(); + const ref = useForkedRef(forwardedRef, ownRef); + + React.useEffect(() => { + if (!disabled) { + context.registerDropdownMenuItem(context.itemRefArray, ownRef); + } + }, []); + + //Allows a custom function to be called when a key is pressed that overrides the default AccordionButton onKeyDown event. + function handleCustomOnKeyDown() { + if (customOnKeyDown && typeof customOnKeyDown === 'function') { + return customOnKeyDown(); + } + } + + return ( + + {icon && ( + + {icon} + + )} + {children} + + ); +}); + +DropdownExpandableMenuButton.displayName = 'DropdownExpandableMenuButton'; diff --git a/packages/react-magma-dom/src/components/Dropdown/DropdownExpandableMenuGroup.tsx b/packages/react-magma-dom/src/components/Dropdown/DropdownExpandableMenuGroup.tsx new file mode 100644 index 000000000..0f91cc79a --- /dev/null +++ b/packages/react-magma-dom/src/components/Dropdown/DropdownExpandableMenuGroup.tsx @@ -0,0 +1,62 @@ +import * as React from 'react'; +import styled from '../../theme/styled'; +import { Accordion, AccordionProps } from '../Accordion'; +import { DropdownContext } from './Dropdown'; + +const StyledAccordion = styled(Accordion)` + border: none; +`; + +export interface DropdownExpandableMenuGroupContextInterface { + expandableMenuButtonHasIcon?: boolean; + isExpandablePanel?: boolean; +} + +export const DropdownExpandableMenuGroupContext = + React.createContext({}); + +export const DropdownExpandableMenuGroup = React.forwardRef< + HTMLDivElement, + AccordionProps +>((props, ref) => { + const { children, testId, ...other } = props; + + const context = React.useContext(DropdownContext); + + let expandableMenuButtonHasIcon = false; + let isExpandablePanel = false; + + React.Children.forEach(children, (child: any) => { + if (child.type?.displayName === 'DropdownExpandableMenuItem') { + React.Children.forEach(child.props.children, (c: any) => { + if (c.type?.displayName === 'DropdownExpandableMenuButton') { + if (c.props.icon) { + expandableMenuButtonHasIcon = true; + return; + } + } + }); + if (React.isValidElement(child)) { + isExpandablePanel = true; + } + } + }); + + return ( + + + {children} + + + ); +}); + +DropdownExpandableMenuGroup.displayName = 'DropdownExpandableMenuGroup'; diff --git a/packages/react-magma-dom/src/components/Dropdown/DropdownExpandableMenuItem.tsx b/packages/react-magma-dom/src/components/Dropdown/DropdownExpandableMenuItem.tsx new file mode 100644 index 000000000..47dce0c8b --- /dev/null +++ b/packages/react-magma-dom/src/components/Dropdown/DropdownExpandableMenuItem.tsx @@ -0,0 +1,53 @@ +import * as React from 'react'; +import { IconProps } from 'react-magma-icons'; +import { AccordionItem, AccordionItemProps } from '../Accordion'; +import { + DropdownExpandableMenuButton, + DropdownExpandableMenuButtonProps, +} from './DropdownExpandableMenuButton'; +export interface DropdownExpandableMenuItemProps extends AccordionItemProps { + /** + * If true, item will be disabled; it will appear dimmed and onClick event (or any other events) will not fire + * @default false + */ + disabled?: boolean; + /** + * Leading icon for the menu item + */ + icon?: React.ReactElement; + /** + * @internal + */ + testId?: string; +} + +export const DropdownExpandableMenuItem = React.forwardRef< + HTMLDivElement, + DropdownExpandableMenuItemProps +>((props, ref) => { + const { children, disabled, testId, ...other } = props; + + const dropdownExpandableMenuItemChildren = React.Children.map( + children, + child => { + const item = child as React.ReactElement< + React.PropsWithChildren + >; + + if (item.type === DropdownExpandableMenuButton) { + if (disabled) { + return React.cloneElement(item, { disabled: true }); + } + } + return child; + } + ); + + return ( + + {dropdownExpandableMenuItemChildren} + + ); +}); + +DropdownExpandableMenuItem.displayName = 'DropdownExpandableMenuItem'; diff --git a/packages/react-magma-dom/src/components/Dropdown/DropdownExpandableMenuListItem.tsx b/packages/react-magma-dom/src/components/Dropdown/DropdownExpandableMenuListItem.tsx new file mode 100644 index 000000000..7e057c5d8 --- /dev/null +++ b/packages/react-magma-dom/src/components/Dropdown/DropdownExpandableMenuListItem.tsx @@ -0,0 +1,62 @@ +import * as React from 'react'; +import styled from '../../theme/styled'; +import { ThemeContext } from '../../theme/ThemeContext'; +import { DropdownContext } from './Dropdown'; +import { Omit, useForkedRef } from '../../utils'; +import { DropdownExpandableMenuGroupContext } from './DropdownExpandableMenuGroup'; +import { DropdownMenuItem, DropdownMenuItemProps } from './DropdownMenuItem'; + +export interface DropdownExpandableMenuListItemProps + extends Omit { + testId?: string; +} + +function menuItemPadding(props) { + if (props.isExpandablePanel) { + if (props.expandableMenuButtonHasIcon) { + return `${props.theme.spaceScale.spacing03} ${props.theme.spaceScale.spacing05} ${props.theme.spaceScale.spacing03} 72px`; + } + return `${props.theme.spaceScale.spacing03} ${props.theme.spaceScale.spacing05} ${props.theme.spaceScale.spacing03} ${props.theme.spaceScale.spacing08}`; + } +} + +const StyledDropdownMenuItem = styled(DropdownMenuItem)<{ + expandableMenuButtonHasIcon?: boolean; + isExpandablePanel?: boolean; +}>` + padding: ${menuItemPadding}; +`; + +export const DropdownExpandableMenuListItem = React.forwardRef< + HTMLDivElement, + DropdownExpandableMenuListItemProps +>((props, forwardedRef) => { + const { children, disabled, ...other } = props; + + const ownRef = React.useRef(); + const theme = React.useContext(ThemeContext); + const context = React.useContext(DropdownContext); + + const menuGroupContext = React.useContext(DropdownExpandableMenuGroupContext); + + const ref = useForkedRef(forwardedRef, ownRef); + + React.useEffect(() => { + if (!disabled) + context.registerDropdownMenuItem(context.itemRefArray, ownRef); + }, []); + + return ( + + {children} + + ); +}); + +DropdownExpandableMenuListItem.displayName = 'DropdownExpandableMenuListItem'; diff --git a/packages/react-magma-dom/src/components/Dropdown/DropdownExpandableMenuPanel.tsx b/packages/react-magma-dom/src/components/Dropdown/DropdownExpandableMenuPanel.tsx new file mode 100644 index 000000000..181d78b11 --- /dev/null +++ b/packages/react-magma-dom/src/components/Dropdown/DropdownExpandableMenuPanel.tsx @@ -0,0 +1,37 @@ +import * as React from 'react'; +import styled from '../../theme/styled'; +import { AccordionPanel, AccordionPanelProps } from '../Accordion'; +import { DropdownExpandableMenuGroup } from './DropdownExpandableMenuGroup'; + +export interface DropdownExpandableMenuPanelProps extends AccordionPanelProps {} + +const StyledAccordionPanel = styled(AccordionPanel)` + padding: 0; +`; + +export const DropdownExpandableMenuPanel = React.forwardRef< + HTMLDivElement, + DropdownExpandableMenuPanelProps +>((props, ref) => { + const { children, testId, ...other } = props; + + React.Children.map(children, child => { + const item = child as React.ReactElement; + + if (item.type === DropdownExpandableMenuGroup) { + console.warn( + ` + React Magma Warning: Only one group level is supported for Expandable Dropdowns, anything nested two levels or more isn't accounted for in the styling + ` + ); + } + }); + + return ( + + {children} + + ); +}); + +DropdownExpandableMenuPanel.displayName = 'DropdownExpandableMenuPanel'; diff --git a/packages/react-magma-dom/src/components/Dropdown/DropdownMenuItem.tsx b/packages/react-magma-dom/src/components/Dropdown/DropdownMenuItem.tsx index 6e77231b9..5b3160c43 100644 --- a/packages/react-magma-dom/src/components/Dropdown/DropdownMenuItem.tsx +++ b/packages/react-magma-dom/src/components/Dropdown/DropdownMenuItem.tsx @@ -60,6 +60,14 @@ export function menuBackground(props) { return props.theme.colors.neutral200; } +function menuItemPadding(props) { + if (props.isInactive) { + return `${props.theme.spaceScale.spacing03} ${props.theme.spaceScale.spacing05} ${props.theme.spaceScale.spacing03} ${props.theme.spaceScale.spacing11}`; + } else { + return `${props.theme.spaceScale.spacing03} ${props.theme.spaceScale.spacing05}`; + } +} + export const MenuItemStyles = props => { return css` align-items: center; @@ -70,9 +78,7 @@ export const MenuItemStyles = props => { font-family: ${props.theme.bodyFont}; line-height: ${props.theme.typeScale.size03.lineHeight}; margin: 0; - padding: ${props.isInactive - ? `${props.theme.spaceScale.spacing03} ${props.theme.spaceScale.spacing05} ${props.theme.spaceScale.spacing03} ${props.theme.spaceScale.spacing11}` - : `${props.theme.spaceScale.spacing03} ${props.theme.spaceScale.spacing05}`}; + padding: ${menuItemPadding(props)}; white-space: ${props.isFixedWidth ? 'normal' : 'nowrap'}; &:hover, diff --git a/packages/react-magma-dom/src/components/Dropdown/index.tsx b/packages/react-magma-dom/src/components/Dropdown/index.tsx index 6f43398e9..50dd9baf6 100644 --- a/packages/react-magma-dom/src/components/Dropdown/index.tsx +++ b/packages/react-magma-dom/src/components/Dropdown/index.tsx @@ -5,4 +5,10 @@ export * from './DropdownDivider'; export * from './DropdownHeader'; export * from './DropdownMenuGroup'; export * from './DropdownMenuItem'; +export * from './DropdownMenuNavItem'; export * from './DropdownSplitButton'; +export * from './DropdownExpandableMenuButton'; +export * from './DropdownExpandableMenuGroup'; +export * from './DropdownExpandableMenuPanel'; +export * from './DropdownExpandableMenuItem'; +export * from './DropdownExpandableMenuListItem'; diff --git a/packages/react-magma-dom/src/index.ts b/packages/react-magma-dom/src/index.ts index 6a33d212f..8999061a8 100644 --- a/packages/react-magma-dom/src/index.ts +++ b/packages/react-magma-dom/src/index.ts @@ -86,6 +86,23 @@ export { DropdownButton, DropdownButtonProps, } from './components/Dropdown/DropdownButton'; +export { DropdownExpandableMenuGroup } from './components/Dropdown/DropdownExpandableMenuGroup'; +export { + DropdownExpandableMenuItem, + DropdownExpandableMenuItemProps, +} from './components/Dropdown/DropdownExpandableMenuItem'; +export { + DropdownExpandableMenuListItem, + DropdownExpandableMenuListItemProps, +} from './components/Dropdown/DropdownExpandableMenuListItem'; +export { + DropdownExpandableMenuButton, + DropdownExpandableMenuButtonProps, +} from './components/Dropdown/DropdownExpandableMenuButton'; +export { + DropdownExpandableMenuPanel, + DropdownExpandableMenuPanelProps, +} from './components/Dropdown/DropdownExpandableMenuPanel'; export * from './components/Flex'; export { Form, FormProps } from './components/Form'; export { FormGroup, FormGroupProps } from './components/FormGroup'; diff --git a/website/react-magma-docs/src/pages/api/dropdown.mdx b/website/react-magma-docs/src/pages/api/dropdown.mdx index a952af179..3076c50dc 100644 --- a/website/react-magma-docs/src/pages/api/dropdown.mdx +++ b/website/react-magma-docs/src/pages/api/dropdown.mdx @@ -10,6 +10,10 @@ props: - DropdownMenuNavItemProps - DropdownButtonProps - DropdownSplitButtonProps + - DropdownExpandableMenuItemProps + - DropdownExpandableMenuListItemProps + - DropdownExpandableMenuButtonProps + - DropdownExpandableMenuPanelProps --- import { LeadParagraph } from '../../components/LeadParagraph'; @@ -353,6 +357,130 @@ export function Example() { } ``` +## Expandable Menu Items + +If a nested menu layout is needed within the `Dropdown`, the `DropdownExpandableMenuGroup` adds an expandable / collapsible menu structure. + +The components of `DropdownExpandableMenuGroup`, `DropdownExpandableMenuItem`, `DropdownExpandableMenuButton`, `DropdownExpandableMenuPanel`, and `DropdownExpandableMenuListItem` necessitate the proper structure of this menu list. + +Please note that only one child panel is supported from a styling standpoint. + +```tsx +import React from 'react'; +import { + Dropdown, + DropdownButton, + DropdownContent, + DropdownExpandableMenuGroup, + DropdownExpandableMenuItem, + DropdownExpandableMenuButton, + DropdownExpandableMenuPanel, + DropdownExpandableMenuListItem, +} from 'react-magma-dom'; + +import { + RestaurantMenuIcon, + LunchDiningIcon, + LocalPizzaIcon, +} from 'react-magma-icons'; + +export function Example() { + return ( + + Expandable Items Dropdown + + + + }> + Pasta + + + + Fresh + + + Processed + + + + + }> + Prosciutto + + + + Domestic + + + Speck + + + + + + }> + Pizza + + + + ); +} +``` + +### Text only Expandable Menu Item + +```tsx +import React from 'react'; +import { + Dropdown, + DropdownButton, + DropdownContent, + DropdownExpandableMenuGroup, + DropdownExpandableMenuItem, + DropdownExpandableMenuButton, + DropdownExpandableMenuPanel, + DropdownExpandableMenuListItem, +} from 'react-magma-dom'; + +export function Example() { + return ( + + Expandable Items Dropdown + + + + Pasta + + + Fresh + + + Processed + + + + + + Prosciutto + + + + Domestic + + + Speck + + + + + + Pizza + + + ); +} +``` + ## Custom DropdownMenuItem Wrappers `DropdownContent` expects its children to be of type `DropdownMenuItem`, be an element in which its lowest child is of type `DropdownMenuItem`, or a custom component @@ -558,7 +686,6 @@ export function Example() { ); } - ``` ### Dropdown with Informational Content @@ -916,4 +1043,28 @@ The DropdownButton can also accept an `icon` property, as in the