feat(site): Crée un composant de recherche et sélection d'une entreprise.

pull/3215/head
Alice Dahan 2024-07-16 17:07:10 +02:00 committed by liliced
parent 64251f8f7d
commit aca54b5e96
2 changed files with 153 additions and 14 deletions

View File

@ -6,7 +6,7 @@ import { styled } from 'styled-components'
import { ForceThemeProvider } from '@/components/utils/DarkModeContext'
import { Message } from '@/design-system'
import { Card } from '@/design-system/card'
import { SearchField } from '@/design-system/field'
import { SearchableSelectField } from '@/design-system/field/SearchableSelectField/SearchableSelectField'
import { FocusStyle } from '@/design-system/global-style'
import { ChevronIcon } from '@/design-system/icons'
import { Grid } from '@/design-system/layout'
@ -30,6 +30,7 @@ const StyledCard = styled(Card)`
export function EntrepriseSearchField(props: {
label?: ReactNode
selectedValue?: ReactNode | null
onValue?: () => void
onClear?: () => void
onSubmit?: (search: Entreprise | null) => void
@ -39,15 +40,22 @@ export function EntrepriseSearchField(props: {
const searchFieldProps = {
...props,
label: t('CompanySearchField.label', "Nom de l'entreprise, SIREN ou SIRET"),
description: t(
'CompanySearchField.description',
'Le numéro Siret est un numéro de 14 chiffres unique pour chaque entreprise. Exemple : 40123778000127'
),
label:
!props.selectedValue &&
t('CompanySearchField.label', "Nom de l'entreprise, SIREN ou SIRET"),
description:
!props.selectedValue &&
t(
'CompanySearchField.description',
'Le numéro Siret est un numéro de 14 chiffres unique pour chaque entreprise. Exemple : 40123778000127'
),
onSubmit() {
const results = refResults.current
props.onSubmit?.(results?.[0] ?? null)
},
onClear() {
props.onClear?.()
},
placeholder: t(
'CompanySearchField.placeholder',
'Exemple : Café de la gare ou 40123778000127'
@ -56,11 +64,7 @@ export function EntrepriseSearchField(props: {
const state = useSearchFieldState(searchFieldProps)
const { onValue, onClear, onSubmit } = props
useEffect(
() => (!state.value ? onClear?.() : onValue?.()),
[state.value, onValue, onClear]
)
const { onSubmit } = props
const [searchPending, results] = useSearchCompany(state.value)
@ -71,11 +75,10 @@ export function EntrepriseSearchField(props: {
return (
<Grid container>
<Grid item xs={12}>
<SearchField
<SearchableSelectField
data-test-id="company-search-input"
state={state}
isSearchStalled={searchPending}
onClear={onClear}
aria-label={
searchFieldProps.label +
', ' +
@ -89,7 +92,7 @@ export function EntrepriseSearchField(props: {
<Grid item xs={12}>
<Appear unless={searchPending || !state.value}>
{state.value && !searchPending && (
{state.value && !searchPending && !props.selectedValue && (
<Results results={results} onSubmit={onSubmit} />
)}
</Appear>

View File

@ -0,0 +1,136 @@
import { useButton } from '@react-aria/button'
import { useSearchField } from '@react-aria/searchfield'
import {
SearchFieldState,
useSearchFieldState,
} from '@react-stately/searchfield'
import { AriaSearchFieldProps } from '@react-types/searchfield'
import { ReactNode, useRef } from 'react'
import { css, styled } from 'styled-components'
import { SearchIcon } from '@/design-system/icons'
import { Loader } from '@/design-system/icons/Loader'
import { FocusStyle } from '../../global-style'
import {
StyledContainer,
StyledDescription,
StyledErrorMessage,
StyledInput,
StyledInputContainer,
StyledLabel,
} from '../TextField'
const SearchInput = styled(StyledInput)`
&,
&::-webkit-search-decoration,
&::-webkit-search-cancel-button,
&::-webkit-search-results-button,
&::-webkit-search-results-decoration {
-webkit-appearance: none;
}
`
const SearchInputContainer = styled(StyledInputContainer)`
padding-left: 0.5rem;
&:focus-within {
${FocusStyle}
}
`
const IconContainer = styled.div<{ $hasLabel?: boolean; $hasValue?: boolean }>`
padding: calc(
${({ $hasLabel = false }) => ($hasLabel ? '1rem' : '0rem')} + 0.5rem
)
0 0.5rem;
${({ $hasValue }) => {
if ($hasValue) {
return css`
width: 100%;
`
}
}}
`
const StyledClearButton = styled.button`
position: absolute;
right: 0;
background: transparent;
border: none;
font-size: 2rem;
line-height: 2rem;
height: ${({ theme }) => theme.spacings.xxxl};
padding: ${({ theme }) => `${theme.spacings.md} ${theme.spacings.xs}`};
${({ theme: { darkMode } }) =>
darkMode &&
css`
color: white !important;
`}
`
export function SearchableSelectField(
props: AriaSearchFieldProps & {
state?: SearchFieldState
isSearchStalled?: boolean
selectedValue?: ReactNode | null
}
) {
const innerState = useSearchFieldState(props)
const state = props.state || innerState
const ref = useRef<HTMLInputElement>(null)
const buttonRef = useRef(null)
const {
labelProps,
inputProps,
descriptionProps,
errorMessageProps,
clearButtonProps,
} = useSearchField(props, state, ref)
const { buttonProps } = useButton(clearButtonProps, buttonRef)
return (
<StyledContainer>
<SearchInputContainer
$hasError={!!props.errorMessage || props.validationState === 'invalid'}
$hasLabel={!!props.label}
>
{props.selectedValue ? (
<IconContainer ref={ref} $hasLabel={!!props.label} $hasValue={true}>
{props.selectedValue}
</IconContainer>
) : (
<>
<IconContainer $hasLabel={!!props.label}>
{props.isSearchStalled ? <Loader /> : <SearchIcon aria-hidden />}
</IconContainer>
<SearchInput
{...inputProps}
placeholder={inputProps.placeholder ?? ''}
ref={ref}
/>
</>
)}
{props.label && (
<StyledLabel aria-hidden {...labelProps}>
{props.label}
</StyledLabel>
)}
{(state.value !== '' || props.selectedValue) && (
<StyledClearButton {...buttonProps} ref={buttonRef}>
×
</StyledClearButton>
)}
</SearchInputContainer>
{props.errorMessage && (
<StyledErrorMessage {...errorMessageProps} role="alert">
{props.errorMessage}
</StyledErrorMessage>
)}
{props.description && (
<StyledDescription {...descriptionProps}>
{props.description}
</StyledDescription>
)}
</StyledContainer>
)
}