import * as React from 'react'; import innerText from 'react-innertext'; import {createComponent, mergeCallback, useLocalRef} from '@workday/canvas-kit-react/common'; import { Placement, Popper, defaultFallbackPlacements, getTransformFromPlacement, } from '@workday/canvas-kit-react/popup'; import {CSProps, handleCsProp} from '@workday/canvas-kit-styling'; import {TooltipContainer} from './TooltipContainer'; import {useTooltip} from './useTooltip'; export interface TooltipProps extends Omit, 'title'>, CSProps { /** * This should be a string in most cases. HTML is supported, but only text is understood * by assistive technology. This is true for both `label` and `describe` modes. * * **Note:** If you use `description` type and want to pass `jsx`, it **must* be inline and **not** a component to ensure the inner text is properly translated. * * ```jsx * // The text will be understood as: You must accept terms and conditions * Youmust accept terms and conditions}/> * * // This will render a string including the html and will not be properly understood by voice over. * const MyComponent = () => Youmust accept terms and conditions * * ``` */ title: React.ReactNode; /** * The target (anchor element) for the Tooltip. * * **Note:** This **must** be a single Element, StyledComponent, or any other component that * forwards extra props to an Element. Tooltip works running `React.cloneElement` on the children * and adds extra properties like aria attributes and event handlers. This is currently a * limitation of the Tooltip component. Functionality will not work if this condition isn't met. */ children: React.ReactElement; /** * Sets the placement preference used by PopperJS. * @default 'top' */ placement?: Placement; /** * Define fallback placements by providing a list of {@link Placement} in array (in order of preference). * The default preference is following the order of `top`, `right`, `bottom`, and `left`. Once the initial * and opposite placements are not available, the fallback placements will be in use. Use an empty array to * disable the fallback placements. */ fallbackPlacements?: Placement[]; /** * Determines the tooltip type for accessibility. * * - `label`: Sets the accessible name for the wrapped element. Use for icons or if tooltip * `title` prop is the same as the text content of the wrapped element. E.g. TertiaryButtons that render an icon or * Ellipsis tooltips. * - **Deprecated: `describe` is deprecated, please use `description`**.`describe`: Sets `aria-describedby` of the wrapped element. Use if the tooltip has additional * information about the target. * - `muted`: No effort is made to make the tooltip accessible to screen readers. Use if the * tooltip contents are not useful to a screen reader or if you have handled accessibility of * the tooltip yourself. * * **Note**: Assistive technology may ignore `describe` techniques based on verbosity settings. * Consider an alternate way to inform a user of additional important information. * @default 'label' */ type?: 'label' | 'describe' | 'muted' | 'description'; /** * Amount of time (in ms) to delay before showing the tooltip */ showDelay?: number; /** * Amount of time (in ms) to delay before hiding the tooltip */ hideDelay?: number; /** * The `alt` variant is designed for use on alternative page backgrounds (`system.color.bg.alt.default`). Use this variant to maintain proper visual hierarchy when placing components on colored backgrounds. While the default variant should be used on `system.color.bg.default` backgrounds, the `alt` variant ensures the component remains visually elevated on `system.color.bg.alt.default` backgrounds. */ variant?: 'alt'; } function mergeCallbacks( elemProps: {[key: string]: any}, componentProps: T, keys: (keyof T)[] = Object.keys(componentProps) ) { return (keys as string[]).reduce( (mergedProps, key) => { if (typeof elemProps[key] === 'function') { mergedProps[key] = mergeCallback(componentProps[key], elemProps[key]); } else { mergedProps[key] = componentProps[key]; } return mergedProps; }, {} as {[key: string]: any} ); } export const Tooltip = createComponent('div')({ displayName: 'Tooltip', Component({ type = 'label', placement = 'top', title, children, showDelay = 300, hideDelay = 100, fallbackPlacements = defaultFallbackPlacements, variant, ...elemProps }: TooltipProps) { const titleText = innerText(title); const {targetProps, popperProps, tooltipProps} = useTooltip({ type, titleText, showDelay, hideDelay, }); const [elementHasFocus, setElementHasFocus] = React.useState(false); const {localRef: targetRef} = useLocalRef(); React.useEffect(() => { const target = targetRef.current; if (!target) { return; } const updateFocusState = () => { setElementHasFocus(target.matches(':focus-visible') || target.classList.contains('focus')); }; updateFocusState(); // Check initial state target.addEventListener('focus', updateFocusState); target.addEventListener('blur', updateFocusState); return () => { target.removeEventListener('focus', updateFocusState); target.removeEventListener('blur', updateFocusState); }; }, [targetRef]); return ( {React.cloneElement(children, { ...targetProps, ...mergeCallbacks(children.props, targetProps), ...(/^(muted|describe)$/.test(type) && children.props['aria-label'] ? {'aria-label': children.props['aria-label']} : {}), })} {({placement}) => { const transformOrigin = getTransformFromPlacement(placement); return ( {title} ); }} ); }, });