From 22b1ea8a4cff781e6a13cbedcbbe1c82f50bcb20 Mon Sep 17 00:00:00 2001 From: Ernst Kaese Date: Wed, 6 May 2026 17:58:15 +0200 Subject: [PATCH 1/3] chore: Convert file token to internal token --- .../__tests__/file-token-group.test.tsx | 6 +- src/file-token-group/file-token.tsx | 160 +++++++++--------- src/token/internal.tsx | 11 +- 3 files changed, 95 insertions(+), 82 deletions(-) diff --git a/src/file-token-group/__tests__/file-token-group.test.tsx b/src/file-token-group/__tests__/file-token-group.test.tsx index 22284f31ae..96253c1a3e 100644 --- a/src/file-token-group/__tests__/file-token-group.test.tsx +++ b/src/file-token-group/__tests__/file-token-group.test.tsx @@ -212,8 +212,10 @@ describe('File loading', () => { test('Spinner added when loading', () => { const wrapper = render({ items: [{ file: file1, loading: true }, { file: file2 }] }); - expect(wrapper.findFileToken(1)?.getElement().firstChild).toHaveClass(styles.loading); - expect(wrapper.findFileToken(2)?.getElement().firstChild).not.toHaveClass(styles.loading); + expect(wrapper.findFileToken(1)?.getElement().querySelector(`.${styles['token-box']}`)).toHaveClass(styles.loading); + expect(wrapper.findFileToken(2)?.getElement().querySelector(`.${styles['token-box']}`)).not.toHaveClass( + styles.loading + ); }); }); diff --git a/src/file-token-group/file-token.tsx b/src/file-token-group/file-token.tsx index 3d66ba004f..43d8ae5160 100644 --- a/src/file-token-group/file-token.tsx +++ b/src/file-token-group/file-token.tsx @@ -11,7 +11,7 @@ import { FormFieldError, FormFieldWarning } from '../form-field/internal'; import { BaseComponentProps } from '../internal/base-component/index.js'; import InternalSpaceBetween from '../space-between/internal.js'; import InternalSpinner from '../spinner/internal.js'; -import DismissButton from '../token/dismiss-button.js'; +import InternalToken from '../token/internal.js'; import { TokenGroupProps } from '../token-group/interfaces.js'; import Tooltip from '../tooltip/internal.js'; import * as defaultFormatters from './default-formatters.js'; @@ -99,6 +99,80 @@ function InternalFileToken({ const fileIsSingleRow = !showFileLastModified && !showFileSize && (!groupContainsImage || (groupContainsImage && !showFileThumbnail)); + const fileLabel = ( + <> + {loading && ( +
+ +
+ )} + + {showFileThumbnail && isImage && } + +
+ +
setShowTooltip(true)} + onMouseOut={() => setShowTooltip(false)} + onFocus={() => setShowTooltip(true)} + onBlur={() => setShowTooltip(false)} + role={isTruncated ? 'button' : undefined} + aria-expanded={isTruncated ? showTooltip : undefined} + tabIndex={isTruncated ? 0 : -1} + ref={fileNameContainerRef} + > + + {file.name} + +
+ + {showFileSize && file.size ? ( + + {formatFileSize(file.size)} + + ) : null} + + {showFileLastModified && file.lastModified ? ( + + {formatFileLastModified(new Date(file.lastModified))} + + ) : null} +
+
+
+ {showTooltip && isTruncated && ( + containerRef.current} + content={{file.name}} + onEscape={() => setShowTooltip(false)} + /> + )} + + ); + return (
-
- {loading && ( -
- -
- )} - - {showFileThumbnail && isImage && } - -
- -
setShowTooltip(true)} - onMouseOut={() => setShowTooltip(false)} - onFocus={() => setShowTooltip(true)} - onBlur={() => setShowTooltip(false)} - role={isTruncated ? 'button' : undefined} - aria-expanded={isTruncated ? showTooltip : undefined} - tabIndex={isTruncated ? 0 : -1} - ref={fileNameContainerRef} - > - - {file.name} - -
- - {showFileSize && file.size ? ( - - {formatFileSize(file.size)} - - ) : null} - - {showFileLastModified && file.lastModified ? ( - - {formatFileLastModified(new Date(file.lastModified))} - - ) : null} -
-
-
- {onDismiss && !readOnly && } -
+ /> {errorText && ( {errorText} @@ -195,13 +210,6 @@ function InternalFileToken({ {warningText} )} - {showTooltip && isTruncated && ( - containerRef.current} - content={{file.name}} - onEscape={() => setShowTooltip(false)} - /> - )}
); } diff --git a/src/token/internal.tsx b/src/token/internal.tsx index 62aca483af..25886d8863 100644 --- a/src/token/internal.tsx +++ b/src/token/internal.tsx @@ -25,6 +25,7 @@ type InternalTokenProps = TokenProps & InternalBaseComponentProps & { role?: string; disableInnerPadding?: boolean; + tokenBoxClassName?: string; }; function InternalToken({ @@ -45,6 +46,7 @@ function InternalToken({ // Internal role, disableInnerPadding, + tokenBoxClassName, // Base __internalRootRef, @@ -116,9 +118,9 @@ function InternalToken({ analyticsSelectors.token, baseProps.className )} - aria-label={ariaLabel} - aria-labelledby={!ariaLabel ? ariaLabelledbyId : undefined} - aria-disabled={!!disabled} + aria-label={role === 'presentation' ? undefined : ariaLabel} + aria-labelledby={role === 'presentation' || ariaLabel ? undefined : ariaLabelledbyId} + aria-disabled={role === 'presentation' ? undefined : !!disabled} role={role ?? 'group'} onFocus={() => { setShowTooltip(true); @@ -140,7 +142,8 @@ function InternalToken({ disabled && styles['token-box-disabled'], readOnly && styles['token-box-readonly'], !isInline && !onDismiss && styles['token-box-without-dismiss'], - disableInnerPadding && styles['disable-padding'] + disableInnerPadding && styles['disable-padding'], + tokenBoxClassName )} style={tokenRootStyleProps} > From e757d37958d6c9784b2d146190c4c505e5a11769 Mon Sep 17 00:00:00 2001 From: Ernst Kaese Date: Fri, 8 May 2026 10:14:50 +0200 Subject: [PATCH 2/3] Fix visual inconsistencies --- src/file-token-group/file-token.tsx | 108 ++++++++++++++-------------- 1 file changed, 54 insertions(+), 54 deletions(-) diff --git a/src/file-token-group/file-token.tsx b/src/file-token-group/file-token.tsx index 43d8ae5160..30402ec550 100644 --- a/src/file-token-group/file-token.tsx +++ b/src/file-token-group/file-token.tsx @@ -99,6 +99,58 @@ function InternalFileToken({ const fileIsSingleRow = !showFileLastModified && !showFileSize && (!groupContainsImage || (groupContainsImage && !showFileThumbnail)); + const fileMetadata = ( +
+ +
setShowTooltip(true)} + onMouseOut={() => setShowTooltip(false)} + onFocus={() => setShowTooltip(true)} + onBlur={() => setShowTooltip(false)} + role={isTruncated ? 'button' : undefined} + aria-expanded={isTruncated ? showTooltip : undefined} + tabIndex={isTruncated ? 0 : -1} + ref={fileNameContainerRef} + > + + {file.name} + +
+ + {showFileSize && file.size ? ( + + {formatFileSize(file.size)} + + ) : null} + + {showFileLastModified && file.lastModified ? ( + + {formatFileLastModified(new Date(file.lastModified))} + + ) : null} +
+
+ ); + const fileLabel = ( <> {loading && ( @@ -110,59 +162,7 @@ function InternalFileToken({ )} - - {showFileThumbnail && isImage && } - -
- -
setShowTooltip(true)} - onMouseOut={() => setShowTooltip(false)} - onFocus={() => setShowTooltip(true)} - onBlur={() => setShowTooltip(false)} - role={isTruncated ? 'button' : undefined} - aria-expanded={isTruncated ? showTooltip : undefined} - tabIndex={isTruncated ? 0 : -1} - ref={fileNameContainerRef} - > - - {file.name} - -
- - {showFileSize && file.size ? ( - - {formatFileSize(file.size)} - - ) : null} - - {showFileLastModified && file.lastModified ? ( - - {formatFileLastModified(new Date(file.lastModified))} - - ) : null} -
-
-
+ {fileMetadata} {showTooltip && isTruncated && ( containerRef.current} @@ -188,8 +188,8 @@ function InternalFileToken({ > : undefined} onDismiss={readOnly ? undefined : onDismiss} dismissLabel={getDismissLabel(index)} tokenBoxClassName={clsx(styles['token-box'], { From a872647d3643faf913323f24a67e29b522e0f3ed Mon Sep 17 00:00:00 2001 From: Ernst Kaese Date: Tue, 12 May 2026 19:03:06 +0200 Subject: [PATCH 3/3] Bring Token usage further away from a custom implementation --- .../__tests__/file-token-group.test.tsx | 1 + src/file-token-group/file-token.tsx | 99 +++++++++---------- src/internal/components/option/index.tsx | 6 +- src/internal/components/option/interfaces.ts | 2 + src/internal/components/option/styles.scss | 11 +++ src/token/internal.tsx | 36 ++++--- 6 files changed, 87 insertions(+), 68 deletions(-) diff --git a/src/file-token-group/__tests__/file-token-group.test.tsx b/src/file-token-group/__tests__/file-token-group.test.tsx index 96253c1a3e..44349df3a0 100644 --- a/src/file-token-group/__tests__/file-token-group.test.tsx +++ b/src/file-token-group/__tests__/file-token-group.test.tsx @@ -212,6 +212,7 @@ describe('File loading', () => { test('Spinner added when loading', () => { const wrapper = render({ items: [{ file: file1, loading: true }, { file: file2 }] }); + // Reach into the token-box since the file-token now composes InternalToken. expect(wrapper.findFileToken(1)?.getElement().querySelector(`.${styles['token-box']}`)).toHaveClass(styles.loading); expect(wrapper.findFileToken(2)?.getElement().querySelector(`.${styles['token-box']}`)).not.toHaveClass( styles.loading diff --git a/src/file-token-group/file-token.tsx b/src/file-token-group/file-token.tsx index 30402ec550..565e8cd56a 100644 --- a/src/file-token-group/file-token.tsx +++ b/src/file-token-group/file-token.tsx @@ -99,35 +99,33 @@ function InternalFileToken({ const fileIsSingleRow = !showFileLastModified && !showFileSize && (!groupContainsImage || (groupContainsImage && !showFileThumbnail)); - const fileMetadata = ( + // File name wrapped in a keyboard-focusable container that drives the custom Tooltip below. + const fileNameLabel = (
setShowTooltip(true)} + onMouseOut={() => setShowTooltip(false)} + onFocus={() => setShowTooltip(true)} + onBlur={() => setShowTooltip(false)} + role={isTruncated ? 'button' : undefined} + aria-expanded={isTruncated ? showTooltip : undefined} + tabIndex={isTruncated ? 0 : -1} + ref={fileNameContainerRef} > - -
setShowTooltip(true)} - onMouseOut={() => setShowTooltip(false)} - onFocus={() => setShowTooltip(true)} - onBlur={() => setShowTooltip(false)} - role={isTruncated ? 'button' : undefined} - aria-expanded={isTruncated ? showTooltip : undefined} - tabIndex={isTruncated ? 0 : -1} - ref={fileNameContainerRef} - > - - {file.name} - -
+ + {file.name} + +
+ ); + const fileMetadataRows = + (showFileSize && file.size) || (showFileLastModified && file.lastModified) ? ( + {showFileSize && file.size ? ( ) : null} - - ); + ) : null; - const fileLabel = ( - <> - {loading && ( -
- -
- )} - {fileMetadata} - {showTooltip && isTruncated && ( - containerRef.current} - content={{file.name}} - onEscape={() => setShowTooltip(false)} - /> - )} - - ); + const loadingOverlay = loading ? ( +
+ +
+ ) : null; return (
: undefined} - onDismiss={readOnly ? undefined : onDismiss} + additionalContent={fileMetadataRows} + tokenBoxContent={loadingOverlay} + // Dismiss button always rendered (matches TokenGroup); disabled while loading. + onDismiss={onDismiss} dismissLabel={getDismissLabel(index)} + readOnly={readOnly || loading} tokenBoxClassName={clsx(styles['token-box'], { [styles.loading]: loading, [styles.error]: errorText, @@ -210,6 +200,13 @@ function InternalFileToken({ {warningText} )} + {showTooltip && isTruncated && ( + containerRef.current} + content={{file.name}} + onEscape={() => setShowTooltip(false)} + /> + )}
); } diff --git a/src/internal/components/option/index.tsx b/src/internal/components/option/index.tsx index ca67f6040f..5b5b8b3cb6 100644 --- a/src/internal/components/option/index.tsx +++ b/src/internal/components/option/index.tsx @@ -36,6 +36,7 @@ const Option = ({ labelRef, labelId, customContent, + additionalContent, ...restProps }: OptionProps) => { if (!option) { @@ -95,7 +96,9 @@ const Option = ({ {option.labelContent ? ( - {option.labelContent} + + {option.labelContent} + ) : ( ); diff --git a/src/internal/components/option/interfaces.ts b/src/internal/components/option/interfaces.ts index 3050f4983d..1cd477579b 100644 --- a/src/internal/components/option/interfaces.ts +++ b/src/internal/components/option/interfaces.ts @@ -55,4 +55,6 @@ export interface OptionProps extends BaseComponentProps { labelRef?: React.RefObject; labelId?: string; customContent?: ReactNode; + /** Extra content rendered inside the option's content column after description, tags, and filtering tags. */ + additionalContent?: ReactNode; } diff --git a/src/internal/components/option/styles.scss b/src/internal/components/option/styles.scss index b1f49b14c3..df379eb86e 100644 --- a/src/internal/components/option/styles.scss +++ b/src/internal/components/option/styles.scss @@ -49,6 +49,17 @@ @include styles.text-wrapping; } +.label-block { + // Let ReactNode labelContent fill the available column width instead of shrink-wrapping. + flex: 1; + min-inline-size: 0; +} + +.additional-content { + // Matches the vertical rhythm of SpaceBetween size="xxxs" between the label and extra rows. + margin-block-start: awsui.$space-scaled-xxxs; +} + .label, .tag { flex-wrap: wrap; diff --git a/src/token/internal.tsx b/src/token/internal.tsx index 25886d8863..d639c7d7d4 100644 --- a/src/token/internal.tsx +++ b/src/token/internal.tsx @@ -25,7 +25,12 @@ type InternalTokenProps = TokenProps & InternalBaseComponentProps & { role?: string; disableInnerPadding?: boolean; + /** Additional class applied to the token-box element, for consumer-specific state styling. */ tokenBoxClassName?: string; + /** Extra content rendered inside the option's content column, after description/tags. */ + additionalContent?: React.ReactNode; + /** Extra content rendered inside the token-box but outside the option (e.g. absolute overlays). */ + tokenBoxContent?: React.ReactNode; }; function InternalToken({ @@ -47,6 +52,8 @@ function InternalToken({ role, disableInnerPadding, tokenBoxClassName, + additionalContent, + tokenBoxContent, // Base __internalRootRef, @@ -59,6 +66,9 @@ function InternalToken({ const [showTooltip, setShowTooltip] = useState(false); const [isEllipsisActive, setIsEllipsisActive] = useState(false); const isInline = variant === 'inline'; + // Consumers with their own grouping semantics can pass role="presentation" to treat the root + // as a pure styling wrapper (strips ARIA and focus/mouse handlers). + const isPresentation = role === 'presentation'; const ariaLabelledbyId = useUniqueId(); const isLabelOverflowing = () => { @@ -118,23 +128,15 @@ function InternalToken({ analyticsSelectors.token, baseProps.className )} - aria-label={role === 'presentation' ? undefined : ariaLabel} - aria-labelledby={role === 'presentation' || ariaLabel ? undefined : ariaLabelledbyId} - aria-disabled={role === 'presentation' ? undefined : !!disabled} + aria-label={isPresentation ? undefined : ariaLabel} + aria-labelledby={isPresentation || ariaLabel ? undefined : ariaLabelledbyId} + aria-disabled={isPresentation ? undefined : !!disabled} role={role ?? 'group'} - onFocus={() => { - setShowTooltip(true); - }} - onBlur={() => { - setShowTooltip(false); - }} - onMouseEnter={() => { - setShowTooltip(true); - }} - onMouseLeave={() => { - setShowTooltip(false); - }} - tabIndex={!!tooltipContent && isInline && isEllipsisActive ? 0 : undefined} + onFocus={isPresentation ? undefined : () => setShowTooltip(true)} + onBlur={isPresentation ? undefined : () => setShowTooltip(false)} + onMouseEnter={isPresentation ? undefined : () => setShowTooltip(true)} + onMouseLeave={isPresentation ? undefined : () => setShowTooltip(false)} + tabIndex={!isPresentation && !!tooltipContent && isInline && isEllipsisActive ? 0 : undefined} > + {tokenBoxContent}