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`
-
+ ${ChevronUpIcon()}
+
`;
}
private _renderStopButton(): TemplateResult {
return html`
-
+ ${StopIcon()}
+
`;
}
@@ -1070,14 +1079,17 @@ export class PromptField extends SpectrumElement {
.inert=${this.collapsed}
>
-
+
+ ${PlusIcon()}
+
+
${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(