diff --git a/2nd-gen/packages/swc/patterns/conversational-ai/prompt-field/PromptField.ts b/2nd-gen/packages/swc/patterns/conversational-ai/prompt-field/PromptField.ts index 630fe155af2..98e5c81cc24 100644 --- a/2nd-gen/packages/swc/patterns/conversational-ai/prompt-field/PromptField.ts +++ b/2nd-gen/packages/swc/patterns/conversational-ai/prompt-field/PromptField.ts @@ -28,7 +28,6 @@ import { ifDefined } from 'lit/directives/if-defined.js'; import { styleMap } from 'lit/directives/style-map.js'; import { ResizeController } from '@lit-labs/observers/resize-controller.js'; -import { Chevron75Icon } from '@adobe/spectrum-wc/icon/elements/index.js'; import { focusgroupNavigationActiveChange, type FocusgroupNavigationActiveChangeDetail, @@ -40,6 +39,7 @@ import { getActiveElement, } from '@adobe/spectrum-wc-core/utils/index.js'; +import '@adobe/spectrum-wc/components/action-button/swc-action-button.js'; import '@adobe/spectrum-wc/components/icon/swc-icon.js'; import '../../../components/ui-icons/swc-ui-icon.js'; @@ -583,7 +583,8 @@ export class PromptField extends SpectrumElement { // wherever focus was before the page turned) — its Close button when // visible, mirroring the forward tile -> Close -> Next chain in reverse, // otherwise the tile itself. - if (active === nextButton && event.shiftKey) { + // deepContains: focus delegates to the chevron's inner shadow button. + if (nextButton && deepContains(nextButton, active) && event.shiftKey) { const activeTile = this._artifactNavigation.getActiveItem(); if (activeTile) { event.preventDefault(); @@ -594,7 +595,7 @@ export class PromptField extends SpectrumElement { // From the Prev chevron: plain Tab moves forward into the roving // controller's current active tile, for the same reason as above. - if (active === prevButton && !event.shiftKey) { + if (prevButton && deepContains(prevButton, active) && !event.shiftKey) { const activeTile = this._artifactNavigation.getActiveItem(); if (activeTile) { event.preventDefault(); @@ -908,18 +909,22 @@ export class PromptField extends SpectrumElement { > ${this._artifactScrollOverflow ? html` - + + ` : nothing}
${this._artifactScrollOverflow ? html` - + + ` : nothing}
@@ -984,26 +993,26 @@ export class PromptField extends SpectrumElement { private _renderSendButton(): TemplateResult { return html` - + + `; } private _renderStopButton(): TemplateResult { return html` - + + `; } @@ -1070,14 +1079,17 @@ export class PromptField extends SpectrumElement { .inert=${this.collapsed} >
- + +
${showStop ? this._renderStopButton() : this._renderSendButton()} diff --git a/2nd-gen/packages/swc/patterns/conversational-ai/prompt-field/prompt-field.css b/2nd-gen/packages/swc/patterns/conversational-ai/prompt-field/prompt-field.css index 612debf3aed..f5056b7e6ad 100644 --- a/2nd-gen/packages/swc/patterns/conversational-ai/prompt-field/prompt-field.css +++ b/2nd-gen/packages/swc/patterns/conversational-ai/prompt-field/prompt-field.css @@ -284,30 +284,27 @@ .swc-PromptField-artifacts-scroll-prev, .swc-PromptField-artifacts-scroll-next { - display: inline-flex; position: absolute; inset-block-start: calc((var(--swc-prompt-field-artifact-badge-clearance, 0) + var(--swc-prompt-field-artifact-media-block-size, 68px)) / 2); z-index: 4; - flex: 0 0 auto; - align-items: center; - justify-content: center; - inline-size: var(--swc-prompt-field-artifact-scroll-button-inline-size, 32px); - block-size: var(--swc-prompt-field-artifact-scroll-button-block-size, 32px); - padding: 0; - color: token("gray-800"); - background: var(--_swc-prompt-field-artifact-scroll-button-background); - border: 1px solid transparent; - border-radius: token("corner-radius-800"); cursor: pointer; transform: translateY(-50%); - --_swc-prompt-field-artifact-scroll-button-background: token("gray-100"); --_swc-prompt-field-artifact-scroll-icon-transform: none; + --swc-action-button-min-block-size: var(--swc-prompt-field-artifact-scroll-button-block-size, 32px); + --swc-action-button-icon-size: token("ui-icon-small"); + --swc-action-button-edge-to-visual-only: calc((var(--swc-prompt-field-artifact-scroll-button-inline-size, 32px) - token("ui-icon-small")) / 2); + --swc-action-button-border-radius: token("corner-radius-800"); + --swc-action-button-content-color-default: token("gray-800"); + --swc-action-button-content-color-hover: token("gray-800"); + --swc-action-button-background-color-default: token("gray-100"); + --swc-action-button-background-color-hover: token("gray-200"); + --swc-action-button-down-state-transform: none; } /* aria-disabled, not the disabled attribute, and hidden with opacity, not * display/visibility: a chevron at the end of the strip stays mounted and - * keeps any focus it already has (native disabled and display/visibility + * keeps any focus it already has (the disabled property and display/visibility * removal both blur it), so the button never needs a manual focus-redirect * when scrolling makes it unusable. */ .swc-PromptField-artifacts-scroll-prev[aria-disabled="true"], @@ -324,10 +321,8 @@ * not landing on a clean device pixel, which can leave a 1px unpainted * sliver at a mask boundary that touches its edge exactly. * - * z-index: -1, not 0: the button's own :focus-visible outline paints with - * its background/border layer, before z-index: 0 positioned descendants, - * so a z-index: 0 mask paints over (hides) the outline. A negative z-index - * puts this mask in the layer painted before that, so the outline is never + * z-index: -1 keeps the mask behind the shadow button face and its focus + * ring, which paint in the descendants layer above it, so neither is * covered. */ .swc-PromptField-artifacts-scroll-prev::before, .swc-PromptField-artifacts-scroll-next::before { @@ -340,17 +335,6 @@ content: ""; } -.swc-PromptField-artifacts-scroll-prev::after, -.swc-PromptField-artifacts-scroll-next::after { - position: absolute; - inset: 0; - z-index: 1; - background: var(--_swc-prompt-field-artifact-scroll-button-background); - border-radius: inherit; - pointer-events: none; - content: ""; -} - .swc-PromptField-artifacts-scroll-prev { inset-inline-start: 0; @@ -371,27 +355,11 @@ background: token("background-layer-2-color"); } -.swc-PromptField-artifacts-scroll-prev:hover, -.swc-PromptField-artifacts-scroll-next:hover { - --_swc-prompt-field-artifact-scroll-button-background: token("gray-200"); -} - -.swc-PromptField-artifacts-scroll-prev swc-icon, -.swc-PromptField-artifacts-scroll-next swc-icon { - display: inline-flex; - position: relative; - z-index: 2; - align-items: center; - justify-content: center; +.swc-PromptField-artifacts-scroll-prev swc-ui-icon, +.swc-PromptField-artifacts-scroll-next swc-ui-icon { transform: var(--_swc-prompt-field-artifact-scroll-icon-transform); } -.swc-PromptField-artifacts-scroll-prev:focus-visible, -.swc-PromptField-artifacts-scroll-next:focus-visible { - outline: token("focus-indicator-thickness") solid token("focus-indicator-color"); - outline-offset: token("focus-ring-gap"); -} - :host(:dir(rtl)) .swc-PromptField-artifacts-scroll-prev { --_swc-prompt-field-artifact-scroll-icon-transform: none; } @@ -477,62 +445,19 @@ } /* Upload button (quiet, left side) */ - .swc-PromptField-upload { - display: flex; - align-items: center; - justify-content: center; - inline-size: var(--swc-prompt-field-upload-inline-size, 32px); - block-size: var(--swc-prompt-field-upload-block-size, 32px); - padding: 0; - color: token("gray-800"); - background: transparent; - border: 1px solid transparent; - border-radius: token("corner-radius-500"); -} - -.swc-PromptField-upload:hover { - color: token("gray-800"); - background: token("gray-75"); + --swc-action-button-min-block-size: var(--swc-prompt-field-upload-block-size, 32px); + --swc-action-button-edge-to-visual-only: calc((var(--swc-prompt-field-upload-inline-size, 32px) - token("workflow-icon-medium")) / 2); + --swc-action-button-icon-size: token("workflow-icon-medium"); + --swc-action-button-border-radius: token("corner-radius-500"); + --swc-action-button-content-color-default: token("gray-800"); + --swc-action-button-content-color-hover: token("gray-800"); + --swc-action-button-background-color-hover: token("gray-75"); + --swc-action-button-content-color-disabled: token("gray-400"); + --swc-action-button-background-color-disabled: transparent; } -.swc-PromptField-upload:disabled { - color: token("gray-400"); - background: transparent; -} - -.swc-PromptField-upload swc-icon { - --swc-icon-inline-size: token("workflow-icon-medium"); - --swc-icon-block-size: token("workflow-icon-medium"); -} - -/* Send button (filled circle, right side) */ - -.swc-PromptField-send { - display: flex; - align-items: center; - justify-content: center; - inline-size: var(--swc-prompt-field-send-inline-size, 32px); - block-size: var(--swc-prompt-field-send-block-size, 32px); - padding: 0; - color: token("gray-25"); - background: token("neutral-background-color-default"); - border: 1px solid transparent; - border-radius: token("corner-radius-full"); -} - -.swc-PromptField-stop { - display: flex; - align-items: center; - justify-content: center; - inline-size: var(--swc-prompt-field-stop-inline-size, 32px); - block-size: var(--swc-prompt-field-stop-block-size, 32px); - padding: 0; - color: token("gray-25"); - background: token("gray-900"); - border: 1px solid transparent; - border-radius: token("corner-radius-full"); -} +/* Send/stop buttons (filled round, anchored bottom-right) */ /* Anchored to .controls's own bottom-right, so it re-aligns on its own as .controls grows or shrinks with either row's content. */ .swc-PromptField-send, @@ -540,46 +465,34 @@ position: absolute; inset-block-end: 0; inset-inline-end: 0; - transition: background token("animation-duration-100") ease; -} - -.swc-PromptField-send:disabled { - color: token("gray-400"); - background: token("gray-100"); -} - -.swc-PromptField-send:focus-visible, -.swc-PromptField-stop:focus-visible { - outline: token("focus-indicator-thickness") solid token("focus-indicator-color"); - outline-offset: token("focus-ring-gap"); -} -.swc-PromptField-send:active, -.swc-PromptField-stop:active { - transform: perspective(64px) translate3d(0, 0, -1px); - transition: transform token("animation-duration-200") ease; - transition-timing-function: token("animation-ease-in-out"); - will-change: transform; + --swc-action-button-min-block-size: var(--swc-prompt-field-send-block-size, 32px); + --swc-action-button-edge-to-visual-only: calc((var(--swc-prompt-field-send-inline-size, 32px) - token("workflow-icon-medium")) / 2); + --swc-action-button-icon-size: token("workflow-icon-medium"); + --swc-action-button-border-radius: token("corner-radius-full"); + --swc-action-button-down-state-transform: perspective(64px) translate3d(0, 0, -1px); } -.swc-PromptField-send:hover:not(:disabled) { - background: token("neutral-background-color-hover"); -} - -.swc-PromptField-send swc-icon { - --swc-icon-inline-size: token("workflow-icon-medium"); - --swc-icon-block-size: token("workflow-icon-medium"); -} - -/* Stop button (filled black circle, right side) */ - -.swc-PromptField-stop:hover { - background: token("gray-800"); +/* Send button (filled neutral circle) */ +.swc-PromptField-send { + --swc-action-button-content-color-default: token("gray-25"); + --swc-action-button-content-color-hover: token("gray-25"); + --swc-action-button-content-color-down: token("gray-25"); + --swc-action-button-background-color-default: token("neutral-background-color-default"); + --swc-action-button-background-color-hover: token("neutral-background-color-hover"); + --swc-action-button-background-color-down: token("neutral-background-color-default"); + --swc-action-button-content-color-disabled: token("gray-400"); + --swc-action-button-background-color-disabled: token("gray-100"); } -.swc-PromptField-stop swc-icon { - --swc-icon-inline-size: token("workflow-icon-medium"); - --swc-icon-block-size: token("workflow-icon-medium"); +/* Stop button (filled black circle) */ +.swc-PromptField-stop { + --swc-action-button-content-color-default: token("gray-25"); + --swc-action-button-content-color-hover: token("gray-25"); + --swc-action-button-content-color-down: token("gray-25"); + --swc-action-button-background-color-default: token("gray-900"); + --swc-action-button-background-color-hover: token("gray-800"); + --swc-action-button-background-color-down: token("gray-900"); } /* ───────────────────────────────────── diff --git a/2nd-gen/packages/swc/patterns/conversational-ai/prompt-field/test/prompt-field.test.ts b/2nd-gen/packages/swc/patterns/conversational-ai/prompt-field/test/prompt-field.test.ts index ddf1080ba65..912f4a66701 100644 --- a/2nd-gen/packages/swc/patterns/conversational-ai/prompt-field/test/prompt-field.test.ts +++ b/2nd-gen/packages/swc/patterns/conversational-ai/prompt-field/test/prompt-field.test.ts @@ -14,7 +14,10 @@ import { html, nothing, render } from 'lit'; import { expect, userEvent } from '@storybook/test'; import type { Meta, StoryObj as Story } from '@storybook/web-components'; -import { getActiveElement } from '@adobe/spectrum-wc-core/utils/index.js'; +import { + deepContains, + getActiveElement, +} from '@adobe/spectrum-wc-core/utils/index.js'; import '../../upload-artifact/swc-upload-artifact.js'; import '../swc-prompt-field.js'; @@ -23,6 +26,10 @@ import { getComponent, withWarningSpy } from '../../../../utils/test-utils.js'; import { PromptField } from '../PromptField.js'; import { meta, Overview } from '../stories/prompt-field.stories.js'; +/** True when focus is on a chevron or its inner shadow button. */ +const focusIsWithinChevron = (host: Element | null | undefined): boolean => + !!host && deepContains(host, getActiveElement()); + export default { ...meta, title: 'Conversational AI/Prompt field/Tests', @@ -310,7 +317,10 @@ export const ArtifactScrollPaginationTest: Story = { el.artifactScrollPrevLabel = 'Show earlier attachments'; el.artifactScrollNextLabel = 'Show later attachments'; await el.updateComplete; - expect(nextButton?.ariaLabel).toBe('Show later attachments'); + // Accessible name is passed via accessible-label on the action button. + expect(nextButton?.getAttribute('accessible-label')).toBe( + 'Show later attachments' + ); } ); @@ -340,9 +350,9 @@ export const ArtifactScrollPaginationTest: Story = { clientWidth / 2 ); expect( - el.shadowRoot?.querySelector( - '.swc-PromptField-artifacts-scroll-prev' - )?.ariaLabel + el.shadowRoot + ?.querySelector('.swc-PromptField-artifacts-scroll-prev') + ?.getAttribute('accessible-label') ).toBe('Show earlier attachments'); const tiles = scrollEl @@ -613,7 +623,7 @@ export const ArtifactFocusOrderTest: Story = { const nextButton = getNextButton(); expect(nextButton).toBeTruthy(); expect(event.defaultPrevented).toBe(true); - expect(getActiveElement()).toBe(nextButton); + expect(focusIsWithinChevron(nextButton)).toBe(true); } ); @@ -654,7 +664,7 @@ export const ArtifactFocusOrderTest: Story = { const prevButton = getPrevButton(); expect(prevButton?.getAttribute('aria-disabled')).toBe('false'); prevButton?.focus(); - expect(getActiveElement()).toBe(prevButton); + expect(focusIsWithinChevron(prevButton)).toBe(true); const secondScrollEnd = waitForScrollEnd(scrollEl); scrollEl?.scrollTo({ left: 0, behavior: 'instant' }); @@ -662,7 +672,7 @@ export const ArtifactFocusOrderTest: Story = { await el.updateComplete; expect(getPrevButton()?.getAttribute('aria-disabled')).toBe('true'); - expect(getActiveElement()).toBe(prevButton); + expect(focusIsWithinChevron(prevButton)).toBe(true); } ); @@ -758,7 +768,7 @@ export const ArtifactChevronPagingFocusTest: Story = { await scrollEnd; await el.updateComplete; - expect(getActiveElement()).toBe(nextButton); + expect(focusIsWithinChevron(nextButton)).toBe(true); } ); @@ -806,7 +816,7 @@ export const ArtifactChevronPagingFocusTest: Story = { await backPage; await el.updateComplete; - expect(getActiveElement()).toBe(prevButton); + expect(focusIsWithinChevron(prevButton)).toBe(true); const tilesAfterPagingBack = visibleTiles(); expect(