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

fix(a11y): date range picker voiceover fix #105

Merged
merged 6 commits into from
Oct 21, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 3 additions & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,9 @@ jobs:
with:
timezone: Asia/Kolkata

- name: install deps
- name: Install deps
run: yarn
- name: Check Types
run: yarn check-types
- name: Test
run: yarn test
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
"build:cjs": "cross-env BABEL_ENV=cjs babel src --extensions .ts,.tsx --config-file ./babel-config.js -d dist/cjs --source-maps",
"build:esm": "cross-env BABEL_ENV=esm babel src --extensions .ts,.tsx --config-file ./babel-config.js -d dist/esm --source-maps",
"build:types": "tsc --emitDeclarationOnly",
"check-types": "tsc --noEmit",
"commit": "gacp",
"format": "prettier --write \"./**/*.{js,ts,css,less,json,md,html,yml,yaml,pcss,jsx,tsx}\"",
"keys": "node scripts/build/keys",
Expand Down
3 changes: 2 additions & 1 deletion src/calendar/CalendarCellButton.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export type CalendarCellButtonOptions = ButtonOptions &
| "maxDate"
| "dateValue"
| "isFocused"
| "isRangeCalendar"
> &
Partial<Pick<RangeCalendarStateReturn, "anchorDate">> & {
date: Date;
Expand Down Expand Up @@ -132,7 +133,7 @@ export const useCalendarCellButton = createHook<

// When a cell is focused and this is a range calendar, add a prompt to help
// screenreader users know that they are in a range selection mode.
if (anchorDate && isFocused && !isDisabled) {
if (options.isRangeCalendar && isFocused && !isDisabled) {
let rangeSelectionPrompt = "";

// If selection has started add "click to finish selecting range"
Expand Down
12 changes: 7 additions & 5 deletions src/calendar/CalendarState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,9 +111,15 @@ export function useCalendarState(props: CalendarInitialState = {}) {
setFocusedDate(date);
}

const announceSelectedDate = React.useCallback((value: Date) => {
if (!value) return;
announce(`Selected Date: ${format(value, "do MMM yyyy")}`);
}, []);

function setValue(value: Date) {
if (!isDisabled && !isReadOnly) {
setControllableValue(value);
announceSelectedDate(value);
}
}

Expand All @@ -129,11 +135,6 @@ export function useCalendarState(props: CalendarInitialState = {}) {
// rather than move focus, we announce the new month value
}, [currentMonth]);

useUpdateEffect(() => {
if (!value) return;
announce(`Selected Date: ${format(value, "do MMM yyyy")}`);
}, [value]);

return {
calendarId,
dateValue: value,
Expand Down Expand Up @@ -190,6 +191,7 @@ export function useCalendarState(props: CalendarInitialState = {}) {
selectDate(date: Date) {
setValue(date);
},
isRangeCalendar: false,
};
}

Expand Down
32 changes: 17 additions & 15 deletions src/calendar/RangeCalendarState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,21 +63,7 @@ export function useRangeCalendarState(props: RangeCalendarInitialState = {}) {
? makeRange(anchorDate, calendar.focusedDate)
: value && dateRange && makeRange(dateRange.start, dateRange.end);

const selectDate = (date: Date) => {
if (props.isReadOnly) {
return;
}

if (!anchorDate) {
setAnchorDate(date);
} else {
setValue(makeRange(anchorDate, date));
setAnchorDate(null);
}
};

useUpdateEffect(() => {
if (anchorDate) return;
const announceRange = React.useCallback(() => {
if (!highlightedRange) return;
if (isSameDay(highlightedRange.start, highlightedRange.end)) {
announce(
Expand All @@ -96,6 +82,21 @@ export function useRangeCalendarState(props: RangeCalendarInitialState = {}) {
}
}, [highlightedRange]);

const selectDate = (date: Date) => {
if (props.isReadOnly) {
return;
}

if (!anchorDate) {
setAnchorDate(date);
announce(`Starting range from ${format(date, "do MMM yyyy")}`);
} else {
setValue(makeRange(anchorDate, date));
announceRange();
setAnchorDate(null);
}
};

return {
...calendar,
dateRangeValue: dateRange,
Expand All @@ -112,6 +113,7 @@ export function useRangeCalendarState(props: RangeCalendarInitialState = {}) {
calendar.setFocusedDate(date);
}
},
isRangeCalendar: true,
};
}

Expand Down
1 change: 1 addition & 0 deletions src/calendar/__keys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ const CALENDAR_STATE_KEYS = [
"focusPreviousYear",
"selectFocusedDate",
"selectDate",
"isRangeCalendar",
] as const;
const RANGE_CALENDAR_STATE_KEYS = [
...CALENDAR_STATE_KEYS,
Expand Down
19 changes: 15 additions & 4 deletions src/calendar/__tests__/RangeCalendar.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,11 @@ describe("RangeCalendar", () => {
press.Tab();
press.Tab();

expect(label("Wednesday, October 7, 2020 selected")).toHaveFocus();
expect(
label(
"Wednesday, October 7, 2020 selected (click to start selecting range)",
),
).toHaveFocus();
press.ArrowDown(); // go to down just for some variety

press.Enter(); // start the selection, currently the start and end should be the same date
Expand All @@ -145,9 +149,12 @@ describe("RangeCalendar", () => {
// check if the selection is actually finished or not
press.ArrowRight();
press.ArrowRight();
expect(label("Wednesday, November 4, 2020")).toHaveFocus();
expect(
label("Wednesday, November 4, 2020")?.parentElement,
label("Wednesday, November 4, 2020 (click to start selecting range)"),
).toHaveFocus();
expect(
label("Wednesday, November 4, 2020 (click to start selecting range)")
?.parentElement,
).not.toHaveAttribute("data-is-range-selection");

// Verify selection ranges
Expand All @@ -174,7 +181,11 @@ describe("RangeCalendar", () => {
press.Tab();
press.Tab();

expect(label("Monday, October 7, 2019 selected")).toHaveFocus();
expect(
label(
"Monday, October 7, 2019 selected (click to start selecting range)",
),
).toHaveFocus();
press.ArrowDown();
press.Enter(); // start the selection

Expand Down
5 changes: 1 addition & 4 deletions src/calendar/stories/RangeCalendar.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -86,10 +86,7 @@ const RangeCalendarComp: React.FC<RangeCalendarInitialState> = props => {
export const Default = () => <RangeCalendarComp />;
export const DefaultValue = () => (
<RangeCalendarComp
defaultValue={{
start: format(new Date(), "yyyy-MM-dd"),
end: format(addDays(new Date(), 4), "yyyy-MM-dd"),
}}
defaultValue={{ start: "2019-10-07", end: "2019-10-30" }}
/>
);

Expand Down
19 changes: 14 additions & 5 deletions src/datepicker/DatePickerSegment.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
import { DatePickerStateReturn } from ".";
import { unstable_useId as useId } from "reakit";
import { createComponent, createHook } from "reakit-system";

import { DATE_PICKER_SEGMENT_KEYS } from "./__keys";
import { DateRangePickerStateReturn } from "./DateRangePickerState";
import { useSegment, SegmentOptions, SegmentHTMLProps } from "../segment";

export type DatePickerSegmentOptions =
| SegmentOptions
| Partial<DatePickerStateReturn>
| Partial<DateRangePickerStateReturn>;
export type DatePickerSegmentOptions = SegmentOptions &
Partial<Pick<DatePickerStateReturn, "pickerId" | "isDateRangePicker">>;

export type DatePickerSegmentHTMLProps = SegmentHTMLProps;

Expand All @@ -22,6 +20,17 @@ export const useDatePickerSegment = createHook<
name: "DatePickerSegment",
compose: useSegment,
keys: DATE_PICKER_SEGMENT_KEYS,

useProps(options, htmlProps) {
const { id } = useId({ baseId: "datepicker-segment" });
return {
id,
...(options.isDateRangePicker
? { "aria-labelledby": `${options.pickerId} ${options.baseId} ${id}` }
: { "aria-labelledby": id }),
...htmlProps,
};
},
});

export const DatePickerSegment = createComponent({
Expand Down
1 change: 1 addition & 0 deletions src/datepicker/DatePickerState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ export const useDatePickerState = (props: DatePickerInitialState = {}) => {
...popover,
...segmentState,
calendar,
isDateRangePicker: false,
};
};

Expand Down
11 changes: 7 additions & 4 deletions src/datepicker/DateRangePickerState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,9 @@ import {
FocusableProps,
ValidationState,
} from "@react-types/shared";
import { v4 } from "uuid";
import * as React from "react";
import { useCompositeState } from "reakit";
import { useControllableState } from "@chakra-ui/hooks";
import { useCompositeState, unstable_useId as useId } from "reakit";

import {
parseDate,
Expand Down Expand Up @@ -159,6 +158,9 @@ export const useDateRangePickerState = (
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [autoFocus, segmentComposite.first]);

const { id: startId } = useId({ baseId: "startsegment" });
const { id: endId } = useId({ baseId: "endsegment" });

return {
dateValue: value,
setDateValue: setValue,
Expand All @@ -171,14 +173,15 @@ export const useDateRangePickerState = (
startSegmentState: {
...startSegmentState,
...segmentComposite,
baseId: v4(),
baseId: startId,
},
endSegmentState: {
...endSegmentState,
...segmentComposite,
baseId: v4(),
baseId: endId,
},
calendar,
isDateRangePicker: true,
};
};

Expand Down
2 changes: 2 additions & 0 deletions src/datepicker/__keys.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Automatically generated
const DATE_PICKER_STATE_KEYS = [
"calendar",
"isDateRangePicker",
"fieldValue",
"setFieldValue",
"segments",
Expand Down Expand Up @@ -83,6 +84,7 @@ const DATE_RANGE_PICKER_STATE_KEYS = [
"startSegmentState",
"endSegmentState",
"calendar",
"isDateRangePicker",
"baseId",
"unstable_idCountRef",
"visible",
Expand Down
12 changes: 8 additions & 4 deletions src/datepicker/__tests__/DateRangePicker.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -146,12 +146,16 @@ describe("DateRangePicker", () => {

openDatePicker(text, testId);

expect(label("Sunday, November 15, 2020 selected")).toHaveFocus();
expect(
label(
"Sunday, November 15, 2020 selected (click to start selecting range)",
),
).toHaveFocus();

// check if current date is selected
isEndSelection(label, "Sunday, November 15, 2020 selected");
isStartSelection(label, "Sunday, November 15, 2020 selected");
isInSelectionRange(label, "Sunday, November 15, 2020 selected");
isEndSelection(label, /Sunday, November 15, 2020 selected/i);
isStartSelection(label, /Sunday, November 15, 2020 selected/i);
isInSelectionRange(label, /Sunday, November 15, 2020 selected/i);

// change date selection
press.Enter();
Expand Down
8 changes: 5 additions & 3 deletions src/datepicker/stories/DateRangePicker.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -98,34 +98,36 @@ const DateRangePickerComp: React.FC<DateRangePickerInitialState> = props => {

return (
<>
<DatePicker className="datepicker" {...state}>
<DatePicker aria-label="Date Range" className="datepicker" {...state}>
<div className="datepicker__header">
<DatePickerSegmentField
{...state.startSegmentState}
className="datepicker__field"
aria-label="start date"
>
{state.startSegmentState.segments.map((segment, i) => (
<DatePickerSegment
key={i}
segment={segment}
className="datepicker__field--item"
{...state.startSegmentState}
{...state}
{...state.startSegmentState}
/>
))}
</DatePickerSegmentField>
&nbsp;-&nbsp;
<DatePickerSegmentField
{...state.endSegmentState}
className="datepicker__field"
aria-label="end date"
>
{state.endSegmentState.segments.map((segment, i) => (
<DatePickerSegment
key={i}
segment={segment}
className="datepicker__field--item"
{...state.endSegmentState}
{...state}
{...state.endSegmentState}
/>
))}
</DatePickerSegmentField>
Expand Down
6 changes: 3 additions & 3 deletions src/meter/__keys.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
// Automatically generated
const METER_STATE_KEYS = [
"value",
"low",
"high",
"optimum",
"min",
"max",
"low",
"optimum",
"high",
"status",
"percent",
] as const;
Expand Down
2 changes: 1 addition & 1 deletion src/segment/Segment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -217,7 +217,7 @@ export const useSegment = createHook<SegmentOptions, SegmentHTMLProps>({
return mergeProps(spinButtonProps, {
id,
"aria-label": segment.type,
"aria-labelledby": `${options["aria-labelledby"]} ${id}`,
"aria-labelledby": `${id}`,
tabIndex: options.isDisabled ? undefined : 0,
onKeyDown: callAllHandlers(htmlOnKeyDown, onKeyDown),
onFocus: callAllHandlers(htmlOnFocus, onFocus),
Expand Down