Skip to content

Commit f759298

Browse files
rise-erpeldingcdransf
authored andcommitted
style(progress-circle): clean up code styles (#6146)
1 parent 7998df6 commit f759298

8 files changed

Lines changed: 189 additions & 67 deletions

File tree

2nd-gen/packages/core/components/progress-circle/ProgressCircle.base.ts

Lines changed: 50 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,14 @@
1010
* governing permissions and limitations under the License.
1111
*/
1212
import { PropertyValues } from 'lit';
13-
import { property, query } from 'lit/decorators.js';
13+
import { property } from 'lit/decorators.js';
1414

1515
import {
1616
LanguageResolutionController,
1717
languageResolverUpdatedSymbol,
1818
} from '@spectrum-web-components/core/controllers/language-resolution.js';
1919
import { SpectrumElement } from '@spectrum-web-components/core/element/index.js';
2020
import { SizedMixin } from '@spectrum-web-components/core/mixins/index.js';
21-
import { getLabelFromSlot } from '@spectrum-web-components/core/utils/get-label-from-slot.js';
2221

2322
import {
2423
PROGRESS_CIRCLE_VALID_SIZES,
@@ -30,14 +29,6 @@ import {
3029
* Can be used in both determinate (with specific progress value) and indeterminate (loading) states.
3130
*
3231
* @attribute {ElementSize} size - The size of the progress circle.
33-
*
34-
* @todo Why do we support both the slot and the label attribute? Should we deprecate the slot?
35-
*
36-
* @todo Figure out why our tool chain doesn't respect the line breaks in this slot description.
37-
*
38-
* @slot - Accessible label for the progress circle.
39-
*
40-
* Used to provide context about what is loading or progressing.
4132
*/
4233
export abstract class ProgressCircleBase extends SizedMixin(SpectrumElement, {
4334
validSizes: PROGRESS_CIRCLE_VALID_SIZES,
@@ -85,6 +76,8 @@ export abstract class ProgressCircleBase extends SizedMixin(SpectrumElement, {
8576
// ──────────────────
8677

8778
/**
79+
* @todo Revisit the default API for `indeterminate` and `progress`. SWC-1891
80+
*
8881
* Whether the progress circle shows indeterminate progress (loading state).
8982
*
9083
* When true, displays an animated loading indicator instead of a specific progress value.
@@ -103,7 +96,8 @@ export abstract class ProgressCircleBase extends SizedMixin(SpectrumElement, {
10396
/**
10497
* Progress value from 0 to 100.
10598
*
106-
* Only relevant when indeterminate is false.
99+
* Only relevant when indeterminate is false. Values outside that range or
100+
* non-finite numbers are clamped to 0–100 (non-finite becomes 0).
107101
*/
108102
@property({ type: Number })
109103
public progress = 0;
@@ -114,23 +108,49 @@ export abstract class ProgressCircleBase extends SizedMixin(SpectrumElement, {
114108
// IMPLEMENTATION
115109
// ──────────────────────
116110

117-
/**
118-
* @internal
119-
*/
120-
@query('slot')
121-
private slotEl!: HTMLSlotElement;
111+
/** True when light DOM has element nodes or non-whitespace text (no default slot). */
112+
private static hasMeaningfulLightDomChildren(host: HTMLElement): boolean {
113+
for (const node of host.childNodes) {
114+
if (
115+
node.nodeType === Node.ELEMENT_NODE ||
116+
(node.nodeType === Node.TEXT_NODE && node.textContent?.trim())
117+
) {
118+
return true;
119+
}
120+
}
121+
return false;
122+
}
123+
124+
private warnDeprecatedLightDomChildren(): void {
125+
if (!window.__swc?.DEBUG) {
126+
return;
127+
}
128+
if (!ProgressCircleBase.hasMeaningfulLightDomChildren(this)) {
129+
return;
130+
}
131+
window.__swc.warn(
132+
this,
133+
`<${this.localName}> no longer has a default slot. Light DOM children are not rendered and are not used for an accessible name. Use the "label" attribute or property, or "aria-label" / "aria-labelledby" on the host instead.`,
134+
'https://opensource.adobe.com/spectrum-web-components/second-gen/?path=/docs/components-progress-circle--docs',
135+
{ level: 'deprecation' }
136+
);
137+
}
122138

123-
protected makeRotation(rotation: number): string | undefined {
124-
return this.indeterminate
125-
? undefined
126-
: `transform: rotate(${rotation}deg);`;
139+
private static clampProgress(value: number): number {
140+
if (!Number.isFinite(value)) {
141+
return 0;
142+
}
143+
return Math.min(100, Math.max(0, value));
127144
}
128145

129-
protected handleSlotchange(): void {
130-
const labelFromSlot = getLabelFromSlot(this.label, this.slotEl);
131-
if (labelFromSlot) {
132-
this.label = labelFromSlot;
146+
protected override willUpdate(changes: PropertyValues): void {
147+
if (changes.has('progress')) {
148+
const clamped = ProgressCircleBase.clampProgress(this.progress);
149+
if (clamped !== this.progress) {
150+
this.progress = clamped;
151+
}
133152
}
153+
super.willUpdate(changes);
134154
}
135155

136156
protected override firstUpdated(changes: PropertyValues): void {
@@ -158,12 +178,12 @@ export abstract class ProgressCircleBase extends SizedMixin(SpectrumElement, {
158178
} else {
159179
this.setAttribute('aria-valuemin', '0');
160180
this.setAttribute('aria-valuemax', '100');
161-
this.setAttribute('aria-valuenow', '' + this.progress);
181+
this.setAttribute('aria-valuenow', String(this.progress));
162182
this.setAttribute('aria-valuetext', this.formatProgress());
163183
}
164184
}
165185
if (!this.indeterminate && changes.has('progress')) {
166-
this.setAttribute('aria-valuenow', '' + this.progress);
186+
this.setAttribute('aria-valuenow', String(this.progress));
167187
this.setAttribute('aria-valuetext', this.formatProgress());
168188
}
169189
if (!this.indeterminate && changes.has(languageResolverUpdatedSymbol)) {
@@ -181,22 +201,21 @@ export abstract class ProgressCircleBase extends SizedMixin(SpectrumElement, {
181201
return Boolean(
182202
this.label ||
183203
this.getAttribute('aria-label') ||
184-
this.getAttribute('aria-labelledby') ||
185-
this.slotEl.assignedNodes().length
204+
this.getAttribute('aria-labelledby')
186205
);
187206
};
188207

189208
if (window.__swc?.DEBUG) {
209+
this.warnDeprecatedLightDomChildren();
190210
if (!hasAccessibleName() && this.getAttribute('role') === 'progressbar') {
191211
window.__swc?.warn(
192212
this,
193-
'<sp-progress-circle> elements need one of the following to be accessible:',
194-
'https://opensource.adobe.com/spectrum-web-components/components/progress-circle/#accessibility',
213+
`<${this.localName}> elements need one of the following to be accessible:`,
214+
'https://opensource.adobe.com/spectrum-web-components/second-gen/?path=/docs/components-progress-circle--docs',
195215
{
196216
type: 'accessibility',
197217
issues: [
198218
'value supplied to the "label" attribute, which will be displayed visually as part of the element, or',
199-
'text content supplied directly to the <sp-progress-circle> element, or',
200219
'value supplied to the "aria-label" attribute, which will only be provided to screen readers, or',
201220
'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.',
202221
],

2nd-gen/packages/swc/components/progress-circle/ProgressCircle.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ import styles from './progress-circle.css';
3636
*
3737
* @example
3838
* <swc-progress-circle indeterminate label="Loading..."></swc-progress-circle>
39+
*
40+
* Light DOM children are not projected into the shadow tree. Use the `label` attribute or property, or `aria-label` / `aria-labelledby` on the host, for an accessible name.
3941
*/
4042
export class ProgressCircle extends ProgressCircleBase {
4143
// ────────────────────
@@ -74,20 +76,18 @@ export class ProgressCircle extends ProgressCircleBase {
7476
<div
7577
class=${classMap({
7678
['swc-ProgressCircle']: true,
77-
[`swc-ProgressCircle--indeterminate`]: this.indeterminate,
79+
['swc-ProgressCircle--indeterminate']: this.indeterminate,
7880
[`swc-ProgressCircle--static${capitalize(this.staticColor)}`]:
7981
typeof this.staticColor !== 'undefined',
8082
[`swc-ProgressCircle--size${this.size?.toUpperCase()}`]:
8183
typeof this.size !== 'undefined',
8284
})}
8385
>
84-
<slot @slotchange=${this.handleSlotchange}></slot>
85-
<svg fill="none" width="100%" height="100%" class="swc-outerCircle">
86+
<svg fill="none" width="100%" height="100%">
8687
<circle
87-
class="swc-innerCircle"
8888
cx="50%"
8989
cy="50%"
90-
r=${`calc(50% - ${strokeWidth / 1}px)`}
90+
r=${`calc(50% - ${strokeWidth}px)`}
9191
stroke-width=${strokeWidth}
9292
/>
9393
<circle

2nd-gen/packages/swc/components/progress-circle/progress-circle.css

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -100,10 +100,6 @@
100100
--swc-progress-circle-fill-border-color: token("static-black-track-indicator-color");
101101
}
102102

103-
slot {
104-
display: none;
105-
}
106-
107103
@media (forced-colors: active) {
108104
.swc-ProgressCircle {
109105
--swc-progress-circle-fill-border-color: Highlight;

2nd-gen/packages/swc/components/progress-circle/stories/progress-circle.stories.ts

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -103,10 +103,10 @@ export const Overview: Story = {
103103
*
104104
* 1. **Track** - Background ring showing the full progress range
105105
* 2. **Fill** - Colored ring segment showing current progress
106-
* 3. **Label** - Accessible text describing the operation (not visually rendered)
106+
* 3. **Label** - Accessible text describing the operation (not visually rendered), provided via the `label` attribute or property, or `aria-label` / `aria-labelledby` on the host
107107
*
108108
* ### Content
109-
* - **Default slot**: Alternative way to provide an accessible label (the `label` attribute is preferred)
109+
*
110110
* - **Label**: Accessible text describing what is loading or progressing (not visually rendered)
111111
*/
112112
export const Anatomy: Story = {
@@ -246,9 +246,7 @@ export const Indeterminate: Story = {
246246
* #### ARIA implementation
247247
*
248248
* 1. **ARIA role**: Automatically sets `role="progressbar"` for proper semantic meaning
249-
* 2. **Labeling**:
250-
* - Uses the `label` attribute value as `aria-label`
251-
* - Alternative: Content in the default slot can provide the label
249+
* 2. **Labeling**: Uses the `label` attribute value as `aria-label`, or rely on `aria-label` / `aria-labelledby` you set on the host
252250
* 3. **Progress state** (determinate):
253251
* - Sets `aria-valuenow` with the current `progress` value
254252
* 4. **Loading state** (indeterminate):

2nd-gen/packages/swc/components/progress-circle/test/progress-circle.test.ts

Lines changed: 87 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -202,10 +202,10 @@ export const AriaLabelledbyAccessibleNameTest: Story = {
202202
};
203203

204204
// ──────────────────────────────────────────────────────────────
205-
// TEST: Slots
205+
// TEST: Light DOM (no default slot)
206206
// ──────────────────────────────────────────────────────────────
207207

208-
export const SlotLabelTest: Story = {
208+
export const LightDomChildrenDoNotSetLabelTest: Story = {
209209
render: () => html`
210210
<swc-progress-circle>Loading data</swc-progress-circle>
211211
`,
@@ -215,10 +215,62 @@ export const SlotLabelTest: Story = {
215215
'swc-progress-circle'
216216
);
217217

218-
await step('uses slot content as the label', async () => {
219-
expect(progressCircle.label).toBe('Loading data');
220-
expect(progressCircle.getAttribute('aria-label')).toBe('Loading data');
221-
});
218+
await step(
219+
'does not use light DOM text as the label or accessible name',
220+
async () => {
221+
expect(progressCircle.label).toBe('');
222+
expect(progressCircle.getAttribute('aria-label')).toBeNull();
223+
}
224+
);
225+
226+
await step(
227+
'warns in dev mode: deprecation for light DOM children and accessibility when there is no name',
228+
() =>
229+
withWarningSpy(async (warnCalls) => {
230+
progressCircle.progress = 10;
231+
await progressCircle.updateComplete;
232+
233+
expect(warnCalls.length).toBeGreaterThanOrEqual(2);
234+
expect(
235+
warnCalls.some((call) =>
236+
String(call[1] ?? '').includes('no longer has a default slot')
237+
)
238+
).toBe(true);
239+
expect(
240+
warnCalls.some((call) =>
241+
String(call[1] ?? '').includes('accessible')
242+
)
243+
).toBe(true);
244+
})
245+
);
246+
},
247+
};
248+
249+
export const LightDomWithLabelDeprecationOnlyTest: Story = {
250+
render: () => html`
251+
<swc-progress-circle label="Uploading" progress="5">
252+
Ignored slot content
253+
</swc-progress-circle>
254+
`,
255+
play: async ({ canvasElement, step }) => {
256+
const progressCircle = await getComponent<ProgressCircle>(
257+
canvasElement,
258+
'swc-progress-circle'
259+
);
260+
261+
await step(
262+
'still deprecates light DOM when label provides the accessible name',
263+
() =>
264+
withWarningSpy(async (warnCalls) => {
265+
progressCircle.progress = 6;
266+
await progressCircle.updateComplete;
267+
268+
expect(warnCalls.length).toBe(1);
269+
expect(String(warnCalls[0]?.[1] ?? '')).toContain(
270+
'no longer has a default slot'
271+
);
272+
})
273+
);
222274
},
223275
};
224276

@@ -243,6 +295,35 @@ export const ProgressValuesTest: Story = {
243295
},
244296
};
245297

298+
export const ProgressClampTest: Story = {
299+
render: () => html`
300+
<swc-progress-circle
301+
progress="150"
302+
label="Clamped high"
303+
></swc-progress-circle>
304+
<swc-progress-circle
305+
progress="-20"
306+
label="Clamped low"
307+
></swc-progress-circle>
308+
`,
309+
play: async ({ canvasElement, step }) => {
310+
const circles = await getComponents<ProgressCircle>(
311+
canvasElement,
312+
'swc-progress-circle'
313+
);
314+
315+
await step('clamps progress above 100 to 100', async () => {
316+
expect(circles[0].progress).toBe(100);
317+
expect(circles[0].getAttribute('aria-valuenow')).toBe('100');
318+
});
319+
320+
await step('clamps progress below 0 to 0', async () => {
321+
expect(circles[1].progress).toBe(0);
322+
expect(circles[1].getAttribute('aria-valuenow')).toBe('0');
323+
});
324+
},
325+
};
326+
246327
export const IndeterminateTest: Story = {
247328
...Indeterminate,
248329
play: async ({ canvasElement, step }) => {

CONTRIBUTOR-DOCS/02_style-guide/02_typescript/06_method-patterns.md

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -27,24 +27,24 @@ This guide explains how to order and name methods in 2nd-gen component classes.
2727

2828
## Method ordering
2929

30-
Within the [IMPLEMENTATION section](02_class-structure.md#section-implementation) of a base class, or the [RENDERING & STYLING section](02_class-structure.md#section-rendering-and-styling) of a concrete class, order methods by access level:
30+
Within the [IMPLEMENTATION section](02_class-structure.md#section-implementation) of a base class, or the [RENDERING & STYLING section](02_class-structure.md#section-rendering-and-styling) of a concrete class, use this **default** order by access level:
3131

3232
1. **public** methods first
33-
2. **protected** methods second (including lifecycle)
33+
2. **protected** methods second (including lifecycle overrides)
3434
3. **private** methods last
3535

36-
**Example from ProgressCircle.base.ts — IMPLEMENTATION section:**
36+
**Exception:** Keep lifecycle overrides in **Lit order** (e.g. `firstUpdated` before `updated`). You may place a **private method** **between** those hooks **when only those hooks call it** (and nothing else in the class does), so the file reads in execution order; if the method is also used elsewhere, put it **after** all protected members instead.
37+
38+
**Example from [ProgressCircle.base.ts](../../../2nd-gen/packages/core/components/progress-circle/ProgressCircle.base.ts) — IMPLEMENTATION section:**
3739

3840
```ts
39-
// Protected methods (lifecycle and helpers)
40-
protected makeRotation(rotation: number): string | undefined { ... }
41-
protected handleSlotchange(): void { ... }
41+
// Protected — firstUpdated
4242
protected override firstUpdated(changes: PropertyValues): void { ... }
4343

44-
// Private methods
44+
// Private — used by updated()
4545
private formatProgress(): string { ... }
4646

47-
// Protected lifecycle (later in call order)
47+
// Protected — updated
4848
protected override updated(changes: PropertyValues): void { ... }
4949
```
5050

0 commit comments

Comments
 (0)