From 80f5678745ca5a6cf45c2ee748379efbc7ea991e Mon Sep 17 00:00:00 2001 From: Philipp Schneider Date: Sun, 7 Dec 2025 18:05:43 +0100 Subject: [PATCH 01/52] new version of tooltip --- .../internal-tooltip-examples.page.tsx | 600 ++++++++++++++++++ .../toolbar/trigger-button/index.tsx | 8 +- .../__integ__/breadcrumb-group.test.ts | 2 +- .../__tests__/breadcrumb-item.test.tsx | 2 +- src/breadcrumb-group/item/item.tsx | 6 +- src/button-group/file-input-item.tsx | 10 +- src/button-group/icon-button-item.tsx | 10 +- src/button-group/icon-toggle-button-item.tsx | 10 +- src/button-group/menu-dropdown-item.tsx | 10 +- src/button/internal.tsx | 8 +- src/calendar/grid/index.tsx | 8 +- .../calendar/grids/grid-cell.tsx | 8 +- .../__tests__/file-token-group.test.tsx | 2 +- src/file-token-group/file-token.tsx | 12 +- .../__tests__/drag-handle-wrapper.test.tsx | 2 +- .../components/drag-handle-wrapper/index.tsx | 4 +- .../__integ__/use-position-observer.test.ts | 2 +- src/segmented-control/segment.tsx | 8 +- src/select/parts/item.tsx | 10 +- src/select/parts/multiselect-item.tsx | 10 +- src/slider/__integ__/slider.test.ts | 2 +- src/slider/internal.tsx | 8 +- .../__tests__/split-panel.test.tsx | 2 +- src/tabs/tab-header-bar.tsx | 8 +- src/test-utils/dom/internal/tooltip.ts | 2 +- src/token/internal.tsx | 11 +- src/tooltip/__integ__/tooltip.test.ts | 38 ++ src/tooltip/__tests__/tooltip.test.tsx | 95 +++ src/tooltip/index.tsx | 80 +++ src/tooltip/interfaces.ts | 42 ++ src/tooltip/internal.tsx | 3 + src/tooltip/styles.scss | 8 + 32 files changed, 947 insertions(+), 84 deletions(-) create mode 100644 pages/tooltip/internal-tooltip-examples.page.tsx create mode 100644 src/tooltip/__integ__/tooltip.test.ts create mode 100644 src/tooltip/__tests__/tooltip.test.tsx create mode 100644 src/tooltip/index.tsx create mode 100644 src/tooltip/interfaces.ts create mode 100644 src/tooltip/internal.tsx create mode 100644 src/tooltip/styles.scss diff --git a/pages/tooltip/internal-tooltip-examples.page.tsx b/pages/tooltip/internal-tooltip-examples.page.tsx new file mode 100644 index 0000000000..4552684b01 --- /dev/null +++ b/pages/tooltip/internal-tooltip-examples.page.tsx @@ -0,0 +1,600 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +import React, { useState } from 'react'; + +import AppLayout from '~components/app-layout'; +import Box from '~components/box'; +import BreadcrumbGroup from '~components/breadcrumb-group'; +import Button from '~components/button'; +import ButtonGroup from '~components/button-group'; +import Calendar from '~components/calendar'; +import Container from '~components/container'; +import DateRangePicker, { DateRangePickerProps } from '~components/date-range-picker'; +import FileTokenGroup, { FileTokenGroupProps } from '~components/file-token-group'; +import Header from '~components/header'; +import Multiselect, { MultiselectProps } from '~components/multiselect'; +import SegmentedControl from '~components/segmented-control'; +import Select, { SelectProps } from '~components/select'; +import Slider from '~components/slider'; +import SpaceBetween from '~components/space-between'; +import Tabs from '~components/tabs'; +import Token from '~components/token'; + +import ScreenshotArea from '../utils/screenshot-area'; + +export default function InternalTooltipExamples() { + return ( + <> + + +
Internal Tooltip - Current Implementations
+ + + + + + + + + + + + + + + + + +
+
+ + ); +} + +function FileInputItemExample() { + return ( + ButtonGroup - FileInputItem}> + + + + + ); +} + +function IconButtonItemExample() { + return ( + ButtonGroup - IconButtonItem}> + + + + + ); +} + +function IconToggleButtonItemExample() { + return ( + ButtonGroup - IconToggleButtonItem}> + + + + + ); +} + +function MenuDropdownItemExample() { + return ( + ButtonGroup - MenuDropdownItem}> + + + + + ); +} + +function ButtonExample() { + return ( + Button (Disabled Reason)}> + + + + + From e744747291d71e9f5e0c83d9e3dbc4b679683eda Mon Sep 17 00:00:00 2001 From: Philipp Schneider Date: Thu, 18 Dec 2025 15:12:07 +0100 Subject: [PATCH 33/52] fix: Update test files --- .../functional-tests/test-utils.test.tsx | 7 +- .../__snapshots__/documenter.test.ts.snap | 108 ++++++++++++++++++ .../test-utils-selectors.test.tsx.snap | 4 +- .../test-utils-wrappers.test.tsx.snap | 66 +++++++++++ src/button-group/icon-button-item.tsx | 8 +- 5 files changed, 184 insertions(+), 9 deletions(-) diff --git a/src/__tests__/functional-tests/test-utils.test.tsx b/src/__tests__/functional-tests/test-utils.test.tsx index 0f87ef1110..97b16472bd 100644 --- a/src/__tests__/functional-tests/test-utils.test.tsx +++ b/src/__tests__/functional-tests/test-utils.test.tsx @@ -30,7 +30,12 @@ afterEach(() => { }); const componentWithMultipleRootElements = ['top-navigation', 'app-layout', 'app-layout-toolbar']; -const componentsWithExceptions = ['annotation-context', 'icon-provider', ...componentWithMultipleRootElements]; +const componentsWithExceptions = [ + 'annotation-context', + 'icon-provider', + 'tooltip', + ...componentWithMultipleRootElements, +]; const components = getAllComponents().filter(component => !componentsWithExceptions.includes(component)); const RENDER_COMPONENTS_DEFAULT_PROPS: Record[] = [ diff --git a/src/__tests__/snapshot-tests/__snapshots__/documenter.test.ts.snap b/src/__tests__/snapshot-tests/__snapshots__/documenter.test.ts.snap index f51f9bdadd..b087d8a6d2 100644 --- a/src/__tests__/snapshot-tests/__snapshots__/documenter.test.ts.snap +++ b/src/__tests__/snapshot-tests/__snapshots__/documenter.test.ts.snap @@ -28569,6 +28569,79 @@ user from modifying the value. A read-only control is still focusable.", } `; +exports[`Components definition for tooltip matches the snapshot: tooltip 1`] = ` +{ + "dashCaseName": "tooltip", + "events": [ + { + "cancelable": false, + "description": "Callback fired when the user presses the Escape key while the tooltip is visible.", + "name": "onEscape", + }, + ], + "functions": [], + "name": "Tooltip", + "properties": [ + { + "description": "Function that returns the element to track for positioning the tooltip. +Can return null if the element is not yet mounted or available.", + "inlineType": { + "name": "() => HTMLElement | SVGElement | null", + "parameters": [], + "returnType": "HTMLElement | SVGElement | null", + "type": "function", + }, + "name": "getTrack", + "optional": false, + "type": "() => HTMLElement | SVGElement | null", + }, + { + "defaultValue": "'top'", + "description": "Position of the tooltip relative to the tracked element.", + "inlineType": { + "name": "PopoverProps.Position", + "type": "union", + "values": [ + "left", + "top", + "bottom", + "right", + ], + }, + "name": "position", + "optional": true, + "type": "string", + }, + { + "description": "Unique identifier for the tooltip instance. Changing this value will cause the tooltip +to recalculate its position, similar to how React's key prop works. +If not provided and content is a string or number, it will be used as key. +For complex content (elements, fragments), you should provide an explicit trackKey.", + "inlineType": { + "name": "string | number", + "type": "union", + "values": [ + "string", + "number", + ], + }, + "name": "trackKey", + "optional": true, + "type": "string | number", + }, + ], + "regions": [ + { + "description": "Content to display in the tooltip. +Accepts any valid React node including strings, numbers, elements, and fragments.", + "isDefault": false, + "name": "content", + }, + ], + "releaseStatus": "stable", +} +`; + exports[`Components definition for top-navigation matches the snapshot: top-navigation 1`] = ` { "dashCaseName": "top-navigation", @@ -40247,6 +40320,26 @@ Returns the current value of the input.", ], "name": "TokenGroupWrapper", }, + { + "methods": [ + { + "description": "Returns the tooltip content element. +Searches within this tooltip's scope to avoid conflicts with popovers.", + "name": "findContent", + "parameters": [], + "returnType": { + "isNullable": true, + "name": "ElementWrapper", + "typeArguments": [ + { + "name": "HTMLElement", + }, + ], + }, + }, + ], + "name": "TooltipWrapper", + }, { "methods": [ { @@ -48600,6 +48693,21 @@ To find a specific row use the \`findRow(n)\` function as chaining \`findRows(). ], "name": "TokenGroupWrapper", }, + { + "methods": [ + { + "description": "Returns the tooltip content element. +Searches within this tooltip's scope to avoid conflicts with popovers.", + "name": "findContent", + "parameters": [], + "returnType": { + "isNullable": false, + "name": "ElementWrapper", + }, + }, + ], + "name": "TooltipWrapper", + }, { "methods": [ { 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 5daaf28ad8..ff3d738ec3 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 @@ -393,7 +393,6 @@ exports[`test-utils selectors 1`] = ` "awsui_root_1fcus", "awsui_root_1kjc7", "awsui_root_1om0h", - "awsui_root_1qprf", "awsui_root_1t44z", "awsui_root_qwoo0", "awsui_root_vrgzu", @@ -664,6 +663,9 @@ exports[`test-utils selectors 1`] = ` "awsui_root_dm8gx", "awsui_token_dm8gx", ], + "tooltip": [ + "awsui_root_1r4p6", + ], "top-navigation": [ "awsui_hidden_k5dlb", "awsui_identity_k5dlb", diff --git a/src/__tests__/snapshot-tests/__snapshots__/test-utils-wrappers.test.tsx.snap b/src/__tests__/snapshot-tests/__snapshots__/test-utils-wrappers.test.tsx.snap index 9e433a47f5..67a2363f8f 100644 --- a/src/__tests__/snapshot-tests/__snapshots__/test-utils-wrappers.test.tsx.snap +++ b/src/__tests__/snapshot-tests/__snapshots__/test-utils-wrappers.test.tsx.snap @@ -90,6 +90,7 @@ import ToggleWrapper from './toggle'; import ToggleButtonWrapper from './toggle-button'; import TokenWrapper from './token'; import TokenGroupWrapper from './token-group'; +import TooltipWrapper from './tooltip'; import TopNavigationWrapper from './top-navigation'; import TreeViewWrapper from './tree-view'; import TutorialPanelWrapper from './tutorial-panel'; @@ -177,6 +178,7 @@ export { ToggleWrapper }; export { ToggleButtonWrapper }; export { TokenWrapper }; export { TokenGroupWrapper }; +export { TooltipWrapper }; export { TopNavigationWrapper }; export { TreeViewWrapper }; export { TutorialPanelWrapper }; @@ -1724,6 +1726,25 @@ findTokenGroup(selector?: string): TokenGroupWrapper | null; * @returns {Array} */ findAllTokenGroups(selector?: string): Array; +/** + * Returns the wrapper of the first Tooltip that matches the specified CSS selector. + * If no CSS selector is specified, returns the wrapper of the first Tooltip. + * If no matching Tooltip is found, returns \`null\`. + * + * @param {string} [selector] CSS Selector + * @returns {TooltipWrapper | null} + */ +findTooltip(selector?: string): TooltipWrapper | null; + +/** + * Returns an array of Tooltip wrapper that matches the specified CSS selector. + * If no CSS selector is specified, returns all of the Tooltips inside the current wrapper. + * If no matching Tooltip is found, returns an empty array. + * + * @param {string} [selector] CSS Selector + * @returns {Array} + */ +findAllTooltips(selector?: string): Array; /** * Returns the wrapper of the first TopNavigation that matches the specified CSS selector. * If no CSS selector is specified, returns the wrapper of the first TopNavigation. @@ -2857,6 +2878,19 @@ ElementWrapper.prototype.findTokenGroup = function(selector) { ElementWrapper.prototype.findAllTokenGroups = function(selector) { return this.findAllComponents(TokenGroupWrapper, selector); }; +ElementWrapper.prototype.findTooltip = function(selector) { + let rootSelector = \`.\${TooltipWrapper.rootSelector}\`; + if("legacyRootSelector" in TooltipWrapper && TooltipWrapper.legacyRootSelector){ + rootSelector = \`:is(.\${TooltipWrapper.rootSelector}, .\${TooltipWrapper.legacyRootSelector})\`; + } + // casting to 'any' is needed to avoid this issue with generics + // https://github.com/microsoft/TypeScript/issues/29132 + return (this as any).findComponent(selector ? appendSelector(selector, rootSelector) : rootSelector, TooltipWrapper); +}; + +ElementWrapper.prototype.findAllTooltips = function(selector) { + return this.findAllComponents(TooltipWrapper, selector); +}; ElementWrapper.prototype.findTopNavigation = function(selector) { let rootSelector = \`.\${TopNavigationWrapper.rootSelector}\`; if("legacyRootSelector" in TopNavigationWrapper && TopNavigationWrapper.legacyRootSelector){ @@ -3010,6 +3044,7 @@ import ToggleWrapper from './toggle'; import ToggleButtonWrapper from './toggle-button'; import TokenWrapper from './token'; import TokenGroupWrapper from './token-group'; +import TooltipWrapper from './tooltip'; import TopNavigationWrapper from './top-navigation'; import TreeViewWrapper from './tree-view'; import TutorialPanelWrapper from './tutorial-panel'; @@ -3097,6 +3132,7 @@ export { ToggleWrapper }; export { ToggleButtonWrapper }; export { TokenWrapper }; export { TokenGroupWrapper }; +export { TooltipWrapper }; export { TopNavigationWrapper }; export { TreeViewWrapper }; export { TutorialPanelWrapper }; @@ -4482,6 +4518,23 @@ findTokenGroup(selector?: string): TokenGroupWrapper; * @returns {MultiElementWrapper} */ findAllTokenGroups(selector?: string): MultiElementWrapper; +/** + * Returns a wrapper that matches the Tooltips with the specified CSS selector. + * If no CSS selector is specified, returns a wrapper that matches Tooltips. + * + * @param {string} [selector] CSS Selector + * @returns {TooltipWrapper} + */ +findTooltip(selector?: string): TooltipWrapper; + +/** + * Returns a multi-element wrapper that matches Tooltips with the specified CSS selector. + * If no CSS selector is specified, returns a multi-element wrapper that matches Tooltips. + * + * @param {string} [selector] CSS Selector + * @returns {MultiElementWrapper} + */ +findAllTooltips(selector?: string): MultiElementWrapper; /** * Returns a wrapper that matches the TopNavigations with the specified CSS selector. * If no CSS selector is specified, returns a wrapper that matches TopNavigations. @@ -5607,6 +5660,19 @@ ElementWrapper.prototype.findTokenGroup = function(selector) { ElementWrapper.prototype.findAllTokenGroups = function(selector) { return this.findAllComponents(TokenGroupWrapper, selector); }; +ElementWrapper.prototype.findTooltip = function(selector) { + let rootSelector = \`.\${TooltipWrapper.rootSelector}\`; + if("legacyRootSelector" in TooltipWrapper && TooltipWrapper.legacyRootSelector){ + rootSelector = \`:is(.\${TooltipWrapper.rootSelector}, .\${TooltipWrapper.legacyRootSelector})\`; + } + // casting to 'any' is needed to avoid this issue with generics + // https://github.com/microsoft/TypeScript/issues/29132 + return (this as any).findComponent(selector ? appendSelector(selector, rootSelector) : rootSelector, TooltipWrapper); +}; + +ElementWrapper.prototype.findAllTooltips = function(selector) { + return this.findAllComponents(TooltipWrapper, selector); +}; ElementWrapper.prototype.findTopNavigation = function(selector) { let rootSelector = \`.\${TopNavigationWrapper.rootSelector}\`; if("legacyRootSelector" in TopNavigationWrapper && TopNavigationWrapper.legacyRootSelector){ diff --git a/src/button-group/icon-button-item.tsx b/src/button-group/icon-button-item.tsx index 307dd7fbc6..a92438d6a1 100644 --- a/src/button-group/icon-button-item.tsx +++ b/src/button-group/icon-button-item.tsx @@ -64,13 +64,7 @@ const IconButtonItem = forwardRef( className={clsx(testUtilStyles.tooltip, testUtilStyles['button-group-tooltip'])} getTrack={() => containerRef.current} trackKey={item.id} - content={ - canShowFeedback - ? typeof item.popoverFeedback === 'string' - ? item.popoverFeedback - : item.text - : item.text - } + content={canShowFeedback && item.popoverFeedback ? item.popoverFeedback : item.text} onEscape={onTooltipDismiss} /> )} From bf850592e547d8f8b9979fd4472d45d520deb979 Mon Sep 17 00:00:00 2001 From: Philipp Schneider Date: Thu, 18 Dec 2025 16:14:41 +0100 Subject: [PATCH 34/52] fix: Add new examples to tooltip simple page --- pages/tooltip/simple.page.tsx | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/pages/tooltip/simple.page.tsx b/pages/tooltip/simple.page.tsx index 749648212b..93988f0de6 100644 --- a/pages/tooltip/simple.page.tsx +++ b/pages/tooltip/simple.page.tsx @@ -76,6 +76,7 @@ export default function TooltipSimple() { nativeButtonAttributes={{ onFocus: () => setShowInteractive(true), onBlur: () => setShowInteractive(false), + 'aria-describedby': `tooltip-position-${interactivePosition}`, }} data-testid="hover-button" > @@ -109,6 +110,7 @@ export default function TooltipSimple() { nativeButtonAttributes={{ onFocus: () => setShowTop(true), onBlur: () => setShowTop(false), + 'aria-describedby': 'tooltip-top', }} > Short @@ -137,6 +139,7 @@ export default function TooltipSimple() { nativeButtonAttributes={{ onFocus: () => setShowBottom(true), onBlur: () => setShowBottom(false), + 'aria-describedby': 'tooltip-bottom', }} > Medium @@ -165,13 +168,14 @@ export default function TooltipSimple() { nativeButtonAttributes={{ onFocus: () => setShowLeft(true), onBlur: () => setShowLeft(false), + 'aria-describedby': 'tooltip-left', }} > Long {showLeft && ( leftRef.current} position="left" onEscape={() => setShowLeft(false)} @@ -186,13 +190,14 @@ export default function TooltipSimple() { ref={rightRef} onMouseEnter={() => setShowRight(true)} onMouseLeave={() => setShowRight(false)} + onFocus={() => setShowRight(true)} + onBlur={() => setShowRight(false)} style={{ display: 'inline-block' }} > - {showDelete && ( - deleteWrapperRef.current} - position="top" - onEscape={() => setShowDelete(false)} - trackKey="delete-disabled" - /> - )} - - -
setShowSave(true)} - onMouseLeave={() => setShowSave(false)} - style={{ display: 'inline-block' }} - > - - {showSave && ( - saveWrapperRef.current} - position="top" - onEscape={() => setShowSave(false)} - trackKey="save-disabled" - /> - )} -
- -
setShowDownload(true)} - onMouseLeave={() => setShowDownload(false)} - style={{ display: 'inline-block' }} - > - - {showDownload && ( - downloadWrapperRef.current} - position="top" - onEscape={() => setShowDownload(false)} - trackKey="download-disabled" - /> - )} -
-
-
-
- ); -} - function TruncatedTextExample() { const ref1 = useRef(null); const ref2 = useRef(null); From 9954e67f192c85c08a80b626a999c53bc457aab5 Mon Sep 17 00:00:00 2001 From: Philipp Schneider Date: Fri, 19 Dec 2025 10:16:54 +0100 Subject: [PATCH 36/52] fix: Update test page --- pages/tooltip/component-usage.page.tsx | 56 +------------------------- src/button-group/icon-button-item.tsx | 6 ++- 2 files changed, 7 insertions(+), 55 deletions(-) diff --git a/pages/tooltip/component-usage.page.tsx b/pages/tooltip/component-usage.page.tsx index 8e4726e123..06ff496272 100644 --- a/pages/tooltip/component-usage.page.tsx +++ b/pages/tooltip/component-usage.page.tsx @@ -29,7 +29,6 @@ export default function InternalTooltipExamples() {
Tooltip
- @@ -76,11 +75,11 @@ function TruncatedTextExample() { borderRadius: '4px', }} > - my-very-long-filename-document-final-v2.pdf + this-is-a-example-filename-document-final-v2.pdf {show1 && ( ref1.current} onEscape={() => setShow1(false)} trackKey="file1" @@ -149,57 +148,6 @@ function TruncatedTextExample() { ); } -function IconOnlyButtonsExample() { - const refs = { - edit: useRef(null), - copy: useRef(null), - delete: useRef(null), - settings: useRef(null), - }; - const [show, setShow] = useState({ edit: false, copy: false, delete: false, settings: false }); - - return ( - Icon-Only Actions}> - - - {[ - { key: 'edit', icon: 'edit', label: 'Edit item' }, - { key: 'copy', icon: 'copy', label: 'Copy to clipboard' }, - { key: 'delete', icon: 'remove', label: 'Delete item' }, - { key: 'settings', icon: 'settings', label: 'Open settings' }, - ].map(({ key, icon, label }) => ( -
setShow({ ...show, [key]: true })} - onMouseLeave={() => setShow({ ...show, [key]: false })} - style={{ display: 'inline-block' }} - > -
- ))} -
-
-
- ); -} - function FileInputItemExample() { return ( ButtonGroup - FileInputItem (Internal Tooltips)}> diff --git a/src/button-group/icon-button-item.tsx b/src/button-group/icon-button-item.tsx index a92438d6a1..f00f50ea69 100644 --- a/src/button-group/icon-button-item.tsx +++ b/src/button-group/icon-button-item.tsx @@ -8,6 +8,7 @@ import { warnOnce } from '@cloudscape-design/component-toolkit/internal'; import { ButtonProps } from '../button/interfaces.js'; import { InternalButton } from '../button/internal.js'; import { CancelableEventHandler, fireCancelableEvent } from '../internal/events/index.js'; +import InternalLiveRegion from '../live-region/internal.js'; import Tooltip from '../tooltip/internal.js'; import { ButtonGroupProps, InternalIconButton } from './interfaces.js'; @@ -64,7 +65,10 @@ const IconButtonItem = forwardRef( className={clsx(testUtilStyles.tooltip, testUtilStyles['button-group-tooltip'])} getTrack={() => containerRef.current} trackKey={item.id} - content={canShowFeedback && item.popoverFeedback ? item.popoverFeedback : item.text} + content={ + (showFeedback && {item.popoverFeedback}) || + item.text + } onEscape={onTooltipDismiss} /> )} From c0f7ec66bdb441ba1e9c3e273507ddf34f9568ff Mon Sep 17 00:00:00 2001 From: Philipp Schneider Date: Fri, 19 Dec 2025 11:20:32 +0100 Subject: [PATCH 37/52] fix: Remove description from text --- pages/tooltip/component-usage.page.tsx | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/pages/tooltip/component-usage.page.tsx b/pages/tooltip/component-usage.page.tsx index 06ff496272..0f0febf8be 100644 --- a/pages/tooltip/component-usage.page.tsx +++ b/pages/tooltip/component-usage.page.tsx @@ -153,10 +153,6 @@ function FileInputItemExample() { ButtonGroup - FileInputItem (Internal Tooltips)}> - - Keyboard Navigation: ButtonGroup is a composite widget. Press Tab to focus the group, then - use Arrow keys to navigate between items within the group. This is standard ARIA behavior. - Token (Inline with Tooltip)}> - - Note: Hover or focus on long tokens. Tooltip shows full text when truncated. Requires - variant="inline" and tooltipContent prop. -
FileTokenGroup - FileToken (Truncated Filename)}> - - Note: FileTokenGroup automatically shows tooltips for truncated filenames. Hover over long - filenames to see the full name in a tooltip. -
Date: Sun, 4 Jan 2026 16:28:12 +0100 Subject: [PATCH 38/52] feat: New hook for ariaDescribeBy --- ...age.tsx => component-integration.page.tsx} | 144 ++++--- pages/tooltip/simple.page.tsx | 391 ++++++++++-------- src/tooltip/index.tsx | 3 +- src/tooltip/internal.tsx | 16 +- src/tooltip/use-hidden-description.ts | 60 +++ 5 files changed, 364 insertions(+), 250 deletions(-) rename pages/tooltip/{component-usage.page.tsx => component-integration.page.tsx} (79%) create mode 100644 src/tooltip/use-hidden-description.ts diff --git a/pages/tooltip/component-usage.page.tsx b/pages/tooltip/component-integration.page.tsx similarity index 79% rename from pages/tooltip/component-usage.page.tsx rename to pages/tooltip/component-integration.page.tsx index 0f0febf8be..e4bb3aec2e 100644 --- a/pages/tooltip/component-usage.page.tsx +++ b/pages/tooltip/component-integration.page.tsx @@ -2,7 +2,6 @@ // SPDX-License-Identifier: Apache-2.0 import React, { useRef, useState } from 'react'; -import Box from '~components/box'; import BreadcrumbGroup from '~components/breadcrumb-group'; import Button from '~components/button'; import ButtonGroup from '~components/button-group'; @@ -16,18 +15,25 @@ import SegmentedControl from '~components/segmented-control'; import Select, { SelectProps } from '~components/select'; import Slider from '~components/slider'; import SpaceBetween from '~components/space-between'; +import StatusIndicator from '~components/status-indicator'; import Tabs from '~components/tabs'; import Token from '~components/token'; -import Tooltip from '~components/tooltip'; +import Tooltip, { useHiddenDescription } from '~components/tooltip'; import ScreenshotArea from '../utils/screenshot-area'; export default function InternalTooltipExamples() { return ( - <> - +
+

Tooltip Component Integration

+

+ Examples showing how tooltips enhance various components with contextual help, disabled state explanations, and + overflow content. Components demonstrated: Button, Select, Multiselect, Token, FileTokenGroup, SegmentedControl, + BreadcrumbGroup, Slider, Calendar, DateRangePicker, and Tabs. +

+ + -
Tooltip
@@ -42,8 +48,8 @@ export default function InternalTooltipExamples() {
- - +
+
); } @@ -55,6 +61,16 @@ function TruncatedTextExample() { const [show2, setShow2] = useState(false); const [show3, setShow3] = useState(false); + const { targetProps: targetProps1, descriptionEl: descriptionEl1 } = useHiddenDescription( + 'Lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor' + ); + const { targetProps: targetProps2, descriptionEl: descriptionEl2 } = useHiddenDescription( + 'Ut enim ad minim veniam quis nostrud exercitation ullamco laboris nisi aliquip' + ); + const { targetProps: targetProps3, descriptionEl: descriptionEl3 } = useHiddenDescription( + 'Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore' + ); + return ( Truncated Text with Overflow Tooltips}> @@ -63,23 +79,25 @@ function TruncatedTextExample() { ref={ref1} onMouseEnter={() => setShow1(true)} onMouseLeave={() => setShow1(false)} - style={{ maxWidth: '200px' }} + onFocus={() => setShow1(true)} + onBlur={() => setShow1(false)} + tabIndex={0} + style={{ + maxWidth: '200px', + overflow: 'hidden', + textOverflow: 'ellipsis', + whiteSpace: 'nowrap', + padding: '8px', + border: '1px solid', + borderRadius: '4px', + }} + {...targetProps1} > -
- this-is-a-example-filename-document-final-v2.pdf -
+ {descriptionEl1} + Lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor {show1 && ( ref1.current} onEscape={() => setShow1(false)} trackKey="file1" @@ -91,23 +109,25 @@ function TruncatedTextExample() { ref={ref2} onMouseEnter={() => setShow2(true)} onMouseLeave={() => setShow2(false)} - style={{ maxWidth: '200px' }} + onFocus={() => setShow2(true)} + onBlur={() => setShow2(false)} + tabIndex={0} + style={{ + maxWidth: '200px', + overflow: 'hidden', + textOverflow: 'ellipsis', + whiteSpace: 'nowrap', + padding: '8px', + border: '1px solid', + borderRadius: '4px', + }} + {...targetProps2} > -
- arn:aws:s3:::my-bucket-name/path/to/resource -
+ {descriptionEl2} + Ut enim ad minim veniam quis nostrud exercitation ullamco laboris nisi aliquip {show2 && ( ref2.current} onEscape={() => setShow2(false)} trackKey="arn" @@ -119,23 +139,25 @@ function TruncatedTextExample() { ref={ref3} onMouseEnter={() => setShow3(true)} onMouseLeave={() => setShow3(false)} - style={{ maxWidth: '200px' }} + onFocus={() => setShow3(true)} + onBlur={() => setShow3(false)} + tabIndex={0} + style={{ + maxWidth: '200px', + overflow: 'hidden', + textOverflow: 'ellipsis', + whiteSpace: 'nowrap', + padding: '8px', + border: '1px solid', + borderRadius: '4px', + }} + {...targetProps3} > -
- user@example-company-domain-name.com -
+ {descriptionEl3} + Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore {show3 && ( ref3.current} onEscape={() => setShow3(false)} trackKey="email" @@ -150,14 +172,20 @@ function TruncatedTextExample() { function FileInputItemExample() { return ( - ButtonGroup - FileInputItem (Internal Tooltips)}> + ButtonGroup with Copy Feedback}> Copied!, + }, { type: 'icon-button', id: 'cut', text: 'Cut', iconName: 'file' }, { type: 'icon-file-input', @@ -268,19 +296,19 @@ function TokenExample() {
{}} - tooltipContent="Very long label that will be truncated and show a tooltip on hover or focus" + tooltipContent="Lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod" />
{}} - tooltipContent="Another extremely long token label that demonstrates the overflow tooltip functionality when text exceeds container width" + tooltipContent="Ut enim ad minim veniam quis nostrud exercitation ullamco laboris nisi ut aliquip" />
@@ -295,21 +323,21 @@ function FileTokenGroupExample() { { file: new File( ['content'], - 'my-extremely-long-document-name-final-version-reviewed-approved-ready-for-production-deployment.pdf', + 'lorem-ipsum-dolor-sit-amet-consectetur-adipiscing-elit-sed-do-eiusmod-tempor-incididunt.pdf', { type: 'application/pdf' } ), }, { file: new File( ['content'], - 'very-long-filename-that-will-definitely-be-truncated-and-show-a-tooltip-when-you-hover-over-it-with-your-mouse-or-keyboard.docx', + 'ut-enim-ad-minim-veniam-quis-nostrud-exercitation-ullamco-laboris-nisi-ut-aliquip-ex-ea-commodo.docx', { type: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' } ), }, { file: new File( ['content'], - 'another-extremely-long-filename-that-demonstrates-tooltip-behavior-with-overflow-ellipsis-and-full-text-display-on-hover.xlsx', + 'duis-aute-irure-dolor-in-reprehenderit-in-voluptate-velit-esse-cillum-dolore-eu-fugiat-nulla.xlsx', { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' } ), }, diff --git a/pages/tooltip/simple.page.tsx b/pages/tooltip/simple.page.tsx index 93988f0de6..4d3a9fe8f8 100644 --- a/pages/tooltip/simple.page.tsx +++ b/pages/tooltip/simple.page.tsx @@ -5,7 +5,7 @@ import React, { useRef, useState } from 'react'; import Button from '~components/button'; import SegmentedControl from '~components/segmented-control'; import SpaceBetween from '~components/space-between'; -import Tooltip from '~components/tooltip'; +import Tooltip, { useHiddenDescription } from '~components/tooltip'; import ScreenshotArea from '../utils/screenshot-area'; @@ -48,178 +48,214 @@ export default function TooltipSimple() { const [showPassword, setShowPassword] = useState(false); const passwordRef = useRef(null); + // Hidden descriptions for Content Length Variations + const { targetProps: shortTargetProps, descriptionEl: shortDescriptionEl } = useHiddenDescription('Short'); + const { targetProps: mediumTargetProps, descriptionEl: mediumDescriptionEl } = useHiddenDescription( + 'This is a medium length tooltip that provides more information than the short one but is not as detailed as the longer versions.' + ); + const { targetProps: longTargetProps, descriptionEl: longDescriptionEl } = useHiddenDescription( + 'This is a longer tooltip that provides comprehensive information about the feature or action. It includes multiple sentences and detailed explanations to help users understand exactly what will happen when they interact with this element.' + ); + const { targetProps: veryLongTargetProps, descriptionEl: veryLongDescriptionEl } = useHiddenDescription( + 'This is a very long tooltip that contains extensive information and will likely wrap to multiple lines depending on the available space. It demonstrates how tooltips handle longer content and provides a comprehensive explanation of the feature, including detailed instructions, warnings, and additional context that users might need to make informed decisions about their actions.' + ); + + // Hidden description for Interactive Position Control (updates with position) + const interactiveTooltipContent = `Tooltip positioned on ${interactivePosition}`; + const { targetProps: interactiveTargetProps, descriptionEl: interactiveDescriptionEl } = + useHiddenDescription(interactiveTooltipContent); + + // Hidden description for Truncated Text + const { targetProps: truncatedTargetProps, descriptionEl: truncatedDescriptionEl } = useHiddenDescription( + 'Lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor incididunt' + ); + + // Hidden descriptions for Interactive & Formatted Content + const { targetProps: linkTargetProps, descriptionEl: linkDescriptionEl } = useHiddenDescription( + 'AWS Documentation - Click to view complete API reference - Last updated: Today' + ); + const { targetProps: codeTargetProps, descriptionEl: codeDescriptionEl } = useHiddenDescription( + "const AWS = require('aws-sdk'); AWS.config.update({ region: 'us-west-2' });" + ); + + // Hidden description for Password Input + const { targetProps: passwordTargetProps, descriptionEl: passwordDescriptionEl } = useHiddenDescription( + 'Password Rules: Minimum of 8 characters, Include at least one lowercase letter, one uppercase letter, one number and one special character, Unique to this website' + ); + return (
-

Tooltip

+

Tooltip Examples

+

Interactive tooltip demonstrations with positioning, content variations, and accessibility features

-

Interactive Position Control

- - setInteractivePosition(detail.selectedId as 'top' | 'right' | 'bottom' | 'left')} - options={[ - { id: 'top', text: 'Top' }, - { id: 'right', text: 'Right' }, - { id: 'bottom', text: 'Bottom' }, - { id: 'left', text: 'Left' }, - ]} - /> -
setShowInteractive(true)} - onMouseLeave={() => setShowInteractive(false)} - style={{ display: 'inline-block' }} - > - - {showInteractive && ( - interactiveRef.current} - position={interactivePosition} - onEscape={() => setShowInteractive(false)} - trackKey={`position-${interactivePosition}`} + +
+

Interactive Position Control

+ + + setInteractivePosition(detail.selectedId as 'top' | 'right' | 'bottom' | 'left') + } + options={[ + { id: 'top', text: 'Top' }, + { id: 'right', text: 'Right' }, + { id: 'bottom', text: 'Bottom' }, + { id: 'left', text: 'Left' }, + ]} /> - )} -
-
-
- -

Content Length Variations

- - -
-
setShowTop(true)} - onMouseLeave={() => setShowTop(false)} - style={{ display: 'inline-block' }} - > - - {showTop && ( - topRef.current} - position="top" - onEscape={() => setShowTop(false)} - trackKey="top" - /> - )} -
+ + {interactiveDescriptionEl} + {showInteractive && ( + interactiveRef.current} + position={interactivePosition} + onEscape={() => setShowInteractive(false)} + trackKey={`position-${interactivePosition}`} + /> + )} +
+
-
-
setShowBottom(true)} - onMouseLeave={() => setShowBottom(false)} - style={{ display: 'inline-block' }} - > - - {showBottom && ( - bottomRef.current} - position="bottom" - onEscape={() => setShowBottom(false)} - trackKey="bottom" - /> - )} -
-
+ + {shortDescriptionEl} + {showTop && ( + topRef.current} + position="top" + onEscape={() => setShowTop(false)} + trackKey="top" + /> + )} +
-
-
setShowLeft(true)} - onMouseLeave={() => setShowLeft(false)} - style={{ display: 'inline-block' }} - > - - {showLeft && ( - leftRef.current} - position="left" - onEscape={() => setShowLeft(false)} - trackKey="left" - /> - )} -
-
+ + {mediumDescriptionEl} + {showBottom && ( + bottomRef.current} + position="bottom" + onEscape={() => setShowBottom(false)} + trackKey="bottom" + /> + )} + -
-
setShowRight(true)} - onMouseLeave={() => setShowRight(false)} - onFocus={() => setShowRight(true)} - onBlur={() => setShowRight(false)} - style={{ display: 'inline-block' }} - > - - {showRight && ( - rightRef.current} - position="right" - onEscape={() => setShowRight(false)} - trackKey="right" - /> - )} -
+ + {longDescriptionEl} + {showLeft && ( + leftRef.current} + position="left" + onEscape={() => setShowLeft(false)} + trackKey="left" + /> + )} + + +
setShowRight(true)} + onMouseLeave={() => setShowRight(false)} + onFocus={() => setShowRight(true)} + onBlur={() => setShowRight(false)} + style={{ display: 'inline-block' }} + > + + {veryLongDescriptionEl} + {showRight && ( + rightRef.current} + position="right" + onEscape={() => setShowRight(false)} + trackKey="right" + /> + )} +
+
-
-
- - - {/* Icon-only buttons */} -
+

Icon-Only Buttons

setShowIconEdit(true), onBlur: () => setShowIconEdit(false), - 'aria-describedby': 'tooltip-icon-edit', }} /> {showIconEdit && ( @@ -260,7 +295,6 @@ export default function TooltipSimple() { nativeButtonAttributes={{ onFocus: () => setShowIconDelete(true), onBlur: () => setShowIconDelete(false), - 'aria-describedby': 'tooltip-icon-delete', }} /> {showIconDelete && ( @@ -286,7 +320,6 @@ export default function TooltipSimple() { nativeButtonAttributes={{ onFocus: () => setShowIconSettings(true), onBlur: () => setShowIconSettings(false), - 'aria-describedby': 'tooltip-icon-settings', }} /> {showIconSettings && ( @@ -302,8 +335,7 @@ export default function TooltipSimple() {
- {/* Disabled button */} -
+

Disabled Button

setShowDisabled(false)} tabIndex={0} style={{ display: 'inline-block' }} - aria-describedby="tooltip-disabled" >
- {/* Truncated text */} -
+

Truncated Text

setShowTruncated(true)} onMouseLeave={() => setShowTruncated(false)} style={{ maxWidth: '200px' }} - aria-describedby="tooltip-truncated" + tabIndex={0} + onFocus={() => setShowTruncated(true)} + onBlur={() => setShowTruncated(false)} + {...truncatedTargetProps} > + {truncatedDescriptionEl}
- my-very-long-filename-document-final-version.pdf + Lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor incididunt
{showTruncated && ( truncatedRef.current} position="top" onEscape={() => setShowTruncated(false)} @@ -365,8 +399,7 @@ export default function TooltipSimple() {
- {/* Interactive & Formatted Content */} -
+

Interactive & Formatted Content

setShowLink(false)} style={{ display: 'inline-block' }} > + {linkDescriptionEl} setShowLink(true)} onBlur={() => setShowLink(false)} style={{ color: '#0073bb', textDecoration: 'underline', cursor: 'pointer' }} onClick={e => e.preventDefault()} - aria-describedby="tooltip-link-tooltip" + {...linkTargetProps} > Documentation Link @@ -412,12 +446,13 @@ export default function TooltipSimple() { onBlur={() => setShowCode(false)} tabIndex={0} style={{ display: 'inline-block' }} - aria-describedby="tooltip-code-tooltip" + {...codeTargetProps} > + {codeDescriptionEl}
-
+

ARIA Described-by Example

setShowPassword(true)} onBlur={() => setShowPassword(false)} - aria-describedby="tooltip-password-rules" + {...passwordTargetProps} /> + {passwordDescriptionEl} + {showPassword && ( { const baseComponentProps = useBaseComponent('Tooltip', { diff --git a/src/tooltip/internal.tsx b/src/tooltip/internal.tsx index 999bb660d9..c3990e71cb 100644 --- a/src/tooltip/internal.tsx +++ b/src/tooltip/internal.tsx @@ -34,25 +34,13 @@ export default function InternalTooltip({ }: InternalTooltipComponentProps) { const baseProps = getBaseProps(restProps); const trackRef = React.useRef(null); - const tooltipId = React.useMemo(() => { - return trackKey ? `tooltip-${trackKey}` : generateTooltipId(); - }, [trackKey]); + const tooltipId = React.useMemo(() => generateTooltipId(), []); // Update the ref with the current tracked element React.useEffect(() => { const element = getTrack(); trackRef.current = element; - // Add aria-describedby to the tracked element for accessibility - if (element && element.nodeType === Node.ELEMENT_NODE) { - element.setAttribute('aria-describedby', tooltipId); - } - return () => { - // Clean up aria-describedby when tooltip unmounts - if (element && element.nodeType === Node.ELEMENT_NODE) { - element.removeAttribute('aria-describedby'); - } - }; - }, [getTrack, tooltipId]); + }, [getTrack]); if (!trackKey && (typeof content === 'string' || typeof content === 'number')) { trackKey = content; diff --git a/src/tooltip/use-hidden-description.ts b/src/tooltip/use-hidden-description.ts new file mode 100644 index 0000000000..25cac37eee --- /dev/null +++ b/src/tooltip/use-hidden-description.ts @@ -0,0 +1,60 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +import * as React from 'react'; + +// Simple unique ID generator for React < 18 compatibility +let idCounter = 0; +function generateUniqueId(prefix = 'tooltip-description') { + return `${prefix}-${++idCounter}`; +} + +/** + * A utility hook for creating accessible descriptions for tooltips. + * + * This hook provides a way to add hidden descriptions to elements that work with screen readers + * while also supporting visual tooltips for sighted users. It creates a unique ID and returns + * props to connect an element to its description via aria-describedby. + * + * @param description - The description text to be announced by screen readers + * + * @returns An object with the following properties: + * - `targetProps`: Props to spread onto the target element (contains aria-describedby) + * - `descriptionEl`: A hidden span element containing the description text + * - `descriptionId`: The unique ID used for the description + * + * @example + * ```tsx + * function MyTooltipButton() { + * const [showTooltip, setShowTooltip] = useState(false); + * const { targetProps, descriptionEl } = useHiddenDescription( + * 'This button saves your changes' + * ); + * + * return ( + *
+ * + * {descriptionEl} + * {showTooltip && ( + * + * )} + *
+ * ); + * } + * ``` + */ +export function useHiddenDescription(description?: string) { + const [id] = React.useState(() => generateUniqueId()); + return { + targetProps: { + 'aria-describedby': description ? id : undefined, + }, + descriptionEl: description ? React.createElement('span', { id, hidden: true }, description) : null, + descriptionId: id, + }; +} From d5bed19310dc5664ef6e074b035b61d9d82dc678 Mon Sep 17 00:00:00 2001 From: Philipp Schneider Date: Sun, 4 Jan 2026 16:38:55 +0100 Subject: [PATCH 39/52] fix: update interface for tooltip with new hook --- src/tooltip/interfaces.ts | 46 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/src/tooltip/interfaces.ts b/src/tooltip/interfaces.ts index e10720cf29..fecd1002d0 100644 --- a/src/tooltip/interfaces.ts +++ b/src/tooltip/interfaces.ts @@ -41,6 +41,52 @@ export namespace TooltipProps { export type Position = PopoverProps.Position; } +/** + * A utility hook for creating accessible descriptions for tooltips. + * + * This hook provides a way to add hidden descriptions to elements that work with screen readers + * while also supporting visual tooltips for sighted users. It creates a unique ID and returns + * props to connect an element to its description via aria-describedby. + * + * @param description - The description text to be announced by screen readers + * + * @returns An object with the following properties: + * - `targetProps`: Props to spread onto the target element (contains aria-describedby) + * - `descriptionEl`: A hidden span element containing the description text + * - `descriptionId`: The unique ID used for the description + * + * @example + * ```tsx + * import Tooltip, { useHiddenDescription } from '@cloudscape-design/components/tooltip'; + * + * function MyComponent() { + * const [show, setShow] = useState(false); + * const { targetProps, descriptionEl } = useHiddenDescription( + * 'This button saves your changes' + * ); + * + * return ( + *
+ * + * {descriptionEl} + * {show && } + *
+ * ); + * } + * ``` + */ +export declare function useHiddenDescription(description?: string): { + targetProps: { 'aria-describedby'?: string }; + descriptionEl: React.ReactElement | null; + descriptionId: string; +}; + /** * Internal tooltip props - includes props not exposed in public API. * @internal From a73d44708a1740a1785a6e3a7f60ac2e24d6a1a7 Mon Sep 17 00:00:00 2001 From: Philipp Schneider Date: Sun, 4 Jan 2026 16:47:42 +0100 Subject: [PATCH 40/52] fix: Add tooltip to whiteList --- build-tools/tasks/docs.js | 1 + 1 file changed, 1 insertion(+) diff --git a/build-tools/tasks/docs.js b/build-tools/tasks/docs.js index f642d0bcc5..34b5834261 100644 --- a/build-tools/tasks/docs.js +++ b/build-tools/tasks/docs.js @@ -12,6 +12,7 @@ module.exports = function docs() { extraExports: { FileDropzone: ['useFilesDragging'], TagEditor: ['getTagsDiff'], + Tooltip: ['useHiddenDescription'], }, }); writeTestUtilsDocumentation({ From 256953dfe2538dddc9ad442e5aedac638207199b Mon Sep 17 00:00:00 2001 From: Philipp Schneider Date: Sun, 4 Jan 2026 17:08:34 +0100 Subject: [PATCH 41/52] fix: Update simple page for tooltip --- pages/tooltip/simple.page.tsx | 130 +++------------------------------- 1 file changed, 8 insertions(+), 122 deletions(-) diff --git a/pages/tooltip/simple.page.tsx b/pages/tooltip/simple.page.tsx index 4d3a9fe8f8..772f67892d 100644 --- a/pages/tooltip/simple.page.tsx +++ b/pages/tooltip/simple.page.tsx @@ -26,16 +26,8 @@ export default function TooltipSimple() { const rightRef = useRef(null); // Common use cases - const [showIconEdit, setShowIconEdit] = useState(false); - const [showIconDelete, setShowIconDelete] = useState(false); - const [showIconSettings, setShowIconSettings] = useState(false); - const [showDisabled, setShowDisabled] = useState(false); const [showTruncated, setShowTruncated] = useState(false); - const iconEditRef = useRef(null); - const iconDeleteRef = useRef(null); - const iconSettingsRef = useRef(null); - const disabledRef = useRef(null); const truncatedRef = useRef(null); // Interactive content @@ -49,15 +41,15 @@ export default function TooltipSimple() { const passwordRef = useRef(null); // Hidden descriptions for Content Length Variations - const { targetProps: shortTargetProps, descriptionEl: shortDescriptionEl } = useHiddenDescription('Short'); + const { targetProps: shortTargetProps, descriptionEl: shortDescriptionEl } = useHiddenDescription('Lorem'); const { targetProps: mediumTargetProps, descriptionEl: mediumDescriptionEl } = useHiddenDescription( - 'This is a medium length tooltip that provides more information than the short one but is not as detailed as the longer versions.' + 'Lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor incididunt ut labore.' ); const { targetProps: longTargetProps, descriptionEl: longDescriptionEl } = useHiddenDescription( - 'This is a longer tooltip that provides comprehensive information about the feature or action. It includes multiple sentences and detailed explanations to help users understand exactly what will happen when they interact with this element.' + 'Lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.' ); const { targetProps: veryLongTargetProps, descriptionEl: veryLongDescriptionEl } = useHiddenDescription( - 'This is a very long tooltip that contains extensive information and will likely wrap to multiple lines depending on the available space. It demonstrates how tooltips handle longer content and provides a comprehensive explanation of the feature, including detailed instructions, warnings, and additional context that users might need to make informed decisions about their actions.' + 'Lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident sunt in culpa qui officia deserunt mollit anim id est laborum.' ); // Hidden description for Interactive Position Control (updates with position) @@ -158,7 +150,7 @@ export default function TooltipSimple() { {shortDescriptionEl} {showTop && ( topRef.current} position="top" onEscape={() => setShowTop(false)} @@ -186,7 +178,7 @@ export default function TooltipSimple() { {mediumDescriptionEl} {showBottom && ( bottomRef.current} position="bottom" onEscape={() => setShowBottom(false)} @@ -214,7 +206,7 @@ export default function TooltipSimple() { {longDescriptionEl} {showLeft && ( leftRef.current} position="left" onEscape={() => setShowLeft(false)} @@ -244,7 +236,7 @@ export default function TooltipSimple() { {veryLongDescriptionEl} {showRight && ( rightRef.current} position="right" onEscape={() => setShowRight(false)} @@ -255,112 +247,6 @@ export default function TooltipSimple() {
-
-

Icon-Only Buttons

- -
setShowIconEdit(true)} - onMouseLeave={() => setShowIconEdit(false)} - style={{ display: 'inline-block' }} - > -
- -
setShowIconDelete(true)} - onMouseLeave={() => setShowIconDelete(false)} - style={{ display: 'inline-block' }} - > -
- -
setShowIconSettings(true)} - onMouseLeave={() => setShowIconSettings(false)} - style={{ display: 'inline-block' }} - > -
-
-
- -
-

Disabled Button

-
setShowDisabled(true)} - onMouseLeave={() => setShowDisabled(false)} - onFocus={() => setShowDisabled(true)} - onBlur={() => setShowDisabled(false)} - tabIndex={0} - style={{ display: 'inline-block' }} - > - - {showDisabled && ( - disabledRef.current} - position="top" - onEscape={() => setShowDisabled(false)} - trackKey="disabled" - /> - )} -
-
-

Truncated Text

Date: Mon, 5 Jan 2026 14:31:10 +0100 Subject: [PATCH 42/52] fix: AriaDescribedBy on simple page --- build-tools/tasks/docs.js | 1 - pages/tooltip/component-integration.page.tsx | 18 +- pages/tooltip/simple.page.tsx | 198 +++++++++---------- src/tooltip/index.tsx | 3 +- src/tooltip/interfaces.ts | 46 ----- src/tooltip/use-hidden-description.ts | 60 ------ 6 files changed, 90 insertions(+), 236 deletions(-) delete mode 100644 src/tooltip/use-hidden-description.ts diff --git a/build-tools/tasks/docs.js b/build-tools/tasks/docs.js index 34b5834261..f642d0bcc5 100644 --- a/build-tools/tasks/docs.js +++ b/build-tools/tasks/docs.js @@ -12,7 +12,6 @@ module.exports = function docs() { extraExports: { FileDropzone: ['useFilesDragging'], TagEditor: ['getTagsDiff'], - Tooltip: ['useHiddenDescription'], }, }); writeTestUtilsDocumentation({ diff --git a/pages/tooltip/component-integration.page.tsx b/pages/tooltip/component-integration.page.tsx index e4bb3aec2e..2d7e35ba5d 100644 --- a/pages/tooltip/component-integration.page.tsx +++ b/pages/tooltip/component-integration.page.tsx @@ -18,7 +18,7 @@ import SpaceBetween from '~components/space-between'; import StatusIndicator from '~components/status-indicator'; import Tabs from '~components/tabs'; import Token from '~components/token'; -import Tooltip, { useHiddenDescription } from '~components/tooltip'; +import Tooltip from '~components/tooltip'; import ScreenshotArea from '../utils/screenshot-area'; @@ -61,16 +61,6 @@ function TruncatedTextExample() { const [show2, setShow2] = useState(false); const [show3, setShow3] = useState(false); - const { targetProps: targetProps1, descriptionEl: descriptionEl1 } = useHiddenDescription( - 'Lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor' - ); - const { targetProps: targetProps2, descriptionEl: descriptionEl2 } = useHiddenDescription( - 'Ut enim ad minim veniam quis nostrud exercitation ullamco laboris nisi aliquip' - ); - const { targetProps: targetProps3, descriptionEl: descriptionEl3 } = useHiddenDescription( - 'Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore' - ); - return ( Truncated Text with Overflow Tooltips}> @@ -91,9 +81,7 @@ function TruncatedTextExample() { border: '1px solid', borderRadius: '4px', }} - {...targetProps1} > - {descriptionEl1} Lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor {show1 && ( - {descriptionEl2} Ut enim ad minim veniam quis nostrud exercitation ullamco laboris nisi aliquip {show2 && ( - {descriptionEl3} Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore {show3 && ( (null); + const truncatedRef = useRef(null); // Interactive content const [showLink, setShowLink] = useState(false); const [showCode, setShowCode] = useState(false); - const linkRef = useRef(null); - const codeRef = useRef(null); + const linkRef = useRef(null); + const codeRef = useRef(null); // Password input const [showPassword, setShowPassword] = useState(false); const passwordRef = useRef(null); - // Hidden descriptions for Content Length Variations - const { targetProps: shortTargetProps, descriptionEl: shortDescriptionEl } = useHiddenDescription('Lorem'); - const { targetProps: mediumTargetProps, descriptionEl: mediumDescriptionEl } = useHiddenDescription( - 'Lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor incididunt ut labore.' - ); - const { targetProps: longTargetProps, descriptionEl: longDescriptionEl } = useHiddenDescription( - 'Lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.' - ); - const { targetProps: veryLongTargetProps, descriptionEl: veryLongDescriptionEl } = useHiddenDescription( - 'Lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident sunt in culpa qui officia deserunt mollit anim id est laborum.' - ); - - // Hidden description for Interactive Position Control (updates with position) - const interactiveTooltipContent = `Tooltip positioned on ${interactivePosition}`; - const { targetProps: interactiveTargetProps, descriptionEl: interactiveDescriptionEl } = - useHiddenDescription(interactiveTooltipContent); - - // Hidden description for Truncated Text - const { targetProps: truncatedTargetProps, descriptionEl: truncatedDescriptionEl } = useHiddenDescription( - 'Lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor incididunt' - ); - - // Hidden descriptions for Interactive & Formatted Content - const { targetProps: linkTargetProps, descriptionEl: linkDescriptionEl } = useHiddenDescription( - 'AWS Documentation - Click to view complete API reference - Last updated: Today' - ); - const { targetProps: codeTargetProps, descriptionEl: codeDescriptionEl } = useHiddenDescription( - "const AWS = require('aws-sdk'); AWS.config.update({ region: 'us-west-2' });" - ); - - // Hidden description for Password Input - const { targetProps: passwordTargetProps, descriptionEl: passwordDescriptionEl } = useHiddenDescription( - 'Password Rules: Minimum of 8 characters, Include at least one lowercase letter, one uppercase letter, one number and one special character, Unique to this website' - ); - return (

Tooltip Examples

-

Interactive tooltip demonstrations with positioning, content variations, and accessibility features

+

Interactive tooltip demonstrations with positioning and content variations

@@ -106,15 +70,17 @@ export default function TooltipSimple() { - {interactiveDescriptionEl} + {showInteractive && ( setShowTop(true), onBlur: () => setShowTop(false), - ...shortTargetProps, }} > Short - {shortDescriptionEl} + {showTop && ( setShowBottom(true), onBlur: () => setShowBottom(false), - ...mediumTargetProps, }} > Medium - {mediumDescriptionEl} + {showBottom && ( setShowLeft(true), onBlur: () => setShowLeft(false), - ...longTargetProps, }} > Long - {longDescriptionEl} + {showLeft && ( setShowRight(true)} onMouseLeave={() => setShowRight(false)} - onFocus={() => setShowRight(true)} - onBlur={() => setShowRight(false)} style={{ display: 'inline-block' }} > - {veryLongDescriptionEl} + {showRight && (

Truncated Text

-
setShowTruncated(true)} onMouseLeave={() => setShowTruncated(false)} - style={{ maxWidth: '200px' }} - tabIndex={0} onFocus={() => setShowTruncated(true)} onBlur={() => setShowTruncated(false)} - {...truncatedTargetProps} + aria-describedby="truncated-description" + style={{ + maxWidth: '200px', + overflow: 'hidden', + textOverflow: 'ellipsis', + whiteSpace: 'nowrap', + padding: '8px', + border: '1px solid', + borderRadius: '4px', + cursor: 'pointer', + background: 'transparent', + textAlign: 'left', + }} > - {truncatedDescriptionEl} -
- Lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor incididunt -
- {showTruncated && ( - truncatedRef.current} - position="top" - onEscape={() => setShowTruncated(false)} - trackKey="truncated" - /> - )} -
+ Lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor incididunt + + + {showTruncated && ( + truncatedRef.current} + position="top" + onEscape={() => setShowTruncated(false)} + trackKey="truncated" + /> + )}

Interactive & Formatted Content

-
setShowLink(true)} - onMouseLeave={() => setShowLink(false)} - style={{ display: 'inline-block' }} - > - {linkDescriptionEl} +
setShowLink(true)} onBlur={() => setShowLink(false)} + onMouseEnter={() => setShowLink(true)} + onMouseLeave={() => setShowLink(false)} + aria-describedby="link-description" style={{ color: '#0073bb', textDecoration: 'underline', cursor: 'pointer' }} onClick={e => e.preventDefault()} - {...linkTargetProps} > Documentation Link + {showLink && ( -
setShowCode(true)} - onMouseLeave={() => setShowCode(false)} - onFocus={() => setShowCode(true)} - onBlur={() => setShowCode(false)} - tabIndex={0} - style={{ display: 'inline-block' }} - {...codeTargetProps} - > - {codeDescriptionEl} - + + {showCode && (
-

ARIA Described-by Example

-
setShowPassword(true)} - onMouseLeave={() => setShowPassword(false)} - > +

Password Input Example

+
@@ -389,6 +363,7 @@ export default function TooltipSimple() { id="password-input" type="password" placeholder="Enter password" + aria-describedby="password-description" style={{ padding: '8px', border: '1px solid #ccc', @@ -396,10 +371,13 @@ export default function TooltipSimple() { }} onFocus={() => setShowPassword(true)} onBlur={() => setShowPassword(false)} - {...passwordTargetProps} + onMouseEnter={() => setShowPassword(true)} + onMouseLeave={() => setShowPassword(false)} /> - {passwordDescriptionEl} - + {showPassword && ( { const baseComponentProps = useBaseComponent('Tooltip', { diff --git a/src/tooltip/interfaces.ts b/src/tooltip/interfaces.ts index fecd1002d0..e10720cf29 100644 --- a/src/tooltip/interfaces.ts +++ b/src/tooltip/interfaces.ts @@ -41,52 +41,6 @@ export namespace TooltipProps { export type Position = PopoverProps.Position; } -/** - * A utility hook for creating accessible descriptions for tooltips. - * - * This hook provides a way to add hidden descriptions to elements that work with screen readers - * while also supporting visual tooltips for sighted users. It creates a unique ID and returns - * props to connect an element to its description via aria-describedby. - * - * @param description - The description text to be announced by screen readers - * - * @returns An object with the following properties: - * - `targetProps`: Props to spread onto the target element (contains aria-describedby) - * - `descriptionEl`: A hidden span element containing the description text - * - `descriptionId`: The unique ID used for the description - * - * @example - * ```tsx - * import Tooltip, { useHiddenDescription } from '@cloudscape-design/components/tooltip'; - * - * function MyComponent() { - * const [show, setShow] = useState(false); - * const { targetProps, descriptionEl } = useHiddenDescription( - * 'This button saves your changes' - * ); - * - * return ( - *
- * - * {descriptionEl} - * {show && } - *
- * ); - * } - * ``` - */ -export declare function useHiddenDescription(description?: string): { - targetProps: { 'aria-describedby'?: string }; - descriptionEl: React.ReactElement | null; - descriptionId: string; -}; - /** * Internal tooltip props - includes props not exposed in public API. * @internal diff --git a/src/tooltip/use-hidden-description.ts b/src/tooltip/use-hidden-description.ts deleted file mode 100644 index 25cac37eee..0000000000 --- a/src/tooltip/use-hidden-description.ts +++ /dev/null @@ -1,60 +0,0 @@ -// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 -import * as React from 'react'; - -// Simple unique ID generator for React < 18 compatibility -let idCounter = 0; -function generateUniqueId(prefix = 'tooltip-description') { - return `${prefix}-${++idCounter}`; -} - -/** - * A utility hook for creating accessible descriptions for tooltips. - * - * This hook provides a way to add hidden descriptions to elements that work with screen readers - * while also supporting visual tooltips for sighted users. It creates a unique ID and returns - * props to connect an element to its description via aria-describedby. - * - * @param description - The description text to be announced by screen readers - * - * @returns An object with the following properties: - * - `targetProps`: Props to spread onto the target element (contains aria-describedby) - * - `descriptionEl`: A hidden span element containing the description text - * - `descriptionId`: The unique ID used for the description - * - * @example - * ```tsx - * function MyTooltipButton() { - * const [showTooltip, setShowTooltip] = useState(false); - * const { targetProps, descriptionEl } = useHiddenDescription( - * 'This button saves your changes' - * ); - * - * return ( - *
- * - * {descriptionEl} - * {showTooltip && ( - * - * )} - *
- * ); - * } - * ``` - */ -export function useHiddenDescription(description?: string) { - const [id] = React.useState(() => generateUniqueId()); - return { - targetProps: { - 'aria-describedby': description ? id : undefined, - }, - descriptionEl: description ? React.createElement('span', { id, hidden: true }, description) : null, - descriptionId: id, - }; -} From 57f7356975446445a4fb718a491bf55b96ad5e7f Mon Sep 17 00:00:00 2001 From: Philipp Schneider Date: Tue, 6 Jan 2026 15:26:23 +0100 Subject: [PATCH 43/52] fix: Hover into content on simple page, remove flikering in token --- pages/tooltip/simple.page.tsx | 102 +++++++++++++++++++--------------- src/token/internal.tsx | 14 +++-- 2 files changed, 66 insertions(+), 50 deletions(-) diff --git a/pages/tooltip/simple.page.tsx b/pages/tooltip/simple.page.tsx index 79a9496a22..cf6715b31e 100644 --- a/pages/tooltip/simple.page.tsx +++ b/pages/tooltip/simple.page.tsx @@ -27,17 +27,17 @@ export default function TooltipSimple() { // Common use cases const [showTruncated, setShowTruncated] = useState(false); - const truncatedRef = useRef(null); + const truncatedRef = useRef(null); // Interactive content const [showLink, setShowLink] = useState(false); const [showCode, setShowCode] = useState(false); - const linkRef = useRef(null); - const codeRef = useRef(null); + const linkRef = useRef(null); + const codeRef = useRef(null); // Password input const [showPassword, setShowPassword] = useState(false); - const passwordRef = useRef(null); + const passwordRef = useRef(null); return (
@@ -227,53 +227,59 @@ export default function TooltipSimple() {

Truncated Text

- - - {showTruncated && ( - truncatedRef.current} - position="top" - onEscape={() => setShowTruncated(false)} - trackKey="truncated" - /> - )} + + + {showTruncated && ( + truncatedRef.current} + position="top" + onEscape={() => setShowTruncated(false)} + trackKey="truncated" + /> + )} +

Interactive & Formatted Content

-
+ -
+
setShowCode(true)} + onMouseLeave={() => setShowCode(false)} + style={{ display: 'inline-block' }} + > -
+ } + getTrack={() => null} + trackKey="complex-wrapper" + /> + ); + + const tooltipWrapper = TooltipWrapper.findByTrackKey('complex-wrapper'); + const content = tooltipWrapper!.findContent(); + + expect(content).not.toBeNull(); + expect(content!.getElement().querySelector('strong')).toHaveTextContent('Bold'); + }); + }); });