From cc4db96211e7115951499aada8809809c8b24e21 Mon Sep 17 00:00:00 2001 From: Yueying Lu <98534165+YueyingLu@users.noreply.github.com> Date: Tue, 29 Nov 2022 15:43:41 +0100 Subject: [PATCH] feat: Show popover on truncated breadcrumb text (#527) --- pages/breadcrumb-group/events.page.tsx | 48 ++++-- .../__integ__/breadcrumb-group.test.ts | 34 ++++ src/breadcrumb-group/interfaces.ts | 1 + src/breadcrumb-group/internal.tsx | 1 + src/breadcrumb-group/item/item.tsx | 145 +++++++++++++++--- src/breadcrumb-group/item/styles.scss | 8 +- 6 files changed, 206 insertions(+), 31 deletions(-) diff --git a/pages/breadcrumb-group/events.page.tsx b/pages/breadcrumb-group/events.page.tsx index 8a9a3481c6..e6973a9645 100644 --- a/pages/breadcrumb-group/events.page.tsx +++ b/pages/breadcrumb-group/events.page.tsx @@ -1,10 +1,20 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 import React, { useState } from 'react'; +import { SpaceBetween } from '~components'; import BreadcrumbGroup, { BreadcrumbGroupProps } from '~components/breadcrumb-group'; import ScreenshotArea from '../utils/screenshot-area'; -const items = ['First', 'Second', 'Third', 'Fourth', 'Fifth', 'Sixth']; +const items = [ + 'First that is very very very very very very long long long text', + 'Second', + 'Third', + 'Fourth', + 'Fifth', + 'Sixth that is very very very very very very long long long text', +]; + +const shortItems = ['1', '2', '3', '4']; export default function ButtonDropdownPage() { const [onFollowMessage, setOnFollowMessage] = useState(''); @@ -20,16 +30,32 @@ export default function ButtonDropdownPage() {

BreadcrumbGroup variations

- ({ text, href: `#` }))} - onFollow={onFollowCallback} - onClick={onClickCallback} - /> -
-
{onFollowMessage}
-
{onClickMessage}
+ +
+ + ({ text, href: `#` }))} + onFollow={onFollowCallback} + onClick={onClickCallback} + /> +
{onFollowMessage}
+
{onClickMessage}
+
+
+ + ({ text, href: `#` }))} + /> +
+
); diff --git a/src/breadcrumb-group/__integ__/breadcrumb-group.test.ts b/src/breadcrumb-group/__integ__/breadcrumb-group.test.ts index 437892b994..d3fe9ce8bc 100644 --- a/src/breadcrumb-group/__integ__/breadcrumb-group.test.ts +++ b/src/breadcrumb-group/__integ__/breadcrumb-group.test.ts @@ -3,6 +3,7 @@ import useBrowser from '@cloudscape-design/browser-test-tools/use-browser'; import createWrapper from '../../../lib/components/test-utils/selectors'; import { BasePageObject } from '@cloudscape-design/browser-test-tools/page-objects'; +import styles from '../../../lib/components/breadcrumb-group/item/styles.selectors.js'; const breadcrumbGroupWrapper = createWrapper().findBreadcrumbGroup(); const dropdownWrapper = breadcrumbGroupWrapper.findDropdown(); @@ -73,4 +74,37 @@ describe('BreadcrumbGroup', () => { await expect(page.getText('#onClickMessage')).resolves.toEqual('OnClick: Second item was selected'); }) ); + + test( + 'Item popover should not show on large screen', + setupTest(async page => { + await page.setWindowSize({ width: 1200, height: 800 }); + await page.click('#focus-target-long-text'); + await page.keys('Tab'); + await expect(page.isExisting(createWrapper().find(`.${styles['item-popover']}`).toSelector())).resolves.toBe( + false + ); + }) + ); + + test( + 'Item popover should show on small screen when text get truncated, and should close pressing Escape', + setupTest(async page => { + await page.setMobileViewport(); + await page.click('#focus-target-long-text'); + await page.keys('Tab'); + await expect(page.isExisting(createWrapper().find(`.${styles['item-popover']}`).toSelector())).resolves.toBe( + true + ); + await page.keys('Escape'); + await expect(page.isExisting(createWrapper().find(`.${styles['item-popover']}`).toSelector())).resolves.toBe( + false + ); + await page.click('#focus-target-short-text'); + await page.keys('Tab'); + await expect(page.isExisting(createWrapper().find(`.${styles['item-popover']}`).toSelector())).resolves.toBe( + false + ); + }) + ); }); diff --git a/src/breadcrumb-group/interfaces.ts b/src/breadcrumb-group/interfaces.ts index f87dfe59cc..b3c83bfbef 100644 --- a/src/breadcrumb-group/interfaces.ts +++ b/src/breadcrumb-group/interfaces.ts @@ -55,6 +55,7 @@ export namespace BreadcrumbGroupProps { export interface BreadcrumbItemProps { item: T; + isDisplayed: boolean; isLast?: boolean; isCompressed?: boolean; onClick?: CancelableEventHandler>; diff --git a/src/breadcrumb-group/internal.tsx b/src/breadcrumb-group/internal.tsx index 826000a376..850d6c17c4 100644 --- a/src/breadcrumb-group/internal.tsx +++ b/src/breadcrumb-group/internal.tsx @@ -101,6 +101,7 @@ export default function InternalBreadcrumbGroup ); diff --git a/src/breadcrumb-group/item/item.tsx b/src/breadcrumb-group/item/item.tsx index 64713b390c..094ccbe592 100644 --- a/src/breadcrumb-group/item/item.tsx +++ b/src/breadcrumb-group/item/item.tsx @@ -1,6 +1,6 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import React from 'react'; +import React, { useEffect, useRef, useState } from 'react'; import { BreadcrumbGroupProps, BreadcrumbItemProps } from '../interfaces'; import InternalIcon from '../../icon/internal'; import styles from './styles.css.js'; @@ -8,11 +8,110 @@ import clsx from 'clsx'; import useFocusVisible from '../../internal/hooks/focus-visible'; import { fireCancelableEvent, isPlainLeftClick } from '../../internal/events'; import { getEventDetail } from '../internal'; +import { Transition } from '../../internal/components/transition'; +import PopoverContainer from '../../popover/container'; +import PopoverBody from '../../popover/body'; +import Portal from '../../internal/components/portal'; +import popoverStyles from '../../popover/styles.css.js'; + +type BreadcrumbItemWithPopoverProps = + React.AnchorHTMLAttributes & { + item: T; + }; + +const BreadcrumbItemWithPopover = ({ + item, + ...anchorAttributes +}: BreadcrumbItemWithPopoverProps) => { + const focusVisible = useFocusVisible(); + const [showPopover, setShowPopover] = useState(false); + const textRef = useRef(null); + const virtualTextRef = useRef(null); + + const isTruncated = (textRef: React.RefObject, virtualTextRef: React.RefObject) => { + if (!textRef || !virtualTextRef || !textRef.current || !virtualTextRef.current) { + return false; + } + const virtualTextWidth = virtualTextRef.current.getBoundingClientRect().width; + const textWidth = textRef.current.getBoundingClientRect().width; + if (virtualTextWidth > textWidth) { + return true; + } + return false; + }; + + const popoverContent = ( + +
+ + {() => ( + ( +
+
+
+
+ )} + > + {}} header={undefined}> + {item.text} + + + )} + +
+ + ); + + useEffect(() => { + const onKeyDown = (event: KeyboardEvent) => { + if (event.key === 'Escape') { + setShowPopover(false); + } + }; + if (showPopover) { + document.addEventListener('keydown', onKeyDown); + } + return () => { + document.removeEventListener('keydown', onKeyDown); + }; + }, [showPopover]); + + return ( + <> + { + isTruncated(textRef, virtualTextRef) && setShowPopover(true); + }} + onBlur={() => setShowPopover(false)} + onMouseEnter={() => { + isTruncated(textRef, virtualTextRef) && setShowPopover(true); + }} + onMouseLeave={() => setShowPopover(false)} + > + + {item.text} + + + {item.text} + + + {showPopover && popoverContent} + + ); +}; export function BreadcrumbItem({ item, onClick, onFollow, + isDisplayed, isLast = false, isCompressed = false, }: BreadcrumbItemProps) { @@ -24,24 +123,32 @@ export function BreadcrumbItem({ } fireCancelableEvent(onClick, getEventDetail(item), event); }; + + const anchorAttributes: React.AnchorHTMLAttributes = { + href: isLast ? undefined : item.href || '#', + className: clsx(styles.anchor, { [styles.compressed]: isCompressed }), + 'aria-current': isLast ? 'page' : undefined, // Active breadcrumb item is implemented according to WAI-ARIA 1.1 + 'aria-disabled': isLast && 'true', + onClick: isLast ? preventDefault : onClickHandler, + tabIndex: isLast ? 0 : undefined, // tabIndex is added to the last crumb to keep it in the index without an href + }; + return ( -
- - {item.text} - - {!isLast ? ( - - - - ) : null} -
+ <> +
+ {isDisplayed && isCompressed ? ( + + ) : ( + + {item.text} + + )} + {!isLast ? ( + + + + ) : null} +
+ ); } diff --git a/src/breadcrumb-group/item/styles.scss b/src/breadcrumb-group/item/styles.scss index 61b9373631..5561acdce6 100644 --- a/src/breadcrumb-group/item/styles.scss +++ b/src/breadcrumb-group/item/styles.scss @@ -36,7 +36,6 @@ font-weight: styles.$font-weight-bold; text-decoration: none; cursor: default; - pointer-events: none; } } } @@ -50,3 +49,10 @@ display: block; } } +.virtual-item { + @include styles.awsui-util-hide; + visibility: hidden; +} +.item-popover { + /* used in tests */ +}