Skip to content

Commit

Permalink
fix(foundation): remove readonly from radio button (#6438)
Browse files Browse the repository at this point in the history
* fix(foundation): remove readonly from radio button

readonly on radio button violates a WCAG rule. see #6437

there's no evidence readonly applies on radio button. it does on radio group however

- [aria-readonly associated roles on MDN](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-readonly#associated_roles) doesn’t list radio
- https://stackoverflow.com/questions/1953017/why-cant-radio-buttons-be-readonly
- https://www.w3schools.com/jsref/dom_obj_radio.asp

* remove any evidence of readonly from class

* completely remove any readonly

* Change files

* group slotted radio readonly handling

* docs update

* Update change/@microsoft-fast-foundation-b9885563-e3e8-4c85-a55d-bac04c627fec.json

Co-authored-by: Nicholas Rice <[email protected]>

* Update change/@microsoft-fast-foundation-b9885563-e3e8-4c85-a55d-bac04c627fec.json

Co-authored-by: Nicholas Rice <[email protected]>

* prevent check of radio buttons

* Update packages/web-components/fast-foundation/src/radio/radio.ts

Co-authored-by: Nicholas Rice <[email protected]>

* revert

* remove unused

* FASTRadioGroup to be used as type only

* fixup prettier and API report

---------

Co-authored-by: Nicholas Rice <[email protected]>
Co-authored-by: Rob Eisenberg <[email protected]>
Co-authored-by: Chris Holt <[email protected]>
  • Loading branch information
4 people authored and janechu committed Jun 10, 2024
1 parent f124e58 commit fda881b
Show file tree
Hide file tree
Showing 12 changed files with 29 additions and 122 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "prerelease",
"comment": "remove readonly support from fast radio",
"packageName": "@microsoft/fast-foundation",
"email": "[email protected]",
"dependentChangeType": "prerelease"
}
7 changes: 1 addition & 6 deletions packages/web-components/fast-foundation/docs/api-report.md
Original file line number Diff line number Diff line change
Expand Up @@ -1638,9 +1638,6 @@ export class FASTRadio extends FormAssociatedRadio implements RadioControl {
// @beta
keypressHandler(e: KeyboardEvent): boolean | void;
name: string;
readOnly: boolean;
// (undocumented)
protected readOnlyChanged(): void;
}

// @public
Expand All @@ -1667,8 +1664,6 @@ export class FASTRadioGroup extends FASTElement {
protected nameChanged(): void;
orientation: RadioGroupOrientation;
readOnly: boolean;
// (undocumented)
protected readOnlyChanged(): void;
// @internal (undocumented)
slottedRadioButtons: HTMLElement[];
// (undocumented)
Expand Down Expand Up @@ -2532,7 +2527,7 @@ export interface PropertyTarget {
export type ProxyElement = HTMLSelectElement | HTMLTextAreaElement | HTMLInputElement;

// @public
export type RadioControl = Pick<HTMLInputElement, "checked" | "disabled" | "readOnly" | "focus" | "setAttribute" | "getAttribute">;
export type RadioControl = Pick<HTMLInputElement, "checked" | "disabled" | "focus" | "setAttribute" | "getAttribute">;

// @public
export const RadioGroupOrientation: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,6 @@ export const myRadioGroup = RadioGroup.compose({

| Name | Privacy | Description | Parameters | Return | Inherited From |
| ---------------------------- | --------- | ----------- | -------------------------------------------- | ------ | -------------- |
| `readOnlyChanged` | protected | | | `void` | |
| `disabledChanged` | protected | | | `void` | |
| `nameChanged` | protected | | | `void` | |
| `valueChanged` | protected | | | `void` | |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -317,32 +317,6 @@ test.describe("Radio Group", () => {
}
});

test("should set all child radio elements to readonly when the `readonly` property is true", async () => {
await root.evaluate(node => {
node.innerHTML = /* html */ `
<fast-radio-group readonly>
<fast-radio></fast-radio>
<fast-radio></fast-radio>
<fast-radio></fast-radio>
</fast-radio-group>
`;
});

await expect(element).toHaveBooleanAttribute("readonly");

expect(
await radios.evaluateAll(radios =>
radios.every(radio => radio.hasAttribute("readonly"))
)
).toBeTruthy();

expect(
await radios.evaluateAll(radios =>
radios.every(radio => radio.getAttribute("aria-readonly") === "true")
)
).toBeTruthy();
});

test("should set tabindex of 0 to a child radio with a matching `value`", async () => {
await root.evaluate(node => {
node.innerHTML = /* html */ `
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,17 +32,6 @@ export class FASTRadioGroup extends FASTElement {
*/
@attr({ attribute: "readonly", mode: "boolean" })
public readOnly: boolean;
protected readOnlyChanged(): void {
if (this.slottedRadioButtons !== undefined) {
this.slottedRadioButtons.forEach((radio: FASTRadio) => {
if (this.readOnly) {
radio.readOnly = true;
} else {
radio.readOnly = false;
}
});
}
}

/**
* Disables the radio group and child radios.
Expand Down Expand Up @@ -173,10 +162,6 @@ export class FASTRadioGroup extends FASTElement {
radio.setAttribute("name", this.name);
}

if (this.readOnly) {
radio.readOnly = true;
}

if (this.value && this.value === radio.value) {
this.selectedRadio = radio;
this.focusedRadio = radio;
Expand Down Expand Up @@ -239,16 +224,8 @@ export class FASTRadioGroup extends FASTElement {
const radio: FASTRadio = group[index] as FASTRadio;
if (!this.isInsideToolbar) {
radio.setAttribute("tabindex", "0");
if (radio.readOnly) {
this.slottedRadioButtons.forEach((nextRadio: FASTRadio) => {
if (nextRadio !== radio) {
nextRadio.setAttribute("tabindex", "-1");
}
});
} else {
radio.checked = true;
this.selectedRadio = radio;
}
radio.checked = true;
this.selectedRadio = radio;
}
this.focusedRadio = radio;
radio.focus();
Expand Down Expand Up @@ -350,11 +327,7 @@ export class FASTRadioGroup extends FASTElement {
};

private checkFocusedRadio = (): void => {
if (
this.focusedRadio !== null &&
!this.focusedRadio.readOnly &&
!this.focusedRadio.checked
) {
if (this.focusedRadio !== null && !this.focusedRadio.checked) {
this.focusedRadio.checked = true;
this.focusedRadio.setAttribute("tabindex", "0");
this.focusedRadio.focus();
Expand Down
22 changes: 7 additions & 15 deletions packages/web-components/fast-foundation/src/radio/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,31 +113,23 @@ export const myRadio = Radio.compose<RadioOptions>({

#### Fields

| Name | Privacy | Type | Default | Description | Inherited From |
| ---------- | ------- | --------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------- |
| `readOnly` | public | `boolean` | | When true, the control will be immutable by user interaction. See [readonly HTML attribute](https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/readonly) for more information. | |
| `name` | public | `string` | | The name of the radio. See [name attribute](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#htmlattrdefname) for more info. | |
| `proxy` | | | | | FormAssociatedRadio |
| Name | Privacy | Type | Default | Description | Inherited From |
| ------- | ------- | -------- | ------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | ------------------- |
| `name` | public | `string` | | The name of the radio. See [name attribute](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#htmlattrdefname) for more info. | |
| `proxy` | | | | | FormAssociatedRadio |

#### Methods

| Name | Privacy | Description | Parameters | Return | Inherited From |
| ----------------- | --------- | --------------------------------- | ------------------ | ----------------- | -------------- |
| `readOnlyChanged` | protected | | | `void` | |
| `keypressHandler` | public | Handles key presses on the radio. | `e: KeyboardEvent` | `boolean or void` | |
| Name | Privacy | Description | Parameters | Return | Inherited From |
| ----------------- | ------- | --------------------------------- | ------------------ | ----------------- | -------------- |
| `keypressHandler` | public | Handles key presses on the radio. | `e: KeyboardEvent` | `boolean or void` | |

#### Events

| Name | Type | Description | Inherited From |
| -------- | ---- | ---------------------------------------------------------- | -------------- |
| `change` | | Emits a custom change event when the checked state changes | |

#### Attributes

| Name | Field | Inherited From |
| ---------- | -------- | -------------- |
| `readonly` | readOnly | |

#### CSS Parts

| Name | Description |
Expand Down
11 changes: 0 additions & 11 deletions packages/web-components/fast-foundation/src/radio/radio.pw.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,17 +71,6 @@ test.describe("Radio", () => {
await element.evaluate((node: FASTRadio) => (node.disabled = false));

await expect(element).toHaveAttribute("aria-disabled", "false");

// Readonly
await expect(element).not.hasAttribute("aria-readonly");

await element.evaluate((node: FASTRadio) => (node.readOnly = true));

await expect(element).toHaveAttribute("aria-readonly", "true");

await element.evaluate((node: FASTRadio) => (node.readOnly = false));

await expect(element).toHaveAttribute("aria-readonly", "false");
});

test("should set a tabindex of 0 on the element", async () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,6 @@ Extends [form associated custom element](../form-associated/form-associated-cust
- The current checked state of the radio

*Content attributes*
- `readonly`
- The radio should be submitted with the form but should not be editable.
- `disabled`
- The radio should be disabled from user interaction and will not be submitted with the form data.
- `value` - Not visible to the user, it's used for form data and to distinguish between other radio buttons of the same name attribute value.
Expand Down Expand Up @@ -72,7 +70,6 @@ Extends [form associated custom element](../form-associated/form-associated-cust
- checked
- disabled
- required
- readonly

*Slotted Content/Slotted Classes*
*CSS Parts*
Expand All @@ -89,9 +86,6 @@ The checked state can be toggled by:
**disabled**: `true` or `false`
When disabled, the value will not be changeable through user interaction. It should also not expose it's value to a form submission.

**readonly**: `true` or `false`
When readonly, the value will not be changeable through user interaction. The value will still be exposed to forms on submission.

### Accessibility
The root element inside the shadow-dom of the radio will be a focusable element with the following accessibility content attributes:
[MDN Web docs](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/ARIA_Techniques/Using_the_radio_role)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ export function radioTemplate<T extends FASTRadio>(
aria-checked="${x => x.checked}"
aria-required="${x => x.required}"
aria-disabled="${x => x.disabled}"
aria-readonly="${x => x.readOnly}"
@keypress="${(x, c) => x.keypressHandler(c.event as KeyboardEvent)}"
>
<div part="control" class="control">
Expand Down
33 changes: 11 additions & 22 deletions packages/web-components/fast-foundation/src/radio/radio.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { attr, observable } from "@microsoft/fast-element";
import { observable } from "@microsoft/fast-element";
import { keySpace } from "@microsoft/fast-web-utilities";
import { FASTRadioGroup } from "../radio-group/index.js";
import type { FASTRadioGroup } from "../radio-group/index.js";
import type { StaticallyComposableHTML } from "../utilities/template-helpers.js";
import { FormAssociatedRadio } from "./radio.form-associated.js";

Expand All @@ -10,7 +10,7 @@ import { FormAssociatedRadio } from "./radio.form-associated.js";
*/
export type RadioControl = Pick<
HTMLInputElement,
"checked" | "disabled" | "readOnly" | "focus" | "setAttribute" | "getAttribute"
"checked" | "disabled" | "focus" | "setAttribute" | "getAttribute"
>;

/**
Expand All @@ -34,20 +34,6 @@ export type RadioOptions = {
* @public
*/
export class FASTRadio extends FormAssociatedRadio implements RadioControl {
/**
* When true, the control will be immutable by user interaction. See {@link https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/readonly | readonly HTML attribute} for more information.
* @public
* @remarks
* HTML Attribute: readonly
*/
@attr({ attribute: "readonly", mode: "boolean" })
public readOnly: boolean; // Map to proxy element
protected readOnlyChanged(): void {
if (this.proxy instanceof HTMLInputElement) {
this.proxy.readOnly = this.readOnly;
}
}

/**
* The name of the radio. See {@link https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#htmlattrdefname | name attribute} for more info.
*/
Expand All @@ -68,6 +54,12 @@ export class FASTRadio extends FormAssociatedRadio implements RadioControl {
@observable
public defaultSlottedNodes: Node[];

private get radioGroup() {
return (this as HTMLElement).closest(
"[role=radiogroup]"
) as FASTRadioGroup | null;
}

/**
* @internal
*/
Expand Down Expand Up @@ -118,10 +110,7 @@ export class FASTRadio extends FormAssociatedRadio implements RadioControl {
}

private isInsideRadioGroup(): boolean {
const parent: HTMLElement | null = (this as HTMLElement).closest(
"[role=radiogroup]"
);
return parent !== null;
return this.radioGroup !== null;
}

/**
Expand All @@ -131,7 +120,7 @@ export class FASTRadio extends FormAssociatedRadio implements RadioControl {
public keypressHandler(e: KeyboardEvent): boolean | void {
switch (e.key) {
case keySpace:
if (!this.checked && !this.readOnly) {
if (!this.checked && !this.radioGroup?.readOnly) {
this.checked = true;
}
return;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -119,8 +119,6 @@ const styles = css`
}
:host([disabled]) .label,
:host([readonly]) .label,
:host([readonly]) .control,
:host([disabled]) .control {
cursor: not-allowed;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ export const storyTemplate = html<StoryArgs<FASTRadio>>`
?checked="${x => x.checked}"
?disabled="${x => x.disabled}"
?required="${x => x.required}"
?readonly="${x => x.readOnly}"
name="${x => x.name}"
value="${x => x.value}"
>
Expand All @@ -23,7 +22,6 @@ export default {
checked: false,
disabled: false,
required: false,
readOnly: false,
storyContent: "Label",
},
argTypes: {
Expand Down

0 comments on commit fda881b

Please sign in to comment.