diff --git a/packages/meter/src/Meter.ts b/packages/meter/src/Meter.ts index 064209ecc2..ba1d7c4850 100644 --- a/packages/meter/src/Meter.ts +++ b/packages/meter/src/Meter.ts @@ -18,8 +18,12 @@ import { SpectrumElement, TemplateResult, } from '@spectrum-web-components/base'; -import { property } from '@spectrum-web-components/base/src/decorators.js'; +import { + property, + query, +} from '@spectrum-web-components/base/src/decorators.js'; +import { getLabelFromSlot } from '@spectrum-web-components/shared/src/get-label-from-slot.js'; import { ObserveSlotText } from '@spectrum-web-components/shared/src/observe-slot-text.js'; import { LanguageResolutionController } from '@spectrum-web-components/reactive-controllers/src/LanguageResolution.js'; import '@spectrum-web-components/field-label/sp-field-label.js'; @@ -53,6 +57,9 @@ export class Meter extends SizedMixin(ObserveSlotText(SpectrumElement, '')) { @property({ type: String, reflect: true }) public label = ''; + @query('slot') + private slotEl!: HTMLSlotElement; + private languageResolver = new LanguageResolutionController(this); @property({ type: Boolean, reflect: true, attribute: 'side-label' }) @@ -66,7 +73,7 @@ export class Meter extends SizedMixin(ObserveSlotText(SpectrumElement, '')) { return html` ${this.slotHasContent ? html`` : this.label} - ${this.label} + ${this.label} ${new Intl.NumberFormat(this.languageResolver.language, { @@ -83,6 +90,13 @@ export class Meter extends SizedMixin(ObserveSlotText(SpectrumElement, '')) { `; } + protected handleSlotchange(): void { + const labelFromSlot = getLabelFromSlot(this.label, this.slotEl); + if (labelFromSlot) { + this.label = labelFromSlot; + } + } + protected override firstUpdated(changes: PropertyValues): void { super.firstUpdated(changes); this.setAttribute('role', 'meter progressbar'); @@ -93,8 +107,12 @@ export class Meter extends SizedMixin(ObserveSlotText(SpectrumElement, '')) { if (changes.has('progress')) { this.setAttribute('aria-valuenow', '' + this.progress); } - if (this.label && changes.has('label')) { - this.setAttribute('aria-label', this.label); + if (changes.has('label')) { + if (this.label.length) { + this.setAttribute('aria-label', this.label); + } else { + this.removeAttribute('aria-label'); + } } } } diff --git a/packages/meter/test/meter.test.ts b/packages/meter/test/meter.test.ts index 766733412c..ba63f21e64 100644 --- a/packages/meter/test/meter.test.ts +++ b/packages/meter/test/meter.test.ts @@ -64,6 +64,15 @@ describe('Meter', () => { expect(el.getAttribute('aria-valuenow')).to.equal('100'); }); + it('accepts label from `slot`', async () => { + const el = await fixture(html` + Label From Slot + `); + + await elementUpdated(el); + + expect(el.getAttribute('label')).to.equal('Label From Slot'); + }); it('accepts a changing process', async () => { const el = await fixture(html` Changing Value diff --git a/packages/progress-bar/src/ProgressBar.ts b/packages/progress-bar/src/ProgressBar.ts index 9eb54605d1..8708086c35 100644 --- a/packages/progress-bar/src/ProgressBar.ts +++ b/packages/progress-bar/src/ProgressBar.ts @@ -18,8 +18,13 @@ import { SpectrumElement, TemplateResult, } from '@spectrum-web-components/base'; -import { property } from '@spectrum-web-components/base/src/decorators.js'; +import { + property, + query, +} from '@spectrum-web-components/base/src/decorators.js'; +import { getLabelFromSlot } from '@spectrum-web-components/shared/src/get-label-from-slot.js'; +import { ObserveSlotText } from '@spectrum-web-components/shared/src/observe-slot-text.js'; import { LanguageResolutionController } from '@spectrum-web-components/reactive-controllers/src/LanguageResolution.js'; import '@spectrum-web-components/field-label/sp-field-label.js'; import styles from './progress-bar.css.js'; @@ -27,7 +32,9 @@ import styles from './progress-bar.css.js'; /** * @element sp-progress-bar */ -export class ProgressBar extends SizedMixin(SpectrumElement) { +export class ProgressBar extends SizedMixin( + ObserveSlotText(SpectrumElement, '') +) { public static override get styles(): CSSResultArray { return [styles]; } @@ -35,7 +42,7 @@ export class ProgressBar extends SizedMixin(SpectrumElement) { @property({ type: Boolean, reflect: true }) public indeterminate = false; - @property({ type: String }) + @property({ type: String, reflect: true }) public label = ''; private languageResolver = new LanguageResolutionController(this); @@ -52,13 +59,23 @@ export class ProgressBar extends SizedMixin(SpectrumElement) { @property({ type: String, reflect: true }) public static: 'white' | undefined; + @query('slot') + private slotEl!: HTMLSlotElement; + protected override render(): TemplateResult { return html` - ${this.label + ${this.slotHasContent || this.label ? html` - ${this.label} + ${this.slotHasContent ? html`` : this.label} + + ${this.label} + + ` + : html``} + ${this.label + ? html` ${this.indeterminate ? html`` : html` @@ -86,6 +103,13 @@ export class ProgressBar extends SizedMixin(SpectrumElement) { `; } + protected handleSlotchange(): void { + const labelFromSlot = getLabelFromSlot(this.label, this.slotEl); + if (labelFromSlot) { + this.label = labelFromSlot; + } + } + protected override firstUpdated(changes: PropertyValues): void { super.firstUpdated(changes); if (!this.hasAttribute('role')) { @@ -99,6 +123,7 @@ export class ProgressBar extends SizedMixin(SpectrumElement) { if (this.indeterminate) { this.removeAttribute('aria-valuemin'); this.removeAttribute('aria-valuemax'); + this.removeAttribute('aria-valuenow'); } else { this.setAttribute('aria-valuemin', '0'); this.setAttribute('aria-valuemax', '100'); @@ -106,27 +131,31 @@ export class ProgressBar extends SizedMixin(SpectrumElement) { } if (!this.indeterminate && changes.has('progress')) { this.setAttribute('aria-valuenow', '' + this.progress); - } else if (this.hasAttribute('aria-valuenow')) { - this.removeAttribute('aria-valuenow'); } - if (this.label && changes.has('label')) { - this.setAttribute('aria-label', this.label); + if (changes.has('label')) { + if (this.label.length) { + this.setAttribute('aria-label', this.label); + } else { + this.removeAttribute('aria-label'); + } } if (window.__swc.DEBUG) { if ( !this.label && !this.getAttribute('aria-label') && - !this.getAttribute('aria-labelledby') + !this.getAttribute('aria-labelledby') && + !this.slotHasContent ) { window.__swc.warn( this, - ' elements will not be accessible to screen readers without one of the following:', + ' elements need one of the following to be accessible:', 'https://opensource.adobe.com/spectrum-web-components/components/progress-bar/#accessibility', { type: 'accessibility', issues: [ 'value supplied to the "label" attribute, which will be displayed visually as part of the element, or', + 'text content supplied directly to the element, or', 'value supplied to the "aria-label" attribute, which will only be provided to screen readers, or', 'an element ID reference supplied to the "aria-labelledby" attribute, which will be provided by screen readers and will need to be managed manually by the parent application.', ], diff --git a/packages/progress-bar/test/progress-bar.test.ts b/packages/progress-bar/test/progress-bar.test.ts index 9272d0d58c..b626baee9d 100644 --- a/packages/progress-bar/test/progress-bar.test.ts +++ b/packages/progress-bar/test/progress-bar.test.ts @@ -40,6 +40,17 @@ describe('ProgressBar', () => { await expect(el).to.be.accessible(); }); + it('accepts label from `slot`', async () => { + const el = await fixture(html` + + Label From Slot + + `); + + await elementUpdated(el); + + expect(el.getAttribute('label')).to.equal('Label From Slot'); + }); it('accepts a changing progress', async () => { const el = await fixture(html` diff --git a/packages/progress-circle/src/ProgressCircle.ts b/packages/progress-circle/src/ProgressCircle.ts index 6b49a90b6c..07a966607d 100644 --- a/packages/progress-circle/src/ProgressCircle.ts +++ b/packages/progress-circle/src/ProgressCircle.ts @@ -18,7 +18,11 @@ import { SpectrumElement, TemplateResult, } from '@spectrum-web-components/base'; -import { property } from '@spectrum-web-components/base/src/decorators.js'; +import { + property, + query, +} from '@spectrum-web-components/base/src/decorators.js'; +import { getLabelFromSlot } from '@spectrum-web-components/shared/src/get-label-from-slot.js'; import { ifDefined } from '@spectrum-web-components/base/src/directives.js'; import progressCircleStyles from './progress-circle.css.js'; @@ -48,6 +52,9 @@ export class ProgressCircle extends SizedMixin(SpectrumElement, { @property({ type: Number }) public progress = 0; + @query('slot') + private slotEl!: HTMLSlotElement; + private makeRotation(rotation: number): string | undefined { return this.indeterminate ? undefined @@ -83,6 +90,7 @@ export class ProgressCircle extends SizedMixin(SpectrumElement, { ]; const masks = ['Mask1', 'Mask2']; return html` +
${masks.map( @@ -101,6 +109,13 @@ export class ProgressCircle extends SizedMixin(SpectrumElement, { `; } + protected handleSlotchange(): void { + const labelFromSlot = getLabelFromSlot(this.label, this.slotEl); + if (labelFromSlot) { + this.label = labelFromSlot; + } + } + protected override firstUpdated(changes: PropertyValues): void { super.firstUpdated(changes); if (!this.hasAttribute('role')) { @@ -115,24 +130,30 @@ export class ProgressCircle extends SizedMixin(SpectrumElement, { } else if (this.hasAttribute('aria-valuenow')) { this.removeAttribute('aria-valuenow'); } - if (this.label && changes.has('label')) { - this.setAttribute('aria-label', this.label); + if (changes.has('label')) { + if (this.label.length) { + this.setAttribute('aria-label', this.label); + } else { + this.removeAttribute('aria-label'); + } } if (window.__swc.DEBUG) { if ( !this.label && !this.getAttribute('aria-label') && - !this.getAttribute('aria-labelledby') + !this.getAttribute('aria-labelledby') && + !this.slotEl.assignedNodes().length ) { window.__swc.warn( this, - ' elements will not be accessible to screen readers without one of the following:', + ' elements need one of the following to be accessible:', 'https://opensource.adobe.com/spectrum-web-components/components/progress-circle/#accessibility', { type: 'accessibility', issues: [ 'value supplied to the "label" attribute, which will be displayed visually as part of the element, or', + 'text content supplied directly to the element, or', 'value supplied to the "aria-label" attribute, which will only be provided to screen readers, or', 'an element ID reference supplied to the "aria-labelledby" attribute, which will be provided by screen readers and will need to be managed manually by the parent application.', ], diff --git a/packages/progress-circle/src/progress-circle.css b/packages/progress-circle/src/progress-circle.css index 65e617662d..6b1e7090cb 100644 --- a/packages/progress-circle/src/progress-circle.css +++ b/packages/progress-circle/src/progress-circle.css @@ -17,3 +17,7 @@ governing permissions and limitations under the License. --spectrum-alias-track-fill-color-overbackground ); } + +slot { + display: none; +} diff --git a/packages/progress-circle/stories/progress-circle.stories.ts b/packages/progress-circle/stories/progress-circle.stories.ts index bb76cdda47..a3a86c9735 100644 --- a/packages/progress-circle/stories/progress-circle.stories.ts +++ b/packages/progress-circle/stories/progress-circle.stories.ts @@ -47,18 +47,18 @@ export const Default = ({ indeterminate }: StoryArgs = {}): TemplateResult => { progress="27" size="s" ?indeterminate=${indeterminate} - label="Loading progess demo" + label="Loading progress demo" >
`; @@ -79,20 +79,20 @@ export const staticWhite = ({ static="white" size="s" ?indeterminate=${indeterminate} - label="Loading progess demo" + label="Loading progress demo" >
`; diff --git a/packages/progress-circle/test/progress-circle.test.ts b/packages/progress-circle/test/progress-circle.test.ts index 6c13a88f6b..a83cb2fcd6 100644 --- a/packages/progress-circle/test/progress-circle.test.ts +++ b/packages/progress-circle/test/progress-circle.test.ts @@ -47,6 +47,17 @@ describe('ProgressCircle', () => { await expect(el).to.be.accessible(); }); + it('accepts label from `slot`', async () => { + const el = await fixture(html` + + Label From Slot + + `); + + await elementUpdated(el); + + expect(el.getAttribute('aria-label')).to.equal('Label From Slot'); + }); it('accepts user `role`', async () => { const el = await fixture(html` { + if (label) return null; + const textContent = slotEl + .assignedNodes() + .reduce((accumulator: string, node: Node) => { + if (node.textContent) { + return accumulator + node.textContent; + } else { + return accumulator; + } + }, ''); + if (textContent) { + return textContent.trim(); + } else { + return null; + } +}; diff --git a/tools/shared/src/index.ts b/tools/shared/src/index.ts index 8159bfb934..f83ccacd54 100644 --- a/tools/shared/src/index.ts +++ b/tools/shared/src/index.ts @@ -19,3 +19,4 @@ export * from './observe-slot-presence.js'; export * from './observe-slot-text.js'; export * from './platform.js'; export * from './reparent-children.js'; +export * from './get-label-from-slot.js';