feat(site): Crée un composant de recherche et sélection d'une entreprise.
parent
64251f8f7d
commit
aca54b5e96
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
}
|
Loading…
Reference in New Issue