From 7b507894547a057f7e35da1ed816d70ec62e82e8 Mon Sep 17 00:00:00 2001 From: Dylan Kilgore Date: Tue, 21 Feb 2023 17:37:48 -0800 Subject: [PATCH 1/3] fix: select: improves select by adding props and enabling default value array --- src/components/Dropdown/Dropdown.tsx | 1 - src/components/Select/Select.stories.tsx | 7 + src/components/Select/Select.test.tsx | 288 ++++++++++++++++-- src/components/Select/Select.tsx | 20 +- src/components/Select/Select.types.ts | 22 +- .../Select/__snapshots__/Select.test.tsx.snap | 282 +++++++++++++++++ 6 files changed, 587 insertions(+), 33 deletions(-) diff --git a/src/components/Dropdown/Dropdown.tsx b/src/components/Dropdown/Dropdown.tsx index 6aa3b5801..fb99ddf1d 100644 --- a/src/components/Dropdown/Dropdown.tsx +++ b/src/components/Dropdown/Dropdown.tsx @@ -107,7 +107,6 @@ export const Dropdown: FC = React.memo( refs.floating, (e) => { if (closeOnOutsideClick) { - console.log('outside clicked'); toggle(false)(e); } onClickOutside?.(e); diff --git a/src/components/Select/Select.stories.tsx b/src/components/Select/Select.stories.tsx index 324f720a8..baf95c925 100644 --- a/src/components/Select/Select.stories.tsx +++ b/src/components/Select/Select.stories.tsx @@ -153,6 +153,7 @@ export type SelectStory = ComponentStory>; export const Basic: SelectStory = Basic_Story.bind({}); export const Dynamic_Width: SelectStory = Basic_Story.bind({}); export const With_DefaultValue: SelectStory = Basic_Story.bind({}); +export const With_DefaultValueMultiple: SelectStory = Basic_Story.bind({}); export const Disabled: SelectStory = Basic_Story.bind({}); export const With_Clear: SelectStory = Basic_Story.bind({}); export const Options_Disabled: SelectStory = Basic_Story.bind({}); @@ -189,6 +190,12 @@ With_DefaultValue.args = { defaultValue: 'hat', }; +With_DefaultValueMultiple.args = { + ...Basic.args, + defaultValue: ['date', 'account', 'hat'], + multiple: true, +}; + Disabled.args = { ...With_DefaultValue.args, disabled: true, diff --git a/src/components/Select/Select.test.tsx b/src/components/Select/Select.test.tsx index 91fdaf9dc..ff2ee9971 100644 --- a/src/components/Select/Select.test.tsx +++ b/src/components/Select/Select.test.tsx @@ -1,11 +1,12 @@ import React from 'react'; -import Enzyme, { mount } from 'enzyme'; +import Enzyme from 'enzyme'; import Adapter from '@wojtekmaj/enzyme-adapter-react-17'; import MatchMediaMock from 'jest-matchmedia-mock'; import { SelectShape, SelectSize } from './Select.types'; -import { Select, SelectOption } from './'; -import { fireEvent, render } from '@testing-library/react'; +import { Select } from './'; import { sleep } from '../../tests/Utilities'; +import { fireEvent, render } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; Enzyme.configure({ adapter: new Adapter() }); @@ -41,32 +42,173 @@ describe('Select', () => { await sleep(); } - const defaultOptions: SelectOption[] = [ - { - text: 'School', - value: 'school', - }, + const ANIMATION_DURATION: number = 200; + + const options = [ + { text: 'Option 1', value: 'option1' }, + { text: 'Option 2', value: 'option2' }, + { text: 'Option 3', value: 'option3' }, ]; - test('Select clearable', async () => { + test('Renders without crashing', () => { + const { container, getByRole } = render(); + expect( + container.querySelector('.select-input').getAttribute('readonly') + ).toBeFalsy(); + }); + + test('Opens the dropdown when clicked', async () => { + const { getByRole, getByText } = render( + ); + const select = getByRole('textbox'); + fireEvent.click(select); + await sleep(ANIMATION_DURATION); + const option = getByText('Option 1'); + fireEvent.click(option); + expect(handleChange).toHaveBeenCalledWith(['option1']); + }); + + test('Selects multiple options', async () => { + const handleChange = jest.fn(); + const { getByRole, getByText } = render( + + ); + const select = getByDisplayValue('Option 2'); + expect(() => container).not.toThrowError(); + expect(select).toBeTruthy(); + expect(container).toMatchSnapshot(); + }); + + test('Updates the selected value', async () => { + const defaultValue = 'option2'; + const handleChange = jest.fn(); + const { getByRole, getByText } = render( + + ); + const option2 = getByText('Option 2'); + const option3 = getByText('Option 3'); + expect(() => container).not.toThrowError(); + expect(option2).toBeTruthy(); + expect(option3).toBeTruthy(); + expect(container).toMatchSnapshot(); + }); + + test('Updates the selected values when multiple', async () => { + const defaultValue = ['option2', 'option3']; + const handleChange = jest.fn(); + const { getByRole, getByText } = render( + + ); + const clearButton = container.querySelector('.clear-icon-button'); + expect(clearButton).toBeTruthy(); + }); + + test('Handles clearing the selected value', async () => { + const defaultValue = 'option2'; + const handleChange = jest.fn(); + const { container, getByRole } = render( + ); + await change(container, 'Option 2'); expect( (container.querySelector('.select-input') as HTMLInputElement).value - ).toBe('School'); + ).toBe('Option 2'); fireEvent.click(container.querySelector('.clear-icon-button')); expect( (container.querySelector('.select-input') as HTMLInputElement).value ).toBe(''); + expect(container.querySelector('.clear-icon-button')).toBeFalsy(); }); test('Select backspace clearable', async () => { @@ -100,12 +242,15 @@ describe('Select', () => { fireEvent.keyUp(element, sharedEventConfig); }; - const { container } = render( + ); + await change(container, 'Option 2'); expect( (container.querySelector('.select-input') as HTMLInputElement).value - ).toBe('School'); - let count = 6; + ).toBe('Option 2'); + let count = 8; do { backspace(container.querySelector('.select-input') as HTMLInputElement); } while (count--); @@ -114,44 +259,133 @@ describe('Select', () => { ).toBe(''); }); + test('Renders as disabled', () => { + const { container } = render( + ); + const select = getByRole('textbox'); + fireEvent.click(select); + await sleep(ANIMATION_DURATION); + expect(container.querySelector('.dropdown')).toBeFalsy(); + expect(handleChange).not.toHaveBeenCalled(); + }); + + test('Renders with all options initially visible', async () => { + const { getAllByRole, getByRole } = render( + + ); + const select = getByRole('textbox'); + fireEvent.click(select); + userEvent.type(select, 'Option 1'); + await sleep(ANIMATION_DURATION); + const option1 = getByText('Option 1'); + const option2 = queryByText('Option 2'); + const option3 = queryByText('Option 3'); + expect(option1).toBeTruthy(); + expect(option2).toBeFalsy(); + expect(option3).toBeFalsy(); + }); + + test('Calls onFocus and onBlur callbacks when Select is focused and blurred', () => { + const handleFocus = jest.fn(); + const handleBlur = jest.fn(); + const { getByRole } = render( + + ); + + expect( + (container.querySelector('.select-input') as HTMLInputElement).value + ).toBe(''); + }); + + test('Sets the input element autocomplete attribute to the specified value', () => { + const { container } = render( + + ); + + const input = container.querySelector('.select-input'); + fireEvent.keyDown(input, { key: 'ArrowDown' }); + expect(onKeyDown).toHaveBeenCalledTimes(1); + }); + test('Select is large', () => { const { container } = render( - ); expect(container.firstChild).toMatchSnapshot(); }); test('Select is medium', () => { const { container } = render( - ); expect(container.firstChild).toMatchSnapshot(); }); test('Select is small', () => { const { container } = render( - ); expect(container.firstChild).toMatchSnapshot(); }); test('Select is rectangle shaped', () => { const { container } = render( - ); expect(container.firstChild).toMatchSnapshot(); }); test('Select is pill shaped', () => { const { container } = render( - ); expect(container.firstChild).toMatchSnapshot(); }); test('Select is underline shaped', () => { const { container } = render( - ); expect(container.firstChild).toMatchSnapshot(); }); diff --git a/src/components/Select/Select.tsx b/src/components/Select/Select.tsx index ecf0f6841..c51e90138 100644 --- a/src/components/Select/Select.tsx +++ b/src/components/Select/Select.tsx @@ -44,6 +44,7 @@ const multiSelectCountOffset: number = +styles.multiSelectCountOffset; export const Select: FC = React.forwardRef( ( { + autocomplete, classNames, clearable = false, configContextProps = { @@ -64,7 +65,10 @@ export const Select: FC = React.forwardRef( loadOptions, menuProps = {}, multiple = false, + onBlur, onClear, + onFocus, + onKeyDown, onOptionsChange, options: _options = [], pillProps = {}, @@ -158,10 +162,13 @@ export const Select: FC = React.forwardRef( }, [getSelectedOptions().join('')]); useEffect(() => { - const updatedOptions = options.map((opt: SelectOption) => ({ + const updatedOptions = options.map((opt) => ({ ...opt, selected: - (defaultValue !== undefined && opt.value === defaultValue) || + (defaultValue !== undefined && + (multiple + ? defaultValue.includes(opt.value) + : opt.value === defaultValue)) || opt.selected, })); setOptions(updatedOptions); @@ -336,7 +343,7 @@ export const Select: FC = React.forwardRef( const isPillEllipsisActive = (element: HTMLElement) => { const labelElement: HTMLSpanElement = - element.firstElementChild as HTMLSpanElement; + element?.firstElementChild as HTMLSpanElement; return labelElement?.offsetWidth < labelElement?.scrollWidth; }; @@ -356,6 +363,7 @@ export const Select: FC = React.forwardRef( classNames={styles.selectTooltip} content={value.text} disabled={!isPillEllipsisActive(document?.getElementById(value.id))} + key={`select-tooltip-${index}`} placement={'top'} theme={TooltipTheme.dark} > @@ -364,6 +372,7 @@ export const Select: FC = React.forwardRef( id={value.id} classNames={pillClasses} disabled={mergedDisabled} + key={`select-pill-${index}`} label={value.text} onClose={() => toggleOption(value)} size={selectSizeToPillSizeMap.get(size)} @@ -381,6 +390,7 @@ export const Select: FC = React.forwardRef( = React.forwardRef( { +export interface SelectProps + extends Omit, 'onFocus' | 'onBlur'> { + /** + * Indicates the autocomplete attribute value for the Select input field. + * @default undefined + */ + autocomplete?: string; /** * Whether the select text input is clearable. * @default false @@ -49,7 +55,7 @@ export interface SelectProps extends OcBaseProps { * The select default value. * @default '' */ - defaultValue?: string; + defaultValue?: string | string[]; /** * The select disabled state. * @default false @@ -104,10 +110,22 @@ export interface SelectProps extends OcBaseProps { * @default false */ multiple?: boolean; + /** + * The Select onBlur event handler. + */ + onBlur?: React.FocusEventHandler; /** * Callback called when the clear button is clicked. */ onClear?: () => void; + /** + * The Select onFocus event handler. + */ + onFocus?: React.FocusEventHandler; + /** + * The Select onKeyDown event handler. + */ + onKeyDown?: React.KeyboardEventHandler; /** * Callback called when options are selected/unselected. * @param options {SelectOption[]} diff --git a/src/components/Select/__snapshots__/Select.test.tsx.snap b/src/components/Select/__snapshots__/Select.test.tsx.snap index d2775bb68..71a0267f6 100644 --- a/src/components/Select/__snapshots__/Select.test.tsx.snap +++ b/src/components/Select/__snapshots__/Select.test.tsx.snap @@ -1,5 +1,287 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`Select Renders with default value 1`] = ` +
+
+
+
+
+
+ +
+
+ +
+
+
+
+
+
+
+`; + +exports[`Select Renders with default values when multiple 1`] = ` +
+
+
+
+ +
+
+ +
+
+
+
+
+
+ +
+
+ +
+
+
+
+
+
+
+`; + +exports[`Select Renders without crashing 1`] = ` +
+
+
+
+
+
+ +
+
+ +
+
+
+
+
+
+
+`; + exports[`Select Select is large 1`] = `
Date: Tue, 21 Feb 2023 17:56:08 -0800 Subject: [PATCH 2/3] chore: select: update snap --- src/components/Select/__snapshots__/Select.test.tsx.snap | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/Select/__snapshots__/Select.test.tsx.snap b/src/components/Select/__snapshots__/Select.test.tsx.snap index 71a0267f6..b3158d5c5 100644 --- a/src/components/Select/__snapshots__/Select.test.tsx.snap +++ b/src/components/Select/__snapshots__/Select.test.tsx.snap @@ -78,7 +78,7 @@ exports[`Select Renders with default values when multiple 1`] = ` >
Date: Wed, 22 Feb 2023 09:25:52 -0800 Subject: [PATCH 3/3] chore: select: adds toolltip id --- src/components/Select/Select.tsx | 1 + src/components/Select/__snapshots__/Select.test.tsx.snap | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/components/Select/Select.tsx b/src/components/Select/Select.tsx index c51e90138..81ba83f17 100644 --- a/src/components/Select/Select.tsx +++ b/src/components/Select/Select.tsx @@ -363,6 +363,7 @@ export const Select: FC = React.forwardRef( classNames={styles.selectTooltip} content={value.text} disabled={!isPillEllipsisActive(document?.getElementById(value.id))} + id={`selectTooltip${index}`} key={`select-tooltip-${index}`} placement={'top'} theme={TooltipTheme.dark} diff --git a/src/components/Select/__snapshots__/Select.test.tsx.snap b/src/components/Select/__snapshots__/Select.test.tsx.snap index b3158d5c5..1eda0d62d 100644 --- a/src/components/Select/__snapshots__/Select.test.tsx.snap +++ b/src/components/Select/__snapshots__/Select.test.tsx.snap @@ -78,7 +78,7 @@ exports[`Select Renders with default values when multiple 1`] = ` >