From 12f2fc94c979de21d0bfbbe1ec1de384ed3e7a7a Mon Sep 17 00:00:00 2001 From: Nikki Massaro Date: Wed, 24 Sep 2025 15:35:24 -0400 Subject: [PATCH 01/39] feat(label): init field label mixin feat(label): init field label mixin --- packages/field-label/src/FieldLabelMixin.ts | 75 +++++++++++++++++++++ packages/textfield/package.json | 1 + packages/textfield/src/Textfield.ts | 25 ++++--- yarn.lock | 1 + 4 files changed, 91 insertions(+), 11 deletions(-) create mode 100644 packages/field-label/src/FieldLabelMixin.ts diff --git a/packages/field-label/src/FieldLabelMixin.ts b/packages/field-label/src/FieldLabelMixin.ts new file mode 100644 index 00000000000..0377beead17 --- /dev/null +++ b/packages/field-label/src/FieldLabelMixin.ts @@ -0,0 +1,75 @@ +/** + * Copyright 2025 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ + +import { + CSSResultArray, + html, + nothing, + SpectrumElement, + TemplateResult, +} from '@spectrum-web-components/base'; +import { property } from '@spectrum-web-components/base/src/decorators.js'; +import '@spectrum-web-components/icons-ui/icons/sp-icon-asterisk100.js'; + +import styles from './field-label.css.js'; +import asteriskIconStyles from '@spectrum-web-components/icon/src/spectrum-icon-asterisk.css.js'; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +type Constructor = new (...args: any[]) => T; + +export declare class FieldLabelMixinInterface { + disabled: boolean; + required: boolean; + sideAligned: 'start' | 'end'; + protected renderFieldLabel(fieldId: string): TemplateResult; +} + +/** + * @mixin FieldLabelMixin + * + * @slot field-label - Text content of the label. + */ +export const FieldLabelMixin = >( + superClass: T +) => { + class FieldLabelMixinClass extends superClass { + static styles(): CSSResultArray { + return [styles, asteriskIconStyles]; + } + + @property({ type: Boolean, reflect: true }) + public disabled = false; + + @property({ type: Boolean, reflect: true }) + public required = false; + + @property({ type: String, reflect: true, attribute: 'side-aligned' }) + public sideAligned?: 'start' | 'end'; + + protected renderFieldLabel(fieldId: string = ''): TemplateResult { + return html` + + `; + } + } + return FieldLabelMixinClass as unknown as Constructor & + T; +}; diff --git a/packages/textfield/package.json b/packages/textfield/package.json index 42fd8eaebf8..970a42805ff 100644 --- a/packages/textfield/package.json +++ b/packages/textfield/package.json @@ -65,6 +65,7 @@ ], "dependencies": { "@spectrum-web-components/base": "1.8.0", + "@spectrum-web-components/field-label": "1.8.0", "@spectrum-web-components/help-text": "1.8.0", "@spectrum-web-components/icon": "1.8.0", "@spectrum-web-components/icons-ui": "1.8.0", diff --git a/packages/textfield/src/Textfield.ts b/packages/textfield/src/Textfield.ts index 7983c9b1b6a..e668d8d39b5 100644 --- a/packages/textfield/src/Textfield.ts +++ b/packages/textfield/src/Textfield.ts @@ -29,6 +29,7 @@ import { } from '@spectrum-web-components/base/src/decorators.js'; import { ManageHelpText } from '@spectrum-web-components/help-text/src/manage-help-text.js'; +import { FieldLabelMixin } from '@spectrum-web-components/field-label/src/FieldLabelMixin.js'; import { Focusable } from '@spectrum-web-components/shared/src/focusable.js'; import '@spectrum-web-components/icons-ui/icons/sp-icon-checkmark100.js'; import '@spectrum-web-components/icons-workflow/icons/sp-icon-alert.js'; @@ -43,13 +44,20 @@ export type TextfieldType = (typeof textfieldTypes)[number]; * @fires input - The value of the element has changed. * @fires change - An alteration to the value of the element has been committed by the user. */ -export class TextfieldBase extends ManageHelpText( - SizedMixin(Focusable, { - noDefaultSize: true, - }) +export class TextfieldBase extends FieldLabelMixin( + ManageHelpText( + SizedMixin(Focusable, { + noDefaultSize: true, + }) + ) ) { public static override get styles(): CSSResultArray { - return [textfieldStyles, checkmarkStyles]; + const superStyles = Array.isArray(super.styles) + ? super.styles + : super.styles + ? [super.styles] + : []; + return [...superStyles, textfieldStyles, checkmarkStyles]; } @state() @@ -186,12 +194,6 @@ export class TextfieldBase extends ManageHelpText( @property({ type: Boolean, reflect: true }) public quiet = false; - /** - * Whether the form control will be found to be invalid when it holds no `value` - */ - @property({ type: Boolean, reflect: true }) - public required = false; - /** * What form of assistance should be provided when attempting to supply a value to the form control */ @@ -377,6 +379,7 @@ export class TextfieldBase extends ManageHelpText( protected override render(): TemplateResult { return html` + ${this.renderFieldLabel('textfield')}
${this.renderField()}
${this.renderHelpText(this.invalid)} `; diff --git a/yarn.lock b/yarn.lock index 1f17ecf1cad..9ab29d8fb50 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4871,6 +4871,7 @@ __metadata: resolution: "@spectrum-web-components/textfield@workspace:packages/textfield" dependencies: "@spectrum-web-components/base": "npm:1.8.0" + "@spectrum-web-components/field-label": "npm:1.8.0" "@spectrum-web-components/help-text": "npm:1.8.0" "@spectrum-web-components/icon": "npm:1.8.0" "@spectrum-web-components/icons-ui": "npm:1.8.0" From b6ea53b56855d3388c83c9b0d6711a8ad66bc8b7 Mon Sep 17 00:00:00 2001 From: Nikki Massaro Date: Wed, 24 Sep 2025 16:46:04 -0400 Subject: [PATCH 02/39] feat(textfield): updated textfiled docs to use label mixin --- packages/textfield/README.md | 70 +++++++++++------------------------- 1 file changed, 21 insertions(+), 49 deletions(-) diff --git a/packages/textfield/README.md b/packages/textfield/README.md index 1a9ea8ebf7a..c788c08a571 100644 --- a/packages/textfield/README.md +++ b/packages/textfield/README.md @@ -27,16 +27,16 @@ import { Textfield } from '@spectrum-web-components/textfield'; ### Anatomy ```html - + ``` #### Label -A text field must have a label in order to be accessible. A label can be provided either via the `label` attribute, like the previous example or with an `` element. +A text field must have a label in order to be accessible. A label can be provided either via the default slot, or via the `label` attribute, for a hidden label that can be read by assistive technology. ```html -Name - +Name + ``` #### Placeholder @@ -44,8 +44,7 @@ A text field must have a label in order to be accessible. A label can be provide Use the `placeholder` attribute to include placeholder text. **Note**: Placeholder text should not be used as a replacement for a label or help help text. ```html -Name - +Name ``` #### Help text @@ -59,8 +58,8 @@ See [help text](../help-text) for more information. ```html -Stay "Positive" - + + Stay "Positive" Tell us how you are feeling today. @@ -73,9 +72,7 @@ See [help text](../help-text) for more information. ```html -Stay "Positive" + Stay "Positive" Tell us how you're feeling today. @@ -103,12 +101,7 @@ See [help text](../help-text) for more information. ```html -Name - +Name ``` @@ -116,8 +109,7 @@ See [help text](../help-text) for more information. ```html -Name - +Name ``` @@ -125,12 +117,7 @@ See [help text](../help-text) for more information. ```html -Name - +Name ``` @@ -138,12 +125,7 @@ See [help text](../help-text) for more information. ```html -Name - +Name ``` @@ -161,14 +143,10 @@ user affordances like mobile keyboards and obscured characters: - `text` (default) ```html -Telephone - -Password - + + Telephone + +Password ``` If the `type` attribute is not specified, or if it does not match any of these values, the default type adopted is "text." @@ -178,8 +156,7 @@ If the `type` attribute is not specified, or if it does not match any of these v The quiet style works best when a clear layout (vertical stack, table, grid) assists in a user's ability to parse the element. Too many quiet components in a small space can be hard to read. ```html -Name (quietly) - +Name (quietly) ``` ### States @@ -187,16 +164,11 @@ The quiet style works best when a clear layout (vertical stack, table, grid) ass Use the `required` attribute to indicate a textfield value is required. Dictate the validity or invalidity state of the text entry with the `valid` or `invalid` attributes. ```html -Name - + + Name +
-Name - +Name ``` ### Accessibility From 6117e6bacb6846eb0518938e1ab043915f4a15eb Mon Sep 17 00:00:00 2001 From: Nikki Massaro Date: Thu, 25 Sep 2025 15:43:42 -0400 Subject: [PATCH 03/39] feat(field-label): added mixin to package exports --- packages/field-label/package.json | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/packages/field-label/package.json b/packages/field-label/package.json index d96616551b7..481e9bb4e0f 100644 --- a/packages/field-label/package.json +++ b/packages/field-label/package.json @@ -29,6 +29,10 @@ "development": "./src/FieldLabel.dev.js", "default": "./src/FieldLabel.js" }, + "./src/FieldLabelMixin.js": { + "development": "./src/FieldLabelMixin.dev.js", + "default": "./src/FieldLabelMixin.js" + }, "./src/field-label-overrides.css.js": "./src/field-label-overrides.css.js", "./src/field-label.css.js": "./src/field-label.css.js", "./src/index.js": { @@ -38,6 +42,10 @@ "./sp-field-label.js": { "development": "./sp-field-label.dev.js", "default": "./sp-field-label.js" + }, + "./sp-field-label-mixin.js": { + "development": "./sp-field-label-mixin.dev.js", + "default": "./sp-field-label-mixin.js" } }, "scripts": { From d5448f1df630a8f40e5ce1df87f10cb85b37a34e Mon Sep 17 00:00:00 2001 From: Nikki Massaro Date: Thu, 25 Sep 2025 15:45:19 -0400 Subject: [PATCH 04/39] feat(field-label): added optional slot name for rendering label slot --- packages/field-label/src/FieldLabelMixin.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/packages/field-label/src/FieldLabelMixin.ts b/packages/field-label/src/FieldLabelMixin.ts index 0377beead17..8482115bfbd 100644 --- a/packages/field-label/src/FieldLabelMixin.ts +++ b/packages/field-label/src/FieldLabelMixin.ts @@ -22,6 +22,7 @@ import '@spectrum-web-components/icons-ui/icons/sp-icon-asterisk100.js'; import styles from './field-label.css.js'; import asteriskIconStyles from '@spectrum-web-components/icon/src/spectrum-icon-asterisk.css.js'; +import { ifDefined } from '@spectrum-web-components/base/src/directives.js'; // eslint-disable-next-line @typescript-eslint/no-explicit-any type Constructor = new (...args: any[]) => T; @@ -42,7 +43,7 @@ export const FieldLabelMixin = >( superClass: T ) => { class FieldLabelMixinClass extends superClass { - static styles(): CSSResultArray { + public static get styles(): CSSResultArray { return [styles, asteriskIconStyles]; } @@ -55,10 +56,13 @@ export const FieldLabelMixin = >( @property({ type: String, reflect: true, attribute: 'side-aligned' }) public sideAligned?: 'start' | 'end'; - protected renderFieldLabel(fieldId: string = ''): TemplateResult { + protected renderFieldLabel( + fieldId: string = '', + slotName?: string + ): TemplateResult { return html`
`; export const quietAutofocus = (): TemplateResult => html` - Enter your life story... + > + Enter your life story... +
`; export const grows = (): TemplateResult => html` - Enter your life story... + > + Enter your life story... +
`; export const growsQuiet = (): TemplateResult => html` - Enter your life story... html` grows quiet placeholder="Enter your life story" - > + > + Enter your life story... + `; export const growsEmpty = (): TemplateResult => html` - - This textfield hasn't been used yet - + This textfield hasn't been used yet Even empty Textfield display correctly while waiting for content. @@ -138,16 +141,15 @@ export const growsEmpty = (): TemplateResult => html` `; export const growsWithLargeWords = (): TemplateResult => html` - - Enter your life story with very long words... - + > + Enter your life story with very long words... + `; export const readonly = (): TemplateResult => html` @@ -184,15 +186,13 @@ export const resizeControls = (): TemplateResult => html` `; export const sized = (): TemplateResult => html` - - This textfield hasn't been used yet - + This textfield hasn't been used yet Even empty Textfield display correctly while waiting for content. @@ -200,9 +200,6 @@ export const sized = (): TemplateResult => html` `; export const with5Rows = (): TemplateResult => html` - - Enter your life story with very long words... - + > + Enter your life story with very long words... + `; export const rowsDefeatsGrows = (): TemplateResult => html` - - Enter your life story with very long words... - + > + Enter your life story... + `; export const with1Row = (): TemplateResult => html` - - Enter your life story with very long words... - + > + Enter your life story... Enter your life story with very long words... + `; diff --git a/packages/textfield/stories/textfield-sizes.stories.ts b/packages/textfield/stories/textfield-sizes.stories.ts index 0180013de5e..9eaa998c753 100644 --- a/packages/textfield/stories/textfield-sizes.stories.ts +++ b/packages/textfield/stories/textfield-sizes.stories.ts @@ -27,10 +27,8 @@ const template = ({ size?: 's' | 'm' | 'l' | 'xl'; } = {}): TemplateResult => { return html` - - Enter your name - + Enter your name This is for the whole enchilada. diff --git a/packages/textfield/stories/textfield.stories.ts b/packages/textfield/stories/textfield.stories.ts index 8a8c525e917..c3ff59e0388 100644 --- a/packages/textfield/stories/textfield.stories.ts +++ b/packages/textfield/stories/textfield.stories.ts @@ -22,15 +22,21 @@ export default { export const Default = (): TemplateResult => { return html` - - + + { disabled > { export const growsOnly = (): TemplateResult => { return html` - - This Textfield has the "grows" attribute without the "multiline" - attribute - + > + This Textfield has the "grows" attribute without the "multiline" + attribute + `; }; export const quiet = (): TemplateResult => { return html` - Enter your name + > + Enter your name + `; }; export const defaultAutofocus = (): TemplateResult => { return html` - Enter your name + > + Enter your name + `; }; export const quietAutofocus = (): TemplateResult => { return html` - Enter your name + > + Enter your name + `; }; export const notRequiredWithPattern = (): TemplateResult => { return html` @@ -112,6 +123,7 @@ export const notRequiredWithPattern = (): TemplateResult => { export const allowedKeys = (): TemplateResult => { return html` @@ -155,10 +167,8 @@ export const types = (): TemplateResult => html` `; export const empty = (): TemplateResult => html` - - This textfield hasn't been used yet - + This textfield hasn't been used yet Even empty Textfield display correctly while waiting for content. @@ -166,14 +176,12 @@ export const empty = (): TemplateResult => html` `; export const sized = (): TemplateResult => html` - - This textfield hasn't been used yet - + This textfield hasn't been used yet Even empty Textfield display correctly while waiting for content. From 743d553853417ab1f249b12e5616f8417e3b632f Mon Sep 17 00:00:00 2001 From: Nikki Massaro Date: Thu, 25 Sep 2025 16:51:36 -0400 Subject: [PATCH 06/39] test(textfield): added test for slotted label --- packages/textfield/test/textfield.test.ts | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/packages/textfield/test/textfield.test.ts b/packages/textfield/test/textfield.test.ts index 324ea1ea6d5..3ec9d8d329d 100644 --- a/packages/textfield/test/textfield.test.ts +++ b/packages/textfield/test/textfield.test.ts @@ -51,6 +51,21 @@ describe('Textfield', () => { await expect(el).to.be.accessible(); }); + testForLitDevWarnings( + async () => + await litFixture(html` + + `) + ); + it('loads textfield with slotted label accessibly', async () => { + const el = await litFixture(html` + Enter Your Name + `); + + await elementUpdated(el); + + await expect(el).to.be.accessible(); + }); it('manages tabIndex while disabled', async () => { const el = await litFixture(html` From c386f09a55a594fde9c7728b6b716e04b7bb7785 Mon Sep 17 00:00:00 2001 From: Nikki Massaro Date: Mon, 29 Sep 2025 13:58:15 -0400 Subject: [PATCH 07/39] feat(textfield): added default slot observer for label changes --- packages/textfield/src/Textfield.ts | 61 ++++++++++++++++++++++------- 1 file changed, 46 insertions(+), 15 deletions(-) diff --git a/packages/textfield/src/Textfield.ts b/packages/textfield/src/Textfield.ts index e668d8d39b5..a6310b8d731 100644 --- a/packages/textfield/src/Textfield.ts +++ b/packages/textfield/src/Textfield.ts @@ -34,6 +34,8 @@ import { Focusable } from '@spectrum-web-components/shared/src/focusable.js'; import '@spectrum-web-components/icons-ui/icons/sp-icon-checkmark100.js'; import '@spectrum-web-components/icons-workflow/icons/sp-icon-alert.js'; +import { ObserveSlotText } from '@spectrum-web-components/shared/src/observe-slot-text.js'; + import textfieldStyles from './textfield.css.js'; import checkmarkStyles from '@spectrum-web-components/icon/src/spectrum-icon-checkmark.css.js'; @@ -44,12 +46,15 @@ export type TextfieldType = (typeof textfieldTypes)[number]; * @fires input - The value of the element has changed. * @fires change - An alteration to the value of the element has been committed by the user. */ -export class TextfieldBase extends FieldLabelMixin( - ManageHelpText( - SizedMixin(Focusable, { - noDefaultSize: true, - }) - ) +export class TextfieldBase extends ObserveSlotText( + FieldLabelMixin( + ManageHelpText( + SizedMixin(Focusable, { + noDefaultSize: true, + }) + ) + ), + '' ) { public static override get styles(): CSSResultArray { const superStyles = Array.isArray(super.styles) @@ -294,6 +299,32 @@ export class TextfieldBase extends FieldLabelMixin( return this.value.toString(); } + protected get _ariaLabel(): string | undefined { + if (this.label && this.label.length > 0) { + return this.label; + } else if (this.appliedLabel && this.appliedLabel.length > 0) { + return this.appliedLabel; + } else if (this.slotHasContent) { + return undefined; + } else if (this.placeholder && this.placeholder.length > 0) { + return this.placeholder; + } else { + window.__swc.warn( + this, + ' elements needs a label:', + 'https://opensource.adobe.com/spectrum-web-components/components/textfield/#accessibility', + { + type: 'accessibility', + issues: [ + 'value supplied to the default slot, which will be displayed visually as part of the element, or', + 'value supplied to the "label" attribute, which will read by assistive technologies', + ], + } + ); + return undefined; + } + } + // prettier-ignore private get renderMultiline(): TemplateResult { return html` @@ -305,11 +336,10 @@ export class TextfieldBase extends FieldLabelMixin( : nothing}