From ed6ed2a8b74d41138f1f003cbcdc25dbe69f3dc8 Mon Sep 17 00:00:00 2001 From: Brian Ingles <brianingles@deephaven.io> Date: Tue, 2 Apr 2024 09:48:53 -0500 Subject: [PATCH 01/41] Generics to narrow types to item only when appropriate (#1909) --- .../src/spectrum/utils/itemUtils.ts | 46 +++++++++++-------- 1 file changed, 27 insertions(+), 19 deletions(-) diff --git a/packages/components/src/spectrum/utils/itemUtils.ts b/packages/components/src/spectrum/utils/itemUtils.ts index 06f5e852dc..b00db9af65 100644 --- a/packages/components/src/spectrum/utils/itemUtils.ts +++ b/packages/components/src/spectrum/utils/itemUtils.ts @@ -67,6 +67,9 @@ export type NormalizedSection = KeyedItem< Key | undefined >; +export type NormalizedItemOrSection<TItemOrSection extends ItemOrSection> = + TItemOrSection extends SectionElement ? NormalizedSection : NormalizedItem; + export type NormalizedSpectrumPickerProps = SpectrumPickerProps<NormalizedItem>; export type TooltipOptions = { placement: PopperOptions['placement'] }; @@ -114,14 +117,19 @@ export function isItemElement<T>( } /** - * Determine if a node is an array containing normalized items with keys. - * Note that this only checks the first node in the array. + * Determine if a node is an array containing normalized items or sections with + * keys. Note that this only checks the first node in the array. * @param node The node to check - * @returns True if the node is a normalized item with keys array + * @returns True if the node is a normalized item or section with keys array */ -export function isNormalizedItemsWithKeysList( - node: ItemOrSection | ItemOrSection[] | (NormalizedItem | NormalizedSection)[] -): node is (NormalizedItem | NormalizedSection)[] { +export function isNormalizedItemsWithKeysList< + TItemOrSection extends ItemOrSection, +>( + node: + | TItemOrSection + | TItemOrSection[] + | NormalizedItemOrSection<TItemOrSection>[] +): node is NormalizedItemOrSection<TItemOrSection>[] { if (!Array.isArray(node)) { return false; } @@ -225,9 +233,9 @@ function normalizeTextValue(item: ItemElementOrPrimitive): string | undefined { * @param itemOrSection item to normalize * @returns NormalizedItem or NormalizedSection object */ -function normalizeItem( - itemOrSection: ItemOrSection -): NormalizedItem | NormalizedSection { +function normalizeItem<TItemOrSection extends ItemOrSection>( + itemOrSection: TItemOrSection +): NormalizedItemOrSection<TItemOrSection> { if (!isItemOrSection(itemOrSection)) { log.debug(INVALID_ITEM_ERROR_MESSAGE, itemOrSection); throw new Error(INVALID_ITEM_ERROR_MESSAGE); @@ -244,7 +252,7 @@ function normalizeItem( return { item: { key, title, items }, - }; + } as NormalizedItemOrSection<TItemOrSection>; } const key = normalizeItemKey(itemOrSection); @@ -255,23 +263,23 @@ function normalizeItem( return { item: { key, content, textValue }, - }; + } as NormalizedItemOrSection<TItemOrSection>; } /** - * Get normalized items from an item or array of items. - * @param itemsOrSections An item or array of items - * @returns An array of normalized items + * Normalize an item or section or a list of items or sections. + * @param itemsOrSections An item or section or array of items or sections + * @returns An array of normalized items or sections */ -export function normalizeItemList( - itemsOrSections: ItemOrSection | ItemOrSection[] | NormalizedItem[] -): (NormalizedItem | NormalizedSection)[] { +export function normalizeItemList<TItemOrSection extends ItemOrSection>( + itemsOrSections: TItemOrSection | TItemOrSection[] | NormalizedItem[] +): NormalizedItemOrSection<TItemOrSection>[] { // If already normalized, just return as-is if (isNormalizedItemsWithKeysList(itemsOrSections)) { - return itemsOrSections; + return itemsOrSections as NormalizedItemOrSection<TItemOrSection>[]; } - const itemsArray = Array.isArray(itemsOrSections) + const itemsArray: TItemOrSection[] = Array.isArray(itemsOrSections) ? itemsOrSections : [itemsOrSections]; From 22191159cb9721e4933af46085c37fd6f42ec015 Mon Sep 17 00:00:00 2001 From: Brian Ingles <brianingles@deephaven.io> Date: Tue, 2 Apr 2024 10:04:48 -0500 Subject: [PATCH 02/41] Tooltip placement override (#1909) --- .../components/src/spectrum/utils/itemUtils.test.tsx | 5 +++++ packages/components/src/spectrum/utils/itemUtils.ts | 9 ++++++--- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/packages/components/src/spectrum/utils/itemUtils.test.tsx b/packages/components/src/spectrum/utils/itemUtils.test.tsx index e2cdad44c7..4be6d49580 100644 --- a/packages/components/src/spectrum/utils/itemUtils.test.tsx +++ b/packages/components/src/spectrum/utils/itemUtils.test.tsx @@ -289,4 +289,9 @@ describe('normalizeTooltipOptions', () => { const actual = normalizeTooltipOptions(options); expect(actual).toEqual(expected); }); + + it('should allow overriding default placement', () => { + const actual = normalizeTooltipOptions(true, 'top'); + expect(actual).toEqual({ placement: 'top' }); + }); }); diff --git a/packages/components/src/spectrum/utils/itemUtils.ts b/packages/components/src/spectrum/utils/itemUtils.ts index b00db9af65..eb7065b94b 100644 --- a/packages/components/src/spectrum/utils/itemUtils.ts +++ b/packages/components/src/spectrum/utils/itemUtils.ts @@ -288,18 +288,21 @@ export function normalizeItemList<TItemOrSection extends ItemOrSection>( /** * Returns a TooltipOptions object or null if options is false or null. - * @param options + * @param options Tooltip options + * @param placement Default placement for the tooltip if `options` is set + * explicitly to `true` * @returns TooltipOptions or null */ export function normalizeTooltipOptions( - options?: boolean | TooltipOptions | null + options?: boolean | TooltipOptions | null, + placement: TooltipOptions['placement'] = 'right' ): TooltipOptions | null { if (options == null || options === false) { return null; } if (options === true) { - return { placement: 'right' }; + return { placement }; } return options; From 5a98665d3e5915b344bacc3b8449fe1231fdf758 Mon Sep 17 00:00:00 2001 From: Brian Ingles <brianingles@deephaven.io> Date: Tue, 2 Apr 2024 10:33:06 -0500 Subject: [PATCH 03/41] Rename / move PickerItemContent (#1909) --- .../{picker/PickerItemContent.tsx => ItemContent.tsx} | 10 +++++----- packages/components/src/spectrum/index.ts | 1 + packages/components/src/spectrum/picker/Picker.tsx | 4 ++-- packages/components/src/spectrum/picker/index.ts | 1 - 4 files changed, 8 insertions(+), 8 deletions(-) rename packages/components/src/spectrum/{picker/PickerItemContent.tsx => ItemContent.tsx} (88%) diff --git a/packages/components/src/spectrum/picker/PickerItemContent.tsx b/packages/components/src/spectrum/ItemContent.tsx similarity index 88% rename from packages/components/src/spectrum/picker/PickerItemContent.tsx rename to packages/components/src/spectrum/ItemContent.tsx index d680c56ed2..7f4af2059d 100644 --- a/packages/components/src/spectrum/picker/PickerItemContent.tsx +++ b/packages/components/src/spectrum/ItemContent.tsx @@ -2,9 +2,9 @@ import { Children, cloneElement, isValidElement, ReactNode } from 'react'; import { Text } from '@adobe/react-spectrum'; import cl from 'classnames'; import { isElementOfType } from '@deephaven/react-hooks'; -import stylesCommon from '../../SpectrumComponent.module.scss'; +import stylesCommon from '../SpectrumComponent.module.scss'; -export interface PickerItemContentProps { +export interface ItemContentProps { children: ReactNode; } @@ -12,9 +12,9 @@ export interface PickerItemContentProps { * Picker item content. Text content will be wrapped in a Spectrum Text * component with ellipsis overflow handling. */ -export function PickerItemContent({ +export function ItemContent({ children: content, -}: PickerItemContentProps): JSX.Element | null { +}: ItemContentProps): JSX.Element | null { if (isValidElement(content)) { return content; } @@ -57,4 +57,4 @@ export function PickerItemContent({ ); } -export default PickerItemContent; +export default ItemContent; diff --git a/packages/components/src/spectrum/index.ts b/packages/components/src/spectrum/index.ts index e001ef8beb..bdb4f7a506 100644 --- a/packages/components/src/spectrum/index.ts +++ b/packages/components/src/spectrum/index.ts @@ -24,4 +24,5 @@ export * from './View'; /** * Custom DH spectrum utils */ +export * from './ItemContent'; export * from './utils'; diff --git a/packages/components/src/spectrum/picker/Picker.tsx b/packages/components/src/spectrum/picker/Picker.tsx index e706ede9d8..8d533480a5 100644 --- a/packages/components/src/spectrum/picker/Picker.tsx +++ b/packages/components/src/spectrum/picker/Picker.tsx @@ -25,7 +25,7 @@ import { ItemKey, getItemKey, } from '../utils/itemUtils'; -import { PickerItemContent } from './PickerItemContent'; +import { ItemContent } from '../ItemContent'; import { Item, Section } from '../shared'; import { Text } from '../Text'; @@ -143,7 +143,7 @@ export function Picker({ textValue={textValue === '' ? 'Empty' : textValue} > <> - <PickerItemContent>{content}</PickerItemContent> + <ItemContent>{content}</ItemContent> {tooltipOptions == null || content === '' ? null : ( <Tooltip options={tooltipOptions}> {createTooltipContent(content)} diff --git a/packages/components/src/spectrum/picker/index.ts b/packages/components/src/spectrum/picker/index.ts index b666d3021b..c434d5d810 100644 --- a/packages/components/src/spectrum/picker/index.ts +++ b/packages/components/src/spectrum/picker/index.ts @@ -1,2 +1 @@ export * from './Picker'; -export * from './PickerItemContent'; From 991042f5651dc2459253aaf14d387ab802254c8a Mon Sep 17 00:00:00 2001 From: Brian Ingles <brianingles@deephaven.io> Date: Tue, 2 Apr 2024 12:26:29 -0500 Subject: [PATCH 04/41] Smarter item tooltips (#1909) --- .../components/src/spectrum/ItemContent.tsx | 76 +++++++++++++++++-- .../components/src/spectrum/ItemTooltip.tsx | 34 +++++++++ packages/components/src/spectrum/Text.tsx | 34 ++++++--- packages/components/src/spectrum/index.ts | 1 + .../components/src/spectrum/picker/Picker.tsx | 36 +-------- 5 files changed, 128 insertions(+), 53 deletions(-) create mode 100644 packages/components/src/spectrum/ItemTooltip.tsx diff --git a/packages/components/src/spectrum/ItemContent.tsx b/packages/components/src/spectrum/ItemContent.tsx index 7f4af2059d..d5c92180d4 100644 --- a/packages/components/src/spectrum/ItemContent.tsx +++ b/packages/components/src/spectrum/ItemContent.tsx @@ -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; } @@ -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 @@ -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} + </> ); } diff --git a/packages/components/src/spectrum/ItemTooltip.tsx b/packages/components/src/spectrum/ItemTooltip.tsx new file mode 100644 index 0000000000..d2f031457d --- /dev/null +++ b/packages/components/src/spectrum/ItemTooltip.tsx @@ -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; diff --git a/packages/components/src/spectrum/Text.tsx b/packages/components/src/spectrum/Text.tsx index 3164c2eafd..da6c7348eb 100644 --- a/packages/components/src/spectrum/Text.tsx +++ b/packages/components/src/spectrum/Text.tsx @@ -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 & { @@ -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; diff --git a/packages/components/src/spectrum/index.ts b/packages/components/src/spectrum/index.ts index bdb4f7a506..8db2ca0cb3 100644 --- a/packages/components/src/spectrum/index.ts +++ b/packages/components/src/spectrum/index.ts @@ -25,4 +25,5 @@ export * from './View'; * Custom DH spectrum utils */ export * from './ItemContent'; +export * from './ItemTooltip'; export * from './utils'; diff --git a/packages/components/src/spectrum/picker/Picker.tsx b/packages/components/src/spectrum/picker/Picker.tsx index 8d533480a5..e1ac6cae2e 100644 --- a/packages/components/src/spectrum/picker/Picker.tsx +++ b/packages/components/src/spectrum/picker/Picker.tsx @@ -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 { @@ -13,7 +12,6 @@ import { PICKER_TOP_OFFSET, } from '@deephaven/utils'; import cl from 'classnames'; -import { Tooltip } from '../../popper'; import { isNormalizedSection, NormalizedSpectrumPickerProps, @@ -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[]; @@ -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, @@ -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> ); }, From 97d74128d019b244d856ae2157be38a2d82427df Mon Sep 17 00:00:00 2001 From: Brian Ingles <brianingles@deephaven.io> Date: Tue, 2 Apr 2024 12:34:17 -0500 Subject: [PATCH 05/41] forwardedRefs (#1909) --- packages/components/src/spectrum/Heading.tsx | 14 +++++++--- packages/components/src/spectrum/Text.tsx | 14 +++------- packages/components/src/spectrum/View.tsx | 29 ++++++++++++-------- 3 files changed, 31 insertions(+), 26 deletions(-) diff --git a/packages/components/src/spectrum/Heading.tsx b/packages/components/src/spectrum/Heading.tsx index 38682e6ca7..f29e7b2729 100644 --- a/packages/components/src/spectrum/Heading.tsx +++ b/packages/components/src/spectrum/Heading.tsx @@ -1,9 +1,10 @@ /* eslint-disable react/jsx-props-no-spreading */ -import { useMemo } from 'react'; +import { forwardRef, useMemo } from 'react'; import { Heading as SpectrumHeading, type HeadingProps as SpectrumHeadingProps, } from '@adobe/react-spectrum'; +import type { DOMRefValue } from '@react-types/shared'; import { type ColorValue, colorValueStyle } from '../theme/colorUtils'; export type HeadingProps = SpectrumHeadingProps & { @@ -20,7 +21,10 @@ export type HeadingProps = SpectrumHeadingProps & { * */ -export function Heading(props: HeadingProps): JSX.Element { +export const Heading = forwardRef< + DOMRefValue<HTMLHeadingElement>, + HeadingProps +>((props, forwardedRef): JSX.Element => { const { color, UNSAFE_style, ...rest } = props; const style = useMemo( () => ({ @@ -30,7 +34,9 @@ export function Heading(props: HeadingProps): JSX.Element { [color, UNSAFE_style] ); - return <SpectrumHeading {...rest} UNSAFE_style={style} />; -} + return <SpectrumHeading {...rest} ref={forwardedRef} UNSAFE_style={style} />; +}); + +Heading.displayName = 'Heading'; export default Heading; diff --git a/packages/components/src/spectrum/Text.tsx b/packages/components/src/spectrum/Text.tsx index da6c7348eb..d0467f275d 100644 --- a/packages/components/src/spectrum/Text.tsx +++ b/packages/components/src/spectrum/Text.tsx @@ -4,7 +4,7 @@ import { Text as SpectrumText, type TextProps as SpectrumTextProps, } from '@adobe/react-spectrum'; -import type { DOMRef, DOMRefValue } from '@react-types/shared'; +import type { DOMRefValue } from '@react-types/shared'; import { type ColorValue, colorValueStyle } from '../theme/colorUtils'; export type TextProps = SpectrumTextProps & { @@ -20,8 +20,8 @@ export type TextProps = SpectrumTextProps & { * @returns The Text component * */ -export const Text = forwardRef<DOMRefValue<HTMLSpanElement>, SpectrumTextProps>( - (props: TextProps, ref): JSX.Element => { +export const Text = forwardRef<DOMRefValue<HTMLSpanElement>, TextProps>( + (props, forwardedRef): JSX.Element => { const { color, UNSAFE_style, ...rest } = props; const style = useMemo( () => ({ @@ -31,13 +31,7 @@ export const Text = forwardRef<DOMRefValue<HTMLSpanElement>, SpectrumTextProps>( [color, UNSAFE_style] ); - return ( - <SpectrumText - {...rest} - ref={ref as unknown as DOMRef<HTMLDivElement>} - UNSAFE_style={style} - /> - ); + return <SpectrumText {...rest} ref={forwardedRef} UNSAFE_style={style} />; } ); diff --git a/packages/components/src/spectrum/View.tsx b/packages/components/src/spectrum/View.tsx index 847900541e..03c28a193e 100644 --- a/packages/components/src/spectrum/View.tsx +++ b/packages/components/src/spectrum/View.tsx @@ -1,9 +1,10 @@ /* eslint-disable react/jsx-props-no-spreading */ -import { useMemo } from 'react'; +import { forwardRef, useMemo } from 'react'; import { View as SpectrumView, type ViewProps as SpectrumViewProps, } from '@adobe/react-spectrum'; +import type { DOMRefValue } from '@react-types/shared'; import { type ColorValue, colorValueStyle } from '../theme/colorUtils'; export type ViewProps = Omit<SpectrumViewProps<6>, 'backgroundColor'> & { @@ -20,17 +21,21 @@ export type ViewProps = Omit<SpectrumViewProps<6>, 'backgroundColor'> & { * */ -export function View(props: ViewProps): JSX.Element { - const { backgroundColor, UNSAFE_style, ...rest } = props; - const style = useMemo( - () => ({ - ...UNSAFE_style, - backgroundColor: colorValueStyle(backgroundColor), - }), - [backgroundColor, UNSAFE_style] - ); +export const View = forwardRef<DOMRefValue<HTMLElement>, ViewProps>( + (props, forwardedRef): JSX.Element => { + const { backgroundColor, UNSAFE_style, ...rest } = props; + const style = useMemo( + () => ({ + ...UNSAFE_style, + backgroundColor: colorValueStyle(backgroundColor), + }), + [backgroundColor, UNSAFE_style] + ); - return <SpectrumView {...rest} UNSAFE_style={style} />; -} + return <SpectrumView {...rest} ref={forwardedRef} UNSAFE_style={style} />; + } +); + +View.displayName = 'View'; export default View; From b7f4df343cc12ec012184ecc1ca2af48b280c89a Mon Sep 17 00:00:00 2001 From: Brian Ingles <brianingles@deephaven.io> Date: Tue, 2 Apr 2024 14:02:31 -0500 Subject: [PATCH 06/41] Basic ListView implementation (#1909) --- .../code-studio/src/styleguide/ListViews.tsx | 119 ++++++++++++++++++ .../code-studio/src/styleguide/Pickers.tsx | 2 +- .../code-studio/src/styleguide/StyleGuide.tsx | 2 + .../components/src/spectrum/collections.ts | 2 - packages/components/src/spectrum/index.ts | 1 + .../src/spectrum/listView/ListView.tsx | 103 +++++++++++++++ .../components/src/spectrum/listView/index.ts | 1 + .../components/src/spectrum/picker/Picker.tsx | 47 ++----- .../components/src/spectrum/utils/index.ts | 2 + .../utils/useRenderNormalizedItem.tsx | 39 ++++++ .../utils/useStringifiedMultiSelection.ts | 104 +++++++++++++++ 11 files changed, 384 insertions(+), 38 deletions(-) create mode 100644 packages/code-studio/src/styleguide/ListViews.tsx create mode 100644 packages/components/src/spectrum/listView/ListView.tsx create mode 100644 packages/components/src/spectrum/listView/index.ts create mode 100644 packages/components/src/spectrum/utils/useRenderNormalizedItem.tsx create mode 100644 packages/components/src/spectrum/utils/useStringifiedMultiSelection.ts diff --git a/packages/code-studio/src/styleguide/ListViews.tsx b/packages/code-studio/src/styleguide/ListViews.tsx new file mode 100644 index 0000000000..f521321c5f --- /dev/null +++ b/packages/code-studio/src/styleguide/ListViews.tsx @@ -0,0 +1,119 @@ +import React, { useCallback, useState } from 'react'; +import { Grid, Item, ListView, ItemKey, Text } from '@deephaven/components'; +import { vsAccount, vsPerson } from '@deephaven/icons'; +import { Icon } from '@adobe/react-spectrum'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { sampleSectionIdAndClasses } from './utils'; + +// Generate enough items to require scrolling +const itemsSimple = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz' + .split('') + .map((key, i) => ({ + key, + item: { key: (i + 1) * 100, content: `${key}${key}${key}` }, + })); + +function AccountIcon({ + slot, +}: { + slot?: 'illustration' | 'image'; +}): JSX.Element { + return ( + // Images in ListView items require a slot of 'image' or 'illustration' to + // be set in order to be positioned correctly: + // https://github.com/adobe/react-spectrum/blob/784737effd44b9d5e2b1316e690da44555eafd7e/packages/%40react-spectrum/list/src/ListViewItem.tsx#L266-L267 + <Icon slot={slot}> + <FontAwesomeIcon icon={vsAccount} /> + </Icon> + ); +} + +export function ListViews(): JSX.Element { + const [selectedKeys, setSelectedKeys] = useState<'all' | Iterable<ItemKey>>( + [] + ); + + const onChange = useCallback((keys: 'all' | Iterable<ItemKey>): void => { + setSelectedKeys(keys); + }, []); + + return ( + // eslint-disable-next-line react/jsx-props-no-spreading + <div {...sampleSectionIdAndClasses('list-views')}> + <h2 className="ui-title">List View</h2> + + <Grid columnGap={14} height="size-4600"> + <Text>Single Child</Text> + <ListView + gridRow="2" + aria-label="Single Child" + selectionMode="multiple" + > + <Item>Aaa</Item> + </ListView> + + <label>Icons</label> + <ListView gridRow="2" aria-label="Icon" selectionMode="multiple"> + <Item textValue="Item with icon A"> + <AccountIcon slot="image" /> + <Text>Item with icon A</Text> + </Item> + <Item textValue="Item with icon B"> + <AccountIcon slot="image" /> + <Text>Item with icon B</Text> + </Item> + <Item textValue="Item with icon C"> + <AccountIcon slot="image" /> + <Text>Item with icon C</Text> + </Item> + <Item textValue="Item with icon D"> + <AccountIcon slot="image" /> + <Text>Item with icon D with overflowing content</Text> + </Item> + </ListView> + + <label>Mixed Children Types</label> + <ListView + gridRow="2" + aria-label="Mixed Children Types" + maxWidth="size-2400" + selectionMode="multiple" + defaultSelectedKeys={[999, 444]} + > + {/* eslint-disable react/jsx-curly-brace-presence */} + {'String 1'} + {'String 2'} + {'String 3'} + {''} + {'Some really long text that should get truncated'} + {/* eslint-enable react/jsx-curly-brace-presence */} + {444} + {999} + {true} + {false} + <Item>Item Aaa</Item> + <Item>Item Bbb</Item> + <Item textValue="Complex Ccc"> + <Icon slot="image"> + <FontAwesomeIcon icon={vsPerson} /> + </Icon> + <Text>Complex Ccc with text that should be truncated</Text> + </Item> + </ListView> + + <label>Controlled</label> + <ListView + gridRow="2" + aria-label="Controlled" + selectionMode="multiple" + selectedKeys={selectedKeys} + onChange={onChange} + > + {itemsSimple} + </ListView> + </Grid> + </div> + ); +} + +export default ListViews; diff --git a/packages/code-studio/src/styleguide/Pickers.tsx b/packages/code-studio/src/styleguide/Pickers.tsx index 18dc678c10..bee79b1d0b 100644 --- a/packages/code-studio/src/styleguide/Pickers.tsx +++ b/packages/code-studio/src/styleguide/Pickers.tsx @@ -29,7 +29,7 @@ function PersonIcon(): JSX.Element { } export function Pickers(): JSX.Element { - const [selectedKey, setSelectedKey] = useState<ItemKey>(); + const [selectedKey, setSelectedKey] = useState<ItemKey | null>(null); const onChange = useCallback((key: ItemKey): void => { setSelectedKey(key); diff --git a/packages/code-studio/src/styleguide/StyleGuide.tsx b/packages/code-studio/src/styleguide/StyleGuide.tsx index 051a58917f..72c3689777 100644 --- a/packages/code-studio/src/styleguide/StyleGuide.tsx +++ b/packages/code-studio/src/styleguide/StyleGuide.tsx @@ -36,6 +36,7 @@ import { GoldenLayout } from './GoldenLayout'; import { RandomAreaPlotAnimation } from './RandomAreaPlotAnimation'; import SpectrumComparison from './SpectrumComparison'; import Pickers from './Pickers'; +import ListViews from './ListViews'; const stickyProps = { position: 'sticky', @@ -109,6 +110,7 @@ function StyleGuide(): React.ReactElement { <Buttons /> <Progress /> <Inputs /> + <ListViews /> <Pickers /> <ItemListInputs /> <DraggableLists /> diff --git a/packages/components/src/spectrum/collections.ts b/packages/components/src/spectrum/collections.ts index 619027a946..a1d502975c 100644 --- a/packages/components/src/spectrum/collections.ts +++ b/packages/components/src/spectrum/collections.ts @@ -3,8 +3,6 @@ export { type SpectrumActionBarProps as ActionBarProps, ActionMenu, type SpectrumActionMenuProps as ActionMenuProps, - ListView, - type SpectrumListViewProps as ListViewProps, MenuTrigger, type SpectrumMenuTriggerProps as MenuTriggerProps, TagGroup, diff --git a/packages/components/src/spectrum/index.ts b/packages/components/src/spectrum/index.ts index 8db2ca0cb3..02f1d4df7a 100644 --- a/packages/components/src/spectrum/index.ts +++ b/packages/components/src/spectrum/index.ts @@ -16,6 +16,7 @@ export * from './status'; /** * Custom DH components wrapping React Spectrum components. */ +export * from './listView'; export * from './picker'; export * from './Heading'; export * from './Text'; diff --git a/packages/components/src/spectrum/listView/ListView.tsx b/packages/components/src/spectrum/listView/ListView.tsx new file mode 100644 index 0000000000..ff9cb04b7f --- /dev/null +++ b/packages/components/src/spectrum/listView/ListView.tsx @@ -0,0 +1,103 @@ +import { useMemo } from 'react'; +import { + ListView as SpectrumListView, + SpectrumListViewProps, +} from '@adobe/react-spectrum'; +import cl from 'classnames'; +import { + ItemElementOrPrimitive, + ItemKey, + NormalizedItem, + normalizeItemList, + normalizeTooltipOptions, + TooltipOptions, + useRenderNormalizedItem, + useStringifiedMultiSelection, +} from '../utils'; + +export type ListViewProps = { + children: + | ItemElementOrPrimitive + | ItemElementOrPrimitive[] + | NormalizedItem[]; + /** Can be set to true or a TooltipOptions to enable item tooltips */ + tooltip?: boolean | TooltipOptions; + selectedKeys?: 'all' | Iterable<ItemKey>; + defaultSelectedKeys?: 'all' | Iterable<ItemKey>; + disabledKeys?: Iterable<ItemKey>; + /** + * Handler that is called when the selection change. + * Note that under the hood, this is just an alias for Spectrum's + * `onSelectionChange`. We are renaming for better consistency with other + * components. + */ + onChange?: (keys: 'all' | Set<ItemKey>) => void; + + /** + * Handler that is called when the selection changes. + * @deprecated Use `onChange` instead + */ + onSelectionChange?: (keys: 'all' | Set<ItemKey>) => void; +} & Omit< + SpectrumListViewProps<NormalizedItem>, + | 'children' + | 'items' + | 'selectedKeys' + | 'defaultSelectedKeys' + | 'disabledKeys' + | 'onSelectionChange' +>; + +export function ListView({ + children, + tooltip = true, + selectedKeys, + defaultSelectedKeys, + disabledKeys, + UNSAFE_className, + onChange, + onSelectionChange, + ...spectrumListViewProps +}: ListViewProps): JSX.Element { + const normalizedItems = useMemo( + () => normalizeItemList(children), + [children] + ); + + const tooltipOptions = useMemo( + () => normalizeTooltipOptions(tooltip, 'bottom'), + [tooltip] + ); + + const renderNormalizedItem = useRenderNormalizedItem(tooltipOptions); + + const { + selectedStringKeys, + defaultSelectedStringKeys, + disabledStringKeys, + onStringSelectionChange, + } = useStringifiedMultiSelection({ + normalizedItems, + selectedKeys, + defaultSelectedKeys, + disabledKeys, + onChange: onChange ?? onSelectionChange, + }); + + return ( + <SpectrumListView + // eslint-disable-next-line react/jsx-props-no-spreading + {...spectrumListViewProps} + UNSAFE_className={cl('dh-list-view', UNSAFE_className)} + items={normalizedItems} + selectedKeys={selectedStringKeys} + defaultSelectedKeys={defaultSelectedStringKeys} + disabledKeys={disabledStringKeys} + onSelectionChange={onStringSelectionChange} + > + {renderNormalizedItem} + </SpectrumListView> + ); +} + +export default ListView; diff --git a/packages/components/src/spectrum/listView/index.ts b/packages/components/src/spectrum/listView/index.ts new file mode 100644 index 0000000000..e1e4de2f28 --- /dev/null +++ b/packages/components/src/spectrum/listView/index.ts @@ -0,0 +1 @@ +export * from './ListView'; diff --git a/packages/components/src/spectrum/picker/Picker.tsx b/packages/components/src/spectrum/picker/Picker.tsx index e1ac6cae2e..415f3051cb 100644 --- a/packages/components/src/spectrum/picker/Picker.tsx +++ b/packages/components/src/spectrum/picker/Picker.tsx @@ -1,4 +1,4 @@ -import { Key, useCallback, useMemo } from 'react'; +import { useCallback, useMemo } from 'react'; import { DOMRef } from '@react-types/shared'; import { Picker as SpectrumPicker } from '@adobe/react-spectrum'; import { @@ -23,8 +23,8 @@ import { ItemKey, getItemKey, } from '../utils/itemUtils'; -import { ItemContent } from '../ItemContent'; -import { Item, Section } from '../shared'; +import { Section } from '../shared'; +import { useRenderNormalizedItem } from '../utils'; export type PickerProps = { children: ItemOrSection | ItemOrSection[] | NormalizedItem[]; @@ -97,34 +97,7 @@ export function Picker({ [tooltip] ); - const renderItem = useCallback( - (normalizedItem: NormalizedItem) => { - const key = getItemKey(normalizedItem); - const content = normalizedItem.item?.content ?? ''; - const textValue = normalizedItem.item?.textValue ?? ''; - - return ( - <Item - // Note that setting the `key` prop explicitly on `Item` elements - // causes the picker to expect `selectedKey` and `defaultSelectedKey` - // to be strings. It also passes the stringified value of the key to - // `onSelectionChange` handlers` regardless of the actual type of the - // key. We can't really get around setting in order to support Windowed - // data, so we'll need to do some manual conversion of keys to strings - // in other places of this component. - key={key as Key} - // The `textValue` prop gets used to provide the content of `<option>` - // elements that back the Spectrum Picker. These are not visible in the UI, - // but are used for accessibility purposes, so we set to an arbitrary - // 'Empty' value so that they are not empty strings. - textValue={textValue === '' ? 'Empty' : textValue} - > - <ItemContent tooltipOptions={tooltipOptions}>{content}</ItemContent> - </Item> - ); - }, - [tooltipOptions] - ); + const renderNormalizedItem = useRenderNormalizedItem(tooltipOptions); const getInitialScrollPositionInternal = useCallback( () => @@ -187,8 +160,12 @@ export function Picker({ // set on `Item` elements. Since we do this in `renderItem`, we need to // ensure that `selectedKey` and `defaultSelectedKey` are strings in order // for selection to work. - selectedKey={selectedKey?.toString()} - defaultSelectedKey={defaultSelectedKey?.toString()} + selectedKey={selectedKey == null ? selectedKey : selectedKey.toString()} + defaultSelectedKey={ + defaultSelectedKey == null + ? defaultSelectedKey + : defaultSelectedKey.toString() + } // `onChange` is just an alias for `onSelectionChange` onSelectionChange={ onSelectionChangeInternal as NormalizedSpectrumPickerProps['onSelectionChange'] @@ -202,12 +179,12 @@ export function Picker({ title={itemOrSection.item?.title} items={itemOrSection.item?.items} > - {renderItem} + {renderNormalizedItem} </Section> ); } - return renderItem(itemOrSection); + return renderNormalizedItem(itemOrSection); }} </SpectrumPicker> ); diff --git a/packages/components/src/spectrum/utils/index.ts b/packages/components/src/spectrum/utils/index.ts index ab03442699..ef406aba98 100644 --- a/packages/components/src/spectrum/utils/index.ts +++ b/packages/components/src/spectrum/utils/index.ts @@ -1,2 +1,4 @@ export * from './itemUtils'; export * from './themeUtils'; +export * from './useRenderNormalizedItem'; +export * from './useStringifiedMultiSelection'; diff --git a/packages/components/src/spectrum/utils/useRenderNormalizedItem.tsx b/packages/components/src/spectrum/utils/useRenderNormalizedItem.tsx new file mode 100644 index 0000000000..52a70f62dc --- /dev/null +++ b/packages/components/src/spectrum/utils/useRenderNormalizedItem.tsx @@ -0,0 +1,39 @@ +import { Key, useCallback } from 'react'; +import { ItemContent } from '../ItemContent'; +import { Item } from '../shared'; +import { getItemKey, NormalizedItem, TooltipOptions } from './itemUtils'; + +export function useRenderNormalizedItem( + tooltipOptions: TooltipOptions | null +): (normalizedItem: NormalizedItem) => JSX.Element { + return useCallback( + (normalizedItem: NormalizedItem) => { + const key = getItemKey(normalizedItem); + const content = normalizedItem.item?.content ?? ''; + const textValue = normalizedItem.item?.textValue ?? ''; + + return ( + <Item + // Note that setting the `key` prop explicitly on `Item` elements + // causes the picker to expect `selectedKey` and `defaultSelectedKey` + // to be strings. It also passes the stringified value of the key to + // `onSelectionChange` handlers` regardless of the actual type of the + // key. We can't really get around setting in order to support Windowed + // data, so we'll need to do some manual conversion of keys to strings + // in other places of this component. + key={key as Key} + // The `textValue` prop gets used to provide the content of `<option>` + // elements that back the Spectrum Picker. These are not visible in the UI, + // but are used for accessibility purposes, so we set to an arbitrary + // 'Empty' value so that they are not empty strings. + textValue={textValue === '' ? 'Empty' : textValue} + > + <ItemContent tooltipOptions={tooltipOptions}>{content}</ItemContent> + </Item> + ); + }, + [tooltipOptions] + ); +} + +export default useRenderNormalizedItem; diff --git a/packages/components/src/spectrum/utils/useStringifiedMultiSelection.ts b/packages/components/src/spectrum/utils/useStringifiedMultiSelection.ts new file mode 100644 index 0000000000..cadb8a4888 --- /dev/null +++ b/packages/components/src/spectrum/utils/useStringifiedMultiSelection.ts @@ -0,0 +1,104 @@ +import { Key, useCallback, useMemo } from 'react'; +import { getItemKey, ItemKey, NormalizedItem } from '.'; + +function toStringKeySet( + keys?: 'all' | Iterable<ItemKey> +): undefined | 'all' | Set<Key> { + if (keys == null || keys === 'all') { + return keys as undefined | 'all'; + } + + return new Set([...keys].map(String)); +} + +export interface UseStringifiedMultiSelectionOptions { + normalizedItems: NormalizedItem[]; + selectedKeys?: 'all' | Iterable<ItemKey>; + defaultSelectedKeys?: 'all' | Iterable<ItemKey>; + disabledKeys?: Iterable<ItemKey>; + /** + * Handler that is called when the selection change. + * Note that under the hood, this is just an alias for Spectrum's + * `onSelectionChange`. We are renaming for better consistency with other + * components. + */ + onChange?: (keys: 'all' | Set<ItemKey>) => void; +} + +export interface UseStringifiedMultiSelectionResult { + /** Stringified selection keys */ + selectedStringKeys?: 'all' | Set<Key>; + /** Stringified default selection keys */ + defaultSelectedStringKeys?: 'all' | Set<Key>; + /** Stringified disabled keys */ + disabledStringKeys?: 'all' | Set<Key>; + /** Handler that is called when the string key selections change */ + onStringSelectionChange: (keys: 'all' | Set<Key>) => void; +} + +/** + * Spectrum collection components treat keys as strings if the `key` prop is + * explicitly set on `Item` elements. Since we do this in `useRenderNormalizedItem`, + * we need to ensure that keys are strings in order for selection to work. We + * then need to convert back to the original key types in the onChange handler. + * This hook encapsulates converting to and from strings so that keys can match + * the original key type. + * @param normalizedItems The normalized items to select from. + * @param selectedKeys The currently selected keys in the collection. + * @param defaultSelectedKeys The initial selected keys in the collection. + * @param disabledKeys The currently disabled keys in the collection. + * @param onChange Handler that is called when the selection changes. + * @returns UseStringifiedMultiSelectionResult with stringified key sets and + * string key selection change handler. + */ +export function useStringifiedMultiSelection({ + normalizedItems, + defaultSelectedKeys, + disabledKeys, + selectedKeys, + onChange, +}: UseStringifiedMultiSelectionOptions): UseStringifiedMultiSelectionResult { + const selectedStringKeys = useMemo( + () => toStringKeySet(selectedKeys), + [selectedKeys] + ); + + const defaultSelectedStringKeys = useMemo( + () => toStringKeySet(defaultSelectedKeys), + [defaultSelectedKeys] + ); + + const disabledStringKeys = useMemo( + () => toStringKeySet(disabledKeys), + [disabledKeys] + ); + + const onStringSelectionChange = useCallback( + (keys: 'all' | Set<Key>) => { + if (keys === 'all') { + onChange?.('all'); + return; + } + + const actualKeys = new Set<ItemKey>(); + + normalizedItems.forEach(item => { + if (keys.has(String(getItemKey(item)))) { + actualKeys.add(getItemKey(item)); + } + }); + + onChange?.(actualKeys); + }, + [normalizedItems, onChange] + ); + + return { + selectedStringKeys, + defaultSelectedStringKeys, + disabledStringKeys, + onStringSelectionChange, + }; +} + +export default useStringifiedMultiSelection; From 9fb7de14f48bd6175404c7bf14d28435017d4be2 Mon Sep 17 00:00:00 2001 From: Brian Ingles <brianingles@deephaven.io> Date: Tue, 2 Apr 2024 15:10:31 -0500 Subject: [PATCH 07/41] ListView table support (#1909) --- .../src/spectrum/listView/ListView.tsx | 12 ++++ .../components/src/spectrum/picker/Picker.tsx | 1 - .../src/spectrum/ListView.tsx | 62 +++++++++++++++++++ .../jsapi-components/src/spectrum/index.ts | 1 + 4 files changed, 75 insertions(+), 1 deletion(-) create mode 100644 packages/jsapi-components/src/spectrum/ListView.tsx diff --git a/packages/components/src/spectrum/listView/ListView.tsx b/packages/components/src/spectrum/listView/ListView.tsx index ff9cb04b7f..8d10b22ecd 100644 --- a/packages/components/src/spectrum/listView/ListView.tsx +++ b/packages/components/src/spectrum/listView/ListView.tsx @@ -3,6 +3,11 @@ import { ListView as SpectrumListView, SpectrumListViewProps, } from '@adobe/react-spectrum'; +import { EMPTY_FUNCTION } from '@deephaven/utils'; +import { + extractSpectrumHTMLElement, + useOnScrollRef, +} from '@deephaven/react-hooks'; import cl from 'classnames'; import { ItemElementOrPrimitive, @@ -33,6 +38,9 @@ export type ListViewProps = { */ onChange?: (keys: 'all' | Set<ItemKey>) => void; + /** Handler that is called when the picker is scrolled. */ + onScroll?: (event: Event) => void; + /** * Handler that is called when the selection changes. * @deprecated Use `onChange` instead @@ -56,6 +64,7 @@ export function ListView({ disabledKeys, UNSAFE_className, onChange, + onScroll = EMPTY_FUNCTION, onSelectionChange, ...spectrumListViewProps }: ListViewProps): JSX.Element { @@ -84,10 +93,13 @@ export function ListView({ onChange: onChange ?? onSelectionChange, }); + const scrollRef = useOnScrollRef(onScroll, extractSpectrumHTMLElement); + return ( <SpectrumListView // eslint-disable-next-line react/jsx-props-no-spreading {...spectrumListViewProps} + ref={scrollRef} UNSAFE_className={cl('dh-list-view', UNSAFE_className)} items={normalizedItems} selectedKeys={selectedStringKeys} diff --git a/packages/components/src/spectrum/picker/Picker.tsx b/packages/components/src/spectrum/picker/Picker.tsx index 415f3051cb..d4bba67aa8 100644 --- a/packages/components/src/spectrum/picker/Picker.tsx +++ b/packages/components/src/spectrum/picker/Picker.tsx @@ -151,7 +151,6 @@ export function Picker({ <SpectrumPicker // eslint-disable-next-line react/jsx-props-no-spreading {...spectrumPickerProps} - // The `ref` prop type defined by React Spectrum is incorrect here ref={scrollRef as unknown as DOMRef<HTMLDivElement>} onOpenChange={onOpenChangeInternal} UNSAFE_className={cl('dh-picker', UNSAFE_className)} diff --git a/packages/jsapi-components/src/spectrum/ListView.tsx b/packages/jsapi-components/src/spectrum/ListView.tsx new file mode 100644 index 0000000000..49d8a166bd --- /dev/null +++ b/packages/jsapi-components/src/spectrum/ListView.tsx @@ -0,0 +1,62 @@ +import { + ListView as ListViewBase, + ListViewProps as ListViewPropsBase, + NormalizedItemData, +} from '@deephaven/components'; +import { dh as DhType } from '@deephaven/jsapi-types'; +import { Settings } from '@deephaven/jsapi-utils'; +import { LIST_VIEW_ROW_HEIGHT } from '@deephaven/utils'; +import useFormatter from '../useFormatter'; +import useViewportData from '../useViewportData'; +import { useItemRowDeserializer } from './utils'; + +export interface ListViewProps extends Omit<ListViewPropsBase, 'children'> { + table: DhType.Table; + /* The column of values to use as item keys. Defaults to the first column. */ + keyColumn?: string; + /* The column of values to display as primary text. Defaults to the `keyColumn` value. */ + labelColumn?: string; + + // TODO #1890 : descriptionColumn, iconColumn + + settings?: Settings; +} + +export function ListView({ + table, + keyColumn: keyColumnName, + labelColumn: labelColumnName, + settings, + ...props +}: ListViewProps): JSX.Element { + const { getFormattedString: formatValue } = useFormatter(settings); + + const deserializeRow = useItemRowDeserializer({ + table, + keyColumnName, + labelColumnName, + formatValue, + }); + + const { viewportData, onScroll } = useViewportData< + NormalizedItemData, + DhType.Table + >({ + reuseItemsOnTableResize: true, + table, + itemHeight: LIST_VIEW_ROW_HEIGHT, + deserializeRow, + }); + + return ( + <ListViewBase + // eslint-disable-next-line react/jsx-props-no-spreading + {...props} + onScroll={onScroll} + > + {viewportData.items} + </ListViewBase> + ); +} + +export default ListView; diff --git a/packages/jsapi-components/src/spectrum/index.ts b/packages/jsapi-components/src/spectrum/index.ts index c434d5d810..49fd5a7e25 100644 --- a/packages/jsapi-components/src/spectrum/index.ts +++ b/packages/jsapi-components/src/spectrum/index.ts @@ -1 +1,2 @@ +export * from './ListView'; export * from './Picker'; From 2a04b319a030065bcf90b2d659c93df3f583db15 Mon Sep 17 00:00:00 2001 From: Brian Ingles <brianingles@deephaven.io> Date: Wed, 3 Apr 2024 14:55:55 -0500 Subject: [PATCH 08/41] Fixed import (#1909) --- .../src/spectrum/utils/useStringifiedMultiSelection.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/components/src/spectrum/utils/useStringifiedMultiSelection.ts b/packages/components/src/spectrum/utils/useStringifiedMultiSelection.ts index cadb8a4888..af0619f62c 100644 --- a/packages/components/src/spectrum/utils/useStringifiedMultiSelection.ts +++ b/packages/components/src/spectrum/utils/useStringifiedMultiSelection.ts @@ -1,5 +1,5 @@ import { Key, useCallback, useMemo } from 'react'; -import { getItemKey, ItemKey, NormalizedItem } from '.'; +import { getItemKey, ItemKey, NormalizedItem } from './itemUtils'; function toStringKeySet( keys?: 'all' | Iterable<ItemKey> From f566e720ef46e7b814c0ecc4c6dddcd1e40c86a7 Mon Sep 17 00:00:00 2001 From: Brian Ingles <brianingles@deephaven.io> Date: Wed, 3 Apr 2024 16:48:25 -0500 Subject: [PATCH 09/41] Mocking virtualizer (#1909) --- .../src/styleguide/StyleGuide.test.tsx | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/packages/code-studio/src/styleguide/StyleGuide.test.tsx b/packages/code-studio/src/styleguide/StyleGuide.test.tsx index f86f510f5e..f238df8847 100644 --- a/packages/code-studio/src/styleguide/StyleGuide.test.tsx +++ b/packages/code-studio/src/styleguide/StyleGuide.test.tsx @@ -14,6 +14,25 @@ describe('<StyleGuide /> mounts', () => { // Provide a non-null array to ThemeProvider to tell it to initialize const customThemes: ThemeData[] = []; + // React Spectrum `useVirtualizerItem` depends on `scrollWidth` and `scrollHeight`. + // Mocking these to avoid React "Maximum update depth exceeded" errors. + // https://github.com/adobe/react-spectrum/blob/0b2a838b36ad6d86eee13abaf68b7e4d2b4ada6c/packages/%40react-aria/virtualizer/src/useVirtualizerItem.ts#L49C3-L49C60 + + // From preview docs: https://reactspectrum.blob.core.windows.net/reactspectrum/726a5e8f0ed50fc8d98e39c74bd6dfeb3660fbdf/docs/react-spectrum/testing.html#virtualized-components + // The virtualizer will now think it has a visible area of 1000px x 1000px and that the items within it are 40px x 40px + jest + .spyOn(window.HTMLElement.prototype, 'clientWidth', 'get') + .mockImplementation(() => 1000); + jest + .spyOn(window.HTMLElement.prototype, 'clientHeight', 'get') + .mockImplementation(() => 1000); + jest + .spyOn(window.HTMLElement.prototype, 'scrollHeight', 'get') + .mockImplementation(() => 40); + jest + .spyOn(window.HTMLElement.prototype, 'scrollWidth', 'get') + .mockImplementation(() => 40); + expect(() => render( <ApiContext.Provider value={dh}> From 9b5a119b63403d21cdd69e4df9a3db67d0fae98b Mon Sep 17 00:00:00 2001 From: Brian Ingles <brianingles@deephaven.io> Date: Wed, 3 Apr 2024 16:50:03 -0500 Subject: [PATCH 10/41] generate normalized items styleguide util (#1909) --- .../code-studio/src/styleguide/ListViews.tsx | 9 +- .../code-studio/src/styleguide/Pickers.tsx | 9 +- .../__snapshots__/utils.test.ts.snap | 706 ++++++++++++++++++ .../code-studio/src/styleguide/utils.test.ts | 8 + packages/code-studio/src/styleguide/utils.ts | 30 + 5 files changed, 748 insertions(+), 14 deletions(-) create mode 100644 packages/code-studio/src/styleguide/__snapshots__/utils.test.ts.snap diff --git a/packages/code-studio/src/styleguide/ListViews.tsx b/packages/code-studio/src/styleguide/ListViews.tsx index f521321c5f..a7fe4be584 100644 --- a/packages/code-studio/src/styleguide/ListViews.tsx +++ b/packages/code-studio/src/styleguide/ListViews.tsx @@ -3,15 +3,10 @@ import { Grid, Item, ListView, ItemKey, Text } from '@deephaven/components'; import { vsAccount, vsPerson } from '@deephaven/icons'; import { Icon } from '@adobe/react-spectrum'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { sampleSectionIdAndClasses } from './utils'; +import { generateNormalizedItems, sampleSectionIdAndClasses } from './utils'; // Generate enough items to require scrolling -const itemsSimple = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz' - .split('') - .map((key, i) => ({ - key, - item: { key: (i + 1) * 100, content: `${key}${key}${key}` }, - })); +const itemsSimple = [...generateNormalizedItems(52)]; function AccountIcon({ slot, diff --git a/packages/code-studio/src/styleguide/Pickers.tsx b/packages/code-studio/src/styleguide/Pickers.tsx index bee79b1d0b..89adb62356 100644 --- a/packages/code-studio/src/styleguide/Pickers.tsx +++ b/packages/code-studio/src/styleguide/Pickers.tsx @@ -10,15 +10,10 @@ import { import { vsPerson } from '@deephaven/icons'; import { Icon } from '@adobe/react-spectrum'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { sampleSectionIdAndClasses } from './utils'; +import { generateNormalizedItems, sampleSectionIdAndClasses } from './utils'; // Generate enough items to require scrolling -const items = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz' - .split('') - .map((key, i) => ({ - key, - item: { key: (i + 1) * 100, content: `${key}${key}${key}` }, - })); +const items = [...generateNormalizedItems(52)]; function PersonIcon(): JSX.Element { return ( diff --git a/packages/code-studio/src/styleguide/__snapshots__/utils.test.ts.snap b/packages/code-studio/src/styleguide/__snapshots__/utils.test.ts.snap new file mode 100644 index 0000000000..a9964ea190 --- /dev/null +++ b/packages/code-studio/src/styleguide/__snapshots__/utils.test.ts.snap @@ -0,0 +1,706 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`generateNormalizedItems should generate normalized items 1`] = ` +[ + { + "item": { + "content": "AAA", + "key": 100, + }, + "key": "A", + }, + { + "item": { + "content": "BBB", + "key": 200, + }, + "key": "B", + }, + { + "item": { + "content": "CCC", + "key": 300, + }, + "key": "C", + }, + { + "item": { + "content": "DDD", + "key": 400, + }, + "key": "D", + }, + { + "item": { + "content": "EEE", + "key": 500, + }, + "key": "E", + }, + { + "item": { + "content": "FFF", + "key": 600, + }, + "key": "F", + }, + { + "item": { + "content": "GGG", + "key": 700, + }, + "key": "G", + }, + { + "item": { + "content": "HHH", + "key": 800, + }, + "key": "H", + }, + { + "item": { + "content": "III", + "key": 900, + }, + "key": "I", + }, + { + "item": { + "content": "JJJ", + "key": 1000, + }, + "key": "J", + }, + { + "item": { + "content": "KKK", + "key": 1100, + }, + "key": "K", + }, + { + "item": { + "content": "LLL", + "key": 1200, + }, + "key": "L", + }, + { + "item": { + "content": "MMM", + "key": 1300, + }, + "key": "M", + }, + { + "item": { + "content": "NNN", + "key": 1400, + }, + "key": "N", + }, + { + "item": { + "content": "OOO", + "key": 1500, + }, + "key": "O", + }, + { + "item": { + "content": "PPP", + "key": 1600, + }, + "key": "P", + }, + { + "item": { + "content": "QQQ", + "key": 1700, + }, + "key": "Q", + }, + { + "item": { + "content": "RRR", + "key": 1800, + }, + "key": "R", + }, + { + "item": { + "content": "SSS", + "key": 1900, + }, + "key": "S", + }, + { + "item": { + "content": "TTT", + "key": 2000, + }, + "key": "T", + }, + { + "item": { + "content": "UUU", + "key": 2100, + }, + "key": "U", + }, + { + "item": { + "content": "VVV", + "key": 2200, + }, + "key": "V", + }, + { + "item": { + "content": "WWW", + "key": 2300, + }, + "key": "W", + }, + { + "item": { + "content": "XXX", + "key": 2400, + }, + "key": "X", + }, + { + "item": { + "content": "YYY", + "key": 2500, + }, + "key": "Y", + }, + { + "item": { + "content": "ZZZ", + "key": 2600, + }, + "key": "Z", + }, + { + "item": { + "content": "aaa", + "key": 2700, + }, + "key": "a", + }, + { + "item": { + "content": "bbb", + "key": 2800, + }, + "key": "b", + }, + { + "item": { + "content": "ccc", + "key": 2900, + }, + "key": "c", + }, + { + "item": { + "content": "ddd", + "key": 3000, + }, + "key": "d", + }, + { + "item": { + "content": "eee", + "key": 3100, + }, + "key": "e", + }, + { + "item": { + "content": "fff", + "key": 3200, + }, + "key": "f", + }, + { + "item": { + "content": "ggg", + "key": 3300, + }, + "key": "g", + }, + { + "item": { + "content": "hhh", + "key": 3400, + }, + "key": "h", + }, + { + "item": { + "content": "iii", + "key": 3500, + }, + "key": "i", + }, + { + "item": { + "content": "jjj", + "key": 3600, + }, + "key": "j", + }, + { + "item": { + "content": "kkk", + "key": 3700, + }, + "key": "k", + }, + { + "item": { + "content": "lll", + "key": 3800, + }, + "key": "l", + }, + { + "item": { + "content": "mmm", + "key": 3900, + }, + "key": "m", + }, + { + "item": { + "content": "nnn", + "key": 4000, + }, + "key": "n", + }, + { + "item": { + "content": "ooo", + "key": 4100, + }, + "key": "o", + }, + { + "item": { + "content": "ppp", + "key": 4200, + }, + "key": "p", + }, + { + "item": { + "content": "qqq", + "key": 4300, + }, + "key": "q", + }, + { + "item": { + "content": "rrr", + "key": 4400, + }, + "key": "r", + }, + { + "item": { + "content": "sss", + "key": 4500, + }, + "key": "s", + }, + { + "item": { + "content": "ttt", + "key": 4600, + }, + "key": "t", + }, + { + "item": { + "content": "uuu", + "key": 4700, + }, + "key": "u", + }, + { + "item": { + "content": "vvv", + "key": 4800, + }, + "key": "v", + }, + { + "item": { + "content": "www", + "key": 4900, + }, + "key": "w", + }, + { + "item": { + "content": "xxx", + "key": 5000, + }, + "key": "x", + }, + { + "item": { + "content": "yyy", + "key": 5100, + }, + "key": "y", + }, + { + "item": { + "content": "zzz", + "key": 5200, + }, + "key": "z", + }, + { + "item": { + "content": "AAA1", + "key": 5300, + }, + "key": "A1", + }, + { + "item": { + "content": "BBB1", + "key": 5400, + }, + "key": "B1", + }, + { + "item": { + "content": "CCC1", + "key": 5500, + }, + "key": "C1", + }, + { + "item": { + "content": "DDD1", + "key": 5600, + }, + "key": "D1", + }, + { + "item": { + "content": "EEE1", + "key": 5700, + }, + "key": "E1", + }, + { + "item": { + "content": "FFF1", + "key": 5800, + }, + "key": "F1", + }, + { + "item": { + "content": "GGG1", + "key": 5900, + }, + "key": "G1", + }, + { + "item": { + "content": "HHH1", + "key": 6000, + }, + "key": "H1", + }, + { + "item": { + "content": "III1", + "key": 6100, + }, + "key": "I1", + }, + { + "item": { + "content": "JJJ1", + "key": 6200, + }, + "key": "J1", + }, + { + "item": { + "content": "KKK1", + "key": 6300, + }, + "key": "K1", + }, + { + "item": { + "content": "LLL1", + "key": 6400, + }, + "key": "L1", + }, + { + "item": { + "content": "MMM1", + "key": 6500, + }, + "key": "M1", + }, + { + "item": { + "content": "NNN1", + "key": 6600, + }, + "key": "N1", + }, + { + "item": { + "content": "OOO1", + "key": 6700, + }, + "key": "O1", + }, + { + "item": { + "content": "PPP1", + "key": 6800, + }, + "key": "P1", + }, + { + "item": { + "content": "QQQ1", + "key": 6900, + }, + "key": "Q1", + }, + { + "item": { + "content": "RRR1", + "key": 7000, + }, + "key": "R1", + }, + { + "item": { + "content": "SSS1", + "key": 7100, + }, + "key": "S1", + }, + { + "item": { + "content": "TTT1", + "key": 7200, + }, + "key": "T1", + }, + { + "item": { + "content": "UUU1", + "key": 7300, + }, + "key": "U1", + }, + { + "item": { + "content": "VVV1", + "key": 7400, + }, + "key": "V1", + }, + { + "item": { + "content": "WWW1", + "key": 7500, + }, + "key": "W1", + }, + { + "item": { + "content": "XXX1", + "key": 7600, + }, + "key": "X1", + }, + { + "item": { + "content": "YYY1", + "key": 7700, + }, + "key": "Y1", + }, + { + "item": { + "content": "ZZZ1", + "key": 7800, + }, + "key": "Z1", + }, + { + "item": { + "content": "aaa1", + "key": 7900, + }, + "key": "a1", + }, + { + "item": { + "content": "bbb1", + "key": 8000, + }, + "key": "b1", + }, + { + "item": { + "content": "ccc1", + "key": 8100, + }, + "key": "c1", + }, + { + "item": { + "content": "ddd1", + "key": 8200, + }, + "key": "d1", + }, + { + "item": { + "content": "eee1", + "key": 8300, + }, + "key": "e1", + }, + { + "item": { + "content": "fff1", + "key": 8400, + }, + "key": "f1", + }, + { + "item": { + "content": "ggg1", + "key": 8500, + }, + "key": "g1", + }, + { + "item": { + "content": "hhh1", + "key": 8600, + }, + "key": "h1", + }, + { + "item": { + "content": "iii1", + "key": 8700, + }, + "key": "i1", + }, + { + "item": { + "content": "jjj1", + "key": 8800, + }, + "key": "j1", + }, + { + "item": { + "content": "kkk1", + "key": 8900, + }, + "key": "k1", + }, + { + "item": { + "content": "lll1", + "key": 9000, + }, + "key": "l1", + }, + { + "item": { + "content": "mmm1", + "key": 9100, + }, + "key": "m1", + }, + { + "item": { + "content": "nnn1", + "key": 9200, + }, + "key": "n1", + }, + { + "item": { + "content": "ooo1", + "key": 9300, + }, + "key": "o1", + }, + { + "item": { + "content": "ppp1", + "key": 9400, + }, + "key": "p1", + }, + { + "item": { + "content": "qqq1", + "key": 9500, + }, + "key": "q1", + }, + { + "item": { + "content": "rrr1", + "key": 9600, + }, + "key": "r1", + }, + { + "item": { + "content": "sss1", + "key": 9700, + }, + "key": "s1", + }, + { + "item": { + "content": "ttt1", + "key": 9800, + }, + "key": "t1", + }, + { + "item": { + "content": "uuu1", + "key": 9900, + }, + "key": "u1", + }, + { + "item": { + "content": "vvv1", + "key": 10000, + }, + "key": "v1", + }, +] +`; diff --git a/packages/code-studio/src/styleguide/utils.test.ts b/packages/code-studio/src/styleguide/utils.test.ts index fdc3f3ad85..0c3e973c7e 100644 --- a/packages/code-studio/src/styleguide/utils.test.ts +++ b/packages/code-studio/src/styleguide/utils.test.ts @@ -1,8 +1,16 @@ import { + generateNormalizedItems, sampleSectionIdAndClasses, sampleSectionIdAndClassesSpectrum, } from './utils'; +describe('generateNormalizedItems', () => { + it('should generate normalized items', () => { + const actual = [...generateNormalizedItems(100)]; + expect(actual).toMatchSnapshot(); + }); +}); + describe('sampleSectionIdAndClasses', () => { it('should return id and className', () => { const actual = sampleSectionIdAndClasses('some-id', [ diff --git a/packages/code-studio/src/styleguide/utils.ts b/packages/code-studio/src/styleguide/utils.ts index a256f79b4c..7e7f9087e3 100644 --- a/packages/code-studio/src/styleguide/utils.ts +++ b/packages/code-studio/src/styleguide/utils.ts @@ -1,9 +1,39 @@ import cl from 'classnames'; import { useCallback, useState } from 'react'; +import { NormalizedItem } from '@deephaven/components'; export const HIDE_FROM_E2E_TESTS_CLASS = 'hide-from-e2e-tests'; export const SAMPLE_SECTION_CLASS = 'sample-section'; +/** + * Generate a given number of NormalizedItems. + * @param count The number of items to generate + */ +export function* generateNormalizedItems( + count: number +): Generator<NormalizedItem> { + const letters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'; + const len = letters.length; + + for (let i = 0; i < count; i += 1) { + const charI = i % len; + let suffix = String(Math.floor(i / len)); + if (suffix === '0') { + suffix = ''; + } + const letter = letters[charI]; + const key = `${letter}${suffix}`; + + yield { + key, + item: { + key: (i + 1) * 100, + content: `${letter}${letter}${letter}${suffix}`, + }, + }; + } +} + /** * Pseudo random number generator with seed so we get reproducible output. * This is necessary in order for e2e tests to work. From 157e46f4d22da6835f1cb958c0b3f6dda353c0bb Mon Sep 17 00:00:00 2001 From: Brian Ingles <brianingles@deephaven.io> Date: Thu, 4 Apr 2024 12:43:16 -0500 Subject: [PATCH 11/41] comments (#1909) --- packages/components/src/spectrum/ItemContent.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/components/src/spectrum/ItemContent.tsx b/packages/components/src/spectrum/ItemContent.tsx index d5c92180d4..6824daddd0 100644 --- a/packages/components/src/spectrum/ItemContent.tsx +++ b/packages/components/src/spectrum/ItemContent.tsx @@ -10,9 +10,9 @@ 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'; +import stylesCommon from '../SpectrumComponent.module.scss'; export interface ItemContentProps { children: ReactNode; @@ -20,8 +20,8 @@ export interface ItemContentProps { } /** - * Picker item content. Text content will be wrapped in a Spectrum Text - * component with ellipsis overflow handling. If text content overflow and + * Item content. Text content will be wrapped in a Spectrum Text + * component with ellipsis overflow handling. If text content overflows and * tooltipOptions are provided a tooltip will be displayed when hovering over * the item content. */ @@ -41,7 +41,7 @@ export function ItemContent({ /** * Whenever a `Text` component renders, see if the content is overflowing so - * we can render a tooltip. + * we know whether to render a tooltip showing the full content or not. */ const checkOverflow = useCallback( (ref: DOMRefValue<HTMLSpanElement> | null) => { From 85f87c3423c2881444e12a6eb4b474d848e305eb Mon Sep 17 00:00:00 2001 From: Brian Ingles <brianingles@deephaven.io> Date: Thu, 4 Apr 2024 12:46:08 -0500 Subject: [PATCH 12/41] Renamed callback ref (#1909) --- packages/components/src/spectrum/ItemContent.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/components/src/spectrum/ItemContent.tsx b/packages/components/src/spectrum/ItemContent.tsx index 6824daddd0..8aea897c47 100644 --- a/packages/components/src/spectrum/ItemContent.tsx +++ b/packages/components/src/spectrum/ItemContent.tsx @@ -43,7 +43,7 @@ export function ItemContent({ * Whenever a `Text` component renders, see if the content is overflowing so * we know whether to render a tooltip showing the full content or not. */ - const checkOverflow = useCallback( + const checkTextOverflowRef = useCallback( (ref: DOMRefValue<HTMLSpanElement> | null) => { const el = ref?.UNSAFE_getDOMNode(); @@ -82,7 +82,7 @@ export function ItemContent({ isElementOfType(el, Text) ? cloneElement(el, { ...el.props, - ref: checkOverflow, + ref: checkTextOverflowRef, UNSAFE_className: cl( el.props.UNSAFE_className, stylesCommon.spectrumEllipsis @@ -95,7 +95,7 @@ export function ItemContent({ if (typeof content === 'string' || typeof content === 'number') { content = ( <Text - ref={checkOverflow} + ref={checkTextOverflowRef} UNSAFE_className={stylesCommon.spectrumEllipsis} > {content} From 9acfc308179978aa614922332125a7eb1f49dcba Mon Sep 17 00:00:00 2001 From: Brian Ingles <brianingles@deephaven.io> Date: Thu, 4 Apr 2024 13:01:06 -0500 Subject: [PATCH 13/41] useCheckOverflowRef hook (#1909) --- .../components/src/spectrum/ItemContent.tsx | 34 ++++---------- packages/react-hooks/src/index.ts | 1 + .../react-hooks/src/useCheckOverflowRef.ts | 44 +++++++++++++++++++ 3 files changed, 54 insertions(+), 25 deletions(-) create mode 100644 packages/react-hooks/src/useCheckOverflowRef.ts diff --git a/packages/components/src/spectrum/ItemContent.tsx b/packages/components/src/spectrum/ItemContent.tsx index 8aea897c47..e085b8372a 100644 --- a/packages/components/src/spectrum/ItemContent.tsx +++ b/packages/components/src/spectrum/ItemContent.tsx @@ -3,12 +3,10 @@ import { cloneElement, isValidElement, ReactNode, - useCallback, useState, } from 'react'; -import { DOMRefValue } from '@react-types/shared'; import cl from 'classnames'; -import { isElementOfType } from '@deephaven/react-hooks'; +import { isElementOfType, useCheckOverflowRef } from '@deephaven/react-hooks'; import { Text } from './Text'; import { TooltipOptions } from './utils'; import ItemTooltip from './ItemTooltip'; @@ -29,35 +27,21 @@ export function ItemContent({ children: content, tooltipOptions, }: ItemContentProps): JSX.Element | null { + const { + isOverflowing: isTextOverflowing, + ref: checkTextOverflowRef, + reset: resetIsOverflowing, + } = useCheckOverflowRef(); + 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); + resetIsOverflowing(); } - /** - * Whenever a `Text` component renders, see if the content is overflowing so - * we know whether to render a tooltip showing the full content or not. - */ - const checkTextOverflowRef = 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; } @@ -105,7 +89,7 @@ export function ItemContent({ /* eslint-enable no-param-reassign */ const tooltip = - tooltipOptions == null || !isOverflowing ? null : ( + tooltipOptions == null || !isTextOverflowing ? null : ( <ItemTooltip options={tooltipOptions}>{content}</ItemTooltip> ); diff --git a/packages/react-hooks/src/index.ts b/packages/react-hooks/src/index.ts index 1555908b88..28f1f8bbe4 100644 --- a/packages/react-hooks/src/index.ts +++ b/packages/react-hooks/src/index.ts @@ -3,6 +3,7 @@ export * from './SelectionUtils'; export * from './SpectrumUtils'; export * from './useAsyncInterval'; export * from './useCallbackWithAction'; +export * from './useCheckOverflowRef'; export { default as useContextOrThrow } from './useContextOrThrow'; export * from './useDebouncedCallback'; export * from './useDelay'; diff --git a/packages/react-hooks/src/useCheckOverflowRef.ts b/packages/react-hooks/src/useCheckOverflowRef.ts new file mode 100644 index 0000000000..48f00df370 --- /dev/null +++ b/packages/react-hooks/src/useCheckOverflowRef.ts @@ -0,0 +1,44 @@ +import { useCallback, useState } from 'react'; +import type { DOMRefValue } from '@react-types/shared'; + +export interface CheckOverflowRefResult { + isOverflowing: boolean; + ref: <T extends HTMLElement>(elRef: DOMRefValue<T> | null) => void; + reset: () => void; +} + +export function useCheckOverflowRef(): CheckOverflowRefResult { + const [isOverflowing, setIsOverflowing] = useState(false); + + /** + * Whenever a Spectrum `DOMRefValue` component renders, see if the content is + * overflowing so we know whether to render a tooltip showing the full content + * or not. + */ + const ref = useCallback( + <T extends HTMLElement>(elRef: DOMRefValue<T> | null) => { + const el = elRef?.UNSAFE_getDOMNode(); + + if (el == null) { + return; + } + + if (el.scrollWidth > el.offsetWidth) { + setIsOverflowing(true); + } + }, + [] + ); + + const reset = useCallback(() => { + setIsOverflowing(false); + }, []); + + return { + isOverflowing, + ref, + reset, + }; +} + +export default useCheckOverflowRef; From 4b2cd06a2c5a6ff770b3af70e39d852f98c7518c Mon Sep 17 00:00:00 2001 From: Brian Ingles <brianingles@deephaven.io> Date: Thu, 4 Apr 2024 13:04:14 -0500 Subject: [PATCH 14/41] renames (#1909) --- packages/components/src/spectrum/ItemContent.tsx | 8 ++++---- packages/react-hooks/src/useCheckOverflowRef.ts | 14 ++++++++------ 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/packages/components/src/spectrum/ItemContent.tsx b/packages/components/src/spectrum/ItemContent.tsx index e085b8372a..fa85d9bb5d 100644 --- a/packages/components/src/spectrum/ItemContent.tsx +++ b/packages/components/src/spectrum/ItemContent.tsx @@ -28,9 +28,9 @@ export function ItemContent({ tooltipOptions, }: ItemContentProps): JSX.Element | null { const { + checkOverflowRef, isOverflowing: isTextOverflowing, - ref: checkTextOverflowRef, - reset: resetIsOverflowing, + resetIsOverflowing, } = useCheckOverflowRef(); const [previousContent, setPreviousContent] = useState(content); @@ -66,7 +66,7 @@ export function ItemContent({ isElementOfType(el, Text) ? cloneElement(el, { ...el.props, - ref: checkTextOverflowRef, + ref: checkOverflowRef, UNSAFE_className: cl( el.props.UNSAFE_className, stylesCommon.spectrumEllipsis @@ -79,7 +79,7 @@ export function ItemContent({ if (typeof content === 'string' || typeof content === 'number') { content = ( <Text - ref={checkTextOverflowRef} + ref={checkOverflowRef} UNSAFE_className={stylesCommon.spectrumEllipsis} > {content} diff --git a/packages/react-hooks/src/useCheckOverflowRef.ts b/packages/react-hooks/src/useCheckOverflowRef.ts index 48f00df370..aefabd6478 100644 --- a/packages/react-hooks/src/useCheckOverflowRef.ts +++ b/packages/react-hooks/src/useCheckOverflowRef.ts @@ -2,9 +2,11 @@ import { useCallback, useState } from 'react'; import type { DOMRefValue } from '@react-types/shared'; export interface CheckOverflowRefResult { + checkOverflowRef: <T extends HTMLElement>( + elRef: DOMRefValue<T> | null + ) => void; isOverflowing: boolean; - ref: <T extends HTMLElement>(elRef: DOMRefValue<T> | null) => void; - reset: () => void; + resetIsOverflowing: () => void; } export function useCheckOverflowRef(): CheckOverflowRefResult { @@ -15,7 +17,7 @@ export function useCheckOverflowRef(): CheckOverflowRefResult { * overflowing so we know whether to render a tooltip showing the full content * or not. */ - const ref = useCallback( + const checkOverflowRef = useCallback( <T extends HTMLElement>(elRef: DOMRefValue<T> | null) => { const el = elRef?.UNSAFE_getDOMNode(); @@ -30,14 +32,14 @@ export function useCheckOverflowRef(): CheckOverflowRefResult { [] ); - const reset = useCallback(() => { + const resetIsOverflowing = useCallback(() => { setIsOverflowing(false); }, []); return { isOverflowing, - ref, - reset, + checkOverflowRef, + resetIsOverflowing, }; } From ebf1598baa4bf02724c462919a74c86600f56b96 Mon Sep 17 00:00:00 2001 From: Brian Ingles <brianingles@deephaven.io> Date: Thu, 4 Apr 2024 13:07:39 -0500 Subject: [PATCH 15/41] Changed "ref" naming to "callback" (#1909) --- packages/components/src/spectrum/ItemContent.tsx | 15 ++++++--------- packages/react-hooks/src/index.ts | 2 +- ...useCheckOverflowRef.ts => useCheckOverflow.ts} | 14 ++++++-------- 3 files changed, 13 insertions(+), 18 deletions(-) rename packages/react-hooks/src/{useCheckOverflowRef.ts => useCheckOverflow.ts} (73%) diff --git a/packages/components/src/spectrum/ItemContent.tsx b/packages/components/src/spectrum/ItemContent.tsx index fa85d9bb5d..729fdcf624 100644 --- a/packages/components/src/spectrum/ItemContent.tsx +++ b/packages/components/src/spectrum/ItemContent.tsx @@ -6,7 +6,7 @@ import { useState, } from 'react'; import cl from 'classnames'; -import { isElementOfType, useCheckOverflowRef } from '@deephaven/react-hooks'; +import { isElementOfType, useCheckOverflow } from '@deephaven/react-hooks'; import { Text } from './Text'; import { TooltipOptions } from './utils'; import ItemTooltip from './ItemTooltip'; @@ -27,11 +27,8 @@ export function ItemContent({ children: content, tooltipOptions, }: ItemContentProps): JSX.Element | null { - const { - checkOverflowRef, - isOverflowing: isTextOverflowing, - resetIsOverflowing, - } = useCheckOverflowRef(); + const { checkOverflow, isOverflowing, resetIsOverflowing } = + useCheckOverflow(); const [previousContent, setPreviousContent] = useState(content); @@ -66,7 +63,7 @@ export function ItemContent({ isElementOfType(el, Text) ? cloneElement(el, { ...el.props, - ref: checkOverflowRef, + ref: checkOverflow, UNSAFE_className: cl( el.props.UNSAFE_className, stylesCommon.spectrumEllipsis @@ -79,7 +76,7 @@ export function ItemContent({ if (typeof content === 'string' || typeof content === 'number') { content = ( <Text - ref={checkOverflowRef} + ref={checkOverflow} UNSAFE_className={stylesCommon.spectrumEllipsis} > {content} @@ -89,7 +86,7 @@ export function ItemContent({ /* eslint-enable no-param-reassign */ const tooltip = - tooltipOptions == null || !isTextOverflowing ? null : ( + tooltipOptions == null || !isOverflowing ? null : ( <ItemTooltip options={tooltipOptions}>{content}</ItemTooltip> ); diff --git a/packages/react-hooks/src/index.ts b/packages/react-hooks/src/index.ts index 28f1f8bbe4..bec624de9e 100644 --- a/packages/react-hooks/src/index.ts +++ b/packages/react-hooks/src/index.ts @@ -3,7 +3,7 @@ export * from './SelectionUtils'; export * from './SpectrumUtils'; export * from './useAsyncInterval'; export * from './useCallbackWithAction'; -export * from './useCheckOverflowRef'; +export * from './useCheckOverflow'; export { default as useContextOrThrow } from './useContextOrThrow'; export * from './useDebouncedCallback'; export * from './useDelay'; diff --git a/packages/react-hooks/src/useCheckOverflowRef.ts b/packages/react-hooks/src/useCheckOverflow.ts similarity index 73% rename from packages/react-hooks/src/useCheckOverflowRef.ts rename to packages/react-hooks/src/useCheckOverflow.ts index aefabd6478..b998f46975 100644 --- a/packages/react-hooks/src/useCheckOverflowRef.ts +++ b/packages/react-hooks/src/useCheckOverflow.ts @@ -1,15 +1,13 @@ import { useCallback, useState } from 'react'; import type { DOMRefValue } from '@react-types/shared'; -export interface CheckOverflowRefResult { - checkOverflowRef: <T extends HTMLElement>( - elRef: DOMRefValue<T> | null - ) => void; +export interface CheckOverflowResult { + checkOverflow: <T extends HTMLElement>(elRef: DOMRefValue<T> | null) => void; isOverflowing: boolean; resetIsOverflowing: () => void; } -export function useCheckOverflowRef(): CheckOverflowRefResult { +export function useCheckOverflow(): CheckOverflowResult { const [isOverflowing, setIsOverflowing] = useState(false); /** @@ -17,7 +15,7 @@ export function useCheckOverflowRef(): CheckOverflowRefResult { * overflowing so we know whether to render a tooltip showing the full content * or not. */ - const checkOverflowRef = useCallback( + const checkOverflow = useCallback( <T extends HTMLElement>(elRef: DOMRefValue<T> | null) => { const el = elRef?.UNSAFE_getDOMNode(); @@ -38,9 +36,9 @@ export function useCheckOverflowRef(): CheckOverflowRefResult { return { isOverflowing, - checkOverflowRef, + checkOverflow, resetIsOverflowing, }; } -export default useCheckOverflowRef; +export default useCheckOverflow; From 55d0fc2430ee829e0a0bbdb86a4ab1ec5c3482a1 Mon Sep 17 00:00:00 2001 From: Brian Ingles <brianingles@deephaven.io> Date: Thu, 4 Apr 2024 13:20:14 -0500 Subject: [PATCH 16/41] cleanup (#1909) --- packages/react-hooks/src/useCheckOverflow.ts | 26 +++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/packages/react-hooks/src/useCheckOverflow.ts b/packages/react-hooks/src/useCheckOverflow.ts index b998f46975..9b33c3eb8e 100644 --- a/packages/react-hooks/src/useCheckOverflow.ts +++ b/packages/react-hooks/src/useCheckOverflow.ts @@ -2,18 +2,37 @@ import { useCallback, useState } from 'react'; import type { DOMRefValue } from '@react-types/shared'; export interface CheckOverflowResult { + /** + * Callback to check if a Spectrum `DOMRefValue` is overflowing. If an + * overflowing value is passed, `isOverflowing` will be set to true. Note that + * calling again with a non-overflowing value will *not* reset the state. + * Instead `resetIsOverflowing` must be called explicitly. This is to allow + * multiple `DOMRefValue`s to be checked and `isOverflowing` to remain `true` + * if at least one of them is overflowing. + */ checkOverflow: <T extends HTMLElement>(elRef: DOMRefValue<T> | null) => void; + + /** + * Will be set to true whenever `checkOverflow` is called with an overflowing + * `DOMRefValue`. It will remain `true` until `resetIsOverflowing` is called. + * Default state is `false`. + */ isOverflowing: boolean; + + /** Reset `isOverflowing` to false */ resetIsOverflowing: () => void; } +/** + * Provides a callback to check a Spectrum `DOMRefValue` for overflow. If + * overflow is detected, `isOverflowing` will be set to `true` until reset by + * calling `resetIsOverflowing`. + */ export function useCheckOverflow(): CheckOverflowResult { const [isOverflowing, setIsOverflowing] = useState(false); /** - * Whenever a Spectrum `DOMRefValue` component renders, see if the content is - * overflowing so we know whether to render a tooltip showing the full content - * or not. + * Check if a Spectrum `DOMRefValue` is overflowing. */ const checkOverflow = useCallback( <T extends HTMLElement>(elRef: DOMRefValue<T> | null) => { @@ -30,6 +49,7 @@ export function useCheckOverflow(): CheckOverflowResult { [] ); + /** Reset `isOverflowing` to false */ const resetIsOverflowing = useCallback(() => { setIsOverflowing(false); }, []); From 9f4c1fbf2b404899ad1d88895ab49eea8bfde765 Mon Sep 17 00:00:00 2001 From: Brian Ingles <brianingles@deephaven.io> Date: Thu, 4 Apr 2024 13:59:32 -0500 Subject: [PATCH 17/41] Cleanup (#1909) --- packages/components/src/spectrum/ItemContent.tsx | 2 +- packages/components/src/spectrum/ItemTooltip.tsx | 10 ++++++---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/packages/components/src/spectrum/ItemContent.tsx b/packages/components/src/spectrum/ItemContent.tsx index 729fdcf624..1526d3ed64 100644 --- a/packages/components/src/spectrum/ItemContent.tsx +++ b/packages/components/src/spectrum/ItemContent.tsx @@ -59,7 +59,7 @@ export function ItemContent({ // <Text>Some Label</Text> // <Text slot="description">Some Description</Text> // </Item> - content = Children.map(content, (el, i) => + content = Children.map(content, el => isElementOfType(el, Text) ? cloneElement(el, { ...el.props, diff --git a/packages/components/src/spectrum/ItemTooltip.tsx b/packages/components/src/spectrum/ItemTooltip.tsx index d2f031457d..a86588d2ff 100644 --- a/packages/components/src/spectrum/ItemTooltip.tsx +++ b/packages/components/src/spectrum/ItemTooltip.tsx @@ -10,17 +10,19 @@ export interface ItemTooltipProps { options: TooltipOptions; } +/** + * Tooltip for `<Item>` content. + */ 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}> + {/* Multiple children scenarios include a `<Text>` node for the label + and at least 1 of an optional icon or `<Text slot="description">` node. + In such cases we only show the label and description `<Text>` nodes. */} <Flex direction="column" alignItems="start"> {children.filter(node => isElementOfType(node, Text))} </Flex> From c964da7ad0669aa9d3b7a4160e3d56488f008bcf Mon Sep 17 00:00:00 2001 From: Brian Ingles <brianingles@deephaven.io> Date: Mon, 8 Apr 2024 14:54:50 -0500 Subject: [PATCH 18/41] ItemSelection type (#1909) --- packages/components/src/spectrum/listView/ListView.tsx | 5 +++-- packages/components/src/spectrum/utils/itemUtils.ts | 2 ++ .../src/spectrum/utils/useStringifiedMultiSelection.ts | 9 +++++++-- 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/packages/components/src/spectrum/listView/ListView.tsx b/packages/components/src/spectrum/listView/ListView.tsx index 8d10b22ecd..d867e2b6b9 100644 --- a/packages/components/src/spectrum/listView/ListView.tsx +++ b/packages/components/src/spectrum/listView/ListView.tsx @@ -12,6 +12,7 @@ import cl from 'classnames'; import { ItemElementOrPrimitive, ItemKey, + ItemSelection, NormalizedItem, normalizeItemList, normalizeTooltipOptions, @@ -36,7 +37,7 @@ export type ListViewProps = { * `onSelectionChange`. We are renaming for better consistency with other * components. */ - onChange?: (keys: 'all' | Set<ItemKey>) => void; + onChange?: (keys: ItemSelection) => void; /** Handler that is called when the picker is scrolled. */ onScroll?: (event: Event) => void; @@ -45,7 +46,7 @@ export type ListViewProps = { * Handler that is called when the selection changes. * @deprecated Use `onChange` instead */ - onSelectionChange?: (keys: 'all' | Set<ItemKey>) => void; + onSelectionChange?: (keys: ItemSelection) => void; } & Omit< SpectrumListViewProps<NormalizedItem>, | 'children' diff --git a/packages/components/src/spectrum/utils/itemUtils.ts b/packages/components/src/spectrum/utils/itemUtils.ts index eb7065b94b..ddbc143693 100644 --- a/packages/components/src/spectrum/utils/itemUtils.ts +++ b/packages/components/src/spectrum/utils/itemUtils.ts @@ -33,6 +33,8 @@ export type ItemOrSection = ItemElementOrPrimitive | SectionElement; */ export type ItemKey = Key | boolean; +export type ItemSelection = 'all' | Set<ItemKey>; + /** * Augment the Spectrum selection change handler type to include boolean keys. * Spectrum components already supports this, but the built in types don't diff --git a/packages/components/src/spectrum/utils/useStringifiedMultiSelection.ts b/packages/components/src/spectrum/utils/useStringifiedMultiSelection.ts index af0619f62c..7104638f2b 100644 --- a/packages/components/src/spectrum/utils/useStringifiedMultiSelection.ts +++ b/packages/components/src/spectrum/utils/useStringifiedMultiSelection.ts @@ -1,5 +1,10 @@ import { Key, useCallback, useMemo } from 'react'; -import { getItemKey, ItemKey, NormalizedItem } from './itemUtils'; +import { + getItemKey, + ItemKey, + ItemSelection, + NormalizedItem, +} from './itemUtils'; function toStringKeySet( keys?: 'all' | Iterable<ItemKey> @@ -22,7 +27,7 @@ export interface UseStringifiedMultiSelectionOptions { * `onSelectionChange`. We are renaming for better consistency with other * components. */ - onChange?: (keys: 'all' | Set<ItemKey>) => void; + onChange?: (keys: ItemSelection) => void; } export interface UseStringifiedMultiSelectionResult { From a111d5aaaeda458bc2bf673544b087e51c879f79 Mon Sep 17 00:00:00 2001 From: Brian Ingles <brianingles@deephaven.io> Date: Tue, 9 Apr 2024 08:55:50 -0500 Subject: [PATCH 19/41] Item heights (#1909) --- .../jsapi-components/src/spectrum/ListView.tsx | 8 ++++++-- packages/utils/src/UIConstants.ts | 17 ++++++++++++++++- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/packages/jsapi-components/src/spectrum/ListView.tsx b/packages/jsapi-components/src/spectrum/ListView.tsx index 49d8a166bd..c01ee194f9 100644 --- a/packages/jsapi-components/src/spectrum/ListView.tsx +++ b/packages/jsapi-components/src/spectrum/ListView.tsx @@ -1,3 +1,4 @@ +import { useProvider } from '@adobe/react-spectrum'; import { ListView as ListViewBase, ListViewProps as ListViewPropsBase, @@ -5,7 +6,7 @@ import { } from '@deephaven/components'; import { dh as DhType } from '@deephaven/jsapi-types'; import { Settings } from '@deephaven/jsapi-utils'; -import { LIST_VIEW_ROW_HEIGHT } from '@deephaven/utils'; +import { LIST_VIEW_ROW_HEIGHTS } from '@deephaven/utils'; import useFormatter from '../useFormatter'; import useViewportData from '../useViewportData'; import { useItemRowDeserializer } from './utils'; @@ -29,6 +30,9 @@ export function ListView({ settings, ...props }: ListViewProps): JSX.Element { + const { scale } = useProvider(); + const itemHeight = LIST_VIEW_ROW_HEIGHTS[props.density ?? 'regular'][scale]; + const { getFormattedString: formatValue } = useFormatter(settings); const deserializeRow = useItemRowDeserializer({ @@ -44,7 +48,7 @@ export function ListView({ >({ reuseItemsOnTableResize: true, table, - itemHeight: LIST_VIEW_ROW_HEIGHT, + itemHeight, deserializeRow, }); diff --git a/packages/utils/src/UIConstants.ts b/packages/utils/src/UIConstants.ts index 3be0c189ef..a7cf22fabf 100644 --- a/packages/utils/src/UIConstants.ts +++ b/packages/utils/src/UIConstants.ts @@ -2,7 +2,6 @@ export const ACTION_ICON_HEIGHT = 24; export const COMBO_BOX_ITEM_HEIGHT = 32; export const COMBO_BOX_TOP_OFFSET = 4; export const ITEM_KEY_PREFIX = 'DH_ITEM_KEY'; -export const LIST_VIEW_ROW_HEIGHT = 32; export const PICKER_ITEM_HEIGHT = 32; export const PICKER_TOP_OFFSET = 4; export const TABLE_ROW_HEIGHT = 33; @@ -12,3 +11,19 @@ export const VIEWPORT_SIZE = 500; export const VIEWPORT_PADDING = 250; export const SPELLCHECK_FALSE_ATTRIBUTE = { spellCheck: false } as const; + +// Copied from https://github.com/adobe/react-spectrum/blob/b2d25ef23b827ec2427bf47b343e6dbd66326ed3/packages/%40react-spectrum/list/src/ListView.tsx#L78 +export const LIST_VIEW_ROW_HEIGHTS = { + compact: { + medium: 32, + large: 40, + }, + regular: { + medium: 40, + large: 50, + }, + spacious: { + medium: 48, + large: 60, + }, +} as const; From d488c926cc9b17b655377569d04bc0014a7c8db3 Mon Sep 17 00:00:00 2001 From: Brian Ingles <brianingles@deephaven.io> Date: Tue, 9 Apr 2024 08:56:22 -0500 Subject: [PATCH 20/41] Only render ListView when non-zero height (#1909) --- package-lock.json | 2 + .../src/spectrum/listView/ListView.tsx | 49 ++++++++++++++----- packages/jsapi-components/package.json | 1 + packages/react-hooks/src/index.ts | 1 + packages/react-hooks/src/useContentRect.ts | 44 +++++++++++++++++ 5 files changed, 85 insertions(+), 12 deletions(-) create mode 100644 packages/react-hooks/src/useContentRect.ts diff --git a/package-lock.json b/package-lock.json index a9d06f5266..acfb66a308 100644 --- a/package-lock.json +++ b/package-lock.json @@ -29360,6 +29360,7 @@ "version": "0.72.0", "license": "Apache-2.0", "dependencies": { + "@adobe/react-spectrum": "^3.34.1", "@deephaven/components": "file:../components", "@deephaven/jsapi-bootstrap": "file:../jsapi-bootstrap", "@deephaven/jsapi-types": "1.0.0-dev0.33.1", @@ -31564,6 +31565,7 @@ "@deephaven/jsapi-components": { "version": "file:packages/jsapi-components", "requires": { + "@adobe/react-spectrum": "^3.34.1", "@deephaven/components": "file:../components", "@deephaven/jsapi-bootstrap": "file:../jsapi-bootstrap", "@deephaven/jsapi-shim": "file:../jsapi-shim", diff --git a/packages/components/src/spectrum/listView/ListView.tsx b/packages/components/src/spectrum/listView/ListView.tsx index d867e2b6b9..723394f219 100644 --- a/packages/components/src/spectrum/listView/ListView.tsx +++ b/packages/components/src/spectrum/listView/ListView.tsx @@ -1,11 +1,13 @@ import { useMemo } from 'react'; import { + Flex, ListView as SpectrumListView, SpectrumListViewProps, } from '@adobe/react-spectrum'; import { EMPTY_FUNCTION } from '@deephaven/utils'; import { extractSpectrumHTMLElement, + useContentRect, useOnScrollRef, } from '@deephaven/react-hooks'; import cl from 'classnames'; @@ -68,7 +70,7 @@ export function ListView({ onScroll = EMPTY_FUNCTION, onSelectionChange, ...spectrumListViewProps -}: ListViewProps): JSX.Element { +}: ListViewProps): JSX.Element | null { const normalizedItems = useMemo( () => normalizeItemList(children), [children] @@ -96,20 +98,43 @@ export function ListView({ const scrollRef = useOnScrollRef(onScroll, extractSpectrumHTMLElement); + // Spectrum ListView crashes when it has zero height. Trac the contentRect + // of the parent container and only render the ListView when it has a height. + const { ref: contentRectRef, contentRect } = useContentRect( + extractSpectrumHTMLElement + ); + return ( - <SpectrumListView - // eslint-disable-next-line react/jsx-props-no-spreading - {...spectrumListViewProps} - ref={scrollRef} + <Flex + ref={contentRectRef} + direction="column" + flex={spectrumListViewProps.flex ?? 1} + minHeight={0} UNSAFE_className={cl('dh-list-view', UNSAFE_className)} - items={normalizedItems} - selectedKeys={selectedStringKeys} - defaultSelectedKeys={defaultSelectedStringKeys} - disabledKeys={disabledStringKeys} - onSelectionChange={onStringSelectionChange} > - {renderNormalizedItem} - </SpectrumListView> + {contentRect.height === 0 ? ( + // Ensure content has a non-zero height so that the container has a height + // whenever it is visible. This helps differentiate when the container + // height has been set to zero (e.g. when a tab is not visible) vs when + // the container height has not been constrained but has not yet rendered + // the ListView. + <> </> + ) : ( + <SpectrumListView + // eslint-disable-next-line react/jsx-props-no-spreading + {...spectrumListViewProps} + minHeight={10} + ref={scrollRef} + items={normalizedItems} + selectedKeys={selectedStringKeys} + defaultSelectedKeys={defaultSelectedStringKeys} + disabledKeys={disabledStringKeys} + onSelectionChange={onStringSelectionChange} + > + {renderNormalizedItem} + </SpectrumListView> + )} + </Flex> ); } diff --git a/packages/jsapi-components/package.json b/packages/jsapi-components/package.json index f849061bd6..ab301c3b9a 100644 --- a/packages/jsapi-components/package.json +++ b/packages/jsapi-components/package.json @@ -22,6 +22,7 @@ "build:sass": "sass --embed-sources --load-path=../../node_modules ./src:./dist" }, "dependencies": { + "@adobe/react-spectrum": "^3.34.1", "@deephaven/components": "file:../components", "@deephaven/jsapi-bootstrap": "file:../jsapi-bootstrap", "@deephaven/jsapi-types": "1.0.0-dev0.33.1", diff --git a/packages/react-hooks/src/index.ts b/packages/react-hooks/src/index.ts index bec624de9e..a3d98b2498 100644 --- a/packages/react-hooks/src/index.ts +++ b/packages/react-hooks/src/index.ts @@ -4,6 +4,7 @@ export * from './SpectrumUtils'; export * from './useAsyncInterval'; export * from './useCallbackWithAction'; export * from './useCheckOverflow'; +export * from './useContentRect'; export { default as useContextOrThrow } from './useContextOrThrow'; export * from './useDebouncedCallback'; export * from './useDelay'; diff --git a/packages/react-hooks/src/useContentRect.ts b/packages/react-hooks/src/useContentRect.ts new file mode 100644 index 0000000000..2018e2a3ab --- /dev/null +++ b/packages/react-hooks/src/useContentRect.ts @@ -0,0 +1,44 @@ +import { identityExtractHTMLElement } from '@deephaven/utils'; +import { useCallback, useRef, useState } from 'react'; +import useMappedRef from './useMappedRef'; +import useResizeObserver from './useResizeObserver'; + +export interface UseContentRectResult<T> { + contentRect: DOMRectReadOnly; + ref: (refValue: T) => void; +} + +/** + * Returns a callback ref that will track the `contentRect` of a given refValue. + * If the `contentRect` is undefined, it will be set to a new `DOMRect` with + * zeros for all dimensions. + * @param map Optional mapping function to extract an HTMLElement from the given + * refValue + * @returns Content rect and a ref callback + */ +export function useContentRect<T>( + map: (ref: T) => HTMLElement | null = identityExtractHTMLElement +): UseContentRectResult<T> { + const [contentRect, setContentRect] = useState<DOMRectReadOnly>( + new DOMRect() + ); + + const handleResize = useCallback( + ([firstEntry]: ResizeObserverEntry[], _observer: ResizeObserver): void => { + setContentRect(firstEntry?.contentRect ?? new DOMRect()); + }, + [] + ); + + const observerRef = useRef<HTMLElement>(null); + useResizeObserver(observerRef.current, handleResize); + + const ref = useMappedRef(observerRef, map); + + return { + ref, + contentRect, + }; +} + +export default useContentRect; From dac492f35b07c09f879f6775eb815c2bd9868adf Mon Sep 17 00:00:00 2001 From: Brian Ingles <brianingles@deephaven.io> Date: Tue, 9 Apr 2024 08:59:14 -0500 Subject: [PATCH 21/41] Removed minHeight (#1909) --- packages/components/src/spectrum/listView/ListView.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/components/src/spectrum/listView/ListView.tsx b/packages/components/src/spectrum/listView/ListView.tsx index 723394f219..d15442f3f7 100644 --- a/packages/components/src/spectrum/listView/ListView.tsx +++ b/packages/components/src/spectrum/listView/ListView.tsx @@ -123,7 +123,6 @@ export function ListView({ <SpectrumListView // eslint-disable-next-line react/jsx-props-no-spreading {...spectrumListViewProps} - minHeight={10} ref={scrollRef} items={normalizedItems} selectedKeys={selectedStringKeys} From f2be02b2b31c6f465442ee6c5b53f135cfe2ea5b Mon Sep 17 00:00:00 2001 From: Brian Ingles <brianingles@deephaven.io> Date: Tue, 9 Apr 2024 08:59:56 -0500 Subject: [PATCH 22/41] Removed unused arg (#1909) --- packages/react-hooks/src/useContentRect.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react-hooks/src/useContentRect.ts b/packages/react-hooks/src/useContentRect.ts index 2018e2a3ab..be26a1c6f6 100644 --- a/packages/react-hooks/src/useContentRect.ts +++ b/packages/react-hooks/src/useContentRect.ts @@ -24,7 +24,7 @@ export function useContentRect<T>( ); const handleResize = useCallback( - ([firstEntry]: ResizeObserverEntry[], _observer: ResizeObserver): void => { + ([firstEntry]: ResizeObserverEntry[]): void => { setContentRect(firstEntry?.contentRect ?? new DOMRect()); }, [] From c7ecd14d72da2eb72e22504b0c59274a3f2c5960 Mon Sep 17 00:00:00 2001 From: Brian Ingles <brianingles@deephaven.io> Date: Tue, 9 Apr 2024 10:19:09 -0500 Subject: [PATCH 23/41] useContentRect tests (#1909) --- jest.setup.ts | 15 ++++ .../react-hooks/src/useContentRect.test.ts | 70 +++++++++++++++++++ 2 files changed, 85 insertions(+) create mode 100644 packages/react-hooks/src/useContentRect.test.ts diff --git a/jest.setup.ts b/jest.setup.ts index 4bb196107b..8db731f817 100644 --- a/jest.setup.ts +++ b/jest.setup.ts @@ -50,6 +50,21 @@ Object.defineProperty(window, 'ResizeObserver', { }, }); +Object.defineProperty(window, 'DOMRect', { + value: function (x: number = 0, y: number = 0, width = 0, height = 0) { + return TestUtils.createMockProxy<DOMRect>({ + x, + y, + width, + height, + top: y, + bottom: y + height, + left: x, + right: x + width, + }); + }, +}); + Object.defineProperty(window, 'TextDecoder', { value: TextDecoder, }); diff --git a/packages/react-hooks/src/useContentRect.test.ts b/packages/react-hooks/src/useContentRect.test.ts new file mode 100644 index 0000000000..7dbf903b8b --- /dev/null +++ b/packages/react-hooks/src/useContentRect.test.ts @@ -0,0 +1,70 @@ +import { act, renderHook } from '@testing-library/react-hooks'; +import { TestUtils } from '@deephaven/utils'; +import { useContentRect } from './useContentRect'; +import useResizeObserver from './useResizeObserver'; + +jest.mock('./useResizeObserver'); + +const { asMock, createMockProxy } = TestUtils; + +beforeEach(() => { + jest.clearAllMocks(); + expect.hasAssertions(); + asMock(useResizeObserver).mockName('useResizeObserver'); +}); + +describe.each([true, false])('useContentRect - explicitMap:%s', explicitMap => { + const mock = { + refValue: document.createElement('div'), + mappedValue: document.createElement('span'), + resizeEntry: createMockProxy<ResizeObserverEntry>({ + contentRect: new DOMRect(0, 0, 100, 100), + }), + observer: createMockProxy<ResizeObserver>(), + }; + + const mockMap = explicitMap ? jest.fn(() => mock.mappedValue) : undefined; + + it('should initially return zero size contentRect', () => { + const { result } = renderHook(() => useContentRect(mockMap)); + expect(useResizeObserver).toHaveBeenCalledWith(null, expect.any(Function)); + expect(result.current.contentRect).toEqual(new DOMRect()); + }); + + it('should pass expected value to resize observer based on presence of map function', () => { + const { result, rerender } = renderHook(() => useContentRect(mockMap)); + + result.current.ref(mock.refValue); + rerender(); + + if (mockMap != null) { + expect(mockMap).toHaveBeenCalledWith(mock.refValue); + } + expect(useResizeObserver).toHaveBeenCalledWith( + mockMap == null ? mock.refValue : mock.mappedValue, + expect.any(Function) + ); + expect(result.current.contentRect).toEqual(new DOMRect()); + }); + + it.each([ + [[], new DOMRect()], + [[mock.resizeEntry], mock.resizeEntry.contentRect], + ])( + 'should update contentRect when resize observer triggers: %s', + (entries, expected) => { + const { result, rerender } = renderHook(() => useContentRect(mockMap)); + + result.current.ref(mock.refValue); + rerender(); + + const handleResize = asMock(useResizeObserver).mock.calls.at(-1)?.[1]; + + act(() => { + handleResize?.(entries, mock.observer); + }); + + expect(result.current.contentRect).toEqual(expected); + } + ); +}); From 663a965b3ce4302d29e66868fdeda3240d15c6ed Mon Sep 17 00:00:00 2001 From: Brian Ingles <brianingles@deephaven.io> Date: Tue, 9 Apr 2024 10:36:05 -0500 Subject: [PATCH 24/41] Updated comments (#1909) --- .../src/spectrum/listView/ListView.tsx | 21 ++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/packages/components/src/spectrum/listView/ListView.tsx b/packages/components/src/spectrum/listView/ListView.tsx index d15442f3f7..60668c2752 100644 --- a/packages/components/src/spectrum/listView/ListView.tsx +++ b/packages/components/src/spectrum/listView/ListView.tsx @@ -98,8 +98,9 @@ export function ListView({ const scrollRef = useOnScrollRef(onScroll, extractSpectrumHTMLElement); - // Spectrum ListView crashes when it has zero height. Trac the contentRect - // of the parent container and only render the ListView when it has a height. + // Spectrum ListView crashes when it has zero height. Track the contentRect + // of the parent container and only render the ListView when it has a non-zero + // height. const { ref: contentRectRef, contentRect } = useContentRect( extractSpectrumHTMLElement ); @@ -113,11 +114,17 @@ export function ListView({ UNSAFE_className={cl('dh-list-view', UNSAFE_className)} > {contentRect.height === 0 ? ( - // Ensure content has a non-zero height so that the container has a height - // whenever it is visible. This helps differentiate when the container - // height has been set to zero (e.g. when a tab is not visible) vs when - // the container height has not been constrained but has not yet rendered - // the ListView. + // Use to ensure content has a non-zero height. This ensures the + // container will also have a non-zero height unless its height is + // explicitly set to zero. Example use case: + // 1. Tab containing ListView is visible. Height is non-zero. ListView is + // rendered. + // 2. Tab is hidden. Height of container is explicitly constrained to zero. + // ListView is not rendered. + // 3. Tab is shown again. Height constraint is removed. Resize observer + // fires and shows non-zero height due to the (without this, + // the height would remain zero forever) + // 4. ListView is rendered again. <> </> ) : ( <SpectrumListView From 195c42c8c66f6ad98b2e85aa6989a9019cfbfe01 Mon Sep 17 00:00:00 2001 From: Brian Ingles <brianingles@deephaven.io> Date: Tue, 9 Apr 2024 10:38:55 -0500 Subject: [PATCH 25/41] Comments (#1909) --- packages/components/src/spectrum/listView/ListView.tsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/components/src/spectrum/listView/ListView.tsx b/packages/components/src/spectrum/listView/ListView.tsx index 60668c2752..183f6e13bb 100644 --- a/packages/components/src/spectrum/listView/ListView.tsx +++ b/packages/components/src/spectrum/listView/ListView.tsx @@ -117,13 +117,13 @@ export function ListView({ // Use to ensure content has a non-zero height. This ensures the // container will also have a non-zero height unless its height is // explicitly set to zero. Example use case: - // 1. Tab containing ListView is visible. Height is non-zero. ListView is - // rendered. - // 2. Tab is hidden. Height of container is explicitly constrained to zero. - // ListView is not rendered. + // 1. Tab containing ListView is visible. Container height is non-zero. + // ListView is rendered. + // 2. Tab is hidden. Container height is explicitly constrained to zero. + // ListView is not rendered. // 3. Tab is shown again. Height constraint is removed. Resize observer // fires and shows non-zero height due to the (without this, - // the height would remain zero forever) + // the height would remain zero forever since ListView hasn't rendered yet) // 4. ListView is rendered again. <> </> ) : ( From 055f9532d7019f0d690db9dbaa2bb86a4e47e501 Mon Sep 17 00:00:00 2001 From: Brian Ingles <brianingles@deephaven.io> Date: Tue, 9 Apr 2024 10:46:01 -0500 Subject: [PATCH 26/41] Comments (#1909) --- .../src/spectrum/utils/useRenderNormalizedItem.tsx | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/components/src/spectrum/utils/useRenderNormalizedItem.tsx b/packages/components/src/spectrum/utils/useRenderNormalizedItem.tsx index 52a70f62dc..2904bbb558 100644 --- a/packages/components/src/spectrum/utils/useRenderNormalizedItem.tsx +++ b/packages/components/src/spectrum/utils/useRenderNormalizedItem.tsx @@ -3,6 +3,12 @@ import { ItemContent } from '../ItemContent'; import { Item } from '../shared'; import { getItemKey, NormalizedItem, TooltipOptions } from './itemUtils'; +/** + * Returns a render function that can be used to render a normalized item in + * collection components. + * @param tooltipOptions Tooltip options to use when rendering the item + * @returns Render function for normalized items + */ export function useRenderNormalizedItem( tooltipOptions: TooltipOptions | null ): (normalizedItem: NormalizedItem) => JSX.Element { @@ -20,7 +26,7 @@ export function useRenderNormalizedItem( // `onSelectionChange` handlers` regardless of the actual type of the // key. We can't really get around setting in order to support Windowed // data, so we'll need to do some manual conversion of keys to strings - // in other places of this component. + // in other components that use this hook. key={key as Key} // The `textValue` prop gets used to provide the content of `<option>` // elements that back the Spectrum Picker. These are not visible in the UI, From 8d382b50d65243b3d5bcde1e12ac3c8076d3854f Mon Sep 17 00:00:00 2001 From: Brian Ingles <brianingles@deephaven.io> Date: Tue, 9 Apr 2024 11:13:02 -0500 Subject: [PATCH 27/41] useRenderNormalizedItem tests (#1909) --- .../utils/useRenderNormalizedItem.test.tsx | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 packages/components/src/spectrum/utils/useRenderNormalizedItem.test.tsx diff --git a/packages/components/src/spectrum/utils/useRenderNormalizedItem.test.tsx b/packages/components/src/spectrum/utils/useRenderNormalizedItem.test.tsx new file mode 100644 index 0000000000..993524b71a --- /dev/null +++ b/packages/components/src/spectrum/utils/useRenderNormalizedItem.test.tsx @@ -0,0 +1,41 @@ +import React, { Key } from 'react'; +import { Item } from '@adobe/react-spectrum'; +import { renderHook } from '@testing-library/react-hooks'; +import ItemContent from '../ItemContent'; +import { useRenderNormalizedItem } from './useRenderNormalizedItem'; +import { getItemKey } from './itemUtils'; + +beforeEach(() => { + jest.clearAllMocks(); + expect.hasAssertions(); +}); + +describe.each([null, { placement: 'top' }] as const)( + 'useRenderNormalizedItem: %s', + tooltipOptions => { + it.each([ + [{}, 'Empty', ''], + [{ item: { content: 'mock.content' } }, 'Empty', 'mock.content'], + [ + { item: { textValue: 'mock.textValue', content: 'mock.content' } }, + 'mock.textValue', + 'mock.content', + ], + ])( + 'should return a render function that can be used to render a normalized item in collection components.', + (normalizedItem, textValue, content) => { + const { result } = renderHook(() => + useRenderNormalizedItem(tooltipOptions) + ); + + const actual = result.current(normalizedItem); + + expect(actual).toEqual( + <Item key={getItemKey(normalizedItem) as Key} textValue={textValue}> + <ItemContent tooltipOptions={tooltipOptions}>{content}</ItemContent> + </Item> + ); + } + ); + } +); From 91deda835e0e8af910d1a9f416b932abaff9344b Mon Sep 17 00:00:00 2001 From: Brian Ingles <brianingles@deephaven.io> Date: Tue, 9 Apr 2024 11:50:35 -0500 Subject: [PATCH 28/41] Fixed useContentRect to update automatically (#1909) --- .../react-hooks/src/useContentRect.test.ts | 14 ++++---- packages/react-hooks/src/useContentRect.ts | 34 ++++++++++++++----- 2 files changed, 34 insertions(+), 14 deletions(-) diff --git a/packages/react-hooks/src/useContentRect.test.ts b/packages/react-hooks/src/useContentRect.test.ts index 7dbf903b8b..42e248b822 100644 --- a/packages/react-hooks/src/useContentRect.test.ts +++ b/packages/react-hooks/src/useContentRect.test.ts @@ -32,10 +32,11 @@ describe.each([true, false])('useContentRect - explicitMap:%s', explicitMap => { }); it('should pass expected value to resize observer based on presence of map function', () => { - const { result, rerender } = renderHook(() => useContentRect(mockMap)); + const { result } = renderHook(() => useContentRect(mockMap)); - result.current.ref(mock.refValue); - rerender(); + act(() => { + result.current.ref(mock.refValue); + }); if (mockMap != null) { expect(mockMap).toHaveBeenCalledWith(mock.refValue); @@ -53,10 +54,11 @@ describe.each([true, false])('useContentRect - explicitMap:%s', explicitMap => { ])( 'should update contentRect when resize observer triggers: %s', (entries, expected) => { - const { result, rerender } = renderHook(() => useContentRect(mockMap)); + const { result } = renderHook(() => useContentRect(mockMap)); - result.current.ref(mock.refValue); - rerender(); + act(() => { + result.current.ref(mock.refValue); + }); const handleResize = asMock(useResizeObserver).mock.calls.at(-1)?.[1]; diff --git a/packages/react-hooks/src/useContentRect.ts b/packages/react-hooks/src/useContentRect.ts index be26a1c6f6..3725cc9da1 100644 --- a/packages/react-hooks/src/useContentRect.ts +++ b/packages/react-hooks/src/useContentRect.ts @@ -1,5 +1,5 @@ import { identityExtractHTMLElement } from '@deephaven/utils'; -import { useCallback, useRef, useState } from 'react'; +import { useCallback, useMemo, useState } from 'react'; import useMappedRef from './useMappedRef'; import useResizeObserver from './useResizeObserver'; @@ -19,21 +19,39 @@ export interface UseContentRectResult<T> { export function useContentRect<T>( map: (ref: T) => HTMLElement | null = identityExtractHTMLElement ): UseContentRectResult<T> { - const [contentRect, setContentRect] = useState<DOMRectReadOnly>( - new DOMRect() + const [x, setX] = useState<number>(0); + const [y, setY] = useState<number>(0); + const [width, setWidth] = useState<number>(0); + const [height, setHeight] = useState<number>(0); + + const contentRect = useMemo( + () => new DOMRect(x, y, width, height), + [height, width, x, y] ); + const [el, setEl] = useState<HTMLElement | null>(null); + + // Callback ref maps the passed in refValue and passes to `setEl` + const ref = useMappedRef(setEl, map); + const handleResize = useCallback( ([firstEntry]: ResizeObserverEntry[]): void => { - setContentRect(firstEntry?.contentRect ?? new DOMRect()); + const rect = firstEntry?.contentRect ?? { + x: 0, + y: 0, + width: 0, + height: 0, + }; + + setX(rect.x); + setY(rect.y); + setWidth(rect.width); + setHeight(rect.height); }, [] ); - const observerRef = useRef<HTMLElement>(null); - useResizeObserver(observerRef.current, handleResize); - - const ref = useMappedRef(observerRef, map); + useResizeObserver(el, handleResize); return { ref, From 962f64cf4174a471d15e491f1c1961b89efe5760 Mon Sep 17 00:00:00 2001 From: Brian Ingles <brianingles@deephaven.io> Date: Tue, 9 Apr 2024 14:23:47 -0500 Subject: [PATCH 29/41] e2e tests (#1909) --- .../code-studio/src/styleguide/ListViews.tsx | 11 +++++++++-- tests/styleguide.spec.ts | 1 + .../list-views-chromium-linux.png | Bin 0 -> 30000 bytes .../list-views-firefox-linux.png | Bin 0 -> 40873 bytes .../list-views-webkit-linux.png | Bin 0 -> 28191 bytes 5 files changed, 10 insertions(+), 2 deletions(-) create mode 100644 tests/styleguide.spec.ts-snapshots/list-views-chromium-linux.png create mode 100644 tests/styleguide.spec.ts-snapshots/list-views-firefox-linux.png create mode 100644 tests/styleguide.spec.ts-snapshots/list-views-webkit-linux.png diff --git a/packages/code-studio/src/styleguide/ListViews.tsx b/packages/code-studio/src/styleguide/ListViews.tsx index a7fe4be584..eded6d1c06 100644 --- a/packages/code-studio/src/styleguide/ListViews.tsx +++ b/packages/code-studio/src/styleguide/ListViews.tsx @@ -37,9 +37,10 @@ export function ListViews(): JSX.Element { <div {...sampleSectionIdAndClasses('list-views')}> <h2 className="ui-title">List View</h2> - <Grid columnGap={14} height="size-4600"> + <Grid columnGap={14} height="size-6000"> <Text>Single Child</Text> <ListView + density="compact" gridRow="2" aria-label="Single Child" selectionMode="multiple" @@ -48,7 +49,12 @@ export function ListViews(): JSX.Element { </ListView> <label>Icons</label> - <ListView gridRow="2" aria-label="Icon" selectionMode="multiple"> + <ListView + gridRow="2" + aria-label="Icon" + density="compact" + selectionMode="multiple" + > <Item textValue="Item with icon A"> <AccountIcon slot="image" /> <Text>Item with icon A</Text> @@ -71,6 +77,7 @@ export function ListViews(): JSX.Element { <ListView gridRow="2" aria-label="Mixed Children Types" + density="compact" maxWidth="size-2400" selectionMode="multiple" defaultSelectedKeys={[999, 444]} diff --git a/tests/styleguide.spec.ts b/tests/styleguide.spec.ts index 0d6cf06304..5c15fe69ee 100644 --- a/tests/styleguide.spec.ts +++ b/tests/styleguide.spec.ts @@ -26,6 +26,7 @@ const sampleSectionIds: string[] = [ 'sample-section-context-menus', 'sample-section-dropdown-menus', 'sample-section-navigations', + 'sample-section-list-views', 'sample-section-pickers', 'sample-section-tooltips', 'sample-section-icons', diff --git a/tests/styleguide.spec.ts-snapshots/list-views-chromium-linux.png b/tests/styleguide.spec.ts-snapshots/list-views-chromium-linux.png new file mode 100644 index 0000000000000000000000000000000000000000..020ed77f301e9a433a1dfb9024142be666f4b9b6 GIT binary patch literal 30000 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 B9xea? literal 0 HcmV?d00001 diff --git a/tests/styleguide.spec.ts-snapshots/list-views-firefox-linux.png b/tests/styleguide.spec.ts-snapshots/list-views-firefox-linux.png new file mode 100644 index 0000000000000000000000000000000000000000..25e11db072e912ba2f449a57ab9ad83e28aaef93 GIT binary patch literal 40873 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 literal 0 HcmV?d00001 diff --git a/tests/styleguide.spec.ts-snapshots/list-views-webkit-linux.png b/tests/styleguide.spec.ts-snapshots/list-views-webkit-linux.png new file mode 100644 index 0000000000000000000000000000000000000000..81be65cac8dd08bce010d590c222aff96e42992e GIT binary patch literal 28191 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 B5TO77 literal 0 HcmV?d00001 From 599902ddafab6182bdeb94198a3686ba0334957a Mon Sep 17 00:00:00 2001 From: Brian Ingles <brianingles@deephaven.io> Date: Tue, 9 Apr 2024 14:47:20 -0500 Subject: [PATCH 30/41] Item key stringify tests (#1909) --- .../src/spectrum/utils/itemUtils.test.tsx | 14 ++++ .../src/spectrum/utils/itemUtils.ts | 15 +++++ .../useStringifiedMultiSelection.test.ts | 65 +++++++++++++++++++ .../utils/useStringifiedMultiSelection.ts | 17 ++--- 4 files changed, 98 insertions(+), 13 deletions(-) create mode 100644 packages/components/src/spectrum/utils/useStringifiedMultiSelection.test.ts diff --git a/packages/components/src/spectrum/utils/itemUtils.test.tsx b/packages/components/src/spectrum/utils/itemUtils.test.tsx index 4be6d49580..0585cfb8c8 100644 --- a/packages/components/src/spectrum/utils/itemUtils.test.tsx +++ b/packages/components/src/spectrum/utils/itemUtils.test.tsx @@ -14,6 +14,7 @@ import { ItemElementOrPrimitive, ItemOrSection, SectionElement, + itemSelectionToStringSet, } from './itemUtils'; import type { PickerProps } from '../picker/Picker'; import { Item, Section } from '../shared'; @@ -256,6 +257,19 @@ describe('isNormalizedSection', () => { }); }); +describe('itemSelectionToStringSet', () => { + it.each([ + ['all', 'all'], + [new Set([1, 2, 3]), new Set(['1', '2', '3'])], + ] as const)( + `should return 'all' or stringify the keys`, + (given, expected) => { + const actual = itemSelectionToStringSet(given); + expect(actual).toEqual(expected); + } + ); +}); + describe('normalizeItemList', () => { it.each([children.empty, children.single, children.mixed])( 'should return normalized items: %#: %s', diff --git a/packages/components/src/spectrum/utils/itemUtils.ts b/packages/components/src/spectrum/utils/itemUtils.ts index ddbc143693..d7b35ac663 100644 --- a/packages/components/src/spectrum/utils/itemUtils.ts +++ b/packages/components/src/spectrum/utils/itemUtils.ts @@ -309,3 +309,18 @@ export function normalizeTooltipOptions( return options; } + +/** + * Convert a selection of `ItemKey`s to a selection of strings. + * @param itemKeys The selection of `ItemKey`s + * @returns The selection of strings + */ +export function itemSelectionToStringSet( + itemKeys?: 'all' | Iterable<ItemKey> +): undefined | 'all' | Set<string> { + if (itemKeys == null || itemKeys === 'all') { + return itemKeys as undefined | 'all'; + } + + return new Set([...itemKeys].map(String)); +} diff --git a/packages/components/src/spectrum/utils/useStringifiedMultiSelection.test.ts b/packages/components/src/spectrum/utils/useStringifiedMultiSelection.test.ts new file mode 100644 index 0000000000..78225225ba --- /dev/null +++ b/packages/components/src/spectrum/utils/useStringifiedMultiSelection.test.ts @@ -0,0 +1,65 @@ +import { renderHook } from '@testing-library/react-hooks'; +import { NormalizedItem } from './itemUtils'; +import { useStringifiedMultiSelection } from './useStringifiedMultiSelection'; + +beforeEach(() => { + jest.clearAllMocks(); + expect.hasAssertions(); +}); + +describe('useStringifiedMultiSelection', () => { + const normalizedItems: NormalizedItem[] = [1, 2, 3, 4, 5, 6, 7, 8, 9].map( + i => ({ + key: i, + item: { key: i, content: `Item ${i}` }, + }) + ); + + const selectedKeys = [1, 2, 3]; + const defaultSelectedKeys = [4, 5, 6]; + const disabledKeys = [7, 8, 9]; + + const selectedStringKeys = new Set(['1', '2', '3']); + const defaultSelectedStringKeys = new Set(['4', '5', '6']); + const disabledStringKeys = new Set(['7', '8', '9']); + + it('should stringify selections', () => { + const { result } = renderHook(() => + useStringifiedMultiSelection({ + normalizedItems, + selectedKeys, + defaultSelectedKeys, + disabledKeys, + }) + ); + + expect(result.current.selectedStringKeys).toEqual(selectedStringKeys); + expect(result.current.defaultSelectedStringKeys).toEqual( + defaultSelectedStringKeys + ); + expect(result.current.disabledStringKeys).toEqual(disabledStringKeys); + }); + + it.each([ + ['all', 'all'], + [new Set(['1', '2', '3']), new Set([1, 2, 3])], + ] as const)( + `should call onChange with 'all' or actual keys`, + (given, expected) => { + const onChange = jest.fn(); + const { result } = renderHook(() => + useStringifiedMultiSelection({ + normalizedItems, + selectedKeys, + defaultSelectedKeys, + disabledKeys, + onChange, + }) + ); + + result.current.onStringSelectionChange(given); + + expect(onChange).toHaveBeenCalledWith(expected); + } + ); +}); diff --git a/packages/components/src/spectrum/utils/useStringifiedMultiSelection.ts b/packages/components/src/spectrum/utils/useStringifiedMultiSelection.ts index 7104638f2b..04ec670168 100644 --- a/packages/components/src/spectrum/utils/useStringifiedMultiSelection.ts +++ b/packages/components/src/spectrum/utils/useStringifiedMultiSelection.ts @@ -3,19 +3,10 @@ import { getItemKey, ItemKey, ItemSelection, + itemSelectionToStringSet, NormalizedItem, } from './itemUtils'; -function toStringKeySet( - keys?: 'all' | Iterable<ItemKey> -): undefined | 'all' | Set<Key> { - if (keys == null || keys === 'all') { - return keys as undefined | 'all'; - } - - return new Set([...keys].map(String)); -} - export interface UseStringifiedMultiSelectionOptions { normalizedItems: NormalizedItem[]; selectedKeys?: 'all' | Iterable<ItemKey>; @@ -64,17 +55,17 @@ export function useStringifiedMultiSelection({ onChange, }: UseStringifiedMultiSelectionOptions): UseStringifiedMultiSelectionResult { const selectedStringKeys = useMemo( - () => toStringKeySet(selectedKeys), + () => itemSelectionToStringSet(selectedKeys), [selectedKeys] ); const defaultSelectedStringKeys = useMemo( - () => toStringKeySet(defaultSelectedKeys), + () => itemSelectionToStringSet(defaultSelectedKeys), [defaultSelectedKeys] ); const disabledStringKeys = useMemo( - () => toStringKeySet(disabledKeys), + () => itemSelectionToStringSet(disabledKeys), [disabledKeys] ); From 5fd74dd743454c019e512c7bb89ba1818ddc9533 Mon Sep 17 00:00:00 2001 From: Brian Ingles <brianingles@deephaven.io> Date: Tue, 9 Apr 2024 14:58:48 -0500 Subject: [PATCH 31/41] Re-use SelectionT (#1909) --- packages/components/src/spectrum/utils/itemUtils.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/components/src/spectrum/utils/itemUtils.ts b/packages/components/src/spectrum/utils/itemUtils.ts index d7b35ac663..351d13f94d 100644 --- a/packages/components/src/spectrum/utils/itemUtils.ts +++ b/packages/components/src/spectrum/utils/itemUtils.ts @@ -2,7 +2,7 @@ import { isValidElement, Key, ReactElement, ReactNode } from 'react'; import { SpectrumPickerProps } from '@adobe/react-spectrum'; import type { ItemRenderer } from '@react-types/shared'; import Log from '@deephaven/log'; -import { KeyedItem } from '@deephaven/utils'; +import { KeyedItem, SelectionT } from '@deephaven/utils'; import { Item, ItemProps, Section, SectionProps } from '../shared'; import { PopperOptions } from '../../popper'; @@ -33,7 +33,7 @@ export type ItemOrSection = ItemElementOrPrimitive | SectionElement; */ export type ItemKey = Key | boolean; -export type ItemSelection = 'all' | Set<ItemKey>; +export type ItemSelection = SelectionT<ItemKey>; /** * Augment the Spectrum selection change handler type to include boolean keys. From 5b7d5a22bf6bec8f747f77f06f99768a6f3ee066 Mon Sep 17 00:00:00 2001 From: Brian Ingles <brianingles@deephaven.io> Date: Tue, 9 Apr 2024 15:04:59 -0500 Subject: [PATCH 32/41] cleanup (#1909) --- .../utils/useStringifiedMultiSelection.test.ts | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/packages/components/src/spectrum/utils/useStringifiedMultiSelection.test.ts b/packages/components/src/spectrum/utils/useStringifiedMultiSelection.test.ts index 78225225ba..652ded544a 100644 --- a/packages/components/src/spectrum/utils/useStringifiedMultiSelection.test.ts +++ b/packages/components/src/spectrum/utils/useStringifiedMultiSelection.test.ts @@ -33,11 +33,12 @@ describe('useStringifiedMultiSelection', () => { }) ); - expect(result.current.selectedStringKeys).toEqual(selectedStringKeys); - expect(result.current.defaultSelectedStringKeys).toEqual( - defaultSelectedStringKeys - ); - expect(result.current.disabledStringKeys).toEqual(disabledStringKeys); + expect(result.current).toEqual({ + selectedStringKeys, + defaultSelectedStringKeys, + disabledStringKeys, + onStringSelectionChange: expect.any(Function), + }); }); it.each([ From 485166927e405a9d62e4f8beda8a7d3d026adc4c Mon Sep 17 00:00:00 2001 From: Brian Ingles <brianingles@deephaven.io> Date: Tue, 9 Apr 2024 15:06:13 -0500 Subject: [PATCH 33/41] cleanup (#1909) --- .../src/spectrum/utils/useStringifiedMultiSelection.test.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/components/src/spectrum/utils/useStringifiedMultiSelection.test.ts b/packages/components/src/spectrum/utils/useStringifiedMultiSelection.test.ts index 652ded544a..a429d9c1e7 100644 --- a/packages/components/src/spectrum/utils/useStringifiedMultiSelection.test.ts +++ b/packages/components/src/spectrum/utils/useStringifiedMultiSelection.test.ts @@ -47,7 +47,8 @@ describe('useStringifiedMultiSelection', () => { ] as const)( `should call onChange with 'all' or actual keys`, (given, expected) => { - const onChange = jest.fn(); + const onChange = jest.fn().mockName('onChange'); + const { result } = renderHook(() => useStringifiedMultiSelection({ normalizedItems, From 36d4134d2ff8a1cff68c1f20e56ca7942bf432f1 Mon Sep 17 00:00:00 2001 From: Brian Ingles <brianingles@deephaven.io> Date: Wed, 10 Apr 2024 15:36:37 -0500 Subject: [PATCH 34/41] Fixed tests (#1909) --- .../src/styleguide/StyleGuide.test.tsx | 28 +++++++++++++------ 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/packages/code-studio/src/styleguide/StyleGuide.test.tsx b/packages/code-studio/src/styleguide/StyleGuide.test.tsx index f238df8847..d7d6853243 100644 --- a/packages/code-studio/src/styleguide/StyleGuide.test.tsx +++ b/packages/code-studio/src/styleguide/StyleGuide.test.tsx @@ -9,29 +9,41 @@ import StyleGuide from './StyleGuide'; window.HTMLElement.prototype.scroll = jest.fn(); window.HTMLElement.prototype.scrollIntoView = jest.fn(); +/* + * React Spectrum `useVirtualizerItem` depends on `scrollWidth` and `scrollHeight`. + * Mocking these to avoid React "Maximum update depth exceeded" errors. + * https://github.com/adobe/react-spectrum/blob/0b2a838b36ad6d86eee13abaf68b7e4d2b4ada6c/packages/%40react-aria/virtualizer/src/useVirtualizerItem.ts#L49C3-L49C60 + */ +function mockListViewDimension(value: number) { + return function getDimension() { + const isSpectrumListView = + this instanceof HTMLElement && + this.className.includes('_react-spectrum-ListView'); + + // For non ListView, just return zero which is the default value anyway. + return isSpectrumListView === true ? value : 0; + }; +} + describe('<StyleGuide /> mounts', () => { test('h1 text of StyleGuide renders', () => { // Provide a non-null array to ThemeProvider to tell it to initialize const customThemes: ThemeData[] = []; - // React Spectrum `useVirtualizerItem` depends on `scrollWidth` and `scrollHeight`. - // Mocking these to avoid React "Maximum update depth exceeded" errors. - // https://github.com/adobe/react-spectrum/blob/0b2a838b36ad6d86eee13abaf68b7e4d2b4ada6c/packages/%40react-aria/virtualizer/src/useVirtualizerItem.ts#L49C3-L49C60 - // From preview docs: https://reactspectrum.blob.core.windows.net/reactspectrum/726a5e8f0ed50fc8d98e39c74bd6dfeb3660fbdf/docs/react-spectrum/testing.html#virtualized-components // The virtualizer will now think it has a visible area of 1000px x 1000px and that the items within it are 40px x 40px jest .spyOn(window.HTMLElement.prototype, 'clientWidth', 'get') - .mockImplementation(() => 1000); + .mockImplementation(mockListViewDimension(1000)); jest .spyOn(window.HTMLElement.prototype, 'clientHeight', 'get') - .mockImplementation(() => 1000); + .mockImplementation(mockListViewDimension(1000)); jest .spyOn(window.HTMLElement.prototype, 'scrollHeight', 'get') - .mockImplementation(() => 40); + .mockImplementation(mockListViewDimension(40)); jest .spyOn(window.HTMLElement.prototype, 'scrollWidth', 'get') - .mockImplementation(() => 40); + .mockImplementation(mockListViewDimension(40)); expect(() => render( From ae099ca0864c02e53311a8087bfae5e57369c3e3 Mon Sep 17 00:00:00 2001 From: Brian Ingles <brianingles@deephaven.io> Date: Wed, 10 Apr 2024 15:51:46 -0500 Subject: [PATCH 35/41] useCheckOverflow Tests (#1909) --- .../react-hooks/src/useCheckOverflow.test.ts | 55 +++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 packages/react-hooks/src/useCheckOverflow.test.ts diff --git a/packages/react-hooks/src/useCheckOverflow.test.ts b/packages/react-hooks/src/useCheckOverflow.test.ts new file mode 100644 index 0000000000..35c2eb38df --- /dev/null +++ b/packages/react-hooks/src/useCheckOverflow.test.ts @@ -0,0 +1,55 @@ +import { act, renderHook } from '@testing-library/react-hooks'; +import type { DOMRefValue } from '@react-types/shared'; +import { TestUtils } from '@deephaven/utils'; +import { useCheckOverflow } from './useCheckOverflow'; + +const { createMockProxy } = TestUtils; + +beforeEach(() => { + jest.clearAllMocks(); + expect.hasAssertions(); +}); + +describe('useCheckOverflow', () => { + const isOverflowing = createMockProxy<HTMLDivElement>({ + scrollWidth: 101, + offsetWidth: 100, + }); + + const scrollWidthMatchesOffsetWidth = createMockProxy<HTMLDivElement>({ + scrollWidth: 100, + offsetWidth: 100, + }); + + const offsetWidthGreaterThanScrollWidth = createMockProxy<HTMLDivElement>({ + scrollWidth: 99, + offsetWidth: 100, + }); + + it.each([ + [isOverflowing, true], + [scrollWidthMatchesOffsetWidth, false], + [offsetWidthGreaterThanScrollWidth, false], + ])( + 'should check if a Spectrum `DOMRefValue` is overflowing', + (el, expected) => { + const { result } = renderHook(() => useCheckOverflow()); + + const elRef = createMockProxy<DOMRefValue<HTMLDivElement>>({ + UNSAFE_getDOMNode: () => createMockProxy<HTMLDivElement>(el), + }); + + act(() => { + result.current.checkOverflow(elRef); + }); + + expect(result.current.isOverflowing).toBe(expected); + + act(() => { + result.current.resetIsOverflowing(); + }); + + expect(result.current.isOverflowing).toBe(false); + } + ); +}); From 342b24cfc94f848613c989b78525cf5053899807 Mon Sep 17 00:00:00 2001 From: Brian Ingles <brianingles@deephaven.io> Date: Wed, 10 Apr 2024 15:59:16 -0500 Subject: [PATCH 36/41] Cleanup (#1909) --- .../src/styleguide/StyleGuide.test.tsx | 43 ++++++++----------- 1 file changed, 19 insertions(+), 24 deletions(-) diff --git a/packages/code-studio/src/styleguide/StyleGuide.test.tsx b/packages/code-studio/src/styleguide/StyleGuide.test.tsx index d7d6853243..28355f2637 100644 --- a/packages/code-studio/src/styleguide/StyleGuide.test.tsx +++ b/packages/code-studio/src/styleguide/StyleGuide.test.tsx @@ -9,20 +9,20 @@ import StyleGuide from './StyleGuide'; window.HTMLElement.prototype.scroll = jest.fn(); window.HTMLElement.prototype.scrollIntoView = jest.fn(); -/* - * React Spectrum `useVirtualizerItem` depends on `scrollWidth` and `scrollHeight`. - * Mocking these to avoid React "Maximum update depth exceeded" errors. - * https://github.com/adobe/react-spectrum/blob/0b2a838b36ad6d86eee13abaf68b7e4d2b4ada6c/packages/%40react-aria/virtualizer/src/useVirtualizerItem.ts#L49C3-L49C60 +/** + * Mock a dimension property of a ListView element. */ -function mockListViewDimension(value: number) { - return function getDimension() { - const isSpectrumListView = - this instanceof HTMLElement && - this.className.includes('_react-spectrum-ListView'); +function mockListViewDimension(propName: keyof HTMLElement, value: number) { + jest + .spyOn(window.HTMLElement.prototype, propName, 'get') + .mockImplementation(function getDimension() { + const isSpectrumListView = + this instanceof HTMLElement && + this.className.includes('_react-spectrum-ListView'); - // For non ListView, just return zero which is the default value anyway. - return isSpectrumListView === true ? value : 0; - }; + // For non ListView, just return zero which is the default value anyway. + return isSpectrumListView === true ? value : 0; + }); } describe('<StyleGuide /> mounts', () => { @@ -30,20 +30,15 @@ describe('<StyleGuide /> mounts', () => { // Provide a non-null array to ThemeProvider to tell it to initialize const customThemes: ThemeData[] = []; + // React Spectrum `useVirtualizerItem` depends on `scrollWidth` and `scrollHeight`. + // Mocking these to avoid React "Maximum update depth exceeded" errors. + // https://github.com/adobe/react-spectrum/blob/0b2a838b36ad6d86eee13abaf68b7e4d2b4ada6c/packages/%40react-aria/virtualizer/src/useVirtualizerItem.ts#L49C3-L49C60 // From preview docs: https://reactspectrum.blob.core.windows.net/reactspectrum/726a5e8f0ed50fc8d98e39c74bd6dfeb3660fbdf/docs/react-spectrum/testing.html#virtualized-components // The virtualizer will now think it has a visible area of 1000px x 1000px and that the items within it are 40px x 40px - jest - .spyOn(window.HTMLElement.prototype, 'clientWidth', 'get') - .mockImplementation(mockListViewDimension(1000)); - jest - .spyOn(window.HTMLElement.prototype, 'clientHeight', 'get') - .mockImplementation(mockListViewDimension(1000)); - jest - .spyOn(window.HTMLElement.prototype, 'scrollHeight', 'get') - .mockImplementation(mockListViewDimension(40)); - jest - .spyOn(window.HTMLElement.prototype, 'scrollWidth', 'get') - .mockImplementation(mockListViewDimension(40)); + mockListViewDimension('clientWidth', 1000); + mockListViewDimension('clientHeight', 1000); + mockListViewDimension('scrollHeight', 40); + mockListViewDimension('scrollWidth', 40); expect(() => render( From a65e539cec3489b51b8b9bc596e6ed68aefd73a8 Mon Sep 17 00:00:00 2001 From: Brian Ingles <brianingles@deephaven.io> Date: Wed, 10 Apr 2024 16:01:53 -0500 Subject: [PATCH 37/41] Added test case (#1909) --- packages/react-hooks/src/useCheckOverflow.test.ts | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/packages/react-hooks/src/useCheckOverflow.test.ts b/packages/react-hooks/src/useCheckOverflow.test.ts index 35c2eb38df..9499e91889 100644 --- a/packages/react-hooks/src/useCheckOverflow.test.ts +++ b/packages/react-hooks/src/useCheckOverflow.test.ts @@ -27,17 +27,21 @@ describe('useCheckOverflow', () => { }); it.each([ + [null, false], [isOverflowing, true], [scrollWidthMatchesOffsetWidth, false], [offsetWidthGreaterThanScrollWidth, false], ])( - 'should check if a Spectrum `DOMRefValue` is overflowing', + 'should check if a Spectrum `DOMRefValue` is overflowing: %s, %s', (el, expected) => { const { result } = renderHook(() => useCheckOverflow()); - const elRef = createMockProxy<DOMRefValue<HTMLDivElement>>({ - UNSAFE_getDOMNode: () => createMockProxy<HTMLDivElement>(el), - }); + const elRef = + el == null + ? null + : createMockProxy<DOMRefValue<HTMLDivElement>>({ + UNSAFE_getDOMNode: () => createMockProxy<HTMLDivElement>(el), + }); act(() => { result.current.checkOverflow(elRef); From 95a47448d1e838483169397b3e32c0cf0b8edc82 Mon Sep 17 00:00:00 2001 From: Brian Ingles <brianingles@deephaven.io> Date: Tue, 16 Apr 2024 15:22:44 -0500 Subject: [PATCH 38/41] Addressed review comments (#1909) --- packages/code-studio/src/styleguide/ListViews.tsx | 10 ++++++++-- packages/components/src/spectrum/icons.ts | 2 ++ packages/components/src/spectrum/index.ts | 1 + packages/components/src/spectrum/listView/ListView.tsx | 6 +++--- 4 files changed, 14 insertions(+), 5 deletions(-) create mode 100644 packages/components/src/spectrum/icons.ts diff --git a/packages/code-studio/src/styleguide/ListViews.tsx b/packages/code-studio/src/styleguide/ListViews.tsx index eded6d1c06..f8fcb4ae8a 100644 --- a/packages/code-studio/src/styleguide/ListViews.tsx +++ b/packages/code-studio/src/styleguide/ListViews.tsx @@ -1,7 +1,13 @@ import React, { useCallback, useState } from 'react'; -import { Grid, Item, ListView, ItemKey, Text } from '@deephaven/components'; +import { + Grid, + Icon, + Item, + ListView, + ItemKey, + Text, +} from '@deephaven/components'; import { vsAccount, vsPerson } from '@deephaven/icons'; -import { Icon } from '@adobe/react-spectrum'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { generateNormalizedItems, sampleSectionIdAndClasses } from './utils'; diff --git a/packages/components/src/spectrum/icons.ts b/packages/components/src/spectrum/icons.ts new file mode 100644 index 0000000000..ac724b4335 --- /dev/null +++ b/packages/components/src/spectrum/icons.ts @@ -0,0 +1,2 @@ +// eslint-disable-next-line import/prefer-default-export +export { Icon } from '@adobe/react-spectrum'; diff --git a/packages/components/src/spectrum/index.ts b/packages/components/src/spectrum/index.ts index 02f1d4df7a..79dee1031d 100644 --- a/packages/components/src/spectrum/index.ts +++ b/packages/components/src/spectrum/index.ts @@ -6,6 +6,7 @@ export * from './collections'; export * from './content'; export * from './dateAndTime'; export * from './forms'; +export * from './icons'; export * from './layout'; export * from './navigation'; export * from './overlays'; diff --git a/packages/components/src/spectrum/listView/ListView.tsx b/packages/components/src/spectrum/listView/ListView.tsx index 183f6e13bb..5b748518b4 100644 --- a/packages/components/src/spectrum/listView/ListView.tsx +++ b/packages/components/src/spectrum/listView/ListView.tsx @@ -1,6 +1,6 @@ import { useMemo } from 'react'; +import cl from 'classnames'; import { - Flex, ListView as SpectrumListView, SpectrumListViewProps, } from '@adobe/react-spectrum'; @@ -10,7 +10,7 @@ import { useContentRect, useOnScrollRef, } from '@deephaven/react-hooks'; -import cl from 'classnames'; +import { Flex } from '../layout'; import { ItemElementOrPrimitive, ItemKey, @@ -100,7 +100,7 @@ export function ListView({ // Spectrum ListView crashes when it has zero height. Track the contentRect // of the parent container and only render the ListView when it has a non-zero - // height. + // height. See https://github.com/adobe/react-spectrum/issues/6213 const { ref: contentRectRef, contentRect } = useContentRect( extractSpectrumHTMLElement ); From 383b98c31286c0aedd63f2feb8deefa506eeebec Mon Sep 17 00:00:00 2001 From: Brian Ingles <brianingles@deephaven.io> Date: Thu, 18 Apr 2024 10:16:07 -0500 Subject: [PATCH 39/41] Simplified illustration example (#1909) --- .../code-studio/src/styleguide/ListViews.tsx | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/packages/code-studio/src/styleguide/ListViews.tsx b/packages/code-studio/src/styleguide/ListViews.tsx index f8fcb4ae8a..d52fe3672e 100644 --- a/packages/code-studio/src/styleguide/ListViews.tsx +++ b/packages/code-studio/src/styleguide/ListViews.tsx @@ -14,16 +14,12 @@ import { generateNormalizedItems, sampleSectionIdAndClasses } from './utils'; // Generate enough items to require scrolling const itemsSimple = [...generateNormalizedItems(52)]; -function AccountIcon({ - slot, -}: { - slot?: 'illustration' | 'image'; -}): JSX.Element { +function AccountIllustration(): JSX.Element { return ( // Images in ListView items require a slot of 'image' or 'illustration' to // be set in order to be positioned correctly: // https://github.com/adobe/react-spectrum/blob/784737effd44b9d5e2b1316e690da44555eafd7e/packages/%40react-spectrum/list/src/ListViewItem.tsx#L266-L267 - <Icon slot={slot}> + <Icon slot="illustration"> <FontAwesomeIcon icon={vsAccount} /> </Icon> ); @@ -62,19 +58,19 @@ export function ListViews(): JSX.Element { selectionMode="multiple" > <Item textValue="Item with icon A"> - <AccountIcon slot="image" /> + <AccountIllustration /> <Text>Item with icon A</Text> </Item> <Item textValue="Item with icon B"> - <AccountIcon slot="image" /> + <AccountIllustration /> <Text>Item with icon B</Text> </Item> <Item textValue="Item with icon C"> - <AccountIcon slot="image" /> + <AccountIllustration /> <Text>Item with icon C</Text> </Item> <Item textValue="Item with icon D"> - <AccountIcon slot="image" /> + <AccountIllustration /> <Text>Item with icon D with overflowing content</Text> </Item> </ListView> From fb8e4a165dc91f0e922aa27c591575afe9872583 Mon Sep 17 00:00:00 2001 From: Brian Ingles <brianingles@deephaven.io> Date: Thu, 18 Apr 2024 13:30:48 -0500 Subject: [PATCH 40/41] Export spectrum useProvider (#1909) --- packages/components/src/theme/index.ts | 1 + packages/components/src/theme/useSpectrumThemeProvider.ts | 5 +++++ packages/jsapi-components/src/spectrum/ListView.tsx | 4 ++-- 3 files changed, 8 insertions(+), 2 deletions(-) create mode 100644 packages/components/src/theme/useSpectrumThemeProvider.ts diff --git a/packages/components/src/theme/index.ts b/packages/components/src/theme/index.ts index 523fd23ddd..f58f84d25b 100644 --- a/packages/components/src/theme/index.ts +++ b/packages/components/src/theme/index.ts @@ -7,3 +7,4 @@ export * from './ThemeUtils'; export * from './useTheme'; export * from './Logo'; export * from './colorUtils'; +export * from './useSpectrumThemeProvider'; diff --git a/packages/components/src/theme/useSpectrumThemeProvider.ts b/packages/components/src/theme/useSpectrumThemeProvider.ts new file mode 100644 index 0000000000..ee6bb35952 --- /dev/null +++ b/packages/components/src/theme/useSpectrumThemeProvider.ts @@ -0,0 +1,5 @@ +import { useProvider } from '@adobe/react-spectrum'; + +export const useSpectrumThemeProvider = useProvider; + +export default useSpectrumThemeProvider; diff --git a/packages/jsapi-components/src/spectrum/ListView.tsx b/packages/jsapi-components/src/spectrum/ListView.tsx index c01ee194f9..fd363c896a 100644 --- a/packages/jsapi-components/src/spectrum/ListView.tsx +++ b/packages/jsapi-components/src/spectrum/ListView.tsx @@ -1,8 +1,8 @@ -import { useProvider } from '@adobe/react-spectrum'; import { ListView as ListViewBase, ListViewProps as ListViewPropsBase, NormalizedItemData, + useSpectrumThemeProvider, } from '@deephaven/components'; import { dh as DhType } from '@deephaven/jsapi-types'; import { Settings } from '@deephaven/jsapi-utils'; @@ -30,7 +30,7 @@ export function ListView({ settings, ...props }: ListViewProps): JSX.Element { - const { scale } = useProvider(); + const { scale } = useSpectrumThemeProvider(); const itemHeight = LIST_VIEW_ROW_HEIGHTS[props.density ?? 'regular'][scale]; const { getFormattedString: formatValue } = useFormatter(settings); From 4feb7d9b08311ee3c3197d007577740e4f0d0652 Mon Sep 17 00:00:00 2001 From: Brian Ingles <brianingles@deephaven.io> Date: Thu, 18 Apr 2024 14:09:00 -0500 Subject: [PATCH 41/41] Removed spectrum dependency (#1909) --- package-lock.json | 2 -- packages/jsapi-components/package.json | 1 - 2 files changed, 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index acfb66a308..a9d06f5266 100644 --- a/package-lock.json +++ b/package-lock.json @@ -29360,7 +29360,6 @@ "version": "0.72.0", "license": "Apache-2.0", "dependencies": { - "@adobe/react-spectrum": "^3.34.1", "@deephaven/components": "file:../components", "@deephaven/jsapi-bootstrap": "file:../jsapi-bootstrap", "@deephaven/jsapi-types": "1.0.0-dev0.33.1", @@ -31565,7 +31564,6 @@ "@deephaven/jsapi-components": { "version": "file:packages/jsapi-components", "requires": { - "@adobe/react-spectrum": "^3.34.1", "@deephaven/components": "file:../components", "@deephaven/jsapi-bootstrap": "file:../jsapi-bootstrap", "@deephaven/jsapi-shim": "file:../jsapi-shim", diff --git a/packages/jsapi-components/package.json b/packages/jsapi-components/package.json index ab301c3b9a..f849061bd6 100644 --- a/packages/jsapi-components/package.json +++ b/packages/jsapi-components/package.json @@ -22,7 +22,6 @@ "build:sass": "sass --embed-sources --load-path=../../node_modules ./src:./dist" }, "dependencies": { - "@adobe/react-spectrum": "^3.34.1", "@deephaven/components": "file:../components", "@deephaven/jsapi-bootstrap": "file:../jsapi-bootstrap", "@deephaven/jsapi-types": "1.0.0-dev0.33.1",