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

[core] fix(Slider, MultiSlider): improve a11y markup #5438

Merged
merged 9 commits into from
Jul 20, 2022
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
10 changes: 8 additions & 2 deletions packages/core/src/components/slider/handle.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -67,19 +67,25 @@ export class Handle extends AbstractPureComponent2<IInternalHandleProps, IHandle
}

public render() {
const { className, disabled, label } = this.props;
const { className, disabled, label, min, max, value, vertical, htmlProps } = this.props;
const { isMoving } = this.state;

return (
<span
role="slider"
tabIndex={0}
{...htmlProps}
className={classNames(Classes.SLIDER_HANDLE, { [Classes.ACTIVE]: isMoving }, className)}
onKeyDown={disabled ? undefined : this.handleKeyDown}
onKeyUp={disabled ? undefined : this.handleKeyUp}
onMouseDown={disabled ? undefined : this.beginHandleMovement}
onTouchStart={disabled ? undefined : this.beginHandleTouchMovement}
ref={this.refHandlers.handle}
style={this.getStyleProperties()}
tabIndex={0}
aria-valuemin={min}
aria-valuemax={max}
aria-valuenow={value}
bvandercar-vt marked this conversation as resolved.
Show resolved Hide resolved
aria-orientation={vertical ? "vertical" : "horizontal"}
>
{label == null ? null : <span className={Classes.SLIDER_LABEL}>{label}</span>}
</span>
Expand Down
7 changes: 7 additions & 0 deletions packages/core/src/components/slider/handleProps.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ export const HandleInteractionKind = {
// eslint-disable-next-line @typescript-eslint/no-redeclare
export type HandleInteractionKind = typeof HandleInteractionKind[keyof typeof HandleInteractionKind];

export type HandleHtmlProps = Pick<React.HTMLProps<HTMLSpanElement>, "aria-label" | "aria-labelledby">;
bvandercar-vt marked this conversation as resolved.
Show resolved Hide resolved

// eslint-disable-next-line deprecation/deprecation
export type HandleProps = IHandleProps;
/** @deprecated use HandleProps */
Expand Down Expand Up @@ -89,4 +91,9 @@ export interface IHandleProps extends Props {
* @default "full"
*/
type?: HandleType;

/**
* A limited subset of HTML props to apply to the rendered `<span>` element.
*/
htmlProps?: HandleHtmlProps;
}
3 changes: 2 additions & 1 deletion packages/core/src/components/slider/multiSlider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -310,8 +310,9 @@ export class MultiSlider extends AbstractPureComponent2<MultiSliderProps, ISlide
return null;
}

return handleProps.map(({ value, type, className }, index) => (
return handleProps.map(({ value, type, className, htmlProps }, index) => (
<Handle
htmlProps={htmlProps}
className={classNames(
{
[Classes.START]: type === HandleType.START,
Expand Down
15 changes: 12 additions & 3 deletions packages/core/src/components/slider/rangeSlider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import * as React from "react";
import { AbstractPureComponent2, Intent } from "../../common";
import * as Errors from "../../common/errors";
import { DISPLAYNAME_PREFIX } from "../../common/props";
import { HandleHtmlProps } from "./handleProps";
import { ISliderBaseProps, MultiSlider } from "./multiSlider";

export type NumberRange = [number, number];
Expand All @@ -44,6 +45,9 @@ export interface IRangeSliderProps extends ISliderBaseProps {

/** Callback invoked when a handle is released. */
onRelease?(value: NumberRange): void;

/** HTML props to apply to the slider Handles */
handleHtmlProps?: { start?: HandleHtmlProps; end?: HandleHtmlProps };
}

export class RangeSlider extends AbstractPureComponent2<RangeSliderProps> {
Expand All @@ -56,11 +60,16 @@ export class RangeSlider extends AbstractPureComponent2<RangeSliderProps> {
public static displayName = `${DISPLAYNAME_PREFIX}.RangeSlider`;

public render() {
const { value, ...props } = this.props;
const { value, handleHtmlProps, ...props } = this.props;
return (
<MultiSlider {...props}>
<MultiSlider.Handle value={value![RangeIndex.START]} type="start" intentAfter={props.intent} />
<MultiSlider.Handle value={value![RangeIndex.END]} type="end" />
<MultiSlider.Handle
value={value![RangeIndex.START]}
type="start"
intentAfter={props.intent}
htmlProps={handleHtmlProps?.start}
/>
<MultiSlider.Handle value={value![RangeIndex.END]} type="end" htmlProps={handleHtmlProps?.end} />
</MultiSlider>
);
}
Expand Down
7 changes: 6 additions & 1 deletion packages/core/src/components/slider/slider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import * as React from "react";

import { AbstractPureComponent2, Intent } from "../../common";
import { DISPLAYNAME_PREFIX } from "../../common/props";
import { HandleHtmlProps } from "./handleProps";
import { ISliderBaseProps, MultiSlider } from "./multiSlider";

// eslint-disable-next-line deprecation/deprecation
Expand All @@ -44,6 +45,9 @@ export interface ISliderProps extends ISliderBaseProps {

/** Callback invoked when the handle is released. */
onRelease?(value: number): void;

/** A limited subset of HTML props to apply to the slider Handle */
handleHtmlProps?: HandleHtmlProps;
}

export class Slider extends AbstractPureComponent2<SliderProps> {
Expand All @@ -57,7 +61,7 @@ export class Slider extends AbstractPureComponent2<SliderProps> {
public static displayName = `${DISPLAYNAME_PREFIX}.Slider`;

public render() {
const { initialValue, intent, value, onChange, onRelease, ...props } = this.props;
const { initialValue, intent, value, onChange, onRelease, handleHtmlProps, ...props } = this.props;
return (
<MultiSlider {...props}>
<MultiSlider.Handle
Expand All @@ -66,6 +70,7 @@ export class Slider extends AbstractPureComponent2<SliderProps> {
intentBefore={value! >= initialValue! ? intent : undefined}
onChange={onChange}
onRelease={onRelease}
htmlProps={handleHtmlProps}
/>
<MultiSlider.Handle value={initialValue!} interactionKind="none" />
</MultiSlider>
Expand Down
13 changes: 11 additions & 2 deletions packages/core/src/components/slider/sliders.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,17 @@ elements, each with their own `value` and other properties.
```tsx
// RangeSlider looks roughly like this:
<MultiSlider onChange={...}>
<MultiSlider.Handle value={startValue} type="start" intentAfter={Intent.PRIMARY} />
<MultiSlider.Handle value={endValue} type="end" />
<MultiSlider.Handle
value={startValue}
type="start"
intentAfter={Intent.PRIMARY}
htmlProps={handleHtmlProps.start}
/>
<MultiSlider.Handle
value={endValue}
type="end"
htmlProps={handleHtmlProps.end}
/>
</MultiSlider>
```

Expand Down
11 changes: 10 additions & 1 deletion packages/demo-app/src/examples/SliderExample.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,17 @@ export class SliderExample extends React.PureComponent<Record<string, unknown>,
labelStepSize={10}
onChange={this.getChangeHandler("value")}
value={this.state.value}
handleHtmlProps={{ "aria-label": "example 1" }}
/>
<Slider
disabled={true}
min={0}
max={10}
stepSize={0.1}
labelStepSize={10}
value={5}
handleHtmlProps={{ "aria-label": "example 2" }}
/>
<Slider disabled={true} min={0} max={10} stepSize={0.1} labelStepSize={10} value={5} />
</ExampleCard>
</div>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,8 @@ export class BreadcrumbsExample extends React.PureComponent<IExampleProps, IBrea
this.setState({ collapseFrom: collapseFrom as Boundary }),
);

private breadcrumbWidthLabelId = "num-visible-items-label";

public render() {
const options = (
<>
Expand All @@ -95,14 +97,15 @@ export class BreadcrumbsExample extends React.PureComponent<IExampleProps, IBrea
checked={this.state.renderCurrentAsInput}
/>
<H5>Example</H5>
<Label>Width</Label>
<Label id={this.breadcrumbWidthLabelId}>Width</Label>
<Slider
labelRenderer={this.renderLabel}
labelStepSize={50}
max={100}
onChange={this.handleChangeWidth}
showTrackFill={false}
value={this.state.width}
handleHtmlProps={{ "aria-labelledby": this.breadcrumbWidthLabelId }}
/>
</>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ export class CardExample extends React.PureComponent<IExampleProps, ICardExample
showTrackFill={false}
value={this.state.elevation}
onChange={this.handleElevationChange}
handleHtmlProps={{ "aria-label": "card elevation" }}
/>
</Label>
</>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,16 +48,19 @@ export class CollapsibleListExample extends React.PureComponent<IExampleProps, I

private handleChangeCollapse = handleValueChange((collapseFrom: Boundary) => this.setState({ collapseFrom }));

private numVisibleItemsLabelId = "num-visible-items-label";

public render() {
const options = (
<>
<H5>Props</H5>
<Label>Visible items</Label>
<Label id={this.numVisibleItemsLabelId}>Visible items</Label>
<Slider
max={6}
onChange={this.handleChangeCount}
showTrackFill={false}
value={this.state.visibleItemCount}
handleHtmlProps={{ "aria-labelledby": this.numVisibleItemsLabelId }}
/>
<RadioGroup
name="collapseFrom"
Expand Down
5 changes: 4 additions & 1 deletion packages/docs-app/src/examples/core-examples/iconExample.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ export class IconExample extends React.PureComponent<IExampleProps, IIconExample

private handleIconNameChange = (icon: IconName) => this.setState({ icon });

private iconSizeLabelId = "icon-size-label";

public render() {
const { icon, iconSize, intent } = this.state;

Expand All @@ -50,14 +52,15 @@ export class IconExample extends React.PureComponent<IExampleProps, IIconExample
<H5>Props</H5>
<IconSelect iconName={icon} onChange={this.handleIconNameChange} />
<IntentSelect intent={this.state.intent} onChange={this.handleIntentChange} />
<Label>Icon size</Label>
<Label id={this.iconSizeLabelId}>Icon size</Label>
<Slider
labelStepSize={MAX_ICON_SIZE / 5}
min={0}
max={MAX_ICON_SIZE}
showTrackFill={false}
value={iconSize}
onChange={this.handleIconSizeChange}
handleHtmlProps={{ "aria-labelledby": this.iconSizeLabelId }}
/>
</>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ export class MultiSliderExample extends React.PureComponent<IExampleProps, IMult
value={values.dangerStart}
intentBefore="danger"
interactionKind={interactionKind}
htmlProps={{ "aria-label": "danger start" }}
/>
)}
{showWarning && (
Expand All @@ -95,6 +96,7 @@ export class MultiSliderExample extends React.PureComponent<IExampleProps, IMult
value={values.warningStart}
intentBefore="warning"
interactionKind={interactionKind}
htmlProps={{ "aria-label": "warning start" }}
/>
)}
{showWarning && (
Expand All @@ -103,6 +105,7 @@ export class MultiSliderExample extends React.PureComponent<IExampleProps, IMult
value={values.warningEnd}
intentAfter="warning"
interactionKind={interactionKind}
htmlProps={{ "aria-label": "warning end" }}
/>
)}
{showDanger && (
Expand All @@ -111,6 +114,7 @@ export class MultiSliderExample extends React.PureComponent<IExampleProps, IMult
value={values.dangerEnd}
intentAfter="danger"
interactionKind={interactionKind}
htmlProps={{ "aria-label": "danger end" }}
/>
)}
</MultiSlider>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ export class ProgressExample extends React.PureComponent<IExampleProps, IProgres
stepSize={0.1}
showTrackFill={false}
value={value}
handleHtmlProps={{ "aria-label": "progressbar value" }}
/>
</>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ export class RangeSliderExample extends React.PureComponent<IExampleProps, IRang
onChange={this.handleValueChange}
value={range}
vertical={vertical}
handleHtmlProps={{ start: { "aria-label": "example start" }, end: { "aria-label": "example end" } }}
/>
</Example>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ export class SliderExample extends React.PureComponent<IExampleProps, ISliderExa
onChange={this.getChangeHandler("value2")}
value={this.state.value2}
vertical={vertical}
handleHtmlProps={{ "aria-label": "example 1" }}
/>
<Slider
min={0}
Expand All @@ -65,6 +66,7 @@ export class SliderExample extends React.PureComponent<IExampleProps, ISliderExa
labelRenderer={this.renderLabel2}
value={this.state.value1}
vertical={vertical}
handleHtmlProps={{ "aria-label": "example 2" }}
/>
<Slider
min={-12}
Expand All @@ -76,6 +78,7 @@ export class SliderExample extends React.PureComponent<IExampleProps, ISliderExa
showTrackFill={false}
value={this.state.value3}
vertical={vertical}
handleHtmlProps={{ "aria-label": "example 3" }}
/>
</Example>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,13 +53,15 @@ export class SpinnerExample extends React.PureComponent<IExampleProps, ISpinnerE
);
}

private spinnerSizeLabelId = "spinner-size-label";

private renderOptions() {
const { size, hasValue, intent, value } = this.state;
return (
<>
<H5>Props</H5>
<IntentSelect intent={intent} onChange={this.handleModifierChange} />
<Label>Size</Label>
<Label id={this.spinnerSizeLabelId}>Size</Label>
<Slider
labelStepSize={50}
min={0}
Expand All @@ -68,6 +70,7 @@ export class SpinnerExample extends React.PureComponent<IExampleProps, ISpinnerE
stepSize={5}
value={size}
onChange={this.handleSizeChange}
handleHtmlProps={{ "aria-labelledby": this.spinnerSizeLabelId }}
/>
<Switch checked={hasValue} label="Known value" onChange={this.handleIndeterminateChange} />
<Slider
Expand All @@ -80,6 +83,7 @@ export class SpinnerExample extends React.PureComponent<IExampleProps, ISpinnerE
stepSize={0.1}
showTrackFill={false}
value={value}
handleHtmlProps={{ "aria-label": "spinner value" }}
/>
</>
);
Expand Down