diff --git a/src/lib/components/searchinput/SearchInput.component.tsx b/src/lib/components/searchinput/SearchInput.component.tsx index 82568071bb..036f79e290 100644 --- a/src/lib/components/searchinput/SearchInput.component.tsx +++ b/src/lib/components/searchinput/SearchInput.component.tsx @@ -10,7 +10,6 @@ export type Props = { value: string; onChange: (e: ChangeEvent) => void; onReset?: () => void; - disableToggle: boolean; disabled?: boolean; id?: string; size?: InputSize; @@ -57,7 +56,6 @@ const ClearButton = styled.div` const SearchInput = forwardRef( ( { - disableToggle, placeholder, value, onChange, diff --git a/src/lib/components/searchinput/SearchInput.test.tsx b/src/lib/components/searchinput/SearchInput.test.tsx new file mode 100644 index 0000000000..c9e6514102 --- /dev/null +++ b/src/lib/components/searchinput/SearchInput.test.tsx @@ -0,0 +1,88 @@ +import React from 'react'; +import { render, screen, waitFor } from '@testing-library/react'; +import { SearchInput, Props } from './SearchInput.component'; +import { QueryClient, QueryClientProvider } from 'react-query'; +import userEvent from '@testing-library/user-event'; + +const queryClient = new QueryClient(); + +const SearchInputRender = (props: Props) => { + return ( + + + + ); +}; + +describe('SearchInput', () => { + const selectors = { + searchInput: () => screen.getByRole('searchbox'), + clearButton: () => screen.queryByRole('button'), + }; + it('should render the SearchInput component', () => { + render( {}} />); + + const searchInput = selectors.searchInput(); + expect(searchInput).toBeInTheDocument(); + }); + + it('should render the SearchInput component with placeholder', () => { + render( + {}} placeholder="Search" />, + ); + + const searchInput = screen.queryByPlaceholderText('Example: Search'); + expect(searchInput).toBeInTheDocument(); + }); + + it('should render the SearchInput component with disabled prop', () => { + render( {}} disabled />); + + const searchInput = selectors.searchInput(); + expect(searchInput).toBeInTheDocument(); + expect(searchInput).toBeDisabled(); + }); + + it('should change value instantly but call the onChange function with a 300ms delay after the end of typing', async () => { + const onChange = jest.fn(); + render(); + const searchInput = selectors.searchInput(); + userEvent.type(searchInput, 'test'); + expect(searchInput).toHaveValue('test'); + expect(onChange).not.toHaveBeenCalled(); + await waitFor( + () => { + expect(onChange).toHaveBeenCalled(); + }, + { timeout: 350 }, + ); + }); + + it('should have a clear button when the input is not empty', () => { + render( {}} />); + + // clear button should not be rendered as value is empty + let clearButton = selectors.clearButton(); + expect(clearButton).not.toBeInTheDocument(); + + const searchInput = selectors.searchInput(); + userEvent.type(searchInput, 'test'); + + // clear button should now be rendered + clearButton = selectors.clearButton(); + expect(clearButton).toBeInTheDocument(); + }); + + it('should call the onReset function when the clear button is clicked and clear the input value', async () => { + const onReset = jest.fn(); + render( + {}} onReset={onReset} />, + ); + const searchInput = selectors.searchInput(); + const clearButton = selectors.clearButton(); + expect(clearButton).toBeInTheDocument(); + clearButton && userEvent.click(clearButton); + expect(onReset).toHaveBeenCalled(); + expect(searchInput).toHaveValue(''); + }); +}); diff --git a/src/lib/components/selectv2/Selectv2.component.tsx b/src/lib/components/selectv2/Selectv2.component.tsx index a84cd33e30..f838973e90 100644 --- a/src/lib/components/selectv2/Selectv2.component.tsx +++ b/src/lib/components/selectv2/Selectv2.component.tsx @@ -135,7 +135,11 @@ const InternalOption = (width, isDefaultVariant) => (props) => { ? 'sc-highlighted-matching-text' : ''; return ( - + {part} ); @@ -238,7 +242,6 @@ const MenuList = (props) => { selectedIndex * optionHeight - (ITEMS_PER_SCROLL_WINDOW - 1) * optionHeight; useEffect(() => { if (listRef && listRef.current) { - // @ts-ignore listRef.current.scrollTo( getScrollOffset( listRef.current, @@ -282,6 +285,7 @@ const ValueContainer = ({ children, ...props }) => { const icon = selectedOption ? selectedOption.icon : null; const ariaProps = { innerProps: { + disabled: true, role: props.selectProps.isSearchable ? 'combobox' : 'listbox', 'aria-expanded': props.selectProps.menuIsOpen, 'aria-autocomplete': 'list', @@ -302,7 +306,6 @@ export type SelectProps = { placeholder?: string; disabled?: boolean; children?: React.ReactNode; - defaultValue?: string; value?: string; onFocus?: (event: FocusEvent) => void; onBlur?: (event: FocusEvent) => void; @@ -310,6 +313,7 @@ export type SelectProps = { variant?: 'default' | 'rounded'; size?: '1' | '2/3' | '1/2' | '1/3'; className?: string; + /** use menuPositon='fixed' inside modal to avoid display issue */ menuPosition?: 'fixed' | 'absolute'; }; type SelectOptionProps = { @@ -357,7 +361,6 @@ function SelectWithOptionContext(props: SelectProps) { function SelectBox({ placeholder = 'Select...', disabled = false, - defaultValue, value, onChange, variant = 'default', @@ -366,11 +369,6 @@ function SelectBox({ id, ...rest }: SelectProps) { - if (defaultValue && value) { - console.error( - 'The `defaultValue` will be overridden by the `value` if they are set at the same time.', - ); - } const [keyboardFocusEnabled, setKeyboardFocusEnabled] = useState(false); const [searchSelection, setSearchSelection] = useState(''); const [searchValue, setSearchValue] = useState(''); @@ -414,7 +412,6 @@ function SelectBox({ // Force to reset the value useEffect(() => { if ( - !defaultValue && !isEmptyStringInOptions && value === '' && selectRef.current && @@ -436,7 +433,6 @@ function SelectBox({ value={ searchSelection || options.find((opt) => opt.value === value) } - defaultValue={defaultValue} inputValue={options.length > NOPT_SEARCH ? searchValue : undefined} selectedOption={options.find((opt) => opt.value === value)} keyboardFocusEnabled={keyboardFocusEnabled} diff --git a/src/lib/components/selectv2/selectv2.test.tsx b/src/lib/components/selectv2/selectv2.test.tsx index dc4262c3d6..68af1fcfe0 100644 --- a/src/lib/components/selectv2/selectv2.test.tsx +++ b/src/lib/components/selectv2/selectv2.test.tsx @@ -1,13 +1,7 @@ -import { - screen, - render as testingRender, - within, -} from '@testing-library/react'; +import { screen, render as testingRender } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import React, { useState } from 'react'; import { QueryClient, QueryClientProvider } from 'react-query'; -import { debug } from 'jest-preview'; -import { Icon } from '../icon/Icon.component'; import { Option, Select } from '../selectv2/Selectv2.component'; const render = (args) => { @@ -27,26 +21,24 @@ const generateOptionsData = (n: number) => const generateOptions = (n: number) => { return generateOptionsData(n).map((o, i) => ( - )); }; +const optionsWithScrollSearchBar = generateOptions(10); // more than 8 options should display searchbar + scrollbar + +const simpleOptions = generateOptions(4); // less than 5 options should not displays any scroll/search bar const SelectWrapper = (props) => { - const [value, setValue] = useState(null); + const [value, setValue] = useState(null); return ( ); }; -const variants = ['default', 'rounded']; -const optionsWithScrollSearchBar = generateOptions(10); // more than 8 options should display searchbar + scrollbar - -const simpleOptions = generateOptions(4); // less than 5 options should not displays any scroll/search bar - const SelectReset = (props) => { const [value, setValue] = useState('default'); @@ -64,220 +56,218 @@ const SelectReset = (props) => { }; describe('SelectV2', () => { - const toBeClose = (container) => { - expect(container.getElementsByClassName('sc-select__option').length).toBe( - 0, - ); - }; - - const toBeOpenWith = (container, optionsLength: number) => { - expect(container.getElementsByClassName('sc-select__option').length).toBe( - optionsLength, - ); - }; - - const toggleSelect = (container) => { - userEvent.click(container.querySelector('.sc-select__control')); + const selectors = { + option: (name: string | RegExp) => screen.getByRole('option', { name }), + options: () => screen.queryAllByRole('option'), + select: (withSearch?: boolean, name?: string) => { + if (withSearch) { + return screen.getByRole('combobox', { name }); + } + return screen.getByRole('listbox', { name }); + }, + input: () => screen.getByRole('textbox'), + noOptions: () => screen.getByText(/No options/i), + highlightedText: () => screen.getByRole('mark'), }; - test.each(variants)( - 'should open select on click/Enter/ArrowDown', - (variant) => { - const { container } = render( - {simpleOptions}, - ); - // should open on click - toBeClose(container); - toggleSelect(container); // open - - toBeOpenWith(container, simpleOptions.length); - toggleSelect(container); // close - - userEvent.tab(); // remove focus + it('should throw error if - - , - ); - toggleSelect(container); - expect(getByTestId('disabledOption')).toHaveAttribute( - 'aria-disabled', - 'true', - ); - const icon = getByTestId('option2').querySelector('i'); - expect(icon).not.toBeNull(); - expect(icon).toHaveAttribute('aria-label', 'Deletion-marker '); + + it('should unfocus the search input when the select is closed', () => { + render({optionsWithScrollSearchBar} ); + const select = selectors.select(true); + userEvent.click(select); + let input = selectors.input(); + expect(input).toHaveFocus(); + const option = selectors.option(/Item 1/); + userEvent.click(option); + input = selectors.input(); + expect(input).not.toHaveFocus(); }); - test.each(variants)( - ' - , + + + , ); - expect(onChange).toBeCalledTimes(0); + const select = selectors.select(); + userEvent.click(select); + const option = selectors.option(/Item 1/); + expect(option).toHaveAttribute('aria-disabled', 'true'); + userEvent.hover(option); + const tooltip = screen.getByText(/This option is disabled/); + expect(tooltip).toBeInTheDocument(); }); it('should select with the right selector', async () => { diff --git a/src/lib/components/tablev2/Search.tsx b/src/lib/components/tablev2/Search.tsx index 0ccd123570..970d3c5105 100644 --- a/src/lib/components/tablev2/Search.tsx +++ b/src/lib/components/tablev2/Search.tsx @@ -17,7 +17,7 @@ export type SearchProps = { value?: string; locale?: TableLocalType; totalCount?: number; -} & Omit; +} & Omit; const SearchContainer = styled.div` display: flex; @@ -88,7 +88,6 @@ export function TableSearch(props: SearchProps) { { if (typeof onChange === 'function') { diff --git a/src/lib/components/tabsv2/StyledTabs.ts b/src/lib/components/tabsv2/StyledTabs.ts index 4cfc72b422..e7ce0a6cf4 100644 --- a/src/lib/components/tabsv2/StyledTabs.ts +++ b/src/lib/components/tabsv2/StyledTabs.ts @@ -58,7 +58,7 @@ export const TabItem = styled.div<{ `; export const TabsContainer = styled.div<{ tabLineColor?: string; - separatorColor: string; + separatorColor?: string; }>` height: 100%; width: 100%; diff --git a/src/lib/components/tabsv2/Tabsv2.component.tsx b/src/lib/components/tabsv2/Tabsv2.component.tsx index cf7c99f6c4..f40967aec6 100644 --- a/src/lib/components/tabsv2/Tabsv2.component.tsx +++ b/src/lib/components/tabsv2/Tabsv2.component.tsx @@ -185,8 +185,8 @@ function Tabs({ }); return ( - {/*@ts-expect-error containerType is not yet a valid prop for react */} & { +export type Props = InputHTMLAttributes & { toggle: boolean; onChange: (e: ChangeEvent) => void; label?: string; @@ -20,8 +19,7 @@ const ToggleContainer = styled.span<{ disabled?: boolean }>` `; const Switch = styled.label<{ disabled?: boolean }>` position: relative; - width: ${spacing.sp24}; - height: ${spacing.sp14}; + width: ${spacing.r24}; align-self: center; ${(props) => { return css` @@ -39,23 +37,22 @@ const Slider = styled.div<{ toggle?: boolean }>` width: 100%; height: 1rem; background-color: ${(props) => props.theme.backgroundLevel1}; - border: ${spacing.sp1} solid + border: ${spacing.r1} solid ${(props) => props.theme[props.toggle ? 'selectedActive' : 'infoPrimary']}; - border-radius: ${spacing.sp8}; + border-radius: ${spacing.r8}; transition: 0.4s; - -moz-transform: rotate(0.02deg); + &:before { border-radius: 100%; position: absolute; content: ''; - height: ${spacing.sp10}; - width: ${spacing.sp10}; + height: ${spacing.r10}; + width: ${spacing.r10}; left: 3px; top: 3.5px; background-color: ${(props) => props.theme[props.toggle ? 'textSecondary' : 'textPrimary']}; transition: 0.4s; - -moz-transform: rotate(0.02deg); } `; const ToggleInput = styled.input` @@ -63,7 +60,7 @@ const ToggleInput = styled.input` background-color: ${(props) => props.theme.selectedActive}; } &:checked + ${Slider}:before { - transform: translateX(${spacing.sp10}); + transform: translateX(${spacing.r10}); } display: none; `; diff --git a/src/lib/components/toggle/Toggle.test.tsx b/src/lib/components/toggle/Toggle.test.tsx new file mode 100644 index 0000000000..24abe47464 --- /dev/null +++ b/src/lib/components/toggle/Toggle.test.tsx @@ -0,0 +1,56 @@ +import { render, screen } from '@testing-library/react'; +import React, { useState } from 'react'; +import { Props, Toggle } from './Toggle.component'; +import userEvent from '@testing-library/user-event'; + +describe('Toggle', () => { + const selectors = { + toggle: () => screen.getByRole('checkbox'), + label: (text: string | RegExp) => screen.getByText(text), + }; + const RenderToggle = (props: Omit) => { + const [toggle, setToggle] = useState(false); + const handleClick = () => { + setToggle(!toggle); + }; + return ; + }; + + it('should render the Toggle component with label', () => { + render(); + const toggle = selectors.toggle(); + expect(toggle).toBeInTheDocument(); + const label = selectors.label(/Test/); + expect(label).toBeInTheDocument(); + }); + + it('should toggle the switch on click on checkbox or label', () => { + render(); + const toggle = selectors.toggle(); + userEvent.click(toggle); + expect(toggle).toBeChecked(); + const label = selectors.label('Test'); + userEvent.click(label); + expect(toggle).not.toBeChecked(); + }); + + it('should toggle the switch when pressing the space key or enter key', () => { + render(); + const toggle = selectors.toggle(); + userEvent.tab(); + userEvent.keyboard('{space}'); + expect(toggle).toBeChecked(); + userEvent.keyboard('{enter}'); + expect(toggle).not.toBeChecked(); + }); + + it('should not toggle the switch when disabled', () => { + render(); + const toggle = selectors.toggle(); + // toBeDisabled is not working for some reason + userEvent.tab(); + expect(toggle).not.toHaveFocus(); + userEvent.click(toggle); + expect(toggle).not.toBeChecked(); + }); +}); diff --git a/src/lib/organisms/attachments/AttachmentTable.tsx b/src/lib/organisms/attachments/AttachmentTable.tsx index 2c9fca09b2..dcc5862e18 100644 --- a/src/lib/organisms/attachments/AttachmentTable.tsx +++ b/src/lib/organisms/attachments/AttachmentTable.tsx @@ -594,7 +594,6 @@ export const AttachmentTable = ({ onBlur={() => { setSearchInputIsFocused(false); }} - disableToggle disabled={filteredEntities.status === 'error'} /> @@ -616,7 +615,6 @@ export const AttachmentTable = ({ onBlur={() => { setSearchInputIsFocused(false); }} - disableToggle searchInputIsFocused={searchInputIsFocused} /> )} diff --git a/stories/SearchInput/searchinput.guideline.mdx b/stories/SearchInput/searchinput.guideline.mdx new file mode 100644 index 0000000000..c10b1ee419 --- /dev/null +++ b/stories/SearchInput/searchinput.guideline.mdx @@ -0,0 +1,20 @@ +import { + Meta, + Story, + Canvas, + Primary, + Controls, + Unstyled, + Source, +} from '@storybook/blocks'; +import { SearchInput } from '../../src/lib/components/searchinput/SearchInput.component'; + +import * as Stories from './searchinput.stories'; + + + +# Search Input + + + + diff --git a/stories/searchinput.stories.tsx b/stories/SearchInput/searchinput.stories.tsx similarity index 76% rename from stories/searchinput.stories.tsx rename to stories/SearchInput/searchinput.stories.tsx index bb0a0e3527..6e95299c6e 100644 --- a/stories/searchinput.stories.tsx +++ b/stories/SearchInput/searchinput.stories.tsx @@ -1,7 +1,7 @@ import React, { useState } from 'react'; import { action } from '@storybook/addon-actions'; -import { SearchInput } from '../src/lib/components/searchinput/SearchInput.component'; -import { Wrapper, Title } from './common'; +import { SearchInput } from '../../src/lib/components/searchinput/SearchInput.component'; +import { Wrapper, Title } from '../common'; export default { title: 'Components/Inputs/SearchInput', component: SearchInput, @@ -21,7 +21,7 @@ export const Default = { placeholder="Search server..." onChange={action('on input change')} onReset={action('on input reset')} - disableToggle={false} + autoComplete="off" /> Disabled @@ -36,7 +36,6 @@ export const Default = { placeholder="Search server..." onChange={action('on input change')} onReset={action('on input reset')} - disableToggle={true} /> Search Input filled @@ -49,7 +48,6 @@ export const Default = { value="carlito" onChange={action('on input change')} onReset={action('on input reset')} - disableToggle={false} data-cy="carlito_searchinput" /> @@ -64,7 +62,6 @@ export const Default = { placeholder="Search and Filter…" onChange={action('on input change')} onReset={action('on input reset')} - disableToggle={true} /> Disable the default toggle undefined onReset action @@ -77,7 +74,6 @@ export const Default = { value="" placeholder="Search and Filter…" onChange={action('on input change')} - disableToggle={true} /> @@ -88,19 +84,16 @@ export const Debounce = { render: (args) => { const [value, setValue] = useState(''); return ( - - { - setValue(e.target.value); - action('debounce')(`${e.target} changed`); - }} - {...args} - /> - + { + setValue(e.target.value); + action('debounce')(`${e.target} changed`); + }} + {...args} + /> ); }, }; diff --git a/stories/Select/selectv2.stories.tsx b/stories/Select/selectv2.stories.tsx index 7d69d69e9d..871e0881c8 100644 --- a/stories/Select/selectv2.stories.tsx +++ b/stories/Select/selectv2.stories.tsx @@ -5,14 +5,15 @@ import { Modal } from '../../src/lib/components/modal/Modal.component'; import { Select } from '../../src/lib/components/selectv2/Selectv2.component'; import { Wrapper } from '../common'; import { Meta, StoryObj } from '@storybook/react'; +import { useArgs } from '@storybook/preview-api'; type SelectStory = StoryObj; const meta: Meta = { title: 'Components/Inputs/Select', component: Select, - decorators: [ - (story) => {story()}, - ], + // decorators: [ + // (story) => {story()}, + // ], }; export default meta; @@ -37,7 +38,7 @@ const generateOptions = (n = 10) => const optionsWithSearchBar = generateOptions(25); const optionsWithoutSearchBar = generateOptions(7); -const defaultOptions = generateOptions(4); +const defaultOptions = generateOptions(5); const thousandsOfOptions = generateOptions(1000); const optionsWithDisabledWithoutMessage = optionsWithSearchBar.map( (option, index) => { @@ -78,7 +79,6 @@ export const WithoutOptions: SelectStory = { export const DisabledSelect: SelectStory = { args: { disabled: true, - defaultValue: defaultOptions[0].props.value, children: defaultOptions, }, }; @@ -167,3 +167,21 @@ export const NotEnoughPlaceAtTheBottom: SelectStory = { children: optionsWithSearchBar, }, }; + +export const WithDefaultValue: SelectStory = { + render: (args) => { + const [{ value }, updateArgs] = useArgs(); + return ( + + ); + }, + args: { + value: defaultOptions[0].props.value, + placeholder: 'Select an option', + children: defaultOptions, + }, +}; diff --git a/stories/Toggle/toggle.guideline.mdx b/stories/Toggle/toggle.guideline.mdx new file mode 100644 index 0000000000..d0095f8855 --- /dev/null +++ b/stories/Toggle/toggle.guideline.mdx @@ -0,0 +1,20 @@ +import { + Meta, + Story, + Canvas, + Primary, + Controls, + Unstyled, + Source, +} from '@storybook/blocks'; + +import * as Stories from './toggle.stories'; + + + + + + + + + diff --git a/stories/toggle.stories.tsx b/stories/Toggle/toggle.stories.tsx similarity index 57% rename from stories/toggle.stories.tsx rename to stories/Toggle/toggle.stories.tsx index 90bfd24aae..b0890384f9 100644 --- a/stories/toggle.stories.tsx +++ b/stories/Toggle/toggle.stories.tsx @@ -1,22 +1,29 @@ import React, { useState } from 'react'; -import { Toggle } from '../src/lib/components/toggle/Toggle.component'; -import { Wrapper } from './common'; +import { + Props, + Toggle, +} from '../../src/lib/components/toggle/Toggle.component'; import { useArgs } from '@storybook/preview-api'; -export default { +import { Meta, StoryObj } from '@storybook/react'; + +type Story = StoryObj; +const meta: Meta = { title: 'Components/Inputs/Toggle', component: Toggle, args: { name: 'toggle', }, }; -export const Playground = { +export default meta; + +export const Playground: Story = { render: (args) => { - const [{ toggle }, updateArgs] = useArgs(); + const [{ toggle }, updateArgs] = useArgs<{ toggle: boolean }>(); return ( updateArgs({ toggle: !toggle })} {...args} + onChange={() => updateArgs({ toggle: !toggle })} + toggle={toggle} /> ); }, @@ -24,18 +31,18 @@ export const Playground = { label: 'Playground', }, }; -export const LabelledToggle = { +export const LabelledToggle: Story = { render: (args) => { const [toggle, setToggle] = useState(false); return ( - setToggle(!toggle)} {...args} /> + setToggle(!toggle)} /> ); }, args: { label: 'Airplane mode', }, }; -export const DisabledToggle = { +export const DisabledToggle: Story = { ...Playground, args: { label: 'Disabled Toggle',