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 ? (
-
- {hasSelectAllOptions && (
- -
-
-
-
- )}
- {filteredOptions.map((option) => (
- -
- onOptionSelect(option)}
- onKeyDown={(e) => handleKeyDown(e, option)}
- tabIndex={0}
- />
-
- ))}
-
- ) : (
-
- {'Aucun résultat trouvé pour votre recherche.'}
-
- )}
+
+ {filteredOptions.length > 0 ? (
+
+ {hasSelectAllOptions && (
+ -
+
+
+
+ )}
+ {filteredOptions.map((option) => (
+ -
+ onOptionSelect(option)}
+ onKeyDown={(e) => handleKeyDown(e, option)}
+ tabIndex={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)
+ })
+})