Skip to content

Commit

Permalink
(PC-33528)[PRO] feat: design adjustement
Browse files Browse the repository at this point in the history
  • Loading branch information
mleroy-pass committed Jan 10, 2025
1 parent 55acade commit 4c43813
Show file tree
Hide file tree
Showing 5 changed files with 257 additions and 42 deletions.
9 changes: 6 additions & 3 deletions pro/src/ui-kit/MultiSelect/MultiSelect.module.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down Expand Up @@ -165,4 +168,4 @@
height: rem.torem(1px);
background: var(--color-grey-medium);
margin: 0 rem.torem(24px);
}
}
70 changes: 36 additions & 34 deletions pro/src/ui-kit/MultiSelect/MultiSelectPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -64,40 +64,42 @@ export const MultiSelectPanel = ({
</div>
)}

{filteredOptions.length > 0 ? (
<ul className={styles['container']} aria-label="Liste des options">
{hasSelectAllOptions && (
<li key={'all-options'} className={styles.item}>
<BaseCheckbox
label={'Tout sélectionner'}
checked={isAllChecked}
labelClassName={styles['label']}
inputClassName={styles['checkbox']}
onChange={onSelectAll}
tabIndex={0}
/>
<div className={styles['separator']} />
</li>
)}
{filteredOptions.map((option) => (
<li key={option.id} className={styles.item}>
<BaseCheckbox
labelClassName={styles['label']}
inputClassName={styles['checkbox']}
label={option.label}
checked={option.checked}
onChange={() => onOptionSelect(option)}
onKeyDown={(e) => handleKeyDown(e, option)}
tabIndex={0}
/>
</li>
))}
</ul>
) : (
<span className={styles['empty-search']}>
{'Aucun résultat trouvé pour votre recherche.'}
</span>
)}
<div className={styles['panel-scrollable']}>
{filteredOptions.length > 0 ? (
<ul className={styles['container']} aria-label="Liste des options">
{hasSelectAllOptions && (
<li key={'all-options'} className={styles.item}>
<BaseCheckbox
label={'Tout sélectionner'}
checked={isAllChecked}
labelClassName={styles['label']}
inputClassName={styles['checkbox']}
onChange={onSelectAll}
tabIndex={0}
/>
<div className={styles['separator']} />
</li>
)}
{filteredOptions.map((option) => (
<li key={option.id} className={styles.item}>
<BaseCheckbox
labelClassName={styles['label']}
inputClassName={styles['checkbox']}
label={option.label}
checked={option.checked}
onChange={() => onOptionSelect(option)}
onKeyDown={(e) => handleKeyDown(e, option)}
tabIndex={0}
/>
</li>
))}
</ul>
) : (
<span className={styles['empty-search']}>
{'Aucun résultat trouvé pour votre recherche.'}
</span>
)}
</div>
</div>
)
}
4 changes: 2 additions & 2 deletions pro/src/ui-kit/MultiSelect/TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -17,4 +17,4 @@ Design

Bonus

- [] typage des props hasSearch et searchExample
- [x] typage des props hasSearch et searchExample
76 changes: 73 additions & 3 deletions pro/src/ui-kit/MultiSelect/__specs__/MultiSelectPanel.spec.tsx
Original file line number Diff line number Diff line change
@@ -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'

Expand Down Expand Up @@ -75,6 +75,76 @@ describe('<MultiSelectPanel />', () => {
expect(screen.queryByText(/Exemple: Nantes/i)).not.toBeInTheDocument()
})

it('should filter options based on the search input', async () => {
render(
<MultiSelectPanel
options={options}
label={''}
onOptionSelect={function (): void {
throw new Error('Function not implemented.')
}}
hasSearch={true}
searchExample="Exemple: Nantes"
searchLabel="Search label"
onSelectAll={function (): void {
throw new Error('Function not implemented.')
}}
isAllChecked={false}
/>
)

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(
<MultiSelectPanel
options={options}
label={''}
onOptionSelect={function (): void {
throw new Error('Function not implemented.')
}}
hasSearch={true}
searchExample="Exemple: Nantes"
searchLabel="Search label"
onSelectAll={function (): void {
throw new Error('Function not implemented.')
}}
isAllChecked={false}
/>
)

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(
<MultiSelectPanel
Expand Down Expand Up @@ -109,10 +179,10 @@ describe('<MultiSelectPanel />', () => {
)

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])
})
})
140 changes: 140 additions & 0 deletions pro/src/ui-kit/MultiSelect/__specs__/MultiSelectTrigger.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
import { fireEvent, render, screen } from '@testing-library/react'
import { axe } from 'vitest-axe'

import { MultiSelectTrigger } from '../MultiSelectTrigger'

describe('<MultiSelectTrigger />', () => {
const mockToggleDropdown = vi.fn()

const props = {
isOpen: false,
selectedCount: 2,
toggleDropdown: mockToggleDropdown,
legend: 'Select Options',
label: 'Options Label',
}

it('should render correctly', () => {
render(<MultiSelectTrigger {...props} />)

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(<MultiSelectTrigger {...props} />)

const results = await axe(container)
expect(results).toHaveNoViolations()
})

it('should disable the button when disabled prop is passed', () => {
render(<MultiSelectTrigger {...props} disabled />)

const button = screen.getByRole('button')

expect(button).toBeDisabled()
})

it('should display the chevron open icon if panel is opened', () => {
const { container } = render(
<MultiSelectTrigger {...props} isOpen={true} />
)

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(
<MultiSelectTrigger {...props} isOpen={false} />
)

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', () => {
// Simulate a selected count of 3
render(
<MultiSelectTrigger
isOpen={false}
selectedCount={3}
toggleDropdown={mockToggleDropdown}
legend="Legend"
label="Select Options"
/>
)

// Check if the badge is rendered with the correct count
expect(screen.getByText('3')).toBeInTheDocument()
})

it('should not render badge when no options are selected', () => {
// Simulate a selected count of 0 (no options selected)
render(
<MultiSelectTrigger
isOpen={false}
selectedCount={0}
toggleDropdown={mockToggleDropdown}
legend="Legend"
label="Select Options"
/>
)

// Check that the badge is not rendered
const badge = screen.queryByText('0')
expect(badge).toBeNull()
})

it('should update badge count when selecting and deselecting options', () => {
const { rerender } = render(
<MultiSelectTrigger
isOpen={false}
selectedCount={2}
toggleDropdown={mockToggleDropdown}
legend="Legend"
label="Select Options"
/>
)

// Check if the badge displays the correct count initially
expect(screen.getByText('2')).toBeInTheDocument()

// Rerender with updated selected count
rerender(
<MultiSelectTrigger
isOpen={false}
selectedCount={5}
toggleDropdown={mockToggleDropdown}
legend="Legend"
label="Select Options"
/>
)

// Check if the badge now displays the updated count
expect(screen.getByText('5')).toBeInTheDocument()
})

it('should call toggleDropdown when the button is clicked', () => {
render(
<MultiSelectTrigger
isOpen={false}
selectedCount={2}
toggleDropdown={mockToggleDropdown}
legend="Legend"
label="Select Options"
/>
)

const button = screen.getByRole('button')
fireEvent.click(button)

// Verify if toggleDropdown was called when the button is clicked
expect(mockToggleDropdown).toHaveBeenCalledTimes(1)
})
})

0 comments on commit 4c43813

Please sign in to comment.