Skip to content

Commit

Permalink
Smarter item tooltips (deephaven#1909)
Browse files Browse the repository at this point in the history
  • Loading branch information
bmingles committed Apr 2, 2024
1 parent 57283d6 commit e711300
Show file tree
Hide file tree
Showing 5 changed files with 128 additions and 53 deletions.
76 changes: 68 additions & 8 deletions packages/components/src/spectrum/ItemContent.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,63 @@
import { Children, cloneElement, isValidElement, ReactNode } from 'react';
import { Text } from '@adobe/react-spectrum';
import {
Children,
cloneElement,
isValidElement,
ReactNode,
useCallback,
useState,
} from 'react';
import { DOMRefValue } from '@react-types/shared';
import cl from 'classnames';
import { isElementOfType } from '@deephaven/react-hooks';
import { Text } from './Text';
import stylesCommon from '../SpectrumComponent.module.scss';
import { TooltipOptions } from './utils';
import ItemTooltip from './ItemTooltip';

export interface ItemContentProps {
children: ReactNode;
tooltipOptions?: TooltipOptions | null;
}

/**
* Picker item content. Text content will be wrapped in a Spectrum Text
* component with ellipsis overflow handling.
* component with ellipsis overflow handling. If text content overflow and
* tooltipOptions are provided a tooltip will be displayed when hovering over
* the item content.
*/
export function ItemContent({
children: content,
tooltipOptions,
}: ItemContentProps): JSX.Element | null {
const [previousContent, setPreviousContent] = useState(content);
const [isOverflowing, setIsOverflowing] = useState(false);

// Reset `isOverflowing` if content changes. It will get re-calculated as
// `Text` components render.
if (previousContent !== content) {
setPreviousContent(content);
setIsOverflowing(false);
}

/**
* Whenever a `Text` component renders, see if the content is overflowing so
* we can render a tooltip.
*/
const checkOverflow = useCallback(
(ref: DOMRefValue<HTMLSpanElement> | null) => {
const el = ref?.UNSAFE_getDOMNode();

if (el == null) {
return;
}

if (el.scrollWidth > el.offsetWidth) {
setIsOverflowing(true);
}
},
[]
);

if (isValidElement(content)) {
return content;
}
Expand All @@ -39,6 +82,7 @@ export function ItemContent({
isElementOfType(el, Text)
? cloneElement(el, {
...el.props,
ref: checkOverflow,
UNSAFE_className: cl(
el.props.UNSAFE_className,
stylesCommon.spectrumEllipsis
Expand All @@ -47,13 +91,29 @@ export function ItemContent({
: el
);
}

if (typeof content === 'string' || typeof content === 'number') {
content = (
<Text
ref={checkOverflow}
UNSAFE_className={stylesCommon.spectrumEllipsis}
>
{content}
</Text>
);
}
/* eslint-enable no-param-reassign */

return typeof content === 'string' || typeof content === 'number' ? (
<Text UNSAFE_className={stylesCommon.spectrumEllipsis}>{content}</Text>
) : (
// eslint-disable-next-line react/jsx-no-useless-fragment
<>{content}</>
const tooltip =
tooltipOptions == null || !isOverflowing ? null : (
<ItemTooltip options={tooltipOptions}>{content}</ItemTooltip>
);

return (
<>
{content}
{tooltip}
</>
);
}

Expand Down
34 changes: 34 additions & 0 deletions packages/components/src/spectrum/ItemTooltip.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { ReactNode } from 'react';
import { isElementOfType } from '@deephaven/react-hooks';
import { TooltipOptions } from './utils';
import { Tooltip } from '../popper';
import { Flex } from './layout';
import { Text } from './Text';

export interface ItemTooltipProps {
children: ReactNode;
options: TooltipOptions;
}

export function ItemTooltip({
children,
options,
}: ItemTooltipProps): JSX.Element {
if (typeof children === 'boolean') {
return <Tooltip options={options}>{children}</Tooltip>;
}

if (Array.isArray(children)) {
return (
<Tooltip options={options}>
<Flex direction="column" alignItems="start">
{children.filter(node => isElementOfType(node, Text))}
</Flex>
</Tooltip>
);
}

return <Tooltip options={options}>{children}</Tooltip>;
}

export default ItemTooltip;
34 changes: 22 additions & 12 deletions packages/components/src/spectrum/Text.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
/* eslint-disable react/jsx-props-no-spreading */
import { useMemo } from 'react';
import { forwardRef, useMemo } from 'react';
import {
Text as SpectrumText,
type TextProps as SpectrumTextProps,
} from '@adobe/react-spectrum';
import type { DOMRef, DOMRefValue } from '@react-types/shared';
import { type ColorValue, colorValueStyle } from '../theme/colorUtils';

export type TextProps = SpectrumTextProps & {
Expand All @@ -19,18 +20,27 @@ export type TextProps = SpectrumTextProps & {
* @returns The Text component
*
*/
export const Text = forwardRef<DOMRefValue<HTMLSpanElement>, SpectrumTextProps>(
(props: TextProps, ref): JSX.Element => {
const { color, UNSAFE_style, ...rest } = props;
const style = useMemo(
() => ({
...UNSAFE_style,
color: colorValueStyle(color),
}),
[color, UNSAFE_style]
);

export function Text(props: TextProps): JSX.Element {
const { color, UNSAFE_style, ...rest } = props;
const style = useMemo(
() => ({
...UNSAFE_style,
color: colorValueStyle(color),
}),
[color, UNSAFE_style]
);
return (
<SpectrumText
{...rest}
ref={ref as unknown as DOMRef<HTMLDivElement>}
UNSAFE_style={style}
/>
);
}
);

return <SpectrumText {...rest} UNSAFE_style={style} />;
}
Text.displayName = 'Text';

export default Text;
1 change: 1 addition & 0 deletions packages/components/src/spectrum/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,5 @@ export * from './View';
* Custom DH spectrum utils
*/
export * from './ItemContent';
export * from './ItemTooltip';
export * from './utils';
36 changes: 3 additions & 33 deletions packages/components/src/spectrum/picker/Picker.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import { Key, ReactNode, useCallback, useMemo } from 'react';
import { Key, useCallback, useMemo } from 'react';
import { DOMRef } from '@react-types/shared';
import { Flex, Picker as SpectrumPicker } from '@adobe/react-spectrum';
import { Picker as SpectrumPicker } from '@adobe/react-spectrum';
import {
getPositionOfSelectedItem,
findSpectrumPickerScrollArea,
isElementOfType,
usePopoverOnScrollRef,
} from '@deephaven/react-hooks';
import {
Expand All @@ -13,7 +12,6 @@ import {
PICKER_TOP_OFFSET,
} from '@deephaven/utils';
import cl from 'classnames';
import { Tooltip } from '../../popper';
import {
isNormalizedSection,
NormalizedSpectrumPickerProps,
Expand All @@ -27,7 +25,6 @@ import {
} from '../utils/itemUtils';
import { ItemContent } from '../ItemContent';
import { Item, Section } from '../shared';
import { Text } from '../Text';

export type PickerProps = {
children: ItemOrSection | ItemOrSection[] | NormalizedItem[];
Expand Down Expand Up @@ -69,26 +66,6 @@ export type PickerProps = {
| 'defaultSelectedKey'
>;

/**
* Create tooltip content optionally wrapping with a Flex column for array
* content. This is needed for Items containing description `Text` elements.
*/
function createTooltipContent(content: ReactNode) {
if (typeof content === 'boolean') {
return String(content);
}

if (Array.isArray(content)) {
return (
<Flex direction="column" alignItems="start">
{content.filter(node => isElementOfType(node, Text))}
</Flex>
);
}

return content;
}

/**
* Picker component for selecting items from a list of items. Items can be
* provided via the `items` prop or as children. Each item can be a string,
Expand Down Expand Up @@ -142,14 +119,7 @@ export function Picker({
// 'Empty' value so that they are not empty strings.
textValue={textValue === '' ? 'Empty' : textValue}
>
<>
<ItemContent>{content}</ItemContent>
{tooltipOptions == null || content === '' ? null : (
<Tooltip options={tooltipOptions}>
{createTooltipContent(content)}
</Tooltip>
)}
</>
<ItemContent tooltipOptions={tooltipOptions}>{content}</ItemContent>
</Item>
);
},
Expand Down

0 comments on commit e711300

Please sign in to comment.