Skip to content

Commit

Permalink
refactor(Tooltip): convert Tooltip and DefinitionTooltip to TypeScript (
Browse files Browse the repository at this point in the history
#13350)

* refactor(Tooltip): convert Tooltip and DefinitionTooltip to TypeScript

* fix(DefinitionTooltip): adjust ommitted attributes list

---------

Co-authored-by: Alessandra Davila <adavila91@gmail.com>
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
  • Loading branch information
3 people committed Mar 16, 2023
1 parent 2f64be3 commit 4df1ef9
Show file tree
Hide file tree
Showing 6 changed files with 167 additions and 29 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,68 @@
import cx from 'classnames';
import PropTypes from 'prop-types';
import React, { useState } from 'react';
import { Popover, PopoverContent } from '../Popover';
import { Popover, PopoverAlignment, PopoverContent } from '../Popover';
import { match, keys } from '../../internal/keyboard';
import { useFallbackId } from '../../internal/useId';
import { usePrefix } from '../../internal/usePrefix';
import deprecate from '../../prop-types/deprecate';

function DefinitionTooltip({
export interface DefinitionTooltipProps
extends Omit<
React.ButtonHTMLAttributes<HTMLButtonElement>,
'id' | 'classname' | 'children' | 'type'
> {
/**
* Specify how the trigger should align with the tooltip
*/
align?: PopoverAlignment;

/**
* The `children` prop will be used as the value that is being defined
*/
children?: React.ReactNode;

/**
* Specify an optional className to be applied to the container node
*/
className?: string;

/**
* Specify whether the tooltip should be open when it first renders
*/
defaultOpen?: boolean;

/**
* The `definition` prop is used as the content inside of the tooltip that
* appears when a user interacts with the element rendered by the `children`
* prop
*/
definition: React.ReactNode;

/**
* Provide a value that will be assigned as the id of the tooltip
*/
id?: string;

/**
* Specifies whether or not the `DefinitionTooltip` should open on hover or not
*/
openOnHover?: boolean;

/**
* @deprecated [Deprecated in v11] Please use the `definition` prop instead.
*
* Provide the text that will be displayed in the tooltip when it is rendered.
*/
tooltipText?: React.ReactNode;

/**
* The CSS class name of the trigger element
*/
triggerClassName?: string;
}

const DefinitionTooltip: React.FC<DefinitionTooltipProps> = ({
align = 'bottom-left',
className,
children,
Expand All @@ -25,12 +80,12 @@ function DefinitionTooltip({
tooltipText,
triggerClassName,
...rest
}) {
}) => {
const [isOpen, setOpen] = useState(defaultOpen);
const prefix = usePrefix();
const tooltipId = useFallbackId(id);

function onKeyDown(event) {
function onKeyDown(event: React.KeyboardEvent) {
if (isOpen && match(event, keys.Escape)) {
event.stopPropagation();
setOpen(false);
Expand Down Expand Up @@ -72,7 +127,7 @@ function DefinitionTooltip({
</PopoverContent>
</Popover>
);
}
};

DefinitionTooltip.propTypes = {
/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import cx from 'classnames';
import PropTypes from 'prop-types';
import React, { useRef, useEffect } from 'react';
import { Popover, PopoverContent } from '../Popover';
import { Popover, PopoverAlignment, PopoverContent } from '../Popover';
import { keys, match } from '../../internal/keyboard';
import { useDelayedState } from '../../internal/useDelayedState';
import { useId } from '../../internal/useId';
Expand All @@ -17,8 +17,74 @@ import {
getInteractiveContent,
} from '../../internal/useNoInteractiveChildren';
import { usePrefix } from '../../internal/usePrefix';
import { PolymorphicProps } from '../../types/common';

function Tooltip({
interface TooltipBaseProps {
/**
* Specify how the trigger should align with the tooltip
*/
align?: PopoverAlignment;

/**
* Pass in the child to which the tooltip will be applied
*/
children?: React.ReactElement;

/**
* Specify an optional className to be applied to the container node
*/
className?: string;

/**
* Determines wether the tooltip should close when inner content is activated (click, Enter or Space)
*/
closeOnActivation?: boolean;

/**
* Specify whether the tooltip should be open when it first renders
*/
defaultOpen?: boolean;

/**
* Provide the description to be rendered inside of the Tooltip. The
* description will use `aria-describedby` and will describe the child node
* in addition to the text rendered inside of the child. This means that if you
* have text in the child node, that it will be announced alongside the
* description to the screen reader.
*
* Note: if label and description are both provided, label will be used and
* description will not be used
*/
description?: React.ReactNode;

/**
* Specify the duration in milliseconds to delay before displaying the tooltip
*/
enterDelayMs?: number;

/**
* Provide the label to be rendered inside of the Tooltip. The label will use
* `aria-labelledby` and will fully describe the child node that is provided.
* This means that if you have text in the child node, that it will not be
* announced to the screen reader.
*
* Note: if label and description are both provided, description will not be
* used
*/
label?: React.ReactNode;

/**
* Specify the duration in milliseconds to delay before hiding the tooltip
*/
leaveDelayMs?: number;
}

export type TooltipProps<T extends React.ElementType> = PolymorphicProps<
T,
TooltipBaseProps
>;

function Tooltip<T extends React.ElementType>({
align = 'top',
className: customClassName,
children,
Expand All @@ -29,9 +95,9 @@ function Tooltip({
defaultOpen = false,
closeOnActivation = false,
...rest
}) {
const containerRef = useRef(null);
const tooltipRef = useRef(null);
}: TooltipProps<T>) {
const containerRef = useRef<Element>(null);
const tooltipRef = useRef<HTMLSpanElement>(null);
const [open, setOpen] = useDelayedState(defaultOpen);
const id = useId('tooltip');
const prefix = usePrefix();
Expand All @@ -45,7 +111,7 @@ function Tooltip({
onMouseEnter,
};

function getChildEventHandlers(childProps) {
function getChildEventHandlers(childProps: any) {
const eventHandlerFunctions = [
'onFocus',
'onBlur',
Expand All @@ -54,7 +120,7 @@ function Tooltip({
];
const eventHandlers = {};
eventHandlerFunctions.forEach((functionName) => {
eventHandlers[functionName] = (evt) => {
eventHandlers[functionName] = (evt: React.SyntheticEvent) => {
triggerProps[functionName]();
if (childProps?.[functionName]) {
childProps?.[functionName](evt);
Expand All @@ -70,7 +136,7 @@ function Tooltip({
triggerProps['aria-describedby'] = id;
}

function onKeyDown(event) {
function onKeyDown(event: React.KeyboardEvent) {
if (open && match(event, keys.Escape)) {
event.stopPropagation();
setOpen(false);
Expand Down Expand Up @@ -99,9 +165,13 @@ function Tooltip({
);

useEffect(() => {
const interactiveContent = getInteractiveContent(containerRef.current);
if (!interactiveContent) {
setOpen(false);
if (containerRef !== null && containerRef.current) {
const interactiveContent = getInteractiveContent(
containerRef.current as HTMLElement
);
if (!interactiveContent) {
setOpen(false);
}
}
});

Expand All @@ -116,10 +186,12 @@ function Tooltip({
onMouseLeave={onMouseLeave}
open={open}
ref={containerRef}>
{React.cloneElement(child, {
...triggerProps,
...getChildEventHandlers(child.props),
})}
{child !== undefined
? React.cloneElement(child, {
...triggerProps,
...getChildEventHandlers(child.props),
})
: null}
<PopoverContent
aria-hidden="true"
className={`${prefix}--tooltip-content`}
Expand Down
2 changes: 1 addition & 1 deletion packages/react/src/internal/keyboard/match.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ export function matches(event, keysToMatch) {
* }
* }
*
* @param {Event|number|string} eventOrCode
* @param {React.KeyboardEvent|Event|number|string} eventOrCode
* @param {Key} key
* @returns {boolean}
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,13 @@
* LICENSE file in the root directory of this source tree.
*/

import { useCallback, useEffect, useRef, useState } from 'react';
import {
SetStateAction,
useCallback,
useEffect,
useRef,
useState,
} from 'react';

/**
* `useDelayedState` mirrors `useState` but also allows you to add a delay to
Expand All @@ -17,29 +23,34 @@ import { useCallback, useEffect, useRef, useState } from 'react';
* pending timers when a `setState` is called before the state is updated from
* a previous call
*/
export function useDelayedState(initialState) {
const [state, setState] = useState(initialState);
const timeoutId = useRef(null);

export type DispatchWithDelay<A> = (value: A, delayMS?: number) => void;

export function useDelayedState<S>(
initialState: S
): [S, DispatchWithDelay<SetStateAction<S>>] {
const [state, setState] = useState<S>(initialState);
const timeoutId = useRef<number | null>(null);
// We use `useCallback` to match the signature of React's `useState` which will
// always return the same reference for the `setState` updater
const setStateWithDelay = useCallback((stateToSet, delayMs = 0) => {
clearTimeout(timeoutId.current);
window.clearTimeout(timeoutId.current ?? undefined);
timeoutId.current = null;

if (delayMs === 0) {
setState(stateToSet);
return;
}

timeoutId.current = setTimeout(() => {
timeoutId.current = window.setTimeout(() => {
setState(stateToSet);
timeoutId.current = null;
}, delayMs);
}, []);

useEffect(() => {
return () => {
clearTimeout(timeoutId.current);
window.clearTimeout(timeoutId.current ?? undefined);
};
}, []);

Expand Down
2 changes: 1 addition & 1 deletion packages/react/src/internal/useId.js
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ export function useId(prefix = 'id') {

/**
* Generate a unique id if a given `id` is not provided
* @param {string} id
* @param {string|undefined} id
* @returns {string}
*/
export function useFallbackId(id) {
Expand Down

0 comments on commit 4df1ef9

Please sign in to comment.