diff --git a/2nd-gen/packages/core/components/color-loupe/ColorLoupe.base.ts b/2nd-gen/packages/core/components/color-loupe/ColorLoupe.base.ts new file mode 100644 index 00000000000..4f1506e5943 --- /dev/null +++ b/2nd-gen/packages/core/components/color-loupe/ColorLoupe.base.ts @@ -0,0 +1,55 @@ +/** + * Copyright 2026 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 { property } from 'lit/decorators.js'; + +import { SpectrumElement } from '@spectrum-web-components/core/element/index.js'; + +/** + * A visual magnifier that shows the currently picked color, including + * transparency over an opacity checkerboard, inside a loupe shape. + * + * The loupe is not an interactive control — accessibility semantics are + * provided by the parent color picker / color field. + * + * @element swc-color-loupe + */ +export abstract class ColorLoupeBase extends SpectrumElement { + // ───────────────── + // SHARED API + // ───────────────── + + /** + * Whether the loupe is visible. When `false` the loupe is hidden via + * CSS opacity and transform transitions. + */ + @property({ type: Boolean, reflect: true }) + public open = false; + + /** + * The CSS color value to display inside the loupe. + * Supports any valid CSS color string, including those with alpha + * transparency (which reveals the checkerboard behind). + * + * Default is semi-transparent red so the opacity checkerboard is visible + * when the component is rendered without a `color` attribute. + * + * @todo Runtime validation is intentionally not performed here. The loupe + * always receives its color from a parent color-picker component, which + * validates upstream via `validateColorString` on `ColorController`. If + * the loupe ever needs standalone validation (e.g. consumed outside a + * parent color picker), reuse `ColorController.validateColorString` + * rather than adding a separate utility. + */ + @property({ type: String }) + public color = 'rgba(255, 0, 0, 0.5)'; +} diff --git a/2nd-gen/packages/core/components/color-loupe/index.ts b/2nd-gen/packages/core/components/color-loupe/index.ts new file mode 100644 index 00000000000..138f712b127 --- /dev/null +++ b/2nd-gen/packages/core/components/color-loupe/index.ts @@ -0,0 +1,12 @@ +/** + * Copyright 2026 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. + */ +export * from './ColorLoupe.base.js'; diff --git a/2nd-gen/packages/swc/.storybook/preview.ts b/2nd-gen/packages/swc/.storybook/preview.ts index f26f4005752..6cd8929cafc 100644 --- a/2nd-gen/packages/swc/.storybook/preview.ts +++ b/2nd-gen/packages/swc/.storybook/preview.ts @@ -360,6 +360,12 @@ const preview = { ['Rendering and styling migration analysis'], 'Color field', ['Rendering and styling migration analysis'], + 'Color loupe', + [ + 'Accessibility migration analysis', + 'Migration checklist', + 'Rendering and styling migration analysis', + ], 'Divider', [ 'Accessibility migration analysis', diff --git a/2nd-gen/packages/swc/components/color-loupe/ColorLoupe.ts b/2nd-gen/packages/swc/components/color-loupe/ColorLoupe.ts new file mode 100644 index 00000000000..701944f6b41 --- /dev/null +++ b/2nd-gen/packages/swc/components/color-loupe/ColorLoupe.ts @@ -0,0 +1,90 @@ +/** + * Copyright 2026 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, TemplateResult } from 'lit'; +import { classMap } from 'lit/directives/class-map.js'; +import { styleMap } from 'lit/directives/style-map.js'; + +import { ColorLoupeBase } from '@spectrum-web-components/core/components/color-loupe'; + +import styles from './color-loupe.css'; + +/** + * A visual magnifier that displays the currently picked color inside a + * loupe-shaped container with an opacity checkerboard behind transparent + * colors. The loupe is a non-interactive, visual-only companion to + * color selection controls such as ``. + * + * @element swc-color-loupe + * @status preview + * @since 0.0.1 + * + * @example + * + */ +export class ColorLoupe extends ColorLoupeBase { + // ────────────────────────────── + // RENDERING & STYLING + // ────────────────────────────── + + /** + * @todo SWC-2029 - Migrate opacity-checkerboard to 2nd gen and consume it + * here; checkerboard styling is currently hardcoded in color-loupe.css. + */ + public static override get styles(): CSSResultArray { + return [styles]; + } + + protected override render(): TemplateResult { + return html` +
+
+
+ +
+ `; + } +} diff --git a/2nd-gen/packages/swc/components/color-loupe/color-loupe.css b/2nd-gen/packages/swc/components/color-loupe/color-loupe.css new file mode 100644 index 00000000000..1cf41650c82 --- /dev/null +++ b/2nd-gen/packages/swc/components/color-loupe/color-loupe.css @@ -0,0 +1,116 @@ +/** + * Copyright 2026 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. + */ + +:host { + --_swc-color-loupe-width: token("color-loupe-width"); + + display: block; + position: absolute; + inset-block-end: calc((token("color-handle-size") - token("color-handle-outer-border-width")) + token("color-loupe-bottom-to-color-handle")); + inset-inline-end: calc(50% - (var(--_swc-color-loupe-width) / 2)); + inline-size: var(--_swc-color-loupe-width); + block-size: token("color-loupe-height"); +} + +* { + box-sizing: border-box; +} + +.swc-ColorLoupe { + position: relative; + inline-size: 100%; + block-size: 100%; + pointer-events: none; + opacity: 0; + filter: drop-shadow(token("drop-shadow-elevated-x") token("drop-shadow-elevated-y") token("drop-shadow-elevated-blur") token("drop-shadow-elevated-color")); + + /* TODO: replace 8px with a forthcoming animation-distance token (matches Spectrum CSS S2). */ + transform: translateY(8px); + transform-origin: bottom center; + transition: + transform 100ms ease-in-out, + opacity 125ms ease-in-out; +} + +/* Opacity checkerboard shown behind transparent picked colors. + The dark-square token has separate light/dark theme values, so + we use light-dark() with the explicit theme variants. */ +.swc-ColorLoupe-checkerboard { + position: absolute; + inset-block-start: 2px; + inset-inline-start: 2px; + inline-size: 100%; + block-size: 100%; + background: repeating-conic-gradient(light-dark(var(--swc-opacity-checkerboard-square-dark-light), var(--swc-opacity-checkerboard-square-dark-dark)) 0% 25%, token("opacity-checkerboard-square-light") 0% 50%) 0 0 / token("opacity-checkerboard-square-size-medium") token("opacity-checkerboard-square-size-medium"); +} + +/* Color fill layer — displays the picked color set by the component via + the --swc-color-loupe-picked-color custom property. */ +.swc-ColorLoupe-colorFill { + position: absolute; + inset-block-start: 2px; + inset-inline-start: 2px; + inline-size: 100%; + block-size: 100%; + background: var(--swc-color-loupe-picked-color); +} + +/* SVG overlay — inherits host dimensions */ +.swc-ColorLoupe-svg { + position: absolute; + inline-size: inherit; + block-size: inherit; +} + +/* Inner border: thin stroke separating color fill from the loupe edge */ +.swc-ColorLoupe-innerBorder { + fill: none; + stroke: token("color-loupe-inner-border"); + stroke-width: token("color-loupe-inner-border-width"); +} + +/* Outer border: wider stroke forming the loupe outline */ +.swc-ColorLoupe-outerBorder { + --_swc-color-loupe-outer-border-color: token("color-loupe-outer-border"); + + fill: none; + stroke: var(--_swc-color-loupe-outer-border-color); + stroke-width: calc(token("color-loupe-outer-border-width") + 2px); +} + +/* Clip utility — applied to the checkerboard and color-fill layers to + constrain them to the loupe teardrop silhouette. Modifier class is placed + after all subcomponent rules per the documented rule order. */ +.swc-ColorLoupe--clipped { + clip-path: path("M 22 60 C 18.2 56 14.6 51.7 11.3 47.2 C 8.3 43.3 5.7 39.1 3.5 34.7 C 1.2 30 0 25.9 0 22.4 C 0 17.2 1.8 12.2 5 8.2 C 8.2 4.2 12.7 1.5 17.6 0.4 C 22.6 -0.6 27.8 0.2 32.3 2.6 C 36.8 5 40.3 8.9 42.3 13.7 C 43.4 16.4 44 19.4 44 22.4 C 44 25.9 42.8 30 40.5 34.7 C 38.3 39.1 35.7 43.3 32.7 47.3 C 29.4 51.7 25.8 56 22 60 Z"); +} + +/* Compensates for sub-pixel rounding in RTL that shifts the loupe */ +:host(:dir(rtl)) { + inset-inline-end: calc(50% - (var(--_swc-color-loupe-width) / 2) - 1px); +} + +:host([open]) .swc-ColorLoupe { + opacity: 1; + transform: translate(0, 0); +} + +@media (forced-colors: active) { + .swc-ColorLoupe-colorFill, + .swc-ColorLoupe-checkerboard { + forced-color-adjust: none; + } + + .swc-ColorLoupe-outerBorder { + --_swc-color-loupe-outer-border-color: CanvasText; + } +} diff --git a/2nd-gen/packages/swc/components/color-loupe/index.ts b/2nd-gen/packages/swc/components/color-loupe/index.ts new file mode 100644 index 00000000000..220a7f99480 --- /dev/null +++ b/2nd-gen/packages/swc/components/color-loupe/index.ts @@ -0,0 +1,22 @@ +/** + * Copyright 2026 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 { defineElement } from '@spectrum-web-components/core/element/index.js'; + +import { ColorLoupe } from './ColorLoupe.js'; + +export * from './ColorLoupe.js'; +declare global { + interface HTMLElementTagNameMap { + 'swc-color-loupe': ColorLoupe; + } +} +defineElement('swc-color-loupe', ColorLoupe); diff --git a/2nd-gen/packages/swc/components/color-loupe/stories/color-loupe.stories.ts b/2nd-gen/packages/swc/components/color-loupe/stories/color-loupe.stories.ts new file mode 100644 index 00000000000..7b73c4ee0c5 --- /dev/null +++ b/2nd-gen/packages/swc/components/color-loupe/stories/color-loupe.stories.ts @@ -0,0 +1,279 @@ +/** + * Copyright 2026 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 { html } from 'lit'; +import type { Meta, StoryObj as Story } from '@storybook/web-components'; +import { getStorybookHelpers } from '@wc-toolkit/storybook-helpers'; + +import '@adobe/spectrum-wc/color-loupe'; + +// ──────────────── +// METADATA +// ──────────────── + +const { events, args, argTypes, template } = + getStorybookHelpers('swc-color-loupe'); + +/** + * An `` shows the output color that would otherwise be + * covered by a cursor, stylus, or finger during color selection. It is a + * visual-only companion to color selection components such as color area, + * color slider, and color wheel — visibility is managed by the parent. + */ +const meta: Meta = { + title: 'Color Components/Color Loupe', + component: 'swc-color-loupe', + args: { + ...args, + open: true, + }, + argTypes, + actions: { + handles: events, + }, + parameters: { + design: { + type: 'figma', + url: 'https://www.figma.com/design/Mngz9H7WZLbrCvGQf3GnsY/S2---Desktop?node-id=13065-162', + }, + docs: { + subtitle: `Visual magnifier showing the picked color during color selection`, + }, + styles: { + position: 'relative', + 'min-block-size': '120px', + }, + }, + render: (args) => template(args), + tags: ['migrated'], +}; + +export default meta; + +// ──────────────────── +// HELPERS +// ──────────────────── + +const COLOR_FORMATS = [ + { label: 'Named', color: 'yellow' }, + { label: 'Hex', color: '#ff0000' }, + { label: 'RGBA', color: 'rgba(44, 62, 224, 0.81)' }, + { label: 'HSL', color: 'hsl(111, 82%, 56%)' }, +] as const satisfies readonly { label: string; color: string }[]; + +/** + * `` is `position: absolute` on `:host` because it normally + * floats above a color handle. In stories that compare multiple loupes side + * by side, each loupe needs its own `position: relative` containing block — + * otherwise every loupe computes the same inset offsets and they stack at + * the same coordinate. This helper provides that containing block plus an + * optional caption below the loupe. + */ +const labeledLoupe = ( + label: string, + templateArgs: Record +) => html` +
+
+ ${template(templateArgs)} +
+ ${label} +
+`; + +// ──────────────────── +// AUTODOCS STORY +// ──────────────────── + +export const Playground: Story = { + args: { + open: true, + color: 'rgba(0, 128, 255, 0.7)', + }, + tags: ['autodocs', 'dev'], +}; + +// ──────────────────── +// OVERVIEW STORY +// ──────────────────── + +export const Overview: Story = { + args: { + open: true, + color: 'rgba(0, 128, 255, 0.7)', + }, + tags: ['overview'], +}; + +// ────────────────────────── +// ANATOMY STORIES +// ────────────────────────── + +/** + * A color loupe consists of: + * + * 1. **Floating loupe element** - A teardrop-shaped container positioned above the interaction point, with an inner and outer border + * 2. **Color preview** - Displays the currently picked color over an opacity checkerboard so transparency is visible + */ +export const Anatomy: Story = { + render: (args) => html` + ${template({ ...args, open: true, color: 'rgba(255, 0, 0, 0.5)' })} + `, + tags: ['anatomy'], +}; + +// ────────────────────────── +// OPTIONS STORIES +// ────────────────────────── + +/** + * The `color` property accepts any valid CSS color string: + * + * - **Named colors**: `yellow`, `red`, `blue`, etc. + * - **Hex**: `#ff0000` + * - **RGB/RGBA**: `rgba(44, 62, 224, 0.81)` — alpha transparency reveals the checkerboard + * - **HSL**: `hsl(111, 82%, 56%)` + * + * When using transparent colors, the opacity checkerboard pattern shows through, + * giving a clear visual indication of the transparency level. + * + * All color formats shown below for comparison. + */ +export const Colors: Story = { + render: (args) => html` + ${COLOR_FORMATS.map(({ label, color }) => + labeledLoupe(label, { ...args, open: true, color }) + )} + `, + tags: ['options'], + parameters: { + flexLayout: 'row-wrap', + 'section-order': 1, + }, +}; + +// ────────────────────────── +// STATES STORIES +// ────────────────────────── + +/** + * The color loupe has two visibility states: + * + * - **Open**: Fully visible with `opacity: 1` and no vertical offset + * - **Closed** (default): Hidden via `opacity: 0` and a downward transform + * + * The transition between states is animated with CSS transitions on + * `opacity` (125 ms) and `transform` (100 ms). + */ +export const OpenAndClosedStates: Story = { + render: (args) => html` + ${labeledLoupe('Open', { + ...args, + open: true, + color: 'rgba(0, 128, 255, 0.7)', + })} + ${labeledLoupe('Closed', { + ...args, + open: false, + color: 'rgba(0, 128, 255, 0.7)', + })} + `, + tags: ['states'], + parameters: { + flexLayout: 'row-wrap', + }, +}; +OpenAndClosedStates.storyName = 'Open and closed states'; + +// ────────────────────────────── +// BEHAVIORS STORIES +// ────────────────────────────── + +/** + * The color loupe's `open` state is entirely managed by its parent color + * component — the loupe does not manage its own visibility: + * + * - **Touch input**: The loupe automatically appears during touch interactions + * with any color component (``, ``, + * ``) to prevent the finger from obscuring the selected color + * - **Mouse/stylus input**: The loupe remains hidden by default for precision + * pointing devices + * - **Parent control**: The parent sets `open` to `true` when the user is actively + * selecting a color and back to `false` when the interaction ends + * + * The loupe animates its visibility with CSS transitions: `opacity` over + * 125 ms and `transform` (vertical offset) over 100 ms. + */ +export const ParentDrivenVisibility: Story = { + render: (args) => html` + ${labeledLoupe('Touch active', { + ...args, + open: true, + color: 'rgba(0, 200, 100, 0.8)', + })} + ${labeledLoupe('Idle', { + ...args, + open: false, + color: 'rgba(0, 200, 100, 0.8)', + })} + `, + tags: ['behaviors'], + parameters: { + flexLayout: 'row-wrap', + }, +}; +ParentDrivenVisibility.storyName = 'Parent-driven visibility'; + +// ──────────────────────────────── +// ACCESSIBILITY STORIES +// ──────────────────────────────── + +/** + * ### Features + * + * The `` element is a **visual-only** component: + * + * #### ARIA implementation + * + * - **SVG is `aria-hidden="true"`**: The loupe graphic is decorative and + * hidden from the accessibility tree + * - **Not focusable**: The component has no tab stop and no keyboard interaction + * + * #### Accessibility model + * + * The loupe does not represent a standalone accessible control. + * Accessibility semantics (name, value, role) are provided by the + * **parent** color selection component — for example, ``, + * ``, or ``. The loupe simply reflects + * the currently picked color as a visual aid during touch interactions. + * + * ### Best practices + * + * - Never use the color loupe as the sole means of communicating a color + * value — always pair it with labeled controls that expose the value + * to assistive technology + * - Ensure the parent color component provides appropriate labeling via + * visible text or ARIA (for example, `aria-label` on ``) + * - Do not add `role`, `aria-label`, or focus management to the loupe + * itself — it is intentionally inert + * - Avoid conveying meaning through the loupe color alone; pair color + * selection with text labels or other indicators as appropriate + */ +export const Accessibility: Story = { + args: { + open: true, + color: 'rgba(0, 128, 255, 0.7)', + }, + tags: ['a11y'], +}; diff --git a/2nd-gen/packages/swc/components/color-loupe/test/color-loupe.a11y.spec.ts b/2nd-gen/packages/swc/components/color-loupe/test/color-loupe.a11y.spec.ts new file mode 100644 index 00000000000..c1f48f31a0a --- /dev/null +++ b/2nd-gen/packages/swc/components/color-loupe/test/color-loupe.a11y.spec.ts @@ -0,0 +1,57 @@ +/** + * Copyright 2026 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 { expect, test } from '@playwright/test'; + +import { gotoStory } from '../../../utils/a11y-helpers.js'; + +/** + * Accessibility tests for ColorLoupe component (2nd generation) + * + * The color loupe is a purely visual, non-interactive component. + * Its SVG carries aria-hidden="true" so the loupe graphic is fully + * hidden from the accessibility tree. These tests assert that + * attribute directly rather than using toMatchAriaSnapshot, which + * does not accept an empty string even when the tree is legitimately + * empty. aXe WCAG compliance and color contrast validation are run + * via test-storybook (see .storybook/test-runner.ts). + */ + +test.describe('ColorLoupe - ARIA Snapshots', () => { + test('should hide SVG from the accessibility tree for overview', async ({ + page, + }) => { + const root = await gotoStory( + page, + 'components-color-components-color-loupe--overview', + 'swc-color-loupe' + ); + await expect(root.locator('svg').first()).toHaveAttribute( + 'aria-hidden', + 'true' + ); + }); + + test('should hide SVG from the accessibility tree for accessibility story', async ({ + page, + }) => { + const root = await gotoStory( + page, + 'components-color-components-color-loupe--accessibility', + 'swc-color-loupe' + ); + await expect(root.locator('svg').first()).toHaveAttribute( + 'aria-hidden', + 'true' + ); + }); +}); diff --git a/2nd-gen/packages/swc/components/color-loupe/test/color-loupe.test.ts b/2nd-gen/packages/swc/components/color-loupe/test/color-loupe.test.ts new file mode 100644 index 00000000000..fc15dc881c5 --- /dev/null +++ b/2nd-gen/packages/swc/components/color-loupe/test/color-loupe.test.ts @@ -0,0 +1,166 @@ +/** + * Copyright 2026 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 { html } from 'lit'; +import { expect, waitFor } from '@storybook/test'; +import type { Meta, StoryObj as Story } from '@storybook/web-components'; + +import { ColorLoupe } from '@adobe/spectrum-wc/color-loupe'; + +import '@adobe/spectrum-wc/color-loupe'; + +import { getComponent } from '../../../utils/test-utils.js'; +import meta from '../stories/color-loupe.stories.js'; +import { Overview } from '../stories/color-loupe.stories.js'; + +export default { + ...meta, + title: 'Color Components/Color Loupe/Tests', + parameters: { + ...meta.parameters, + docs: { disable: true, page: null }, + }, + tags: ['!autodocs', 'dev'], +} as Meta; + +// ────────────────────────────────────────────────────────────── +// TEST: Defaults +// ────────────────────────────────────────────────────────────── + +export const OverviewTest: Story = { + ...Overview, + play: async ({ canvasElement, step }) => { + const loupe = await getComponent( + canvasElement, + 'swc-color-loupe' + ); + + await step('renders with open attribute reflected', async () => { + expect(loupe.open).toBe(true); + expect(loupe.hasAttribute('open')).toBe(true); + }); + + await step('has an aria-hidden SVG', async () => { + const svg = loupe.shadowRoot?.querySelector('svg'); + expect(svg).toBeTruthy(); + expect(svg?.getAttribute('aria-hidden')).toBe('true'); + }); + + await step('applies the color property', async () => { + expect(loupe.color).toBe('rgba(0, 128, 255, 0.7)'); + const fill = loupe.shadowRoot?.querySelector( + '.swc-ColorLoupe-colorFill' + ) as HTMLElement; + expect(fill).toBeTruthy(); + expect( + fill.style.getPropertyValue('--swc-color-loupe-picked-color') + ).toContain('0, 128, 255'); + }); + }, +}; + +// ────────────────────────────────────────────────────────────── +// TEST: Open attribute +// ────────────────────────────────────────────────────────────── + +export const OpenAttributeTest: Story = { + render: () => html` +
+ +
+ `, + play: async ({ canvasElement, step }) => { + const loupe = await getComponent( + canvasElement, + 'swc-color-loupe' + ); + + const getInnerLoupe = (): HTMLElement => + loupe.shadowRoot?.querySelector('.swc-ColorLoupe') as HTMLElement; + + await step('defaults to closed (open = false)', async () => { + expect(loupe.open).toBe(false); + expect(loupe.hasAttribute('open')).toBe(false); + }); + + await step('inner .swc-ColorLoupe has opacity 0 when closed', async () => { + const innerLoupe = getInnerLoupe(); + expect(innerLoupe).toBeTruthy(); + // Initial state: no transition in flight, value should already be 0. + expect(getComputedStyle(innerLoupe).opacity).toBe('0'); + }); + + await step( + 'reflects open attribute when set programmatically', + async () => { + loupe.open = true; + await loupe.updateComplete; + expect(loupe.hasAttribute('open')).toBe(true); + } + ); + + // The loupe animates opacity over 125 ms, so poll via waitFor until the + // transition settles at the target value. + await step('inner .swc-ColorLoupe has opacity 1 when open', async () => { + await waitFor(() => { + expect(getComputedStyle(getInnerLoupe()).opacity).toBe('1'); + }); + }); + + await step('removes open attribute when set to false', async () => { + loupe.open = false; + await loupe.updateComplete; + expect(loupe.hasAttribute('open')).toBe(false); + }); + + await step( + 'inner .swc-ColorLoupe returns to opacity 0 when closed again', + async () => { + await waitFor(() => { + expect(getComputedStyle(getInnerLoupe()).opacity).toBe('0'); + }); + } + ); + }, +}; + +// ────────────────────────────────────────────────────────────── +// TEST: Color property +// ────────────────────────────────────────────────────────────── + +export const ColorPropertyTest: Story = { + render: () => html` +
+ +
+ `, + play: async ({ canvasElement, step }) => { + const loupe = await getComponent( + canvasElement, + 'swc-color-loupe' + ); + + await step('defaults to semi-transparent red', async () => { + expect(loupe.color).toBe('rgba(255, 0, 0, 0.5)'); + }); + + await step('updates color fill when color property changes', async () => { + loupe.color = 'rgb(0, 255, 0)'; + await loupe.updateComplete; + const fill = loupe.shadowRoot?.querySelector( + '.swc-ColorLoupe-colorFill' + ) as HTMLElement; + expect( + fill.style.getPropertyValue('--swc-color-loupe-picked-color') + ).toContain('0, 255, 0'); + }); + }, +}; diff --git a/CONTRIBUTOR-DOCS/02_style-guide/02_typescript/09_rendering-patterns.md b/CONTRIBUTOR-DOCS/02_style-guide/02_typescript/09_rendering-patterns.md index b8a13f5a417..226a55a111e 100644 --- a/CONTRIBUTOR-DOCS/02_style-guide/02_typescript/09_rendering-patterns.md +++ b/CONTRIBUTOR-DOCS/02_style-guide/02_typescript/09_rendering-patterns.md @@ -15,6 +15,7 @@ - [Size modifier pattern](#size-modifier-pattern) - [Inline SVG](#inline-svg) - [classMap patterns](#classmap-patterns) +- [Inline CSS strings from component properties](#inline-css-strings-from-component-properties) @@ -218,3 +219,43 @@ class=${classMap({ [`swc-Badge--${this.variant}`]: true, // Bracketed })} ``` + +## Inline CSS strings from component properties + +Some components accept a property whose value is a CSS string — for example, `` exposes a `color` property that accepts any valid CSS color. When the value must influence rendering, set it as a **CSS custom property via `styleMap`**, and have the component's stylesheet consume that custom property. + +```ts +// ColorLoupe.ts +
+``` + +```css +/* color-loupe.css */ +.swc-ColorLoupe-colorFill { + background: var(--swc-color-loupe-picked-color); +} +``` + +When a property value reaches an inline `style` attribute — directly or via `styleMap` — the browser's CSS parser reads it verbatim. That means: + +- **Only accept CSS strings from trusted sources.** Component properties that participate in an inline `style` (or a `styleMap` entry) must be treated as trusted input. Do not expose such properties to arbitrary user-generated content without validation at the call site. +- **Use a CSS custom property, not a full declaration.** Set `--swc--: ${value}` via `styleMap` rather than interpolating an entire declaration. This scopes what the property can affect to what the component's CSS explicitly consumes. +- **Document the contract on the property.** The property's JSDoc must state that the value is passed to the CSS parser as-is and that callers are responsible for ensuring it is a valid, trusted CSS value. + +```ts +// ✅ Good — typed CSS property, scoped via a custom property +style=${styleMap({ + '--swc-color-loupe-picked-color': this.color, +})} + +// ❌ Bad — full inline declaration interpolated from a property. +// The entire declaration is re-parsed on every render, and a malformed +// value can corrupt adjacent styles. Always route property values through +// a custom property that the stylesheet consumes explicitly. +style="background: ${this.color}" +``` diff --git a/CONTRIBUTOR-DOCS/03_project-planning/02_workstreams/02_2nd-gen-component-migration/01_status.md b/CONTRIBUTOR-DOCS/03_project-planning/02_workstreams/02_2nd-gen-component-migration/01_status.md index 93fd5b18b4f..547ea2b881d 100644 --- a/CONTRIBUTOR-DOCS/03_project-planning/02_workstreams/02_2nd-gen-component-migration/01_status.md +++ b/CONTRIBUTOR-DOCS/03_project-planning/02_workstreams/02_2nd-gen-component-migration/01_status.md @@ -41,7 +41,7 @@ | Color Area | | | | | | | | | Color Field | ✓ | | | | | | | | Color Handle | | | | | | | | -| Color Loupe | | | | | | | | +| Color Loupe | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | | Color Slider | | | | | | | | | Color Wheel | | | | | | | | | Combobox | | | | | | | | diff --git a/CONTRIBUTOR-DOCS/03_project-planning/03_components/README.md b/CONTRIBUTOR-DOCS/03_project-planning/03_components/README.md index 94dd50d92c2..a512b260037 100644 --- a/CONTRIBUTOR-DOCS/03_project-planning/03_components/README.md +++ b/CONTRIBUTOR-DOCS/03_project-planning/03_components/README.md @@ -43,6 +43,8 @@ - [Color field migration roadmap](color-field/rendering-and-styling-migration-analysis.md) - Color Loupe - [Color loupe accessibility migration analysis](color-loupe/accessibility-migration-analysis.md) + - [Color loupe migration checklist](color-loupe/migration-checklist.md) + - [Color loupe migration analysis](color-loupe/rendering-and-styling-migration-analysis.md) - Divider - [Divider accessibility migration analysis](divider/accessibility-migration-analysis.md) - [Divider migration roadmap](divider/rendering-and-styling-migration-analysis.md) diff --git a/CONTRIBUTOR-DOCS/03_project-planning/03_components/color-loupe/accessibility-migration-analysis.md b/CONTRIBUTOR-DOCS/03_project-planning/03_components/color-loupe/accessibility-migration-analysis.md index 13282f1d8b1..6e794ad85c7 100644 --- a/CONTRIBUTOR-DOCS/03_project-planning/03_components/color-loupe/accessibility-migration-analysis.md +++ b/CONTRIBUTOR-DOCS/03_project-planning/03_components/color-loupe/accessibility-migration-analysis.md @@ -14,7 +14,6 @@ - [Overview](#overview) - [Also read](#also-read) - [What it is](#what-it-is) - - [When to use something else](#when-to-use-something-else) - [ARIA and WCAG context](#aria-and-wcag-context) - [Pattern in the APG](#pattern-in-the-apg) - [Guidelines that apply](#guidelines-that-apply) diff --git a/CONTRIBUTOR-DOCS/03_project-planning/03_components/color-loupe/migration-checklist.md b/CONTRIBUTOR-DOCS/03_project-planning/03_components/color-loupe/migration-checklist.md new file mode 100644 index 00000000000..de4f305a925 --- /dev/null +++ b/CONTRIBUTOR-DOCS/03_project-planning/03_components/color-loupe/migration-checklist.md @@ -0,0 +1,340 @@ + + +[CONTRIBUTOR-DOCS](../../../README.md) / [Project planning](../../README.md) / [Components](../README.md) / Color Loupe / Color loupe migration checklist + + + +# Color loupe migration checklist + + + +
+In this doc + +- [Overview](#overview) +- [Phase 1: Planning and analysis (SWC-1783)](#phase-1-planning-and-analysis-swc-1783) +- [Phase 2: File structure setup (SWC-1784)](#phase-2-file-structure-setup-swc-1784) +- [Phase 3: API and TypeScript migration (SWC-1785)](#phase-3-api-and-typescript-migration-swc-1785) + - [Properties](#properties) + - [Render](#render) + - [TypeScript](#typescript) + - [Mod overrides](#mod-overrides) +- [Phase 4: Accessibility implementation (SWC-1786)](#phase-4-accessibility-implementation-swc-1786) + - [ARIA](#aria) + - [Focus and keyboard](#focus-and-keyboard) + - [Non-text contrast (WCAG 1.4.11)](#non-text-contrast-wcag-1411) + - [Forced colors / high contrast mode](#forced-colors--high-contrast-mode) + - [Documentation notes](#documentation-notes) +- [Phase 5: S2 visual fidelity and CSS (SWC-1787)](#phase-5-s2-visual-fidelity-and-css-swc-1787) + - [Token migration](#token-migration) + - [Visual review](#visual-review) +- [Phase 6: Code style conformance (SWC-1788)](#phase-6-code-style-conformance-swc-1788) +- [Phase 7: Test suites (SWC-1789)](#phase-7-test-suites-swc-1789) + - [Unit tests](#unit-tests) + - [Accessibility tests (axe)](#accessibility-tests-axe) + - [Integration / E2E tests](#integration--e2e-tests) + - [Browser compatibility](#browser-compatibility) +- [Phase 8: Storybook documentation (SWC-1790)](#phase-8-storybook-documentation-swc-1790) +- [Phase 9: Consumer migration guide (SWC-1791)](#phase-9-consumer-migration-guide-swc-1791) +- [Phase 10: Review and finalize (SWC-1792)](#phase-10-review-and-finalize-swc-1792) +- [Known issues and decisions](#known-issues-and-decisions) +- [References](#references) + +
+ + + +## Overview + +This checklist tracks the full 2nd-gen migration of the **``** component. It is derived from the acceptance criteria in [SWC-1783](https://jira.corp.adobe.com/browse/SWC-1783) and the component analysis documented in: + +- [Rendering and styling migration analysis](./rendering-and-styling-migration-analysis.md) +- [Accessibility migration analysis](./accessibility-migration-analysis.md) + +**API summary:** +- Properties: `open` (boolean, reflects to attribute), `color` (string) +- Slots: None +- Events: None dispatched +- Breaking changes: None anticipated — the `open` / `color` API is unchanged between S1 and S2 + +**Key bugs to fix in 2nd-gen:** +- SVG path data ends with `ZZ` (double close command); fix to single `Z` +- `` references `#path` (non-existent); fix to `#loupe-path` +- Opacity checkerboard must be inlined (no longer imported from `@spectrum-web-components/opacity-checkerboard`) +- Drop-shadow tokens must migrate from S1 (`--spectrum-drop-shadow-*`) to S2 (`--spectrum-drop-shadow-elevated-*`) + +--- + + + +## Phase 1: Planning and analysis (SWC-1783) + +> Goal: Document the 1st-gen API surface, identify all dependencies and breaking changes, and produce a migration plan before any implementation begins. + +- [ ] 1st-gen API surface documented — properties, methods, events, slots, CSS custom properties + - `open` — `boolean`, default `false`, reflects to attribute + - `color` — `string`, default `'rgba(255, 0, 0, 0.5)'`, does not reflect + - No public methods beyond inherited defaults + - No events dispatched + - No slots + - CSS custom properties: see [rendering and styling analysis](./rendering-and-styling-migration-analysis.md) +- [ ] All dependencies identified + - 1st-gen imports `@spectrum-web-components/opacity-checkerboard` — must be inlined in 2nd-gen + - `SpectrumElement` base class (no mixins) + - Spectrum CSS tokens (S1 → S2 mapping required for drop-shadow and colorloupe tokens) +- [ ] Breaking changes identified and documented + - No breaking API changes anticipated (`open`, `color` are preserved) + - Opacity checkerboard import removed — inlined via `--swc-opacity-checkerboard-*` tokens +- [ ] SVG bugs identified for fix in 2nd-gen + - Double-Z path bug (`61.575ZZ` → `61.575Z`) + - Mask ID mismatch (`#path` → `#loupe-path`) +- [ ] Accessibility analysis reviewed and incorporated into plan (see [SWC-1782](https://jira.corp.adobe.com/browse/SWC-1782) and [accessibility migration analysis](./accessibility-migration-analysis.md)) +- [ ] Factor assessment: `SKIP_FACTOR` confirmed — component has no extractable state logic; total logic under 40 lines +- [ ] Plan linked as comment on parent epic [SWC-1781](https://jira.corp.adobe.com/browse/SWC-1781) +- [ ] Any blockers or open questions flagged + +--- + +## Phase 2: File structure setup (SWC-1784) + +> Goal: Create the 2nd-gen scaffold following the washing machine workflow conventions. + +- [ ] Directory created at `2nd-gen/packages/swc/components/color-loupe/` +- [ ] Core directory created at `2nd-gen/packages/core/components/color-loupe/` +- [ ] Required files scaffolded: + - `ColorLoupe.ts` (swc package — element registration) + - `ColorLoupe.base.ts` (core package — base class logic) + - `color-loupe.css` (swc package — S2 styles) + - `index.ts` (swc package — barrel export) + - `package.json` (swc package) + - `tsconfig.json` (swc package) + +> **Note**: `ColorLoupe.types.ts` is intentionally omitted — the component has no enums or unions to declare, and bare default values belong with the property in the base class. See [PR #6147 discussion r3119292750](https://github.com/adobe/spectrum-web-components/pull/6147#discussion_r3119292750). +- [ ] `stories/` directory created with placeholder `color-loupe.stories.ts` +- [ ] Component registered with `customElements.define('swc-color-loupe', ColorLoupe)` +- [ ] Entry added to the 2nd-gen package manifest / workspace + +--- + +## Phase 3: API and TypeScript migration (SWC-1785) + +> Goal: Port the 1st-gen TypeScript class to the 2nd-gen architecture. + +### Properties + +- [ ] `open` property migrated — `@property({ type: Boolean, reflect: true })` +- [ ] `color` property migrated — `@property({ type: String })`, default `'rgba(255, 0, 0, 0.5)'` +- [ ] `--spectrum-picked-color` CSS variable set via inline style on the SVG element, driven by `this.color` + +### Render + +- [ ] SVG loupe markup ported to `render()` method +- [ ] SVG path bug fixed: `61.575ZZ` → `61.575Z` +- [ ] Mask ID bug fixed: `` inside `` → `` +- [ ] Opacity checkerboard markup inlined using `--swc-opacity-checkerboard-*` tokens (no package import) +- [ ] `aria-hidden="true"` retained on the SVG element (decorative graphic) +- [ ] `is-open` CSS class applied when `open` is `true` + +### TypeScript + +- [ ] No separate `ColorLoupe.types.ts` — component has no enums or unions +- [ ] No public methods to migrate +- [ ] No events to define + +### Mod overrides + +- [ ] All `--mod-colorloupe-*` CSS custom properties passed through in stylesheet: + - `--mod-colorloupe-offset` + - `--mod-colorloupe-drop-shadow-x` + - `--mod-colorloupe-drop-shadow-y` + - `--mod-colorloupe-drop-shadow-blur` + - `--mod-colorloupe-drop-shadow-color` + - `--mod-colorloupe-animation-distance` + - `--mod-colorloupe-inner-border-color` + - `--mod-colorloupe-inner-border-width` + - `--mod-colorloupe-outer-border-color` + - `--mod-colorloupe-outer-border-width` + +--- + +## Phase 4: Accessibility implementation (SWC-1786) + +> Goal: Implement WCAG 2.2 Level AA accessibility requirements per the [accessibility migration analysis](./accessibility-migration-analysis.md). + +### ARIA + +- [ ] SVG element retains `aria-hidden="true"` — the loupe is decorative; accessible color information is carried by surrounding controls +- [ ] No ARIA role needed on the host element — component is not focusable and not a standalone widget + +### Focus and keyboard + +- [ ] Component is **not focusable** — confirm `tabindex` is absent and host element is skipped by keyboard navigation +- [ ] Closed state (`open: false`) does not trap focus or create phantom focus targets + +### Non-text contrast (WCAG 1.4.11) + +- [ ] Loupe chrome contrast measured on key themes (light, dark, high contrast) +- [ ] Where 3:1 contrast against adjacent UI is not achievable due to variable color backgrounds, gap is documented referencing [SWC-1193](https://jira.corp.adobe.com/browse/SWC-1193) as the product/a11y decision record +- [ ] Any known failures listed as explicit known issues — not silent + +### Forced colors / high contrast mode + +- [ ] `@media (forced-colors: active)` styles verified to maintain loupe shape visibility +- [ ] `forced-color-adjust` applied where needed to avoid invisible borders in Windows High Contrast mode + +### Documentation notes + +- [ ] Component documentation states the loupe is a visual aid, not a standalone accessible color control +- [ ] Authors are pointed to color field / picker docs for labels, keyboard navigation, and value communication + +--- + +## Phase 5: S2 visual fidelity and CSS (SWC-1787) + +> Goal: Achieve full Spectrum 2 visual fidelity by migrating all CSS tokens. + +### Token migration + +- [ ] S1 drop-shadow tokens replaced with S2 equivalents: + - `--spectrum-drop-shadow-x` → `--spectrum-drop-shadow-elevated-x` + - `--spectrum-color-loupe-drop-shadow-y` → `--spectrum-drop-shadow-elevated-y` + - `--spectrum-color-loupe-drop-shadow-blur` → `--spectrum-drop-shadow-elevated-blur` + - `--spectrum-color-loupe-drop-shadow-color` → `--spectrum-drop-shadow-elevated-color` +- [ ] All `--spectrum-colorloupe-*` intermediary tokens consumed via `token('color-loupe-*')` helper +- [ ] All `--spectrum-color-handle-*` tokens consumed correctly for handle size and border width +- [ ] Opacity checkerboard tokens migrated to `--swc-opacity-checkerboard-square-dark` / `--swc-opacity-checkerboard-square-light` + +### Visual review + +- [ ] Loupe shape, size, and border rendering matches S2 Figma spec at all standard viewports +- [ ] Open/closed animation transition is smooth and matches S2 motion spec +- [ ] Transparency checkerboard renders correctly behind semi-transparent colors +- [ ] Inner and outer border styling matches S2 spec +- [ ] VRT (visual regression test) baselines captured and approved + +--- + +## Phase 6: Code style conformance (SWC-1788) + +> Goal: Ensure the implementation follows all project style guides and linting rules. + +- [ ] CSS passes stylelint with no errors (see `stylelint.config.js`) +- [ ] No duplicate CSS properties (cascade order respected) +- [ ] High-contrast and other media queries sorted to bottom of CSS file +- [ ] Copyright header reflects current year in all files +- [ ] Comments in CSS use sentence case +- [ ] TypeScript passes ESLint with no errors +- [ ] No `xlink:href` deprecated SVG attributes — use `href` if supported, or document why `xlink:href` is retained +- [ ] Imports are ordered consistently per project conventions +- [ ] No unused imports or dead code + +--- + +## Phase 7: Test suites (SWC-1789) + +> Goal: Achieve complete test coverage following 2nd-gen patterns. + +### Unit tests + +- [ ] `open` property: toggling `open` adds/removes `is-open` class on SVG +- [ ] `color` property: changing `color` updates `--spectrum-picked-color` inline style on SVG +- [ ] Default property values verified (`open: false`, `color: 'rgba(255, 0, 0, 0.5)'`) +- [ ] SVG `aria-hidden="true"` attribute is present in rendered DOM +- [ ] Component is not in the tab order (no `tabindex` on host or internals) +- [ ] Mask ID fix verified: `` inside `` references `#loupe-path`, not `#path` +- [ ] Path data has single `Z` close command (no `ZZ` bug) + +### Accessibility tests (axe) + +- [ ] axe runs on composite stories (loupe + color field / sliders), not the loupe in isolation +- [ ] No axe violations on composite Storybook stories +- [ ] Contrast checks run on representative backgrounds; gaps documented per SWC-1193 + +### Integration / E2E tests + +- [ ] Snapshot / VRT tests use realistic page layouts (labeled controls, focus order, value text on surrounding elements) +- [ ] `open` toggling produces expected visual diff captured by VRT + +### Browser compatibility + +- [ ] Tests pass in Chrome, Firefox, Safari +- [ ] SVG mask rendering verified in all supported browsers + +--- + +## Phase 8: Storybook documentation (SWC-1790) + +> Goal: Create complete, accurate, and accessible Storybook stories following the 2nd-gen stories format. + +- [ ] `color-loupe.stories.ts` created in `2nd-gen/packages/swc/components/color-loupe/stories/` +- [ ] Stories follow the format defined in `.ai/rules/stories-format.md` +- [ ] Meta object includes: `title`, `component`, `args`, `argTypes`, `render`, `parameters.docs.subtitle`, `tags: ['migrated']` +- [ ] JSDoc description above meta object explains the component's purpose +- [ ] **Playground** story — `['autodocs', 'dev']` tags, most common use case args +- [ ] **Overview** story — `['overview']` tag +- [ ] **Anatomy** story — `['anatomy']` tag, shows loupe with opacity checkerboard and with solid color +- [ ] **Options** stories — `['options']` tag: + - [ ] Open / closed states shown as options (since `open` is a visual toggle) + - [ ] Representative color values demonstrating transparency handling +- [ ] **States** story — `['states']` tag: open, closed, with transparent color +- [ ] **Accessibility** story — `['a11y']` tag — loupe shown in context with a color field or labeled picker, not in isolation +- [ ] All stories demonstrate accessible usage (no placeholder-only content) +- [ ] `flexLayout: true` used for multi-item comparison stories +- [ ] Figma design link added to `parameters.design` + +--- + +## Phase 9: Consumer migration guide (SWC-1791) + +> Goal: Produce a guide for teams upgrading from `` (1st-gen) to `` (2nd-gen). + +- [ ] Guide created at the agreed CONTRIBUTOR-DOCS or docs-site location +- [ ] Element tag rename documented: `` → `` +- [ ] Import path changes documented +- [ ] Confirm `open` and `color` properties are unchanged — no migration action required for consumers using these +- [ ] Opacity checkerboard: consumers who imported `@spectrum-web-components/opacity-checkerboard` alongside the loupe are notified the dependency is now internal +- [ ] CSS custom property changes documented: + - Drop-shadow token renames (S1 → S2) listed for consumers using mod overrides +- [ ] Note that the component is not focusable and should always be paired with a fully labeled color control +- [ ] Guide reviewed by at least one other engineer before shipping + +--- + +## Phase 10: Review and finalize (SWC-1792) + +> Goal: Final quality gate before the migration is considered complete. + +- [ ] All prior phases complete and their Jira tickets closed +- [ ] All automated tests pass (unit, a11y, VRT, integration) +- [ ] VRT baselines approved +- [ ] Code reviewed by at least one other engineer +- [ ] Accessibility review complete — known gaps documented with references to SWC-1193 +- [ ] Storybook docs reviewed for accuracy and accessible content +- [ ] Consumer migration guide reviewed and approved +- [ ] No open blocking issues or unresolved questions +- [ ] Changeset written and included in PR +- [ ] PR linked to parent epic [SWC-1781](https://jira.corp.adobe.com/browse/SWC-1781) +- [ ] Release notes / CHANGELOG updated as needed + +--- + +## Known issues and decisions + +| Issue | Status | Reference | +|-------|--------|-----------| +| Non-text contrast (WCAG 1.4.11) on loupe chrome — may not be achievable at 3:1 for all color states due to variable overlaid content | Documented risk; accepted per product/a11y decision | [SWC-1193](https://jira.corp.adobe.com/browse/SWC-1193) | +| SVG mask ID mismatch (`#path` instead of `#loupe-path`) in 1st-gen | Fix in 2nd-gen render method | [Rendering analysis](./rendering-and-styling-migration-analysis.md) | +| Double-Z SVG path close command (`ZZ` in 1st-gen) | Fix in 2nd-gen render method | [Rendering analysis](./rendering-and-styling-migration-analysis.md) | +| Opacity checkerboard imported from external package in 1st-gen | Inline in 2nd-gen using `--swc-opacity-checkerboard-*` tokens | [Rendering analysis](./rendering-and-styling-migration-analysis.md) | + +--- + +## References + +- [SWC-1781 — Migration of the color-loupe (epic)](https://jira.corp.adobe.com/browse/SWC-1781) +- [SWC-1783 — Analyze component and create migration plan](https://jira.corp.adobe.com/browse/SWC-1783) +- [Rendering and styling migration analysis](./rendering-and-styling-migration-analysis.md) +- [Accessibility migration analysis](./accessibility-migration-analysis.md) +- [SWC-1193 — Non-text contrast / realistic constraints for color loupe](https://jira.corp.adobe.com/browse/SWC-1193) +- [Washing machine workflow guide](../../../01_contributor-guides/README.md) +- [WCAG 2.2 SC 1.4.11 — Non-text contrast](https://www.w3.org/WAI/WCAG22/Understanding/non-text-contrast) diff --git a/CONTRIBUTOR-DOCS/03_project-planning/03_components/color-loupe/rendering-and-styling-migration-analysis.md b/CONTRIBUTOR-DOCS/03_project-planning/03_components/color-loupe/rendering-and-styling-migration-analysis.md new file mode 100644 index 00000000000..e8e91de71f3 --- /dev/null +++ b/CONTRIBUTOR-DOCS/03_project-planning/03_components/color-loupe/rendering-and-styling-migration-analysis.md @@ -0,0 +1,144 @@ + + +[CONTRIBUTOR-DOCS](../../../README.md) / [Project planning](../../README.md) / [Components](../README.md) / Color Loupe / Color loupe migration analysis + + + +# Color loupe migration analysis + + + +
+In this doc + +- [Properties](#properties) +- [Slots](#slots) +- [Events](#events) +- [CSS custom properties](#css-custom-properties) + - [Spectrum tokens consumed (host / loupe)](#spectrum-tokens-consumed-host--loupe) + - [Mod overrides](#mod-overrides) + - [Set in TypeScript](#set-in-typescript) +- [Mixins](#mixins) +- [Render DOM structure](#render-dom-structure) +- [Gen-2 delta notes](#gen-2-delta-notes) +- [Factor assessment](#factor-assessment) + +
+ + + +## Properties + +| Name | Type | Default | Reflects | Deprecated | +| ------- | --------- | ------------------------ | -------- | ---------- | +| `open` | `boolean` | `false` | Yes | No | +| `color` | `string` | `'rgba(255, 0, 0, 0.5)'` | No | No | + +## Slots + +None. The component renders only internal markup. + +## Events + +None dispatched. + +## CSS custom properties + +### Spectrum tokens consumed (host / loupe) + +- `--spectrum-color-handle-size` +- `--spectrum-color-handle-outer-border-width` +- `--spectrum-color-loupe-bottom-to-color-handle` +- `--spectrum-color-loupe-width` +- `--spectrum-color-loupe-height` +- `--spectrum-drop-shadow-x` (S1) / `--spectrum-drop-shadow-elevated-x` (S2) +- `--spectrum-color-loupe-drop-shadow-y` (S1) / `--spectrum-drop-shadow-elevated-y` (S2) +- `--spectrum-color-loupe-drop-shadow-blur` (S1) / `--spectrum-drop-shadow-elevated-blur` (S2) +- `--spectrum-color-loupe-drop-shadow-color` (S1) / `--spectrum-drop-shadow-elevated-color` (S2) +- `--spectrum-color-loupe-inner-border` +- `--spectrum-color-loupe-inner-border-width` +- `--spectrum-color-loupe-outer-border` +- `--spectrum-color-loupe-outer-border-width` +- `--spectrum-opacity-checkerboard-square-dark` +- `--spectrum-opacity-checkerboard-square-light` + +### Mod overrides + +- `--mod-colorloupe-offset` +- `--mod-colorloupe-drop-shadow-x` +- `--mod-colorloupe-drop-shadow-y` +- `--mod-colorloupe-drop-shadow-blur` +- `--mod-colorloupe-drop-shadow-color` +- `--mod-colorloupe-animation-distance` +- `--mod-colorloupe-inner-border-color` +- `--mod-colorloupe-inner-border-width` +- `--mod-colorloupe-outer-border-color` +- `--mod-colorloupe-outer-border-width` + +### Set in TypeScript + +- `--spectrum-picked-color` — inline style on ``, driven by `this.color` + +## Mixins + +None. Extends `SpectrumElement` directly. + +## Render DOM structure + +```html + +
+ +
+ +
+ + +``` + +## Gen-2 delta notes + +- **Double-Z path bug**: The SVG path data ends with `61.575ZZ` (double close command); fix to `61.575Z`. +- **Mask ID mismatch**: `` inside `` references a non-existent `#path` ID — should be `#loupe-path`. Fix in gen-2. +- **S2 token migration**: The `spectrum-two` branch of spectrum-css introduces `--spectrum-colorloupe-*` intermediary tokens and switches drop shadows to `--spectrum-drop-shadow-elevated-*`. In SWC gen-2 these map to `token("color-loupe-*")` and `token("drop-shadow-elevated-*")`. +- **Opacity checkerboard**: 1st-gen imports from `@spectrum-web-components/opacity-checkerboard`. Gen-2 should inline the checkerboard pattern using `--swc-opacity-checkerboard-*` tokens from `tokens.css`. +- **No new S2 properties anticipated**: The component API (`open`, `color`) is unchanged between S1 and S2. + +## Factor assessment + +**Recommendation: SKIP_FACTOR** + +The component is purely presentational with only two `@property` fields, no lifecycle methods beyond the inherited defaults, and no event dispatching. The `render()` method is tightly coupled to the `color` property (inline style) and `open` state (CSS-driven). There is no separable state logic to extract. Total substantive logic is under 40 lines.