diff --git a/playwright/cps-ui-kit/components/cps-radio-group.spec.ts b/playwright/cps-ui-kit/components/cps-radio-group.spec.ts
new file mode 100644
index 000000000..ef01fa5ad
--- /dev/null
+++ b/playwright/cps-ui-kit/components/cps-radio-group.spec.ts
@@ -0,0 +1,128 @@
+import { test, expect, type Page, type Locator } from '@playwright/test';
+
+function example(page: Page, testId: string): Locator {
+ return page.getByTestId(testId);
+}
+
+function radioGroup(page: Page, testId: string): Locator {
+ return example(page, testId).getByRole('radiogroup');
+}
+
+test.describe('cps-radio-group', () => {
+ test.beforeEach(async ({ page }) => {
+ await page.goto('/radio-group');
+ });
+
+ test.describe('Real native keyboard navigation skips disabled radios', () => {
+ test('ArrowDown from an enabled radio real-skips a disabled one via native grouping', async ({
+ page
+ }) => {
+ const group = example(page, 'partially-disabled-radio-group');
+ const option2 = group.getByRole('radio', { name: 'Option 2' });
+ const option4 = group.getByRole('radio', { name: 'Option 4' });
+
+ await option2.focus();
+ await page.keyboard.press('ArrowDown');
+
+ await expect(option4).toBeFocused();
+ await expect(option4).toBeChecked();
+ });
+ });
+
+ test.describe('Real form validation shows and clears a real error', () => {
+ test('selecting the wrong option shows a real error; selecting the right one clears it', async ({
+ page
+ }) => {
+ const group = radioGroup(page, 'required-radio-group');
+ const errorEl = group.getByTestId('cps-radio-group-error');
+
+ const option1 = group.getByRole('radio', { name: 'Option 1' });
+ await option1.focus();
+ await page.keyboard.press('Space');
+ await page.evaluate(() =>
+ (document.activeElement as HTMLElement)?.blur()
+ );
+
+ await expect(errorEl).toBeVisible();
+ await expect(errorEl).toHaveText('Only third option must be selected');
+ await expect(group).toHaveAttribute(
+ 'aria-describedby',
+ (await errorEl.getAttribute('id')) ?? ''
+ );
+
+ const option3 = group.getByRole('radio', { name: 'Option 3' });
+ await option3.focus();
+ await page.keyboard.press('Space');
+ await page.evaluate(() =>
+ (document.activeElement as HTMLElement)?.blur()
+ );
+
+ await expect(errorEl).toHaveCount(0);
+ });
+ });
+
+ test.describe('Real inert blocks focus on unselected custom content', () => {
+ test('the nested control real-refuses focus until its radio is selected', async ({
+ page
+ }) => {
+ const group = example(page, 'custom-content-radio-group');
+ const customRadio = group.getByRole('radio', {
+ name: 'Custom option with inline selectors'
+ });
+ const nestedCombobox = group.getByRole('combobox', {
+ name: 'Select day'
+ });
+
+ await expect(customRadio).not.toBeChecked();
+
+ await nestedCombobox.focus();
+ await expect(nestedCombobox).not.toBeFocused();
+
+ await customRadio.click();
+ await expect(customRadio).toBeChecked();
+
+ await nestedCombobox.focus();
+ await expect(nestedCombobox).toBeFocused();
+ });
+ });
+
+ test.describe('Real focus/blur forwarding from custom-content cps-radio', () => {
+ test('focusing then blurring a custom-content radio without selecting shows a real required error', async ({
+ page
+ }) => {
+ const group = radioGroup(page, 'custom-content-radio-group');
+ const errorEl = group.getByTestId('cps-radio-group-error');
+
+ const simpleOption = group.getByRole('radio', { name: 'Simple option' });
+ await simpleOption.focus();
+ await page.evaluate(() =>
+ (document.activeElement as HTMLElement)?.blur()
+ );
+
+ await expect(errorEl).toBeVisible();
+ await expect(errorEl).toHaveText('Field is required');
+ await expect(group).toHaveAttribute('aria-invalid', 'true');
+ });
+ });
+
+ test.describe('Real hideDetails suppresses both a real hint and a real validation error', () => {
+ test('a hint stays real-absent untouched, and a real error stays real-absent once invalid', async ({
+ page
+ }) => {
+ const group = radioGroup(page, 'required-hidden-radio-group');
+
+ await expect(group.getByTestId('cps-radio-group-hint')).toHaveCount(0);
+
+ const option1 = group.getByRole('radio', { name: 'Option 1' });
+ await option1.focus();
+ await page.keyboard.press('Space');
+ await page.evaluate(() =>
+ (document.activeElement as HTMLElement)?.blur()
+ );
+
+ await expect(group).toHaveAttribute('aria-invalid', 'true');
+ await expect(group).not.toHaveAttribute('aria-describedby', /.+/);
+ await expect(group.getByTestId('cps-radio-group-error')).toHaveCount(0);
+ });
+ });
+});
diff --git a/projects/composition/src/app/pages/radio-page/radio-page.component.html b/projects/composition/src/app/pages/radio-page/radio-page.component.html
index 70fde3e68..d4279077d 100644
--- a/projects/composition/src/app/pages/radio-page/radio-page.component.html
+++ b/projects/composition/src/app/pages/radio-page/radio-page.component.html
@@ -6,6 +6,7 @@
[tsCode]="examples.requiredRadioGroup.ts">
+
+
+
+
+
+
+
+
+
-
-
-
- On the
-
-
- of every
-
-
- month(s) at
-
-
- :
-
-
-
-
-
-
-
+
diff --git a/projects/composition/src/app/pages/radio-page/radio-page.component.ts b/projects/composition/src/app/pages/radio-page/radio-page.component.ts
index f70572799..556680524 100644
--- a/projects/composition/src/app/pages/radio-page/radio-page.component.ts
+++ b/projects/composition/src/app/pages/radio-page/radio-page.component.ts
@@ -1,4 +1,4 @@
-import { Component, OnInit } from '@angular/core';
+import { Component, inject, OnInit } from '@angular/core';
import {
AbstractControl,
FormsModule,
@@ -88,19 +88,18 @@ export class RadioPageComponent implements OnInit {
componentData = ComponentData;
- // eslint-disable-next-line no-useless-constructor
- constructor(private _formBuilder: UntypedFormBuilder) {}
+ private readonly _formBuilder = inject(UntypedFormBuilder);
ngOnInit() {
+ const requiredThirdValidators = [
+ Validators.required,
+ (control: AbstractControl): ValidationErrors | null =>
+ this._checkThirdSelected(control)
+ ];
this.form = this._formBuilder.group({
- requiredRadio: [
- '',
- [
- Validators.required,
- (control: AbstractControl): ValidationErrors | null =>
- this._checkThirdSelected(control)
- ]
- ]
+ requiredRadio: ['', requiredThirdValidators],
+ requiredRadioHidden: ['', requiredThirdValidators],
+ requiredCustomContentRadio: ['', Validators.required]
});
}
diff --git a/projects/composition/src/app/pages/radio-page/radio-page.examples.ts b/projects/composition/src/app/pages/radio-page/radio-page.examples.ts
index 85bc6fde3..92632496d 100644
--- a/projects/composition/src/app/pages/radio-page/radio-page.examples.ts
+++ b/projects/composition/src/app/pages/radio-page/radio-page.examples.ts
@@ -35,6 +35,48 @@ ngOnInit() {
});
}
+private _checkThirdSelected(control: AbstractControl): ValidationErrors | null {
+ const val = control.value;
+ if (!val) return null;
+
+ if (val !== 'third') {
+ return { mustSelectThird: 'Only third option must be selected' };
+ }
+ return null;
+}`
+ },
+
+ requiredHiddenRadioGroup: {
+ html: `
+`,
+ ts: `
+private readonly _formBuilder = inject(UntypedFormBuilder);
+
+${radioOptionsTs.trim()}
+
+form!: UntypedFormGroup;
+
+ngOnInit() {
+ this.form = this._formBuilder.group({
+ requiredRadioHidden: [
+ '',
+ [
+ Validators.required,
+ (control: AbstractControl): ValidationErrors | null =>
+ this._checkThirdSelected(control)
+ ]
+ ]
+ });
+}
+
private _checkThirdSelected(control: AbstractControl): ValidationErrors | null {
const val = control.value;
if (!val) return null;
@@ -105,6 +147,16 @@ partiallyDisabledOptions: CpsRadioOption[] = [
];`
},
+ hintRadioGroup: {
+ html: `
+
+`,
+ ts: radioOptionsTs
+ },
+
twoWayBindingRadioGroup: {
html: `
@@ -123,57 +175,63 @@ syncVal = 'first';`
customContentRadioGroup: {
html: `
-
-
-
- On the
-
-
- of every
-
-
- month(s) at
-
-
- :
-
-
-
-
-
-
-`,
+
`,
ts: `
+private readonly _formBuilder = inject(UntypedFormBuilder);
+
+form!: UntypedFormGroup;
+
dayOptions = [
{ name: '1st day', data: { code: '1' } },
{ name: '2nd day', data: { code: '2' } },
@@ -193,6 +251,12 @@ hourOptions = [...Array(24).keys()].map((n) => ({
minuteOptions = [...Array(60).keys()].map((n) => ({
name: n,
data: { code: n }
-}));`
+}));
+
+ngOnInit() {
+ this.form = this._formBuilder.group({
+ requiredCustomContentRadio: ['', Validators.required]
+ });
+}`
}
};
diff --git a/projects/cps-ui-kit/src/lib/components/cps-radio-group/cps-radio-button/cps-radio-button.component.html b/projects/cps-ui-kit/src/lib/components/cps-radio-group/cps-radio-button/cps-radio-button.component.html
index 451efc2f4..abb5bc63a 100644
--- a/projects/cps-ui-kit/src/lib/components/cps-radio-group/cps-radio-button/cps-radio-button.component.html
+++ b/projects/cps-ui-kit/src/lib/components/cps-radio-group/cps-radio-button/cps-radio-button.component.html
@@ -1,6 +1,7 @@
@if (option.tooltip) {
@@ -13,7 +14,7 @@
">
} @else {
-
+
@if (!contentRef.innerHTML.trim() && option.label) {