diff --git a/package.json b/package.json index b81ca3cde11..48dece8de52 100644 --- a/package.json +++ b/package.json @@ -183,7 +183,7 @@ { "path": "lib/components/internal/widget-exports.js", "brotli": false, - "limit": "1450 kB", + "limit": "1470 kB", "ignore": "react-dom" } ], diff --git a/pages/control-group/validation.page.tsx b/pages/control-group/validation.page.tsx new file mode 100644 index 00000000000..8f679e789ed --- /dev/null +++ b/pages/control-group/validation.page.tsx @@ -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 = ['horizontal', 'vertical']; + +const actionButtons: ReadonlyArray = [ + undefined, + { iconName: 'remove', ariaLabel: 'Remove' }, +]; + +const labels = [undefined, 'Label']; + +const permutations = createPermutations([ + { 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 ( + + ( + + + + )} + /> + + ); +} diff --git a/pages/multiselect/validation-focus.page.tsx b/pages/multiselect/validation-focus.page.tsx new file mode 100644 index 00000000000..15cad0242b3 --- /dev/null +++ b/pages/multiselect/validation-focus.page.tsx @@ -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 ( + } + > + + {}} + /> + {}} + /> + + + ); +} diff --git a/pages/select/validation-focus.page.tsx b/pages/select/validation-focus.page.tsx new file mode 100644 index 00000000000..304e14d3293 --- /dev/null +++ b/pages/select/validation-focus.page.tsx @@ -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 ( + }> + + {}} + /> + + + ); +} diff --git a/src/button/styles.scss b/src/button/styles.scss index e77ee13e364..3b701539502 100644 --- a/src/button/styles.scss +++ b/src/button/styles.scss @@ -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); @@ -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); @@ -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, @@ -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 ); } @@ -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 ); } } @@ -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 { diff --git a/src/input/styles.scss b/src/input/styles.scss index 9236921d3aa..c5814aad830 100644 --- a/src/input/styles.scss +++ b/src/input/styles.scss @@ -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. @@ -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); } } @@ -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); } } @@ -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; + } } } @@ -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, diff --git a/src/internal/components/button-trigger/styles.scss b/src/internal/components/button-trigger/styles.scss index d550bb574ee..87bc1a862fe 100644 --- a/src/internal/components/button-trigger/styles.scss +++ b/src/internal/components/button-trigger/styles.scss @@ -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 { @@ -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 { diff --git a/src/internal/components/control-group/grouped-control-styles.ts b/src/internal/components/control-group/grouped-control-styles.ts index 0f6f81f7e12..08a3a999d06 100644 --- a/src/internal/components/control-group/grouped-control-styles.ts +++ b/src/internal/components/control-group/grouped-control-styles.ts @@ -11,11 +11,25 @@ import { GroupedControlContextProps } from '../../context/control-group-context' */ export function getGroupedControlClassNames( styles: Record, - { 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; } diff --git a/src/internal/components/control-group/index.tsx b/src/internal/components/control-group/index.tsx index ca3da291077..bc3089d494f 100644 --- a/src/internal/components/control-group/index.tsx +++ b/src/internal/components/control-group/index.tsx @@ -8,6 +8,7 @@ import { useUniqueId } from '@cloudscape-design/component-toolkit/internal'; import { InternalButton } from '../../../button/internal'; import { getBaseProps } from '../../base-component'; import { GroupedControlContext, GroupedControlPosition } from '../../context/control-group-context'; +import { FormFieldContext, useFormFieldContext } from '../../context/form-field-context'; import { flattenChildren } from '../../utils/flatten-children'; import { ControlGroupProps } from './interfaces'; @@ -26,6 +27,10 @@ export default function ControlGroup({ const baseProps = getBaseProps(props); const labelId = useUniqueId('control-group-label'); + const formFieldContext = useFormFieldContext({}); + const isInvalid = !!formFieldContext.invalid; + const isWarning = !!formFieldContext.warning && !isInvalid; + const flattenedChildren = flattenChildren(children, 'ControlGroup'); const controlCount = flattenedChildren.length; @@ -38,6 +43,7 @@ export default function ControlGroup({ className={clsx( styles.controls, styles[`controls-${direction}`], + actionButton && styles['controls-with-action'], !actionButton && !inlineLabelText && rootClassnames )} > @@ -45,13 +51,33 @@ export default function ControlGroup({ const key = child && typeof child === 'object' ? (child as Record<'key', unknown>).key : undefined; const position: GroupedControlPosition = controlCount === 1 ? 'only' : index === 0 ? 'first' : index === controlCount - 1 ? 'last' : 'middle'; + // Only the leading (inline-start) slots host the thick validation bar: + // the first/only control horizontally, every control vertically. Mirrors + // the isLeading rule in grouped-control-styles.ts. + const isLeading = direction === 'vertical' || position === 'first' || position === 'only'; return (
- - {child} + + {/* Mask the form-field flags so inner controls do not color their own seams. */} + + {child} +
); @@ -67,7 +93,7 @@ export default function ControlGroup({ {controls}
(not the control) so it + // is a flat rectangle that cannot miter against the control's thin block seams + // and renders for replaced-element controls (Input/Autosuggest). Offset one + // border width outboard and widened to match so it sits flush with the control's + // outer edge and overpaints the control's 1px gray leading border. z-index: 1 + // keeps it above the control's border/box-shadow phase; the slot's own + // isolation keeps it from reaching an adjacent slot. + &-validation { + position: relative; + + &::before { + content: ''; + position: absolute; + inset-block: 0; + inline-size: calc(#{constants.$invalid-control-left-border} + #{awsui.$border-width-field}); + z-index: 1; + } + + // Per-position leading corner radii. border-radius is not inherited and the + // slot carries none, so match the leading corners each position rounds in + // grouped-control.scss so the bar's rounded corners coincide with the group's + // outer corners (and stay square where the group is square). + // Leading horizontal positions (first/only) round both leading corners. + &.control-first-horizontal::before, + &.control-only-horizontal::before, + &.control-only-vertical::before { + border-start-start-radius: constants.$control-border-radius; + border-end-start-radius: constants.$control-border-radius; + } + // Vertical first rounds only the top-leading corner. + &.control-first-vertical::before { + border-start-start-radius: constants.$control-border-radius; + } + // Vertical last rounds only the bottom-leading corner. + &.control-last-vertical::before { + border-end-start-radius: constants.$control-border-radius; + } + // Vertical middle rounds neither (no radius rule needed). + + &.control-in-error::before { + background-color: awsui.$color-text-status-error; + } + &.control-in-warning::before { + background-color: awsui.$color-text-status-warning; + } + } } .action-slot, diff --git a/src/internal/context/control-group-context.tsx b/src/internal/context/control-group-context.tsx index 7acd0a97818..089ee2eb095 100644 --- a/src/internal/context/control-group-context.tsx +++ b/src/internal/context/control-group-context.tsx @@ -6,6 +6,8 @@ export type GroupedControlPosition = 'first' | 'middle' | 'last' | 'only'; export type GroupedControlDirection = 'horizontal' | 'vertical'; +export type GroupedControlValidation = 'error' | 'warning'; + export interface GroupedControlContextProps { /** * The control's position within a control group, @@ -22,12 +24,19 @@ export interface GroupedControlContextProps { * inline-end corners facing the action so it owns the only rounded ones. */ hasAction?: boolean; + /** + * The surrounding control group's validation state, or `null` for none. The + * group owns the validation border; individual controls do not independently + * paint it. + */ + validation?: GroupedControlValidation | null; } export const GroupedControlContext = createContext({ position: null, direction: 'horizontal', hasAction: false, + validation: null, }); export function useGroupedControlContext() { @@ -41,11 +50,15 @@ export function useGroupedControlContext() { * content) should wrap that content with this provider. Otherwise a nested * control rendered inside such a slot would inherit the surrounding group's * `position` and incorrectly pick up the grouped-control styling - * where adjacent borders lose their border radii. + * where adjacent borders lose their border radii. The group's `validation` + * state is also cleared so nested custom-slot controls never inherit the + * group's validation styling. */ export function ResetGroupedControlContext({ children }: { children: React.ReactNode }) { const { direction } = useGroupedControlContext(); return ( - {children} + + {children} + ); } diff --git a/src/internal/generated/styles/tokens.d.ts b/src/internal/generated/styles/tokens.d.ts index b42ce787d0a..c29e9bbf787 100644 --- a/src/internal/generated/styles/tokens.d.ts +++ b/src/internal/generated/styles/tokens.d.ts @@ -62,6 +62,10 @@ export const colorChartsThresholdPositive: string; export const colorChartsThresholdInfo: string; export const colorChartsThresholdNeutral: string; +// Status text colors +export const colorTextStatusError: string; +export const colorTextStatusWarning: string; + // Spacing export const spaceXs: string; export const spaceXxs: string; diff --git a/src/internal/styles/forms/grouped-control.scss b/src/internal/styles/forms/grouped-control.scss index 2e29f0808ea..58a3dd2f92f 100644 --- a/src/internal/styles/forms/grouped-control.scss +++ b/src/internal/styles/forms/grouped-control.scss @@ -1,12 +1,61 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 +@use 'sass:list'; @use '../tokens' as awsui; @use '../utils' as styles; +@use '../foundation' as foundation; @use './constants' as constants; +@use '@cloudscape-design/component-toolkit/internal/focus-visible' as focus-visible; $grouped-control-border-radius: constants.$control-border-radius; +$grouped-control-error-marker: '.grouped-in-error'; +$grouped-control-warning-marker: '.grouped-in-warning'; + +// Sets the given color on the outward-facing border edges of `$edges`. +// Inner borders stay neutral. +@mixin grouped-control-edge-color($edges, $color) { + @if list.index($edges, inline-start) { + border-inline-start-color: $color; + } + @if list.index($edges, inline-end) { + border-inline-end-color: $color; + } + @if list.index($edges, block-start) { + border-block-start-color: $color; + } + @if list.index($edges, block-end) { + border-block-end-color: $color; + } +} + +@mixin grouped-control-status-edges($edges) { + @each $marker, + $color + in ( + $grouped-control-error-marker: awsui.$color-text-status-error, + $grouped-control-warning-marker: awsui.$color-text-status-warning + ) + { + &#{$marker}, + &#{$marker}:hover, + &#{$marker}:focus { + @include grouped-control-edge-color($edges, $color); + } + &#{$marker} { + @include focus-visible.when-visible { + @include grouped-control-edge-color($edges, $color); + } + } + } +} + +@mixin grouped-control-outer-border-color($edges) { + @include grouped-control-edge-color($edges, awsui.$color-border-input-default); + @include grouped-control-status-edges($edges); +} + @mixin grouped-control-base { @include styles.logical-radius($grouped-control-border-radius); } @@ -37,29 +86,58 @@ $grouped-control-border-radius: constants.$control-border-radius; &-horizontal { &-first { @include grouped-control-horizontal-first; + @include grouped-control-outer-border-color((inline-start, block-start, block-end)); } &-middle { @include grouped-control-middle; + @include grouped-control-outer-border-color((block-start, block-end)); } &-last { @include grouped-control-horizontal-last; + @include grouped-control-outer-border-color((inline-end, block-start, block-end)); } } &-vertical { &-first { @include grouped-control-vertical-first; + @include grouped-control-outer-border-color((block-start, inline-start, inline-end)); } &-middle { @include grouped-control-middle; + @include grouped-control-outer-border-color((inline-start, inline-end)); } &-last { @include grouped-control-vertical-last; + @include grouped-control-outer-border-color((block-end, inline-start, inline-end)); } } + &-horizontal-only, + &-vertical-only { + @include grouped-control-outer-border-color((inline-start, inline-end, block-start, block-end)); + } + &-with-action { border-start-end-radius: 0; border-end-end-radius: 0; } + + // Invalid focus ring, position-independent. Keyed on the state markers with + // :focus-within so it fires for the button-trigger (focus on self) and the + // Input .input-field wrapper (focus on the inner native input). + &#{$grouped-control-error-marker}:focus-within, + &#{$grouped-control-warning-marker}:focus-within { + box-shadow: foundation.$box-shadow-focused-light-invalid; + } + &#{$grouped-control-error-marker}, + &#{$grouped-control-warning-marker} { + @include focus-visible.when-visible { + box-shadow: foundation.$box-shadow-focused-light-invalid; + } + } + + &#{&}-validation { + padding-inline-start: calc(#{constants.$invalid-control-left-border} + #{constants.$invalid-control-left-padding}); + } } diff --git a/src/internal/styles/forms/mixins.scss b/src/internal/styles/forms/mixins.scss index 209f2090fcc..59a313cd265 100644 --- a/src/internal/styles/forms/mixins.scss +++ b/src/internal/styles/forms/mixins.scss @@ -124,8 +124,13 @@ // See focus-highlight above. outline: 2px dotted transparent; - border-block: awsui.$border-width-field solid $border-color; - border-inline: awsui.$border-width-field solid $border-color; + border-block-style: solid; + border-inline-style: solid; + + @if $border-color { + border-block-color: $border-color; + border-inline-color: $border-color; + } box-shadow: $box-shadow; } @@ -198,12 +203,20 @@ ) { color: $color; border-color: $border-color; - padding-inline-start: constants.$invalid-control-left-padding; - border-inline-start-width: constants.$invalid-control-left-border; + // Standalone only: thicken the real leading border and reduce the padding so + // text stays at the neutral position. Grouped controls get the overlay bar and + // a bar+cph content inset instead (grouped-control.scss), so they must not pull + // a real thick border here. @content is scoped to the standalone path so a + // caller (Input) can zero the wrapper padding when an inner cell owns the + // compensation, at the same specificity as the padding above. + &:not(.grouped) { + padding-inline-start: constants.$invalid-control-left-padding; + border-inline-start-width: constants.$invalid-control-left-border; + @content; + } &:focus { box-shadow: $focus-box-shadow; } - @content; } @mixin form-warning-control( @@ -213,12 +226,17 @@ ) { color: $color; border-color: $border-color; - padding-inline-start: constants.$invalid-control-left-padding; - border-inline-start-width: constants.$invalid-control-left-border; + // Symmetric with form-invalid-control: standalone only gets the thick real + // border + reduced padding; grouped uses the overlay bar + bar+cph inset. + // @content is scoped to the standalone path for the same reason. + &:not(.grouped) { + padding-inline-start: constants.$invalid-control-left-padding; + border-inline-start-width: constants.$invalid-control-left-border; + @content; + } &:focus { box-shadow: $focus-box-shadow; } - @content; } @mixin input-default-styles { diff --git a/src/prompt-input/styles.scss b/src/prompt-input/styles.scss index 3887fec3461..8d69e9144a6 100644 --- a/src/prompt-input/styles.scss +++ b/src/prompt-input/styles.scss @@ -71,11 +71,15 @@ $invalid-border-offset: constants.$invalid-control-left-padding; } &.textarea-invalid { + // The thick leading border lives on this root while the compensating reduced + // padding lives on inner content cells (.textarea etc.), so the root must own + // zero inline-start padding. Pass the zero as mixin @content so it lands inside + // the mixin's `:not(.grouped)` branch and wins the specificity contest against + // the mixin's own padding (matching the working Input pattern). @include styles.form-invalid-control( $color: var(#{custom-props.$promptInputStyleColorDefault}, awsui.$color-text-status-error), $border-color: var(#{custom-props.$promptInputStyleBorderColorDefault}, awsui.$color-text-status-error) - ); - & { + ) { padding-inline-start: 0; } @@ -84,8 +88,7 @@ $invalid-border-offset: constants.$invalid-control-left-padding; @include styles.form-invalid-control( $color: var(#{custom-props.$promptInputStyleColorDefault}, awsui.$color-text-status-error), $border-color: var(#{custom-props.$promptInputStyleBorderColorDefault}, awsui.$color-text-status-error) - ); - & { + ) { padding-inline-start: 0; box-shadow: foundation.$box-shadow-focused-light-invalid; } @@ -99,11 +102,13 @@ $invalid-border-offset: constants.$invalid-control-left-padding; } &.textarea-warning { + // Symmetric with the invalid state: pass the root's zero inline-start padding + // as mixin @content so it wins the specificity contest against the mixin's own + // padding, keeping text at the neutral x-position next to the thick border. @include styles.form-warning-control( $color: var(#{custom-props.$promptInputStyleColorDefault}, awsui.$color-text-status-warning), $border-color: var(#{custom-props.$promptInputStyleBorderColorDefault}, awsui.$color-text-status-warning) - ); - & { + ) { padding-inline-start: 0; } @@ -112,8 +117,7 @@ $invalid-border-offset: constants.$invalid-control-left-padding; @include styles.form-warning-control( $color: var(#{custom-props.$promptInputStyleColorDefault}, awsui.$color-text-status-warning), $border-color: var(#{custom-props.$promptInputStyleBorderColorDefault}, awsui.$color-text-status-warning) - ); - & { + ) { padding-inline-start: 0; box-shadow: foundation.$box-shadow-focused-light-invalid; } diff --git a/src/segmented-control/styles.scss b/src/segmented-control/styles.scss index aa5deb60e44..0f07763fa99 100644 --- a/src/segmented-control/styles.scss +++ b/src/segmented-control/styles.scss @@ -8,6 +8,7 @@ @use 'sass:list' as list; @use '../internal/styles/tokens' as awsui; @use '../internal/styles/utils/theming' as theming; +@use '@cloudscape-design/component-toolkit/internal/focus-visible' as focus-visible; @function repeatValue($value, $times) { $result: (); @@ -69,6 +70,17 @@ &.grouped-horizontal-last { border-inline-start: none; } + + // The focused inner segment option draws its own ring, so suppress the group's. + &.grouped-in-error, + &.grouped-in-warning { + &:focus-within { + box-shadow: none; + } + @include focus-visible.when-visible { + box-shadow: none; + } + } } .select {