Replace popper by floating-ui; add aria label

pull/2573/head
Jérémy Rialland 2023-04-26 11:24:17 +02:00 committed by Jérémy Rialland
parent 208f982691
commit 7aca423931
4 changed files with 114 additions and 98 deletions

View File

@ -52,6 +52,7 @@
"@atomik-color/component": "^1.0.17",
"@atomik-color/core": "^1.0.13",
"@axe-core/react": "^4.5.2",
"@floating-ui/react-dom": "^1.3.0",
"@internationalized/number": "^3.2.0",
"@leeoniya/ufuzzy": "^1.0.2",
"@popperjs/core": "^2.11.7",
@ -98,7 +99,6 @@
"react-i18next": "^12.1.5",
"react-instantsearch": "^6.38.1",
"react-instantsearch-dom": "^6.38.1",
"react-popper": "^2.3.0",
"react-redux": "^8.0.5",
"react-router-dom": "^6.4.4",
"react-signature-pad-wrapper": "^3.3.1",

View File

@ -1,34 +1,29 @@
import 'react-day-picker/dist/style.css'
import { autoUpdate, flip, offset, useFloating } from '@floating-ui/react-dom'
import { format as formatDate, isValid, parse } from 'date-fns'
import { enUS, fr } from 'date-fns/locale'
import FocusTrap from 'focus-trap-react'
import { useCallback, useEffect, useRef, useState } from 'react'
import { useId } from 'react-aria'
import { DayPicker, useInput } from 'react-day-picker'
import { Trans, useTranslation } from 'react-i18next'
import { usePopper } from 'react-popper'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
import { useOnClickOutside } from '@/hooks/useClickOutside'
import { Button } from '../buttons'
import { Emoji } from '../emoji'
import { Spacing } from '../layout'
import { Body } from '../typography/paragraphs'
import TextField from './TextField'
interface DateFieldProps {
defaultSelected?: Date
inputValue?: string
onChange?: (value?: string) => void
placeholder?: string
label?: string
'aria-label'?: string
'aria-labelby'?: string
autoFocus?: boolean
isRequired?: boolean
}
export default function DateField(props: DateFieldProps) {
@ -40,15 +35,7 @@ export default function DateField(props: DateFieldProps) {
const [isChangeOnce, setIsChangeOnce] = useState(false)
const [selected, setSelected] = useState<Date>()
const [isPopperOpen, setIsPopperOpen] = useState(false)
const popperRef = useRef<HTMLDivElement>(null)
const dayPickerRef = useRef<HTMLDivElement>(null)
const buttonRef = useRef<HTMLButtonElement>(null)
const containerRef = useRef<HTMLDivElement>(null)
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(
null
)
const [isOpen, setIsOpen] = useState(false)
const id = useId()
@ -66,19 +53,27 @@ export default function DateField(props: DateFieldProps) {
inputProps.value as string
)
const popper = usePopper(popperRef.current, popperElement, {
placement: 'bottom-end',
const { x, y, strategy, refs } = useFloating<HTMLButtonElement>({
open: isOpen,
placement: 'bottom',
middleware: [offset(8), flip()],
whileElementsMounted: autoUpdate,
})
useOnClickOutside(dayPickerRef, () => setIsPopperOpen(false))
useOnClickOutside(refs.floating, () => setIsOpen(false))
const closePopper = () => {
setIsPopperOpen(false)
buttonRef?.current?.focus()
}
const close = useCallback(() => {
setIsOpen((open) => {
if (open) {
refs.reference?.current?.focus()
}
return false
})
}, [refs.reference])
const handleInputChange = (value: string) => {
setIsPopperOpen(false)
setIsOpen(false)
setIsChangeOnce(true)
setInputValue(value)
const date = parse(value, format, new Date())
@ -96,7 +91,7 @@ export default function DateField(props: DateFieldProps) {
}
const handleButtonPress = () => {
setIsPopperOpen((open) => !open)
setIsOpen((open) => !open)
}
const handleDaySelect = useCallback(
@ -105,14 +100,14 @@ export default function DateField(props: DateFieldProps) {
if (date) {
const value = formatDate(date, format)
setInputValue(value)
closePopper()
close()
onChange?.(value)
} else {
setInputValue('')
onChange?.()
}
},
[onChange]
[close, onChange]
)
const oldDefaultSelected = useRef<Date | undefined>(defaultSelected)
@ -127,11 +122,15 @@ export default function DateField(props: DateFieldProps) {
}, [defaultSelected, handleDaySelect])
return (
<div ref={containerRef}>
<Wrapper ref={popperRef}>
<div>
<Wrapper>
<TextField
{...ariaProps}
label={label}
aria-label={t(
'design-system.date-picker.label',
'Champ de date au format jours/mois/année'
)}
placeholder={placeholder}
value={inputValue}
onChange={(value) => {
@ -157,13 +156,13 @@ export default function DateField(props: DateFieldProps) {
}
/>
<StyledButton
ref={buttonRef}
ref={refs.setReference}
onPress={handleButtonPress}
type="button"
aria-haspopup="dialog"
size="XXS"
aria-expanded={isPopperOpen}
aria-controls={isPopperOpen ? id : undefined}
aria-expanded={isOpen}
aria-controls={isOpen ? id : undefined}
aria-label={t(
'design-system.date-picker.open-selector',
'Ouvrir le sélecteur de date'
@ -173,55 +172,70 @@ export default function DateField(props: DateFieldProps) {
</StyledButton>
</Wrapper>
{isPopperOpen && (
{isOpen && (
<FocusTrap
active
focusTrapOptions={{
allowOutsideClick: true,
clickOutsideDeactivates: true,
escapeDeactivates: true,
fallbackFocus: refs.reference.current ?? undefined,
}}
>
<StyledBody
as="div"
id={id}
tabIndex={-1}
style={popper.styles.popper}
className="dialog-sheet"
{...popper.attributes.popper}
ref={setPopperElement}
ref={refs.setFloating}
style={{
position: strategy,
top: y ?? 0,
left: x ?? 0,
width: 'max-content',
}}
role="dialog"
onKeyDown={(e) => {
if (e.key === 'Escape') {
closePopper()
close()
}
}}
aria-label="Calendrier de selection de date"
aria-label="Calendrier de sélection de date"
>
<div
ref={dayPickerRef}
css={`
text-align: center;
`}
>
<DayPicker
{...dayPickerProps}
captionLayout="dropdown-buttons"
mode="single"
defaultMonth={selected}
selected={selected}
onSelect={handleDaySelect}
/>
<Button
light
size="XXS"
onPress={closePopper}
aria-label="Fermer le calendrier de selection"
>
<Trans>Fermer</Trans> ×
</Button>
<Spacing sm />
</div>
<DayPicker
{...dayPickerProps}
captionLayout="dropdown-buttons"
mode="single"
defaultMonth={selected}
selected={selected}
onSelect={handleDaySelect}
labels={{
labelMonthDropdown: () =>
t('design-system.date-picker.month', 'Mois'),
labelYearDropdown: () =>
t('design-system.date-picker.year', 'Année'),
labelNext: () =>
t('design-system.date-picker.next-month', 'Mois suivant'),
labelPrevious: () =>
t('design-system.date-picker.prev-month', 'Mois précédent'),
}}
locale={language === 'fr' ? fr : enUS}
footer={
<div style={{ display: 'flex', justifyContent: 'center' }}>
<Button
light
type="button"
onPress={close}
size="XXS"
aria-label={t(
'design-system.date-picker.close-selector',
'Fermer le sélecteur de date'
)}
>
{t('design-system.date-picker.close', 'Fermer')}
</Button>
</div>
}
/>
</StyledBody>
</FocusTrap>
)}
@ -237,20 +251,23 @@ const StyledBody = styled(Body)`
? theme.colors.extended.grey[700]
: theme.colors.extended.grey[200]};
box-shadow: ${({ theme }) =>
theme.darkMode ? theme.elevationsDarkMode[2] : theme.elevations[2]};
theme.darkMode ? theme.elevationsDarkMode[3] : theme.elevations[3]};
`
const Wrapper = styled.div`
width: fit-content;
position: relative;
& input {
height: 3.5rem;
}
`
const StyledButton = styled(Button)`
position: absolute;
max-width: 55px;
right: 0;
top: 0;
margin: ${({ theme }) => theme.spacings.xs};
position: absolute;
margin: 0.7rem;
`
type OnlyAriaType<T> = {

View File

@ -5,6 +5,7 @@ import 'react-tooltip/dist/react-tooltip.css'
import styled from 'styled-components'
// TODO: Replace react-tooltip with @floating-ui/react-dom for more control (see DateField.tsx for example)
export const Tooltip = ({
children,
tooltip,

View File

@ -3998,6 +3998,13 @@ __metadata:
languageName: node
linkType: hard
"@floating-ui/core@npm:^1.2.6":
version: 1.2.6
resolution: "@floating-ui/core@npm:1.2.6"
checksum: e4aa96c435277f1720d4bc939e17a79b1e1eebd589c20b622d3c646a5273590ff889b8c6e126f7be61873cf8c4d7db7d418895986ea19b8b0d0530de32504c3a
languageName: node
linkType: hard
"@floating-ui/dom@npm:1.1.1":
version: 1.1.1
resolution: "@floating-ui/dom@npm:1.1.1"
@ -4007,6 +4014,27 @@ __metadata:
languageName: node
linkType: hard
"@floating-ui/dom@npm:^1.2.1":
version: 1.2.6
resolution: "@floating-ui/dom@npm:1.2.6"
dependencies:
"@floating-ui/core": ^1.2.6
checksum: 2226c6c244b96ae75ab14cc35bb119c8d7b83a85e2ff04e9d9800cffdb17faf4a7cf82db741dd045242ced56e31c8a08e33c8c512c972309a934d83b1f410441
languageName: node
linkType: hard
"@floating-ui/react-dom@npm:^1.3.0":
version: 1.3.0
resolution: "@floating-ui/react-dom@npm:1.3.0"
dependencies:
"@floating-ui/dom": ^1.2.1
peerDependencies:
react: ">=16.8.0"
react-dom: ">=16.8.0"
checksum: ce0ad3e3bbe43cfd15a6a0d5cccede02175c845862bfab52027995ab99c6b29630180dc7d146f76ebb34730f90a6ab9bf193c8984fe8d7f56062308e4ca98f77
languageName: node
linkType: hard
"@formatjs/ecma402-abstract@npm:1.14.3":
version: 1.14.3
resolution: "@formatjs/ecma402-abstract@npm:1.14.3"
@ -23920,13 +23948,6 @@ __metadata:
languageName: node
linkType: hard
"react-fast-compare@npm:^3.0.1":
version: 3.2.1
resolution: "react-fast-compare@npm:3.2.1"
checksum: 209b4dc3a9cc79c074a26ec020459efd8be279accaca612db2edb8ada2a28849ea51cf3d246fc0fafb344949b93a63a43798b6c1787559b0a128571883fe6859
languageName: node
linkType: hard
"react-helmet-async@npm:^1.3.0":
version: 1.3.0
resolution: "react-helmet-async@npm:1.3.0"
@ -24068,20 +24089,6 @@ __metadata:
languageName: node
linkType: hard
"react-popper@npm:^2.3.0":
version: 2.3.0
resolution: "react-popper@npm:2.3.0"
dependencies:
react-fast-compare: ^3.0.1
warning: ^4.0.2
peerDependencies:
"@popperjs/core": ^2.0.0
react: ^16.8.0 || ^17 || ^18
react-dom: ^16.8.0 || ^17 || ^18
checksum: 837111c98738011c69b3069a464ea5bdcbf487105b6148e8faf90cb7337e134edb1b98b8824322941c378756cca30a15c18c25f558e53b85ed5762fa0dc8e6b2
languageName: node
linkType: hard
"react-redux@npm:^8.0.5":
version: 8.0.5
resolution: "react-redux@npm:8.0.5"
@ -25590,6 +25597,7 @@ __metadata:
"@atomik-color/component": ^1.0.17
"@atomik-color/core": ^1.0.13
"@axe-core/react": ^4.5.2
"@floating-ui/react-dom": ^1.3.0
"@internationalized/number": ^3.2.0
"@leeoniya/ufuzzy": ^1.0.2
"@popperjs/core": ^2.11.7
@ -25670,7 +25678,6 @@ __metadata:
react-i18next: ^12.1.5
react-instantsearch: ^6.38.1
react-instantsearch-dom: ^6.38.1
react-popper: ^2.3.0
react-redux: ^8.0.5
react-router-dom: ^6.4.4
react-signature-pad-wrapper: ^3.3.1
@ -28269,15 +28276,6 @@ __metadata:
languageName: node
linkType: hard
"warning@npm:^4.0.2":
version: 4.0.3
resolution: "warning@npm:4.0.3"
dependencies:
loose-envify: ^1.0.0
checksum: 4f2cb6a9575e4faf71ddad9ad1ae7a00d0a75d24521c193fa464f30e6b04027bd97aa5d9546b0e13d3a150ab402eda216d59c1d0f2d6ca60124d96cd40dfa35c
languageName: node
linkType: hard
"watchpack@npm:^2.2.0":
version: 2.4.0
resolution: "watchpack@npm:2.4.0"