Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

(PC-33528)[PRO] feat : add new Multiselect component #15628

Draft
wants to merge 2 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
149 changes: 149 additions & 0 deletions pro/src/ui-kit/MultiSelect/MultiSelect.module.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
@use "styles/mixins/_fonts.scss" as fonts;
@use "styles/mixins/fonts-design-system.scss" as fonts-design-system;
@use "styles/mixins/_rem.scss" as rem;
@use "styles/mixins/_a11y.scss" as a11y;
@use "styles/mixins/_size.scss" as size;

.container {
display: flex;
flex-direction: column;
width: 100%;
max-width: 400px;
position: relative;
}

.legend {
// @include fonts-design-system.body;
@include fonts.body;

color: var(--color-black);
margin-bottom: 8px;
}

.trigger {
@include fonts.body;

display: flex;
align-items: center;
justify-content: space-between;
padding: 12px;
background-color: var(--color-white);
border: 1px solid var(--color-grey-dark);
border-radius: 10px;
width: 100%;
cursor: pointer;

&:disabled {
background-color: var(--color-grey-light);
border: none;
color: var(--color-black);
}

&:hover {
background: var(--color-grey-light);
}

&:focus {
border: 1px solid var(--color-black);
}

&:focus-visible {
outline: rem.torem(1px) solid var(--color-input-text-color);
outline-offset: rem.torem(2px);
border-radius: rem.torem(8px);
border: 1px solid var(--color-grey-dark);
}

&-selected {
border: 2px solid var(--color-black);
}
}

.trigger-content {
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
max-width: 500px;
}

.trigger-label {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}

.badge {
// @include fonts-design-system.body-semi-bold-xs;
@include fonts.caption;

display: flex;
align-items: center;
justify-content: center;
width: 20px;
height: 20px;
background-color: var(--color-primary);
color: var(--color-white);
border-radius: 50%;
}

.chevron {
width: 16px;
height: 16px;
color: var(--color-black);
}

.chevron-open {
transform: rotate(180deg);
}

.item {
display: block;
width: 100%;
text-align: left;
font-size: 14px;
background: none;
border: none;
cursor: pointer;
}

.checkbox {
padding-top: rem.torem(16px);
}

.tags {
display: flex;
flex-wrap: wrap;
gap: 8px;
}

.panel {
position: absolute;
background-color: var(--color-white);
left: 0;
right: 0;
top: rem.torem(48px);
box-shadow: 0 rem.torem(3px) rem.torem(4px) var(--color-medium-shadow);
padding: rem.torem(24px);
}

.search-example {
// @include fonts-design-system.body-semi-bold-xs;
@include fonts.caption;

display: block;
min-height: rem.torem(16px);
color: var(--color-grey-dark);
padding-top: 8px;
padding-bottom: 24px;
}

.visually-hidden {
@include a11y.visually-hidden;
}

.separator {
height: rem.torem(1px);
background: var(--color-grey-medium);
margin-top: rem.torem(12px);
}
70 changes: 70 additions & 0 deletions pro/src/ui-kit/MultiSelect/MultiSelect.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { StoryObj } from '@storybook/react'
import { withRouter } from 'storybook-addon-remix-react-router'

import { MultiSelect } from './MultiSelect'

export default {
title: 'ui-kit/MultiSelect',
decorators: [withRouter],
component: MultiSelect,
parameters: {
docs: {
story: {
inline: false,
iframeHeight: 380,
},
},
},
}

const defaultOptions = [
{ id: '1', label: '78 - Yvelines' },
{ id: '2', label: '75 - Paris' },
{ id: '3', label: '44 - Nantes' },
{ id: '4', label: '76 - Rouen' },
{ id: '5', label: '77 - Seine et Marne' },
]

const defaultProps = {
options: defaultOptions,
legend: 'Département',
label: 'Selectionner un département',
}

export const Default: StoryObj<typeof MultiSelect> = {
args: {
...defaultProps,
},
}

export const WithDefaultOptions: StoryObj<typeof MultiSelect> = {
args: {
...defaultProps,
defaultOptions: [
{ id: '2', label: '75 - Paris' },
{ id: '3', label: '44 - Nantes' },
],
},
}

export const WithSearchInput: StoryObj<typeof MultiSelect> = {
args: {
...defaultProps,
hasSearch: true,
searchExample: 'Ex : 44 - Nantes',
searchLabel: 'Rechercher des départements',
legend: 'Départements',
label: 'Selectionner des départements',
},
}

export const WithSelectAllOption: StoryObj<typeof MultiSelect> = {
args: {
...defaultProps,
hasSelectAllOptions: true,
searchExample: 'Ex : 44 - Nantes',
searchLabel: 'Rechercher des départements',
legend: 'Départements',
label: 'Selectionner des départements',
},
}
138 changes: 138 additions & 0 deletions pro/src/ui-kit/MultiSelect/MultiSelect.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
import { useEffect, useRef, useState } from 'react'

import { SelectedValuesTags } from 'ui-kit/form/SelectAutoComplete/SelectedValuesTags/SelectedValuesTags'

import styles from './MultiSelect.module.scss'
import { MultiSelectPanel } from './MultiSelectPanel'
import { MultiSelectTrigger } from './MultiSelectTrigger'

export type Option = {
id: string
label: string
}

type MultiSelectProps = {
options: Option[]
defaultOptions?: Option[]
label: string
legend: string
hasSearch?: boolean
searchExample?: string
searchLabel?: string
hasSelectAllOptions?: boolean
disabled?: boolean
}

export const MultiSelect = ({
options,
defaultOptions,
hasSearch,
searchExample,
searchLabel,
label,
legend,
hasSelectAllOptions,
disabled,
}: MultiSelectProps): JSX.Element => {
const [isOpen, setIsOpen] = useState(false)
const containerRef = useRef<HTMLFieldSetElement>(null)
const [selectedItems, setSelectedItems] = useState<Option[]>(
defaultOptions ?? []
)

const handleSelectOrRemoveItem = (item: Option | 'all' | undefined) => {
if (item === 'all') {
setSelectedItems(options)
} else if (item === undefined) {
setSelectedItems([])
} else {
setSelectedItems((prev) =>
prev.some((prevItem) => prevItem.id === item.id)
? prev.filter((prevItem) => prevItem.id !== item.id)
: [...prev, item]
)
}
}

const handleRemoveItem = (itemId: string) => {
setSelectedItems((prev) => prev.filter((item) => item.id !== itemId))
}

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

const handleKeyDown = (event: React.KeyboardEvent) => {
event.preventDefault()
if (event.key === 'Enter' || event.key === ' ') {
toggleDropdown()
}
}

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)
}
}

window.addEventListener('click', handleWindowClick)
window.addEventListener('keydown', handleKeyDown)

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

return (
<fieldset
className={styles['container']}
style={{ position: 'relative' }}
ref={containerRef}
>
<MultiSelectTrigger
legend={legend}
label={label}
isOpen={isOpen}
toggleDropdown={toggleDropdown}
handleKeyDown={handleKeyDown}
selectedCount={selectedItems.length}
disabled={disabled}
/>

{isOpen && (
<MultiSelectPanel
label={label}
options={options.map((option) => ({
...option,
checked: selectedItems.some((item) => item.id === option.id),
}))}
onOptionSelect={handleSelectOrRemoveItem}
hasSearch={hasSearch}
searchExample={searchExample}
searchLabel={searchLabel}
hasSelectAllOptions={hasSelectAllOptions}
/>
)}

<SelectedValuesTags
disabled={false}
selectedOptions={selectedItems.map((item) => item.id)}
removeOption={handleRemoveItem}
fieldName="tags"
optionsLabelById={selectedItems.reduce(
(acc, item) => ({ ...acc, [item.id]: item.label }),
{}
)}
/>
</fieldset>
)
}
Loading
Loading