Skip to content

Commit

Permalink
(PC-33528)[PRO] feat: add keyboard nav
Browse files Browse the repository at this point in the history
  • Loading branch information
mleroy-pass committed Jan 13, 2025
1 parent 166c277 commit 9e8f561
Show file tree
Hide file tree
Showing 7 changed files with 142 additions and 83 deletions.
2 changes: 1 addition & 1 deletion pro/src/ui-kit/MultiSelect/MultiSelect.module.scss
Original file line number Diff line number Diff line change
Expand Up @@ -62,13 +62,13 @@
align-items: center;
justify-content: center;
gap: rem.torem(8px);
max-width: 500px;
}

.trigger-label {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-width: 300px;
}

.badge {
Expand Down
90 changes: 43 additions & 47 deletions pro/src/ui-kit/MultiSelect/MultiSelect.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,17 +16,22 @@ type MultiSelectProps = {
defaultOptions?: Option[]
label: string
legend: string
hasSearch?: boolean
searchExample?: string
searchLabel?: string
hasSelectAllOptions?: boolean
disabled?: boolean
}
} & ( // If `hasSearch` is `true`, `searchExample` and `searchLabel` are required. // This part applies the condition
| { hasSearch: true; searchExample: string; searchLabel: string }
// If `hasSearch` is `false` or undefined, `searchExample` and `searchLabel` are optional.
| {
hasSearch?: false | undefined
searchExample?: never
searchLabel?: never
}
)

export const MultiSelect = ({
options,
defaultOptions,
hasSearch,
defaultOptions = [],
hasSearch = false,
searchExample,
searchLabel,
label,
Expand All @@ -35,78 +40,69 @@ export const MultiSelect = ({
disabled,
}: MultiSelectProps): JSX.Element => {
const [isOpen, setIsOpen] = useState(false)
const containerRef = useRef<HTMLFieldSetElement>(null)
const [selectedItems, setSelectedItems] = useState<Option[]>(
defaultOptions ?? []
)
const [selectedItems, setSelectedItems] = useState<Option[]>(defaultOptions)
const [isSelectAllChecked, setIsSelectAllChecked] = useState(false)

const handleSelectOrRemoveItem = (item: Option) => {
const itemsToBeSelected = selectedItems.some(
(prevItem) => prevItem.id === item.id
)
? selectedItems.filter((prevItem) => prevItem.id !== item.id)
const containerRef = useRef<HTMLFieldSetElement>(null)

const toggleDropdown = () => setIsOpen((prev) => !prev)

const handleSelectItem = (item: Option) => {
const updatedSelectedItems = selectedItems.some((i) => i.id === item.id)
? selectedItems.filter((i) => i.id !== item.id)
: [...selectedItems, item]

setIsSelectAllChecked(itemsToBeSelected.length === options.length)
setSelectedItems(itemsToBeSelected)
setSelectedItems(updatedSelectedItems)
setIsSelectAllChecked(updatedSelectedItems.length === options.length)
}

const handleSelectAll = () => {
const updatedItems = isSelectAllChecked ? [] : options
setSelectedItems(updatedItems)
setIsSelectAllChecked(!isSelectAllChecked)
if (isSelectAllChecked) {
setSelectedItems([])
} else {
setSelectedItems(options)
}
}

const handleRemoveTag = (itemId: string) => {
setSelectedItems((prev) => prev.filter((item) => item.id !== itemId))
const updatedItems = selectedItems.filter((item) => item.id !== itemId)
setSelectedItems(updatedItems)
setIsSelectAllChecked(updatedItems.length === options.length)
}

const toggleDropdown = () => setIsOpen(!isOpen)

const handleKeyDown = (event: React.KeyboardEvent) => {
event.preventDefault()
if (event.key === 'Enter' || event.key === ' ') {
toggleDropdown()
const handleClickOutside = (event: MouseEvent) => {
if (
containerRef.current &&
!containerRef.current.contains(event.target as Node)
) {
setIsOpen(false)
}
}

useEffect(() => {
const handleWindowClick = (event: MouseEvent) => {
if (
containerRef.current &&
!containerRef.current.contains(event.target as Node)
) {
setIsOpen(false)
}
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === 'Escape') {
setIsOpen(false)
}

const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === 'Escape') {
setIsOpen(false)
}
if (event.key === 'Enter' || event.key === ' ') {
setIsOpen(true)
}
}

window.addEventListener('click', handleWindowClick)
useEffect(() => {
window.addEventListener('click', handleClickOutside)
window.addEventListener('keydown', handleKeyDown)

return () => {
window.removeEventListener('click', handleWindowClick)
window.removeEventListener('click', handleClickOutside)
window.removeEventListener('keydown', handleKeyDown)
}
}, [])

return (
<fieldset className={styles['container']} ref={containerRef}>
<fieldset className={styles.container} ref={containerRef}>
<MultiSelectTrigger
legend={legend}
label={label}
isOpen={isOpen}
toggleDropdown={toggleDropdown}
handleKeyDown={handleKeyDown}
selectedCount={selectedItems.length}
disabled={disabled}
/>
Expand All @@ -118,7 +114,7 @@ export const MultiSelect = ({
...option,
checked: selectedItems.some((item) => item.id === option.id),
}))}
onOptionSelect={handleSelectOrRemoveItem}
onOptionSelect={handleSelectItem}
onSelectAll={handleSelectAll}
isAllChecked={isSelectAllChecked}
hasSearch={hasSearch}
Expand Down
32 changes: 17 additions & 15 deletions pro/src/ui-kit/MultiSelect/MultiSelectPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,13 @@ type MultiSelectPanelProps = {
className?: string
label: string
options: (Option & { checked: boolean })[]
onOptionSelect: (option: Option) => void
onSelectAll: () => void
hasSelectAllOptions?: boolean
isAllChecked: boolean
hasSearch?: boolean
searchExample?: string
searchLabel?: string
hasSelectAllOptions?: boolean
isAllChecked: boolean
onOptionSelect: (option: Option) => void
onSelectAll: () => void
}

export const MultiSelectPanel = ({
Expand All @@ -31,20 +31,19 @@ export const MultiSelectPanel = ({
isAllChecked,
}: MultiSelectPanelProps): JSX.Element => {
const [searchValue, setSearchValue] = useState('')
const searchedValues = useMemo(

const filteredOptions = useMemo(
() =>
options.filter((option) =>
option.label.toLowerCase().includes(searchValue.toLowerCase())
),
[options, searchValue]
)

const onToggleAllOption = () => {
onSelectAll()
}

const onToggleOption = (option: Option) => {
onOptionSelect(option)
const handleKeyDown = (e: React.KeyboardEvent, option: Option) => {
if (e.key === 'Enter' || e.key === ' ') {
onOptionSelect(option)
}
}

return (
Expand All @@ -65,7 +64,7 @@ export const MultiSelectPanel = ({
</div>
)}

{searchedValues.length > 0 ? (
{filteredOptions.length > 0 ? (
<ul className={styles['container']} aria-label="Liste des options">
{hasSelectAllOptions && (
<li key={'all-options'} className={styles.item}>
Expand All @@ -74,19 +73,22 @@ export const MultiSelectPanel = ({
checked={isAllChecked}
labelClassName={styles['label']}
inputClassName={styles['checkbox']}
onChange={() => onToggleAllOption()}
onChange={onSelectAll}
tabIndex={0}
/>
<div className={styles['separator']} />
</li>
)}
{searchedValues.map((option) => (
{filteredOptions.map((option) => (
<li key={option.id} className={styles.item}>
<BaseCheckbox
labelClassName={styles['label']}
inputClassName={styles['checkbox']}
label={option.label}
checked={option.checked}
onChange={() => onToggleOption(option)}
onChange={() => onOptionSelect(option)}
onKeyDown={(e) => handleKeyDown(e, option)}
tabIndex={0}
/>
</li>
))}
Expand Down
3 changes: 0 additions & 3 deletions pro/src/ui-kit/MultiSelect/MultiSelectTrigger.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ type MultiSelectTriggerProps = {
isOpen: boolean
selectedCount: number
toggleDropdown: () => void
handleKeyDown: (event: React.KeyboardEvent) => void
legend: string
label: string
disabled?: boolean
Expand All @@ -21,7 +20,6 @@ export const MultiSelectTrigger = ({
isOpen,
selectedCount,
toggleDropdown,
handleKeyDown,
legend,
label,
disabled,
Expand All @@ -38,7 +36,6 @@ export const MultiSelectTrigger = ({
[styles['trigger-selected']]: selectedCount > 0,
})}
onClick={toggleDropdown}
onKeyDown={handleKeyDown}
aria-haspopup="listbox"
aria-expanded={isOpen}
aria-labelledby={legendId}
Expand Down
2 changes: 1 addition & 1 deletion pro/src/ui-kit/MultiSelect/TODO.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
Fonctionnel

- [] gérer la navigation clavier
- [x] gérer la navigation clavier
- [x] Corriger le all selected
- [] jsdoc
- [x] ne pas afficher plus de 3 lignes de tags (refacto des tags dans un autre ticket PC-33762)
Expand Down
Loading

0 comments on commit 9e8f561

Please sign in to comment.