Skip to content

Commit

Permalink
feat(Menu): add submenu
Browse files Browse the repository at this point in the history
  • Loading branch information
maxime-gendron committed Nov 26, 2021
1 parent 83a692f commit 36a3c1f
Show file tree
Hide file tree
Showing 11 changed files with 455 additions and 92 deletions.
2 changes: 2 additions & 0 deletions packages/react/src/components/buttons/button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,3 +54,5 @@ export const Button = forwardRef(({
</StyledButton>
);
});

Button.displayName = 'Button';
Original file line number Diff line number Diff line change
Expand Up @@ -71,17 +71,17 @@ describe('MenuButton', () => {
expect(getByTestId(wrapper, 'menu').exists()).toBe(false);
});

describe('caret icon', () => {
describe('chevron icon', () => {
it('should point downwards when menu is not open', () => {
const wrapper = mountWithTheme(<MenuButton buttonType="primary" options={options} />);

expect(getByTestId(wrapper, 'caret-icon').prop('name')).toBe('chevronDown');
expect(getByTestId(wrapper, 'chevron-icon').prop('name')).toBe('chevronDown');
});

it('should point upwards when menu is open', () => {
const wrapper = mountWithTheme(<MenuButton buttonType="primary" defaultOpen options={options} />);

expect(getByTestId(wrapper, 'caret-icon').prop('name')).toBe('chevronUp');
expect(getByTestId(wrapper, 'chevron-icon').prop('name')).toBe('chevronUp');
});
});
});
12 changes: 6 additions & 6 deletions packages/react/src/components/menu-button/menu-button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@ import { usePopperTooltip } from 'react-popper-tooltip';
import { Menu, MenuOption } from '../menu/menu';
import { Button, ButtonType } from '../buttons/button';
import { Icon } from '../icon/icon';
import { menuDimensions } from '../../tokens/menuDimensions';

const StyledMenu = styled(Menu)`
max-width: 350px;
min-width: 200px;
width: initial;
max-width: ${menuDimensions.maxWidth};
min-width: ${menuDimensions.minWidth};
`;

const StyledIcon = styled(Icon)`
Expand Down Expand Up @@ -50,7 +50,7 @@ export const MenuButton: FunctionComponent<Props> = ({ buttonType, defaultOpen,
}

return (
<div className="App">
<div>
<Button
data-testid="menu-button"
type="button"
Expand All @@ -62,8 +62,8 @@ export const MenuButton: FunctionComponent<Props> = ({ buttonType, defaultOpen,
>
Trigger
<StyledIcon
ria-hidden="true"
data-testid="caret-icon"
aria-hidden="true"
data-testid="chevron-icon"
name={visible ? 'chevronUp' : 'chevronDown'}
size="16"
/>
Expand Down
150 changes: 132 additions & 18 deletions packages/react/src/components/menu/menu.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,27 @@ const options = [
},
];

const optionsWithSubMenu = [
{
label: 'Mango',
onClick: jest.fn(),
options,
},
{
label: 'Pineapple',
onClick: jest.fn(),
},
{
label: 'Lime',
onClick: jest.fn(),
},
];

function expectFocusToBeOn(element: ReactWrapper): void {
expect(document.activeElement).toBe(element.getDOMNode());
}

describe('Menu', () => {
beforeAll(() => {
window.HTMLElement.prototype.scrollIntoView = jest.fn();
});

it('should call onClick callback when option is clicked', () => {
const wrapper = mountWithTheme(<Menu options={options} />);

Expand All @@ -55,73 +67,175 @@ describe('Menu', () => {
expect(callback).toHaveBeenCalledTimes(1);
});

it('should open subMenu when option is clicked given option as subMenu', () => {
const wrapper = mountWithTheme(<Menu options={optionsWithSubMenu} />);

getByTestId(wrapper, 'menu-option-0').simulate('click');

expect(getByTestId(wrapper, 'menu-option-0-sub-menu').exists()).toBe(true);
});

it('should open subMenu when ArrowRight key is pressed given option as subMenu', () => {
const wrapper = mountWithTheme(<Menu options={optionsWithSubMenu} initialFocusIndex={0} />);

getByTestId(wrapper, 'menu').simulate('keydown', { key: 'ArrowRight' });

expect(getByTestId(wrapper, 'menu-option-0-sub-menu').exists()).toBe(true);
});

it('should open subMenu when mouse enters given option as subMenu', () => {
const wrapper = mountWithTheme(<Menu options={optionsWithSubMenu} />);

getByTestId(wrapper, 'menu-option-0').simulate('mouseEnter');

expect(getByTestId(wrapper, 'menu-option-0-sub-menu').exists()).toBe(true);
});

it('should collapse subMenu when mouse leaves given option as subMenu', () => {
const wrapper = mountWithTheme(<Menu options={optionsWithSubMenu} />);

getByTestId(wrapper, 'menu-option-0').simulate('mouseEnter');
getByTestId(wrapper, 'menu-option-0').simulate('mouseLeave');

expect(getByTestId(wrapper, 'menu-option-0-sub-menu').exists()).toBe(false);
});

it('subMenu should stay open when mouse enters', () => {
const wrapper = mountWithTheme(<Menu options={optionsWithSubMenu} />);

getByTestId(wrapper, 'menu-option-0').simulate('mouseEnter');
getByTestId(wrapper, 'menu-option-0-sub-menu').simulate('mouseEnter');

expect(getByTestId(wrapper, 'menu-option-0-sub-menu').exists()).toBe(true);
});

it('subMenu should close when mouse leaves', () => {
const wrapper = mountWithTheme(<Menu options={optionsWithSubMenu} />);

getByTestId(wrapper, 'menu-option-0').simulate('mouseEnter');
getByTestId(wrapper, 'menu-option-0-sub-menu').simulate('mouseLeave');

expect(getByTestId(wrapper, 'menu-option-0-sub-menu').exists()).toBe(false);
});

it('should collapse subMenu when ArrowLeft key is pressed inside subMenu', () => {
const wrapper = mountWithTheme(<Menu options={optionsWithSubMenu} initialFocusIndex={0} />);

getByTestId(wrapper, 'menu').simulate('keydown', { key: 'ArrowRight' });
getByTestId(wrapper, 'menu-option-0-sub-menu').simulate('keydown', { key: 'ArrowLeft' });

expect(getByTestId(wrapper, 'menu-option-0-sub-menu').exists()).toBe(false);
});

describe('focus', () => {
const divElement = document.createElement('div');

beforeAll(() => {
document.body.appendChild(divElement);
});

afterEach(() => {
ReactDOM.unmountComponentAtNode(document.body);
ReactDOM.unmountComponentAtNode(divElement);
});

it('should be on first option when initialFocus is set to 0', () => {
it('should be on the first option when initialFocus is set to 0', () => {
const wrapper = mountWithTheme(
<Menu options={options} initialFocusIndex={0} />,
{ attachTo: document.body },
<div id="root">
<Menu options={options} initialFocusIndex={0} />
</div>,
{ attachTo: divElement },
);

expectFocusToBeOn(getByTestId(wrapper, 'menu-option-0'));
});

it('should go to next option when ArrowDown key is pressed', () => {
it('should be on the next option when ArrowDown key is pressed', () => {
const wrapper = mountWithTheme(
<Menu options={options} initialFocusIndex={0} />,
{ attachTo: document.body },
{ attachTo: divElement },
);

getByTestId(wrapper, 'menu').simulate('keydown', { key: 'ArrowDown' });

expectFocusToBeOn(getByTestId(wrapper, 'menu-option-1'));
});

it('should go to the first option when ArrowDown key is pressed on last option', () => {
it('should be on the first option when ArrowDown key is pressed on last option', () => {
const wrapper = mountWithTheme(
<Menu options={options} initialFocusIndex={options.length - 1} />,
{ attachTo: document.body },
{ attachTo: divElement },
);

getByTestId(wrapper, 'menu').simulate('keydown', { key: 'ArrowDown' });

expectFocusToBeOn(getByTestId(wrapper, 'menu-option-0'));
});

it('should go to the previous option when ArrowUp key is pressed', () => {
it('should be on the previous option when ArrowUp key is pressed', () => {
const wrapper = mountWithTheme(
<Menu options={options} initialFocusIndex={1} />,
{ attachTo: document.body },
{ attachTo: divElement },
);

getByTestId(wrapper, 'menu').simulate('keydown', { key: 'ArrowUp' });

expectFocusToBeOn(getByTestId(wrapper, `menu-option-${0}`));
});

it('should go to the last option when ArrowUp key is pressed on first option', () => {
it('should be on the last option when ArrowUp key is pressed on first option', () => {
const wrapper = mountWithTheme(
<Menu options={options} initialFocusIndex={0} />,
{ attachTo: document.body },
{ attachTo: divElement },
);

getByTestId(wrapper, 'menu').simulate('keydown', { key: 'ArrowUp' });

expectFocusToBeOn(getByTestId(wrapper, `menu-option-${options.length - 1}`));
});

it('should focus the first option starting with typed character', () => {
it('should be on the first option starting with typed character', () => {
const wrapper = mountWithTheme(
<Menu options={options} initialFocusIndex={0} />,
{ attachTo: document.body },
{ attachTo: divElement },
);

getByTestId(wrapper, 'menu').simulate('keydown', { key: 'l' });

expectFocusToBeOn(getByTestId(wrapper, 'menu-option-2'));
});

it('should be on the first element of subMenu when ArrowRight key is pressed given option as subMenu', () => {
const wrapper = mountWithTheme(
<Menu options={optionsWithSubMenu} initialFocusIndex={0} />,
{ attachTo: divElement },
);

getByTestId(wrapper, 'menu').simulate('keydown', { key: 'ArrowRight' });

expectFocusToBeOn(getByTestId(wrapper, 'sub-menu-option-0'));
});

it('should be on the subMenu parent option when ArrowLeft key is pressed inside subMenu', () => {
const wrapper = mountWithTheme(
<Menu options={optionsWithSubMenu} initialFocusIndex={0} />,
{ attachTo: divElement },
);

getByTestId(wrapper, 'menu').simulate('keydown', { key: 'ArrowRight' });
getByTestId(wrapper, 'menu-option-0-sub-menu').simulate('keydown', { key: 'ArrowLeft' });

expectFocusToBeOn(getByTestId(wrapper, 'menu-option-0'));
});

it('should stay inside the menu when the subMenu is open by hovering with the mouse', () => {
const wrapper = mountWithTheme(
<Menu options={optionsWithSubMenu} initialFocusIndex={0} />,
{ attachTo: divElement },
);

getByTestId(wrapper, 'menu-option-0').simulate('mouseEnter');

expectFocusToBeOn(getByTestId(wrapper, 'menu-option-0'));
});
});
});
Loading

0 comments on commit 36a3c1f

Please sign in to comment.