microsoft / microsoft/playwright
[Feature]: Enable Playwright to Click Accessible, Re-styled `checkbox`es and `radio`s
Nobody has claimed this yet.
- Dominant language
- TypeScript
- Stars
- 96.3k
- Forks
- 6.5k
- Avg merge
- 1d 6h
- Merged PRs (30d)
- 180
Description
### 🚀 Feature Request
Enable Playwright to interact with ``s and ``s that are hidden for re-styling purposes but still fully-accessible to all users (including those without JavaScript).
(Relates to https://github.com/microsoft/playwright/issues/12267#issuecomment-1499146439.)
---
### Example
If this feature was supported, commands like the following would become possible in Playwright for _re-styled_ `checkbox`es and `radio`s:
```ts
await page.getByRole("checkbox", { name: "Check Me" }).click();
await page.getByRole("checkbox", { name: "Check Me" }).setChecked(false);
await page.getByRole("radio", { name: "Option 1" }).click();
await page.getByRole("radio", { name: "Option 1" }).setChecked(true);
```
There might be other commands that I'm missing, but you get the idea. (To define "re-styled", see the `Motivation` section of this OP.)
---
### Motivation
Oftentimes, developers/companies will want to apply custom styles to an `` or an ``. Unfortunately, modern browsers do not provide an easy way for developers to accomplish this. Consequently, the standard has been to do the following:
1) Hide these inputs (without taking them out of the Tab Order or making them inaccessible to Screen Readers)
2) Use the associated `` element to receive clicks, toggle the `checkbox`/`radio`, and display various states like `:checked` or `:focus`
If you're unfamiliar with this pattern, here's a _very rough_ idea of how this might be implemented:
Show Code Sample
```html
.visually-hidden {
/* Do not obscure the DOM layout. */
position: absolute;
padding: 0;
border: 0;
margin: 0;
/* Place text in a screen-readable (non-zero-sized) block. Then clip the block so that it isn't visible (to the eye). */
width: 1px;
height: 1px;
clip-path: inset(50%);
/* Help screen readers see full sentences (instead of multi-lined, separated letters). */
white-space: nowrap;
/* Disable scrolling */
overflow: hidden;
}
input[type="checkbox"].visually-hidden + label {
display: flex;
align-items: center;
gap: 4px;
cursor: pointer;
&::before {
content: "";
display: flex;
justify-content: center;
align-items: center;
box-sizing: border-box;
width: var(--checkbox-size);
height: var(--checkbox-size);
padding: 2px;
border: 1px solid black;
border-radius: 4px;
background-color: white;
}
&:is(input:checked + label)::before {
content: "\00D7" / "";
}
&:is(input:focus-visible + label)::before {
border-color: dodgerblue;
outline: 1px solid dodgerblue;
}
}
Check Me
```
If you play with this (e.g., on [MDN Playground](https://developer.mozilla.org/en-US/play?id=r5X%2Fom47VLKesTp%2F2g%2BHR5YF8uagbUPcA4dZ2QYhXJHz8amGraFWvaPAANYBD8MpajndvaO%2FIb6lPEyv)), you'll find that this `checkbox` solution:
1) Is accessible to keyboard users
2) Is accessible to mouse users
3) Is accessible to screen reader users
4) Works without JavaScript (very important)
However, Playwright does not recognize this, and it will fail if we attempt to toggle this checkbox with something like
```ts
await page.getByRole("checkbox", { name: "Check Me" }).click();
```
According to Playwright, this test fails for the following reason:
> `Check Me` intercepts pointer events
But this behavior is exactly what developers want! And it creates a perfectly-accessible user experience as well. You can see an example of this problem at: https://github.com/ITenthusiasm/playwright-issue-checkboxes. Again, the same problem will occur for `radio`s.
---
### Possible Implementations
Playwright's logic wouldn't need to change too much to support this use case. If Playwright can't `click` a re-styled `checkbox` or `radio`, it can simply try clicking one of its [associated labels](https://developer.mozilla.org/en-US/docs/Web/API/HTMLInputElement/labels) instead. Whatever actionability Playwright tests for `input:is([type="checkbox"], [type="radio"])`, it can simply re-run that logic on the available `input.labels` and consider everything good to go if actionability is possible on a (valid) owning label.
For bonus points, Playwright can ignore all `` elements in `input.labels` whose text content doesn't match the `name` passed to the `getByRole()` call -- to be on the safe side.
**_Alternatively_**, if the team feels iffy about applying this logic exclusively to `checkbox`es and `radio`s, then perhaps Playwright could support an option like `includeLabels` to indicate that Playwright should feel free to check for a semantic, accessible, associated `` with text content that matches the `` of interest:
```ts
await page.getByRole("checkbox", { name: "Check Me" }).click({ includeLabels: true });
```
However, Playwright would need to support this option for all relevant commands (like `setChecked()`). And at the end of the day, I'm not sure if this would really be relevant (or should even be allowed) for anything that isn't a `checkbox` or `radio`.
Perhaps a better approach would be to apply `includeLabels` to the `getByRole` method itself. This would be similar to the `includeHidden` option:
```ts
await page.getByRole("checkbox", { name: "Check Me", includeLabels: true }).click();
```
---
### (Invalid) Workarounds
There are workarounds to this problem, but they are either unorthodox or a poor DX. This section is optional. Feel free to expand it if desired, though.
Dissatisfactory Workarounds (3)
#### 1) Directly Clicking Elements with JavaScript
Developers can technically do either of the following themselves
```ts
const checkbox = page.getByRole("checkbox", { name: "Check Me" });
await checkbox.evaluate((c) => c.click());
await checkbox.evaluate((c) => c.labels?.[0].click());
```
But that defeats the whole point of running Playwright. We want Playwright to _verify_ that the owning `` (which we've re-purposed to display the `checkbox` state to visual users) is in fact clickable, and that clicking it produces the desired result (toggling the `checkbox`, etc.).
#### 2) Getting the `` by Text
Developers can also do
```ts
const checkboxLabelText = "Check Me";
const checkboxLabel = page.getByText(checkboxLabelText);
await checkboxLabel.click();
```
But there are two problems with this approach:
1) It _assumes_ that no other elements on the page have similar text
2) It is less convenient than simply calling `click()` on the `checkbox` (which is _effectively_ what the user will be doing)
The 1st issue in this list can be resolved by using `getByTestId()`. But like the previous workaround, this is an anti-pattern, and it unnecessarily pollutes the DOM.
#### 3) Focusing the `checkbox` and Pressing `SpaceBar`
```ts
const checkbox = page.getByRole("checkbox", { name: "Check Me" });
await checkbox.focus();
await page.keyboard.press(" ");
```
For developers simply desiring to toggle the checkbox in a valid, accessible manner, this technically gets the job done. _But for developers desiring to prove that the checkbox is accessible to Mouse Users, this test does nothing at all._
Additionally, this solution may not be 100% reliable. (Do we know _for certain_ that the checkbox doesn't match `[tabindex="-1"]`? A better test for accessibility here would be to use `page.keyboard.press("Tab")`. But now you have to search for the place in the Document where pressing `Tab` would lead you to the `checkbox`. This is again inconvenient.)
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start by tracing the actionability checks used by getByRole("checkbox"), click(), and setChecked() for checkbox and radio inputs. Compare those checks with the associated input.labels behavior described here, then add coverage showing that accessible, visually hidden controls can be operated through a valid label while preserving existing behavior.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- playwright, typescript
- Domain
- testing-qa
- Issue type
- Feature
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 52/100