Améliore le style de la recherche du code APE

pull/2782/head
Johan Girod 2023-04-24 16:12:14 +02:00
parent 63ada8a447
commit 4864741073
11 changed files with 187 additions and 154 deletions

View File

@ -10,7 +10,6 @@ import { BrowserRouter } from 'react-router-dom'
import logo from '@/assets/images/logo-monentreprise.svg'
import { ThemeColorsProvider } from '@/components/utils/colors'
import { DisableAnimationOnPrintProvider } from '@/components/utils/DisableAnimationContext'
import { GlobalStyle } from '@/design-system/global-style'
import { Container, Grid } from '@/design-system/layout'
import DesignSystemThemeProvider from '@/design-system/root'
import { H1, H4 } from '@/design-system/typography/heading'
@ -45,7 +44,6 @@ export default function Provider({
return (
<DarkModeProvider>
<DesignSystemThemeProvider>
<GlobalStyle />
<ErrorBoundary showDialog fallback={ErrorFallback}>
{!import.meta.env.SSR &&
import.meta.env.MODE === 'production' &&

View File

@ -10,28 +10,26 @@ import { Body } from '@/design-system/typography/paragraphs'
export const RadioContext = createContext<RadioGroupState | null>(null)
type RadioProps = AriaRadioProps & {
hideRadio?: boolean
className?: string
role?: string
// eslint-disable-next-line @typescript-eslint/no-explicit-any
visibleRadioAs?: string | React.ComponentType<any>
}
// TDOO: isDisabled style
export function Radio(props: RadioProps) {
const { hideRadio, children, id } = props
const { children } = props
return (
<RadioSkeleton role="radio" aria-atomic {...props}>
{!hideRadio && <RadioPoint />}
<LabelBody as="span" htmlFor={id} $hideRadio={hideRadio}>
{children}
</LabelBody>
<RadioPoint />
<SpanBody>{children}</SpanBody>
</RadioSkeleton>
)
}
export const RadioSkeleton = (props: RadioProps) => {
const { hideRadio, visibleRadioAs, id, ...ariaProps } = props
const { visibleRadioAs, id, ...ariaProps } = props
const { children } = ariaProps
const state = useContext(RadioContext)
if (!state) {
@ -42,7 +40,7 @@ export const RadioSkeleton = (props: RadioProps) => {
const { inputProps } = useRadio(ariaProps, state, ref)
return (
<Label $hideRadio={hideRadio} htmlFor={id} className={props.className}>
<Label htmlFor={id} className={props.className}>
<InputRadio
{...inputProps}
// Avoid react-aria focus next element (input, button, etc.) on keydown for rgaa
@ -136,26 +134,12 @@ export const VisibleRadio = styled.span`
}
`
const Label = styled.label<{ $hideRadio?: boolean; htmlFor?: string }>`
${({ $hideRadio }) =>
$hideRadio &&
css`
margin-top: -1px;
`}
`
const Label = styled.label<{ htmlFor?: string }>``
export const LabelBody = styled(Body)<{
$hideRadio?: boolean
htmlFor?: string
}>`
export const SpanBody = styled(Body).attrs({ as: 'span' })`
margin: ${({ theme }) => theme.spacings.xs} 0px;
margin-left: ${({ theme }) => theme.spacings.xxs};
background-color: transparent;
${({ $hideRadio }) =>
$hideRadio &&
css`
margin: 0 !important;
`}
`
export const InputRadio = styled.input`

View File

@ -6,7 +6,7 @@ import { Markdown } from '@/components/utils/markdown'
import { CardContainer } from '@/design-system/card/Card'
import { Emoji } from '@/design-system/emoji'
import { LabelBody, RadioPoint, RadioSkeleton, VisibleRadio } from './Radio'
import { RadioPoint, RadioSkeleton, SpanBody, VisibleRadio } from './Radio'
const Description = styled.span`
display: block;
@ -19,7 +19,7 @@ const StyledRadioPoint = styled(RadioPoint)`
margin-top: 0.2rem;
`
export const StyledRadioSkeleton = ({
export const RadioCardSkeleton = ({
children,
...rest
}: ComponentProps<typeof RadioSkeleton>) => (
@ -30,14 +30,15 @@ export const StyledRadioSkeleton = ({
const StyledCardContainer = styled(CardContainer)`
${VisibleRadio} {
padding: 0;
margin: 0;
width: 100%;
background: transparent;
border: none;
border: 2px solid transparent;
border-radius: 0.25rem;
}
margin: 0.5rem 0;
padding: 0;
margin: ${({ theme }) => theme.spacings.xs} 0;
`
type RadioCardProps = AriaRadioProps & {
@ -47,9 +48,9 @@ type RadioCardProps = AriaRadioProps & {
description?: string
emoji?: string
autoFocus?: boolean
hideRadio?: boolean
}
// TODO: isDisabled style
export function RadioCard({
label,
description,
@ -57,16 +58,16 @@ export function RadioCard({
...props
}: RadioCardProps) {
return (
<StyledRadioSkeleton {...props}>
{!props.hideRadio && <StyledRadioPoint />}
<LabelBody as="span" $hideRadio={props.hideRadio}>
<RadioCardSkeleton {...props}>
<StyledRadioPoint />
<SpanBody>
<span>
{label} {emoji && <Emoji emoji={emoji} />}
</span>
{description && (
<Markdown as={Description}>{description ?? ''}</Markdown>
)}
</LabelBody>
</StyledRadioSkeleton>
</SpanBody>
</RadioCardSkeleton>
)
}

View File

@ -1,6 +1,7 @@
import { Meta, StoryObj } from '@storybook/react'
import { RadioCard, RadioCardGroup } from '.'
import { RadioCardSkeleton } from './RadioCard'
const meta: Meta<typeof RadioCardGroup> = {
component: RadioCardGroup,
@ -29,12 +30,27 @@ export const Basic: Story = {
description={'ceci est une description'}
/>
<RadioCard value="valueC" label={'Title C'} />
<RadioCard
hideRadio
value="valueD"
label={'Title D'}
description="hidden radio"
/>
<RadioCard isDisabled value="valueD" label={'RadioCard disabled'} />
</RadioCardGroup>
),
args: {},
}
export const Custom: Story = {
render: (args) => (
<RadioCardGroup {...args}>
<RadioCardSkeleton value="value1">
<p>Custom content without radio point</p>
</RadioCardSkeleton>
<RadioCardSkeleton value="value2">
<p>Value 2</p>
</RadioCardSkeleton>
<RadioCardSkeleton value="value3">
<p>Value 3</p>
</RadioCardSkeleton>
<RadioCardSkeleton isDisabled value="value4">
<p>RadioCard disabled</p>
</RadioCardSkeleton>
</RadioCardGroup>
),
args: {},

View File

@ -16,6 +16,9 @@ export const Basic: Story = {
<Radio value="valueA">Radio A</Radio>
<Radio value="valueB">Radio B</Radio>
<Radio value="valueC">Radio C</Radio>
<Radio isDisabled value="valueD">
Radio Disabled
</Radio>
</RadioGroup>
),
args: {},

View File

@ -16,6 +16,9 @@ export const Basic: Story = {
<Radio value="valueA">Radio A</Radio>
<Radio value="valueB">Radio B</Radio>
<Radio value="valueC">Radio C</Radio>
<Radio isDisabled value="valueD">
Radio Disabled
</Radio>
</ToggleGroup>
),
args: {},

View File

@ -5,9 +5,9 @@ import styled, { css } from 'styled-components'
import {
InputRadio,
LabelBody,
RadioButton,
RadioContext,
SpanBody,
VisibleRadio,
} from './Radio'
@ -71,7 +71,7 @@ const TabModeCheckedStyle = css`
border: none !important;
background-color: ${({ theme }) => theme.colors.bases.primary[600]};
${LabelBody} {
${SpanBody} {
color: ${({ theme }) => theme.colors.extended.grey[100]}!important;
}
`
@ -112,7 +112,7 @@ export const ToggleGroupContainer = styled.div<{
${({ $isDisabled: isDisabled }) => !isDisabled && 'cursor: pointer;'}
}
${LabelBody} {
${SpanBody} {
margin: 0;
margin-left: ${({ theme }) => theme.spacings.xxs};
}

View File

@ -9,6 +9,8 @@ import styled, {
import urssafTheme from '@/design-system/theme'
import { useDarkMode } from '@/hooks/useDarkMode'
import { GlobalStyle } from './global-style'
type SystemRootProps = {
children: ReactNode
forceDarkMode?: boolean
@ -24,7 +26,10 @@ const SystemRoot = ({ children, forceDarkMode }: SystemRootProps) => {
return (
<StyleSheetManager disableCSSOMInjection={isbot(userAgent)}>
<ThemeProvider theme={{ ...urssafTheme, darkMode }}>
<BackgroundStyle $darkMode={darkMode}>{children}</BackgroundStyle>
<BackgroundStyle $darkMode={darkMode}>
<GlobalStyle />
{children}
</BackgroundStyle>
</ThemeProvider>
</StyleSheetManager>
)

View File

@ -27,7 +27,7 @@ export const Body = styled.p`
line-height: 1.5rem;
`
export const SmallBody = styled.p<{ grey: boolean }>`
export const SmallBody = styled.p<{ grey?: boolean }>`
${baseParagraphStyle}
font-size: 0.875rem;
line-height: 1.25rem;

View File

@ -3,12 +3,12 @@ import { Trans, useTranslation } from 'react-i18next'
import styled, { css } from 'styled-components'
import { Appear } from '@/components/ui/animate'
import { Chip } from '@/design-system'
import { Button, HelpButtonWithPopover } from '@/design-system/buttons'
import { ChevronIcon } from '@/design-system/icons'
import InfoBulle from '@/design-system/InfoBulle'
import { Grid } from '@/design-system/layout'
import { H3, H4 } from '@/design-system/typography/heading'
import { Strong } from '@/design-system/typography'
import { H4, H5 } from '@/design-system/typography/heading'
import { Link } from '@/design-system/typography/link'
import { Li, Ul } from '@/design-system/typography/list'
import { Body } from '@/design-system/typography/paragraphs'
@ -24,45 +24,50 @@ interface ResultProps {
contenuAnnexe: string[]
contenuExclu: string[]
}
hideGuichetUnique: boolean
}
export const Result = ({ item, debug }: ResultProps) => {
export const Result = ({ item, debug, hideGuichetUnique }: ResultProps) => {
const { title, codeApe, contenuCentral, contenuAnnexe, contenuExclu } = item
const [open, setOpen] = useState(false)
const { t } = useTranslation()
return (
<Grid container style={{ alignItems: 'center' }}>
<Grid item xs={12} sm={8} md={9} xl={10}>
<H3 style={{ marginTop: 0, marginBottom: '.5rem' }}>
{title} <Chip type="secondary">{codeApe}</Chip>
{debug && (
<InfoBulle>
<pre>{debug}</pre>
</InfoBulle>
)}
</H3>
</Grid>
<StyledGrid item xs={12} sm={4} md={3} xl={2}>
<>
<H4 as="h3">
{title}
{debug && (
<InfoBulle>
<pre>{debug}</pre>
</InfoBulle>
)}
</H4>
<Body
css={`
display: flex;
justify-content: space-between;
align-items: center;
`}
>
<Strong>Code : {codeApe}</Strong>
<Button
light
size="XXS"
light
color="secondary"
onPress={() => setOpen((x) => !x)}
aria-expanded={open}
aria-controls={`info-${codeApe}`}
aria-label={!open ? t('En savoir plus') : t('Replier')}
>
{!open ? t('En savoir plus') : t('Replier')}{' '}
{!open ? t('En savoir plus') : t('Replier')}&nbsp;
<StyledChevron aria-hidden $isOpen={open} />
</Button>
</StyledGrid>
</Body>
{open && (
<Appear id={`info-${codeApe}`}>
{contenuCentral.length ? (
<>
<H4>Contenu central de cette activité :</H4>
<H5 as="h4">Contenu central de cette activité :</H5>
<Ul>
{contenuCentral.map((contenu, i) => (
<Li key={i}>{contenu}</Li>
@ -73,7 +78,7 @@ export const Result = ({ item, debug }: ResultProps) => {
{contenuAnnexe.length ? (
<>
<H4>Contenu annexe de cette activité :</H4>
<H5 as="h4">Contenu annexe de cette activité :</H5>
<Ul>
{contenuAnnexe.map((contenu, i) => (
<Li key={i}>{contenu}</Li>
@ -84,7 +89,7 @@ export const Result = ({ item, debug }: ResultProps) => {
{contenuExclu.length ? (
<>
<H4>Contenu exclu de cette activité :</H4>
<H5 as="h4">Contenu exclu de cette activité :</H5>
<Ul>
{contenuExclu.map((contenu, i) => (
<Li key={i}>{contenu}</Li>
@ -92,33 +97,37 @@ export const Result = ({ item, debug }: ResultProps) => {
</Ul>
</>
) : null}
<Trans i18nKey={'codeApe.catégorie-guichet'}>
<H4>
Catégories du Guichet unique
<HelpButtonWithPopover
type="info"
title="Qu'est-ce que le guichet unique ?"
>
<Body>
Le{' '}
<Link href="https://procedures.inpi.fr/">
Guichet électronique des formalités dentreprises
</Link>{' '}
(Guichet unique) est un portail internet sécurisé, auprès
duquel toute entreprise est tenue de déclarer sa création,
depuis le 1er janvier 2023.
</Body>
<Body>
Il utilise une classification des activités différente de
celle utilisée par l'INSEE pour code APE.
</Body>
</HelpButtonWithPopover>
</H4>
</Trans>
<GuichetInfo apeCode={codeApe} />
{!hideGuichetUnique && (
<>
<Trans i18nKey={'codeApe.catégorie-guichet'}>
<H4>
Catégories du Guichet unique
<HelpButtonWithPopover
type="info"
title="Qu'est-ce que le guichet unique ?"
>
<Body>
Le{' '}
<Link href="https://procedures.inpi.fr/">
Guichet électronique des formalités dentreprises
</Link>{' '}
(Guichet unique) est un portail internet sécurisé, auprès
duquel toute entreprise est tenue de déclarer sa création,
depuis le 1er janvier 2023.
</Body>
<Body>
Il utilise une classification des activités différente de
celle utilisée par l'INSEE pour code APE.
</Body>
</HelpButtonWithPopover>
</H4>
</Trans>
<GuichetInfo apeCode={codeApe} />
</>
)}
</Appear>
)}
</Grid>
</>
)
}

View File

@ -14,8 +14,8 @@ import {
} from '@/design-system'
import { Button } from '@/design-system/buttons'
import { Emoji } from '@/design-system/emoji'
import { StyledRadioSkeleton } from '@/design-system/field/Radio/RadioCard'
import { Grid, Spacing } from '@/design-system/layout'
import { RadioCardSkeleton } from '@/design-system/field/Radio/RadioCard'
import { Spacing } from '@/design-system/layout'
import { SmallBody } from '@/design-system/typography/paragraphs'
import { useAsyncData } from '@/hooks/useAsyncData'
@ -81,9 +81,15 @@ interface ListResult {
interface SearchCodeApeProps {
disabled?: boolean
hideGuichetUnique?: boolean
onCodeAPESelected?: (codeAPE: string) => void
}
export default function SearchCodeAPE({ disabled }: SearchCodeApeProps) {
export default function SearchCodeAPE({
disabled,
hideGuichetUnique = false,
onCodeAPESelected,
}: SearchCodeApeProps) {
const { t } = useTranslation()
const [job, setJob] = useState('')
const [selected, setSelected] = useState('')
@ -193,62 +199,70 @@ export default function SearchCodeAPE({ disabled }: SearchCodeApeProps) {
})
}, [job])
const ret = (
<Grid container>
<Grid item lg={12} xl={11}>
<SearchField
value={job}
onChange={setJob}
label={t("Mots-clés définissants l'activité")}
placeholder={t('Par exemple : coiffure, boulangerie ou restauration')}
/>
{alternative ? (
<FromTop>
<Message border={false} icon mini style={{ margin: '.5rem 0' }}>
<SmallBody>
<Trans i18nKey="search-code-ape.alternative" shouldUnescape>
Vous pouvez essayer "
{{ proposal: alternative.proposal.join('", "') }}" au lieu de
"{{ match: alternative.match }}"
</Trans>
</SmallBody>
</Message>
</FromTop>
) : (
<Spacing xs />
)}
{list.length > 0 && (
<FromTop>
<TrackPage name="recherche" />
<StyledRadioCardGroup
value={selected}
onChange={setSelected}
isDisabled={disabled}
aria-label={t(
'search-code-ape.radio-card-group.aria-label',
'Liste des activités'
)}
>
{list.slice(0, 25).map(({ item, debug }) => {
return (
<StyledRadioSkeleton
isDisabled={disabled}
value={item.codeApe}
key={item.codeApe}
visibleRadioAs="div"
>
<Result item={item} debug={debug} />
</StyledRadioSkeleton>
)
})}
</StyledRadioCardGroup>
</FromTop>
)}
useEffect(() => {
if (onCodeAPESelected) {
onCodeAPESelected(selected)
}
}, [selected, onCodeAPESelected])
<Spacing md />
{/* <ActivityNotFound job={job} /> */}
</Grid>
</Grid>
const ret = (
<>
<SearchField
value={job}
onChange={setJob}
label={t("Mots-clés définissants l'activité")}
placeholder={t('Par exemple : coiffure, boulangerie ou restauration')}
/>
{alternative ? (
<FromTop>
<Message border={false} icon mini style={{ margin: '.5rem 0' }}>
<SmallBody>
<Trans i18nKey="search-code-ape.alternative" shouldUnescape>
Vous pouvez essayer "
{{ proposal: alternative.proposal.join('", "') }}" au lieu de "
{{ match: alternative.match }}"
</Trans>
</SmallBody>
</Message>
</FromTop>
) : (
<Spacing xs />
)}
{list.length > 0 && (
<FromTop>
<TrackPage name="recherche" />
<StyledRadioCardGroup
value={selected}
onChange={setSelected}
isDisabled={disabled}
aria-label={t(
'search-code-ape.radio-card-group.aria-label',
'Liste des activités'
)}
>
{list.slice(0, 25).map(({ item, debug }) => {
return (
<RadioCardSkeleton
isDisabled={disabled}
value={item.codeApe}
key={item.codeApe}
visibleRadioAs="div"
>
<Result
item={item}
debug={debug}
hideGuichetUnique={hideGuichetUnique}
/>
</RadioCardSkeleton>
)
})}
</StyledRadioCardGroup>
</FromTop>
)}
<Spacing md />
{/* <ActivityNotFound job={job} /> */}
</>
)
return ret