diff --git a/src/action-card/internal.tsx b/src/action-card/internal.tsx index cca176a92f..3b7b8206b1 100644 --- a/src/action-card/internal.tsx +++ b/src/action-card/internal.tsx @@ -79,7 +79,7 @@ const InternalActionCard = React.forwardRef( const headerRowEmpty = !header && !description; const iconWrapper = icon && ( - )} diff --git a/src/internal/components/button-trigger/index.tsx b/src/internal/components/button-trigger/index.tsx index abc46867bc..462a77c0fe 100644 --- a/src/internal/components/button-trigger/index.tsx +++ b/src/internal/components/button-trigger/index.tsx @@ -128,6 +128,7 @@ const ButtonTrigger = ( ); diff --git a/src/link/__tests__/index.test.tsx b/src/link/__tests__/index.test.tsx index f033577bf9..e2f404f153 100644 --- a/src/link/__tests__/index.test.tsx +++ b/src/link/__tests__/index.test.tsx @@ -47,6 +47,19 @@ describe('Link component', () => { expect(createWrapper(wrapper.getElement()).find('[aria-label="External link"]')).toBeTruthy(); }); + describe('icon hover motion', () => { + test('every link is hover-motion trigger and target', () => { + const wrapper = renderLink({ href: '#', children: 'Internal' }); + expect(wrapper.getElement()).toHaveAttribute('data-awsui-motion-trigger', 'hover'); + expect(wrapper.getElement()).toHaveAttribute('data-awsui-motion-target', ''); + }); + + test('the external icon sits inside the link and needs no own target', () => { + const wrapper = renderLink({ href: '#', external: true, children: 'External' }); + expect(wrapper.getElement().querySelector(`.${styles['icon-wrapper']}`)).toBeTruthy(); + }); + }); + describe('i18n', () => { test('supports providing externalIconAriaLabel through i18n provider', () => { const { container } = render( diff --git a/src/link/internal.tsx b/src/link/internal.tsx index 74f4bfe670..2aa0a80fec 100644 --- a/src/link/internal.tsx +++ b/src/link/internal.tsx @@ -182,6 +182,8 @@ const InternalLink = React.forwardRef( 'aria-label': ariaLabel, 'aria-labelledby': undefined as string | undefined, [DATA_ATTR_FUNNEL_VALUE]: uniqueId, + 'data-awsui-motion-trigger': 'hover', + 'data-awsui-motion-target': '', }; if (variant === 'info' && infoLinkLabelFromContext && !ariaLabel) { diff --git a/src/pagination/internal.tsx b/src/pagination/internal.tsx index 413b01b2d3..f83debe7c5 100644 --- a/src/pagination/internal.tsx +++ b/src/pagination/internal.tsx @@ -70,6 +70,7 @@ function PageButton({ tabIndex={disabled ? -1 : 0} onClick={handleClick} aria-current={isCurrent} + data-awsui-motion-trigger="hover" {...(disabled ? {} : getAnalyticsMetadataAttribute({ @@ -259,7 +260,11 @@ const InternalPagination = React.forwardRef( }, }))} > - + {pagesVariant === 'compact' ? (
  • @@ -317,7 +322,11 @@ const InternalPagination = React.forwardRef( }, }))} > - + {jumpToPage && (
  • diff --git a/src/property-filter/filtering-token/__tests__/filtering-token.test.tsx b/src/property-filter/filtering-token/__tests__/filtering-token.test.tsx index aa6cd76841..9e7c1c86c4 100644 --- a/src/property-filter/filtering-token/__tests__/filtering-token.test.tsx +++ b/src/property-filter/filtering-token/__tests__/filtering-token.test.tsx @@ -84,6 +84,13 @@ test('renders a single token as role="group" with token ARIA label and dismiss b expect(token.findTokenOperation()!).toBeNull(); }); +test('the dismiss button is a hover-motion trigger and its close icon carries the motion target', () => { + const token = renderToken({ tokens: [token1] }); + const removeButton = token.findRemoveButton()!.getElement(); + expect(removeButton).toHaveAttribute('data-awsui-motion-trigger', 'hover'); + expect(removeButton.querySelector('[data-awsui-motion-target]')).toBeTruthy(); +}); + test('renders 3 tokens as role="group" with group ARIA label no dismiss button', () => { const token = renderToken({ tokens: [token1, token2, token3], groupAriaLabel: 'filter group with 3 tokens' }); expect(token.getElement()).toHaveAttribute('role', 'group'); diff --git a/src/property-filter/filtering-token/index.tsx b/src/property-filter/filtering-token/index.tsx index a631178e3d..5e0cabb6d6 100644 --- a/src/property-filter/filtering-token/index.tsx +++ b/src/property-filter/filtering-token/index.tsx @@ -340,9 +340,10 @@ function TokenDismissButton({ aria-label={ariaLabel} onClick={onClick} disabled={disabled} + data-awsui-motion-trigger="hover" {...getAnalyticsMetadataAttribute({ action: 'dismiss' })} > - + ); } @@ -354,8 +355,9 @@ function TokenEditButton({ ariaLabel, disabled }: { ariaLabel: string; disabled? className={clsx(styles['edit-button'], testUtilStyles['filtering-token-edit-button'])} aria-label={ariaLabel} disabled={disabled} + data-awsui-motion-trigger="hover" > - + ); } diff --git a/src/segmented-control/segment.tsx b/src/segmented-control/segment.tsx index 035204c0fa..2388bdc39b 100644 --- a/src/segmented-control/segment.tsx +++ b/src/segmented-control/segment.tsx @@ -71,6 +71,8 @@ export const Segment = React.forwardRef( onMouseLeave={isDisabledWithReason ? () => setShowTooltip(false) : undefined} {...(isDisabledWithReason ? targetProps : {})} data-testid={id} + data-awsui-motion-trigger="hover" + data-awsui-motion-target="" style={getSegmentedControlSegmentStyles(style)} > {(iconName || iconUrl || iconSvg) && ( diff --git a/src/select/__tests__/trigger.test.tsx b/src/select/__tests__/trigger.test.tsx index 571ae3d19c..e8f557a942 100644 --- a/src/select/__tests__/trigger.test.tsx +++ b/src/select/__tests__/trigger.test.tsx @@ -34,6 +34,12 @@ const defaultProps: any = { }; describe('Trigger component', () => { + test('is a hover-motion trigger and its caret icon carries the motion target', () => { + const wrapper = renderComponent(defaultProps); + expect(wrapper.getElement()).toHaveAttribute('data-awsui-motion-trigger', 'hover'); + expect(wrapper.getElement().querySelector('[data-awsui-motion-target]')).toBeTruthy(); + }); + describe('Empty state', () => { const wrapper = renderComponent(defaultProps); const buttonTriggerEl = wrapper.getElement(); diff --git a/src/side-navigation/__tests__/side-navigation.test.tsx b/src/side-navigation/__tests__/side-navigation.test.tsx index ec35b3a89b..bb2020dcd9 100644 --- a/src/side-navigation/__tests__/side-navigation.test.tsx +++ b/src/side-navigation/__tests__/side-navigation.test.tsx @@ -273,6 +273,46 @@ describe('SideNavigation', () => { expect(createWrapper(wrapper.getElement()).find('[role="img"][aria-label="External link"]')).toBeTruthy(); }); + it('external link is a hover-motion trigger and its icon carries the motion target', () => { + const wrapper = renderSideNavigation({ + items: [{ type: 'link', text: 'Page 1', href: '#something', external: true }], + }); + const externalLink = wrapper.findItemByIndex(1)!.findLink()!.getElement(); + + expect(externalLink).toHaveAttribute('data-awsui-motion-trigger', 'hover'); + expect(externalLink.querySelector('[data-awsui-motion-target]')).toBeTruthy(); + }); + + it('link with an icon slot is a hover-motion trigger and the icon wrapper is the target', () => { + const wrapper = renderSideNavigation({ + items: [{ type: 'link', text: 'Page 1', href: '#something', icon: }], + }); + const link = wrapper.findItemByIndex(1)!.findLink()!.getElement(); + + expect(link).toHaveAttribute('data-awsui-motion-trigger', 'hover'); + // Target sits on the wrapper (not the icon itself) so that slot-provided icons animate too. + const target = link.querySelector('[data-awsui-motion-target]')!; + expect(target.querySelector('[data-testid="custom-icon"]')).toBeTruthy(); + }); + + it('expandable link group header with an icon slot is a hover-motion trigger containing the target', () => { + const wrapper = renderSideNavigation({ + items: [ + { + type: 'expandable-link-group', + text: 'Group', + href: '#group', + icon: , + items: [{ type: 'link', text: 'Child', href: '#child' }], + }, + ], + }); + const header = wrapper.getElement().querySelector('a[href="#group"]')!; + + expect(header).toHaveAttribute('data-awsui-motion-trigger', 'hover'); + expect(header.querySelector('[data-awsui-motion-target]')).toBeTruthy(); + }); + it('has an additional info when "info" property is specified', () => { const wrapper = renderSideNavigation({ items: [{ type: 'link', text: 'Page 1', href: '#something', info: Additional info }], diff --git a/src/side-navigation/parts.tsx b/src/side-navigation/parts.tsx index a8c3934159..02b7c93891 100644 --- a/src/side-navigation/parts.tsx +++ b/src/side-navigation/parts.tsx @@ -413,6 +413,7 @@ const ItemIcon = React.forwardRef(function ItemI collapsed && styles['item-icon--collapsed'], className )} + data-awsui-motion-target="" {...rest} > {icon} @@ -527,6 +528,7 @@ function Link({ definition, activeHref, fireFollow, position, collapsed, activeT rel={definition.external ? 'noopener noreferrer' : undefined} aria-current={definition.href === activeHref ? 'page' : undefined} aria-label={collapsed ? definition.text : undefined} + {...(definition.external || definition.icon ? { 'data-awsui-motion-trigger': 'hover' } : {})} onClick={onClick} {...(collapsed ? collapsedTooltip.triggerProps : {})} {...getAnalyticsMetadataAttribute(clickActionAnalyticsMetadata)} @@ -540,7 +542,11 @@ function Link({ definition, activeHref, fireFollow, position, collapsed, activeT {definition.text} {definition.external && ( - + )} diff --git a/src/table/body-cell/index.tsx b/src/table/body-cell/index.tsx index 23e7da4499..29be28ca7b 100644 --- a/src/table/body-cell/index.tsx +++ b/src/table/body-cell/index.tsx @@ -126,9 +126,10 @@ function TableCellEditable({ ref={editActivateRef} onClick={!isEditing && isExpandableColumn ? onEditStart : undefined} tabIndex={editActivateTabIndex} + data-awsui-motion-trigger="hover" > - + diff --git a/src/table/header-cell/index.tsx b/src/table/header-cell/index.tsx index 62e4c45d12..75c1e9e0fe 100644 --- a/src/table/header-cell/index.tsx +++ b/src/table/header-cell/index.tsx @@ -271,6 +271,7 @@ export function TableHeaderCell({ tabIndex: clickableHeaderTabIndex, role: 'button', onClick: event => handleClick(event.shiftKey), + 'data-awsui-motion-trigger': 'hover', // Prevent the browser from extending the text selection on Shift+click. onMouseDown: (event: React.MouseEvent) => { if (event.shiftKey) { @@ -297,6 +298,7 @@ export function TableHeaderCell({ 'columnDefinitions.editConfig.editIconAriaLabel', column.editConfig?.editIconAriaLabel )} + nativeAttributes={{ 'data-awsui-motion-target': '' }} /> ) : null} @@ -309,7 +311,10 @@ export function TableHeaderCell({ {multiSortIndex} )} - + )} diff --git a/src/tabs/tab-header-bar.tsx b/src/tabs/tab-header-bar.tsx index 014ada060a..c7e2de6d41 100644 --- a/src/tabs/tab-header-bar.tsx +++ b/src/tabs/tab-header-bar.tsx @@ -619,11 +619,16 @@ const TabTrigger = forwardRef( }; return tab.href ? ( - + {children} ) : ( - ); diff --git a/src/token/__tests__/token.test.tsx b/src/token/__tests__/token.test.tsx index 53d6069da3..0808b3b842 100644 --- a/src/token/__tests__/token.test.tsx +++ b/src/token/__tests__/token.test.tsx @@ -108,6 +108,13 @@ describe('Token', () => { expect(onDismiss).toHaveBeenCalledTimes(1); }); + test('is a hover-motion trigger and its close icon carries the motion target', () => { + const wrapper = renderToken({ label: 'Test token', onDismiss: jest.fn() }); + const dismiss = wrapper.findDismiss()!.getElement(); + expect(dismiss).toHaveAttribute('data-awsui-motion-trigger', 'hover'); + expect(dismiss.querySelector('[data-awsui-motion-target]')).toBeTruthy(); + }); + test('shows for inline readonly tokens', () => { const onDismiss = jest.fn(); const wrapper = renderToken({ diff --git a/src/token/dismiss-button.tsx b/src/token/dismiss-button.tsx index 1b42412dba..f751760eb6 100644 --- a/src/token/dismiss-button.tsx +++ b/src/token/dismiss-button.tsx @@ -55,9 +55,14 @@ function DismissButton( fireNonCancelableEvent(onDismiss); }} aria-label={dismissLabel} + data-awsui-motion-trigger="hover" {...(disabled || readOnly ? {} : getAnalyticsMetadataAttribute(analyticsMetadata))} > - + ); } diff --git a/src/top-navigation/parts/overflow-menu/menu-item.tsx b/src/top-navigation/parts/overflow-menu/menu-item.tsx index 3cafabe5b6..2b4fe958eb 100644 --- a/src/top-navigation/parts/overflow-menu/menu-item.tsx +++ b/src/top-navigation/parts/overflow-menu/menu-item.tsx @@ -59,6 +59,7 @@ const LinkItem = forwardRef( href={href} target={anchorTarget} rel={anchorRel} + data-awsui-motion-trigger="hover" {...(testId ? { 'data-testid': testId } : {})} > @@ -83,6 +84,7 @@ const ButtonItem = forwardRef( ref={ref} className={styles['overflow-menu-control']} onClick={onClick} + data-awsui-motion-trigger="hover" {...(typeof testId === 'string' ? { 'data-testid': testId } : {})} > @@ -109,7 +111,7 @@ const NavigationItem = forwardRef( } + endIcon={} testId={testId} onClick={() => navigate('dropdown-menu', { @@ -139,11 +141,12 @@ const ExpandableItem: React.FC< className={clsx(styles['overflow-menu-control'], styles['overflow-menu-control-expandable-menu-trigger'])} onClick={() => setExpanded(value => !value)} aria-expanded={expanded} + data-awsui-motion-trigger="hover" > - + } >