From efd321e5a7c8361590094a2a21203d983a1ab6b5 Mon Sep 17 00:00:00 2001 From: kmcfaul <45077788+kmcfaul@users.noreply.github.com> Date: Thu, 20 Jun 2024 14:51:52 -0400 Subject: [PATCH] feat(templates): toggle props & improvements (#10473) * feat(templates): toggle props & improvements * remove toggleContent from typeahead template * update template names * update tests * added SimpleSelect tests * fix yarnlock --- package.json | 3 +- ...{DropdownSimple.tsx => SimpleDropdown.tsx} | 39 ++- ...imple.test.tsx => SimpleDropdown.test.tsx} | 58 +++-- ....tsx.snap => SimpleDropdown.test.tsx.snap} | 0 .../Dropdown/examples/DropdownTemplates.md | 20 +- ...eExample.tsx => SimpleDropdownExample.tsx} | 14 +- .../src/components/Dropdown/index.ts | 2 +- .../src/components/Select/CheckboxSelect.tsx | 25 +- .../{SelectSimple.tsx => SimpleSelect.tsx} | 41 ++- ...electTypeahead.tsx => TypeaheadSelect.tsx} | 59 +++-- .../{ => __tests__}/CheckboxSelect.test.tsx | 30 ++- .../CheckboxSelectSnapshots.test.tsx | 2 +- .../Select/__tests__/SimpleSelect.test.tsx | 243 ++++++++++++++++++ .../CheckboxSelectSnapshots.test.tsx.snap | 1 - .../__snapshots__/SimpleSelect.test.tsx.snap | 170 ++++++++++++ .../Select/examples/CheckboxSelectDemo.tsx | 2 +- .../Select/examples/SelectTemplates.md | 10 +- ...ectSimpleDemo.tsx => SimpleSelectDemo.tsx} | 12 +- ...eaheadDemo.tsx => TypeaheadSelectDemo.tsx} | 6 +- .../src/components/Select/index.ts | 4 +- 20 files changed, 614 insertions(+), 127 deletions(-) rename packages/react-templates/src/components/Dropdown/{DropdownSimple.tsx => SimpleDropdown.tsx} (76%) rename packages/react-templates/src/components/Dropdown/__tests__/{DropdownSimple.test.tsx => SimpleDropdown.test.tsx} (80%) rename packages/react-templates/src/components/Dropdown/__tests__/__snapshots__/{DropdownSimple.test.tsx.snap => SimpleDropdown.test.tsx.snap} (100%) rename packages/react-templates/src/components/Dropdown/examples/{DropdownSimpleExample.tsx => SimpleDropdownExample.tsx} (83%) rename packages/react-templates/src/components/Select/{SelectSimple.tsx => SimpleSelect.tsx} (66%) rename packages/react-templates/src/components/Select/{SelectTypeahead.tsx => TypeaheadSelect.tsx} (86%) rename packages/react-templates/src/components/Select/{ => __tests__}/CheckboxSelect.test.tsx (88%) rename packages/react-templates/src/components/Select/{ => __tests__}/CheckboxSelectSnapshots.test.tsx (94%) create mode 100644 packages/react-templates/src/components/Select/__tests__/SimpleSelect.test.tsx rename packages/react-templates/src/components/Select/{ => __tests__}/__snapshots__/CheckboxSelectSnapshots.test.tsx.snap (99%) create mode 100644 packages/react-templates/src/components/Select/__tests__/__snapshots__/SimpleSelect.test.tsx.snap rename packages/react-templates/src/components/Select/examples/{SelectSimpleDemo.tsx => SimpleSelectDemo.tsx} (59%) rename packages/react-templates/src/components/Select/examples/{SelectTypeaheadDemo.tsx => TypeaheadSelectDemo.tsx} (81%) diff --git a/package.json b/package.json index 8e0d30c17d8..caf7b40e41e 100644 --- a/package.json +++ b/package.json @@ -111,5 +111,6 @@ "packages/**" ] }, - "dependencies": {} + "dependencies": {}, + "packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e" } diff --git a/packages/react-templates/src/components/Dropdown/DropdownSimple.tsx b/packages/react-templates/src/components/Dropdown/SimpleDropdown.tsx similarity index 76% rename from packages/react-templates/src/components/Dropdown/DropdownSimple.tsx rename to packages/react-templates/src/components/Dropdown/SimpleDropdown.tsx index 7f2c773e92e..31e770ea1a0 100644 --- a/packages/react-templates/src/components/Dropdown/DropdownSimple.tsx +++ b/packages/react-templates/src/components/Dropdown/SimpleDropdown.tsx @@ -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 { +export interface SimpleDropdownItem extends Omit { /** 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 */ @@ -24,9 +25,9 @@ export interface DropdownSimpleItem extends Omit { isDivider?: boolean; } -export interface DropdownSimpleProps extends OUIAProps { +export interface SimpleDropdownProps extends Omit, OUIAProps { /** Initial items of the dropdown. */ - initialItems?: DropdownSimpleItem[]; + initialItems?: SimpleDropdownItem[]; /** @hide Forwarded ref */ innerRef?: React.Ref; /** Flag indicating the dropdown should be disabled. */ @@ -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 = ({ +const SimpleDropdownBase: React.FunctionComponent = ({ innerRef, initialItems, onSelect: onSelectProp, @@ -59,13 +64,16 @@ const DropdownSimpleBase: React.FunctionComponent = ({ toggleContent, isToggleFullWidth, toggleVariant = 'default', + toggleWidth, + toggleProps, shouldFocusToggleOnSelect, ...props -}: DropdownSimpleProps) => { +}: SimpleDropdownProps) => { const [isOpen, setIsOpen] = React.useState(false); const onSelect = (event: React.MouseEvent, value: string | number) => { onSelectProp && onSelectProp(event, value); + onToggleProp && onToggleProp(false); setIsOpen(false); }; @@ -83,6 +91,12 @@ const DropdownSimpleBase: React.FunctionComponent = ({ variant={toggleVariant} aria-label={toggleAriaLabel} isFullWidth={isToggleFullWidth} + style={ + { + width: toggleWidth + } as React.CSSProperties + } + {...toggleProps} > {toggleContent} @@ -106,7 +120,10 @@ const DropdownSimpleBase: React.FunctionComponent = ({ isOpen={isOpen} onSelect={onSelect} shouldFocusToggleOnSelect={shouldFocusToggleOnSelect} - onOpenChange={(isOpen) => setIsOpen(isOpen)} + onOpenChange={(isOpen) => { + onToggleProp && onToggleProp(isOpen); + setIsOpen(isOpen); + }} ref={innerRef} {...props} > @@ -115,8 +132,8 @@ const DropdownSimpleBase: React.FunctionComponent = ({ ); }; -export const DropdownSimple = React.forwardRef((props: DropdownSimpleProps, ref: React.Ref) => ( - +export const SimpleDropdown = React.forwardRef((props: SimpleDropdownProps, ref: React.Ref) => ( + )); -DropdownSimple.displayName = 'DropdownSimple'; +SimpleDropdown.displayName = 'SimpleDropdown'; diff --git a/packages/react-templates/src/components/Dropdown/__tests__/DropdownSimple.test.tsx b/packages/react-templates/src/components/Dropdown/__tests__/SimpleDropdown.test.tsx similarity index 80% rename from packages/react-templates/src/components/Dropdown/__tests__/DropdownSimple.test.tsx rename to packages/react-templates/src/components/Dropdown/__tests__/SimpleDropdown.test.tsx index ddd8b37df66..15a141e518f 100644 --- a/packages/react-templates/src/components/Dropdown/__tests__/DropdownSimple.test.tsx +++ b/packages/react-templates/src/components/Dropdown/__tests__/SimpleDropdown.test.tsx @@ -1,30 +1,42 @@ 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(); + render(); expect(screen.getByRole('button', { name: 'Dropdown' })).not.toBeDisabled(); }); test('Renders dropdown toggle as disabled when isDisabled is true', () => { - render(); + render(); expect(screen.getByRole('button', { name: 'Dropdown' })).toBeDisabled(); }); test('Passes toggleVariant', () => { - render(); + render(); expect(screen.getByRole('button', { name: 'Dropdown' })).toHaveClass(styles.modifiers.plain); }); + test('Passes toggleWidth', () => { + render(); + + expect(screen.getByRole('button', { name: 'Dropdown' })).toHaveAttribute('style', 'width: 500px;'); + }); + + test('Passes additional toggleProps', () => { + render(); + + expect(screen.getByRole('button', { name: 'Dropdown' })).toHaveAttribute('id', 'toggle'); + }); + test('Passes toggleAriaLabel', () => { - render(); + render(); expect(screen.getByRole('button')).toHaveAccessibleName('Aria label content'); }); @@ -32,7 +44,7 @@ describe('Dropdown toggle', () => { test('Calls onToggle with next isOpen state when dropdown toggle is clicked', async () => { const onToggle = jest.fn(); const user = userEvent.setup(); - render(); + render(); const toggle = screen.getByRole('button', { name: 'Dropdown' }); await user.click(toggle); @@ -46,7 +58,7 @@ describe('Dropdown toggle', () => { render(
- +
); @@ -59,7 +71,7 @@ describe('Dropdown toggle', () => { const onSelect = jest.fn(); const items = [{ content: 'Action', value: 1 }]; const user = userEvent.setup(); - render(); + render(); const toggle = screen.getByRole('button', { name: 'Dropdown' }); await user.click(toggle); @@ -73,7 +85,7 @@ describe('Dropdown toggle', () => { const onSelect = jest.fn(); const items = [{ content: 'Action', value: 1 }]; const user = userEvent.setup(); - render(); + render(); const toggle = screen.getByRole('button', { name: 'Dropdown' }); await user.click(toggle); @@ -82,13 +94,13 @@ describe('Dropdown toggle', () => { }); test('Does not pass isToggleFullWidth to menu toggle by default', () => { - render(); + render(); expect(screen.getByRole('button', { name: 'Dropdown' })).not.toHaveClass(styles.modifiers.fullWidth); }); test('Passes isToggleFullWidth to menu toggle when passed in', () => { - render(); + render(); expect(screen.getByRole('button', { name: 'Dropdown' })).toHaveClass(styles.modifiers.fullWidth); }); @@ -96,7 +108,7 @@ describe('Dropdown toggle', () => { test('Does not focus toggle on item select by default', async () => { const items = [{ content: 'Action', value: 1 }]; const user = userEvent.setup(); - render(); + render(); const toggle = screen.getByRole('button', { name: 'Dropdown' }); await user.click(toggle); @@ -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(); + render(); const toggle = screen.getByRole('button', { name: 'Dropdown' }); await user.click(toggle); @@ -120,7 +132,7 @@ describe('Dropdown toggle', () => { }); test('Matches snapshot', () => { - const { asFragment } = render(); + const { asFragment } = render(); expect(asFragment()).toMatchSnapshot(); }); @@ -133,7 +145,7 @@ describe('Dropdown items', () => { { value: 'separator', isDivider: true } ]; const user = userEvent.setup(); - render(); + render(); const toggle = screen.getByRole('button', { name: 'Dropdown' }); await user.click(toggle); @@ -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(); + render(); const toggle = screen.getByRole('button', { name: 'Dropdown' }); await user.click(toggle); @@ -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(); + render(); const toggle = screen.getByRole('button', { name: 'Dropdown' }); await user.click(toggle); @@ -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(); + render(); const toggle = screen.getByRole('button', { name: 'Dropdown' }); await user.click(toggle); @@ -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(); + render(); const toggle = screen.getByRole('button', { name: 'Dropdown' }); await user.click(toggle); @@ -196,7 +208,7 @@ describe('Dropdown items', () => { const onClick = jest.fn(); const items = [{ content: 'Action', value: 1, onClick }]; const user = userEvent.setup(); - render(); + render(); const toggle = screen.getByRole('button', { name: 'Dropdown' }); await user.click(toggle); @@ -213,7 +225,7 @@ describe('Dropdown items', () => { { content: 'Action 2', value: 2 } ]; const user = userEvent.setup(); - render(); + render(); const toggle = screen.getByRole('button', { name: 'Dropdown' }); await user.click(toggle); @@ -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(); + render(); const toggle = screen.getByRole('button', { name: 'Dropdown' }); await user.click(toggle); @@ -244,7 +256,7 @@ describe('Dropdown items', () => { { content: 'Link', value: 'separator', to: '#', ouiaId: '3' } ]; const user = userEvent.setup(); - const { asFragment } = render(); + const { asFragment } = render(); const toggle = screen.getByRole('button', { name: 'Dropdown' }); await user.click(toggle); diff --git a/packages/react-templates/src/components/Dropdown/__tests__/__snapshots__/DropdownSimple.test.tsx.snap b/packages/react-templates/src/components/Dropdown/__tests__/__snapshots__/SimpleDropdown.test.tsx.snap similarity index 100% rename from packages/react-templates/src/components/Dropdown/__tests__/__snapshots__/DropdownSimple.test.tsx.snap rename to packages/react-templates/src/components/Dropdown/__tests__/__snapshots__/SimpleDropdown.test.tsx.snap diff --git a/packages/react-templates/src/components/Dropdown/examples/DropdownTemplates.md b/packages/react-templates/src/components/Dropdown/examples/DropdownTemplates.md index 723e8acc93c..92382f1b179 100644 --- a/packages/react-templates/src/components/Dropdown/examples/DropdownTemplates.md +++ b/packages/react-templates/src/components/Dropdown/examples/DropdownTemplates.md @@ -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" ``` diff --git a/packages/react-templates/src/components/Dropdown/examples/DropdownSimpleExample.tsx b/packages/react-templates/src/components/Dropdown/examples/SimpleDropdownExample.tsx similarity index 83% rename from packages/react-templates/src/components/Dropdown/examples/DropdownSimpleExample.tsx rename to packages/react-templates/src/components/Dropdown/examples/SimpleDropdownExample.tsx index 6129f7ff33f..449cd238a58 100644 --- a/packages/react-templates/src/components/Dropdown/examples/DropdownSimpleExample.tsx +++ b/packages/react-templates/src/components/Dropdown/examples/SimpleDropdownExample.tsx @@ -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 @@ -21,17 +21,17 @@ export const DropdownSimpleExample: React.FunctionComponent = () => { , checked: boolean) => setIsDisabled(checked)} style={{ marginBottom: 20 }} /> - + - { /> - { @@ -16,7 +18,7 @@ export interface CheckboxSelectOption extends Omit value: string | number; } -export interface CheckboxSelectProps { +export interface CheckboxSelectProps extends Omit { /** @hide Forwarded ref */ innerRef?: React.Ref; /** Initial options of the select. */ @@ -27,8 +29,12 @@ export interface CheckboxSelectProps { onToggle?: (nextIsOpen: boolean) => void; /** Flag indicating the select should be disabled. */ isDisabled?: boolean; - /** Content of the toggle. Defaults to the selected option. */ + /** Content of the toggle. Defaults to a string with badge count of selected options. */ toggleContent?: React.ReactNode; + /** Width of the toggle. */ + toggleWidth?: string; + /** Additional props passed to the toggle. */ + toggleProps?: MenuToggleProps; } const CheckboxSelectBase: React.FunctionComponent = ({ @@ -38,6 +44,8 @@ const CheckboxSelectBase: React.FunctionComponent = ({ onSelect: passedOnSelect, onToggle, toggleContent, + toggleWidth = '200px', + toggleProps, ...props }: CheckboxSelectProps) => { const [isOpen, setIsOpen] = React.useState(false); @@ -47,7 +55,7 @@ const CheckboxSelectBase: React.FunctionComponent = ({ const { content, value, ...props } = option; const isSelected = selected.includes(`${value}`); return ( - + {content} ); @@ -83,9 +91,10 @@ const CheckboxSelectBase: React.FunctionComponent = ({ isDisabled={isDisabled} style={ { - width: '200px' + width: toggleWidth } as React.CSSProperties } + {...toggleProps} > {toggleContent || defaultToggleContent} @@ -93,11 +102,13 @@ const CheckboxSelectBase: React.FunctionComponent = ({ return ( setIsOpen(isOpen)} + onOpenChange={(isOpen) => { + onToggle && onToggle(isOpen); + setIsOpen(isOpen); + }} toggle={toggle} shouldFocusToggleOnSelect ref={innerRef} @@ -93,8 +106,8 @@ const SelectSimpleBase: React.FunctionComponent = ({ ); }; -export const SelectSimple = React.forwardRef((props: SelectSimpleProps, ref: React.Ref) => ( - +export const SimpleSelect = React.forwardRef((props: SimpleSelectProps, ref: React.Ref) => ( + )); -SelectSimple.displayName = 'SelectSimple'; +SimpleSelect.displayName = 'SimpleSelect'; diff --git a/packages/react-templates/src/components/Select/SelectTypeahead.tsx b/packages/react-templates/src/components/Select/TypeaheadSelect.tsx similarity index 86% rename from packages/react-templates/src/components/Select/SelectTypeahead.tsx rename to packages/react-templates/src/components/Select/TypeaheadSelect.tsx index 4d2657ad2bf..eeb9bf37af1 100644 --- a/packages/react-templates/src/components/Select/SelectTypeahead.tsx +++ b/packages/react-templates/src/components/Select/TypeaheadSelect.tsx @@ -9,22 +9,24 @@ import { TextInputGroup, TextInputGroupMain, TextInputGroupUtilities, - Button + Button, + MenuToggleProps, + SelectProps } from '@patternfly/react-core'; import TimesIcon from '@patternfly/react-icons/dist/esm/icons/times-icon'; -export interface SelectTypeaheadOption extends Omit { +export interface TypeaheadSelectOption extends Omit { /** Content of the select option. */ content: string | number; /** Value of the select option. */ value: string | number; } -export interface SelectTypeaheadProps { +export interface TypeaheadSelectProps extends Omit { /** @hide Forwarded ref */ innerRef?: React.Ref; /** Initial options of the select. */ - initialOptions: SelectTypeaheadOption[]; + initialOptions: TypeaheadSelectOption[]; /** Callback triggered on selection. */ onSelect?: ( _event: React.MouseEvent | React.KeyboardEvent, @@ -40,9 +42,13 @@ export interface SelectTypeaheadProps { noOptionsFoundMessage?: string | ((filter: string) => string); /** Flag indicating the select should be disabled. */ isDisabled?: boolean; + /** Width of the toggle. */ + toggleWidth?: string; + /** Additional props passed to the toggle. */ + toggleProps?: MenuToggleProps; } -export const SelectTypeaheadBase: React.FunctionComponent = ({ +export const TypeaheadSelectBase: React.FunctionComponent = ({ innerRef, initialOptions, onSelect, @@ -51,13 +57,15 @@ export const SelectTypeaheadBase: React.FunctionComponent placeholder = 'Select an option', noOptionsFoundMessage = (filter) => `No results found for "${filter}"`, isDisabled, + toggleWidth, + toggleProps, ...props -}: SelectTypeaheadProps) => { +}: TypeaheadSelectProps) => { const [isOpen, setIsOpen] = React.useState(false); const [selected, setSelected] = React.useState(''); const [inputValue, setInputValue] = React.useState(''); const [filterValue, setFilterValue] = React.useState(''); - const [selectOptions, setSelectOptions] = React.useState(initialOptions); + const [selectOptions, setSelectOptions] = React.useState(initialOptions); const [focusedItemIndex, setFocusedItemIndex] = React.useState(null); const [activeItemId, setActiveItemId] = React.useState(null); const textInputRef = React.useRef(); @@ -65,7 +73,7 @@ export const SelectTypeaheadBase: React.FunctionComponent const NO_RESULTS = 'no results'; React.useEffect(() => { - let newSelectOptions: SelectTypeaheadOption[] = initialOptions; + let newSelectOptions: TypeaheadSelectOption[] = initialOptions; // Filter menu items based on the text input value when one exists if (filterValue) { @@ -92,14 +100,12 @@ export const SelectTypeaheadBase: React.FunctionComponent } setSelectOptions(newSelectOptions); - }, [filterValue]); - - const createItemId = (value: string | number) => `select-typeahead-${String(value).replace(' ', '-')}`; + }, [filterValue, initialOptions]); const setActiveAndFocusedItem = (itemIndex: number) => { setFocusedItemIndex(itemIndex); const focusedItem = selectOptions[itemIndex]; - setActiveItemId(createItemId(focusedItem.value)); + setActiveItemId(focusedItem.value as string); }; const resetActiveAndFocusedItem = () => { @@ -128,7 +134,7 @@ export const SelectTypeaheadBase: React.FunctionComponent const selectOption = ( _event: React.MouseEvent | React.KeyboardEvent | undefined, - option: SelectTypeaheadOption + option: TypeaheadSelectOption ) => { onSelect && onSelect(_event, option.value); @@ -216,6 +222,7 @@ export const SelectTypeaheadBase: React.FunctionComponent } if (!isOpen) { + onToggle && onToggle(true); setIsOpen(true); } @@ -252,6 +259,12 @@ export const SelectTypeaheadBase: React.FunctionComponent isExpanded={isOpen} isDisabled={isDisabled} isFullWidth + style={ + { + width: toggleWidth + } as React.CSSProperties + } + {...toggleProps} > onClick={onInputClick} onChange={onTextInputChange} onKeyDown={onInputKeyDown} - id="typeahead-select-input" autoComplete="off" innerRef={textInputRef} placeholder={placeholder} @@ -280,7 +292,6 @@ export const SelectTypeaheadBase: React.FunctionComponent return ( ); }; -SelectTypeaheadBase.displayName = 'SelectTypeaheadBase'; +TypeaheadSelectBase.displayName = 'TypeaheadSelectBase'; -export const SelectTypeahead = React.forwardRef((props: SelectTypeaheadProps, ref: React.Ref) => ( - +export const TypeaheadSelect = React.forwardRef((props: TypeaheadSelectProps, ref: React.Ref) => ( + )); -SelectTypeahead.displayName = 'SelectTypeahead'; +TypeaheadSelect.displayName = 'TypeaheadSelect'; diff --git a/packages/react-templates/src/components/Select/CheckboxSelect.test.tsx b/packages/react-templates/src/components/Select/__tests__/CheckboxSelect.test.tsx similarity index 88% rename from packages/react-templates/src/components/Select/CheckboxSelect.test.tsx rename to packages/react-templates/src/components/Select/__tests__/CheckboxSelect.test.tsx index e47a40783fe..1c1292af863 100644 --- a/packages/react-templates/src/components/Select/CheckboxSelect.test.tsx +++ b/packages/react-templates/src/components/Select/__tests__/CheckboxSelect.test.tsx @@ -1,7 +1,7 @@ import * as React from 'react'; import { render, screen, waitForElementToBeRemoved } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; -import { CheckboxSelect } from './CheckboxSelect'; +import { CheckboxSelect } from '../CheckboxSelect'; import styles from '@patternfly/react-styles/css/components/Badge/badge'; test('renders checkbox select with options', async () => { @@ -156,6 +156,34 @@ test('displays custom toggle content', async () => { expect(toggleButton).toBeInTheDocument(); }); +test('Passes toggleWidth', () => { + const initialOptions = [ + { content: 'Option 1', value: 'option1' }, + { content: 'Option 2', value: 'option2' }, + { content: 'Option 3', value: 'option3' } + ]; + + render(); + + const toggleButton = screen.getByRole('button', { name: 'Custom Toggle' }); + expect(toggleButton).toHaveAttribute('style', 'width: 500px;'); +}); + +test('Passes additional toggleProps', () => { + const initialOptions = [ + { content: 'Option 1', value: 'option1' }, + { content: 'Option 2', value: 'option2' }, + { content: 'Option 3', value: 'option3' } + ]; + + render( + + ); + + const toggleButton = screen.getByRole('button', { name: 'Custom Toggle' }); + expect(toggleButton).toHaveAttribute('id', 'toggle'); +}); + test('calls the onToggle callback when the select opens or closes', async () => { const initialOptions = [ { content: 'Option 1', value: 'option1' }, diff --git a/packages/react-templates/src/components/Select/CheckboxSelectSnapshots.test.tsx b/packages/react-templates/src/components/Select/__tests__/CheckboxSelectSnapshots.test.tsx similarity index 94% rename from packages/react-templates/src/components/Select/CheckboxSelectSnapshots.test.tsx rename to packages/react-templates/src/components/Select/__tests__/CheckboxSelectSnapshots.test.tsx index 5e305d00042..b0b52331ae6 100644 --- a/packages/react-templates/src/components/Select/CheckboxSelectSnapshots.test.tsx +++ b/packages/react-templates/src/components/Select/__tests__/CheckboxSelectSnapshots.test.tsx @@ -1,7 +1,7 @@ import * as React from 'react'; import { render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; -import { CheckboxSelect } from './CheckboxSelect'; +import { CheckboxSelect } from '../CheckboxSelect'; jest.mock('@patternfly/react-core/dist/js/helpers/GenerateId/GenerateId', () => ({ GenerateId: ({ children }) => children('generated-id') diff --git a/packages/react-templates/src/components/Select/__tests__/SimpleSelect.test.tsx b/packages/react-templates/src/components/Select/__tests__/SimpleSelect.test.tsx new file mode 100644 index 00000000000..ef02b577870 --- /dev/null +++ b/packages/react-templates/src/components/Select/__tests__/SimpleSelect.test.tsx @@ -0,0 +1,243 @@ +import * as React from 'react'; +import { render, screen, waitForElementToBeRemoved } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { SimpleSelect } from '../SimpleSelect'; + +test('renders checkbox select with options', async () => { + const initialOptions = [ + { content: 'Option 1', value: 'option1' }, + { content: 'Option 2', value: 'option2' }, + { content: 'Option 3', value: 'option3' } + ]; + + const user = userEvent.setup(); + + render(); + + const toggle = screen.getByRole('button', { name: 'Select a value' }); + + await user.click(toggle); + + const option1 = screen.getByText('Option 1'); + const option2 = screen.getByText('Option 2'); + const option3 = screen.getByText('Option 3'); + + expect(option1).toBeInTheDocument(); + expect(option2).toBeInTheDocument(); + expect(option3).toBeInTheDocument(); +}); + +test('selects options when clicked', async () => { + const initialOptions = [ + { content: 'Option 1', value: 'option1' }, + { content: 'Option 2', value: 'option2' }, + { content: 'Option 3', value: 'option3' } + ]; + + const user = userEvent.setup(); + + render(); + + const toggle = screen.getByRole('button', { name: 'Select a value' }); + + await user.click(toggle); + + const option1 = screen.getByText('Option 1'); + await user.click(option1); + + expect(option1).toBeInTheDocument(); +}); + +test('calls the onSelect callback with the selected value when an option is selected', async () => { + const initialOptions = [ + { content: 'Option 1', value: 'option1' }, + { content: 'Option 2', value: 'option2' }, + { content: 'Option 3', value: 'option3' } + ]; + + const user = userEvent.setup(); + const onSelectMock = jest.fn(); + + render(); + + const toggle = screen.getByRole('button', { name: 'Select a value' }); + + await user.click(toggle); + + expect(onSelectMock).not.toHaveBeenCalled(); + + const option1 = screen.getByText('Option 1'); + + await user.click(option1); + + expect(onSelectMock).toHaveBeenCalledTimes(1); + expect(onSelectMock).toHaveBeenCalledWith(expect.anything(), 'option1'); +}); + +test('toggles the select menu when the toggle button is clicked', async () => { + const initialOptions = [ + { content: 'Option 1', value: 'option1' }, + { content: 'Option 2', value: 'option2' }, + { content: 'Option 3', value: 'option3' } + ]; + + const user = userEvent.setup(); + + render(); + + const toggleButton = screen.getByRole('button', { name: 'Select a value' }); + + await user.click(toggleButton); + + expect(screen.getByRole('listbox')).toBeInTheDocument(); + + await user.click(toggleButton); + + await waitForElementToBeRemoved(() => screen.queryByRole('listbox')); + + expect(screen.queryByRole('listbox')).not.toBeInTheDocument(); +}); + +test('displays custom toggle content', async () => { + const initialOptions = [ + { content: 'Option 1', value: 'option1' }, + { content: 'Option 2', value: 'option2' }, + { content: 'Option 3', value: 'option3' } + ]; + + render(); + + const toggleButton = screen.getByRole('button', { name: 'Custom Toggle' }); + + expect(toggleButton).toBeInTheDocument(); +}); + +test('Passes toggleWidth', () => { + const initialOptions = [ + { content: 'Option 1', value: 'option1' }, + { content: 'Option 2', value: 'option2' }, + { content: 'Option 3', value: 'option3' } + ]; + + render(); + + const toggleButton = screen.getByRole('button', { name: 'Custom Toggle' }); + expect(toggleButton).toHaveAttribute('style', 'width: 500px;'); +}); + +test('Passes additional toggleProps', () => { + const initialOptions = [ + { content: 'Option 1', value: 'option1' }, + { content: 'Option 2', value: 'option2' }, + { content: 'Option 3', value: 'option3' } + ]; + + render(); + + const toggleButton = screen.getByRole('button', { name: 'Custom Toggle' }); + expect(toggleButton).toHaveAttribute('id', 'toggle'); +}); + +test('calls the onToggle callback when the select opens or closes', async () => { + const initialOptions = [ + { content: 'Option 1', value: 'option1' }, + { content: 'Option 2', value: 'option2' }, + { content: 'Option 3', value: 'option3' } + ]; + + const user = userEvent.setup(); + const onToggleMock = jest.fn(); + + render(); + + const toggle = screen.getByRole('button', { name: 'Select a value' }); + + await user.click(toggle); + + expect(onToggleMock).toHaveBeenCalledTimes(1); + expect(onToggleMock).toHaveBeenCalledWith(true); + + await user.click(toggle); + + expect(onToggleMock).toHaveBeenCalledTimes(2); + expect(onToggleMock).toHaveBeenCalledWith(false); +}); + +test('does not call the onToggle callback when the toggle is not clicked', async () => { + const initialOptions = [ + { content: 'Option 1', value: 'option1' }, + { content: 'Option 2', value: 'option2' }, + { content: 'Option 3', value: 'option3' } + ]; + + const onToggleMock = jest.fn(); + + render(); + + expect(onToggleMock).not.toHaveBeenCalled(); +}); + +test('disables the select when isDisabled prop is true', async () => { + const initialOptions = [ + { content: 'Option 1', value: 'option1' }, + { content: 'Option 2', value: 'option2' }, + { content: 'Option 3', value: 'option3' } + ]; + + const user = userEvent.setup(); + + render(); + + const toggleButton = screen.getByRole('button', { name: 'Select a value' }); + + expect(toggleButton).toBeDisabled(); + + await user.click(toggleButton); + + expect(screen.queryByRole('menu')).not.toBeInTheDocument(); +}); + +test('passes other SelectOption props to the SelectOption component', async () => { + const initialOptions = [{ content: 'Option 1', value: 'option1', isDisabled: true }]; + + const user = userEvent.setup(); + + render(); + + const toggle = screen.getByRole('button', { name: 'Select a value' }); + + await user.click(toggle); + + const option1 = screen.getByRole('option', { name: 'Option 1' }); + + expect(option1).toBeDisabled(); +}); + +jest.mock('@patternfly/react-core/dist/js/helpers/GenerateId/GenerateId', () => ({ + GenerateId: ({ children }) => children('generated-id') +})); + +test('checkbox select with no props snapshot', () => { + const { asFragment } = render(); + + expect(asFragment()).toMatchSnapshot(); +}); + +test('Matches snapshot', async () => { + const initialOptions = [ + { content: 'Option 1', value: 'option1' }, + { content: 'Option 2', value: 'option2' }, + { content: 'Option 3', value: 'option3' } + ]; + + const user = userEvent.setup(); + + render(); + + const { asFragment } = render(); + + const toggle = screen.getByRole('button', { name: 'Select' }); + await user.click(toggle); + + expect(asFragment()).toMatchSnapshot(); +}); diff --git a/packages/react-templates/src/components/Select/__snapshots__/CheckboxSelectSnapshots.test.tsx.snap b/packages/react-templates/src/components/Select/__tests__/__snapshots__/CheckboxSelectSnapshots.test.tsx.snap similarity index 99% rename from packages/react-templates/src/components/Select/__snapshots__/CheckboxSelectSnapshots.test.tsx.snap rename to packages/react-templates/src/components/Select/__tests__/__snapshots__/CheckboxSelectSnapshots.test.tsx.snap index 6d8189bf009..f0d7630a5cd 100644 --- a/packages/react-templates/src/components/Select/__snapshots__/CheckboxSelectSnapshots.test.tsx.snap +++ b/packages/react-templates/src/components/Select/__tests__/__snapshots__/CheckboxSelectSnapshots.test.tsx.snap @@ -81,7 +81,6 @@ exports[`opened checkbox select snapshot 1`] = ` data-popper-escaped="true" data-popper-placement="bottom-start" data-popper-reference-hidden="true" - id="checkbox-select" style="position: absolute; left: 0px; top: 0px; z-index: 9999; opacity: 1; transition: opacity 0ms cubic-bezier(.54, 1.5, .38, 1.11); min-width: 0px; transform: translate(0px, 0px);" >
+ +
+
+
    +
  • + +
  • +
  • + +
  • +
  • + +
  • +
+
+
+ +`; + +exports[`checkbox select with no props snapshot 1`] = ` + + + +`; diff --git a/packages/react-templates/src/components/Select/examples/CheckboxSelectDemo.tsx b/packages/react-templates/src/components/Select/examples/CheckboxSelectDemo.tsx index 676bd922868..f096f1887c0 100644 --- a/packages/react-templates/src/components/Select/examples/CheckboxSelectDemo.tsx +++ b/packages/react-templates/src/components/Select/examples/CheckboxSelectDemo.tsx @@ -4,7 +4,7 @@ import { CheckboxSelect, CheckboxSelectOption } from '@patternfly/react-template export const SelectBasic: React.FunctionComponent = () => { const initialOptions: CheckboxSelectOption[] = [ { content: 'Option 1', value: 'option-1' }, - { content: 'Option 2', value: 'option-2' }, + { content: 'Option 2', value: 'option-2', description: 'Option with description' }, { content: 'Option 3', value: 'option-3', isDisabled: true }, { content: 'Option 4', value: 'option-4' } ]; diff --git a/packages/react-templates/src/components/Select/examples/SelectTemplates.md b/packages/react-templates/src/components/Select/examples/SelectTemplates.md index db22de4f084..237d67df510 100644 --- a/packages/react-templates/src/components/Select/examples/SelectTemplates.md +++ b/packages/react-templates/src/components/Select/examples/SelectTemplates.md @@ -4,21 +4,21 @@ section: components subsection: menus template: true beta: true -propComponents: ['SelectSimple', 'CheckboxSelect', 'SelectTypeahead'] +propComponents: ['SimpleSelect', 'CheckboxSelect', 'TypeaheadSelect'] --- 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 select component suite from [@patternfly/react-core](https://www.npmjs.com/package/@patternfly/react-core). -import { SelectOption, Checkbox } from '@patternfly/react-core'; -import { SelectSimple, CheckboxSelect, SelectTypeahead } from '@patternfly/react-templates'; +import { Checkbox } from '@patternfly/react-core'; +import { SimpleSelect, CheckboxSelect, TypeaheadSelect } from '@patternfly/react-templates'; ## Select template examples ### Simple -```ts file="SelectSimpleDemo.tsx" +```ts file="SimpleSelectDemo.tsx" ``` @@ -30,6 +30,6 @@ import { SelectSimple, CheckboxSelect, SelectTypeahead } from '@patternfly/react ### Typeahead -```ts file="SelectTypeaheadDemo.tsx" +```ts file="TypeaheadSelectDemo.tsx" ``` diff --git a/packages/react-templates/src/components/Select/examples/SelectSimpleDemo.tsx b/packages/react-templates/src/components/Select/examples/SimpleSelectDemo.tsx similarity index 59% rename from packages/react-templates/src/components/Select/examples/SelectSimpleDemo.tsx rename to packages/react-templates/src/components/Select/examples/SimpleSelectDemo.tsx index da98bbcfcf2..3a2d67f277a 100644 --- a/packages/react-templates/src/components/Select/examples/SelectSimpleDemo.tsx +++ b/packages/react-templates/src/components/Select/examples/SimpleSelectDemo.tsx @@ -1,14 +1,14 @@ import React from 'react'; import { Checkbox } from '@patternfly/react-core'; -import { SelectSimple, SelectSimpleOption } from '@patternfly/react-templates'; +import { SimpleSelect, SimpleSelectOption } from '@patternfly/react-templates'; export const SelectSimpleDemo: React.FunctionComponent = () => { const [isDisabled, setIsDisabled] = React.useState(false); - const initialOptions: SelectSimpleOption[] = [ - { content: 'Option 1', value: 'option1' }, - { content: 'Option 2', value: 'option2' }, - { content: 'Option 3', value: 'option3' } + const initialOptions: SimpleSelectOption[] = [ + { content: 'Option 1', value: 'Option 1' }, + { content: 'Option 2', value: 'Option 2', description: 'Option with description' }, + { content: 'Option 3', value: 'Option 3' } ]; return ( @@ -20,7 +20,7 @@ export const SelectSimpleDemo: React.FunctionComponent = () => { onChange={(_event, checked) => setIsDisabled(checked)} style={{ marginBottom: 20 }} /> - + ); }; diff --git a/packages/react-templates/src/components/Select/examples/SelectTypeaheadDemo.tsx b/packages/react-templates/src/components/Select/examples/TypeaheadSelectDemo.tsx similarity index 81% rename from packages/react-templates/src/components/Select/examples/SelectTypeaheadDemo.tsx rename to packages/react-templates/src/components/Select/examples/TypeaheadSelectDemo.tsx index d189783d3f0..765fda790b2 100644 --- a/packages/react-templates/src/components/Select/examples/SelectTypeaheadDemo.tsx +++ b/packages/react-templates/src/components/Select/examples/TypeaheadSelectDemo.tsx @@ -1,8 +1,8 @@ import React from 'react'; -import { SelectTypeahead, SelectTypeaheadOption } from '@patternfly/react-templates'; +import { TypeaheadSelect, TypeaheadSelectOption } from '@patternfly/react-templates'; export const SelectTypeaheadDemo: React.FunctionComponent = () => { - const initialOptions: SelectTypeaheadOption[] = [ + const initialOptions: TypeaheadSelectOption[] = [ { content: 'Alabama', value: 'option1' }, { content: 'Florida', value: 'option2' }, { content: 'New Jersey', value: 'option3' }, @@ -12,7 +12,7 @@ export const SelectTypeaheadDemo: React.FunctionComponent = () => { ]; return ( - `No state was found for "${filter}"`} diff --git a/packages/react-templates/src/components/Select/index.ts b/packages/react-templates/src/components/Select/index.ts index c01277b9ccc..b5a5cb3446e 100644 --- a/packages/react-templates/src/components/Select/index.ts +++ b/packages/react-templates/src/components/Select/index.ts @@ -1,3 +1,3 @@ -export * from './SelectSimple'; +export * from './SimpleSelect'; export * from './CheckboxSelect'; -export * from './SelectTypeahead'; +export * from './TypeaheadSelect';