Skip to content

Commit

Permalink
feat(Dropdown): Added simple template (#10308)
Browse files Browse the repository at this point in the history
* feat(Dropdown): Added simple template

* Added tests

* Added imports to example file

* Additional fixes for docs fail

* Updated import name

* Added additional tests
  • Loading branch information
thatblindgeye authored and kmcfaul committed Jun 27, 2024
1 parent ed62ec2 commit 47dcd5a
Show file tree
Hide file tree
Showing 7 changed files with 614 additions and 0 deletions.
122 changes: 122 additions & 0 deletions packages/react-templates/src/components/Dropdown/DropdownSimple.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import React from 'react';
import {
Dropdown,
DropdownItem,
DropdownList,
DropdownItemProps
} from '@patternfly/react-core/dist/esm/components/Dropdown';
import { MenuToggle, MenuToggleElement } from '@patternfly/react-core/dist/esm/components/MenuToggle';
import { Divider } from '@patternfly/react-core/dist/esm/components/Divider';
import { OUIAProps } from '@patternfly/react-core/dist/esm/helpers';

export interface DropdownSimpleItem extends Omit<DropdownItemProps, 'content'> {
/** Content of the dropdown item. If the isDivider prop is true, this prop will be ignored. */
content?: React.ReactNode;
/** Unique identifier for the dropdown item, which is used in the dropdown onSelect callback */
value: string | number;
/** Callback for when the dropdown item is clicked. */
onClick?: (event?: any) => void;
/** URL to redirect to when the dropdown item is clicked. */
to?: string;
/** Flag indicating whether the dropdown item should render as a divider. If true, the item will be rendered without
* the dropdown item wrapper.
*/
isDivider?: boolean;
}

export interface DropdownSimpleProps extends OUIAProps {
/** Initial items of the dropdown. */
initialItems?: DropdownSimpleItem[];
/** @hide Forwarded ref */
innerRef?: React.Ref<any>;
/** Flag indicating the dropdown should be disabled. */
isDisabled?: boolean;
/** Flag indicated whether the dropdown toggle should take up the full width of its parent. */
isToggleFullWidth?: boolean;
/** Callback triggered when any dropdown item is clicked. */
onSelect?: (event?: React.MouseEvent<Element, MouseEvent>, value?: string | number) => void;
/** Callback triggered when the dropdown toggle opens or closes. */
onToggle?: (nextIsOpen: boolean) => void;
/** Flag indicating the dropdown toggle should be focused after a dropdown item is clicked. */
shouldFocusToggleOnSelect?: boolean;
/** Adds an accessible name to the dropdown toggle. Required when the dropdown toggle does not
* have any text content.
*/
toggleAriaLabel?: string;
/** Content of the toggle. */
toggleContent: React.ReactNode;
/** Variant style of the dropdown toggle. */
toggleVariant?: 'default' | 'plain' | 'plainText';
}

const DropdownSimpleBase: React.FunctionComponent<DropdownSimpleProps> = ({
innerRef,
initialItems,
onSelect: onSelectProp,
onToggle: onToggleProp,
isDisabled,
toggleAriaLabel,
toggleContent,
isToggleFullWidth,
toggleVariant = 'default',
shouldFocusToggleOnSelect,
...props
}: DropdownSimpleProps) => {
const [isOpen, setIsOpen] = React.useState(false);

const onSelect = (event: React.MouseEvent<Element, MouseEvent>, value: string | number) => {
onSelectProp && onSelectProp(event, value);
setIsOpen(false);
};

const onToggle = () => {
onToggleProp && onToggleProp(!isOpen);
setIsOpen(!isOpen);
};

const dropdownToggle = (toggleRef: React.Ref<MenuToggleElement>) => (
<MenuToggle
ref={toggleRef}
onClick={onToggle}
isExpanded={isOpen}
isDisabled={isDisabled}
variant={toggleVariant}
aria-label={toggleAriaLabel}
isFullWidth={isToggleFullWidth}
>
{toggleContent}
</MenuToggle>
);

const dropdownSimpleItems = initialItems?.map((item) => {
const { content, onClick, to, value, isDivider, ...itemProps } = item;

return isDivider ? (
<Divider component="li" key={value} />
) : (
<DropdownItem onClick={onClick} to={to} key={value} value={value} {...itemProps}>
{content}
</DropdownItem>
);
});

return (
<Dropdown
toggle={dropdownToggle}
isOpen={isOpen}
onSelect={onSelect}
shouldFocusToggleOnSelect={shouldFocusToggleOnSelect}
onOpenChange={(isOpen) => setIsOpen(isOpen)}
ref={innerRef}
{...props}
>
<DropdownList>{dropdownSimpleItems}</DropdownList>
</Dropdown>
);
};

export const DropdownSimple = React.forwardRef((props: DropdownSimpleProps, ref: React.Ref<any>) => (
<DropdownSimpleBase {...props} innerRef={ref} />
));

DropdownSimple.displayName = 'DropdownSimple';
Original file line number Diff line number Diff line change
@@ -0,0 +1,254 @@
import * as React from 'react';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { DropdownSimple } from '../DropdownSimple';
import styles from '@patternfly/react-styles/css/components/MenuToggle/menu-toggle';

describe('Dropdown toggle', () => {
test('Renders dropdown toggle as not disabled when isDisabled is not true', () => {
render(<DropdownSimple toggleContent="Dropdown" />);

expect(screen.getByRole('button', { name: 'Dropdown' })).not.toBeDisabled();
});

test('Renders dropdown toggle as disabled when isDisabled is true', () => {
render(<DropdownSimple toggleContent="Dropdown" isDisabled />);

expect(screen.getByRole('button', { name: 'Dropdown' })).toBeDisabled();
});

test('Passes toggleVariant', () => {
render(<DropdownSimple toggleContent="Dropdown" toggleVariant="plain" />);

expect(screen.getByRole('button', { name: 'Dropdown' })).toHaveClass(styles.modifiers.plain);
});

test('Passes toggleAriaLabel', () => {
render(<DropdownSimple toggleContent="Dropdown" toggleAriaLabel="Aria label content" />);

expect(screen.getByRole('button')).toHaveAccessibleName('Aria label content');
});

test('Calls onToggle with next isOpen state when dropdown toggle is clicked', async () => {
const onToggle = jest.fn();
const user = userEvent.setup();
render(<DropdownSimple onToggle={onToggle} toggleContent="Dropdown" />);

const toggle = screen.getByRole('button', { name: 'Dropdown' });
await user.click(toggle);
expect(onToggle).toHaveBeenCalledWith(true);
});

test('Does not call onToggle when dropdown toggle is not clicked', async () => {
const onToggle = jest.fn();
const items = [{ content: 'Action', value: 1 }];
const user = userEvent.setup();
render(
<div>
<button>Actual</button>
<DropdownSimple initialItems={items} onToggle={onToggle} toggleContent="Dropdown" />
</div>
);

const btn = screen.getByRole('button', { name: 'Actual' });
await user.click(btn);
expect(onToggle).not.toHaveBeenCalled();
});

test('Calls toggle onSelect when item is clicked', async () => {
const onSelect = jest.fn();
const items = [{ content: 'Action', value: 1 }];
const user = userEvent.setup();
render(<DropdownSimple initialItems={items} toggleContent="Dropdown" onSelect={onSelect} />);

const toggle = screen.getByRole('button', { name: 'Dropdown' });
await user.click(toggle);

const actionItem = screen.getByRole('menuitem', { name: 'Action' });
await user.click(actionItem);
expect(onSelect).toHaveBeenCalledTimes(1);
});

test('Does not call toggle onSelect when item is not clicked', async () => {
const onSelect = jest.fn();
const items = [{ content: 'Action', value: 1 }];
const user = userEvent.setup();
render(<DropdownSimple initialItems={items} toggleContent="Dropdown" onSelect={onSelect} />);

const toggle = screen.getByRole('button', { name: 'Dropdown' });
await user.click(toggle);
await user.click(toggle);
expect(onSelect).not.toHaveBeenCalled();
});

test('Does not pass isToggleFullWidth to menu toggle by default', () => {
render(<DropdownSimple toggleContent="Dropdown" />);

expect(screen.getByRole('button', { name: 'Dropdown' })).not.toHaveClass(styles.modifiers.fullWidth);
});

test('Passes isToggleFullWidth to menu toggle when passed in', () => {
render(<DropdownSimple isToggleFullWidth toggleContent="Dropdown" />);

expect(screen.getByRole('button', { name: 'Dropdown' })).toHaveClass(styles.modifiers.fullWidth);
});

test('Does not focus toggle on item select by default', async () => {
const items = [{ content: 'Action', value: 1 }];
const user = userEvent.setup();
render(<DropdownSimple initialItems={items} toggleContent="Dropdown" />);

const toggle = screen.getByRole('button', { name: 'Dropdown' });
await user.click(toggle);
const actionItem = screen.getByRole('menuitem', { name: 'Action' });
await user.click(actionItem);

expect(toggle).not.toHaveFocus();
});

test('Focuses toggle on item select when shouldFocusToggleOnSelect is true', async () => {
const items = [{ content: 'Action', value: 1 }];
const user = userEvent.setup();
render(<DropdownSimple shouldFocusToggleOnSelect initialItems={items} toggleContent="Dropdown" />);

const toggle = screen.getByRole('button', { name: 'Dropdown' });
await user.click(toggle);
const actionItem = screen.getByRole('menuitem', { name: 'Action' });
await user.click(actionItem);

expect(toggle).toHaveFocus();
});

test('Matches snapshot', () => {
const { asFragment } = render(<DropdownSimple toggleContent="Dropdown" />);

expect(asFragment()).toMatchSnapshot();
});
});

describe('Dropdown items', () => {
test('Renders with items', async () => {
const items = [
{ content: 'Action', value: 1 },
{ value: 'separator', isDivider: true }
];
const user = userEvent.setup();
render(<DropdownSimple initialItems={items} toggleContent="Dropdown" />);

const toggle = screen.getByRole('button', { name: 'Dropdown' });
await user.click(toggle);

const actionItem = screen.getByRole('menuitem', { name: 'Action' });
const dividerItem = screen.getByRole('separator');
expect(actionItem).toBeInTheDocument();
expect(dividerItem).toBeInTheDocument();
});

test('Renders with a link item', async () => {
const items = [{ content: 'Link', value: 1, to: '#' }];
const user = userEvent.setup();
render(<DropdownSimple initialItems={items} toggleContent="Dropdown" />);

const toggle = screen.getByRole('button', { name: 'Dropdown' });
await user.click(toggle);

const linkItem = screen.getByRole('menuitem', { name: 'Link' });
expect(linkItem.getAttribute('href')).toBe('#');
});

test('Renders with items not disabled by default', async () => {
const items = [{ content: 'Action', value: 1 }];
const user = userEvent.setup();
render(<DropdownSimple initialItems={items} toggleContent="Dropdown" />);

const toggle = screen.getByRole('button', { name: 'Dropdown' });
await user.click(toggle);

const actionItem = screen.getByRole('menuitem', { name: 'Action' });
expect(actionItem).not.toBeDisabled();
});

test('Renders with a disabled item', async () => {
const items = [{ content: 'Action', value: 1, isDisabled: true }];
const user = userEvent.setup();
render(<DropdownSimple initialItems={items} toggleContent="Dropdown" />);

const toggle = screen.getByRole('button', { name: 'Dropdown' });
await user.click(toggle);

const actionItem = screen.getByRole('menuitem', { name: 'Action' });
expect(actionItem).toBeDisabled();
});

test('Spreads props on item', async () => {
const items = [{ content: 'Action', value: 1, id: 'Test' }];
const user = userEvent.setup();
render(<DropdownSimple initialItems={items} toggleContent="Dropdown" />);

const toggle = screen.getByRole('button', { name: 'Dropdown' });
await user.click(toggle);

const actionItem = screen.getByRole('menuitem', { name: 'Action' });
expect(actionItem.getAttribute('id')).toBe('Test');
});

test('Calls item onClick when clicked', async () => {
const onClick = jest.fn();
const items = [{ content: 'Action', value: 1, onClick }];
const user = userEvent.setup();
render(<DropdownSimple initialItems={items} toggleContent="Dropdown" />);

const toggle = screen.getByRole('button', { name: 'Dropdown' });
await user.click(toggle);

const actionItem = screen.getByRole('menuitem', { name: 'Action' });
await user.click(actionItem);
expect(onClick).toHaveBeenCalledTimes(1);
});

test('Does not call item onClick when not clicked', async () => {
const onClick = jest.fn();
const items = [
{ content: 'Action', value: 1, onClick },
{ content: 'Action 2', value: 2 }
];
const user = userEvent.setup();
render(<DropdownSimple initialItems={items} toggleContent="Dropdown" />);

const toggle = screen.getByRole('button', { name: 'Dropdown' });
await user.click(toggle);

const actionItem = screen.getByRole('menuitem', { name: 'Action 2' });
await user.click(actionItem);
expect(onClick).not.toHaveBeenCalled();
});

test('Does not call item onClick when clicked and item is disabled', async () => {
const onClick = jest.fn();
const items = [{ content: 'Action', value: 1, onClick, isDisabled: true }];
const user = userEvent.setup();
render(<DropdownSimple initialItems={items} toggleContent="Dropdown" />);

const toggle = screen.getByRole('button', { name: 'Dropdown' });
await user.click(toggle);

const actionItem = screen.getByRole('menuitem', { name: 'Action' });
await user.click(actionItem);
expect(onClick).not.toHaveBeenCalled();
});

test('Matches snapshot', async () => {
const items = [
{ content: 'Action', value: 1, ouiaId: '1' },
{ value: 'separator', isDivider: true, ouiaId: '2' },
{ content: 'Link', value: 'separator', to: '#', ouiaId: '3' }
];
const user = userEvent.setup();
const { asFragment } = render(<DropdownSimple ouiaId={4} initialItems={items} toggleContent="Dropdown" />);

const toggle = screen.getByRole('button', { name: 'Dropdown' });
await user.click(toggle);

expect(asFragment()).toMatchSnapshot();
});
});
Loading

0 comments on commit 47dcd5a

Please sign in to comment.