Repository navigation
chore: Add validation states to control group - #5143
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.
…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✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #5143 +/- ##
==========================================
- Coverage 97.74% 97.73% -0.01%
==========================================
Files 996 996
Lines 32182 32194 +12
Branches 11876 11885 +9
==========================================
+ Hits 31455 31465 +10
- Misses 721 723 +2
Partials 6 6 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
The grouped-control focus-visible duplication fix dropped widget-exports to 1,449.55 kB. Lower the limit from 1470 kB back toward the measured size, leaving ~10 kB headroom, so the limit no longer masks future growth.
Render the grouped-control thick validation bar via a real border-inline-start on the .control-validation::before pseudo-element instead of a background-color fill, in all modes. In forced-colors mode the browser flattens background-color to Canvas, so the filled bar blended into the grouped Input's background and the error/warning cue vanished. A real border is recolored to the system palette (CanvasText) instead, so the thick bar stays visible against a neutral group. No forced-colors media query or system-color keyword is introduced. Normal-mode rendering is unchanged: box-sizing:border-box with inset-inline-start:0 and no inline-size shrink-wraps the box to the same 5px (calc(border-invalid-width + border-width-field)) bar width and full block height; corner-radius rules are untouched. A Chrome pixel diff of the dev-page geometry shows the normal-mode bar is byte-for-byte identical old vs new (red/orange, rounded leading corners), while in forced-colors the old fill samples Canvas (invisible) and the new border samples CanvasText (visible). Verified: npm run build (exit 0); compiled styles.scoped.css shows the ::before using border-inline-start with status colors and no background-color; jest src/internal/components/control-group src/input = 163 tests passed.
RadioGroup onChange yields a plain string; route it through parseValidationState and a new parseDirection helper instead of casting to the union types. The hook now reuses parseDirection too.
Wrap each rendered control in a keyed React.Fragment to resolve the missing-key warning from PermutationsView's child list.
|
/override-visual-regression See previous comment |
…ol-group-validation # Conflicts: # src/button/styles.scss



Description
Relevant docs:
kMUBQtg6r1WV,KaCZyfbvq46DHow has this been tested?
Added new dev page.
Tested manually, test definitions for visual regression will be provided separately.
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.