Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve accessibility for DatePicker #793

Merged
merged 27 commits into from
Aug 24, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
00f70b7
Remove unused functionality
heisand Aug 14, 2023
8c02a19
Test some more..
heisand Aug 14, 2023
f9c5c0d
Make the datepicker work the Entur way
heisand Aug 15, 2023
9a0be3d
Make DatePicker tabbable at least
heisand Aug 15, 2023
7883b32
Use darkGrey the correct place
heisand Aug 21, 2023
53dee9e
Add a better aria label to calendar
heisand Aug 21, 2023
cc015d7
Format files
heisand Aug 21, 2023
955da59
Close DatePicker with button when already open
heisand Aug 21, 2023
34d816d
Remove downlevelIteration
heisand Aug 21, 2023
5c49239
Set focus back to trigger button on Escape
heisand Aug 21, 2023
d6deb63
Re-add dialogProps and buttonProps
heisand Aug 21, 2023
306a437
Add visible focus to today's date
heisand Aug 21, 2023
dafb6ad
Use the correct language codes for all languages
heisand Aug 21, 2023
818f4a7
Refactor and simplify code a little bit
heisand Aug 22, 2023
ac86914
Add changeset
heisand Aug 22, 2023
6fc900e
Test popover trigger
heisand Aug 22, 2023
54ea538
Test more with popover
heisand Aug 22, 2023
3004cb7
Clean up
heisand Aug 22, 2023
9ad58cf
Make sure the trigger button has the ref
heisand Aug 22, 2023
12b64cf
Clean up more
heisand Aug 23, 2023
9a52857
Make sure both simple and trigger variant works
heisand Aug 23, 2023
c46770a
Make DateRangePicker work
heisand Aug 23, 2023
cd624a3
Be able to specify using Portal or not
heisand Aug 23, 2023
96c7947
Use the from calendarProps until further
heisand Aug 23, 2023
3cd8e06
Make sure calendar does not appear twice
heisand Aug 23, 2023
8e2cb90
Correct language code
heisand Aug 24, 2023
7f99fd8
Simplify code
heisand Aug 24, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/serious-pillows-occur.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@vygruppen/spor-react": patch
---

Improve accessibility for DatePicker
17 changes: 16 additions & 1 deletion packages/spor-react/src/datepicker/Calendar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,13 @@ import {
import { CalendarGrid } from "./CalendarGrid";
import { CalendarHeader } from "./CalendarHeader";
import { useCurrentLocale } from "./utils";
import { createTexts, useTranslation } from "../i18n";

type CalendarProps = ReactAriaCalendarProps<DateValue> & {
showYearNavigation?: boolean;
};
export function Calendar({ showYearNavigation, ...props }: CalendarProps) {
const { t } = useTranslation();
const locale = useCurrentLocale();
const state = useCalendarState({
...props,
Expand All @@ -22,11 +24,24 @@ export function Calendar({ showYearNavigation, ...props }: CalendarProps) {
});

const { calendarProps } = useCalendar(props, state);
const calendarAriaLabel = calendarProps["aria-label"];

const ariaLabel =
t(texts.calendar) + (calendarAriaLabel ? ` ${calendarAriaLabel}` : "");
Copy link
Contributor Author

@heisand heisand Aug 23, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add a slightly better aria-label than just, e.g, "august 2023". The result will be "Calendar august 2023". The August 2023 part comes from the React Aria calendarProps


return (
<Box {...calendarProps}>
<Box {...calendarProps} aria-label={ariaLabel}>
<CalendarHeader state={state} showYearNavigation={showYearNavigation} />
<CalendarGrid state={state} />
</Box>
);
}

const texts = createTexts({
calendar: {
nb: "Kalender",
nn: "Kalender",
sv: "Kalender",
en: "Calendar",
},
});
13 changes: 10 additions & 3 deletions packages/spor-react/src/datepicker/CalendarCell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,14 @@ type CalendarCellProps = {
};
export function CalendarCell({ state, date, currentMonth }: CalendarCellProps) {
const ref = useRef(null);
const { cellProps, buttonProps, isSelected, isDisabled, isUnavailable } =
useCalendarCell({ date }, state, ref);
const {
cellProps,
buttonProps,
isSelected,
isDisabled,
isUnavailable,
isOutsideVisibleRange,
} = useCalendarCell({ date }, state, ref);

const isOutsideMonth = !isSameMonth(currentMonth, date);
const styles = useMultiStyleConfig("Datepicker", {});
Expand All @@ -35,7 +41,7 @@ export function CalendarCell({ state, date, currentMonth }: CalendarCellProps) {
if (isOutsideMonth) {
stateProps["data-unavailable"] = true;
}

return (
<Box
as="td"
Expand Down Expand Up @@ -65,6 +71,7 @@ export function CalendarCell({ state, date, currentMonth }: CalendarCellProps) {
{...stateProps}
ref={ref}
sx={styles.dateCell}
hidden={isOutsideVisibleRange}
width="100%"
>
{date.day}
Expand Down
52 changes: 29 additions & 23 deletions packages/spor-react/src/datepicker/CalendarTriggerButton.tsx
Original file line number Diff line number Diff line change
@@ -1,30 +1,36 @@
import { Box, PopoverAnchor, useMultiStyleConfig } from "@chakra-ui/react";
import {
Box,
PopoverAnchor,
useMultiStyleConfig,
forwardRef,
As,
} from "@chakra-ui/react";
import { CalendarOutline24Icon } from "@vygruppen/spor-icon-react";
import React, { useRef } from "react";
import { AriaButtonProps, useButton } from "react-aria";
import React from "react";
import { AriaButtonProps } from "react-aria";
import { createTexts, useTranslation } from "..";

type CalendarTriggerButtonProps = AriaButtonProps<"button">;
export const CalendarTriggerButton = (props: CalendarTriggerButtonProps) => {
const { t } = useTranslation();
const styles = useMultiStyleConfig("Datepicker", {});
const ref = useRef(null);
const { buttonProps } = useButton(props, ref);
return (
<PopoverAnchor>
<Box
ref={ref}
as="button"
type="button"
aria-label={t(texts.openCalendar)}
sx={styles.calendarTriggerButton}
{...(buttonProps as any)}
>
<CalendarOutline24Icon />
</Box>
</PopoverAnchor>
);
};
export const CalendarTriggerButton = forwardRef<CalendarTriggerButtonProps, As>(
({ ...buttonProps }, ref) => {
const { t } = useTranslation();
const styles = useMultiStyleConfig("Datepicker", {});

return (
<PopoverAnchor>
<Box
ref={ref}
as="button"
aria-label={t(texts.openCalendar)}
sx={styles.calendarTriggerButton}
{...buttonProps}
>
<CalendarOutline24Icon />
</Box>
</PopoverAnchor>
);
}
);

const texts = createTexts({
openCalendar: {
Expand Down
78 changes: 37 additions & 41 deletions packages/spor-react/src/datepicker/DatePicker.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import {
Box,
BoxProps,
FocusLock,
InputGroup,
Popover,
PopoverAnchor,
PopoverArrow,
PopoverBody,
PopoverContent,
PopoverTrigger,
Portal,
ResponsiveValue,
useBreakpointValue,
Expand All @@ -17,7 +19,12 @@ import { DateValue } from "@internationalized/date";
import { useDatePickerState } from "@react-stately/datepicker";
import { CalendarOutline24Icon } from "@vygruppen/spor-icon-react";
import React, { forwardRef, useRef } from "react";
import { AriaDatePickerProps, I18nProvider, useDatePicker } from "react-aria";
import {
AriaDatePickerProps,
CalendarProps,
I18nProvider,
useDatePicker,
} from "react-aria";
import { FormErrorMessage } from "..";
import { Calendar } from "./Calendar";
import { CalendarTriggerButton } from "./CalendarTriggerButton";
Expand All @@ -30,6 +37,7 @@ type DatePickerProps = AriaDatePickerProps<DateValue> &
variant: ResponsiveValue<"simple" | "with-trigger">;
name?: string;
showYearNavigation?: boolean;
withPortal?: boolean;
};
/**
* A date picker component.
Expand All @@ -47,6 +55,7 @@ export const DatePicker = forwardRef<HTMLDivElement, DatePickerProps>(
errorMessage,
minHeight,
showYearNavigation,
withPortal = true,
width = "auto",
...props
},
Expand Down Expand Up @@ -76,33 +85,33 @@ export const DatePicker = forwardRef<HTMLDivElement, DatePickerProps>(
ref as React.MutableRefObject<HTMLDivElement>
);

const styles = useMultiStyleConfig("Datepicker", {});
const locale = useCurrentLocale();

const responsiveVariant =
useBreakpointValue(typeof variant === "string" ? [variant] : variant) ??
"simple";

const locale = useCurrentLocale();

const handleEnterClick = (e: React.KeyboardEvent) => {
if (
responsiveVariant === "simple" &&
e.key === "Enter" &&
!state.isOpen
) {
// Don't submit the form
e.stopPropagation();
state.setOpen(true);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I struggled finding that this code is really doing anything, other than creating confusion... I tried using Enter within the input field, but the calendar does not open...

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm that's unfortunate. Without this code, there isn't really any way a screen reader can trigger the calendar.

Not much of a difference since it didn't work anyways, but we should probably find a way to trigger the calendar for the simple variant, right?

Copy link
Contributor Author

@heisand heisand Aug 24, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The normal way to open stuff when navigating with a screen reader is in fact by using space, not enter - and that works in the simple variant when I focus at the label. But keyboard users will not be able to focus on this element.. I do not know what has been discussed about the simple variant, but maybe rather it should open automatically when receiving focus as there is no actual indication that the user should press either space or enter in the input field to open it.

But I will leave it as is until now and this can be discussed separately 👍

}
};
const hasTrigger = responsiveVariant === "with-trigger";

const onFieldClick = () => {
if (!hasTrigger) {
state.setOpen(true);
}
};

const hasTrigger = responsiveVariant === "with-trigger";

const styles = useMultiStyleConfig("Datepicker", {});
const popoverContent = (
<PopoverContent color="darkGrey" boxShadow="md" sx={styles.calendar}>
<PopoverArrow sx={styles.arrow} />
<PopoverBody>
<FocusLock>
<Calendar
{...calendarProps}
showYearNavigation={showYearNavigation}
/>
</FocusLock>
</PopoverBody>
</PopoverContent>
);

return (
<I18nProvider locale={locale}>
Expand All @@ -115,18 +124,14 @@ export const DatePicker = forwardRef<HTMLDivElement, DatePickerProps>(
<Popover
{...dialogProps}
isOpen={state.isOpen}
onClose={state.close}
onOpen={state.open}
closeOnBlur
closeOnEsc
returnFocusOnClose
onClose={state.close}
>
<InputGroup {...groupProps} display="inline-flex">
<PopoverAnchor>
<StyledField
variant={responsiveVariant}
onClick={onFieldClick}
onKeyPress={handleEnterClick}
paddingX={3}
minHeight={minHeight}
>
Expand All @@ -137,33 +142,24 @@ export const DatePicker = forwardRef<HTMLDivElement, DatePickerProps>(
label={props.label}
labelProps={labelProps}
name={props.name}
ref={ref}
ref={hasTrigger ? undefined : ref}
{...fieldProps}
/>
</StyledField>
</PopoverAnchor>
{hasTrigger && <CalendarTriggerButton {...buttonProps} />}
{hasTrigger && (
<PopoverTrigger>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🙌

<CalendarTriggerButton ref={ref} {...buttonProps} />
</PopoverTrigger>
)}
</InputGroup>
<FormErrorMessage {...errorMessageProps}>
{errorMessage}
</FormErrorMessage>
{state.isOpen && !props.isDisabled && (
<Portal>
<PopoverContent
color="darkGrey"
boxShadow="md"
sx={styles.calendar}
>
<PopoverArrow sx={styles.arrow} />
<PopoverBody>
<Calendar
{...calendarProps}
showYearNavigation={showYearNavigation}
/>
</PopoverBody>
</PopoverContent>
</Portal>
{state.isOpen && !props.isDisabled && withPortal && (
<Portal>{popoverContent}</Portal>
)}
{state.isOpen && !props.isDisabled && !withPortal && popoverContent}
</Popover>
</Box>
</I18nProvider>
Expand Down
Loading
Loading