import { ComponentProps, ReactElement, useId, useRef, useState } from 'react'; import classnames from 'classnames'; import Box from './Box'; import { useColorScheme } from './contexts/ColorSchemeProvider'; import { useDefaultLabelContext } from './contexts/DefaultLabelProvider'; import Flex from './Flex'; import focusStyles from './Focus.css'; import styles from './HelpButton.css'; import Icon from './Icon'; import { ESCAPE, TAB } from './keyCodes'; import Layer from './Layer'; import Link from './Link'; import InternalPopover from './Popover/InternalPopover'; import MaybeTooltip from './sharedSubcomponents/MaybeTooltip'; import TapArea from './TapArea'; import Text from './Text'; import useFocusVisible from './useFocusVisible'; import useExperimentalTheme from './utils/useExperimentalTheme'; import { CompositeZIndex, FixedZIndex, Indexable } from './zIndex'; type LinkType = { accessibilityLabel?: ComponentProps['accessibilityLabel']; externalLinkIcon?: | 'none' | 'default' | { color: ComponentProps['color']; size: ComponentProps['size']; }; href: ComponentProps['href']; onClick?: ComponentProps['onClick']; ref?: ComponentProps['ref']; text: string; target?: ComponentProps['target']; }; type Props = { /** * Supply a short, descriptive label screen readers. Follow the pattern `Click to learn more about [description]`. See [Accessibility](https://gestalt.pinterest.systems/web/helpbutton#Accessibility) section for guidance. */ accessibilityLabel: string; /** * Supply a short, descriptive label for screen-readers to describe the popover content. Used for [accessibility](https://gestalt.pinterest.systems/web/popover#ARIA-attributes) purposes. */ accessibilityPopoverLabel: string; /** * Specifies the preferred position of the tooltip and popover relative to HelpButton. See [Popover's ideal direction variant](https://gestalt.pinterest.systems/web/popover#Ideal-direction) to learn more. */ idealDirection?: 'up' | 'right' | 'down' | 'left'; /** * Enables correct behavior when HelpButton is used within a fixed container. To achieve this it removes the Layer component around Popover and enables positioning relative to its anchor element. Should only be used in cases where Layer breaks the HelpButton positionings such as when the anchor element is within a sticky component. */ isWithinFixedContainer?: boolean; /** * If provided, displays a [link api](https://gestalt.pinterest.systems/web/link#Props) at the bottom of the popover message. * - `href` is the URL that the hyperlink points to. * - `text` is the displayed text for the link. See the [link variant](https://gestalt.pinterest.systems/web/helpbutton#With-a-link) for more details. * - `target` see the [target Link variant](https://gestalt.pinterest.systems/web/link#Target) to learn more. If not defined the link will open in a new window. * - Optionally use `accessibilityLabel` to supply a short, descriptive label for screen-readers to replace link texts that don't provide sufficient context about the link component behavior. Texts like "Click Here", or "Read More" can be confusing when a screen reader reads them out of context. In those cases, we must pass an alternative text to replace the link text. It populates `aria-label`. Screen readers read the `accessibilityLabel` prop, if present, instead of the link text. See [ Link's accessibility guidelines](https://gestalt.pinterest.systems/web/link#Accessibility) for more information. * - Optionally provide an `onClick` callback, which is fired when the link is clicked (pressed and released) with a mouse or keyboard. See [GlobalEventsHandlerProvider](https://gestalt.pinterest.systems/web/utilities/globaleventshandlerprovider#Link-handlers) to learn more about link navigation. */ link?: LinkType; /** * Callback fired when HelpIcon is clicked (pressed and released) with a mouse or keyboard. */ onClick?: (arg1: { event: React.MouseEvent | React.KeyboardEvent; }) => void; /** * Informational content that's displayed when the user clicks on HelpButton. */ text: string | ReactElement; /** * Specifies the z-index for HelpButton's tooltip and popover to resolve any layering issues with other elements. See the [zIndex variant](https://gestalt.pinterest.systems/web/helpbutton#With-Z-index) for more details. */ zIndex?: Indexable; }; /** * [HelpButton](https://gestalt.pinterest.systems/web/helpbutton) is an affordance that accompanies an element on the screen. It helps describe or provide assistance on how to use the accompanying element. */ export default function HelpButton({ accessibilityLabel, accessibilityPopoverLabel, idealDirection = 'down', isWithinFixedContainer = false, link, onClick, text, zIndex, }: Props) { const tapAreaRef = useRef(null); const textRef = useRef(null); const [open, setOpen] = useState(false); const [hovered, setHovered] = useState(false); const [focused, setFocused] = useState(false); const { isFocusVisible } = useFocusVisible(); // Define where the focused content stays const [innerModalFocus, setInnerModalFocus] = useState(false); const theme = useExperimentalTheme(); const { colorSchemeName } = useColorScheme(); const popoverId = useId(); const { tooltipMessage } = useDefaultLabelContext('HelpButton'); const handlePopoverKeyDown: ComponentProps['onKeyDown'] = ({ event }) => { // Avoid others KeyDown events to listen this call if (innerModalFocus) event.stopPropagation(); // # Manual trap focus // If `TAB is clicked` when the focus is on `lastElement` of popover // the focus back to `textRef` element. const elementsInnerPopover = document.querySelectorAll( [ `div[id="helpButtonText-${popoverId}"]`, // Text query `a[href="${link?.href ?? ''}"]`, // Link query ].join(' ,'), ); const lastElement = elementsInnerPopover[elementsInnerPopover.length - 1]; if (innerModalFocus && event.keyCode === TAB && lastElement === event.target) { event.preventDefault(); textRef.current?.focus(); } // If the popover was closed pressing `ESCAPE`, the focus back to TapArea element if (event.keyCode === ESCAPE && open) { setOpen(false); tapAreaRef.current?.focus(); } }; const handleTapAreaKeyDown: ComponentProps['onKeyDown'] = ({ event }) => { if (event.keyCode === TAB && open) { event.preventDefault(); textRef.current?.focus(); setInnerModalFocus(true); } // Avoid others KeyDown events to listen this call event.stopPropagation(); }; const toggleView = () => setOpen((currVal) => !currVal); let bgIconColor: ComponentProps['color'] = theme.MAIN ? 'default' : 'tertiary'; let iconColor: ComponentProps['color'] = theme.MAIN ? 'subtle' : 'inverse'; if (!theme.MAIN && (open || hovered || focused)) { bgIconColor = 'selected'; } if (theme.MAIN && (open || hovered || focused)) { iconColor = 'default'; } // Overriding color of `Text` components const isDarkMode = colorSchemeName === 'darkMode'; const textColorOverrideStyles = isDarkMode ? styles.textColorOverrideLight : styles.textColorOverrideDark; const tooltipZIndex = zIndex ?? new FixedZIndex(1); const zIndexWrapper = new CompositeZIndex([tooltipZIndex]); const popoverElement = ( {/* * `id` - used to tracking children by line 130 * `tabIndex={0}` - It's used to make the text element as a focusable element */}
{typeof text === 'string' ? ( {text} ) : ( {text} )}
{link && link?.href && ( setFocused(false)} onClick={link.onClick} onFocus={() => setFocused(true)} target={link.target ?? 'blank'} underline="always" > {link.text} )}
); return ( // The only purpose of this Flex is to make zIndex work (Tooltip over Popover). setFocused(false)} onFocus={() => setFocused(true)} onKeyDown={handleTapAreaKeyDown} onMouseEnter={() => setHovered(true)} onMouseLeave={() => setHovered(false)} onTap={({ event }) => { toggleView(); onClick?.({ event }); }} role="button" rounding="circle" > {open && (isWithinFixedContainer ? ( // This Box is handling the zIndex work (Tooltip over Popover) {popoverElement} ) : ( {popoverElement} ))} ); } HelpButton.displayName = 'HelpButton';