Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
ec225d9
feat: Add validation states to ControlGroup
jperals Oct 13, 2026
9bb8c6a
fix: Render ControlGroup validation action seam on the control not th…
jperals Oct 13, 2026
6e1e935
fix: Render ControlGroup validation thick border on first control inl…
jperals Oct 13, 2026
6f70aca
refactor: Convert ControlGroup validation dev page to a permutations …
jperals Oct 13, 2026
63b994d
fix: Color ControlGroup action button inline-start border in validati…
jperals Oct 13, 2026
e13b8b5
chore: Add segmented-control last-control permutation to ControlGroup…
jperals Oct 13, 2026
4b30530
fix: Publish ControlGroup validation color on the group wrapper so th…
jperals Oct 13, 2026
444a630
fix: Layer focus ring over preserved ControlGroup validation border
jperals Oct 13, 2026
0ab46fe
chore: Add multiselect permutations to ControlGroup validation page
jperals Oct 13, 2026
30cf375
fix: Preserve ControlGroup validation border color on hover
jperals Oct 13, 2026
e43dc2b
fix: Keep thick invalid border on inline-start of focused select and …
jperals Oct 13, 2026
0e196f5
fix: Keep ControlGroup validation border color on keyboard focus
jperals Oct 13, 2026
60bf87b
refactor: Drive ControlGroup validation color from context state inst…
jperals Oct 14, 2026
110252b
chore: Increase widget export size limit for ControlGroup validation
jperals Oct 14, 2026
e98ab5b
refactor: Use a single validation field on ControlGroup context
jperals Oct 14, 2026
767925b
refactor: Dedupe ControlGroup validation ring and state styles
jperals Oct 14, 2026
809acad
chore: Lower widget export size limit after SCSS dedup
jperals Oct 14, 2026
9f17058
fix: Keep validation border color on grouped controls and action butt…
jperals Oct 14, 2026
6a1a066
Revert "fix: Keep validation border color on grouped controls and act…
jperals Oct 14, 2026
297adb6
fix: Keep standalone select and multiselect validation border on keyb…
jperals Oct 14, 2026
a9f6c2b
chore: Split standalone validation-focus dev pages for select and mul…
jperals Oct 14, 2026
8833170
Refactor
jperals Oct 14, 2026
fd83aa6
chore: Use shared FocusTarget in validation-focus dev pages
jperals Oct 14, 2026
5872df6
chore: Move FocusTarget to settings slot so it is excluded from scree…
jperals Oct 14, 2026
897ac50
chore: Drop captioned Section from validation-focus dev pages
jperals Oct 14, 2026
1af6022
Refine page title
jperals Oct 14, 2026
c818f9b
Refine dev page
jperals Oct 14, 2026
65b9ff6
Fix and simplify borders
jperals Oct 14, 2026
7e5e0d4
refactor: Remove unused neutral-color argument from grouped-control m…
jperals Oct 14, 2026
c1a3fff
chore: Revert visual test definition changes
jperals Oct 14, 2026
db46ce0
test: Remove ControlGroup validation classname unit tests
jperals Oct 14, 2026
cda1254
chore: Remove unused ControlGroup validation test-util classes
jperals Oct 14, 2026
034e1b9
fix: Draw ControlGroup validation leading bar as a pseudo-element to …
jperals Oct 15, 2026
3f4a8b7
fix: Paint ControlGroup validation leading bar on the group slot to f…
jperals Oct 15, 2026
fcbb69a
Add inline labels to dev page
jperals Oct 15, 2026
eb21ba2
Refine dev page
jperals Oct 15, 2026
4d681d0
refactor: Let ControlGroup controls own the action-button seam border
jperals Oct 15, 2026
b3a8f27
Fixes
jperals Oct 15, 2026
bf0fd99
Merge remote-tracking branch 'origin/main' into dev-v3-jotresse-contr…
jperals Oct 16, 2026
5fcf584
fix: Correct invalid-state inline padding in prompt input
jperals Oct 16, 2026
22fa300
Fixes and improvements
jperals Oct 17, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,7 @@
{
"path": "lib/components/internal/widget-exports.js",
"brotli": false,
"limit": "1450 kB",
"limit": "1470 kB",
"ignore": "react-dom"
}
],
Expand Down
63 changes: 63 additions & 0 deletions pages/control-group/validation.page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
import React from 'react';

import FormField from '~components/form-field';
import ControlGroup, { ControlGroupProps } from '~components/internal/components/control-group';

import { PermutationsPage } from '../app/templates';
import createPermutations from '../utils/permutations';
import PermutationsView from '../utils/permutations-view';
import { input, multiselect, segmentedControl, select } from './common';

type ValidationPermutation = ControlGroupProps & { errorText?: string; warningText?: string };

const children = [
<>
{input}
{select}
</>,
<>
{input}
{segmentedControl}
</>,
<>
{select}
{multiselect}
</>,
];

const directions: ReadonlyArray<ControlGroupProps['direction']> = ['horizontal', 'vertical'];

const actionButtons: ReadonlyArray<ControlGroupProps['actionButton']> = [
undefined,
{ iconName: 'remove', ariaLabel: 'Remove' },
];

const labels = [undefined, 'Label'];

const permutations = createPermutations<ValidationPermutation>([
{ children, errorText: ['This value is required'], direction: directions, actionButton: actionButtons },
{
children,
warningText: ['Double-check this value'],
direction: directions,
actionButton: actionButtons,
inlineLabelText: labels,
},
]);

export default function ControlGroupValidation() {
return (
<PermutationsPage title="Control group validation" i18n={{}}>
<PermutationsView
permutations={permutations}
render={({ errorText, warningText, ...controlGroupProps }) => (
<FormField errorText={errorText} warningText={warningText}>
<ControlGroup {...controlGroupProps} />
</FormField>
)}
/>
</PermutationsPage>
);
}
46 changes: 46 additions & 0 deletions pages/multiselect/validation-focus.page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
import React from 'react';

import Multiselect from '~components/multiselect';
import SpaceBetween from '~components/space-between';

import { SimplePage } from '../app/templates';
import FocusTarget from '../common/focus-target';

const options = [
{ value: 'option1', label: 'Option 1' },
{ value: 'option2', label: 'Option 2' },
{ value: 'option3', label: 'Option 3' },
];

export default function MultiselectValidationFocusPage() {
return (
<SimplePage
title="Multiselect validation focus"
screenshotArea={{ disableAnimations: true }}
settings={<FocusTarget />}
>
<SpaceBetween size="m">
<Multiselect
data-testid="multiselect-invalid"
ariaLabel="multiselect invalid"
invalid={true}
options={options}
selectedOptions={[]}
placeholder="Choose options"
onChange={() => {}}
/>
<Multiselect
data-testid="multiselect-warning"
ariaLabel="multiselect warning"
warning={true}
options={options}
selectedOptions={[]}
placeholder="Choose options"
onChange={() => {}}
/>
</SpaceBetween>
</SimplePage>
);
}
42 changes: 42 additions & 0 deletions pages/select/validation-focus.page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
import React from 'react';

import Select from '~components/select';
import SpaceBetween from '~components/space-between';

import { SimplePage } from '../app/templates';
import FocusTarget from '../common/focus-target';

const options = [
{ value: 'option1', label: 'Option 1' },
{ value: 'option2', label: 'Option 2' },
{ value: 'option3', label: 'Option 3' },
];

export default function SelectValidationFocusPage() {
return (
<SimplePage title="Select validation focus" screenshotArea={{ disableAnimations: true }} settings={<FocusTarget />}>
<SpaceBetween size="m">
<Select
data-testid="select-invalid"
ariaLabel="select invalid"
invalid={true}
options={options}
selectedOption={null}
placeholder="Choose an option"
onChange={() => {}}
/>
<Select
data-testid="select-warning"
ariaLabel="select warning"
warning={true}
options={options}
selectedOption={null}
placeholder="Choose an option"
onChange={() => {}}
/>
</SpaceBetween>
</SimplePage>
);
}
30 changes: 19 additions & 11 deletions src/button/styles.scss
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
@use '../internal/generated/custom-css-properties/index.scss' as custom-props;
@use '@cloudscape-design/component-toolkit/internal/focus-visible' as focus-visible;
@use './constants' as constants;
@use '../internal/styles/forms/constants' as form-constants;
@use '../internal/styles/style-api' as style-api;

$props: style-api.combine(style-api.$surface, (box-shadow, padding-block, padding-inline), style-api.$focus-ring);
Expand Down Expand Up @@ -100,6 +101,9 @@ $tokens: style-api.resolve($props, carrier);
@include styles.styles-reset;
@include styles.text-wrapping;
@include styles.font-button;

$focus-border-color: var(#{custom-props.$styleFocusRingBorderColor}, awsui.$color-border-item-focused);

letter-spacing: awsui.$font-button-letter-spacing;
border-start-start-radius: style-api.value($tokens, border-radius, awsui.$border-radius-button);
border-start-end-radius: style-api.value($tokens, border-radius, awsui.$border-radius-button);
Expand Down Expand Up @@ -128,10 +132,9 @@ $tokens: style-api.resolve($props, carrier);
@include focus-visible.when-visible {
$radius: var(#{custom-props.$styleFocusRingBorderRadius}, awsui.$border-radius-control-default-focus-ring);
$width: var(#{custom-props.$styleFocusRingBorderWidth}, foundation.$box-shadow-focused-width);
$color: var(#{custom-props.$styleFocusRingBorderColor}, awsui.$color-border-item-focused);

&:not(.variant-control-group-action) {
@include style-api.focus-highlight($radius: $radius, $width: $width, $color: $color);
@include style-api.focus-highlight($radius: $radius, $width: $width, $color: $focus-border-color);
}

&.variant-icon,
Expand All @@ -141,7 +144,7 @@ $tokens: style-api.resolve($props, carrier);
$gutter-block: awsui.$space-button-icon-focus-outline-gutter-vertical,
$radius: $radius,
$width: $width,
$color: $color
$color: $focus-border-color
);
}

Expand All @@ -152,14 +155,7 @@ $tokens: style-api.resolve($props, carrier);
$gutter-inline: awsui.$space-button-inline-icon-focus-outline-gutter,
$radius: $radius,
$width: $width,
$color: $color
);
}

&.variant-control-group-action {
@include styles.form-focus-element(
$border-color: $color,
$box-shadow: var(#{custom-props.$styleBoxShadowFocus}, foundation.$box-shadow-focused-light)
$color: $focus-border-color
);
}
}
Expand Down Expand Up @@ -219,6 +215,18 @@ $tokens: style-api.resolve($props, carrier);
padding-block: awsui.$space-field-vertical;
padding-inline: awsui.$space-xs;
min-block-size: awsui.$size-vertical-input;

&,
&:hover {
border-inline-start-color: transparent;
}

@include focus-visible.when-visible {
@include styles.form-focus-element(
$border-color: $focus-border-color,
$box-shadow: var(#{custom-props.$styleBoxShadowFocus}, foundation.$box-shadow-focused-light)
);
}
}

> .icon-left {
Expand Down
32 changes: 29 additions & 3 deletions src/input/styles.scss
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,15 @@
var(#{custom-props.$styleColorDefault}, awsui.$color-text-form-secondary)
);
background-color: var(#{custom-props.$styleBackgroundFocus}, awsui.$color-background-input-default);
@include styles.input-focus-styles;
&:not(.grouped-validation) {
@include styles.input-focus-styles;
}
&.grouped-validation {
@include styles.form-focus-element(
$border-color: false,
$box-shadow: var(#{custom-props.$styleBoxShadowFocus}, foundation.$box-shadow-focused-light)
);
}
}

// Keep disabled styles before validation modifiers so invalid/warning border colors win when both modifiers are present.
Expand All @@ -186,6 +194,9 @@
}

&:focus-within {
// Re-assert the validation border under focus so the neutral focus color
// (.input-field:focus-within, equal specificity) does not win by order.
border-color: var(#{custom-props.$styleBorderColorDefault}, awsui.$color-text-status-error);
box-shadow: var(#{custom-props.$styleBoxShadowFocus}, foundation.$box-shadow-focused-light-invalid);
}
}
Expand All @@ -200,6 +211,9 @@
}

&:focus-within {
// Re-assert the validation border under focus so the neutral focus color
// (.input-field:focus-within, equal specificity) does not win by order.
border-color: var(#{custom-props.$styleBorderColorDefault}, awsui.$color-text-status-warning);
box-shadow: var(#{custom-props.$styleBoxShadowFocus}, foundation.$box-shadow-focused-light-invalid);
}
}
Expand All @@ -226,6 +240,15 @@

&.grouped {
@include styles.grouped-control;

// The grouped inset (bar + cph) assumes a single-element control. Input's
// inner native .input already supplies its own cph, so the wrapper adds only
// one border width; wrapper(1px) + inner(cph) nets the same bar + invalid gap
// as single-element controls. The doubled selector matches the grouped-control
// validation rule's specificity and wins by source order.
&#{&}-validation {
padding-inline-start: awsui.$border-width-field;
}
}
}

Expand Down Expand Up @@ -262,8 +285,11 @@
cursor: default;
}

// The wider invalid/warning start border is absorbed by the first cell, so the content does not shift.
// The first cell is the prefix when present, otherwise the native input.
// Standalone only: the wider invalid/warning start border is absorbed by the first
// cell, so the content does not shift. The first cell is the prefix when present,
// otherwise the native input. Grouped controls keep their 1px gray border under the
// overlay bar and get their inset from the grouped wrapper, so the inner cell keeps
// its normal cph (no re-pad here).
.input-field-invalid,
.input-field-warning {
> .input-prefix,
Expand Down
15 changes: 13 additions & 2 deletions src/internal/components/button-trigger/styles.scss
Original file line number Diff line number Diff line change
Expand Up @@ -128,8 +128,10 @@ $icon-offset: awsui.$space-xxxs;
}

&:not(.in-filtering-token):not(.compact) {
@include focus-visible.when-visible {
@include styles.form-focus-element();
&:not(.grouped) {
@include focus-visible.when-visible {
@include styles.form-focus-element();
}
}

&:invalid {
Expand All @@ -146,6 +148,15 @@ $icon-offset: awsui.$space-xxxs;
&.warning:focus {
@include styles.form-warning-control();
}

@include focus-visible.when-visible {
&.invalid {
border-color: awsui.$color-text-status-error;
}
&.warning {
border-color: awsui.$color-text-status-warning;
}
}
}

&.custom-option {
Expand Down
20 changes: 17 additions & 3 deletions src/internal/components/control-group/grouped-control-styles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,25 @@ import { GroupedControlContextProps } from '../../context/control-group-context'
*/
export function getGroupedControlClassNames(
styles: Record<string, string>,
{ position, direction, hasAction }: GroupedControlContextProps
{ position, direction, hasAction, validation }: GroupedControlContextProps
): string[] {
if (!position) {
return [];
}
const baseClassNames = [styles.grouped, styles[`grouped-${direction}-${position}`]];
return hasAction ? [...baseClassNames, styles['grouped-with-action']] : baseClassNames;
const classNames = [styles.grouped, styles[`grouped-${direction}-${position}`]];
if (hasAction) {
classNames.push(styles['grouped-with-action']);
}
if (validation) {
// State class drives the outward validation color (and invalid ring) in SCSS.
// Every control gets it so each re-asserts its own outward edges on focus.
classNames.push(validation === 'error' ? styles['grouped-in-error'] : styles['grouped-in-warning']);
// Only the leading (inline-start) controls draw the thick validation seam:
// the first/only control horizontally, every control vertically.
const isLeading = direction === 'vertical' || position === 'first' || position === 'only';
if (isLeading) {
classNames.push(styles['grouped-validation']);
}
}
return classNames;
}
Loading
Loading