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

Stepper accessibility fixes #4571

Merged
merged 3 commits into from
Jan 28, 2025
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
26 changes: 14 additions & 12 deletions docs/src/__examples__/Stepper/DEFAULT.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,22 @@
import React from "react";
import { Heading, Stepper, Stack } from "@kiwicom/orbit-components";
import { Heading, Stepper, Stack, OrbitProvider, defaultTheme } from "@kiwicom/orbit-components";
import { Passengers } from "@kiwicom/orbit-components/icons";

export default {
Example: () => (
<Stack align="center" spacing="400" desktop={{ spacing: "600" }}>
<Heading type="title4">
<Stack align="center">
<Passengers />
Travelers
</Stack>
</Heading>
<div style={{ maxWidth: "8em" }}>
<Stepper defaultValue={2} maxValue={10} minValue={1} />
</div>
</Stack>
<OrbitProvider theme={defaultTheme} useId={React.useId}>
<Stack align="center" spacing="400" desktop={{ spacing: "600" }}>
<Heading type="title4">
<Stack align="center">
<Passengers />
Travelers
</Stack>
</Heading>
<div style={{ maxWidth: "8em" }}>
<Stepper defaultValue={2} maxValue={10} minValue={1} />
</div>
</Stack>
</OrbitProvider>
),
exampleKnobs: [
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
---
title: Accessibility
redirect_from:
- /components/stepper/accessibility/
---

## Accessibility

The Stepper component has been designed with accessibility in mind. It can be used with keyboard navigation and includes properties that enhance the experience for users of assistive technologies.
domihustinova marked this conversation as resolved.
Show resolved Hide resolved

The following props provide additional information to screen readers:

- The `ariaLabelValue` prop allows you to specify an `aria-label` attribute for the input element (stepper value) in the Stepper (StepperStateless) component.

- The `titleDecrement` prop allows you to specify an `aria-label` attribute for the decrement icon button in the Stepper (StepperStateless) component.

- The `titleIncrement` prop allows you to specify an `aria-label` attribute for the increment icon button in the Stepper (StepperStateless) component.

- The `ariaLabelledBy` prop allows you to specify an `aria-labelledby` attribute for the Stepper component. This attribute references the ID of the element that labels the Stepper (StepperStateless), ensuring that screen readers announce the label correctly.

Although these props are optional for the Stepper (StepperStateless) component itself, it is recommended to fill them in.

### Example

```jsx
<Stepper
domihustinova marked this conversation as resolved.
Show resolved Hide resolved
step={1}
minValue={0}
minValue={10}
ariaLabelValue="Number of passengers"
titleDecrement="Remove a passenger"
titleIncrement="Add a passenger"
/>
```

The screen reader will announce the value title (`Number of passengers`) and buttons title (`Add a passenger`, `Remove a passenger`) once they are focused by the screen reader.

```jsx
<Stack>
<Stack>
<Text id="passengers">Passengers</Text>
</Stack>
<Stepper
step={1}
minValue={0}
minValue={10}
ariaLabelValue="Number of passengers"
ariaLabelledby="passengers"
titleDecrement="Remove a passenger"
titleIncrement="Add a passenger"
/>
</Stack>
```

This example includes `ariaLabelledby` prop. In this case, `ariaLabelledBy` prop is prioritized over `ariaLabelValue`, so the screen reader will announce the value title (`Passengers`) and buttons title (`Add a passenger`, `Remove a passenger`) once they are focused by the screen reader.
19 changes: 8 additions & 11 deletions packages/orbit-components/src/Stepper/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,15 +31,10 @@ Table below contains all types of the props available in Stepper component.
| onChange | `number => void \| Promise` | | Function for handling onClick event. |
| onFocus | `event => void \| Promise` | | Function for handling onFocus event. |
| step | `number` | `1` | Specifies the value of step to increment and decrement. |
| titleDecrement | `string \| (any => string)` | | Specifies `title` property on decrement `Button`. |
| titleIncrement | `string \| (any => string)` | | Specifies `title` property on increment `Button`. |

### size
DSil marked this conversation as resolved.
Show resolved Hide resolved

| size |
| :--------- |
| `"small"` |
| `"normal"` |
| titleDecrement | `string \| (any => string)` | | Specifies `aria-label` property on decrement `Button`. |
| titleIncrement | `string \| (any => string)` | | Specifies `aria-label` property on increment `Button`. |
| ariaLabelValue | `string` | | Optional prop for `aria-label` value. |
| ariaLabelledBy | `string` | | Optional prop for `aria-labelledby` value. |

## Functional specs

Expand Down Expand Up @@ -77,9 +72,11 @@ Table below contains all types of the props available in `StepperStateless` comp
| onIncrement | `event => void \| Promise` | | Function for handling increment event. |
| onKeyDown | `event => void \| Promise` | | Function for handling onKeyDown event present on input. |
| step | `number` | `1` | Specifies the value of step to increment and decrement. |
| titleDecrement | `string \| (any => string)` | | Specifies `title` property on decrement `Button`. |
| titleIncrement | `string \| (any => string)` | | Specifies `title` property on increment `Button`. |
| titleDecrement | `string \| (any => string)` | | Specifies `aria-label` property on decrement `Button`. |
| titleIncrement | `string \| (any => string)` | | Specifies `aria-label` property on increment `Button`. |
| value | `number \| string` | | Specifies the value of the StepperStateless. |
| ariaLabelValue | `string` | | Optional prop for `aria-label` value. |
| ariaLabelledBy | `string` | | Optional prop for `aria-labelledby` value. |

### Usage:

Expand Down
19 changes: 14 additions & 5 deletions packages/orbit-components/src/Stepper/Stepper.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,21 +23,26 @@ export const Default: Story = {
disable: true,
},
},

args: {
ariaLabelValue: "Number of passengers",
titleIncrement: "Add a passenger",
titleDecrement: "Remove a passenger",
},
};

export const Playground: Story = {
args: {
...Default.args,
id: "stepper-ID",
name: "Passengers number",
name: "Number of passengers",
step: 1,
minValue: 0,
maxValue: 20,
defaultValue: 0,
active: false,
disabled: false,
maxWidth: 120,
titleIncrement: "Add a passenger",
titleDecrement: "Remove a passenger",
onChange: action("onChange"),
onFocus: action("onFocus"),
onBlur: action("onBlur"),
Expand Down Expand Up @@ -83,9 +88,9 @@ export const Stateless: Story & StoryObj<typeof StatelessStepper> = {
};

export const Rtl: Story = {
render: () => (
render: args => (
<RenderInRtl>
<Stepper />
<Stepper {...args} />
</RenderInRtl>
),

Expand All @@ -95,4 +100,8 @@ export const Rtl: Story = {
disable: true,
},
},

args: {
...Default.args,
},
};
14 changes: 12 additions & 2 deletions packages/orbit-components/src/Stepper/StepperStateless/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import Plus from "../../icons/Plus";
import ButtonPrimitive from "../../primitives/ButtonPrimitive";
import useTheme from "../../hooks/useTheme";
import type { Props } from "./types";
import useRandomId from "../../hooks/useRandomId";

const getMaxWidth = ({ maxWidth }: { maxWidth: string | number }) => {
if (typeof maxWidth === "string") return maxWidth;
Expand Down Expand Up @@ -48,6 +49,8 @@ const StepperStateless = ({
titleDecrement,
disabledIncrement,
disabledDecrement,
ariaLabelValue,
ariaLabelledBy,
}: Props) => {
const theme = useTheme();

Expand All @@ -60,6 +63,8 @@ const StepperStateless = ({
const isPlusDisabled =
disabled || disabledIncrement || (typeof value === "number" && value >= +maxValue);

const inputId = useRandomId();

return (
<div
data-test={dataTest}
Expand All @@ -77,8 +82,9 @@ const StepperStateless = ({
onDecrement(ev);
}
}}
title={titleDecrement}
icons={iconStyles}
title={titleDecrement}
aria-controls={inputId}
/>
<input
className={cx(
Expand All @@ -103,6 +109,9 @@ const StepperStateless = ({
}}
onBlur={onBlur}
onFocus={onFocus}
aria-label={ariaLabelValue}
aria-labelledby={ariaLabelledBy}
id={inputId}
readOnly
/>
<ButtonPrimitive
Expand All @@ -115,8 +124,9 @@ const StepperStateless = ({
onIncrement(ev);
}
}}
title={titleIncrement}
icons={iconStyles}
title={titleIncrement}
Copy link
Member Author

@sarkaaa sarkaaa Jan 9, 2025

Choose a reason for hiding this comment

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

I was thinking of changing this prop name from title to ariaLabel in ButtonPrimitive and making it more specific, however, this would be (probably) BC. I searched where this prop is used and I've found it's used here.

I'm not sure if it makes sense to change this prop name, if it could be included within this PR or in a separate one.

Copy link
Contributor

@domihustinova domihustinova Jan 17, 2025

Choose a reason for hiding this comment

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

So your suggestion is to keep prop names in Stepper as titleIncrement and titleDecrement and change the prop on the ButtonPrimitive from title to ariaLabel so the usage would be ariaLabel={titleIncrement}?

But anyway, I think it's out of scope and this should be tackled when making Button/ButtonPrimitive accessible.

Copy link
Member Author

Choose a reason for hiding this comment

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

No, that wouldn't make sense. :D My suggestion was to rename aria attribute in ButtonPrimitive (probably in a different PR) and also change titleIncrement (titleDecrement). I kept titleDecrement (titleIncrement) because of attribute naming in ButtonPrimitive.

As you said, I also think it's out of the scope of this PR, that's the reason why I didn't touch ButtonPrimitive at all.

aria-controls={inputId}
/>
</div>
);
Expand Down
4 changes: 4 additions & 0 deletions packages/orbit-components/src/Stepper/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ const Stepper = ({ onChange, defaultValue = 0, maxWidth = 108, ...props }: Props
titleIncrement,
titleDecrement,
active,
ariaLabelValue,
ariaLabelledBy,
} = props;
return (
<StepperStateless
Expand All @@ -63,6 +65,8 @@ const Stepper = ({ onChange, defaultValue = 0, maxWidth = 108, ...props }: Props
id={id}
value={value}
name={name}
ariaLabelValue={ariaLabelValue}
ariaLabelledBy={ariaLabelledBy}
titleIncrement={titleIncrement}
titleDecrement={titleDecrement}
/>
Expand Down
2 changes: 2 additions & 0 deletions packages/orbit-components/src/Stepper/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ export interface SharedProps extends Common.Globals {
readonly maxWidth?: string | number;
readonly maxValue?: number;
readonly minValue?: number;
readonly ariaLabelValue?: string;
readonly ariaLabelledBy?: string;
// Deviation from other stepper properties
readonly titleIncrement?: string;
readonly titleDecrement?: string;
Expand Down
Loading