Skip to content
Merged
Show file tree
Hide file tree
Changes from 14 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
2 changes: 2 additions & 0 deletions packages/core/src/components.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2969,6 +2969,7 @@ export namespace Components {
* @default false
*/
"required": boolean;
"setCheckedState": (newChecked: boolean) => Promise<void>;
/**
* Value of the radio component
*/
Expand Down Expand Up @@ -3006,6 +3007,7 @@ export namespace Components {
* @default false
*/
"required"?: boolean;
"setCheckedToNextItem": (currentRadio: HTMLIxRadioElement, forward?: boolean) => Promise<void>;
/**
* Show helper, info, warning, error and valid text as tooltip
*/
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/components/checkbox/checkbox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,7 @@ export class Checkbox implements IxFormComponent<string> {
<Host
aria-checked={a11yBoolean(this.checked)}
aria-disabled={a11yBoolean(this.disabled)}
aria-label={this.label}
role="checkbox"
class={{
disabled: this.disabled,
Expand Down
16 changes: 15 additions & 1 deletion packages/core/src/components/checkbox/tests/checkbox.ct.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,13 @@
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import { expect } from '@playwright/test';
import {
getFormValue,
preventFormSubmission,
regressionTest,
test,
} from '@utils/test';
import { expect } from '@playwright/test';

regressionTest(`form-ready`, async ({ mount, page }) => {
await mount(`<form><ix-checkbox name="my-field-name"></ix-checkbox></form>`);
Expand Down Expand Up @@ -119,3 +119,17 @@ test('Checkbox should not cause layout shift when checked', async ({
expect(newBounds.top).toBeCloseTo(initialBounds.top, 0);
expect(newBounds.left).toBeCloseTo(initialBounds.left, 0);
});

test.describe('accessibility', () => {
test('should expose aria-label for accessibility queries', async ({
mount,
page,
}) => {
await mount(`<ix-checkbox label="Accept Terms"></ix-checkbox>`);
const checkbox = page.getByRole('checkbox', { name: 'Accept Terms' });
await expect(checkbox).toBeVisible();
await expect(checkbox).toHaveAttribute('aria-label', 'Accept Terms');
await checkbox.click();
await expect(checkbox).toHaveAttribute('aria-checked', 'true');
});
});
51 changes: 50 additions & 1 deletion packages/core/src/components/radio-group/radio-group.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,7 @@ export class RadiobuttonGroup
if (!this.value) {
return;
}

this.radiobuttonElements.forEach((radiobutton) => {
radiobutton.checked = radiobutton.value === this.value;
});
Expand All @@ -153,6 +154,16 @@ export class RadiobuttonGroup
}
radio.checked = false;
});

const hasCheckedRadio = this.isSomeRadioChecked();

this.radiobuttonElements.forEach((radio) => {
radio.tabIndex = radio.checked ? 0 : -1;
});

if (!hasCheckedRadio && this.radiobuttonElements.length > 0) {
this.radiobuttonElements[0].tabIndex = 0;
}
}

private hasNestedRequiredRadio() {
Expand All @@ -161,6 +172,10 @@ export class RadiobuttonGroup
);
}

private isSomeRadioChecked() {
return this.radiobuttonElements.some((radio) => radio.checked);
}

@Watch('value')
onValueChangeHandler(newValue: string) {
this.touched = true;
Expand Down Expand Up @@ -213,9 +228,43 @@ export class RadiobuttonGroup
return Promise.resolve(this.touched);
}

/** @internal */
@Method()
setCheckedToNextItem(
currentRadio: HTMLIxRadioElement,
forward = true
): Promise<void> {
const { radiobuttonElements } = this;
const { length } = radiobuttonElements;
if (length <= 1) {
return Promise.resolve();
}

const index = radiobuttonElements.indexOf(currentRadio);
const step = forward ? 1 : -1;
let nextIndex = (index + step + length) % length;

while (radiobuttonElements[nextIndex].disabled) {
if (nextIndex === index) {
return Promise.resolve();
}
nextIndex = (nextIndex + step + length) % length;
}

const nextRadio = radiobuttonElements[nextIndex];
nextRadio.setCheckedState(true);
nextRadio.focus();

return Promise.resolve();
}

render() {
return (
<Host onIxBlur={() => (this.touched = true)} ref={this.groupRef}>
<Host
onIxBlur={() => (this.touched = true)}
ref={this.groupRef}
role="radiogroup"
>
<ix-field-wrapper
label={this.label}
helperText={this.helperText}
Expand Down
99 changes: 99 additions & 0 deletions packages/core/src/components/radio-group/test/radio-group.ct.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,3 +143,102 @@ regressionTest('disabled', async ({ mount, page }) => {
await expect(radioOption3).not.toBeEnabled();
await expect(radioOption3.locator('.checkmark')).not.toBeVisible();
});

regressionTest.describe('keyboard navigation', () => {
regressionTest('Initial', async ({ mount, page }) => {
await mount(
`
<ix-radio-group>
<ix-radio aria-label="Option 1" label="Option 1" value="option1"></ix-radio>
<ix-radio aria-label="Option 2" label="Option 2" value="option2"></ix-radio>
<ix-radio aria-label="Option 3" label="Option 3" value="option3"></ix-radio>
<ix-radio aria-label="Option 4" label="Option 4" value="option4"></ix-radio>
</ix-radio-group>
`
);
const firstRadio = page.getByLabel('Option 1');
await expect(firstRadio).toBeVisible();

await page.keyboard.press('Tab');
await expect(page.getByLabel('Option 1')).not.toBeChecked();

await page.keyboard.press('ArrowDown');
await expect(page.getByLabel('Option 1')).not.toBeChecked();
await expect(page.getByLabel('Option 2')).toBeChecked();
});

regressionTest('Initial checked', async ({ mount, page }) => {
await mount(
`
<ix-radio-group>
<ix-radio aria-label="Option 1" label="Option 1" value="option1"></ix-radio>
<ix-radio aria-label="Option 2" label="Option 2" value="option2" checked></ix-radio>
<ix-radio aria-label="Option 3" label="Option 3" value="option3"></ix-radio>
<ix-radio aria-label="Option 4" label="Option 4" value="option4"></ix-radio>
</ix-radio-group>
`
);
const firstRadio = page.getByLabel('Option 1');
await expect(firstRadio).toBeVisible();

await page.keyboard.press('Tab');
await expect(page.getByLabel('Option 1')).not.toBeChecked();
await expect(page.getByLabel('Option 2')).toBeChecked();
});

regressionTest('Tab skip to next focus element', async ({ mount, page }) => {
await mount(
`
<ix-radio-group>
<ix-radio aria-label="Option 1" label="Option 1" value="option1"></ix-radio>
<ix-radio aria-label="Option 2" label="Option 2" value="option2" checked></ix-radio>
<ix-radio aria-label="Option 3" label="Option 3" value="option3" disabled></ix-radio>
<ix-radio aria-label="Option 4" label="Option 4" value="option4"></ix-radio>
</ix-radio-group>

<ix-button aria-label="Focusable Element">Focusable Element</ix-button>
`
);
const firstRadio = page.getByLabel('Option 1');
await expect(firstRadio).toBeVisible();

await page.keyboard.press('Tab');
await expect(page.getByLabel('Option 1')).not.toBeChecked();
await expect(page.getByLabel('Option 2')).toBeChecked();

await page.keyboard.press('Tab');
await expect(page.getByLabel('Focusable Element')).toBeFocused();
});

regressionTest('arrow navigation', async ({ mount, page }) => {
await mount(
`
<ix-radio-group>
<ix-radio aria-label="Option 1" label="Option 1" value="option1"></ix-radio>
<ix-radio aria-label="Option 2" label="Option 2" value="option2" checked></ix-radio>
<ix-radio aria-label="Option 3" label="Option 3" value="option3" disabled></ix-radio>
<ix-radio aria-label="Option 4" label="Option 4" value="option4"></ix-radio>
</ix-radio-group>
`
);
const firstRadio = page.getByLabel('Option 1');
await expect(firstRadio).toBeVisible();

await page.keyboard.press('Tab');
await expect(page.getByLabel('Option 1')).not.toBeChecked();
await expect(page.getByLabel('Option 2')).toBeChecked();

await page.keyboard.press('ArrowDown');
await expect(page.getByLabel('Option 3')).not.toBeChecked();
await expect(page.getByLabel('Option 4')).toBeChecked();

await page.keyboard.press('ArrowRight');
await expect(page.getByLabel('Option 1')).toBeChecked();

await page.keyboard.press('ArrowUp');
await expect(page.getByLabel('Option 4')).toBeChecked();

await page.keyboard.press('ArrowLeft');
await expect(page.getByLabel('Option 2')).toBeChecked();
});
});
Loading
Loading