Skip to content

Commit

Permalink
[datetime] fix(TimePicker): improve a11y markup (#5397)
Browse files Browse the repository at this point in the history
  • Loading branch information
bvandercar-vt authored Jul 7, 2022
1 parent de78c39 commit 7916db8
Show file tree
Hide file tree
Showing 2 changed files with 51 additions and 7 deletions.
12 changes: 12 additions & 0 deletions packages/datetime/src/common/timeUnit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,18 @@ export enum TimeUnit {
MS = "ms",
}

/** Gets a descriptive label representing the plural of the given time unit. */
export function getTimeUnitPrintStr(unit: TimeUnit) {
const timeUnitToPrintstr: { [k in TimeUnit]: string } = {
[TimeUnit.HOUR_24]: "hours (24hr clock)",
[TimeUnit.HOUR_12]: "hours (12hr clock)",
[TimeUnit.MINUTE]: "minutes",
[TimeUnit.SECOND]: "seconds",
[TimeUnit.MS]: "milliseconds",
};
return timeUnitToPrintstr[unit];
}

/** Returns the given time unit component of the date. */
export function getTimeUnit(unit: TimeUnit, date: Date) {
switch (unit) {
Expand Down
46 changes: 39 additions & 7 deletions packages/datetime/src/timePicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,16 @@
import classNames from "classnames";
import * as React from "react";

import { Classes as CoreClasses, DISPLAYNAME_PREFIX, HTMLSelect, Icon, Intent, Keys, Props } from "@blueprintjs/core";
import {
Classes as CoreClasses,
Utils as CoreUtils,
DISPLAYNAME_PREFIX,
HTMLSelect,
Icon,
Intent,
Keys,
Props,
} from "@blueprintjs/core";

import * as Classes from "./common/classes";
import * as DateUtils from "./common/dateUtils";
Expand All @@ -26,6 +35,8 @@ import {
getDefaultMinTime,
getTimeUnit,
getTimeUnitClassName,
getTimeUnitMax,
getTimeUnitPrintStr,
isTimeUnitValid,
setTimeUnit,
TimeUnit,
Expand Down Expand Up @@ -172,6 +183,14 @@ export class TimePicker extends React.Component<TimePickerProps, ITimePickerStat
this.state = this.getFullStateFromValue(this.getInitialValue(), props.useAmPm);
}

private timeInputIds: { [key in TimeUnit]: string } = {
[TimeUnit.HOUR_24]: CoreUtils.uniqueId(TimeUnit.HOUR_24 + "-input"),
[TimeUnit.HOUR_12]: CoreUtils.uniqueId(TimeUnit.HOUR_12 + "-input"),
[TimeUnit.MINUTE]: CoreUtils.uniqueId(TimeUnit.MINUTE + "-input"),
[TimeUnit.SECOND]: CoreUtils.uniqueId(TimeUnit.SECOND + "-input"),
[TimeUnit.MS]: CoreUtils.uniqueId(TimeUnit.MS + "-input"),
};

public render() {
const shouldRenderMilliseconds = this.props.precision === TimePrecision.MILLISECOND;
const shouldRenderSeconds = shouldRenderMilliseconds || this.props.precision === TimePrecision.SECOND;
Expand Down Expand Up @@ -241,13 +260,18 @@ export class TimePicker extends React.Component<TimePickerProps, ITimePickerStat
}
const classes = classNames(Classes.TIMEPICKER_ARROW_BUTTON, getTimeUnitClassName(timeUnit));
const onClick = () => (isDirectionUp ? this.incrementTime : this.decrementTime)(timeUnit);
const label = `${isDirectionUp ? "Increase" : "Decrease"} ${getTimeUnitPrintStr(timeUnit)}`;

// set tabIndex=-1 to ensure a valid FocusEvent relatedTarget when focused
return (
<span tabIndex={-1} className={classes} onClick={onClick}>
<Icon
icon={isDirectionUp ? "chevron-up" : "chevron-down"}
title={isDirectionUp ? "Increase" : "Decrease"}
/>
<span
aria-controls={this.timeInputIds[timeUnit]}
aria-label={label}
tabIndex={-1}
className={classes}
onClick={onClick}
>
<Icon icon={isDirectionUp ? "chevron-up" : "chevron-down"} title={label} />
</span>
);
}
Expand All @@ -257,21 +281,29 @@ export class TimePicker extends React.Component<TimePickerProps, ITimePickerStat
}

private renderInput(className: string, unit: TimeUnit, value: string) {
const isValid = isTimeUnitValid(unit, parseInt(value, 10));
const valueNumber = parseInt(value, 10);
const isValid = isTimeUnitValid(unit, valueNumber);
const isHour = unit === TimeUnit.HOUR_12 || unit === TimeUnit.HOUR_24;

return (
<input
// we use a type="text" input here, so we must set some a11y attributes
// which we would otherwise get for free with a type="number" input
aria-valuemin={0}
aria-valuenow={valueNumber}
aria-valuemax={getTimeUnitMax(unit)}
className={classNames(
Classes.TIMEPICKER_INPUT,
{ [CoreClasses.intentClass(Intent.DANGER)]: !isValid },
className,
)}
id={this.timeInputIds[unit]}
onBlur={this.getInputBlurHandler(unit)}
onChange={this.getInputChangeHandler(unit)}
onFocus={this.getInputFocusHandler(unit)}
onKeyDown={this.getInputKeyDownHandler(unit)}
onKeyUp={this.getInputKeyUpHandler(unit)}
role={this.props.showArrowButtons ? "spinbutton" : undefined}
value={value}
disabled={this.props.disabled}
autoFocus={isHour && this.props.autoFocus}
Expand Down

1 comment on commit 7916db8

@blueprint-bot
Copy link

Choose a reason for hiding this comment

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

[datetime] fix(TimePicker): improve a11y markup (#5397)

Previews: documentation | landing | table | demo

Please sign in to comment.