Skip to content

Commit

Permalink
Fixed branch coverage
Browse files Browse the repository at this point in the history
  • Loading branch information
Dantemss committed Jan 23, 2024
1 parent a55b993 commit d65bb54
Show file tree
Hide file tree
Showing 5 changed files with 276 additions and 43 deletions.
67 changes: 48 additions & 19 deletions src/components/DropdownMenu.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,55 +4,84 @@ import renderer from 'react-test-renderer';
import { DropdownMenu, DropdownMenuContext, DropdownMenuItemButton } from './DropdownMenu';

describe('DropdownMenu', () => {
const TestMenu = () => (
const TestMenu = (props: { disabled?: boolean; variant: 'light' | 'primary' | 'secondary'; width?: string }) => (
<DropdownMenu
disabled={props.disabled}
id='test-menu'
text='Test Menu'
variant='light'
variant={props.variant}
width={props.width}
>
<DropdownMenuItemButton onClick={jest.fn()}>Test Menu Item 1</DropdownMenuItemButton>
<DropdownMenuItemButton onClick={jest.fn()}>Test Menu Item 2</DropdownMenuItemButton>
</DropdownMenu>
);

it('should open on a button click and close on an item click or outside click', () => {
const component = renderer.create(<TestMenu/>);
it('should open on a button click and close on another button click', () => {
const component = renderer.create(<TestMenu variant='light'/>);
expect(component.toJSON()).toMatchSnapshot();

component.root.findByProps({ 'aria-controls': 'test-menu' }).props.onClick();
expect(component.toJSON()).toMatchSnapshot();
component.root.findAllByProps({ role: 'menuitem' })[0].props.onClick();
component.root.findByProps({ 'aria-controls': 'test-menu' }).props.onClick();
expect(component.toJSON()).toMatchSnapshot();
});

it('should open on a button click and close on an item click', () => {
const component = renderer.create(<TestMenu variant='light'/>);
expect(component.toJSON()).toMatchSnapshot();

component.root.findByProps({ 'aria-controls': 'test-menu' }).props.onClick();
expect(component.toJSON()).toMatchSnapshot();
window.dispatchEvent(new MouseEvent('click'))
component.root.findAllByProps({ role: 'menuitem' })[0].props.onClick();
expect(component.toJSON()).toMatchSnapshot();
});

it('should open on a button click and close on an outside click', () => {
const { asFragment, getByText } = render(<TestMenu variant='primary'/>);
expect(asFragment()).toMatchSnapshot();

const menuButton = getByText('Test Menu');
menuButton.click();
expect(asFragment()).toMatchSnapshot();
getByText('Test Menu Item 1').parentElement?.click();
expect(asFragment()).toMatchSnapshot();
menuButton.parentElement?.parentElement?.click();
expect(asFragment()).toMatchSnapshot();
});

it('should handle keyboard events', () => {
const { asFragment, getByText } = render(<TestMenu/>);
const { asFragment, getByText } = render(<TestMenu variant='secondary' width='10rem'/>);
const menuButton = getByText('Test Menu');
expect(asFragment()).toMatchSnapshot();

fireEvent.keyDown(menuButton, { key: 'ArrowDown' });
fireEvent.keyDown(menuButton, { key: 'ArrowUp' });
expect(asFragment()).toMatchSnapshot();

const menuItem = getByText('Test Menu Item 1');
fireEvent.keyDown(menuItem, { key: 'ArrowUp' });
fireEvent.keyDown(menuItem, { key: 'ArrowDown' });
fireEvent.keyDown(menuItem, { key: 'End' });
fireEvent.keyDown(menuItem, { key: 'Home' });
fireEvent.keyDown(menuItem, { key: 'ArrowDown' });
fireEvent.keyDown(menuItem, { key: 'ArrowUp' });
fireEvent.keyDown(menuItem, { key: 't' });
fireEvent.keyDown(menuItem, { key: 'T' });
fireEvent.keyDown(menuItem, { key: 'Escape' });
const menuItem1 = getByText('Test Menu Item 1');
const menuItem2 = getByText('Test Menu Item 2');
fireEvent.keyDown(menuItem2, { key: 'ArrowDown' });
fireEvent.keyDown(menuItem1, { key: 'ArrowUp' });
fireEvent.keyDown(menuItem2, { key: 'Home' });
fireEvent.keyDown(menuItem1, { key: 'End' });
fireEvent.keyDown(menuItem2, { key: 'ArrowUp' });
fireEvent.keyDown(menuItem1, { key: 'ArrowDown' });
fireEvent.keyDown(menuItem2, { key: 't' });
fireEvent.keyDown(menuItem1, { key: 'T' });
fireEvent.keyDown(menuItem2, { key: 'Escape' });
expect(asFragment()).toMatchSnapshot();

fireEvent.keyDown(menuButton, { key: 'ArrowUp' });
fireEvent.keyDown(menuButton, { key: 'ArrowDown' });
expect(asFragment()).toMatchSnapshot();
});

it('should not open when disabled', () => {
const component = renderer.create(<TestMenu disabled={true} variant='light'/>);
expect(component.toJSON()).toMatchSnapshot();

component.root.findByProps({ 'aria-controls': 'test-menu' }).props.onClick();
expect(component.toJSON()).toMatchSnapshot();
});
});

describe('DropdownMenuContext', () => {
Expand Down
35 changes: 21 additions & 14 deletions src/components/DropdownMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import React from 'react';
import styled from 'styled-components';
import { ButtonVariant, applyButtonVariantStyles } from '../theme/buttons';
import { palette } from '../theme/palette';
import { assertNotNull, focusElement } from '../utils';

const StyledDropdownMenu = styled.div`
position: relative;
Expand Down Expand Up @@ -153,10 +154,6 @@ const DropdownMenuButton = ({
</StyledDropdownMenuButton>;
};

const focus = (element?: Element | null) => {
if (element instanceof HTMLElement) { element.focus(); }
};

const DropdownMenuItemContainer = ({
children, ...divProps
}: React.PropsWithChildren<React.HTMLAttributes<HTMLDivElement>>) => {
Expand All @@ -166,7 +163,9 @@ const DropdownMenuItemContainer = ({

React.useEffect(() => {
// Focus the first or last child when opened
focus(openFocus === 'first' ? ref.current?.firstElementChild : ref.current?.lastElementChild);
if (ref.current) {
focusElement(openFocus === 'first' ? ref.current.firstElementChild : ref.current.lastElementChild);
}
}, [openFocus, ref]);

return <StyledDropdownMenuItemContainer {...divProps} ref={ref} role='menu'>
Expand Down Expand Up @@ -223,8 +222,12 @@ export const DropdownMenu = ({
</StyledDropdownMenu>;
};

const firstSibling = (element: Element) => element.parentElement?.firstElementChild;
const lastSibling = (element: Element) => element.parentElement?.lastElementChild;
const firstSibling = (element: Element) => assertNotNull(
assertNotNull(element.parentElement, 'menuItem has no parent').firstElementChild, 'menuItemContainer is empty'
);
const lastSibling = (element: Element) => assertNotNull(
assertNotNull(element.parentElement, 'menuItem has no parent').lastElementChild, 'menuItemContainer is empty'
);
const nextWithWraparound = (element: Element) => element.nextElementSibling ?? firstSibling(element);
const previousWithWraparound = (element: Element) => element.previousElementSibling ?? lastSibling(element);

Expand All @@ -244,31 +247,35 @@ export const DropdownMenuItemButton = ({
switch (event.key) {
case 'Escape':
closeMenu();
focus(event.currentTarget.parentElement?.parentElement?.firstElementChild);
focusElement(assertNotNull(
assertNotNull(event.currentTarget.parentElement, 'menuItem has no parent').parentElement,
'menuItemContainer has no parent'
).firstElementChild);
break;
case 'ArrowUp':
focus(previousWithWraparound(event.currentTarget));
focusElement(previousWithWraparound(event.currentTarget));
event.preventDefault();
break;
case 'ArrowDown':
focus(nextWithWraparound(event.currentTarget));
focusElement(nextWithWraparound(event.currentTarget));
event.preventDefault();
break;
case 'Home':
focus(firstSibling(event.currentTarget));
focusElement(firstSibling(event.currentTarget));
event.preventDefault();
break;
case 'End':
focus(lastSibling(event.currentTarget));
focusElement(lastSibling(event.currentTarget));
event.preventDefault();
break;
default:
if (/^[A-Za-z]$/.test(event.key)) {
for (let element: Element | null | undefined = nextWithWraparound(event.currentTarget);
element !== event.currentTarget && element instanceof HTMLElement;
element = nextWithWraparound(element)) {
if (element.textContent?.toLowerCase()?.startsWith(event.key.toLowerCase())) {
focus(element);
const textContent = assertNotNull(element.textContent, 'menuItem has no textContent');
if (textContent.toLowerCase().startsWith(event.key.toLowerCase())) {
focusElement(element);
break;
}
}
Expand Down
Loading

0 comments on commit d65bb54

Please sign in to comment.