Repository navigation
Conversation
ControlGroup now reflects the surrounding form field's validation state (error and warning) by reading invalid/warning from the form-field context, with error taking precedence over warning. - The group owns the validation state and masks it from its children, so inner controls stop painting their own borders and inner seams between controls stay neutral. - The validation color is painted per control on each control's outward-facing edges only (first/middle/last/only, horizontal and vertical), threaded through the shared grouped-control mixin via an inherited custom property with a neutral fallback. No row-spanning overlay. - When an action button is present, the boundary between the controls and the button (the button's inline-start border) takes the validation color and reuses the same extra-thick invalid border width as the inner controls; the button's other edges stay neutral. - The seam's validation color and thickness persist through the button's hover and active states, scoped strictly to the validation + action case. Adds a FormField-wrapped validation dev page, a declarative visual-test entry, and unit tests covering context wiring, per-control edge styling, neutral seams, action-button seam, and error-over-warning precedence.
…e button Move the thick, validation-colored controls-to-action-button seam from the action button to the control(s) facing it, reusing the existing grouped-control validation-border mechanism. - getGroupedControlClassNames emits a grouped-validation class on the control facing the button (last/only horizontally, all controls vertically) when the group is in a validation state and has an action button. - The shared grouped-control mixin applies the thick inline-end border on that class, reusing constants.$invalid-control-left-border plus the standard padding compensation. The inline-end color is already published via the grouped-control border custom property. - Remove the inline-start color override and action-button-validation thick-width block from the button's variant-control-group-action; the button is neutral on all edges. Drop the action-button-validation marker wiring and the action-slot color publishing from ControlGroup. Net visual result is unchanged: one thick validation-colored boundary line between the controls and the action button, persisting through hover/active, now drawn by the control. box-sizing: border-box keeps the row from shifting. Verification: quick-build exit 0; control-group jest 28/28 passed; stylelint and eslint exit 0 on all changed files. Compiled CSS confirms the same-element compound selector .grouped.grouped-validation emits border-inline-end-width: var(--border-invalid-width, 4px) (reused token).
…ine-start edge Move the extra-thick validation border from the last (button-facing) control's inline-end edge to the first control's inline-start edge, the group's outer leading edge. Gate the thick width on validation state alone (not the presence of the action button). Reuse the existing mechanism: the shared grouped-control mixin now sets border-inline-start-width to constants.$invalid-control-left-border and padding-inline-start to the named constants.$invalid-control-left-padding token (the same width/padding the standalone invalid controls use on their inline-start edge). getGroupedControlClassNames emits grouped-validation on the leading control (first/only horizontally, every control vertically) in a validation state. The old inline-end thick width + padding compensation are removed. Button stays neutral on all edges; color publishing, masked child context, and error-over-warning precedence are unchanged. Verification: - npm run quick-build: SUCCESS - jest -c jest.unit.config.js src/internal/components/control-group: 29 passed, 29 total (seam now asserted on first control; last carries none) - stylelint grouped-control.scss: exit 0 - eslint grouped-control-styles.ts, control-group.test.tsx, index.tsx, validation.page.tsx: exit 0 - compiled input/styles.scoped.css: border-inline-start-width uses --border-invalid-width; zero inline-end thick-width occurrences
…page Replace the SimplePage-based validation page and its runtime DirectionSettings toggle with a PermutationsPage that renders a static createPermutations matrix: error/warning x direction (horizontal, vertical) x with/without action button. Each permutation renders ControlGroup inside a FormField supplying the matching errorText/warningText so validation propagates through form-field context. Dev-page-only change; ControlGroup behavior and styling are untouched. Verification: - eslint pages/control-group/validation.page.tsx: exit 0 (clean) - npm run quick-build: exit 0 (TS + SCSS compile, no errors) - jest control-group.test.tsx: 29 passed, 29 total (no regression) - stylelint skipped (no SCSS changed)
…e action button inherits it
The group masks invalid/warning flags on its children, so a grouped
Input carries no .input-invalid class to defend its border color. On
hover, Input's neutral %input-hover-styles re-sets border-color to the
neutral input color, overriding the group's outward-edge validation
color. The outward-color helper had no :hover awareness, mirroring the
focus bug already fixed by grouped-control-validation-focus.
Extend that mixin so the .grouped-in-validation marker also re-asserts
grouped-control-outer-border-color on :hover, color-only.
By-reading verification:
1. On hover of a grouped control in a validation state, the mixin's
:hover branch re-applies grouped-control-outer-border-color on the
same outward $edges per position. The grouped mixin is included via
`.grouped { @include grouped-control; }` at the end of
src/input/styles.scss, after `.input &:hover { @extend
%input-hover-styles; }`, so source order favors the re-assertion; the
.grouped-in-validation marker also raises specificity above the bare
&:hover. The validation color wins over the neutral hover color. No
!important needed.
2. No box-shadow ring on hover: the invalid ring stays in the
:focus-only branch; the :hover selector sets color only.
3. Focus unchanged: the :focus selector still gets both the color
re-assertion and foundation.$box-shadow-focused-light-invalid.
4. No thick-width change: the &-validation
border-inline-start-width/padding block is untouched (Fix B not
implemented).
Inner control-to-control seams stay neutral (hover re-colors only
outward $edges). Color comes from the published
--awsui-grouped-control-border-color property, so error-over-warning
precedence is unchanged. Color-only change, no layout shift.
…input form-focus-element wrote border-block / border-inline width-carrying shorthands on focus, resetting border-inline-start-width back to field width. This clobbered the thick inline-start validation border that form-invalid-control / form-warning-control set at the element level, so an invalid control lost its thick leading side on focus. Fix site: form-focus-element in src/internal/styles/forms/mixins.scss (source-level fix). Replace the two width-carrying shorthands with color + style longhands on all edges (border-block-style / border-inline-style: solid; border-block-color / border-inline-color: $border-color). This sets the focus color and style without resetting any edge's width, so each edge keeps the width already applied at rest. Blast radius: all form-focus-element includers rest at field width when non-invalid (input, textarea, button-trigger base) and at the thick inline-start width when invalid (form-invalid-control). The focus width reset was only ever a reset to the already-correct resting width, so dropping it changes no width any control actually shows; it only stops clobbering an already-correct width. prompt-input is gated out of validation focus; the group action button carries no thick seam. One source edit fixes both the standalone trigger and the grouped (masked) select/multiselect, which fall through to the same neutral focus path. Reasoning-based verification (read source + cascade, no suites): 1. Standalone invalid Select/Multiselect keeps its thick inline-start border on focus: form-focus-element no longer emits border-inline-start-width, so the element-level border-inline-start-width: constants.$invalid-control-left-border from &.invalid (form-invalid-control) is the only width declaration and applies; the &.invalid:focus box-shadow ring still layers over. 2. Grouped select/multiselect keeps its thick leading side on hover-while-focused: the group &-validation border-inline-start-width is no longer reset by the inner control's neutral form-focus-element. 3. Non-invalid focused controls unchanged: resting field-width border is preserved (no width reset) and all four edges get the focus color via border-block-color / border-inline-color plus border-*-style: solid. No layout shift. 4. Validation color + invalid focus ring unchanged: form-invalid-control / form-warning-control, the box-shadow rings, and the group border custom property are untouched; error-over-warning precedence and masked ControlGroup children are unchanged.
A grouped select/multiselect trigger in a validation state lost its validation border color (turned blue) on keyboard focus, because the trigger's neutral focus runs through focus-visible.when-visible, whose body[data-awsui-focus-visible] descendant outranks the group's plain :focus re-assertion. Add a when-visible branch to grouped-control-validation-focus that re-asserts the outward validation color and the invalid ring. For the trigger it compiles to (0,5,1), beating the neutral (0,4,1); for grouped Input it is (0,4,1), beating .input:focus (0,2,0). The existing :hover and plain :focus branches are kept so mouse focus and Input stay correct. No !important; wins on specificity. Thick inline-start border, inner seams, non-validation focus, and standalone controls are unaffected.
There was a problem hiding this comment.
Warning
Copilot couldn't run its full agentic review because it didn't start before the timeout. Make sure your repository has a runner available, or add a copilot-code-review.yml file specifying one with the runs-on attribute. See the docs for more details.
Copilot review overview
5 open findings
This adds new imports from the generated tokens module. In this PR, only… · New The validation CSS custom property is applied inline to both.controlsand (when present)… · New The validation CSS custom property is applied inline to both.controlsand (when present)… · New The helper’s parameter type includes'neutral', but the implementation treats any non-'error'… · New Typo/grammar in the comment: “Marker the group adds …” should be “Marker that the group adds …” (or… · New
What changed in this PR
Adds ControlGroup-level validation styling (error/warning) that applies a single outer validation border to grouped controls while keeping internal seams neutral, and updates visual coverage for the new states.
Changes:
- Publish validation state and border color from ControlGroup via context + an inherited CSS custom property.
- Update grouped-control styling to apply validation color only on outward-facing edges and preserve invalid seam behavior on focus.
- Add unit + visual permutations coverage for validation states (including action button + mixed controls).
| File | Description |
|---|---|
| test/definitions/visual/control-group.ts | Adds a new visual scenario for ControlGroup validation states. |
| src/segmented-control/styles.scss | Passes correct neutral border color into grouped-control mixin for segmented control. |
| src/internal/styles/forms/mixins.scss | Prevents focus styles from resetting border width so invalid thick seam survives focus. |
| src/internal/styles/forms/grouped-control.scss | Implements outward-edge validation coloring + focus behavior and thick leading seam for validation. |
| src/internal/generated/styles/tokens.d.ts | Adds typings for status tokens used to derive validation border colors. |
| src/internal/context/control-group-context.tsx | Extends grouped control context to include invalid/warning state and resets it for nested slots. |
| src/internal/components/control-group/test-classes/styles.scss | Adds marker classes for test-utils to detect invalid/warning state. |
| src/internal/components/control-group/styles.scss | Adds marker selectors for invalid/warning on the controls container and comments for action overlap. |
| src/internal/components/control-group/index.tsx | Reads FormField validation, publishes CSS var + context flags, masks FormField flags for children, and styles action button in validation. |
| src/internal/components/control-group/grouped-control-styles.ts | Adds validation marker/seam classes based on resolved validation state and position. |
| src/internal/components/control-group/__tests__/control-group.test.tsx | Adds unit tests for validation classes, CSS var publishing, seam application, and context masking/exposure. |
| src/internal/components/control-group/__tests__/common.tsx | Adds probes to assert FormField masking and grouped-control validation context. |
| src/button/styles.scss | Ensures action button seam + focus ring follow group validation styling via inherited CSS var. |
| pages/control-group/validation.page.tsx | Adds a visual permutations page for validation across control combinations. |
| build-tools/utils/custom-css-properties.js | Registers the new groupedControlBorderColor custom property for generation. |
🧠 Review effort: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| import { FormFieldContext, useFormFieldContext } from '../../context/form-field-context'; | ||
| import customCssProps from '../../generated/custom-css-properties'; | ||
| import { colorTextStatusError, colorTextStatusWarning } from '../../generated/styles/tokens'; |
| <div | ||
| {...(inlineLabelText ? { 'aria-labelledby': labelId } : actionButton ? {} : baseProps)} | ||
| role="group" | ||
| style={controlsStyle} | ||
| className={clsx( | ||
| styles.controls, | ||
| styles[`controls-${direction}`], | ||
| isInvalid && [styles['controls-invalid'], testUtilStyles['controls-invalid']], | ||
| isWarning && [styles['controls-warning'], testUtilStyles['controls-warning']], | ||
| actionButton && styles['controls-with-action'], | ||
| !actionButton && !inlineLabelText && rootClassnames | ||
| )} | ||
| > |
| const controlsWithAction = actionButton ? ( | ||
| <div | ||
| {...(inlineLabelText ? {} : baseProps)} | ||
| style={controlsStyle} | ||
| className={clsx(styles['group-layout'], styles[`group-layout-${direction}`], !inlineLabelText && rootClassnames)} | ||
| > |
| } | ||
|
|
||
| function renderGroupedInputs(validation: 'error' | 'warning' | 'neutral') { | ||
| const field = validation === 'error' ? { errorText: 'Required' } : { warningText: 'Careful' }; |
| @use '@cloudscape-design/component-toolkit/internal/focus-visible' as focus-visible; | ||
|
|
||
| $grouped-control-border-radius: constants.$control-border-radius; | ||
| // Marker the group adds to every control in a validation state (focus gate). |
…ead of a CSS custom property Behavior-preserving refactor: the validation border color is no longer published as the --awsui-grouped-control-border-color custom property. Instead the group emits an error/warning state class driven by the existing GroupedControlContext invalid/warning booleans, and SCSS selects the status token directly off that class. Verification by reading the resulting code: 1. Color source. index.tsx no longer imports colorTextStatus* or computes groupedControlBorderColor / controlsStyle, and sets no inline style. The custom property is fully removed: grep for groupedControlBorderColor and grouped-control-border-color across all source (.ts/.tsx/.scss/.js, excluding generated/ and .agents/) returns nothing, and the registration is deleted from build-tools/utils/custom-css-properties.js. grouped- control.scss and button/styles.scss now read awsui.$color-text-status-error / awsui.$color-text-status-warning (the exact names used by form-invalid- control / form-warning-control in mixins.scss) selected by .grouped-in-error / .grouped-in-warning and .variant-control-group-action-error / -warning. 2. Error vs warning with error precedence. grouped-control-styles.ts pushes grouped-in-error when invalid else grouped-in-warning when warning; index.tsx computes isWarning = warning && !isInvalid, so the classes are mutually exclusive and error wins. The SCSS error/warning rules pick the matching token on the same outward edges; the button variant classes do the same for the inline-start seam. 3. Preserved behaviors, unchanged specificity. Neutral path is byte-identical (bare neutral color on the position class, as before). Error/warning now sit on &.grouped-in-error / &.grouped-in-warning (one class on top of the position class) — at least as specific as the former single-class var() declaration, so they win on the same selectors against the control's neutral form-focus-element swap and the trigger's body[data-awsui-focus-visible] rule. The hover/focus re-assertions, the focus-visible when-visible branch, the invalid focus ring ($box-shadow-focused-light-invalid) for both states, the leading thick inline-start border (still gated by grouped-validation, leading-only), and the action-button seam across &,:hover,:active,.disabled, :focus,:focus-visible are all retained. Children stay masked (FormFieldContext invalid:false/warning:false). No layout shift, no new !important, no hardcoded colors. 4. No other consumer. The repo-wide grep confirms nothing besides the files touched here referenced the removed custom property; generated output under src/internal/generated/ is build-regenerated and not hand-edited. Tests updated to assert state-class application and gating (jsdom cannot assert computed border color/width or the focus ring; those stay in the visual test).
The ControlGroup validation feature adds ~27 KB of inlined CSS (grouped control validation border rules + button action variants) to the widget export bundle, pushing it from ~1445 kB to ~1473 kB. Raise the limit from 1450 kB to 1485 kB to accommodate the legitimate growth with headroom.
Collapse the two booleans (invalid, warning) on GroupedControlContext into
one field validation: 'error' | 'warning' | null.
Verification (read-only; build/test/lint NOT run per the user's watch-job
constraint):
1. The context now exposes a single validation?: GroupedControlValidation |
null field (GroupedControlValidation = 'error' | 'warning'); the invalid /
warning members are gone. createContext default and
ResetGroupedControlContext both provide validation: null.
2. Every grouped-context consumer reads validation and emits the SAME classes:
getGroupedControlClassNames pushes grouped-in-error for 'error',
grouped-in-warning for 'warning', and the grouped-validation leading seam
when validation is non-null and the control is leading (unchanged isLeading
rule). GroupedValidationProbe reads validation.
3. Error-over-warning precedence preserved: producer sets
validation = isInvalid ? 'error' : isWarning ? 'warning' : null, and
isWarning stays warning && !isInvalid.
4. No SCSS/behavioral change: no .scss edited; emitted class names identical.
The action-button variant classes and controls-invalid/-warning still wire
off the local isInvalid/isWarning (not the context) and are unchanged.
Children stay masked (FormFieldContext invalid:false/warning:false).
5. Former grouped-context invalid/warning consumers, each converted:
- src/internal/context/control-group-context.tsx (type, default, reset) ->
validation.
- src/internal/components/control-group/index.tsx (provider value) ->
validation.
- src/internal/components/control-group/grouped-control-styles.ts
(destructure + branch) -> validation.
- src/internal/components/control-group/__tests__/common.tsx
(GroupedValidationProbe) -> validation.
Opaque forwarders (input, select/multiselect/autosuggest, segmented-control,
button-trigger, select trigger) never name the fields; unchanged. The one
context-shape test assertion now expects 'validation:error'; all class-name
assertions unchanged. form-field-context invalid/warning left intact.
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #5143 +/- ##
==========================================
- Coverage 97.71% 97.71% -0.01%
==========================================
Files 991 995 +4
Lines 31858 32149 +291
Branches 11770 11865 +95
==========================================
+ Hits 31130 31414 +284
- Misses 721 728 +7
Partials 7 7 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
Use a plain SpaceBetween around the controls instead of the local Section helper with captions, on both the select and multiselect standalone validation-focus dev pages.
…ixin The $neutral-border-color parameter was dead flexibility: segmented-control was the only call site passing a non-default value (awsui.$color-border-segment-wrapper), a pure alias of awsui.$color-border-input-default that resolves pixel-identically across all shipped themes/modes. ControlGroup owns the border color and it does not need to be themeable per-control for now. Removed the parameter from grouped-control, grouped-control-outer-border-color, and grouped-control-validation-focus, hard-coding awsui.$color-border-input-default at the leaf neutral-edge usage. Updated segmented-control to call @include styles.grouped-control; with no argument. Verification (by reasoning, no build run — user has a watch job): 1. grouped-control, grouped-control-outer-border-color, and grouped-control-validation-focus no longer take a neutral-color param. 2. The neutral outward-edge color now references awsui.$color-border-input-default directly at the single leaf usage. 3. Call sites: segmented-control updated to no-arg; input and button-trigger already called no-arg and compile under the new signature; button uses grouped-control-base + grouped-control-horizontal-last and never called the arg-taking mixin. No call site passes an argument. 4. segmented-control now resolves the default token, pixel-identical to the prior segment-wrapper alias per the investigation. 5. Validation (error/warning status tokens), thick leading border, outward-edge logic, inner-seam neutrality, hover/focus/keyboard-focus, and the hoisted invalid ring are unchanged. 6. Only SCSS files changed; no TSX/test change (emitted class names unchanged). Non-blocking: pixel confirmation needs the control-group + segmented-control visual-regression run.
Restore test/definitions/visual/{control-group,multiselect,select}.ts to
their origin/main state; drop the validation visual-test entries added on
this branch.
…avoid border miter
…ix miter and input
Move the thick leading validation bar off the control and onto the group's
per-control leading `.control` slot <div>, drawn as a positive-z-index ::before.
Reading the resulting code:
1. The bar is now a `z-index: 1` `::before` on the leading `.control` slot
(control-group/styles.scss `.control-validation`); the control-side `::before`
and its `position: relative` are removed from grouped-control.scss (only the
`padding-inline-start` content inset remains there).
2. It is a flat rectangle (no border, no miter), offset
`inset-inline-start: -1 * $border-width-field` and widened by the same, so its
outer edge sits flush with the control's outer edge and overpaints the 1px gray
leading border; `inset-block: 0` avoids bleeding past rounded first/last corners
(the overlap seam already collapses the block seams).
3. It renders for ALL control types (input/autosuggest/select/multiselect/
segmented) because the host is a real <div> slot, not a replaced <input> that
generates no pseudo-elements.
4. Per-position leading corner radii are set explicitly on the `::before` keyed on
the existing `control-${position}-${direction}` slot classes (border-radius is
not inherited and the slot carries none): horizontal first/only and vertical
only round both leading corners; vertical first rounds only top-leading;
vertical last rounds only bottom-leading; horizontal/vertical middle stay
square — mirroring the grouped-control mixin's outer corners.
5. Status color comes from new leading-slot markers `control-in-error` /
`control-in-warning`, gated in index.tsx by `isLeading && isInvalid/isWarning`;
error precedence holds because `isWarning` is already `!!warning && !isInvalid`.
6. Content inset is unchanged — `padding-inline-start` stays on the control.
7. Inner seams, outward-edge coloring, invalid focus ring, `$margin-overlap`,
`isolation: isolate`, and `:focus-within { z-index: 1 }` are untouched.
8. No control-package, test, or dev-page changes.
Make the last control's inline-end edge the single controls-to-action-button
seam, removing the button's redundant inline-start border and the button-side
validation special-case. The control already draws and validation-colors its
outward inline-end edge, so the button border was duplicate paint on top of it.
Changes:
- button/styles.scss: variant-control-group-action gets border-inline-start-width:
0 (base) so no neutral or colored inline-start border is painted; re-asserted
inside the when-visible block (form-focus-element sets inline style+color but not
width, so width 0 survives focus). Deleted the &-error/&-warning sub-blocks.
- control-group/styles.scss: .action-slot margin-inline-start -1px -> 0 (butt-join).
- control-group/index.tsx: removed the two buttonStyles variant-control-group-
action-error/-warning class pushes and the now-unused buttonStyles import. Kept
isInvalid/isWarning (still feed GroupedControlContext.validation and the
leading-bar classes).
Verified by reading the resulting code (no build/sass run; user watch job owns pixels):
(1) button variant paints no inline-start border in any state: base width 0, and
width 0 re-asserted under when-visible so it survives form-focus-element.
(2) .action-slot margin is 0; total row width = C + B - 1 matches today's
C + (-1) + B, so no 1px shift and no gap; single inline-start margin covers both
horizontal and vertical (vertical only adds align-items: stretch, no own margin).
(3) seam validation color now sourced from the control's existing outward inline-end
coloring (horizontal-last and all vertical positions, error precedence, neutral
otherwise) via grouped-control.scss; no button-side color needed.
(4) button-side variant-control-group-action-error/-warning SCSS rules and the two
index.tsx class pushes deleted; no remaining references in src/.
(5) corner radii square at the seam (unchanged), focus halo on other edges and
.action-slot:focus-within z-index unchanged, leading .control-validation::before
thick bar untouched, button block-start/block-end/inline-end edges unchanged.
(6) only 3 files changed plus the unused buttonStyles import cleanup; no change to
constants.scss, tests, or dev pages.



Description
Related links, issue #, if available: n/a
How has this been tested?
Review checklist
The following items are to be evaluated by the author(s) and the reviewer(s).
Correctness
CONTRIBUTING.md.CONTRIBUTING.md.Security
checkSafeUrlfunction.Testing
By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.