Skip to content

Commit

Permalink
fix(date-picker): improve date-picker a11y (#6715)
Browse files Browse the repository at this point in the history
**Related Issue:** #5570 

## Summary

This improves `calcite-date-picker` a11y by:

* moving `gridcell` role to `calcite-date-picker-month` component
rendering (day wrapper)
* adding `button` role to `calcite-date-picker-day`
* providing a locale-aware `aria-label` to `calcite-date-picker-day`
describing the associated day
  • Loading branch information
jcfranco authored Apr 5, 2023
1 parent 0561497 commit 74b3b96
Show file tree
Hide file tree
Showing 7 changed files with 117 additions and 56 deletions.
20 changes: 20 additions & 0 deletions src/components/date-picker-day/date-picker-day.e2e.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,37 @@
import { disabled } from "../../tests/commonTests";
import { newProgrammaticE2EPage } from "../../tests/utils";
import { DATE_PICKER_FORMAT_OPTIONS } from "../date-picker/resources";
import { Serializable } from "puppeteer";

describe("calcite-date-picker-day", () => {
it("can be disabled", async () => {
const page = await newProgrammaticE2EPage();
await page.evaluate(() => {
const dateEl = document.createElement("calcite-date-picker-day") as HTMLCalciteDatePickerDayElement;
dateEl.active = true;
dateEl.dateTimeFormat = new Intl.DateTimeFormat("en"); // options not needed as this is only needed for rendering
dateEl.day = 3;
document.body.append(dateEl);
});
await page.waitForChanges();

return disabled({ tag: "calcite-date-picker-day", page });
});

describe("accessibility", () => {
it("labels its associated day", async () => {
const page = await newProgrammaticE2EPage();
await page.evaluate((dateTimeFormatOptions: Intl.DateTimeFormatOptions) => {
const dateEl = document.createElement("calcite-date-picker-day") as HTMLCalciteDatePickerDayElement;
dateEl.dateTimeFormat = new Intl.DateTimeFormat("en", dateTimeFormatOptions);
dateEl.day = 20;
dateEl.value = new Date("2020-02-20T08:00:00.000Z");
document.body.append(dateEl);
}, DATE_PICKER_FORMAT_OPTIONS as Serializable);
await page.waitForChanges();
const day = await page.find(`calcite-date-picker-day`);

expect(day.getAttribute("aria-label")).toBe("Thursday, February 20, 2020");
});
});
});
7 changes: 1 addition & 6 deletions src/components/date-picker-day/date-picker-day.scss
Original file line number Diff line number Diff line change
@@ -1,10 +1,5 @@
:host {
@apply text-color-3
flex
min-w-0
cursor-pointer
justify-center;
inline-size: calc(100% / 7);
@apply text-color-3 flex cursor-pointer;
}

@include disabled();
Expand Down
21 changes: 19 additions & 2 deletions src/components/date-picker-day/date-picker-day.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {
} from "@stencil/core";
import { dateToISO } from "../../utils/date";

import { closestElementCrossShadowBoundary, getElementDir } from "../../utils/dom";
import { closestElementCrossShadowBoundary, getElementDir, toAriaBoolean } from "../../utils/dom";
import { InteractiveComponent, updateHostInteraction } from "../../utils/interactive";
import { isActivationKey } from "../../utils/key";
import { numberStringFormatter } from "../../utils/locale";
Expand Down Expand Up @@ -41,6 +41,13 @@ export class DatePickerDay implements InteractiveComponent {
/** Day of the month to be shown. */
@Prop() day!: number;

/**
* The DateTimeFormat used to provide screen reader labels.
*
* @internal
*/
@Prop() dateTimeFormat: Intl.DateTimeFormat;

/** When `true`, interaction is prevented and the component is displayed with lower opacity. */
@Prop({ reflect: true }) disabled = false;

Expand Down Expand Up @@ -140,8 +147,18 @@ export class DatePickerDay implements InteractiveComponent {
}
const formattedDay = numberStringFormatter.localize(String(this.day));
const dir = getElementDir(this.el);
const dayLabel = this.dateTimeFormat.format(this.value);

return (
<Host id={dayId} onClick={this.onClick} onKeyDown={this.keyDownHandler} role="gridcell">
<Host
aria-disabled={toAriaBoolean(this.disabled)}
aria-label={dayLabel}
aria-selected={toAriaBoolean(this.active)}
id={dayId}
onClick={this.onClick}
onKeyDown={this.keyDownHandler}
role="button"
>
<div class={{ "day-v-wrapper": true, [CSS_UTILITY.rtl]: dir === "rtl" }}>
<div class="day-wrapper">
<span class="day">
Expand Down
7 changes: 7 additions & 0 deletions src/components/date-picker-month/date-picker-month.scss
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,13 @@
inline-size: calc(100% / 7);
}

.day {
@apply flex
min-w-0
justify-center;
inline-size: calc(100% / 7);
}

:host([scale="s"]) .week-header {
@apply text-n2h px-0 pt-2 pb-3;
}
Expand Down
70 changes: 40 additions & 30 deletions src/components/date-picker-month/date-picker-month.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,13 @@ export class DatePickerMonth {
//
//--------------------------------------------------------------------------

/**
* The DateTimeFormat used to provide screen reader labels.
*
* @internal
*/
@Prop() dateTimeFormat: Intl.DateTimeFormat;

/** Already selected date.*/
@Prop() selectedDate: Date;

Expand Down Expand Up @@ -418,36 +425,39 @@ export class DatePickerMonth {
(!this.endDate && this.hoverRange && sameDate(this.hoverRange?.end, this.startDate));

return (
<calcite-date-picker-day
active={active}
class={{
"hover--inside-range": this.startDate && isHoverInRange,
"hover--outside-range": this.startDate && !isHoverInRange,
"focused--start": isFocusedOnStart,
"focused--end": !isFocusedOnStart
}}
currentMonth={currentMonth}
day={day}
disabled={!inRange(date, this.min, this.max)}
endOfRange={this.isEndOfRange(date)}
highlighted={this.betweenSelectedRange(date)}
key={date.toDateString()}
onCalciteDaySelect={this.daySelect}
onCalciteInternalDayHover={this.dayHover}
range={!!this.startDate && !!this.endDate && !sameDate(this.startDate, this.endDate)}
rangeHover={this.isRangeHover(date)}
scale={this.scale}
selected={this.isSelected(date)}
startOfRange={this.isStartOfRange(date)}
value={date}
// eslint-disable-next-line react/jsx-sort-props
ref={(el: HTMLCalciteDatePickerDayElement) => {
// when moving via keyboard, focus must be updated on active date
if (ref && active && this.activeFocus) {
el?.focus();
}
}}
/>
<div class="day" role="gridcell">
<calcite-date-picker-day
active={active}
class={{
"hover--inside-range": this.startDate && isHoverInRange,
"hover--outside-range": this.startDate && !isHoverInRange,
"focused--start": isFocusedOnStart,
"focused--end": !isFocusedOnStart
}}
currentMonth={currentMonth}
dateTimeFormat={this.dateTimeFormat}
day={day}
disabled={!inRange(date, this.min, this.max)}
endOfRange={this.isEndOfRange(date)}
highlighted={this.betweenSelectedRange(date)}
key={date.toDateString()}
onCalciteDaySelect={this.daySelect}
onCalciteInternalDayHover={this.dayHover}
range={!!this.startDate && !!this.endDate && !sameDate(this.startDate, this.endDate)}
rangeHover={this.isRangeHover(date)}
scale={this.scale}
selected={this.isSelected(date)}
startOfRange={this.isStartOfRange(date)}
value={date}
// eslint-disable-next-line react/jsx-sort-props
ref={(el: HTMLCalciteDatePickerDayElement) => {
// when moving via keyboard, focus must be updated on active date
if (ref && active && this.activeFocus) {
el?.focus();
}
}}
/>
</div>
);
}

Expand Down
46 changes: 28 additions & 18 deletions src/components/date-picker/date-picker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import {
import {
connectLocalized,
disconnectLocalized,
getDateTimeFormat,
LocalizedComponent,
NumberingSystem,
numberStringFormatter
Expand All @@ -42,7 +43,7 @@ import {
} from "../../utils/t9n";
import { HeadingLevel } from "../functional/Heading";
import { DatePickerMessages } from "./assets/date-picker/t9n";
import { HEADING_LEVEL } from "./resources";
import { DATE_PICKER_FORMAT_OPTIONS, HEADING_LEVEL } from "./resources";
import { DateLocaleData, getLocaleData, getValueAsDateRange } from "./utils";

@Component({
Expand Down Expand Up @@ -183,20 +184,6 @@ export class DatePicker implements LocalizedComponent, LoadableComponent, T9nCom
*/
@Event({ cancelable: false }) calciteDatePickerRangeChange: EventEmitter<void>;

/**
* Active start date.
*/
@State() activeStartDate: Date;

/**
* Active end date.
*/
@State() activeEndDate: Date;

@State() startAsDate: Date;

@State() endAsDate: Date;

//--------------------------------------------------------------------------
//
// Public Methods
Expand Down Expand Up @@ -300,21 +287,42 @@ export class DatePicker implements LocalizedComponent, LoadableComponent, T9nCom
//
//--------------------------------------------------------------------------

/**
* Active end date.
*/
@State() activeEndDate: Date;

/**
* Active start date.
*/
@State() activeStartDate: Date;

/**
* The DateTimeFormat used to provide screen reader labels.
*
* @internal
*/
@State() dateTimeFormat: Intl.DateTimeFormat;

@State() defaultMessages: DatePickerMessages;

@State() effectiveLocale = "";

@Watch("effectiveLocale")
effectiveLocaleChange(): void {
updateMessages(this, this.effectiveLocale);
}

@State() defaultMessages: DatePickerMessages;

@State() private localeData: DateLocaleData;
@State() endAsDate: Date;

@State() private hoverRange: HoverRange;

@State() private localeData: DateLocaleData;

private mostRecentRangeValue?: Date;

@State() startAsDate: Date;

//--------------------------------------------------------------------------
//
// Private Methods
Expand Down Expand Up @@ -349,6 +357,7 @@ export class DatePicker implements LocalizedComponent, LoadableComponent, T9nCom
};

this.localeData = await getLocaleData(this.effectiveLocale);
this.dateTimeFormat = getDateTimeFormat(this.effectiveLocale, DATE_PICKER_FORMAT_OPTIONS);
}

monthHeaderSelectChange = (event: CustomEvent<Date>): void => {
Expand Down Expand Up @@ -482,6 +491,7 @@ export class DatePicker implements LocalizedComponent, LoadableComponent, T9nCom
/>,
<calcite-date-picker-month
activeDate={activeDate}
dateTimeFormat={this.dateTimeFormat}
endDate={this.range ? endDate : undefined}
hoverRange={this.hoverRange}
localeData={this.localeData}
Expand Down
2 changes: 2 additions & 0 deletions src/components/date-picker/resources.ts
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
export const HEADING_LEVEL = 2;

export const DATE_PICKER_FORMAT_OPTIONS: Intl.DateTimeFormatOptions = { dateStyle: "full" };

0 comments on commit 74b3b96

Please sign in to comment.