Améliore le Drawer

pull/2573/head
Jérémy Rialland 2023-04-27 14:41:57 +02:00 committed by Jérémy Rialland
parent bb0de0997c
commit 25d06e5791
10 changed files with 324 additions and 266 deletions

View File

@ -21,7 +21,7 @@ import { Strong } from '@/design-system/typography'
import { H4 } from '@/design-system/typography/heading'
import { StyledLink } from '@/design-system/typography/link'
import { Body } from '@/design-system/typography/paragraphs'
import { useOnClickOutside } from '@/hooks/useClickOutside'
import { useOnClickOutside } from '@/hooks/useOnClickOutside'
import { useSitePaths } from '@/sitePaths'
import * as safeLocalStorage from '../../storage/safeLocalStorage'

View File

@ -54,10 +54,6 @@ const StyledButton = styled(Button)`
flex: 0;
align-items: center;
margin-right: ${({ theme }) => theme.spacings.md};
@media (max-width: ${({ theme }) => theme.breakpointsWidth.sm}) {
order: 3;
margin-right: 0;
}
`
const StyledIcon = styled.svg`

View File

@ -347,8 +347,9 @@ export const SwitchInput = (props: {
label?: string
id?: string
key?: string
invertLabel?: boolean
}) => {
const { onChange, id, label, defaultSelected, key } = props
const { onChange, id, label, defaultSelected, key, invertLabel } = props
return (
<Switch
@ -357,6 +358,7 @@ export const SwitchInput = (props: {
light
id={id}
key={key}
invertLabel={invertLabel}
>
{label}
</Switch>

View File

@ -1,15 +1,18 @@
import FocusTrap from 'focus-trap-react'
import React, { ReactNode, useCallback, useEffect, useState } from 'react'
import { ReactNode, useCallback, useEffect, useRef, useState } from 'react'
import ReactDOM from 'react-dom'
import { Trans } from 'react-i18next'
import styled, { css } from 'styled-components'
import { useOnClickOutside } from '@/hooks/useOnClickOutside'
import { useOnKeyDown } from '@/hooks/useOnKeyDown'
import { Button } from '../buttons'
import { Grid } from '../layout'
import { CloseButton, CloseButtonContainer } from '../popover/Popover'
export type DrawerButtonProps = {
onClick: () => void
onPress: () => void
['aria-expanded']: boolean
['aria-haspopup']:
| boolean
@ -32,14 +35,15 @@ export const Drawer = ({
cancelLabel,
isDismissable = true,
}: {
trigger: ({ onClick }: DrawerButtonProps) => ReactNode
trigger: (props: DrawerButtonProps) => ReactNode
children: ReactNode
confirmLabel?: string
cancelLabel?: string
onConfirm: () => void
onConfirm?: () => void
onCancel?: () => void
isDismissable?: boolean
}) => {
const panel = useRef<HTMLDivElement>(null)
const [isOpen, setIsOpen] = useState(false)
const [isMounted, setIsMounted] = useState(false)
@ -55,8 +59,10 @@ export const Drawer = ({
const scrollY = document.body.style.top
document.body.style.position = ''
document.body.style.top = ''
// Avoid scroll jump
window.scrollTo(0, parseInt(scrollY || '0') * -1)
if (scrollY) {
// Avoid scroll jump
window.scrollTo(0, parseInt(scrollY) * -1)
}
}
}
@ -67,8 +73,9 @@ export const Drawer = ({
}
}, [isMounted])
const closeDrawer = () => {
const closeDrawer = useCallback(() => {
setIsOpen(false)
disablePageScrolling(false)
setTimeout(() => {
setIsMounted(false)
@ -76,18 +83,21 @@ export const Drawer = ({
onCancel()
}
}, 500)
}
}, [onCancel])
const handleDeactivate = useCallback(() => {
disablePageScrolling(false)
}, [])
useOnClickOutside(panel, () => {
closeDrawer()
})
useOnKeyDown('Escape', () => {
closeDrawer()
})
return (
<>
{trigger({
'aria-expanded': isOpen,
'aria-haspopup': 'dialog',
onClick: openDrawer,
onPress: openDrawer,
})}
{isMounted &&
ReactDOM.createPortal(
@ -96,13 +106,10 @@ export const Drawer = ({
<FocusTrap
focusTrapOptions={{
clickOutsideDeactivates: true,
onDeactivate: () => {
closeDrawer()
handleDeactivate()
},
fallbackFocus: panel.current ?? undefined,
}}
>
<DrawerPanel $isOpen={isOpen} role="dialog">
<DrawerPanel ref={panel} $isOpen={isOpen} role="dialog">
{isDismissable && (
<StyledCloseButtonContainer>
{/* TODO : replace with Link when in design system */}
@ -129,15 +136,10 @@ export const Drawer = ({
</CloseButton>
</StyledCloseButtonContainer>
)}
<DrawerContent>
{React.Children.map(children, (child) => {
if (React.isValidElement(child)) {
return React.cloneElement(child, { closeDrawer } as {
closeDrawer: () => void
})
}
})}
</DrawerContent>
<DrawerContentWrapper>
<DrawerContent>{children}</DrawerContent>
</DrawerContentWrapper>
{onConfirm && (
<DrawerFooter>
@ -197,12 +199,15 @@ const DrawerBackground = styled.div<{ $isOpen?: boolean }>`
const DrawerPanel = styled.div<{
$isOpen: boolean
}>`
display: flex;
flex-direction: column;
width: 500px;
max-width: 100vw;
height: 100vh;
overflow-x: hidden;
overflow-y: auto;
background-color: ${({ theme }) => theme.colors.extended.grey[100]};
background-color: ${({ theme }) =>
theme.darkMode
? theme.colors.extended.dark[600]
: theme.colors.extended.grey[100]};
transition: transform 0.5s ease-in-out;
position: fixed;
right: 0;
@ -216,6 +221,11 @@ const DrawerPanel = styled.div<{
`}
`
const DrawerContentWrapper = styled.div`
overflow: auto;
flex-grow: 1;
`
const DrawerContent = styled.div`
padding: 0 ${({ theme }) => theme.spacings.xxl};
padding-bottom: 2rem;
@ -223,14 +233,12 @@ const DrawerContent = styled.div`
`
const DrawerFooter = styled.div`
position: sticky;
bottom: 0;
left: 0;
right: 0;
background: ${({ theme }) => theme.colors.extended.grey[100]};
background-color: ${({ theme }) =>
theme.darkMode
? theme.colors.extended.dark[600]
: theme.colors.extended.grey[100]};
padding: ${({ theme }) => theme.spacings.xl};
box-shadow: 0px 1px 15px rgba(0, 0, 0, 0.15);
z-index: 10;
`
const StyledGrid = styled(Grid)`
@ -239,11 +247,4 @@ const StyledGrid = styled(Grid)`
gap: ${({ theme }) => theme.spacings.md};
`
const StyledCloseButtonContainer = styled(CloseButtonContainer)`
position: sticky;
top: 0;
left: 0;
right: 0;
background: ${({ theme }) => theme.colors.extended.grey[100]};
z-index: 10;
`
const StyledCloseButtonContainer = styled(CloseButtonContainer)``

View File

@ -11,7 +11,7 @@ import { DayPicker, useInput } from 'react-day-picker'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
import { useOnClickOutside } from '@/hooks/useClickOutside'
import { useOnClickOutside } from '@/hooks/useOnClickOutside'
import { Button } from '../buttons'
import { Emoji } from '../emoji'
@ -133,6 +133,7 @@ export default function DateField(props: DateFieldProps) {
<div>
<Wrapper>
<TextField
// eslint-disable-next-line react/jsx-props-no-spreading
{...ariaProps}
label={label}
aria-label={t(
@ -185,7 +186,6 @@ export default function DateField(props: DateFieldProps) {
<FocusTrap
active
focusTrapOptions={{
allowOutsideClick: true,
clickOutsideDeactivates: true,
fallbackFocus: refs.reference.current ?? undefined,
}}
@ -211,6 +211,7 @@ export default function DateField(props: DateFieldProps) {
aria-label="Calendrier de sélection de date"
>
<DayPicker
// eslint-disable-next-line react/jsx-props-no-spreading
{...dayPickerProps}
captionLayout="dropdown-buttons"
mode="single"

View File

@ -16,6 +16,7 @@ import styled, { css, keyframes } from 'styled-components'
import { Grid } from '@/design-system/layout'
import { getIframeOffset, wrapperDebounceEvents } from '@/utils'
import { FocusStyle } from '../global-style'
import { Container } from '../layout'
import { H2 } from '../typography/heading'
@ -233,9 +234,13 @@ export const CloseButtonContainer = styled.div`
`}
display: flex;
align-items: center;
height: ${({ theme }) => theme.spacings.xxl};
justify-content: flex-end;
background-color: ${({ theme }) =>
theme.darkMode
? theme.colors.extended.dark[600]
: theme.colors.extended.grey[100]};
`
export const CloseButton = styled.button`
display: inline-flex;
align-items: center;
@ -263,6 +268,11 @@ export const CloseButton = styled.button`
:hover {
text-decoration: underline;
}
:focus {
${FocusStyle}
box-shadow: inset 0 0 0 3px #ffffff;
outline-offset: -2px;
}
`
const PopoverContent = styled.div`

View File

@ -86,8 +86,15 @@ const LabelBody = styled(Body)`
cursor: pointer;
`
const Text = styled.span`
margin-right: ${({ theme }) => theme.spacings.xxs};
const Text = styled.span<{ invertLabel?: boolean }>`
${({ theme, invertLabel: $invertLabel }) =>
$invertLabel
? css`
margin-left: ${theme.spacings.xxs};
`
: css`
margin-right: ${theme.spacings.xxs};
`};
`
type AriaSwitchProps = Parameters<typeof useSwitch>[0]
@ -98,6 +105,10 @@ export type SwitchProps = AriaSwitchProps & {
children?: ReactNode
className?: string
role?: string
/**
* Invert the position of the label and the switch
*/
invertLabel?: boolean
}
export const Switch = (props: SwitchProps) => {
@ -106,6 +117,7 @@ export const Switch = (props: SwitchProps) => {
light = false,
children,
className,
invertLabel = false,
...ariaProps
} = props
const state = useToggleState(ariaProps)
@ -116,8 +128,10 @@ export const Switch = (props: SwitchProps) => {
const { isSelected } = state
return (
<LabelBody as="label" className={className}>
{children && <Text>{children}</Text>}
<LabelBody as="label" htmlFor={inputProps.id} className={className}>
{children && !invertLabel && (
<Text invertLabel={invertLabel}>{children}</Text>
)}
<StyledSwitch
light={light}
size={size}
@ -125,6 +139,7 @@ export const Switch = (props: SwitchProps) => {
disabled={isDisabled}
>
<HiddenInput
// eslint-disable-next-line react/jsx-props-no-spreading
{...inputProps}
type="checkbox"
tabIndex={0}
@ -139,6 +154,9 @@ export const Switch = (props: SwitchProps) => {
disabled={isDisabled}
/>
</StyledSwitch>
{children && invertLabel && (
<Text invertLabel={invertLabel}>{children}</Text>
)}
</LabelBody>
)
}

View File

@ -0,0 +1,19 @@
import { useEffect } from 'react'
export const useOnKeyDown = (
keyPress: string | null,
handler: (event: KeyboardEvent) => void
) => {
useEffect(() => {
const listener = (event: KeyboardEvent) => {
if (!keyPress || event.key === keyPress) {
handler(event)
}
}
window.addEventListener('keydown', listener)
return () => {
window.removeEventListener('keydown', listener)
}
}, [handler, keyPress])
}

View File

@ -18,7 +18,7 @@ import { Grid, Spacing } from '@/design-system/layout'
import { Tag, TagType } from '@/design-system/tag'
import { Tooltip } from '@/design-system/tooltip'
import { Strong } from '@/design-system/typography'
import { H1, H4, H5 } from '@/design-system/typography/heading'
import { H2, H4, H5 } from '@/design-system/typography/heading'
import { Link, StyledLink } from '@/design-system/typography/link'
import { Body } from '@/design-system/typography/paragraphs'
import { answerQuestion } from '@/store/actions/actions'
@ -62,8 +62,14 @@ const AllerPlusLoinRevenus = ({
return (
<Drawer
trigger={(buttonProps: { onClick: () => void }) => (
<Button color="secondary" light size="XS" {...buttonProps}>
trigger={(buttonProps) => (
<Button
color="secondary"
light
size="XS"
// eslint-disable-next-line react/jsx-props-no-spreading
{...buttonProps}
>
<Trans>Aller plus loin</Trans> <StyledArrowRightIcon />
</Button>
)}
@ -105,226 +111,229 @@ const AllerPlusLoinRevenus = ({
}}
>
<>
<H1>
<H2>
<Trans>Aller plus loin sur les revenus</Trans>
</H1>
</H2>
<H4
as="h2"
as="h3"
css={`
margin-bottom: 1rem;
`}
>
<Trans>Calculer vos revenus</Trans>
</H4>
<StyledTable>
<caption className="sr-only">
{t(
'comparateur.allerPlusLoin.tableCaption',
"Tableau affichant le détail du calcul du revenu net pour la SASU, l'entreprise individuelle (EI) et l'auto-entreprise (AE)."
)}
</caption>
<thead>
<tr>
<th className="sr-only">Type de structure</th>
<th scope="col">
<span className="table-title-sasu">
<StatusTagIcon status="sasu" /> SASU
</span>
</th>
<th scope="col">
<Tooltip
tooltip="Entreprise individuelle"
id="tooltip-ei-table"
>
<span className="table-title-ei">
<StatusTagIcon status="ei" /> EI
<WrapperTable>
<StyledTable>
<caption className="sr-only">
{t(
'comparateur.allerPlusLoin.tableCaption',
"Tableau affichant le détail du calcul du revenu net pour la SASU, l'entreprise individuelle (EI) et l'auto-entreprise (AE)."
)}
</caption>
<thead>
<tr>
<th className="sr-only">Type de structure</th>
<th scope="col">
<span className="table-title-sasu">
<StatusTagIcon status="sasu" /> SASU
</span>
</Tooltip>
</th>
</th>
<th scope="col">
<Tooltip tooltip="Auto-entreprise" id="tooltip-ae-table">
<span className="table-title-ae">
<StatusTagIcon status="ae" /> AE
</span>
</Tooltip>
</th>
</tr>
</thead>
<tbody>
<tr>
<th scope="row">
<Minus
css={`
opacity: 0;
`}
aria-hidden
>
-
</Minus>{' '}
<Trans>Chiffre d'affaires</Trans>
</th>
<td colSpan={3}>
<StyledTag $color={'grey' as TagType}>
<Value
expression="entreprise . chiffre d'affaires"
displayedUnit="€"
linkToRule={false}
/>
</StyledTag>
</td>
</tr>
<tr>
<th scope="row">
<Minus aria-label={t('moins')}>-</Minus> <Trans>Charges</Trans>
</th>
<td colSpan={3}>
<StyledTag $color={'grey' as TagType}>
<Value
expression="entreprise . charges"
unit="€/an"
displayedUnit="€"
linkToRule={false}
/>
</StyledTag>
</td>
</tr>
<tr>
<th scope="row">
<Minus aria-label={t('moins')}>-</Minus>{' '}
<Trans>Cotisations</Trans>
</th>
<td>
<StyledTag $color={'secondary' as TagType}>
<Value
expression="dirigeant . rémunération . cotisations"
engine={assimiléEngine}
unit="€/an"
displayedUnit="€"
linkToRule={false}
/>
</StyledTag>
</td>
<td>
<StyledTag $color={'independant' as TagType}>
<Value
expression="dirigeant . rémunération . cotisations"
engine={indépendantEngine}
unit="€/an"
displayedUnit="€"
linkToRule={false}
/>
</StyledTag>
</td>
<td>
<StyledTag $color={'tertiary' as TagType}>
<Value
expression="dirigeant . rémunération . cotisations"
engine={autoEntrepreneurEngine}
unit="€/an"
displayedUnit="€"
linkToRule={false}
/>
</StyledTag>
</td>
</tr>
<tr>
<th scope="row">
<Minus aria-label={t('moins')}>-</Minus> <Trans>Impôts</Trans>
</th>
<td>
<StyledTag $color={'secondary' as TagType}>
<Value
expression="dirigeant . rémunération . impôt"
engine={assimiléEngine}
unit="€/an"
displayedUnit="€"
linkToRule={false}
/>
</StyledTag>
</td>
<td>
<StyledTag $color={'independant' as TagType}>
<Value
expression="dirigeant . rémunération . impôt"
engine={indépendantEngine}
unit="€/an"
displayedUnit="€"
linkToRule={false}
/>
</StyledTag>
</td>
<td>
<StyledTag $color={'tertiary' as TagType}>
<Value
expression="dirigeant . rémunération . impôt"
engine={autoEntrepreneurEngine}
unit="€/an"
displayedUnit="€"
linkToRule={false}
/>
</StyledTag>
</td>
</tr>
</tbody>
<tfoot>
<tr>
<th scope="row">
<Minus
css={`
opacity: 0;
`}
aria-hidden
>
-
</Minus>{' '}
<StyledStrong>
<Trans>Revenu net</Trans>
</StyledStrong>
</th>
<td>
<StyledTag $color={'secondary' as TagType}>
<Strong>
<th scope="col">
<Tooltip
tooltip="Entreprise individuelle"
id="tooltip-ei-table"
>
<span className="table-title-ei">
<StatusTagIcon status="ei" /> EI
</span>
</Tooltip>
</th>
<th scope="col">
<Tooltip tooltip="Auto-entreprise" id="tooltip-ae-table">
<span className="table-title-ae">
<StatusTagIcon status="ae" /> AE
</span>
</Tooltip>
</th>
</tr>
</thead>
<tbody>
<tr>
<th scope="row">
<Minus
css={`
opacity: 0;
`}
aria-hidden
>
-
</Minus>{' '}
<Trans>Chiffre d'affaires</Trans>
</th>
<td colSpan={3}>
<StyledTag $color={'grey' as TagType}>
<Value
expression="dirigeant . rémunération . net . après impôt"
expression="entreprise . chiffre d'affaires"
displayedUnit="€"
linkToRule={false}
/>
</StyledTag>
</td>
</tr>
<tr>
<th scope="row">
<Minus aria-label={t('moins')}>-</Minus>{' '}
<Trans>Charges</Trans>
</th>
<td colSpan={3}>
<StyledTag $color={'grey' as TagType}>
<Value
expression="entreprise . charges"
unit="€/an"
displayedUnit="€"
linkToRule={false}
/>
</StyledTag>
</td>
</tr>
<tr>
<th scope="row">
<Minus aria-label={t('moins')}>-</Minus>{' '}
<Trans>Cotisations</Trans>
</th>
<td>
<StyledTag $color={'secondary' as TagType}>
<Value
expression="dirigeant . rémunération . cotisations"
engine={assimiléEngine}
unit="€/an"
displayedUnit="€"
linkToRule={false}
/>
</Strong>
</StyledTag>
</td>
<td>
<StyledTag $color={'independant' as TagType}>
<Strong>
</StyledTag>
</td>
<td>
<StyledTag $color={'independant' as TagType}>
<Value
expression="dirigeant . rémunération . net . après impôt"
expression="dirigeant . rémunération . cotisations"
engine={indépendantEngine}
unit="€/an"
displayedUnit="€"
linkToRule={false}
/>
</Strong>
</StyledTag>
</td>
<td>
<StyledTag $color={'tertiary' as TagType}>
<Strong>
</StyledTag>
</td>
<td>
<StyledTag $color={'tertiary' as TagType}>
<Value
expression="dirigeant . rémunération . net . après impôt"
expression="dirigeant . rémunération . cotisations"
engine={autoEntrepreneurEngine}
unit="€/an"
displayedUnit="€"
linkToRule={false}
/>
</Strong>
</StyledTag>
</td>
</tr>
</tfoot>
</StyledTable>
</StyledTag>
</td>
</tr>
<tr>
<th scope="row">
<Minus aria-label={t('moins')}>-</Minus> <Trans>Impôts</Trans>
</th>
<td>
<StyledTag $color={'secondary' as TagType}>
<Value
expression="dirigeant . rémunération . impôt"
engine={assimiléEngine}
unit="€/an"
displayedUnit="€"
linkToRule={false}
/>
</StyledTag>
</td>
<td>
<StyledTag $color={'independant' as TagType}>
<Value
expression="dirigeant . rémunération . impôt"
engine={indépendantEngine}
unit="€/an"
displayedUnit="€"
linkToRule={false}
/>
</StyledTag>
</td>
<td>
<StyledTag $color={'tertiary' as TagType}>
<Value
expression="dirigeant . rémunération . impôt"
engine={autoEntrepreneurEngine}
unit="€/an"
displayedUnit="€"
linkToRule={false}
/>
</StyledTag>
</td>
</tr>
</tbody>
<tfoot>
<tr>
<th scope="row">
<Minus
css={`
opacity: 0;
`}
aria-hidden
>
-
</Minus>{' '}
<StyledStrong>
<Trans>Revenu net</Trans>
</StyledStrong>
</th>
<td>
<StyledTag $color={'secondary' as TagType}>
<Strong>
<Value
expression="dirigeant . rémunération . net . après impôt"
engine={assimiléEngine}
unit="€/an"
displayedUnit="€"
linkToRule={false}
/>
</Strong>
</StyledTag>
</td>
<td>
<StyledTag $color={'independant' as TagType}>
<Strong>
<Value
expression="dirigeant . rémunération . net . après impôt"
engine={indépendantEngine}
unit="€/an"
displayedUnit="€"
linkToRule={false}
/>
</Strong>
</StyledTag>
</td>
<td>
<StyledTag $color={'tertiary' as TagType}>
<Strong>
<Value
expression="dirigeant . rémunération . net . après impôt"
engine={autoEntrepreneurEngine}
unit="€/an"
displayedUnit="€"
linkToRule={false}
/>
</Strong>
</StyledTag>
</td>
</tr>
</tfoot>
</StyledTable>
</WrapperTable>
<Spacing md />
<Flex>
<H4 as="h2">Bénéficier de l'ACRE</H4>
@ -358,10 +367,9 @@ const AllerPlusLoinRevenus = ({
id="activation-acre"
onChange={(value: boolean) => setAcreValue(value)}
defaultSelected={defaultValueACRE as boolean}
label="Activer l'ACRE dans la simulation"
invertLabel
/>
<Label htmlFor="activation-acre">
Activer l'ACRE dans la simulation
</Label>
</FlexCentered>
{(acreValue || defaultValueACRE) && (
@ -378,10 +386,9 @@ const AllerPlusLoinRevenus = ({
id="activation-acre-ae"
onChange={(value: boolean) => setAEAcreValue(value)}
defaultSelected={isAutoEntrepreneurACREEnabled}
label="Je suis éligible à l'ACRE pour mon auto-entreprise"
invertLabel
/>
<Label htmlFor="activation-acre-ae">
Je suis éligible à l'ACRE pour mon auto-entreprise
</Label>
</FlexCentered>
</>
)}
@ -462,10 +469,9 @@ const AllerPlusLoinRevenus = ({
id="versement-liberatoire"
onChange={setVersementLiberatoireValue}
defaultSelected={defaultValueVersementLiberatoire as boolean}
label="Activer le versement libératoire dans la simulation."
invertLabel
/>
<Label htmlFor="versement-liberatoire">
Activer le versement libératoire dans la simulation.
</Label>
</FlexCentered>
</>
</Drawer>
@ -497,16 +503,14 @@ const FlexCentered = styled.div`
align-items: center;
`
const Label = styled.label`
margin-left: ${({ theme }) => theme.spacings.md};
font-family: ${({ theme }) => theme.fonts.main};
font-size: 1rem;
`
const StyledArrowRightIcon = styled(ArrowRightIcon)`
margin-left: ${({ theme }) => theme.spacings.sm};
`
const WrapperTable = styled.div`
overflow: auto;
`
const StyledTable = styled.table`
width: 100%;
text-align: left;
@ -519,6 +523,13 @@ const StyledTable = styled.table`
border-spacing: ${({ theme }) => theme.spacings.md}!important;
}
th {
color: ${({ theme }) =>
theme.darkMode
? theme.colors.extended.grey[200]
: theme.colors.extended.grey[800]};
}
thead th {
text-align: center;
font-size: 0.75rem;