-
Notifications
You must be signed in to change notification settings - Fork 355
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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
- Loading branch information
1 parent
ed62ec2
commit 47dcd5a
Showing
7 changed files
with
614 additions
and
0 deletions.
There are no files selected for viewing
122 changes: 122 additions & 0 deletions
122
packages/react-templates/src/components/Dropdown/DropdownSimple.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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'; |
254 changes: 254 additions & 0 deletions
254
packages/react-templates/src/components/Dropdown/__tests__/DropdownSimple.test.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | ||
}); | ||
}); |
Oops, something went wrong.