From a773081a56162291d48241a549d3f0207a48ae46 Mon Sep 17 00:00:00 2001 From: mollykreis <20542556+mollykreis@users.noreply.github.com> Date: Tue, 1 Oct 2024 11:09:53 -0500 Subject: [PATCH 1/8] Implement error states on nimble-radio-group --- .../src/radio-group/index.ts | 12 +++++- .../src/radio-group/styles.ts | 15 +++++++ .../src/radio-group/template.ts | 42 +++++++++++++++++++ .../nimble/radio-group/radio-group.stories.ts | 21 +++++++++- 4 files changed, 87 insertions(+), 3 deletions(-) create mode 100644 packages/nimble-components/src/radio-group/template.ts diff --git a/packages/nimble-components/src/radio-group/index.ts b/packages/nimble-components/src/radio-group/index.ts index deeb9f87f0..fee994518a 100644 --- a/packages/nimble-components/src/radio-group/index.ts +++ b/packages/nimble-components/src/radio-group/index.ts @@ -1,10 +1,12 @@ import { RadioGroup as FoundationRadioGroup, - radioGroupTemplate as template, DesignSystem } from '@microsoft/fast-foundation'; import { Orientation } from '@microsoft/fast-web-utilities'; +import { attr } from '@microsoft/fast-element'; import { styles } from './styles'; +import { template } from './template'; +import type { ErrorPattern } from '../patterns/error/types'; declare global { interface HTMLElementTagNameMap { @@ -17,7 +19,13 @@ export { Orientation }; /** * A nimble-styled grouping element for radio buttons */ -export class RadioGroup extends FoundationRadioGroup {} +export class RadioGroup extends FoundationRadioGroup implements ErrorPattern { + @attr({ attribute: 'error-text' }) + public errorText?: string; + + @attr({ attribute: 'error-visible', mode: 'boolean' }) + public errorVisible = false; +} const nimbleRadioGroup = RadioGroup.compose({ baseName: 'radio-group', diff --git a/packages/nimble-components/src/radio-group/styles.ts b/packages/nimble-components/src/radio-group/styles.ts index 37d2ac8d93..0e6b82a4cc 100644 --- a/packages/nimble-components/src/radio-group/styles.ts +++ b/packages/nimble-components/src/radio-group/styles.ts @@ -4,15 +4,19 @@ import { controlLabelDisabledFontColor, controlLabelFont, controlLabelFontColor, + smallPadding, standardPadding } from '../theme-provider/design-tokens'; +import { styles as errorStyles } from '../patterns/error/styles'; export const styles = css` ${display('inline-block')} + ${errorStyles} .positioning-region { display: flex; gap: ${standardPadding}; + position: relative; } :host([orientation='vertical']) .positioning-region { @@ -23,6 +27,12 @@ export const styles = css` flex-direction: row; } + .label-container { + display: flex; + gap: ${smallPadding}; + margin-bottom: ${smallPadding}; + } + slot[name='label'] { font: ${controlLabelFont}; color: ${controlLabelFontColor}; @@ -31,4 +41,9 @@ export const styles = css` :host([disabled]) slot[name='label'] { color: ${controlLabelDisabledFontColor}; } + + .error-icon { + margin-left: auto; + margin-right: ${smallPadding}; + } `; diff --git a/packages/nimble-components/src/radio-group/template.ts b/packages/nimble-components/src/radio-group/template.ts new file mode 100644 index 0000000000..7556070f1f --- /dev/null +++ b/packages/nimble-components/src/radio-group/template.ts @@ -0,0 +1,42 @@ +import { elements, html, slotted } from '@microsoft/fast-element'; +import type { ViewTemplate } from '@microsoft/fast-element'; +import { Orientation } from '@microsoft/fast-web-utilities'; +import type { FoundationElementTemplate } from '@microsoft/fast-foundation'; +import type { RadioGroup } from '.'; +import { errorTextTemplate } from '../patterns/error/template'; +import { iconExclamationMarkTag } from '../icons/exclamation-mark'; + +/* eslint-disable @typescript-eslint/indent */ +export const template: FoundationElementTemplate> = ( + _context, + _definition +) => html` + +`; diff --git a/packages/storybook/src/nimble/radio-group/radio-group.stories.ts b/packages/storybook/src/nimble/radio-group/radio-group.stories.ts index 98825d3fb5..d9c31a51e0 100644 --- a/packages/storybook/src/nimble/radio-group/radio-group.stories.ts +++ b/packages/storybook/src/nimble/radio-group/radio-group.stories.ts @@ -8,6 +8,8 @@ import { apiCategory, createUserSelectedThemeStory, disabledDescription, + errorTextDescription, + errorVisibleDescription, slottedLabelDescription } from '../../utilities/storybook'; @@ -19,6 +21,8 @@ interface RadioGroupArgs { value: string; buttons: undefined; change: undefined; + errorVisible: boolean; + errorText: string; } interface RadioArgs { @@ -49,6 +53,9 @@ export const radioGroup: StoryObj = { ?disabled="${x => x.disabled}" name="${x => x.name}" value="${x => x.value}" + ?error-visible="${x => x.errorVisible}" + error-text="${x => x.errorText}" + style="min-width: 200px;" > <${radioTag} value="apple">Apple @@ -61,7 +68,9 @@ export const radioGroup: StoryObj = { orientation: Orientation.horizontal, disabled: false, name: 'fruit', - value: 'none' + value: 'none', + errorVisible: false, + errorText: 'Value is invalid' }, argTypes: { value: { @@ -105,6 +114,16 @@ export const radioGroup: StoryObj = { 'Event emitted when the user selects a new value in the radio group.', table: { category: apiCategory.events }, control: false + }, + errorText: { + name: 'error-text', + description: errorTextDescription, + table: { category: apiCategory.attributes } + }, + errorVisible: { + name: 'error-visible', + description: errorVisibleDescription, + table: { category: apiCategory.attributes } } } }; From 5dfaf97129547d41a36022e45809cb8078abdd28 Mon Sep 17 00:00:00 2001 From: mollykreis <20542556+mollykreis@users.noreply.github.com> Date: Tue, 1 Oct 2024 11:50:44 -0500 Subject: [PATCH 2/8] Update radio-group-matrix.stories.ts --- .../radio-group/radio-group-matrix.stories.ts | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/packages/storybook/src/nimble/radio-group/radio-group-matrix.stories.ts b/packages/storybook/src/nimble/radio-group/radio-group-matrix.stories.ts index 3f43b7c573..44470ced42 100644 --- a/packages/storybook/src/nimble/radio-group/radio-group-matrix.stories.ts +++ b/packages/storybook/src/nimble/radio-group/radio-group-matrix.stories.ts @@ -8,7 +8,12 @@ import { sharedMatrixParameters, createMatrixThemeStory } from '../../utilities/matrix'; -import { disabledStates, DisabledState } from '../../utilities/states'; +import { + disabledStates, + DisabledState, + errorStates, + ErrorState +} from '../../utilities/states'; import { createStory } from '../../utilities/storybook'; import { hiddenWrapper } from '../../utilities/hidden'; import { textCustomizationWrapper } from '../../utilities/text-customization'; @@ -30,19 +35,22 @@ export default metadata; const component = ( [disabledName, disabled]: DisabledState, - [orientationName, orientation]: OrientationState + [orientationName, orientation]: OrientationState, + [errorName, errorVisible, errorText]: ErrorState ): ViewTemplate => html`<${radioGroupTag} orientation="${() => orientation}" ?disabled="${() => disabled}" + ?error-visible="${() => errorVisible}" + error-text="${() => errorText}" value="1" > - + <${radioTag} value="1">Option 1 <${radioTag} value="2">Option 2 `; export const radioGroupThemeMatrix: StoryFn = createMatrixThemeStory( - createMatrix(component, [disabledStates, orientationStates]) + createMatrix(component, [disabledStates, orientationStates, errorStates]) ); export const hiddenRadioGroup: StoryFn = createStory( From 8503595c2cc7c4531cd8e1f7ff4346cec250298b Mon Sep 17 00:00:00 2001 From: mollykreis <20542556+mollykreis@users.noreply.github.com> Date: Tue, 1 Oct 2024 12:07:34 -0500 Subject: [PATCH 3/8] Create radio-group.foundation.spec.ts --- .../tests/radio-group.foundation.spec.ts | 464 ++++++++++++++++++ 1 file changed, 464 insertions(+) create mode 100644 packages/nimble-components/src/radio-group/tests/radio-group.foundation.spec.ts diff --git a/packages/nimble-components/src/radio-group/tests/radio-group.foundation.spec.ts b/packages/nimble-components/src/radio-group/tests/radio-group.foundation.spec.ts new file mode 100644 index 0000000000..79be8af773 --- /dev/null +++ b/packages/nimble-components/src/radio-group/tests/radio-group.foundation.spec.ts @@ -0,0 +1,464 @@ +// Based on tests in FAST repo: https://github.com/microsoft/fast/blob/913c27e7e8503de1f7cd50bdbc9388134f52ef5d/packages/web-components/fast-foundation/src/radio-group/radio-group.spec.ts + +import { DOM } from '@microsoft/fast-element'; +import { Radio, radioTemplate as itemTemplate } from '@microsoft/fast-foundation'; +import { Orientation } from '@microsoft/fast-web-utilities'; +import { RadioGroup } from '..'; +import { fixture } from '../../utilities/tests/fixture'; +import { template } from '../template'; +import { radioTag } from '../../radio'; + +// eslint-disable-next-line @typescript-eslint/naming-convention +const FASTRadioGroup = RadioGroup.compose({ + baseName: 'radio-group', + template +}); + +// TODO: Need to add tests for keyboard handling & focus management +describe('Radio Group', () => { + // eslint-disable-next-line @typescript-eslint/naming-convention + const FASTRadio = Radio.compose({ + baseName: 'radio', + template: itemTemplate + }); + + async function setup(): Promise<{ element: RadioGroup, connect: () => Promise, disconnect: () => Promise, parent: HTMLElement, radio1: Radio, radio2: Radio, radio3: Radio }> { + const { element, connect, disconnect, parent } = await fixture([FASTRadioGroup(), FASTRadio()]); + + const radio1 = document.createElement(radioTag); + const radio2 = document.createElement(radioTag); + const radio3 = document.createElement(radioTag); + + radio1.className = 'one'; + radio2.className = 'two'; + radio3.className = 'three'; + + element.appendChild(radio1); + element.appendChild(radio2); + element.appendChild(radio3); + + return { element, connect, disconnect, parent, radio1, radio2, radio3 }; + } + + it('should have a role of `radiogroup`', async () => { + const { element, connect, disconnect } = await setup(); + + await connect(); + + expect(element.getAttribute('role')).toEqual('radiogroup'); + + await disconnect(); + }); + + it("should set a `horizontal` class on the 'positioning-region' when an orientation of `horizontal` is provided", async () => { + const { element, connect, disconnect } = await setup(); + + element.orientation = Orientation.horizontal; + + await connect(); + + expect( + element.shadowRoot + ?.querySelector('.positioning-region') + ?.classList.contains('horizontal') + ).toBeTrue(); + + await disconnect(); + }); + + it("should set a `vertical` class on the 'positioning-region' when an orientation of `vertical` is provided", async () => { + const { element, connect, disconnect } = await setup(); + + element.orientation = Orientation.vertical; + + await connect(); + + expect( + element.shadowRoot + ?.querySelector('.positioning-region') + ?.classList.contains('vertical') + ).toBeTrue(); + + await disconnect(); + }); + + it("should set a default class on the 'positioning-region' of `horizontal` when no orientation is provided", async () => { + const { element, connect, disconnect } = await setup(); + + await connect(); + + expect( + element.shadowRoot + ?.querySelector('.positioning-region') + ?.classList.contains('horizontal') + ).toBeTrue(); + + await disconnect(); + }); + + it('should set the `aria-disabled` attribute equal to the `disabled` value', async () => { + const { element, connect, disconnect } = await setup(); + + element.disabled = true; + + await connect(); + + expect(element.getAttribute('aria-disabled')).toBe('true'); + + element.disabled = false; + + await DOM.nextUpdate(); + + expect(element.getAttribute('aria-disabled')).toBe('false'); + + await disconnect(); + }); + + it('should NOT set a default `aria-disabled` value when `disabled` is not defined', async () => { + const { element, connect, disconnect } = await setup(); + + await connect(); + + expect(element.getAttribute('aria-disabled')).toBe(null); + + await disconnect(); + }); + + it('should set all child radio elements to disabled when the`disabled` is passed', async () => { + const { element, connect, disconnect } = await setup(); + element.disabled = true; + + await connect(); + await DOM.nextUpdate(); + + expect((element.querySelector('.one')!).disabled).toBeTrue(); + expect((element.querySelector('.two')!).disabled).toBeTrue(); + expect((element.querySelector('.three')!).disabled).toBeTrue(); + + expect(element.querySelector('.one')?.getAttribute('aria-disabled')).toBe('true'); + expect(element.querySelector('.two')?.getAttribute('aria-disabled')).toBe('true'); + expect(element.querySelector('.three')?.getAttribute('aria-disabled')).toBe('true'); + + await disconnect(); + }); + + it('should set the `aria-readonly` attribute equal to the `readonly` value', async () => { + const { element, connect, disconnect } = await fixture(FASTRadioGroup()); + + element.readOnly = true; + + await connect(); + + expect(element.getAttribute('aria-readonly')).toBe('true'); + + element.readOnly = false; + + await DOM.nextUpdate(); + + expect(element.getAttribute('aria-readonly')).toBe('false'); + + await disconnect(); + }); + + it('should NOT set a default `aria-readonly` value when `readonly` is not defined', async () => { + const { element, connect, disconnect } = await fixture(FASTRadioGroup()); + + await connect(); + + expect(element.getAttribute('aria-readonly')).toBe(null); + + await disconnect(); + }); + + it('should set all child radio elements to readonly when the`readonly` is passed', async () => { + const { element, connect, disconnect } = await setup(); + element.readOnly = true; + + await connect(); + await DOM.nextUpdate(); + + expect((element.querySelector('.one')!).readOnly).toBeTrue(); + expect((element.querySelector('.two')!).readOnly).toBeTrue(); + expect((element.querySelector('.three')!).readOnly).toBeTrue(); + + expect(element.querySelector('.one')?.getAttribute('aria-readonly')).toBe('true'); + expect(element.querySelector('.two')?.getAttribute('aria-readonly')).toBe('true'); + expect(element.querySelector('.three')?.getAttribute('aria-readonly')).toBe('true'); + + await disconnect(); + }); + + it('should set tabindex of 0 to a child radio with a matching `value`', async () => { + const { element, connect, disconnect } = await fixture([FASTRadioGroup(), FASTRadio()]); + + element.value = 'baz'; + + const radio1 = document.createElement(radioTag); + const radio2 = document.createElement(radioTag); + const radio3 = document.createElement(radioTag); + + radio1.className = 'one'; + radio2.className = 'two'; + radio3.className = 'three'; + + radio1.value = 'foo'; + radio2.value = 'bar'; + radio3.value = 'baz'; + + element.appendChild(radio1); + element.appendChild(radio2); + element.appendChild(radio3); + + await connect(); + await DOM.nextUpdate(); + + expect( + element.querySelectorAll(radioTag)[2]!.getAttribute('tabindex') + ).toBe('0'); + + await disconnect(); + }); + + it('should NOT set tabindex of 0 to a child radio if its value does not match the radiogroup `value`', async () => { + const { element, connect, disconnect } = await fixture([FASTRadioGroup(), FASTRadio()]); + + element.value = 'baz'; + + const radio1 = document.createElement(radioTag); + const radio2 = document.createElement(radioTag); + const radio3 = document.createElement(radioTag); + + radio1.className = 'one'; + radio2.className = 'two'; + radio3.className = 'three'; + + radio1.value = 'foo'; + radio2.value = 'bar'; + radio3.value = 'baz'; + + element.appendChild(radio1); + element.appendChild(radio2); + element.appendChild(radio3); + + await connect(); + await DOM.nextUpdate(); + + expect( + element.querySelectorAll(radioTag)[0]!.getAttribute('tabindex') + ).toBe('-1'); + expect( + element.querySelectorAll(radioTag)[1]!.getAttribute('tabindex') + ).toBe('-1'); + + await disconnect(); + }); + + it('should set a child radio with a matching `value` to `checked`', async () => { + const { element, connect, disconnect } = await fixture([FASTRadioGroup(), FASTRadio()]); + + element.value = 'baz'; + + const radio1 = document.createElement(radioTag); + const radio2 = document.createElement(radioTag); + const radio3 = document.createElement(radioTag); + + radio1.className = 'one'; + radio2.className = 'two'; + radio3.className = 'three'; + + radio1.value = 'foo'; + radio2.value = 'bar'; + radio3.value = 'baz'; + + element.appendChild(radio1); + element.appendChild(radio2); + element.appendChild(radio3); + + await connect(); + await DOM.nextUpdate(); + + expect((element.querySelectorAll(radioTag)[2]!).checked).toBeTrue(); + expect( + element.querySelectorAll(radioTag)[2]!.getAttribute('aria-checked') + ).toBe('true'); + + await disconnect(); + }); + + it('should set a child radio with a matching `value` to `checked` when value changes', async () => { + const { element, connect, disconnect } = await fixture([FASTRadioGroup(), FASTRadio()]); + + element.value = 'baz'; + + const radio1 = document.createElement(radioTag); + const radio2 = document.createElement(radioTag); + const radio3 = document.createElement(radioTag); + + radio1.className = 'one'; + radio2.className = 'two'; + radio3.className = 'three'; + + radio1.value = 'foo'; + radio2.value = 'bar'; + radio3.value = 'baz'; + + element.appendChild(radio1); + element.appendChild(radio2); + element.appendChild(radio3); + + await connect(); + await DOM.nextUpdate(); + + element.value = 'foo'; + + await DOM.nextUpdate(); + + expect((element.querySelectorAll(radioTag)[0]!).checked).toBeTrue(); + expect( + element.querySelectorAll(radioTag)[0]!.getAttribute('aria-checked') + ).toBe('true'); + + await disconnect(); + }); + + it('should mark the last radio defaulted to checked as checked, the rest should not be checked', async () => { + const { element, connect, disconnect } = await fixture([FASTRadioGroup(), FASTRadio()]); + + const radio1 = document.createElement(radioTag); + const radio2 = document.createElement(radioTag); + const radio3 = document.createElement(radioTag); + + radio1.className = 'one'; + radio2.className = 'two'; + radio3.className = 'three'; + + radio1.value = 'foo'; + radio2.value = 'bar'; + radio3.value = 'baz'; + + radio2.setAttribute('checked', ''); + radio3.setAttribute('checked', ''); + + element.appendChild(radio1); + element.appendChild(radio2); + element.appendChild(radio3); + + await connect(); + await DOM.nextUpdate(); + + const radios: NodeList = element.querySelectorAll(radioTag); + expect((radios[2] as HTMLInputElement).checked).toBeTrue(); + expect((radios[1] as HTMLInputElement).checked).toBeFalse(); + + await disconnect(); + }); + + it('should mark radio matching value on radio-group over any checked attributes', async () => { + const { element, connect, disconnect } = await fixture([FASTRadioGroup(), FASTRadio()]); + + element.value = 'bar'; + + const radio1 = document.createElement(radioTag); + const radio2 = document.createElement(radioTag); + const radio3 = document.createElement(radioTag); + + radio1.className = 'one'; + radio2.className = 'two'; + radio3.className = 'three'; + + radio1.value = 'foo'; + radio2.value = 'bar'; + radio3.value = 'baz'; + + radio2.setAttribute('checked', ''); + radio3.setAttribute('checked', ''); + + element.appendChild(radio1); + element.appendChild(radio2); + element.appendChild(radio3); + + await connect(); + await DOM.nextUpdate(); + + const radios: NodeList = element.querySelectorAll(radioTag); + expect((radios[1] as HTMLInputElement).checked).toBeTrue(); + + // radio-group explicitly sets non-matching radio's checked to false if a value match was found, + // but the attribute should still persist. + expect((radios[2] as HTMLInputElement).hasAttribute('checked')).toBeTrue(); + expect((radios[2] as HTMLInputElement).checked).toBeFalse(); + + await disconnect(); + }); + + it('should NOT set a child radio to `checked` if its value does not match the radiogroup `value`', async () => { + const { element, connect, disconnect } = await fixture([FASTRadioGroup(), FASTRadio()]); + + element.value = 'baz'; + + const radio1 = document.createElement(radioTag); + const radio2 = document.createElement(radioTag); + const radio3 = document.createElement(radioTag); + + radio1.className = 'one'; + radio2.className = 'two'; + radio3.className = 'three'; + + radio1.value = 'foo'; + radio2.value = 'bar'; + radio3.value = 'baz'; + + element.appendChild(radio1); + element.appendChild(radio2); + element.appendChild(radio3); + + await connect(); + await DOM.nextUpdate(); + + expect((element.querySelectorAll(radioTag)[0]!).checked).toBeFalse(); + expect( + element.querySelectorAll(radioTag)[0]!.getAttribute('aria-checked') + ).toBe('false'); + + expect((element.querySelectorAll(radioTag)[1]!).checked).toBeFalse(); + expect( + element.querySelectorAll(radioTag)[1]!.getAttribute('aria-checked') + ).toBe('false'); + + await disconnect(); + }); + + it('should allow resetting of elements by the parent form', async () => { + const { + element, + connect, + disconnect, + parent, + radio1, + radio2, + radio3, + } = await setup(); + + radio2.setAttribute('checked', ''); + + const form = document.createElement('form'); + form.appendChild(element); + parent.appendChild(form); + + await connect(); + + radio1.checked = true; + + expect(!!radio1.checked).toBeTrue(); + expect(!!radio2.checked).toBeFalse(); + expect(!!radio3.checked).toBeFalse(); + + form.reset(); + + expect(!!radio1.checked).toBeFalse(); + expect(!!radio2.checked).toBeTrue(); + expect(!!radio3.checked).toBeFalse(); + + await disconnect(); + }); +}); \ No newline at end of file From dfb6d257b2b410d5505bfeac682d9061bd48312a Mon Sep 17 00:00:00 2001 From: mollykreis <20542556+mollykreis@users.noreply.github.com> Date: Tue, 1 Oct 2024 12:09:19 -0500 Subject: [PATCH 4/8] Change files --- ...le-components-e5bfd621-8ece-4d9c-a6ea-eb128a70e8ac.json | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 change/@ni-nimble-components-e5bfd621-8ece-4d9c-a6ea-eb128a70e8ac.json diff --git a/change/@ni-nimble-components-e5bfd621-8ece-4d9c-a6ea-eb128a70e8ac.json b/change/@ni-nimble-components-e5bfd621-8ece-4d9c-a6ea-eb128a70e8ac.json new file mode 100644 index 0000000000..5d44c0b333 --- /dev/null +++ b/change/@ni-nimble-components-e5bfd621-8ece-4d9c-a6ea-eb128a70e8ac.json @@ -0,0 +1,7 @@ +{ + "type": "minor", + "comment": "Implement error states on nimble-radio-group", + "packageName": "@ni/nimble-components", + "email": "20542556+mollykreis@users.noreply.github.com", + "dependentChangeType": "patch" +} From 97fc13a99fc2f6d03d0a9735b7ba1c3650742c41 Mon Sep 17 00:00:00 2001 From: mollykreis <20542556+mollykreis@users.noreply.github.com> Date: Tue, 1 Oct 2024 12:12:47 -0500 Subject: [PATCH 5/8] format --- .../src/radio-group/styles.ts | 2 +- .../src/radio-group/template.ts | 2 +- .../tests/radio-group.foundation.spec.ts | 115 ++++++++++++------ 3 files changed, 80 insertions(+), 39 deletions(-) diff --git a/packages/nimble-components/src/radio-group/styles.ts b/packages/nimble-components/src/radio-group/styles.ts index 0e6b82a4cc..3fc6f5f5a0 100644 --- a/packages/nimble-components/src/radio-group/styles.ts +++ b/packages/nimble-components/src/radio-group/styles.ts @@ -41,7 +41,7 @@ export const styles = css` :host([disabled]) slot[name='label'] { color: ${controlLabelDisabledFontColor}; } - + .error-icon { margin-left: auto; margin-right: ${smallPadding}; diff --git a/packages/nimble-components/src/radio-group/template.ts b/packages/nimble-components/src/radio-group/template.ts index 7556070f1f..507b15760a 100644 --- a/packages/nimble-components/src/radio-group/template.ts +++ b/packages/nimble-components/src/radio-group/template.ts @@ -33,7 +33,7 @@ export const template: FoundationElementTemplate> = ( ${errorTextTemplate} diff --git a/packages/nimble-components/src/radio-group/tests/radio-group.foundation.spec.ts b/packages/nimble-components/src/radio-group/tests/radio-group.foundation.spec.ts index 79be8af773..cdf0d1b8eb 100644 --- a/packages/nimble-components/src/radio-group/tests/radio-group.foundation.spec.ts +++ b/packages/nimble-components/src/radio-group/tests/radio-group.foundation.spec.ts @@ -1,7 +1,10 @@ // Based on tests in FAST repo: https://github.com/microsoft/fast/blob/913c27e7e8503de1f7cd50bdbc9388134f52ef5d/packages/web-components/fast-foundation/src/radio-group/radio-group.spec.ts import { DOM } from '@microsoft/fast-element'; -import { Radio, radioTemplate as itemTemplate } from '@microsoft/fast-foundation'; +import { + Radio, + radioTemplate as itemTemplate +} from '@microsoft/fast-foundation'; import { Orientation } from '@microsoft/fast-web-utilities'; import { RadioGroup } from '..'; import { fixture } from '../../utilities/tests/fixture'; @@ -22,8 +25,19 @@ describe('Radio Group', () => { template: itemTemplate }); - async function setup(): Promise<{ element: RadioGroup, connect: () => Promise, disconnect: () => Promise, parent: HTMLElement, radio1: Radio, radio2: Radio, radio3: Radio }> { - const { element, connect, disconnect, parent } = await fixture([FASTRadioGroup(), FASTRadio()]); + async function setup(): Promise<{ + element: RadioGroup, + connect: () => Promise, + disconnect: () => Promise, + parent: HTMLElement, + radio1: Radio, + radio2: Radio, + radio3: Radio + }> { + const { element, connect, disconnect, parent } = await fixture([ + FASTRadioGroup(), + FASTRadio() + ]); const radio1 = document.createElement(radioTag); const radio2 = document.createElement(radioTag); @@ -131,13 +145,19 @@ describe('Radio Group', () => { await connect(); await DOM.nextUpdate(); - expect((element.querySelector('.one')!).disabled).toBeTrue(); - expect((element.querySelector('.two')!).disabled).toBeTrue(); - expect((element.querySelector('.three')!).disabled).toBeTrue(); + expect(element.querySelector('.one')!.disabled).toBeTrue(); + expect(element.querySelector('.two')!.disabled).toBeTrue(); + expect(element.querySelector('.three')!.disabled).toBeTrue(); - expect(element.querySelector('.one')?.getAttribute('aria-disabled')).toBe('true'); - expect(element.querySelector('.two')?.getAttribute('aria-disabled')).toBe('true'); - expect(element.querySelector('.three')?.getAttribute('aria-disabled')).toBe('true'); + expect( + element.querySelector('.one')?.getAttribute('aria-disabled') + ).toBe('true'); + expect( + element.querySelector('.two')?.getAttribute('aria-disabled') + ).toBe('true'); + expect( + element.querySelector('.three')?.getAttribute('aria-disabled') + ).toBe('true'); await disconnect(); }); @@ -177,19 +197,28 @@ describe('Radio Group', () => { await connect(); await DOM.nextUpdate(); - expect((element.querySelector('.one')!).readOnly).toBeTrue(); - expect((element.querySelector('.two')!).readOnly).toBeTrue(); - expect((element.querySelector('.three')!).readOnly).toBeTrue(); + expect(element.querySelector('.one')!.readOnly).toBeTrue(); + expect(element.querySelector('.two')!.readOnly).toBeTrue(); + expect(element.querySelector('.three')!.readOnly).toBeTrue(); - expect(element.querySelector('.one')?.getAttribute('aria-readonly')).toBe('true'); - expect(element.querySelector('.two')?.getAttribute('aria-readonly')).toBe('true'); - expect(element.querySelector('.three')?.getAttribute('aria-readonly')).toBe('true'); + expect( + element.querySelector('.one')?.getAttribute('aria-readonly') + ).toBe('true'); + expect( + element.querySelector('.two')?.getAttribute('aria-readonly') + ).toBe('true'); + expect( + element.querySelector('.three')?.getAttribute('aria-readonly') + ).toBe('true'); await disconnect(); }); it('should set tabindex of 0 to a child radio with a matching `value`', async () => { - const { element, connect, disconnect } = await fixture([FASTRadioGroup(), FASTRadio()]); + const { element, connect, disconnect } = await fixture([ + FASTRadioGroup(), + FASTRadio() + ]); element.value = 'baz'; @@ -220,7 +249,10 @@ describe('Radio Group', () => { }); it('should NOT set tabindex of 0 to a child radio if its value does not match the radiogroup `value`', async () => { - const { element, connect, disconnect } = await fixture([FASTRadioGroup(), FASTRadio()]); + const { element, connect, disconnect } = await fixture([ + FASTRadioGroup(), + FASTRadio() + ]); element.value = 'baz'; @@ -254,7 +286,10 @@ describe('Radio Group', () => { }); it('should set a child radio with a matching `value` to `checked`', async () => { - const { element, connect, disconnect } = await fixture([FASTRadioGroup(), FASTRadio()]); + const { element, connect, disconnect } = await fixture([ + FASTRadioGroup(), + FASTRadio() + ]); element.value = 'baz'; @@ -277,7 +312,7 @@ describe('Radio Group', () => { await connect(); await DOM.nextUpdate(); - expect((element.querySelectorAll(radioTag)[2]!).checked).toBeTrue(); + expect(element.querySelectorAll(radioTag)[2]!.checked).toBeTrue(); expect( element.querySelectorAll(radioTag)[2]!.getAttribute('aria-checked') ).toBe('true'); @@ -286,7 +321,10 @@ describe('Radio Group', () => { }); it('should set a child radio with a matching `value` to `checked` when value changes', async () => { - const { element, connect, disconnect } = await fixture([FASTRadioGroup(), FASTRadio()]); + const { element, connect, disconnect } = await fixture([ + FASTRadioGroup(), + FASTRadio() + ]); element.value = 'baz'; @@ -313,7 +351,7 @@ describe('Radio Group', () => { await DOM.nextUpdate(); - expect((element.querySelectorAll(radioTag)[0]!).checked).toBeTrue(); + expect(element.querySelectorAll(radioTag)[0]!.checked).toBeTrue(); expect( element.querySelectorAll(radioTag)[0]!.getAttribute('aria-checked') ).toBe('true'); @@ -322,7 +360,10 @@ describe('Radio Group', () => { }); it('should mark the last radio defaulted to checked as checked, the rest should not be checked', async () => { - const { element, connect, disconnect } = await fixture([FASTRadioGroup(), FASTRadio()]); + const { element, connect, disconnect } = await fixture([ + FASTRadioGroup(), + FASTRadio() + ]); const radio1 = document.createElement(radioTag); const radio2 = document.createElement(radioTag); @@ -354,7 +395,10 @@ describe('Radio Group', () => { }); it('should mark radio matching value on radio-group over any checked attributes', async () => { - const { element, connect, disconnect } = await fixture([FASTRadioGroup(), FASTRadio()]); + const { element, connect, disconnect } = await fixture([ + FASTRadioGroup(), + FASTRadio() + ]); element.value = 'bar'; @@ -385,14 +429,19 @@ describe('Radio Group', () => { // radio-group explicitly sets non-matching radio's checked to false if a value match was found, // but the attribute should still persist. - expect((radios[2] as HTMLInputElement).hasAttribute('checked')).toBeTrue(); + expect( + (radios[2] as HTMLInputElement).hasAttribute('checked') + ).toBeTrue(); expect((radios[2] as HTMLInputElement).checked).toBeFalse(); await disconnect(); }); it('should NOT set a child radio to `checked` if its value does not match the radiogroup `value`', async () => { - const { element, connect, disconnect } = await fixture([FASTRadioGroup(), FASTRadio()]); + const { element, connect, disconnect } = await fixture([ + FASTRadioGroup(), + FASTRadio() + ]); element.value = 'baz'; @@ -415,12 +464,12 @@ describe('Radio Group', () => { await connect(); await DOM.nextUpdate(); - expect((element.querySelectorAll(radioTag)[0]!).checked).toBeFalse(); + expect(element.querySelectorAll(radioTag)[0]!.checked).toBeFalse(); expect( element.querySelectorAll(radioTag)[0]!.getAttribute('aria-checked') ).toBe('false'); - expect((element.querySelectorAll(radioTag)[1]!).checked).toBeFalse(); + expect(element.querySelectorAll(radioTag)[1]!.checked).toBeFalse(); expect( element.querySelectorAll(radioTag)[1]!.getAttribute('aria-checked') ).toBe('false'); @@ -429,15 +478,7 @@ describe('Radio Group', () => { }); it('should allow resetting of elements by the parent form', async () => { - const { - element, - connect, - disconnect, - parent, - radio1, - radio2, - radio3, - } = await setup(); + const { element, connect, disconnect, parent, radio1, radio2, radio3 } = await setup(); radio2.setAttribute('checked', ''); @@ -461,4 +502,4 @@ describe('Radio Group', () => { await disconnect(); }); -}); \ No newline at end of file +}); From cf3290e0554977af92df868cd0bc04fded02beb2 Mon Sep 17 00:00:00 2001 From: mollykreis <20542556+mollykreis@users.noreply.github.com> Date: Tue, 1 Oct 2024 13:03:52 -0500 Subject: [PATCH 6/8] Update radio-group-matrix.stories.ts --- .../src/nimble/radio-group/radio-group-matrix.stories.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/storybook/src/nimble/radio-group/radio-group-matrix.stories.ts b/packages/storybook/src/nimble/radio-group/radio-group-matrix.stories.ts index 44470ced42..ce58d37df9 100644 --- a/packages/storybook/src/nimble/radio-group/radio-group-matrix.stories.ts +++ b/packages/storybook/src/nimble/radio-group/radio-group-matrix.stories.ts @@ -1,6 +1,7 @@ import type { StoryFn, Meta } from '@storybook/html'; import { html, ViewTemplate } from '@microsoft/fast-element'; import { Orientation } from '@microsoft/fast-web-utilities'; +import { standardPadding } from '../../../../nimble-components/src/theme-provider/design-tokens'; import { radioTag } from '../../../../nimble-components/src/radio'; import { radioGroupTag } from '../../../../nimble-components/src/radio-group'; import { @@ -43,6 +44,7 @@ const component = ( ?error-visible="${() => errorVisible}" error-text="${() => errorText}" value="1" + style="width: 250px; margin: var(${standardPadding.cssCustomProperty});" > <${radioTag} value="1">Option 1 From e472b67b5231be77923b5fdbdc7da0fef9949e23 Mon Sep 17 00:00:00 2001 From: mollykreis <20542556+mollykreis@users.noreply.github.com> Date: Tue, 1 Oct 2024 14:59:00 -0500 Subject: [PATCH 7/8] simplify template declaration --- packages/nimble-components/src/radio-group/template.ts | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/packages/nimble-components/src/radio-group/template.ts b/packages/nimble-components/src/radio-group/template.ts index 507b15760a..4d538f2490 100644 --- a/packages/nimble-components/src/radio-group/template.ts +++ b/packages/nimble-components/src/radio-group/template.ts @@ -1,16 +1,11 @@ import { elements, html, slotted } from '@microsoft/fast-element'; -import type { ViewTemplate } from '@microsoft/fast-element'; import { Orientation } from '@microsoft/fast-web-utilities'; -import type { FoundationElementTemplate } from '@microsoft/fast-foundation'; import type { RadioGroup } from '.'; import { errorTextTemplate } from '../patterns/error/template'; import { iconExclamationMarkTag } from '../icons/exclamation-mark'; /* eslint-disable @typescript-eslint/indent */ -export const template: FoundationElementTemplate> = ( - _context, - _definition -) => html` +export const template = html`