From e84a161be341e3c4c0e62f224ce918b291595eb7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Myl=C3=A8ne?= <187286904+mleroy-pass@users.noreply.github.com> Date: Fri, 10 Jan 2025 15:30:52 +0100 Subject: [PATCH] (PC-33528)[PRO] feat: design adjustement --- .../MultiSelect/MultiSelect.module.scss | 9 +- .../ui-kit/MultiSelect/MultiSelectPanel.tsx | 70 ++--- pro/src/ui-kit/MultiSelect/TODO.md | 4 +- .../__specs__/MultiSelect.spec.tsx | 37 +-- .../__specs__/MultiSelectPanel.spec.tsx | 288 ++++++++++++++++-- .../__specs__/MultiSelectTrigger.spec.tsx | 134 ++++++++ 6 files changed, 449 insertions(+), 93 deletions(-) create mode 100644 pro/src/ui-kit/MultiSelect/__specs__/MultiSelectTrigger.spec.tsx diff --git a/pro/src/ui-kit/MultiSelect/MultiSelect.module.scss b/pro/src/ui-kit/MultiSelect/MultiSelect.module.scss index 4d1e7866930..729d533208e 100644 --- a/pro/src/ui-kit/MultiSelect/MultiSelect.module.scss +++ b/pro/src/ui-kit/MultiSelect/MultiSelect.module.scss @@ -115,15 +115,18 @@ } .panel { - max-height: rem.torem(340px); position: absolute; background-color: var(--color-white); left: 0; right: 0; top: rem.torem(60px); box-shadow: 0 rem.torem(3px) rem.torem(4px) var(--color-medium-shadow); - padding: rem.torem(24px) 0 rem.torem(16px); + padding: rem.torem(24px) 0 rem.torem(24px); border-radius: rem.torem(8px); +} + +.panel-scrollable { + max-height: rem.torem(340px); overflow: auto; } @@ -165,4 +168,4 @@ height: rem.torem(1px); background: var(--color-grey-medium); margin: 0 rem.torem(24px); -} +} \ No newline at end of file diff --git a/pro/src/ui-kit/MultiSelect/MultiSelectPanel.tsx b/pro/src/ui-kit/MultiSelect/MultiSelectPanel.tsx index fdc5a94d487..88771d9a5c1 100644 --- a/pro/src/ui-kit/MultiSelect/MultiSelectPanel.tsx +++ b/pro/src/ui-kit/MultiSelect/MultiSelectPanel.tsx @@ -64,40 +64,42 @@ export const MultiSelectPanel = ({ )} - {filteredOptions.length > 0 ? ( - - ) : ( - - {'Aucun résultat trouvé pour votre recherche.'} - - )} +
+ {filteredOptions.length > 0 ? ( + + ) : ( + + {'Aucun résultat trouvé pour votre recherche.'} + + )} +
) } diff --git a/pro/src/ui-kit/MultiSelect/TODO.md b/pro/src/ui-kit/MultiSelect/TODO.md index 03a83fe749f..c28a7e34d12 100644 --- a/pro/src/ui-kit/MultiSelect/TODO.md +++ b/pro/src/ui-kit/MultiSelect/TODO.md @@ -8,7 +8,7 @@ Fonctionnel Design -- [] gérer le responsive pour un titre long +- [x] gérer le responsive pour un titre long - [x] centrer le aucun résultat - [x] Passer les selected tag en violet (refacto des tags dans un autre ticket PC-33762) - [x] utiliser les fonts du design system (devrait être pris en compte avec les nouvelles maj prévues) @@ -17,4 +17,4 @@ Design Bonus -- [] typage des props hasSearch et searchExample +- [x] typage des props hasSearch et searchExample diff --git a/pro/src/ui-kit/MultiSelect/__specs__/MultiSelect.spec.tsx b/pro/src/ui-kit/MultiSelect/__specs__/MultiSelect.spec.tsx index 0735f82e702..dcc44cd49b2 100644 --- a/pro/src/ui-kit/MultiSelect/__specs__/MultiSelect.spec.tsx +++ b/pro/src/ui-kit/MultiSelect/__specs__/MultiSelect.spec.tsx @@ -1,4 +1,4 @@ -import { render, screen, fireEvent, waitFor } from '@testing-library/react' +import { render, screen, waitFor } from '@testing-library/react' import { userEvent } from '@testing-library/user-event' import { axe } from 'vitest-axe' @@ -66,10 +66,10 @@ describe('', () => { /> ) const toggleButton = screen.getByText('Select Options') - await userEvent.click(toggleButton) + await userEvent.click(toggleButton) expect(screen.getByText(/Tout sélectionner/i)).toBeInTheDocument() - await userEvent.click(toggleButton) + await userEvent.click(toggleButton) expect(screen.queryByText(/Tout sélectionner/i)).not.toBeInTheDocument() }) @@ -85,10 +85,10 @@ describe('', () => { ) const toggleButton = screen.getByText('Select Options') - await userEvent.click(toggleButton) + await userEvent.click(toggleButton) const selectAllCheckbox = screen.getByLabelText(/Tout sélectionner/i) - fireEvent.click(selectAllCheckbox) + await userEvent.click(selectAllCheckbox) options.forEach((option) => { expect(screen.getByLabelText(option.label)).toBeChecked() @@ -106,9 +106,8 @@ describe('', () => { /> ) - // Initially, Option 1 is selected const selectedTag = screen.getByText('Option 1') - await userEvent.click(selectedTag) + await userEvent.click(selectedTag) expect(screen.queryByText('Option 1')).not.toBeInTheDocument() }) @@ -124,16 +123,17 @@ describe('', () => { const toggleButton = screen.getByText('Select Options') - fireEvent.click(toggleButton) + await userEvent.click(toggleButton) expect(screen.queryByText('Option 1')).toBeInTheDocument() - fireEvent.click(document.body) + await userEvent.click(document.body) await waitFor(() => expect(screen.queryByText('Option 1')).not.toBeInTheDocument() ) - fireEvent.click(toggleButton) - fireEvent.keyDown(toggleButton, { key: 'Escape' }) + await userEvent.click(toggleButton) + await userEvent.keyboard('[Escape]') + await waitFor(() => expect(screen.queryByText('Option 1')).not.toBeInTheDocument() ) @@ -151,23 +151,14 @@ describe('', () => { ) const toggleButton = screen.getByText('Select Options') + toggleButton.focus() - fireEvent.keyDown(toggleButton, { key: 'Enter' }) - await waitFor(() => - expect(screen.getByText(/Tout sélectionner/i)).toBeInTheDocument() - ) - - fireEvent.keyDown(toggleButton, { key: 'Escape' }) - await waitFor(() => - expect(screen.queryByText(/Tout sélectionner/i)).not.toBeInTheDocument() - ) - - fireEvent.keyDown(toggleButton, { key: ' ' }) + await userEvent.keyboard('[Enter]') await waitFor(() => expect(screen.getByText(/Tout sélectionner/i)).toBeInTheDocument() ) - fireEvent.keyDown(toggleButton, { key: 'Escape' }) + await userEvent.keyboard('[Escape]') await waitFor(() => expect(screen.queryByText(/Tout sélectionner/i)).not.toBeInTheDocument() ) diff --git a/pro/src/ui-kit/MultiSelect/__specs__/MultiSelectPanel.spec.tsx b/pro/src/ui-kit/MultiSelect/__specs__/MultiSelectPanel.spec.tsx index 7be2d49905a..0de7b76309d 100644 --- a/pro/src/ui-kit/MultiSelect/__specs__/MultiSelectPanel.spec.tsx +++ b/pro/src/ui-kit/MultiSelect/__specs__/MultiSelectPanel.spec.tsx @@ -1,4 +1,4 @@ -import { render, screen } from '@testing-library/react' +import { render, screen, waitFor } from '@testing-library/react' import userEvent from '@testing-library/user-event' import { axe } from 'vitest-axe' @@ -13,6 +13,7 @@ describe('', () => { ] const onOptionSelect = vi.fn() + const onSelectAll = vi.fn() it('renders options with checkboxes', () => { render( @@ -20,12 +21,8 @@ describe('', () => { options={options} label={''} hasSearch={false} - onOptionSelect={function (): void { - throw new Error('Function not implemented.') - }} - onSelectAll={function (): void { - throw new Error('Function not implemented.') - }} + onOptionSelect={onOptionSelect} + onSelectAll={onSelectAll} isAllChecked={false} /> ) @@ -40,15 +37,11 @@ describe('', () => { ) @@ -56,18 +49,52 @@ describe('', () => { expect(screen.getByText(/Exemple: Nantes/i)).toBeInTheDocument() }) + test('updates search value on input change', async () => { + render( + + ) + + const input = screen.getByRole('searchbox') + + await userEvent.type(input, 'apple') + + expect(input).toHaveValue('apple') + }) + + test('displays the search example text', () => { + render( + + ) + + expect(screen.getByText('Exemple: Nantes')).toBeInTheDocument() + }) + it('not renders the search input if hasSearch is false', () => { render( ) @@ -75,18 +102,76 @@ describe('', () => { expect(screen.queryByText(/Exemple: Nantes/i)).not.toBeInTheDocument() }) + it('should filter options based on the search input', async () => { + render( + + ) + + expect(screen.getByText('Option 1')).toBeInTheDocument() + expect(screen.getByText('Option 2')).toBeInTheDocument() + expect(screen.getByText('Option 3')).toBeInTheDocument() + + const searchInput = screen.getByRole('searchbox') + + await userEvent.type(searchInput, 'Option 1') + + await waitFor(() => { + expect(screen.queryByText('Option 2')).not.toBeInTheDocument() + expect(screen.queryByText('Option 3')).not.toBeInTheDocument() + expect(screen.getByText('Option 1')).toBeInTheDocument() + }) + + await userEvent.clear(searchInput) + + await waitFor(() => { + expect(screen.getByText('Option 1')).toBeInTheDocument() + expect(screen.getByText('Option 2')).toBeInTheDocument() + expect(screen.getByText('Option 3')).toBeInTheDocument() + }) + }) + + it('should show "No results found" when no options match the search', async () => { + render( + + ) + + const searchInput = screen.getByRole('searchbox') + + await userEvent.type(searchInput, 'Non-matching option') + + await waitFor(() => + expect( + screen.getByText('Aucun résultat trouvé pour votre recherche.') + ).toBeInTheDocument() + ) + }) + it('should not have accessibility violations', async () => { const { container } = render( ) @@ -101,18 +186,159 @@ describe('', () => { label="" hasSearch={false} onOptionSelect={onOptionSelect} - onSelectAll={function (): void { - throw new Error('Function not implemented.') - }} + onSelectAll={onSelectAll} isAllChecked={false} /> ) const option2Checkbox = screen.getByLabelText(/Option 2/i) - await userEvent.click(option2Checkbox) + await userEvent.click(option2Checkbox) expect(onOptionSelect).toHaveBeenCalledWith(options[1]) - await userEvent.click(option2Checkbox) + await userEvent.click(option2Checkbox) expect(onOptionSelect).toHaveBeenCalledWith(options[1]) }) + + test('renders "Select All" checkbox when hasSelectAllOptions is true', () => { + render( + + ) + + expect(screen.getByLabelText('Tout sélectionner')).toBeInTheDocument() + }) + + test('triggers onSelectAll when "Select All" checkbox is clicked', async () => { + render( + + ) + + const selectAllCheckbox = screen.getByLabelText('Tout sélectionner') + await userEvent.click(selectAllCheckbox) + + expect(onSelectAll).toHaveBeenCalledTimes(1) + }) + + test('reflects isAllChecked state in "Select All" checkbox', () => { + render( + + ) + + const selectAllCheckbox = screen.getByLabelText('Tout sélectionner') + expect(selectAllCheckbox).toBeChecked() + }) + + test('does not render "Select All" checkbox when hasSelectAllOptions is false', () => { + render( + + ) + + const selectAllCheckbox = screen.queryByLabelText('Tout sélectionner') + expect(selectAllCheckbox).not.toBeInTheDocument() + }) + + test('calls onOptionSelect when Enter key is pressed on an option', async () => { + render( + + ) + + const optionCheckbox = screen.getByLabelText('Option 1') + + optionCheckbox.focus() + + await userEvent.keyboard('[Enter]') + + expect(onOptionSelect).toHaveBeenCalledWith({ + id: '1', + label: 'Option 1', + checked: false, + }) + }) + + test('calls onOptionSelect when Space key is pressed on an option', async () => { + render( + + ) + + const optionCheckbox = screen.getByLabelText('Option 2') + + optionCheckbox.focus() + + await userEvent.keyboard('[Space]') + + expect(onOptionSelect).toHaveBeenCalledWith({ + id: '2', + label: 'Option 2', + checked: false, + }) + }) + + test('does not call onOptionSelect when other keys are pressed', async () => { + render( + + ) + + const optionCheckbox = screen.getByLabelText('Option 3') + + optionCheckbox.focus() + + await userEvent.keyboard('[ArrowDown]') + + expect(onOptionSelect).not.toHaveBeenCalled() + }) }) diff --git a/pro/src/ui-kit/MultiSelect/__specs__/MultiSelectTrigger.spec.tsx b/pro/src/ui-kit/MultiSelect/__specs__/MultiSelectTrigger.spec.tsx new file mode 100644 index 00000000000..b1e8dbc749b --- /dev/null +++ b/pro/src/ui-kit/MultiSelect/__specs__/MultiSelectTrigger.spec.tsx @@ -0,0 +1,134 @@ +import { render, screen } from '@testing-library/react' +import userEvent from '@testing-library/user-event' +import { axe } from 'vitest-axe' + +import { MultiSelectTrigger } from '../MultiSelectTrigger' + +describe('', () => { + const mockToggleDropdown = vi.fn() + + const props = { + isOpen: false, + selectedCount: 2, + toggleDropdown: mockToggleDropdown, + legend: 'Select Options', + label: 'Options Label', + } + + it('should render correctly', () => { + render() + + expect(screen.getByText('Options Label')).toBeInTheDocument() + expect(screen.getByText('2')).toBeInTheDocument() // This is the selectedCount badge + }) + + it('should have no accessibility violations', async () => { + const { container } = render() + + const results = await axe(container) + expect(results).toHaveNoViolations() + }) + + it('should disable the button when disabled prop is passed', () => { + render() + + const button = screen.getByRole('button') + + expect(button).toBeDisabled() + }) + + it('should display the chevron open icon if panel is opened', () => { + const { container } = render( + + ) + + let chevronIcon = container.querySelector('svg') + + expect(chevronIcon).toHaveClass('chevron chevronOpen') + }) + + it('should not display the chevron icon open if panel is closed', () => { + const { container } = render( + + ) + + let chevronIcon = container.querySelector('svg') + + chevronIcon = container.querySelector('svg') + + expect(chevronIcon).not.toHaveClass('chevron chevronOpen') + }) + + it('should render badge with correct count when options are selected', () => { + render( + + ) + + expect(screen.getByText('3')).toBeInTheDocument() + }) + + it('should not render badge when no options are selected', () => { + render( + + ) + + const badge = screen.queryByText('0') + expect(badge).toBeNull() + }) + + it('should update badge count when selecting and deselecting options', () => { + const { rerender } = render( + + ) + + expect(screen.getByText('2')).toBeInTheDocument() + + rerender( + + ) + + expect(screen.getByText('5')).toBeInTheDocument() + }) + + it('should call toggleDropdown when the button is clicked', async () => { + render( + + ) + + const button = screen.getByRole('button') + + await userEvent.click(button) + + expect(mockToggleDropdown).toHaveBeenCalledTimes(1) + }) +})