From 55697a0c6a0218b146efdc906afe9d360af7dea7 Mon Sep 17 00:00:00 2001 From: Nathnael Dereje Date: Tue, 21 Jul 2026 20:44:27 +0200 Subject: [PATCH 1/3] feat: Input prefix suffix --- pages/input/adornments.page.tsx | 336 ++++++++++++++++++ .../__snapshots__/documenter.test.ts.snap | 198 +++++++++++ .../test-utils-selectors.test.tsx.snap | 2 + src/input/index.tsx | 4 + src/input/interfaces.ts | 38 ++ src/input/internal.tsx | 66 +++- src/input/styles.scss | 121 +++++++ src/test-utils/dom/input/index.ts | 20 ++ 8 files changed, 771 insertions(+), 14 deletions(-) create mode 100644 pages/input/adornments.page.tsx diff --git a/pages/input/adornments.page.tsx b/pages/input/adornments.page.tsx new file mode 100644 index 0000000000..c458e83d15 --- /dev/null +++ b/pages/input/adornments.page.tsx @@ -0,0 +1,336 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +import React, { useState } from 'react'; + +import Badge from '~components/badge'; +import Box from '~components/box'; +import ColumnLayout from '~components/column-layout'; +import Container from '~components/container'; +import ContentLayout from '~components/content-layout'; +import FormField from '~components/form-field'; +import Header from '~components/header'; +import Input from '~components/input'; +import SpaceBetween from '~components/space-between'; +import Toggle from '~components/toggle'; + +// ─── State controls panel ───────────────────────────────────────────────────── + +interface StateControlsProps { + disabled: boolean; + setDisabled: (v: boolean) => void; + readOnly: boolean; + setReadOnly: (v: boolean) => void; + invalid: boolean; + setInvalid: (v: boolean) => void; + warning: boolean; + setWarning: (v: boolean) => void; + compact: boolean; + setCompact: (v: boolean) => void; + rtl: boolean; + setRtl: (v: boolean) => void; +} + +function StateControls({ + disabled, + setDisabled, + readOnly, + setReadOnly, + invalid, + setInvalid, + warning, + setWarning, + compact, + setCompact, + rtl, + setRtl, +}: StateControlsProps) { + return ( + State controls}> + + + setDisabled(detail.checked)}> + Disabled + + setReadOnly(detail.checked)}> + Read-only + + + + setInvalid(detail.checked)}> + Invalid + + setWarning(detail.checked)}> + Warning + + + + setCompact(detail.checked)}> + Compact density + + setRtl(detail.checked)}> + RTL layout + + + + + ); +} + +// ─── Format hint examples ───────────────────────────────────────────────────── + +interface FormatHintProps { + disabled: boolean; + readOnly: boolean; + invalid: boolean; + warning: boolean; +} + +function FormatHintExamples({ disabled, readOnly, invalid, warning }: FormatHintProps) { + const [currency, setCurrency] = useState(''); + const [percent, setPercent] = useState(''); + const [compound, setCompound] = useState(''); + const [url, setUrl] = useState(''); + const [both, setBoth] = useState(''); + const [ms, setMs] = useState(''); + + return ( + + Format hint adornments + + Decorative + + + } + > + + + + setCurrency(e.detail.value)} + prefix="$" + inputMode="decimal" + placeholder="0.00" + disabled={disabled} + readOnly={readOnly} + invalid={invalid} + warning={warning} + /> + + + + setPercent(e.detail.value)} + suffix="%" + inputMode="numeric" + placeholder="0" + disabled={disabled} + readOnly={readOnly} + invalid={invalid} + warning={warning} + /> + + + + setCompound(e.detail.value)} + suffix="TB/mo" + inputMode="numeric" + placeholder="0" + disabled={disabled} + readOnly={readOnly} + invalid={invalid} + warning={warning} + /> + + + + setMs(e.detail.value)} + suffix="ms" + inputMode="numeric" + placeholder="0" + disabled={disabled} + readOnly={readOnly} + invalid={invalid} + warning={warning} + /> + + + + setUrl(e.detail.value)} + prefix="https://" + type="url" + placeholder="example.com/webhook" + disabled={disabled} + readOnly={readOnly} + invalid={invalid} + warning={warning} + /> + + + + setBoth(e.detail.value)} + prefix="<" + suffix="%" + inputMode="decimal" + placeholder="0.5" + disabled={disabled} + readOnly={readOnly} + invalid={invalid} + warning={warning} + /> + + + + + ); +} + +// ─── Baseline comparison ────────────────────────────────────────────────────── + +function BaselineComparison({ disabled, readOnly, invalid, warning }: FormatHintProps) { + const [value, setValue] = useState(''); + return ( + + Baseline: plain Input + + } + > + + setValue(e.detail.value)} + placeholder="Enter a value" + disabled={disabled} + readOnly={readOnly} + invalid={invalid} + warning={warning} + /> + + + ); +} + +// ─── RTL section ────────────────────────────────────────────────────────────── + +function RtlSection() { + const [val, setVal] = useState(''); + const [val2, setVal2] = useState(''); + return ( + RTL layout}> + + In RTL locales,{' '} + + prefix + {' '} + appears on the trailing edge and{' '} + + suffix + {' '} + appears on the leading edge. The prop names are semantic (before/after the value), not directional. + + +
+ + + setVal(e.detail.value)} + prefix="$" + inputMode="decimal" + placeholder="0.00" + /> + + + setVal2(e.detail.value)} + suffix="%" + inputMode="numeric" + placeholder="0" + /> + + +
+
+
+ ); +} + +// ─── Main page ──────────────────────────────────────────────────────────────── + +export default function AdornmentsPage() { + const [disabled, setDisabled] = useState(false); + const [readOnly, setReadOnly] = useState(false); + const [invalid, setInvalid] = useState(false); + const [warning, setWarning] = useState(false); + const [compact, setCompact] = useState(false); + const [rtl, setRtl] = useState(false); + + const stateProps = { disabled, readOnly, invalid, warning }; + + return ( +
+ + Input — prefix/suffix adornments + + } + > + + + + + + + +
+ ); +} diff --git a/src/__tests__/snapshot-tests/__snapshots__/documenter.test.ts.snap b/src/__tests__/snapshot-tests/__snapshots__/documenter.test.ts.snap index d835dc74ff..25122ba54e 100644 --- a/src/__tests__/snapshot-tests/__snapshots__/documenter.test.ts.snap +++ b/src/__tests__/snapshot-tests/__snapshots__/documenter.test.ts.snap @@ -16986,6 +16986,26 @@ We do not support using this attribute to apply custom styling.", "optional": true, "type": "string", }, + { + "description": "Short text or symbol rendered in a shaded cell on the leading edge of the +input field, before the editable value. + +Use for contextual decorators such as currency symbols (\`$\`, \`€\`), +URL schemes (\`https://\`), or path prefixes (\`/api/v2/\`). + +The adornment is purely decorative — it does not mutate the \`value\` prop +or the \`onChange\` detail. Ensure the unit is also reflected in the visible +\`\` label so screen reader users receive the same context +(for example, label = "Amount in US dollars", not just "Amount"). + +In RTL locales the \`prefix\` cell appears on the trailing edge. +"prefix" means "before the value" semantically, not "on the left". + +Don't use \`prefix\` as a substitute for a visible label.", + "name": "prefix", + "optional": true, + "type": "string", + }, { "description": "Specifies if the control is read-only, which prevents the user from modifying the value but includes it in a form @@ -17257,6 +17277,22 @@ Refer to the [style](/components/input/?tabId=style) tab for more details.", ], "type": "InputProps.Style", }, + { + "description": "Short text or symbol rendered in a shaded cell on the trailing edge of +the input field, after the editable value. + +Use for units of measure (\`%\`, \`ms\`, \`TB/mo\`, \`GB\`, \`req/s\`, \`days\`), +currency codes (\`USD\`, \`EUR\`), or degree notation (\`°C\`, \`°E\`). + +The adornment is purely decorative — it does not mutate the \`value\` prop +or the \`onChange\` detail. Ensure the unit is also reflected in the visible +\`\` label (for example, label = "CPU utilization (percent)"). + +In RTL locales the \`suffix\` cell appears on the leading edge.", + "name": "suffix", + "optional": true, + "type": "string", + }, { "defaultValue": "'text'", "description": "Specifies the type of control to render. @@ -35226,6 +35262,34 @@ Supported options: ], }, }, + { + "description": "Returns the prefix adornment element, or null if no prefix is set.", + "name": "findPrefix", + "parameters": [], + "returnType": { + "isNullable": true, + "name": "ElementWrapper", + "typeArguments": [ + { + "name": "HTMLElement", + }, + ], + }, + }, + { + "description": "Returns the suffix adornment element, or null if no suffix is set.", + "name": "findSuffix", + "parameters": [], + "returnType": { + "isNullable": true, + "name": "ElementWrapper", + "typeArguments": [ + { + "name": "HTMLElement", + }, + ], + }, + }, { "inheritedFrom": { "name": "BaseInputWrapper.focus", @@ -36880,6 +36944,23 @@ To find a specific row use the \`findRow(n)\` function as chaining \`findRows(). ], }, }, + { + "description": "Returns the prefix adornment element, or null if no prefix is set.", + "inheritedFrom": { + "name": "InputWrapper.findPrefix", + }, + "name": "findPrefix", + "parameters": [], + "returnType": { + "isNullable": true, + "name": "ElementWrapper", + "typeArguments": [ + { + "name": "HTMLElement", + }, + ], + }, + }, { "name": "findStatusIndicator", "parameters": [ @@ -36903,6 +36984,23 @@ To find a specific row use the \`findRow(n)\` function as chaining \`findRows(). ], }, }, + { + "description": "Returns the suffix adornment element, or null if no suffix is set.", + "inheritedFrom": { + "name": "InputWrapper.findSuffix", + }, + "name": "findSuffix", + "parameters": [], + "returnType": { + "isNullable": true, + "name": "ElementWrapper", + "typeArguments": [ + { + "name": "HTMLElement", + }, + ], + }, + }, { "inheritedFrom": { "name": "BaseInputWrapper.focus", @@ -43165,6 +43263,23 @@ and this method will have no effect.", ], }, }, + { + "description": "Returns the prefix adornment element, or null if no prefix is set.", + "inheritedFrom": { + "name": "InputWrapper.findPrefix", + }, + "name": "findPrefix", + "parameters": [], + "returnType": { + "isNullable": true, + "name": "ElementWrapper", + "typeArguments": [ + { + "name": "HTMLElement", + }, + ], + }, + }, { "description": "Returns custom property form cancel button.", "name": "findPropertyCancelButton", @@ -43256,6 +43371,23 @@ and this method will have no effect.", ], }, }, + { + "description": "Returns the suffix adornment element, or null if no suffix is set.", + "inheritedFrom": { + "name": "InputWrapper.findSuffix", + }, + "name": "findSuffix", + "parameters": [], + "returnType": { + "isNullable": true, + "name": "ElementWrapper", + "typeArguments": [ + { + "name": "HTMLElement", + }, + ], + }, + }, { "name": "findTokens", "parameters": [], @@ -47245,6 +47377,24 @@ Supported options: "name": "ElementWrapper", }, }, + { + "description": "Returns the prefix adornment element, or null if no prefix is set.", + "name": "findPrefix", + "parameters": [], + "returnType": { + "isNullable": false, + "name": "ElementWrapper", + }, + }, + { + "description": "Returns the suffix adornment element, or null if no suffix is set.", + "name": "findSuffix", + "parameters": [], + "returnType": { + "isNullable": false, + "name": "ElementWrapper", + }, + }, ], "name": "InputWrapper", }, @@ -48381,6 +48531,18 @@ To find a specific row use the \`findRow(n)\` function as chaining \`findRows(). "name": "ElementWrapper", }, }, + { + "description": "Returns the prefix adornment element, or null if no prefix is set.", + "inheritedFrom": { + "name": "InputWrapper.findPrefix", + }, + "name": "findPrefix", + "parameters": [], + "returnType": { + "isNullable": false, + "name": "ElementWrapper", + }, + }, { "name": "findStatusIndicator", "parameters": [ @@ -48401,6 +48563,18 @@ To find a specific row use the \`findRow(n)\` function as chaining \`findRows(). "name": "ElementWrapper", }, }, + { + "description": "Returns the suffix adornment element, or null if no suffix is set.", + "inheritedFrom": { + "name": "InputWrapper.findSuffix", + }, + "name": "findSuffix", + "parameters": [], + "returnType": { + "isNullable": false, + "name": "ElementWrapper", + }, + }, ], "name": "AutosuggestWrapper", }, @@ -52863,6 +53037,18 @@ In this case, use findContentEditableElement() instead.", "name": "ElementWrapper", }, }, + { + "description": "Returns the prefix adornment element, or null if no prefix is set.", + "inheritedFrom": { + "name": "InputWrapper.findPrefix", + }, + "name": "findPrefix", + "parameters": [], + "returnType": { + "isNullable": false, + "name": "ElementWrapper", + }, + }, { "description": "Returns custom property form cancel button.", "name": "findPropertyCancelButton", @@ -52945,6 +53131,18 @@ In this case, use findContentEditableElement() instead.", "name": "ElementWrapper", }, }, + { + "description": "Returns the suffix adornment element, or null if no suffix is set.", + "inheritedFrom": { + "name": "InputWrapper.findSuffix", + }, + "name": "findSuffix", + "parameters": [], + "returnType": { + "isNullable": false, + "name": "ElementWrapper", + }, + }, { "name": "findTokens", "parameters": [], diff --git a/src/__tests__/snapshot-tests/__snapshots__/test-utils-selectors.test.tsx.snap b/src/__tests__/snapshot-tests/__snapshots__/test-utils-selectors.test.tsx.snap index 4b1af42bc5..ed248e6f53 100644 --- a/src/__tests__/snapshot-tests/__snapshots__/test-utils-selectors.test.tsx.snap +++ b/src/__tests__/snapshot-tests/__snapshots__/test-utils-selectors.test.tsx.snap @@ -378,6 +378,8 @@ exports[`test-utils selectors 1`] = ` "input": [ "awsui_input-button-right_2rhyz", "awsui_input-container_2rhyz", + "awsui_input-prefix_2rhyz", + "awsui_input-suffix_2rhyz", "awsui_input_2rhyz", "awsui_root_2rhyz", ], diff --git a/src/input/index.tsx b/src/input/index.tsx index 351bdf14af..f194cff2a6 100644 --- a/src/input/index.tsx +++ b/src/input/index.tsx @@ -44,6 +44,8 @@ const Input = React.forwardRef( clearAriaLabel, nativeInputAttributes, style, + prefix, + suffix, ...rest }: InputProps, ref: Ref @@ -101,6 +103,8 @@ const Input = React.forwardRef( clearAriaLabel, nativeInputAttributes, style, + prefix, + suffix, }} className={clsx(styles.root, baseProps.className)} __inheritFormFieldProps={true} diff --git a/src/input/interfaces.ts b/src/input/interfaces.ts index 86a9cbeffd..81fa01436c 100644 --- a/src/input/interfaces.ts +++ b/src/input/interfaces.ts @@ -188,6 +188,44 @@ export interface InputProps * @awsuiSystem core */ style?: InputProps.Style; + + /** + * Short text or symbol rendered in a shaded cell on the leading edge of the + * input field, before the editable value. + * + * Use for contextual decorators such as currency symbols (`$`, `€`), + * URL schemes (`https://`), or path prefixes (`/api/v2/`). + * + * The adornment is purely decorative — it does not mutate the `value` prop + * or the `onChange` detail. Ensure the unit is also reflected in the visible + * `` label so screen reader users receive the same context + * (for example, label = "Amount in US dollars", not just "Amount"). + * + * In RTL locales the `prefix` cell appears on the trailing edge. + * "prefix" means "before the value" semantically, not "on the left". + * + * Don't use `prefix` as a substitute for a visible label. + * + * @see suffix + */ + prefix?: string; + + /** + * Short text or symbol rendered in a shaded cell on the trailing edge of + * the input field, after the editable value. + * + * Use for units of measure (`%`, `ms`, `TB/mo`, `GB`, `req/s`, `days`), + * currency codes (`USD`, `EUR`), or degree notation (`°C`, `°E`). + * + * The adornment is purely decorative — it does not mutate the `value` prop + * or the `onChange` detail. Ensure the unit is also reflected in the visible + * `` label (for example, label = "CPU utilization (percent)"). + * + * In RTL locales the `suffix` cell appears on the leading edge. + * + * @see prefix + */ + suffix?: string; } export namespace InputProps { diff --git a/src/input/internal.tsx b/src/input/internal.tsx index 7dad564dbe..d01716d329 100644 --- a/src/input/internal.tsx +++ b/src/input/internal.tsx @@ -57,6 +57,9 @@ export interface InternalInputProps __skipNativeAttributesWarnings?: SkipWarnings; __inlineLabelText?: string; __fullWidth?: boolean; + + prefix?: string; + suffix?: string; } function InternalInput( @@ -101,6 +104,8 @@ function InternalInput( __inlineLabelText, __fullWidth, style, + prefix, + suffix, ...rest }: InternalInputProps, ref: Ref @@ -142,6 +147,7 @@ function InternalInput( __rightIcon && styles['input-has-icon-right'], __leftIcon && styles['input-has-icon-left'], __noBorderRadius && styles['input-has-no-border-radius'], + (prefix !== undefined || suffix !== undefined) && styles['input-adorned'], { [styles['input-readonly']]: readOnly, [styles['input-invalid']]: invalid, @@ -215,6 +221,25 @@ function InternalInput( /> ); + // The input element, optionally wrapped in an inline-label structure. + const inputWithLabel = __inlineLabelText ? ( +
+ +
+ {mainInput} +
+
+ ) : ( + mainInput + ); + return (
)} - {__inlineLabelText ? ( -
- -
- {mainInput} -
+ {prefix !== undefined || suffix !== undefined ? ( + // One flex bar owns the border, radius, background, and focus ring. + // Layout: [prefix][divider][input][divider][suffix] + // Dividers only render on sides that have an adornment. +
+ {prefix !== undefined && ( + + )} + {prefix !== undefined && } + {inputWithLabel} + {suffix !== undefined && } + {suffix !== undefined && ( + + )}
) : ( - mainInput + inputWithLabel )} {__rightIcon && ( Date: Wed, 22 Jul 2026 10:14:25 +0200 Subject: [PATCH 2/3] chore: Split required prefix/suffix into its own dev-page section Move the ambiguous https:// example out of the decorative section and add a clearly-labeled required prefix/suffix section demonstrating the userland concatenation pattern (webhook, S3 bucket, private DNS suffix). Clarifies that prefix/suffix are decorative only and the builder concatenates the value. --- pages/input/adornments.page.tsx | 136 +++++++++++++++++++++++++++----- 1 file changed, 118 insertions(+), 18 deletions(-) diff --git a/pages/input/adornments.page.tsx b/pages/input/adornments.page.tsx index c458e83d15..13b81e0dcc 100644 --- a/pages/input/adornments.page.tsx +++ b/pages/input/adornments.page.tsx @@ -89,7 +89,6 @@ function FormatHintExamples({ disabled, readOnly, invalid, warning }: FormatHint const [currency, setCurrency] = useState(''); const [percent, setPercent] = useState(''); const [compound, setCompound] = useState(''); - const [url, setUrl] = useState(''); const [both, setBoth] = useState(''); const [ms, setMs] = useState(''); @@ -98,7 +97,7 @@ function FormatHintExamples({ disabled, readOnly, invalid, warning }: FormatHint header={
Format hint adornments @@ -173,20 +172,6 @@ function FormatHintExamples({ disabled, readOnly, invalid, warning }: FormatHint /> - - setUrl(e.detail.value)} - prefix="https://" - type="url" - placeholder="example.com/webhook" - disabled={disabled} - readOnly={readOnly} - invalid={invalid} - warning={warning} - /> - - + Required prefix/suffix + + Builder concatenates + +
+ } + > + + + + + setUrl(e.detail.value)} + prefix="https://" + type="url" + placeholder="example.com/webhook" + disabled={disabled} + readOnly={readOnly} + invalid={invalid} + warning={warning} + /> + {url && ( + + Submitted value:{' '} + + https://{url} + + + )} + + + + + + setBucket(e.detail.value)} + prefix="amazon-braket-" + placeholder="my-bucket" + disabled={disabled} + readOnly={readOnly} + invalid={invalid} + warning={warning} + /> + {bucket && ( + + Submitted value:{' '} + + amazon-braket-{bucket} + + + )} + + + + + + setHost(e.detail.value)} + suffix=".ec2.internal" + placeholder="my-host" + disabled={disabled} + readOnly={readOnly} + invalid={invalid} + warning={warning} + /> + {host && ( + + Submitted value:{' '} + + {host}.ec2.internal + + + )} + + + + + + ); +} + // ─── Baseline comparison ────────────────────────────────────────────────────── function BaselineComparison({ disabled, readOnly, invalid, warning }: FormatHintProps) { @@ -305,9 +404,9 @@ export default function AdornmentsPage() { header={
- Input — prefix/suffix adornments + Input: prefix/suffix adornments
} > @@ -327,6 +426,7 @@ export default function AdornmentsPage() { setRtl={setRtl} /> + From 3bf9ca315537ddb6f31411ffabcc843310da2d21 Mon Sep 17 00:00:00 2001 From: Nathnael Dereje Date: Thu, 23 Jul 2026 14:36:43 +0200 Subject: [PATCH 3/3] feat: widen prefix/suffix API to ReactNode - Change prefix and suffix props from string to React.ReactNode in InputProps and InternalInputProps - Update JSDoc to document ReactNode usage (icons, badges, status indicators, composite elements) - Expand adornments dev page with icon, badge, StatusIndicator, and composite ReactNode examples - Update documenter snapshot --- pages/input/adornments.page.tsx | 362 +++++++++++++++++- .../__snapshots__/documenter.test.ts.snap | 79 ++-- src/input/interfaces.ts | 30 +- src/input/internal.tsx | 4 +- 4 files changed, 410 insertions(+), 65 deletions(-) diff --git a/pages/input/adornments.page.tsx b/pages/input/adornments.page.tsx index 13b81e0dcc..ed558b75ca 100644 --- a/pages/input/adornments.page.tsx +++ b/pages/input/adornments.page.tsx @@ -9,8 +9,10 @@ import Container from '~components/container'; import ContentLayout from '~components/content-layout'; import FormField from '~components/form-field'; import Header from '~components/header'; +import Icon from '~components/icon'; import Input from '~components/input'; import SpaceBetween from '~components/space-between'; +import StatusIndicator from '~components/status-indicator'; import Toggle from '~components/toggle'; // ─── State controls panel ───────────────────────────────────────────────────── @@ -76,16 +78,16 @@ function StateControls({ ); } -// ─── Format hint examples ───────────────────────────────────────────────────── +// ─── Format hint examples — string adornments ───────────────────────────────── -interface FormatHintProps { +interface StateProps { disabled: boolean; readOnly: boolean; invalid: boolean; warning: boolean; } -function FormatHintExamples({ disabled, readOnly, invalid, warning }: FormatHintProps) { +function FormatHintExamples({ disabled, readOnly, invalid, warning }: StateProps) { const [currency, setCurrency] = useState(''); const [percent, setPercent] = useState(''); const [compound, setCompound] = useState(''); @@ -97,9 +99,9 @@ function FormatHintExamples({ disabled, readOnly, invalid, warning }: FormatHint header={
- Format hint adornments + Format hint adornments — string Decorative @@ -196,13 +198,324 @@ function FormatHintExamples({ disabled, readOnly, invalid, warning }: FormatHint ); } +// ─── ReactNode adornments — icons ───────────────────────────────────────────── + +function ReactNodeIconExamples({ disabled, readOnly, invalid, warning }: StateProps) { + const [search, setSearch] = useState(''); + const [email, setEmail] = useState(''); + const [url, setUrl] = useState(''); + const [amount, setAmount] = useState(''); + const [locked, setLocked] = useState(''); + + return ( + + ReactNode adornments — Icons + + ReactNode + +
+ } + > + + + + setSearch(e.detail.value)} + prefix={ + + + setEmail(e.detail.value)} + prefix={ + + + setUrl(e.detail.value)} + prefix={ + + + setAmount(e.detail.value)} + prefix={ + + + setLocked(e.detail.value)} + prefix={ + + + + ); +} + +// ─── ReactNode adornments — Badges ──────────────────────────────────────────── + +function ReactNodeBadgeExamples({ disabled, readOnly, invalid, warning }: StateProps) { + const [beta, setBeta] = useState(''); + const [required, setRequired] = useState(''); + const [new_, setNew_] = useState(''); + + return ( + + ReactNode adornments — Badges + + ReactNode + + + } + > + + + + setBeta(e.detail.value)} + suffix={Beta} + placeholder="my-feature-flag" + disabled={disabled} + readOnly={readOnly} + invalid={invalid} + warning={warning} + /> + + + + setRequired(e.detail.value)} + suffix={Required} + inputMode="numeric" + placeholder="123456789012" + disabled={disabled} + readOnly={readOnly} + invalid={invalid} + warning={warning} + /> + + + + setNew_(e.detail.value)} + suffix={New} + placeholder="arn:aws:bedrock:..." + disabled={disabled} + readOnly={readOnly} + invalid={invalid} + warning={warning} + /> + + + + + ); +} + +// ─── ReactNode adornments — StatusIndicator ─────────────────────────────────── + +function ReactNodeStatusExamples({ disabled, readOnly, invalid, warning }: StateProps) { + const [endpoint, setEndpoint] = useState(''); + const [pending, setPending] = useState(''); + + return ( + + ReactNode adornments — StatusIndicator + + ReactNode + + + } + > + + + + setEndpoint(e.detail.value)} + suffix={Connected} + placeholder="db.example.com:5432" + disabled={disabled} + readOnly={readOnly} + invalid={invalid} + warning={warning} + /> + + + + setPending(e.detail.value)} + suffix={Queued} + placeholder="job-abc123" + disabled={disabled} + readOnly={readOnly} + invalid={invalid} + warning={warning} + /> + + + + + ); +} + +// ─── ReactNode adornments — icon + text combination ─────────────────────────── + +function ReactNodeCompositeExamples({ disabled, readOnly, invalid, warning }: StateProps) { + const [rate, setRate] = useState(''); + const [storage, setStorage] = useState(''); + + return ( + + ReactNode adornments — Icon + text composite + + ReactNode + + + } + > + + + + setRate(e.detail.value)} + suffix={ + + + } + inputMode="numeric" + placeholder="1000" + disabled={disabled} + readOnly={readOnly} + invalid={invalid} + warning={warning} + /> + + + + setStorage(e.detail.value)} + prefix={ + + + + ); +} + // ─── Required prefix/suffix (userland concatenation) ────────────────────────── -// These examples answer the "mandatory prefix/suffix" use-case. The component -// does NOT do this. The `prefix`/`suffix` props are always decorative and never -// mutate `onChange`. The fixed segment is shown visually with the decorative prop, -// and the builder concatenates it into the value in application code. -function RequiredAffixExamples({ disabled, readOnly, invalid, warning }: FormatHintProps) { +function RequiredAffixExamples({ disabled, readOnly, invalid, warning }: StateProps) { const [url, setUrl] = useState(''); const [bucket, setBucket] = useState(''); const [host, setHost] = useState(''); @@ -312,14 +625,14 @@ function RequiredAffixExamples({ disabled, readOnly, invalid, warning }: FormatH // ─── Baseline comparison ────────────────────────────────────────────────────── -function BaselineComparison({ disabled, readOnly, invalid, warning }: FormatHintProps) { +function BaselineComparison({ disabled, readOnly, invalid, warning }: StateProps) { const [value, setValue] = useState(''); return ( Baseline: plain Input @@ -345,6 +658,7 @@ function BaselineComparison({ disabled, readOnly, invalid, warning }: FormatHint function RtlSection() { const [val, setVal] = useState(''); const [val2, setVal2] = useState(''); + const [val3, setVal3] = useState(''); return ( RTL layout}> @@ -356,7 +670,12 @@ function RtlSection() { suffix {' '} - appears on the leading edge. The prop names are semantic (before/after the value), not directional. + appears on the leading edge. The prop names are semantic (before/after the value), not directional. Icons flip + correctly because the flex container inherits{' '} + + dir + + .
@@ -379,6 +698,15 @@ function RtlSection() { placeholder="0" /> + + setVal3(e.detail.value)} + prefix={
@@ -404,9 +732,9 @@ export default function AdornmentsPage() { header={
- Input: prefix/suffix adornments + Input: prefix/suffix adornments (ReactNode)
} > @@ -426,6 +754,10 @@ export default function AdornmentsPage() { setRtl={setRtl} /> + + + + diff --git a/src/__tests__/snapshot-tests/__snapshots__/documenter.test.ts.snap b/src/__tests__/snapshot-tests/__snapshots__/documenter.test.ts.snap index 25122ba54e..7a7c25be6f 100644 --- a/src/__tests__/snapshot-tests/__snapshots__/documenter.test.ts.snap +++ b/src/__tests__/snapshot-tests/__snapshots__/documenter.test.ts.snap @@ -16986,26 +16986,6 @@ We do not support using this attribute to apply custom styling.", "optional": true, "type": "string", }, - { - "description": "Short text or symbol rendered in a shaded cell on the leading edge of the -input field, before the editable value. - -Use for contextual decorators such as currency symbols (\`$\`, \`€\`), -URL schemes (\`https://\`), or path prefixes (\`/api/v2/\`). - -The adornment is purely decorative — it does not mutate the \`value\` prop -or the \`onChange\` detail. Ensure the unit is also reflected in the visible -\`\` label so screen reader users receive the same context -(for example, label = "Amount in US dollars", not just "Amount"). - -In RTL locales the \`prefix\` cell appears on the trailing edge. -"prefix" means "before the value" semantically, not "on the left". - -Don't use \`prefix\` as a substitute for a visible label.", - "name": "prefix", - "optional": true, - "type": "string", - }, { "description": "Specifies if the control is read-only, which prevents the user from modifying the value but includes it in a form @@ -17277,22 +17257,6 @@ Refer to the [style](/components/input/?tabId=style) tab for more details.", ], "type": "InputProps.Style", }, - { - "description": "Short text or symbol rendered in a shaded cell on the trailing edge of -the input field, after the editable value. - -Use for units of measure (\`%\`, \`ms\`, \`TB/mo\`, \`GB\`, \`req/s\`, \`days\`), -currency codes (\`USD\`, \`EUR\`), or degree notation (\`°C\`, \`°E\`). - -The adornment is purely decorative — it does not mutate the \`value\` prop -or the \`onChange\` detail. Ensure the unit is also reflected in the visible -\`\` label (for example, label = "CPU utilization (percent)"). - -In RTL locales the \`suffix\` cell appears on the leading edge.", - "name": "suffix", - "optional": true, - "type": "string", - }, { "defaultValue": "'text'", "description": "Specifies the type of control to render. @@ -17334,7 +17298,48 @@ with the input using \`ariaDescribedby\`.", "type": "boolean", }, ], - "regions": [], + "regions": [ + { + "description": "Content rendered in a shaded cell on the leading edge of the input field, +before the editable value. + +Accepts any React node — plain text, a short symbol, an icon, or a badge. +Use for contextual decorators such as currency symbols (\`$\`, \`€\`), +URL schemes (\`https://\`), path prefixes (\`/api/v2/\`), or an icon that +clarifies the expected data type. + +The adornment is purely decorative — it does not mutate the \`value\` prop +or the \`onChange\` detail. Wrap any icon or custom element in +\`aria-hidden="true"\` if it is already conveyed by the visible +\`\` label, so screen reader users are not double-announced. +Ensure the unit or context is also reflected in the visible label +(for example, label = "Amount in US dollars", not just "Amount"). + +In RTL locales the \`prefix\` cell appears on the trailing edge. +"prefix" means "before the value" semantically, not "on the left". + +Don't use \`prefix\` as a substitute for a visible label.", + "isDefault": false, + "name": "prefix", + }, + { + "description": "Content rendered in a shaded cell on the trailing edge of the input field, +after the editable value. + +Accepts any React node — plain text, a short unit abbreviation, an icon, +or a badge. Use for units of measure (\`%\`, \`ms\`, \`TB/mo\`, \`GB\`, \`req/s\`, +\`days\`), currency codes (\`USD\`, \`EUR\`), degree notation (\`°C\`, \`°E\`), or +an icon indicating a secondary action. + +The adornment is purely decorative — it does not mutate the \`value\` prop +or the \`onChange\` detail. Ensure the unit is also reflected in the visible +\`\` label (for example, label = "CPU utilization (percent)"). + +In RTL locales the \`suffix\` cell appears on the leading edge.", + "isDefault": false, + "name": "suffix", + }, + ], "releaseStatus": "stable", } `; diff --git a/src/input/interfaces.ts b/src/input/interfaces.ts index 81fa01436c..1f75e611f5 100644 --- a/src/input/interfaces.ts +++ b/src/input/interfaces.ts @@ -1,5 +1,7 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 +import React from 'react'; + import { BaseComponentProps } from '../types/base-component'; import { BaseKeyDetail, CancelableEventHandler, NonCancelableEventHandler } from '../types/events'; import { FormFieldValidationControlProps } from '../types/form-field'; @@ -190,15 +192,19 @@ export interface InputProps style?: InputProps.Style; /** - * Short text or symbol rendered in a shaded cell on the leading edge of the - * input field, before the editable value. + * Content rendered in a shaded cell on the leading edge of the input field, + * before the editable value. * + * Accepts any React node — plain text, a short symbol, an icon, or a badge. * Use for contextual decorators such as currency symbols (`$`, `€`), - * URL schemes (`https://`), or path prefixes (`/api/v2/`). + * URL schemes (`https://`), path prefixes (`/api/v2/`), or an icon that + * clarifies the expected data type. * * The adornment is purely decorative — it does not mutate the `value` prop - * or the `onChange` detail. Ensure the unit is also reflected in the visible - * `` label so screen reader users receive the same context + * or the `onChange` detail. Wrap any icon or custom element in + * `aria-hidden="true"` if it is already conveyed by the visible + * `` label, so screen reader users are not double-announced. + * Ensure the unit or context is also reflected in the visible label * (for example, label = "Amount in US dollars", not just "Amount"). * * In RTL locales the `prefix` cell appears on the trailing edge. @@ -208,14 +214,16 @@ export interface InputProps * * @see suffix */ - prefix?: string; + prefix?: React.ReactNode; /** - * Short text or symbol rendered in a shaded cell on the trailing edge of - * the input field, after the editable value. + * Content rendered in a shaded cell on the trailing edge of the input field, + * after the editable value. * - * Use for units of measure (`%`, `ms`, `TB/mo`, `GB`, `req/s`, `days`), - * currency codes (`USD`, `EUR`), or degree notation (`°C`, `°E`). + * Accepts any React node — plain text, a short unit abbreviation, an icon, + * or a badge. Use for units of measure (`%`, `ms`, `TB/mo`, `GB`, `req/s`, + * `days`), currency codes (`USD`, `EUR`), degree notation (`°C`, `°E`), or + * an icon indicating a secondary action. * * The adornment is purely decorative — it does not mutate the `value` prop * or the `onChange` detail. Ensure the unit is also reflected in the visible @@ -225,7 +233,7 @@ export interface InputProps * * @see prefix */ - suffix?: string; + suffix?: React.ReactNode; } export namespace InputProps { diff --git a/src/input/internal.tsx b/src/input/internal.tsx index d01716d329..516da8f39d 100644 --- a/src/input/internal.tsx +++ b/src/input/internal.tsx @@ -58,8 +58,8 @@ export interface InternalInputProps __inlineLabelText?: string; __fullWidth?: boolean; - prefix?: string; - suffix?: string; + prefix?: React.ReactNode; + suffix?: React.ReactNode; } function InternalInput(