Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(Dropdown): Added simple template #10308

Merged
merged 6 commits into from
May 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading