Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
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');
});
});
17 changes: 12 additions & 5 deletions packages/core/src/components/radio/radio.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,22 +10,22 @@
import {
AttachInternals,
Component,
Element,
Event,
EventEmitter,
h,
Host,
Method,
Prop,
Watch,
h,
Method,
Element,
} from '@stencil/core';
import { makeRef } from '../utils/make-ref';
import { a11yBoolean } from '../utils/a11y';
import {
ClassMutationObserver,
createClassMutationObserver,
IxFormComponent,
} from '../utils/input';
import { a11yBoolean } from '../utils/a11y';
import { makeRef } from '../utils/make-ref';

/**
* @form-ready
Expand Down Expand Up @@ -165,11 +165,18 @@ export class Radio implements IxFormComponent<string> {
<Host
aria-checked={a11yBoolean(this.checked)}
aria-disabled={a11yBoolean(this.disabled)}
aria-label={this.label}
role="radio"
class={{
disabled: this.disabled,
checked: this.checked,
}}
onClick={(event: MouseEvent) => {
const target = event.target as HTMLElement;
if (!this.disabled && !target.closest('label')) {
this.setCheckedState(true);
}
}}
onBlur={() => this.ixBlur.emit()}
>
<label>
Expand Down
64 changes: 64 additions & 0 deletions packages/core/src/components/radio/test/radio.ct.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,3 +120,67 @@ test('Clicking label (including padding) checks the radio', async ({
await label.waitFor({ state: 'visible' });
await expect(radio).toHaveAttribute('aria-checked', 'true');
});

test.describe('accessibility & click handling', () => {
test('should be directly clickable via accessible role with label', async ({
mount,
page,
}) => {
await mount(`<ix-radio label="Small" value="small"></ix-radio>`);
const radio = page.getByRole('radio', { name: 'Small' });
await expect(radio).toHaveAttribute('aria-checked', 'false');
await radio.click();
await expect(radio).toHaveAttribute('aria-checked', 'true');
await expect(radio).toHaveAttribute('aria-label', 'Small');
});

test('should respect disabled state when clicking host element', async ({
mount,
page,
}) => {
await mount(`<ix-radio label="Disabled Option" disabled></ix-radio>`);
const radio = page.getByRole('radio', { name: 'Disabled Option' });
await expect(radio).toHaveAttribute('aria-checked', 'false');
await expect(radio).toHaveAttribute('aria-disabled', 'true');
await radio.click({ force: true });
await expect(radio).toHaveAttribute('aria-checked', 'false');
});

test('should emit checkedChange event when clicked via host', async ({
mount,
page,
}) => {
await mount(`
<ix-radio-group>
<ix-radio label="Option 1" value="opt1"></ix-radio>
<ix-radio label="Option 2" value="opt2"></ix-radio>
</ix-radio-group>
`);

const radio1 = page.getByRole('radio', { name: 'Option 1' });
const radio2 = page.getByRole('radio', { name: 'Option 2' });

await expect(radio1).toHaveAttribute('aria-checked', 'false');
await expect(radio2).toHaveAttribute('aria-checked', 'false');

await radio1.click();
await expect(radio1).toHaveAttribute('aria-checked', 'true');
await expect(radio2).toHaveAttribute('aria-checked', 'false');

await radio2.click();
await expect(radio1).toHaveAttribute('aria-checked', 'false');
await expect(radio2).toHaveAttribute('aria-checked', 'true');
});

test('radio should not toggle back to unchecked when clicked again', async ({
mount,
page,
}) => {
await mount(`<ix-radio label="No Toggle"></ix-radio>`);
const radio = page.getByRole('radio', { name: 'No Toggle' });
await radio.click();
await expect(radio).toHaveAttribute('aria-checked', 'true');
await radio.click();
await expect(radio).toHaveAttribute('aria-checked', 'true');
});
});
Loading