Replace popper by floating-ui; add aria label
parent
208f982691
commit
7aca423931
|
@ -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",
|
||||
|
|
|
@ -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> = {
|
||||
|
|
|
@ -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,
|
||||
|
|
60
yarn.lock
60
yarn.lock
|
@ -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"
|
||||
|
|
Loading…
Reference in New Issue