Skip to content

Commit

Permalink
feat(templates): toggle props & improvements (#10473)
Browse files Browse the repository at this point in the history
* feat(templates): toggle props & improvements

* remove toggleContent from typeahead template

* update template names

* update tests

* added SimpleSelect tests

* fix yarnlock
  • Loading branch information
kmcfaul committed Jun 27, 2024
1 parent 47dcd5a commit efd321e
Show file tree
Hide file tree
Showing 20 changed files with 614 additions and 127 deletions.
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -111,5 +111,6 @@
"packages/**"
]
},
"dependencies": {}
"dependencies": {},
"packageManager": "[email protected]+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,14 @@ import {
Dropdown,
DropdownItem,
DropdownList,
DropdownItemProps
DropdownItemProps,
DropdownProps
} from '@patternfly/react-core/dist/esm/components/Dropdown';
import { MenuToggle, MenuToggleElement } from '@patternfly/react-core/dist/esm/components/MenuToggle';
import { MenuToggle, MenuToggleElement, MenuToggleProps } 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'> {
export interface SimpleDropdownItem 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 */
Expand All @@ -24,9 +25,9 @@ export interface DropdownSimpleItem extends Omit<DropdownItemProps, 'content'> {
isDivider?: boolean;
}

export interface DropdownSimpleProps extends OUIAProps {
export interface SimpleDropdownProps extends Omit<DropdownProps, 'toggle'>, OUIAProps {
/** Initial items of the dropdown. */
initialItems?: DropdownSimpleItem[];
initialItems?: SimpleDropdownItem[];
/** @hide Forwarded ref */
innerRef?: React.Ref<any>;
/** Flag indicating the dropdown should be disabled. */
Expand All @@ -47,9 +48,13 @@ export interface DropdownSimpleProps extends OUIAProps {
toggleContent: React.ReactNode;
/** Variant style of the dropdown toggle. */
toggleVariant?: 'default' | 'plain' | 'plainText';
/** Width of the toggle. */
toggleWidth?: string;
/** Additional props passed to the toggle. */
toggleProps?: MenuToggleProps;
}

const DropdownSimpleBase: React.FunctionComponent<DropdownSimpleProps> = ({
const SimpleDropdownBase: React.FunctionComponent<SimpleDropdownProps> = ({
innerRef,
initialItems,
onSelect: onSelectProp,
Expand All @@ -59,13 +64,16 @@ const DropdownSimpleBase: React.FunctionComponent<DropdownSimpleProps> = ({
toggleContent,
isToggleFullWidth,
toggleVariant = 'default',
toggleWidth,
toggleProps,
shouldFocusToggleOnSelect,
...props
}: DropdownSimpleProps) => {
}: SimpleDropdownProps) => {
const [isOpen, setIsOpen] = React.useState(false);

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

Expand All @@ -83,6 +91,12 @@ const DropdownSimpleBase: React.FunctionComponent<DropdownSimpleProps> = ({
variant={toggleVariant}
aria-label={toggleAriaLabel}
isFullWidth={isToggleFullWidth}
style={
{
width: toggleWidth
} as React.CSSProperties
}
{...toggleProps}
>
{toggleContent}
</MenuToggle>
Expand All @@ -106,7 +120,10 @@ const DropdownSimpleBase: React.FunctionComponent<DropdownSimpleProps> = ({
isOpen={isOpen}
onSelect={onSelect}
shouldFocusToggleOnSelect={shouldFocusToggleOnSelect}
onOpenChange={(isOpen) => setIsOpen(isOpen)}
onOpenChange={(isOpen) => {
onToggleProp && onToggleProp(isOpen);
setIsOpen(isOpen);
}}
ref={innerRef}
{...props}
>
Expand All @@ -115,8 +132,8 @@ const DropdownSimpleBase: React.FunctionComponent<DropdownSimpleProps> = ({
);
};

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

DropdownSimple.displayName = 'DropdownSimple';
SimpleDropdown.displayName = 'SimpleDropdown';
Original file line number Diff line number Diff line change
@@ -1,38 +1,50 @@
import * as React from 'react';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { DropdownSimple } from '../DropdownSimple';
import { SimpleDropdown } from '../SimpleDropdown';
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" />);
render(<SimpleDropdown 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 />);
render(<SimpleDropdown toggleContent="Dropdown" isDisabled />);

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

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

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

test('Passes toggleWidth', () => {
render(<SimpleDropdown toggleContent="Dropdown" toggleWidth="500px" />);

expect(screen.getByRole('button', { name: 'Dropdown' })).toHaveAttribute('style', 'width: 500px;');
});

test('Passes additional toggleProps', () => {
render(<SimpleDropdown toggleContent="Dropdown" toggleProps={{ id: 'toggle' }} />);

expect(screen.getByRole('button', { name: 'Dropdown' })).toHaveAttribute('id', 'toggle');
});

test('Passes toggleAriaLabel', () => {
render(<DropdownSimple toggleContent="Dropdown" toggleAriaLabel="Aria label content" />);
render(<SimpleDropdown 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" />);
render(<SimpleDropdown onToggle={onToggle} toggleContent="Dropdown" />);

const toggle = screen.getByRole('button', { name: 'Dropdown' });
await user.click(toggle);
Expand All @@ -46,7 +58,7 @@ describe('Dropdown toggle', () => {
render(
<div>
<button>Actual</button>
<DropdownSimple initialItems={items} onToggle={onToggle} toggleContent="Dropdown" />
<SimpleDropdown initialItems={items} onToggle={onToggle} toggleContent="Dropdown" />
</div>
);

Expand All @@ -59,7 +71,7 @@ describe('Dropdown toggle', () => {
const onSelect = jest.fn();
const items = [{ content: 'Action', value: 1 }];
const user = userEvent.setup();
render(<DropdownSimple initialItems={items} toggleContent="Dropdown" onSelect={onSelect} />);
render(<SimpleDropdown initialItems={items} toggleContent="Dropdown" onSelect={onSelect} />);

const toggle = screen.getByRole('button', { name: 'Dropdown' });
await user.click(toggle);
Expand All @@ -73,7 +85,7 @@ describe('Dropdown toggle', () => {
const onSelect = jest.fn();
const items = [{ content: 'Action', value: 1 }];
const user = userEvent.setup();
render(<DropdownSimple initialItems={items} toggleContent="Dropdown" onSelect={onSelect} />);
render(<SimpleDropdown initialItems={items} toggleContent="Dropdown" onSelect={onSelect} />);

const toggle = screen.getByRole('button', { name: 'Dropdown' });
await user.click(toggle);
Expand All @@ -82,21 +94,21 @@ describe('Dropdown toggle', () => {
});

test('Does not pass isToggleFullWidth to menu toggle by default', () => {
render(<DropdownSimple toggleContent="Dropdown" />);
render(<SimpleDropdown 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" />);
render(<SimpleDropdown 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" />);
render(<SimpleDropdown initialItems={items} toggleContent="Dropdown" />);

const toggle = screen.getByRole('button', { name: 'Dropdown' });
await user.click(toggle);
Expand All @@ -109,7 +121,7 @@ describe('Dropdown toggle', () => {
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" />);
render(<SimpleDropdown shouldFocusToggleOnSelect initialItems={items} toggleContent="Dropdown" />);

const toggle = screen.getByRole('button', { name: 'Dropdown' });
await user.click(toggle);
Expand All @@ -120,7 +132,7 @@ describe('Dropdown toggle', () => {
});

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

expect(asFragment()).toMatchSnapshot();
});
Expand All @@ -133,7 +145,7 @@ describe('Dropdown items', () => {
{ value: 'separator', isDivider: true }
];
const user = userEvent.setup();
render(<DropdownSimple initialItems={items} toggleContent="Dropdown" />);
render(<SimpleDropdown initialItems={items} toggleContent="Dropdown" />);

const toggle = screen.getByRole('button', { name: 'Dropdown' });
await user.click(toggle);
Expand All @@ -147,7 +159,7 @@ describe('Dropdown items', () => {
test('Renders with a link item', async () => {
const items = [{ content: 'Link', value: 1, to: '#' }];
const user = userEvent.setup();
render(<DropdownSimple initialItems={items} toggleContent="Dropdown" />);
render(<SimpleDropdown initialItems={items} toggleContent="Dropdown" />);

const toggle = screen.getByRole('button', { name: 'Dropdown' });
await user.click(toggle);
Expand All @@ -159,7 +171,7 @@ describe('Dropdown items', () => {
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" />);
render(<SimpleDropdown initialItems={items} toggleContent="Dropdown" />);

const toggle = screen.getByRole('button', { name: 'Dropdown' });
await user.click(toggle);
Expand All @@ -171,7 +183,7 @@ describe('Dropdown items', () => {
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" />);
render(<SimpleDropdown initialItems={items} toggleContent="Dropdown" />);

const toggle = screen.getByRole('button', { name: 'Dropdown' });
await user.click(toggle);
Expand All @@ -183,7 +195,7 @@ describe('Dropdown items', () => {
test('Spreads props on item', async () => {
const items = [{ content: 'Action', value: 1, id: 'Test' }];
const user = userEvent.setup();
render(<DropdownSimple initialItems={items} toggleContent="Dropdown" />);
render(<SimpleDropdown initialItems={items} toggleContent="Dropdown" />);

const toggle = screen.getByRole('button', { name: 'Dropdown' });
await user.click(toggle);
Expand All @@ -196,7 +208,7 @@ describe('Dropdown items', () => {
const onClick = jest.fn();
const items = [{ content: 'Action', value: 1, onClick }];
const user = userEvent.setup();
render(<DropdownSimple initialItems={items} toggleContent="Dropdown" />);
render(<SimpleDropdown initialItems={items} toggleContent="Dropdown" />);

const toggle = screen.getByRole('button', { name: 'Dropdown' });
await user.click(toggle);
Expand All @@ -213,7 +225,7 @@ describe('Dropdown items', () => {
{ content: 'Action 2', value: 2 }
];
const user = userEvent.setup();
render(<DropdownSimple initialItems={items} toggleContent="Dropdown" />);
render(<SimpleDropdown initialItems={items} toggleContent="Dropdown" />);

const toggle = screen.getByRole('button', { name: 'Dropdown' });
await user.click(toggle);
Expand All @@ -227,7 +239,7 @@ describe('Dropdown items', () => {
const onClick = jest.fn();
const items = [{ content: 'Action', value: 1, onClick, isDisabled: true }];
const user = userEvent.setup();
render(<DropdownSimple initialItems={items} toggleContent="Dropdown" />);
render(<SimpleDropdown initialItems={items} toggleContent="Dropdown" />);

const toggle = screen.getByRole('button', { name: 'Dropdown' });
await user.click(toggle);
Expand All @@ -244,7 +256,7 @@ describe('Dropdown items', () => {
{ content: 'Link', value: 'separator', to: '#', ouiaId: '3' }
];
const user = userEvent.setup();
const { asFragment } = render(<DropdownSimple ouiaId={4} initialItems={items} toggleContent="Dropdown" />);
const { asFragment } = render(<SimpleDropdown ouiaId={4} initialItems={items} toggleContent="Dropdown" />);

const toggle = screen.getByRole('button', { name: 'Dropdown' });
await user.click(toggle);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,33 +4,21 @@ section: components
subsection: menus
template: true
beta: true
propComponents: ['DropdownSimple', 'DropdownSimpleItem']
propComponents: ['SimpleDropdown', 'SimpleDropdownItem']
---

Note: Templates live in their own package at [@patternfly/react-templates](https://www.npmjs.com/package/@patternfly/react-templates)!

For custom use cases, please see the dropdown component suite from [@patternfly/react-core](https://www.npmjs.com/package/@patternfly/react-core).

import {
Checkbox,
Divider,
Dropdown,
DropdownItem,
DropdownList,
DropdownItemProps,
Flex,
FlexItem,
MenuToggle,
MenuToggleElement,
OUIAProps
} from '@patternfly/react-core';
import { DropdownSimple, DropdownSimpleItem } from '@patternfly/react-templates';
import { Checkbox, Flex, FlexItem } from '@patternfly/react-core';
import { SimpleDropdown } from '@patternfly/react-templates';
import EllipsisVIcon from '@patternfly/react-icons/dist/esm/icons/ellipsis-v-icon';

## Examples

### Simple

```ts file="./DropdownSimpleExample.tsx"
```ts file="./SimpleDropdownExample.tsx"

```
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import React from 'react';
import { Checkbox, Flex, FlexItem } from '@patternfly/react-core';
import { DropdownSimple, DropdownSimpleItem } from '@patternfly/react-templates';
import { SimpleDropdown, SimpleDropdownItem } from '@patternfly/react-templates';
import EllipsisVIcon from '@patternfly/react-icons/dist/esm/icons/ellipsis-v-icon';

export const DropdownSimpleExample: React.FunctionComponent = () => {
export const SimpleDropdownExample: React.FunctionComponent = () => {
const [isDisabled, setIsDisabled] = React.useState(false);

const items: DropdownSimpleItem[] = [
const items: SimpleDropdownItem[] = [
// eslint-disable-next-line no-console
{ content: 'Action', value: 1, onClick: () => console.log('Action clicked') },
// Prevent default click behavior on link for example purposes
Expand All @@ -21,25 +21,25 @@ export const DropdownSimpleExample: React.FunctionComponent = () => {
<React.Fragment>
<Checkbox
id="simple-example-disabled-toggle"
label="Disable dropdown simple toggles"
label="Disable simple dropdown toggles"
isChecked={isDisabled}
onChange={(_event: React.FormEvent<HTMLInputElement>, checked: boolean) => setIsDisabled(checked)}
style={{ marginBottom: 20 }}
/>
<Flex gap={{ default: 'gapLg' }}>
<FlexItem>
<DropdownSimple initialItems={items} isDisabled={isDisabled} toggleContent="Dropdown" />
<SimpleDropdown initialItems={items} isDisabled={isDisabled} toggleContent="Dropdown" />
</FlexItem>
<FlexItem>
<DropdownSimple
<SimpleDropdown
toggleVariant="plainText"
initialItems={items}
isDisabled={isDisabled}
toggleContent="Dropdown with plainText styling"
/>
</FlexItem>
<FlexItem>
<DropdownSimple
<SimpleDropdown
toggleVariant="plain"
initialItems={items}
isDisabled={isDisabled}
Expand Down
Loading

0 comments on commit efd321e

Please sign in to comment.