Skip to content

Commit 86dcc02

Browse files
feat(Dropdown): Added simple template (#10308)
* feat(Dropdown): Added simple template * Added tests * Added imports to example file * Additional fixes for docs fail * Updated import name * Added additional tests
1 parent f96f7a0 commit 86dcc02

File tree

7 files changed

+614
-0
lines changed

7 files changed

+614
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
import React from 'react';
2+
import {
3+
Dropdown,
4+
DropdownItem,
5+
DropdownList,
6+
DropdownItemProps
7+
} from '@patternfly/react-core/dist/esm/components/Dropdown';
8+
import { MenuToggle, MenuToggleElement } from '@patternfly/react-core/dist/esm/components/MenuToggle';
9+
import { Divider } from '@patternfly/react-core/dist/esm/components/Divider';
10+
import { OUIAProps } from '@patternfly/react-core/dist/esm/helpers';
11+
12+
export interface DropdownSimpleItem extends Omit<DropdownItemProps, 'content'> {
13+
/** Content of the dropdown item. If the isDivider prop is true, this prop will be ignored. */
14+
content?: React.ReactNode;
15+
/** Unique identifier for the dropdown item, which is used in the dropdown onSelect callback */
16+
value: string | number;
17+
/** Callback for when the dropdown item is clicked. */
18+
onClick?: (event?: any) => void;
19+
/** URL to redirect to when the dropdown item is clicked. */
20+
to?: string;
21+
/** Flag indicating whether the dropdown item should render as a divider. If true, the item will be rendered without
22+
* the dropdown item wrapper.
23+
*/
24+
isDivider?: boolean;
25+
}
26+
27+
export interface DropdownSimpleProps extends OUIAProps {
28+
/** Initial items of the dropdown. */
29+
initialItems?: DropdownSimpleItem[];
30+
/** @hide Forwarded ref */
31+
innerRef?: React.Ref<any>;
32+
/** Flag indicating the dropdown should be disabled. */
33+
isDisabled?: boolean;
34+
/** Flag indicated whether the dropdown toggle should take up the full width of its parent. */
35+
isToggleFullWidth?: boolean;
36+
/** Callback triggered when any dropdown item is clicked. */
37+
onSelect?: (event?: React.MouseEvent<Element, MouseEvent>, value?: string | number) => void;
38+
/** Callback triggered when the dropdown toggle opens or closes. */
39+
onToggle?: (nextIsOpen: boolean) => void;
40+
/** Flag indicating the dropdown toggle should be focused after a dropdown item is clicked. */
41+
shouldFocusToggleOnSelect?: boolean;
42+
/** Adds an accessible name to the dropdown toggle. Required when the dropdown toggle does not
43+
* have any text content.
44+
*/
45+
toggleAriaLabel?: string;
46+
/** Content of the toggle. */
47+
toggleContent: React.ReactNode;
48+
/** Variant style of the dropdown toggle. */
49+
toggleVariant?: 'default' | 'plain' | 'plainText';
50+
}
51+
52+
const DropdownSimpleBase: React.FunctionComponent<DropdownSimpleProps> = ({
53+
innerRef,
54+
initialItems,
55+
onSelect: onSelectProp,
56+
onToggle: onToggleProp,
57+
isDisabled,
58+
toggleAriaLabel,
59+
toggleContent,
60+
isToggleFullWidth,
61+
toggleVariant = 'default',
62+
shouldFocusToggleOnSelect,
63+
...props
64+
}: DropdownSimpleProps) => {
65+
const [isOpen, setIsOpen] = React.useState(false);
66+
67+
const onSelect = (event: React.MouseEvent<Element, MouseEvent>, value: string | number) => {
68+
onSelectProp && onSelectProp(event, value);
69+
setIsOpen(false);
70+
};
71+
72+
const onToggle = () => {
73+
onToggleProp && onToggleProp(!isOpen);
74+
setIsOpen(!isOpen);
75+
};
76+
77+
const dropdownToggle = (toggleRef: React.Ref<MenuToggleElement>) => (
78+
<MenuToggle
79+
ref={toggleRef}
80+
onClick={onToggle}
81+
isExpanded={isOpen}
82+
isDisabled={isDisabled}
83+
variant={toggleVariant}
84+
aria-label={toggleAriaLabel}
85+
isFullWidth={isToggleFullWidth}
86+
>
87+
{toggleContent}
88+
</MenuToggle>
89+
);
90+
91+
const dropdownSimpleItems = initialItems?.map((item) => {
92+
const { content, onClick, to, value, isDivider, ...itemProps } = item;
93+
94+
return isDivider ? (
95+
<Divider component="li" key={value} />
96+
) : (
97+
<DropdownItem onClick={onClick} to={to} key={value} value={value} {...itemProps}>
98+
{content}
99+
</DropdownItem>
100+
);
101+
});
102+
103+
return (
104+
<Dropdown
105+
toggle={dropdownToggle}
106+
isOpen={isOpen}
107+
onSelect={onSelect}
108+
shouldFocusToggleOnSelect={shouldFocusToggleOnSelect}
109+
onOpenChange={(isOpen) => setIsOpen(isOpen)}
110+
ref={innerRef}
111+
{...props}
112+
>
113+
<DropdownList>{dropdownSimpleItems}</DropdownList>
114+
</Dropdown>
115+
);
116+
};
117+
118+
export const DropdownSimple = React.forwardRef((props: DropdownSimpleProps, ref: React.Ref<any>) => (
119+
<DropdownSimpleBase {...props} innerRef={ref} />
120+
));
121+
122+
DropdownSimple.displayName = 'DropdownSimple';
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,254 @@
1+
import * as React from 'react';
2+
import { render, screen } from '@testing-library/react';
3+
import userEvent from '@testing-library/user-event';
4+
import { DropdownSimple } from '../DropdownSimple';
5+
import styles from '@patternfly/react-styles/css/components/MenuToggle/menu-toggle';
6+
7+
describe('Dropdown toggle', () => {
8+
test('Renders dropdown toggle as not disabled when isDisabled is not true', () => {
9+
render(<DropdownSimple toggleContent="Dropdown" />);
10+
11+
expect(screen.getByRole('button', { name: 'Dropdown' })).not.toBeDisabled();
12+
});
13+
14+
test('Renders dropdown toggle as disabled when isDisabled is true', () => {
15+
render(<DropdownSimple toggleContent="Dropdown" isDisabled />);
16+
17+
expect(screen.getByRole('button', { name: 'Dropdown' })).toBeDisabled();
18+
});
19+
20+
test('Passes toggleVariant', () => {
21+
render(<DropdownSimple toggleContent="Dropdown" toggleVariant="plain" />);
22+
23+
expect(screen.getByRole('button', { name: 'Dropdown' })).toHaveClass(styles.modifiers.plain);
24+
});
25+
26+
test('Passes toggleAriaLabel', () => {
27+
render(<DropdownSimple toggleContent="Dropdown" toggleAriaLabel="Aria label content" />);
28+
29+
expect(screen.getByRole('button')).toHaveAccessibleName('Aria label content');
30+
});
31+
32+
test('Calls onToggle with next isOpen state when dropdown toggle is clicked', async () => {
33+
const onToggle = jest.fn();
34+
const user = userEvent.setup();
35+
render(<DropdownSimple onToggle={onToggle} toggleContent="Dropdown" />);
36+
37+
const toggle = screen.getByRole('button', { name: 'Dropdown' });
38+
await user.click(toggle);
39+
expect(onToggle).toHaveBeenCalledWith(true);
40+
});
41+
42+
test('Does not call onToggle when dropdown toggle is not clicked', async () => {
43+
const onToggle = jest.fn();
44+
const items = [{ content: 'Action', value: 1 }];
45+
const user = userEvent.setup();
46+
render(
47+
<div>
48+
<button>Actual</button>
49+
<DropdownSimple initialItems={items} onToggle={onToggle} toggleContent="Dropdown" />
50+
</div>
51+
);
52+
53+
const btn = screen.getByRole('button', { name: 'Actual' });
54+
await user.click(btn);
55+
expect(onToggle).not.toHaveBeenCalled();
56+
});
57+
58+
test('Calls toggle onSelect when item is clicked', async () => {
59+
const onSelect = jest.fn();
60+
const items = [{ content: 'Action', value: 1 }];
61+
const user = userEvent.setup();
62+
render(<DropdownSimple initialItems={items} toggleContent="Dropdown" onSelect={onSelect} />);
63+
64+
const toggle = screen.getByRole('button', { name: 'Dropdown' });
65+
await user.click(toggle);
66+
67+
const actionItem = screen.getByRole('menuitem', { name: 'Action' });
68+
await user.click(actionItem);
69+
expect(onSelect).toHaveBeenCalledTimes(1);
70+
});
71+
72+
test('Does not call toggle onSelect when item is not clicked', async () => {
73+
const onSelect = jest.fn();
74+
const items = [{ content: 'Action', value: 1 }];
75+
const user = userEvent.setup();
76+
render(<DropdownSimple initialItems={items} toggleContent="Dropdown" onSelect={onSelect} />);
77+
78+
const toggle = screen.getByRole('button', { name: 'Dropdown' });
79+
await user.click(toggle);
80+
await user.click(toggle);
81+
expect(onSelect).not.toHaveBeenCalled();
82+
});
83+
84+
test('Does not pass isToggleFullWidth to menu toggle by default', () => {
85+
render(<DropdownSimple toggleContent="Dropdown" />);
86+
87+
expect(screen.getByRole('button', { name: 'Dropdown' })).not.toHaveClass(styles.modifiers.fullWidth);
88+
});
89+
90+
test('Passes isToggleFullWidth to menu toggle when passed in', () => {
91+
render(<DropdownSimple isToggleFullWidth toggleContent="Dropdown" />);
92+
93+
expect(screen.getByRole('button', { name: 'Dropdown' })).toHaveClass(styles.modifiers.fullWidth);
94+
});
95+
96+
test('Does not focus toggle on item select by default', async () => {
97+
const items = [{ content: 'Action', value: 1 }];
98+
const user = userEvent.setup();
99+
render(<DropdownSimple initialItems={items} toggleContent="Dropdown" />);
100+
101+
const toggle = screen.getByRole('button', { name: 'Dropdown' });
102+
await user.click(toggle);
103+
const actionItem = screen.getByRole('menuitem', { name: 'Action' });
104+
await user.click(actionItem);
105+
106+
expect(toggle).not.toHaveFocus();
107+
});
108+
109+
test('Focuses toggle on item select when shouldFocusToggleOnSelect is true', async () => {
110+
const items = [{ content: 'Action', value: 1 }];
111+
const user = userEvent.setup();
112+
render(<DropdownSimple shouldFocusToggleOnSelect initialItems={items} toggleContent="Dropdown" />);
113+
114+
const toggle = screen.getByRole('button', { name: 'Dropdown' });
115+
await user.click(toggle);
116+
const actionItem = screen.getByRole('menuitem', { name: 'Action' });
117+
await user.click(actionItem);
118+
119+
expect(toggle).toHaveFocus();
120+
});
121+
122+
test('Matches snapshot', () => {
123+
const { asFragment } = render(<DropdownSimple toggleContent="Dropdown" />);
124+
125+
expect(asFragment()).toMatchSnapshot();
126+
});
127+
});
128+
129+
describe('Dropdown items', () => {
130+
test('Renders with items', async () => {
131+
const items = [
132+
{ content: 'Action', value: 1 },
133+
{ value: 'separator', isDivider: true }
134+
];
135+
const user = userEvent.setup();
136+
render(<DropdownSimple initialItems={items} toggleContent="Dropdown" />);
137+
138+
const toggle = screen.getByRole('button', { name: 'Dropdown' });
139+
await user.click(toggle);
140+
141+
const actionItem = screen.getByRole('menuitem', { name: 'Action' });
142+
const dividerItem = screen.getByRole('separator');
143+
expect(actionItem).toBeInTheDocument();
144+
expect(dividerItem).toBeInTheDocument();
145+
});
146+
147+
test('Renders with a link item', async () => {
148+
const items = [{ content: 'Link', value: 1, to: '#' }];
149+
const user = userEvent.setup();
150+
render(<DropdownSimple initialItems={items} toggleContent="Dropdown" />);
151+
152+
const toggle = screen.getByRole('button', { name: 'Dropdown' });
153+
await user.click(toggle);
154+
155+
const linkItem = screen.getByRole('menuitem', { name: 'Link' });
156+
expect(linkItem.getAttribute('href')).toBe('#');
157+
});
158+
159+
test('Renders with items not disabled by default', async () => {
160+
const items = [{ content: 'Action', value: 1 }];
161+
const user = userEvent.setup();
162+
render(<DropdownSimple initialItems={items} toggleContent="Dropdown" />);
163+
164+
const toggle = screen.getByRole('button', { name: 'Dropdown' });
165+
await user.click(toggle);
166+
167+
const actionItem = screen.getByRole('menuitem', { name: 'Action' });
168+
expect(actionItem).not.toBeDisabled();
169+
});
170+
171+
test('Renders with a disabled item', async () => {
172+
const items = [{ content: 'Action', value: 1, isDisabled: true }];
173+
const user = userEvent.setup();
174+
render(<DropdownSimple initialItems={items} toggleContent="Dropdown" />);
175+
176+
const toggle = screen.getByRole('button', { name: 'Dropdown' });
177+
await user.click(toggle);
178+
179+
const actionItem = screen.getByRole('menuitem', { name: 'Action' });
180+
expect(actionItem).toBeDisabled();
181+
});
182+
183+
test('Spreads props on item', async () => {
184+
const items = [{ content: 'Action', value: 1, id: 'Test' }];
185+
const user = userEvent.setup();
186+
render(<DropdownSimple initialItems={items} toggleContent="Dropdown" />);
187+
188+
const toggle = screen.getByRole('button', { name: 'Dropdown' });
189+
await user.click(toggle);
190+
191+
const actionItem = screen.getByRole('menuitem', { name: 'Action' });
192+
expect(actionItem.getAttribute('id')).toBe('Test');
193+
});
194+
195+
test('Calls item onClick when clicked', async () => {
196+
const onClick = jest.fn();
197+
const items = [{ content: 'Action', value: 1, onClick }];
198+
const user = userEvent.setup();
199+
render(<DropdownSimple initialItems={items} toggleContent="Dropdown" />);
200+
201+
const toggle = screen.getByRole('button', { name: 'Dropdown' });
202+
await user.click(toggle);
203+
204+
const actionItem = screen.getByRole('menuitem', { name: 'Action' });
205+
await user.click(actionItem);
206+
expect(onClick).toHaveBeenCalledTimes(1);
207+
});
208+
209+
test('Does not call item onClick when not clicked', async () => {
210+
const onClick = jest.fn();
211+
const items = [
212+
{ content: 'Action', value: 1, onClick },
213+
{ content: 'Action 2', value: 2 }
214+
];
215+
const user = userEvent.setup();
216+
render(<DropdownSimple initialItems={items} toggleContent="Dropdown" />);
217+
218+
const toggle = screen.getByRole('button', { name: 'Dropdown' });
219+
await user.click(toggle);
220+
221+
const actionItem = screen.getByRole('menuitem', { name: 'Action 2' });
222+
await user.click(actionItem);
223+
expect(onClick).not.toHaveBeenCalled();
224+
});
225+
226+
test('Does not call item onClick when clicked and item is disabled', async () => {
227+
const onClick = jest.fn();
228+
const items = [{ content: 'Action', value: 1, onClick, isDisabled: true }];
229+
const user = userEvent.setup();
230+
render(<DropdownSimple initialItems={items} toggleContent="Dropdown" />);
231+
232+
const toggle = screen.getByRole('button', { name: 'Dropdown' });
233+
await user.click(toggle);
234+
235+
const actionItem = screen.getByRole('menuitem', { name: 'Action' });
236+
await user.click(actionItem);
237+
expect(onClick).not.toHaveBeenCalled();
238+
});
239+
240+
test('Matches snapshot', async () => {
241+
const items = [
242+
{ content: 'Action', value: 1, ouiaId: '1' },
243+
{ value: 'separator', isDivider: true, ouiaId: '2' },
244+
{ content: 'Link', value: 'separator', to: '#', ouiaId: '3' }
245+
];
246+
const user = userEvent.setup();
247+
const { asFragment } = render(<DropdownSimple ouiaId={4} initialItems={items} toggleContent="Dropdown" />);
248+
249+
const toggle = screen.getByRole('button', { name: 'Dropdown' });
250+
await user.click(toggle);
251+
252+
expect(asFragment()).toMatchSnapshot();
253+
});
254+
});

0 commit comments

Comments
 (0)