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.
+        <>&nbsp;</>
+      ) : (
+        <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 &nbsp; 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 &nbsp; (without this,
+        //    the height would remain zero forever)
+        // 4. ListView is rendered again.
         <>&nbsp;</>
       ) : (
         <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 &nbsp; 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 &nbsp; (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.
         <>&nbsp;</>
       ) : (

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
zcmcG$XH-*J`!|d_>I?{siVXxt4P8K~QUYQDL+>Rt3pMoKK}Qh<DTWg1y%Rtn^rnai
zNGF62(jgFP=ppY;@V-Ah@ALlG^Wk}C&02#_vd=ktU;Em>c6hI@sz6W6OiM#ULl1lM
zNRx)<4}BV%lQ;i71ODa^;d>AK=Y)%<!ULMZ&g+XbH2=`R9?5BYCM=J6=*O6oTh<vJ
zZz-KWeU_J(;cboc)Ih&8Mx@<4E^AJ)1Km)bE`(O@bJpq73fnHu{WtC3N}BVIFC%a8
z{`n{G4t;}ccg>&LlW+g_ZU{?%9fCKS2-(l>ax$4W$Ceo6Uq1n^Ohc10SkPWg{fXw~
zM%c^K)E^rk{BisE?=Ihbdi=TKh~^XRb)<vEY}0z2-h{3CNV!M!BbAE4;C&<NM^k1D
zt{0#4G5eH|n5a{#s{&o?_T$j#vrA2rxkCK_x7Xau=8+b|5s!VF1JwZN#zOG}wc=+a
zl%k3XcBmA#PZ(iW%;4tYQlP&&Xxb%x5CzBB8)SSgs#_1<lVBF#t~Dp8GBV!XaTu$P
z-t)JXJ{~sb)43Sc;>B#0;jO+bWKMH|E*c(p*Cr0Kc(_MLOGhubrB6nmSQ^R?g0vR7
zSh7l8_USwFPn1$b3M4e76c@|lctYn-Qt$ARR-n|3JFfE|<9c5MWPg=XcKo)Rv$Hl{
z49x)>DYw&8RaMQ!ouz)3H;ML2v6+PhubIa?NNYuGQJzk|dsF?2R9Pe%jfU_%#g_+U
z$WTA3zOH}0c&ok!wRbL(B@yL3Z|t#z#hDK1P1yIxyqv%+;@B~V)8^hwi(E5PQ_l%I
zM`u?NV21oQCPe3aw6avhy2wN1fy;TE;6acO8yg$9<^Er9Ns@beFr=oI7AAoF>ZU9T
zA|WMZ^X-QvX1i2$-)^&OcncrC@7SzK@8|hWqPD8gC!BCPX~%ht#5ihcpG=;s^<9bP
zld>KOLl4*YnZ90S@tl^W-Y?Crf`RXucJv2UvTyxE7dtyUY^|5DsKdB&6PusDi>YZ!
z<I1aw1pLp~$_MDGTaMX<CUtp=(Y(1XDAj$hKEb_xeFKBsNK^aW3zblH&d6MK7qU*e
zd=h+X{6ST9^<33Nnz;+cYRHI0SjoS_rHXA1xxxYa^(!E0eQISov%9-kdOA+jVSC<9
zIN2BPb>P|F)~1MLa=MfDgGgPdm%SQ!CzM)LS2}(vx@wonz&~rYgtMcJIeaLi?#SNL
zH!(3$eDV$j>4>{)C?<AB)Joq}N!I8lZ2xJi5UFHQ57vt<$Pa|?^k$x0`D=);p);|+
z&`;``ExnM%waqQ-o{}oBoeu$bO&IfxhFY%|LO2t!LG3YoLPCnFVX8|rI5{;L(wo}4
zIuVy`*N#tON}2?@YA2`lJgpYvahdVGIot8HSZ8PFs3d<F);K?3cO_0y`JRpaPL&uZ
z6l#3;ZcB)fFm<lESu>f7cijlK_5ORAIM=zYYn!Xts)@Z_@%|c-St_nuKkmXbz4tRm
zldTu${87S!@g6Uk0?7k3du{}Xf`Y=BQw!^yi@{IKo?CUfMfHOR+e}_ZT*>|q3JVLX
zy@`3>!~Nve*VpIDZz+Uv^DHJ<bi}518XwNAOrsZatYDg+`jt!lPU1lvI31_Lw0$Df
zb*Y~xQPK+zc1SA@C$84$42^m(&D8p;mU_RV@`}U=LmL~uO-x;#@hiVDHVHVd%~^X!
zkyZHOs011J=CFDusiDNBrO>y=kT%_6LI#<EHay&X!-penY!)fjTxN1x8EE#Em0nJP
zw5HbwB_v$$*oimwfpTxCN2L`8-j%8MGQGI5-a)U=@OIPtZ|Y?$A~ost-Txqnm^R4l
z=zswX&OFRzKe!1~R8qo?*ZFXeu4NhN)wtV<B4&7UJ&VqYS`R-0Gp~e9OJ}$;$OW2;
zlclrAvWRu>pf7py%jnByP*W;(adE5S4n0P_n3o(vikjmt`k;Bu5(omY;CJ`5XX2=>
z@BShWw(JGy3VufoZp=r6w>H++x@%9nM90Jwk`A_c91m3Y4$g85-zx1NU|gHXVqlf}
zF$NV^fB*h{G{lV%$5Lr#VTSNnBi!u56HMBDq0gr7seHOe$RZ^sCU)TNwmhed#H+*G
z4w~cH?I1-YysWbR^=!n{)CCC4WZa~rq(oCyHO#jtkY7SVBKY2uM(QqTT*=V(D1Oo#
zq=d3r-khFo5WOE88=I$@SyJt`nq}Z(vCv+GLoLm}tvG$S=a$kycwTkXrmn7DTxe9K
zIpdYDoeN)%14Zh}eRJnhGJd`o?Aq2AoWl3p=?v^Lc{4L+9GnT!k&%(iVzv?4tLxJ(
z(Md@rYctdZNl`w$wTgce_?GXsh*31JPA{uRxyBCL7&hC%fzpSTws?fDp4;EgcTn;j
z3%Me=)Oe&mhTT?mBP?f={r6=hBqj4YSnVLQ1V^5*f&$rxu^g*{*dkf7d*2bK*0`_<
z1DkXPWhO^jl#nxvL+a6ddb-+XQD810u$~@`Je`^>Utd2a7N1eyM*d_=%4te7xvjM%
zrGK=vgw3aA7)DoIu0?Z^H$U`h_Ca83GQ>x@=tVF|$o_(=A|oRs%8nlz9$spT<{QQc
z=jgj|CaMUT;tDh#UJ*2@U6jQ&gt;1n{Q=1}sq+SX0M=1d$L6}A9#y4gW=4>!sPFLQ
zAbB;~d0s6#ODz<~nmvV6F%7{|7qg*0&a&Y~W0-1Qy+T`izE-ur&;DVn<?dcyfJS3m
zi(VfV*0GgId4>xK2?dK<x^A7{RE<<U?VGxw%fK2O)oNDl-i<~{JbfShL1gEB_2ps_
zo4NF*LM-T_^f1jwe@Foc0sg5Ri$XL`Ei70a!PV*m!N}dEsSqAMK0dX<kFj1uzQ$;8
z(GbLw`iO#Jn`C)HFQ!y-&n*ICN5?&mm#ZS0Bk-34V49?TwrcXL1KQ>JnHj62VfCc-
zYq65<%by^vEiFnN@$>$23q?};Y3a^IEaLW*(^U}(X4G|R_x*DmJ@q~m6<%2uiy%gy
zK)u}?ex>~Q@dQ*(-Qu_M^76QfNqUTZpQ53d19pJl&~4eOKzT?Wi#AjXXJF@4>l$4%
zZ=XZAb#{t{t0*gLD=G$4RwkekI?T$H78PZNS?xAa2)oX+q33$C{7jBYyzWvjCIrl)
z`A;E)!erRZj&%?AB7F|f!L_TL%~ibDPh)cp6&j8XLo_utXXobf7+6Q}sW%vxW^()D
z?raD8);NEC$k^82u4FedmU)Jrg&PWms(B*878K6z%CWDjW)|^$36beKM|rfymAuB6
zv*Sl3R|m4c`1P%+jNCw4eF38<F<+jlPx$-wSC}TBJO~VYi?8!Q%~YWxBh$NNNXg&T
zuJU<!c%b#GevV#d_0#Wy3g<m3I)8fCq3_N6X*0XTnrD`)<8_KkrOQgvfBp4WoP=Nb
z9RH_$&;;;3znZod)}HO{KYsM6t+n+oS>pDuv&8Q1vsh5C1_lN^l1q;DpFe)o$TKV|
z9L6}Hq^_JDr8WddL*4<sH&dP8CQ8)g<=>k4%(|gXE96d{CF%hLqg`sDGBh;ALwuf>
zmd=RZnefN4H#qRzxjj^DpDrXUwAI~xBhR!U2>^;qF1O`P1BfagyR2Y}ikjcPJVf*e
z?dT*-`9l|08I~EbYtO=M25&tCl^i*W?+0_yU6tWN3;jEfJzzAWxO?qn%iEy&@rsfX
zB;+w$@J}2UI5<6a7VQWwcM^^v=F5?fI_(x{MZzNkY6-mzNEJE=K5q8+AIlwgIGU$7
zPF|+|ZyLJu#m7JBzxy9B>fa~mjz<gp^LU5Q+<x?bZz!{9r1OpJf{^1I;qEGci`hK>
z&+sQq&L4xw;{Shd;{W3F{GSa)_H3M*o68!;*hDWeGcYg&5`6N#ZSCyL9UM$Am{S|y
zOWHj36YV3bwYj))&#Fh46G*+vP?4=K7h?1ODXKpz;Z*hi^>!s7AjS~eSNqt7R6mQl
z87M}b#>t81e_%58GR)%7KKT09y3Mv-I=w<Wuwl;5!@~nZf-duyxrIfHb!q?>fX2}4
zq7T4_0Z7Bx=tac%ucZPE|Bnqm-cr8V|8oBG%g4*lD~)f9PApp(3k(i@j}3S$o0%JF
z`Qz!G9LrU<IaT)C23b0MI3}ADQc_Z0K0em{3-e}ouHF`?G$^(3c4&RhD&hO63n~TI
z%2q}t8gmTkk1!!>Il7LQSN-pY54+w{PFb;n15E&}g2Tgc?c^Oql4?<xpU$;wv2n5<
zul9C6ebTS0tXxR)IanI4+Kk)Tmj?S;5jk9LlYjSxLcYcewV}a5?i)8WV3v_Hb8|5b
zG={G^RpU}j>U?;nHQ$Pn%3wA)-@N7J<&$)5!v1CDr)_b_9aQXUM{M*Ck%NWBMkPT+
zZik_g%hV3cY(psqgYCDD#G)0%IUotqWL@%uBOdDNG&V#NTQ6ou#eU2sOW!rpNyumN
zo-9^)%X>q{L{0iADvTAH<#h2o44GG>i~F}?4q@0R29=&U-{JG(m9l)gg=#YT&6_vA
zo6@otWz3?8jGkL9R-MZzSC5HtX5WdFlQ>s*cintl{@Dh%5fvMJPif7Cix-JA(`tNT
zoZpQMpGk`&-VD20WiM)3EMZWlS||bkK(^zBI!Y|i($W(38BnU8E|emUF`C*O#c%ef
zzBn*s7_HcOS-<OBcj00uuj2s&)?xAn3^82x9?VD?A%tEs{!>P>IO(RPr6qvEQ4S2R
z(Q>cP(031uCNIpqj|>j}&|6^|kS*i4D&W`=he5Ofyay1OINHv`@f~&C3l}hcBgvRc
z-vO(&p57O{#LS!pR*j$Z)GM`xy4WuRxL!j7ml=ABZ-nRh1-UQDPO|YEZN=#N)xyEz
zcsISWwzi(LiEM@N<&QR*zX)rlfQcnP4?)t_G$+OmryHHGJ&>!Fz0skOt;#BKPtuBT
zh>MO)yag+{JGk&QaONbgjCTfBY7M}>O?r=2;fgMxk(^hqTxtH!CO*;L+RD>^+{o5e
z&1Kxgq@)bxYW}@c#;5<HB^kTc!j9##k>zu4jEIWL5T9?zB>Q-UWvK>e$fs-Y$-*;V
z|2n_7ZxQd@We~gb-4}G?qfP(v&y7_VFJ1(=E*)%X9$ulmCvKPC_C(bPq*62r8XE54
zld|}WEG`e+-Q7z*fBx8`?>OirT<5(raIos1G7)g_aB*=_C|3FOw$c-2bzR*UM1M{y
z`mYle3F4xnI%k`1_17tg?>W@Dk<%2FRJ7pm>LuKcdUxSd81hS&qbb+%HX@r0_#KR)
zFY%#rUu17n&>W$1lYunf`ipjyO~1hLF%p5z%k?HMwGDrVgFEpWE;OpYsi!BdQ)pxW
z5NbV{pa9+8b;Q{Dpq2*m^SP3I53?*AZyWBXR_`oieV$I%Bb_160-7^K7S{q$Ekp-z
zivO4~;lHQyFo-_7BD|uuwpQeMe`#~vv5s))qkq<ZBA0aVKy<Mg2Cq1BsXJV{zQQn`
z?&ZLAcy0QtejNhsy}gvW8gMAzaP%357xynz^=IXP$@-GhCEaa{?oKD6ApEA~52QQ@
z{WdP9*x+zfE5NVo>w67iu47-;{#5b*^>}Z0A2GXh34Ta;KtZ2}doIm+#8qO!p=UH8
zQ(5NiD68)U*84M@^WD_-pm~C%o|IdSA1MpQ*PgRE-=%~E+d#y5>rdiBk1->3%@HPW
ztlT&na<84ctR#JJMh=6(KA@Xx$PD4h&0Cr(09zMS934j;6Lr`|yVP$U#g&znR!dv0
z2p0twfXI3IMO9K$pY1yi(KaW(=WES8z5#nW4z}N?@8O6(%8lApXlQ0x|9je=Lz<a$
z7#IwJ9{b~IF%LfO_ZMyC(_x?%B_ud!XP%rffQg-X4Uk53f-m^n&jmJrP(N0QzIN^m
zxO(fC9y!i*jWaAZEanzwyrRVC1~@9Lr%@NU8v5PFKrswFw7K1jVhsy<c>~<>1FI9v
z%*=i~wTZxd`JQt7g{}ea6m?W}!qrguYqsZnAOA`HbED?}ThjX9oYZFAg!>Nc4sQ5y
z6~LeamjaFsz3k`)vlem|rUd??js#%VHbabnaSfBC(MO1$o}Sy{aAsxutX8fjN8mEu
zKz%pV+0m8zv)A!F1?LS)>4QCT0|u{M7<)iW7V%2+Qid(e;56pDl2Je`Iy^iq+Y9gD
z5EG}4e_tbNL|EUONXSGRmh(0S#DgDWv+ep9K2j8&;Vab&0k(U_jP!oRPgba3pn0sZ
z%zE1#t<Xh?6Y`2%URl|iGo&-KvqLX0+roP5{SGLn<%NqEum=Pa?^!Gg^WuPk0|L36
z$4dQv3U}KqLyVXmJYO?Yak#=G`QO*>?d3=`p%%~J;`4*>H^8?%ckgQ8>-7Zzxe!H+
zR(AjQ$^st6TT5d~BPYw-&4gObgskVo5`oy3z5Ql68iKKz(CrFmMSl-v0lf@1Fffp(
z-8vwBr2D^oUF+1DZN1%8^+0x7?3f=xEV*flU}D&==bLk}MGevau7_EA%|BL|b92xT
zbG*DvI#nZIdW)Yu^b*_XZ+gh40RS%~H?Oo*#*{28YTx@Llvy<9b^7aZ?@c>UO>M)H
z4sZL@%-iG3|NDKVD_~-Cb8@&JU;m(Pt6cE>#dEjG`#H~Fyujvr2qwz-!IAw%rlu4+
zAy~H&09xM0#x^p;@V_HSoYVK_xo723e^B%KVSm1d8tAb7H6asy{nlDrvpAM0S20*o
z!}eQ3?WQWM+Fa`Q>KeFNjoh8@&y9S{cJIZ&Iau%1<JHS~<c!vf95A{$e(nwU@v2tv
zF^2Cq%f99SKu8qPf9EV$AnWhi?qNE(lnO*R#rg=Ng(FZ#dXw9YQv%1zq~i#sK7qRP
zSm80sjQ@DBS<Nb`nPV(H>HhFPp!I)**8}pA8s%q3N(zrJnlgI#>N%mf4<%yEDxkh^
z<GqhZZes;9)V0zNLS~lnGn145@F-lnc1?&m)ZCbb`hiBf4$u_<We3<1%%Kpsf>7*-
zl8RDPq}HWt=fup7Z~f<ERwAudKas6wnyO)D5g7^BTcaZR`>=e~-k1WV<5t0Ky8D|W
zy!;;#zJXG2lyxL#m*mk_ef-cbo!_(mN75Ez^6Y!sz4<~RcLM4cotTJDNtE+m9pk8W
z<#~9<@O2Wqu%h4oIKPjN56}kFD<<F>Gjy%U4BUKu-v0-xBLqy!K(~{B<t;52yl8n|
z=H9^t1PD?W^e*+;?*GN-c(#4>g?yeuI3W%?O&0NAev~Nf29J$Jall@D57Vt6jEPtd
zC^3spc~w<v_mS&F__}tVg`d-T;9HyIdC0>m<*hu;`5_VlbS-^B)x?Z&y@LLW;j)<y
z4STAhHUr$V3cqK_c+b7~sx3Y9J<pUFLBrJU?mMvF_tRVu7)7LMlsmQc1!)dcaTLU_
z(zYswpM0Ol(vNGs$i)FW*k<d#l3av>oVK4xy}}uag6QN<*(M1BjQUr@p>A9Fae1ll
zZ>&58m?rExD=Q$1Vy2cmk_2Z*M|Inu$#$Rbws64cnLR(PW@cu_7O8MX78E$6B#Ox|
z%k9^38{w$M6_0Vv<oY@#0_=DBJgI-lF%rcO6EctAa1&z=-6I$n%pR3}=2^Rkk_PhB
zR&Roy$W-h{N$;6=S;{^drDO*S4V&YGp%Ohqr+?wX#i;1$%(wI`bNGZ$5jlK%g+KZ>
zyTrQWlaLvMgVTW~TZ27U(fUW9DTIMQ=rh#CX)#l;{?eIyqWt2E7>$X5o%Ey4Z!dyF
zLv;XRIXslKZrTq8RlHA#U)|ch7|zGZsf+3rwBFiGFSG12*_7ZH@0$w~TN=%b^dH&^
z5fT>6e;DL0>9<q{Zdq~m*YU$0{dKw?S31=cJ~R0=J1}G%*<;n;$tvgU>Z<O&&w<vj
z*L2_B_QtQAE-C*!P?#tOFLQKCy{x4CL>siPGRO51@Ja1&U;gZfB{{#BIcCR=vK@Km
zrMkB4$Hz<Y{d(cwTvuJ>|LBPOKcjvB(@@mo9-OTuVpq*xsLUNBI!EEGWm|KWP|zaU
zT0fpELE4@xSA@ZOqWMq&Y-$aN9)E0fprD|@%+j(bPe4HETB(_xO|g!mVu^rhC&cLZ
ziMZbYN!YkPAvdJ86~N@S;o<M{dPPP&`0C#klgOu6^yB+?`2n$kf#v^v=`HxukiOwO
zo3r70Aamq`!*HpuBAi)tBT7m1icY?+3Nk7op&)982{=IM6c~Hr=tT8b0XFKO>dLyT
z!|p#N+?LD?a2n4?M@L1TO?lE^7rDQ^y&cFL85^6s+CY+18YkbsB4ETlOG~}g`;Vu7
zX*4`PeS4bgL!a=iq!=LRLaY@g8BU)es4u)yPn7kLv+PQWhF}YHwCwE*5o1}HYWI~i
zRsTfD>=}OcfHKW?6q>u#^7z4Kl>-0L&#TtPD)8y$n73a>X0QkB!Zzmx)Im+9e#hk)
zJS?66sSRW&4%ipD8d3k{KQgnkQSc7x*HfOat>p;fO9Z}ZhO&qch)?)s#_$_*-@T!6
zE9qLUwN};@09P_HN?~#Lo|Sx;kyQl`q8=2sf;nmz)Xom`;n8KFj>8-5%v#)^XRMSz
z;9CF?rwtGZ$HW&-tc-r%d`t3+-)n2Z9lTvJtk}gL$uaU;=|xCGp&0KKUL7<XBcgcR
zBrZLv@_U@47~QtN9FW}FUX$xjUh$~A<FEOOl;pK-O<(U&Tw5DISH*tm8k&e7upw2`
zpAIvubI+7?U&<jJFf4plU>989Kc400$zK|#V#K9>9}X%g)HLg}P}T`g8gtv<vJPcl
zuLb^7Ty`BS@;iKUk(r)ZTD{P|5R{&VzJ6tA!h4`#M=p8wYk@CdGi@wSpIy;KAP`CG
zBNe9+Yoyoxl}@)>6={c9H`HW~i-DUf52&0n9>t!Xo(nix{}>28%SO)Erl!p@9NJKi
zr5>IQ$O#N7qhF$JqZg3&;lnu?5?mz7dmk3cDl3nSiik*g_K-~nD3D2B)vhZA;kt$0
znM$bsiXG@<NnB={lcTG;g}j9Uiu!<!b}dcm_2-&dmjPX|AFFP~c3{*=UCB~Kb|d9H
zlDyTGN^!t2FhOpuh)>MX=|QXy>85}FYFn63!j;>NY?hf;yTX)Y^|Ri+`;*_WUK?YB
z8~D-FWlg|uZfwzw)`LnPp^&oB>(B=dodF|L!w%x@vis`SuAxM{rlMH3y)4%sP%O_<
z%TAup64ZOjc@pgR!wy$yOvhUI1eqV759$^^I{PzEJGbD(sRJeCWqx1rfEt%eU@0RY
z)<>hK_r69qY)mkxfb)_Y#w@gT13>yY{ATCK>JucPhFt#ux4V%qXpJ2X?`R0WPD+^T
z3`p;uCd2ODy&L@gy(rm49W5sI_@HZM)#a#<S0^tc!tV<O>@tKgnCnujYH}hSK<QCL
zB@B64u#MfB$*gzg{oLJ{1)vB+c<yoQ&vz!a<Hm__9|R&#=3uL#BEg{}55o-gj>@~I
zr%|(QcWJVLU+thLy@iQ|g-=aQjp9hGjFlGs5Ur`HiTCIqwI7E^c<8CZ`uBGv$kvI6
zl7OFWmS8Z^$;ruL<j+;gc<StE`_2-CJD@(qD2UzI1@)&suiw%`VCT=?qJ>(TX-7eF
zpEbQbU4GWKgWlMCYguDKqUgYH(}8*WN7E9JM;IQn_rGR@a>KyD*!H6OjFbrD;aVai
zrFI4e0*>&WJ9h&g0R8zm3qX3zEM@-zOqa3GPSC<8k*w1ZCn`FhIW$}vP%&V)YXsJt
z*Rex<u0AVp_3)Z=0m&6p{>S07Bfq(VYw*eiyG==)N5N>0E)3B5o@I$rrV>GYS<=R9
zc!j-NljRXU0SV+Ukrx9ZE9YpnCp+!pq{wbMh0A6~Qa?Y5s=Sh_uwFg-o8MqR$;jAv
z^QWl$=+zD{YxmTFm<%9RS>t2Sw7~TOKaQ6PQacDo>geg)^vnY&2$<W7M)$Kpqk0cY
zK74=B73;P__Xu><)8#>LX8h%HV3C}GNt@L{Y**79>W_4#PI~+(dU1ucMq)%}T*+3|
z)hf#Jb{%xAyYnpV%(>;Fpdk7V;q6?@q<+Aqp`PTwb&5=k@nZgkZ{GX?y|a5`XlMjK
zbiV6yrG=H%C*SzJCrEJC%*@Vm04V3;_-h)UzE*r_iK9%#(nwMG)bzC5M9P8N)(;Jp
zcnNfOz-w?#08acw{73(Qt|w=^9s>2zNa~J9TmKceoiRU=XNPpm+iSP-bnB|eeSQ3s
z)+shH5ZtZT8V_9W?XU6U2w;87ZMSj2>%f*;hD(3BM4gh%)KOBhC2<MM0pz~U%Y#X-
zVmnIrL7(Q`O2EC2?8{cWGUyM?UszR97n2SsUy7w(a64TSsRghNhoPY%{k8oF8KeDF
z3QM_jm@?p3Z*uA^{ZC=BdsY=d&Pr5GxR+Yzv%UCj{UUs?hc{8uP4SD|7sSF)N~35~
z(^qWDvr|J-gB0DdxupfK{_TdSLyt0YW*Toi`K$3X)_W|hF%e#e2nA1Lv&n8avuLnp
zpWM*yH*kY899T#-U{r3c2S)?1hH}omJ?Hm?9liVHV4HvgieTBo%Hs|EG8X_{a}r?1
zt%!M|!Qo2XF&%~u)$Z|PXdS?gS4L{oz`<p1*sAL>?xv86pi=mh%3!}UN(NQ_GCzp_
z%&xkxj`Q>EXmy}R$}|^V6~}jafV@P`07lOa49he)Uv~P0k{sygk?HB_F}`c{B`zf#
zv-SVF9E}D_lLSm^t?E0)VmA)$g<{S8UN>1Xvdg5kqXgSW+Le+*r7xX#dU8ceAP*?L
z!J!fYfVr?rG8tsV0Tr?X2b79|0kJ@4y{`16goFgw-L+r7c5ZlVG~kgynnBci%nW0)
zEgv?UNO6l#xJ;O>Q0m{X(H$^T@Gm$ZwLnYgPm1{0Lz>zq)GxFdH$E4Ng}{J<RqZ^h
zSiilH(<E6et8GWHLN5wu+3em5A9EA{<=K${eTdv@?`q)X$(`(;+mGV_pqM4^XTn{j
zU%$?hog2&sT01*+ffci{PNXISKoRuz^|?8aeV8s@jAv;yGZFX*@-poh%ete@q7Mdp
zrqjSIMMAEfD{qDu>8&P#T2LpQ9p!_Sl$Q^*Msg9i<LN>~*{Sszs6j9}9~`b00icS(
z=3BokmDs|vG7(HwI!4#G)`&pBY!Y(~D++ae4^!jB9SY0(UHRdayKgtEj1R86`TF{P
zMs&8ewr=_u86{9k7mh*j5R?=4lHa`f_PoP1qPY0Zp4cWpVf>U8_!yKngC9`_s+{YZ
zU}`5Cf$1k&+Vw-oe1=X=PSw79CCuX1w)MCSYXY$+U15{h5Dn0MTj|H_xAcD6ug{Jb
zeL1y?dl4>Oc!r)i$LR!~%23rc+r}7TzP)+m<t3`q0T%^HhTM|}34q5>w<vcHcMas&
zR0s?&lY;=m$8eby0i6V!rDEgNzjJ4Y9g5hZ?=t;(bO>>CIu!%@iQ5J~ry9tQ&CQQN
zUoUfNLNVXnFaXwowq<8RPaSDh<I$rxCo5LD2bcq{TxKrpc_H7{)bubYow40YBrh+d
zRMKrJCzPe(wkA5}M_XI(cY0IAOwPRP@{ha5$|^eHY-7aMVKs9RW{m%sH}Ewy6~eFU
zz`f%*)nW^bYutI}^p!v2masM1X=!O2*y}7T`JmVq2Yb@Gs4CR`XHhk*x4BdkA|M#M
zk?kzfg{~x>CqLLWSCf*GnD^KI1~BN3T4jkxmyEG8jNgj36o5fRWEgumzitPMxNk0i
zl?y@a4Sj=7FCehh+eR5L(I?=CM>o+UYgKU-R$0Q9Q^BGEa$lRi&fxL+kzA_0e0)lj
z_(7Nq@PG{$pALq46f<e(XI`!=)xS~mu<ZG)GifIvIS<yy3E*lJq^@Sc@AR27h@s%j
zecv&1)ZeHS*hQ1u1Cl^W)^zM9`0jX|4O*+Rpw!>}+>)j4@)w=CT3jL0j^7JLOO-AQ
zdRM5ExKxt3VemT+avyQ~`5+Ep)xnkT`ZQpHzDw`gJ*$`F@Lh`}AK(;8`t6sU6DtRM
zqf^{sZp#2<$yVtS99FXPvGosqpzFJl*JF<5Z4lnbF+J3K)5(z-X-F^b;;1=zH-?sh
zcA&~-zJD%kz}ch7P9}1;`g|8Ozg3G9hU`q1Eiz~K%~6phCCn>{AhjFh{D_M#6($Li
z21pTdhnKU52X2E=Z)Opf0eeC5t3eRx4Igz|rQC1!e{6NIy;AM?>nmEy1T-lxyNUWb
zpcZ}xi8XM@an?o5`>)pc#5YB=zLM&1n4s}n->dfh6%0v~cGAq(EgXRQ9oUfTPa!k@
zy4aHM&`|HS9~ys0tSgRs0h$24WxY3T$oTkp|DzQyus(}D&zdfI|D!A*AV84|ot>O|
z#-RsW$t_9Ty$Lol2AN<Mxq(o>v(1_UA|61EUcfsvXu}b4`@||eFqN5GkB;xXb_2}R
z5kpmUhRb~C;_DrhB68exBMH%8kgD*OF)}111Y*74zxX)GrESJqQB4h1T3X6>O-32)
za_rZ~4iu2)k8(U#X-6Ye>LiIKFqIB6ZO4kh*8QCQEqpqN8})-F7bPNifs9J=s%4MX
zv_8(a|NIFu5J*!?S!~X)EdQOY=<x7xhR4@?m0V1C0=bT#{E{3tPp5sJAqFoWb~UrI
z(sOUuyS+U>7qW;KY~b1UjTd+L2m!j;6F^IPV}+fVHxDZ><Q^Z#+D5aIPbK0#b=73|
zl#`m=!JUUdlfi+qw?EY!;!PO(C>ifLkj6~y;`f{N3Qt{zNxE%Bv0VckD(EH$jHSV(
zXwRWjD^ZdVn3PUo9wl2)KVpYGtoP$B1>!<+i>vguXKp_=L_otE<Ggn%_1}9^Qur@7
z7#FHO-qJ?HMPa=tmIfqQHz4rM8*YA4W8}=tOolg`8^|9Og66T%Swqk!pV(2PM*Q4~
zT<`|_*MMfUFCRPR_xFAQ@;UX#Z5*Jx)<3#~wtl{|oFSOhsJ}oWkrWAPe<}d)y0<Z_
zS$M1*XhSXuJ+0~lMi@P_SVVYu1%OCxEiDQ%_2b}3prmU+uW<>Gm**1`8%QfPq|S3=
zgI*xRZ-L?HFE%z}JNZV%@d}uS{@02>!{fnsZ0XSixBLT3_;O74)usRY)5-shBmdCg
ziaH!<`u*va9>m2Fg-~X%w#>K?iUkH!R8`egbPf(<4e0^QJC%k^V_fB&`GN0P_?eB?
z2`XoG;qARqVQ+6AcgLyr3<L8&K0XHzLR)|S?BAI<O5s=yNTItT5ak?rT+TPwK{<au
z8MA)z`(o^Qwf4?VRb<&M$Dos^Jh(wUb|%-Plv;KvA`t_5JmTVdAhZGpeOQC5K^5uz
zIwo8eo;%yF8#Qzs+k3f@`Sp+6I>vcR>Ct@sCFXRn-f-E&j6Iht+!7L1>eDu0lMK07
z<bnN)%$Q6WW0rOQ7o65gXllEoQNOGGu1iGRepG+LcPAQNZkq~@tx!*k3~=t6ECBNh
z*~w`=^`@2i;P9StrK23HWI1kMut6P^Tc*AQ^?7o*+uRy8^WFK;@XgN%Tp=2S>;RfQ
zTjtWz$+Ep;H^;|ry2?`SFg8_ma2A@AVl20@wbj3b$=3$HP%rnf@~{2R?DnNpO6sJP
z?8hm%nzc`=m>_=Lg`M8^nH$O$-L3z!v0)!K@2_oEHc(_(M8u0R-LoeSM=uTLrf;uG
zrL?g5)qcbO=KQaN6#Rzz^R-r^`};%8jg>yC#NX8cks#b+U$!XX+a|~(B9gW_RhVr$
zvTa1H*6vqTSMhRiJbw1<m!u0P$kwXn&1<NEFX*8P6E9saGtAA+_4@8GaXIJ94x{Gu
z_fQBQkD130**(wW6-~$iFk^4@%ha!5six#z(2Z?>q|-7ocJ-N5*c;pJA`+>U1TvqR
zXk*fQkhOxqz(puITT%DJt1!(65B_~i@1+fc!Mx@cK{`qXUiAPaV5~VYzQeR=gbMx+
z9I4oAvF66dMOVQJ1bHDQk78gRR=eRj^~7-xh_V5MT0p{&4I*nXpc=t?D<*v71%cSI
zF8Dz>h%Y_a#{`6jyZZbWdpUr+9DDQT%`)3PO>CjD;VlK(YykP&`vif-4dOvsAVNp6
z_39+Zb|E$mW4Fl*56#Vt0MFI3ADmBYrgPidFz-l`$o@7*)q7KJ@@(Iz0CmdmT6q%&
zWRE4F*f^ntfs<1L7?U6jUbZ%NxYDU3vRf~Qq}1mOo!(JffX&Nsf16YYrESbCEPN{J
zl5%Lc1*={?I-oV4rchEH8(A^3jPsTYAcLU~!pU6C-@b8kat<8&$YF*DnO0;Y8@l$?
zy!WsuSML3kn`vJ$!rbb~zU8`g-o)FHaOwh)*_y|{OzAeMAj>>8jv~_kiR4rh3IF^E
zRcH(W0@fEv4}wwsb|G#Upl{+(Ho0!r*G3nOUFb>IZmtRj(WZPFvYpp(I2$x;a8TdN
z+k2_7+yk+i<oCF6Xl!h(Hhs>0bKu|F8fV3{0m|Lb4b>?KDu2ppQ;o(}n6lS3Y(b%t
z%Xb5VW+zstvvnWF?U8Iv@Uj#CI>3M8bh)_I;C*VYxZ6cu{xJ**f{yx6eHsTwyE7HK
z*eJ|&LY$oSEL^&P4O)X&-;#swcE3hK02>3=uo5lpP-cfAxqEQ^g_#{`z9GzG!ZhRa
z^mmHKkMRh22b?HVfTp!vOybbc0}q?Aw_Q3l?&y;^-xmZM_-<$Njh10GJsYJ)_PGhm
z&bU;d4~rGK__ef!BBCKIEG!*K!b&#|{w4Ma%I3J?b;W(>8ajDkXI{$*Td4GPrrbYG
z7|p8MG}W7JQ=MyQ`e4w<ROu4GO<?RnX0Enkv7%i@FIqx-5>OUb5Oy%3xRNMjLK+~N
zHh`H`yDlO@*Kz7Q_9yhy{ujc2_AE>@rbmB4qO{QO)sOM<`o`^uh*7~mMXh@?x?N0m
zx=%ga>cCBa03O4h+j1IvJy{O9HPzpWSi)+DGq4n1;Z*K_`HDnbD^B(LE<^koDS4GG
z_3SEp8c1f^jD7El1_IgHGqMmVA0MdH)X+)XIePj?2Vr&?GCDdM`0{2B4h{?gAm2$l
z+ToNA=g4(vkfG#k{9aSYrh=&5T-gIMJwRYEzy0l=U8jp%-vW|)*QdFnoK_k~d9+OX
z$2@j=-mD8+4K{`6z==!Vn=5->-(~l<E>hB&fIam6T4)M185tgiGfMBiq{vN^lami{
z)HCe9#wB2{m1x#^yTN*gGD=UEb0v0=E~tYJL<^lKPR?vC_MI+Imhh9;B;bV~N5{qG
z<?9vk0%snm%dn~%F$y;p{E(b8Xk!^&aNt`9&ZZnU(*p5udXP#LlriOk4cAyEgWNT)
zEnL^whOYjX@Y|rke7UAGB_73kW7Td<?CeDVoKYw`#aL~@8LrkHn4}`=>=h<e(Iyyq
zBruHm!CMaAsY$Xs20>OmUz_J}`Om|epHPDVW(yC!UA`CsFSh|=-7^xhH6bVyX?aU+
zbHW0oU%XcM<0}wz%h$w>OZ^VEDKZje0t)gC;-D^JVXUD%+I7?o%%o67r(c-N=nQ?b
zS2I@Qq0~mwb^%dBChvcivo^=PaIAT5D?0IGUv8Y<mavm+)Qs?EBZ7XC!@*u#w47iI
z)QrglS&tiP4Ilv=S5&H!CEXCO%xN$&xi-`k6uz)A+rF{Afdn`9_|f&==BN<FKtb&D
z7h&zdEev9Kd^vZnzh8S3(_<Bd!KkEu$8P3;AU7AQjPE^D!!d8S*~x{JbcL|ERn{u^
zmBpd%>%}uZ1j7PWfPVY3R8c^r05R4S@wqyI*I!ar8xDSDO;aC>yU_Vc@h4?YuvF|I
z3ymy-N-h<HSfft76x?S2!y@z1j~C(z?c?L)wJcMp<N%=Keo5^hiYh6V^N4H5O1y~q
z+QfJ?>)%=s3CzQHuY-V!lFq3TzwmTd4hZu2@uMBkD#gTKw$>Wmv>7XjvFY+A&6{2;
z-Dvo3Zf?#26S`^}Qa2Z!kGkZ+sfuJ`xtM>M-*Dk`OVGvRn1!NBmDFnv85ubw2yJ$-
zY{!b3SNHbzQs@bMecidL2G@zp20}6TLl9!TwhF{?F;>5(SUTt3S6uo*CapP70!&ob
zDuqphmjHlTomnaDG#7B;!i5kDsr&kMX5^<H0LI#nzTE(>HL}P-81VC+FAZ-bEAp_K
zxMYk*<YAF;b93_|SR}+~#^*hg$gC=yLO5FouAx%E^vLongff``9s(mC`6+^1uqZ3f
zApfmuqNL8#WJ;eo{PY+6V2Bxn^&iKR$m+JX1(ZxcU0vO9nKgg-KHJ<;x<Y9EuRXrr
z<m3zi`$i;a3LsCTQ(<BZl5^ZsPv3~y?`qzXm&v~s6u$H`R=B!?3=MwwPJMElwHZFW
z`t?DRsQ1q&UO=RG+<JB`R9?r;Nf*SdFiz8`$9dQbAS*ju`AaGEIx!)PRU#`SH+QKg
zy++31AoqiOIt9&qOqX9;_?WJ!q}DgX6A4sQ0buJjKP&zm4ICqh!I6CPd|l={jxPs{
zq?)KS=4G!s;bhE3F4dg;>KvfW&pp(lLwar(fVVDkjBCn~5FjaoV`>gxaZI~AJjg|$
z&GNt`{iVzGMIA=hG68PLV)s8(m6Q9zEbLt{ZczDT+ufZ)yv^g4hk;_~f3UJcQB2l=
zVzR%#1cW+JGBEHO10%juYPB0A4HZ8Ey(G=!a{HGrCVaXAsJrdl0c09L{doELjaLP{
zO}Doh;N{PAZEbDKIs$4;e28u#12(vNwELOUOTC#&paT|##{^`E=HuXp*Qq;V^c@vW
zgS9i5c_nNyn`B(#*;5m^Bx}9#2-f?r7hQ1efT`QUpd5ujo;rBw;91{B@sbC=if@i8
z0h13PJaaH_;YA+xcR`H0*JHxpbkf&G))>9bI*fncl1x#>WEttHS363SmR`CNaJC(U
zhMn_&Sum*5N`2Qn1_TMiqdjmSH+5K+7>Q3+S&DkKL14XmoAa3E*Nm9O?Bq?~_wvXk
z@th(VKf*wW7MBuou0cYLKp?=6eg#XD;zX<qz$N(v1d@JJPo|u~pi%pYdSl>sRl2p<
zbOK*xWu)SYN0l@sWQpARnsH@gbk)zp!KJbU@PYjumK4yVQ4|0NWDf+k4O^Y<5U?`g
zaUV%B1&J%*t(XQ7n4B}XNPV2i)nHPvx`DHcOJ7$52?91ARMNKc0ppuipdH2glQbw)
z9Q6C6&B2S0<6MA(0RoO^GqbXm#_LZ3Px3aqDZ3wO1w~2Afwx-#BF>CTIu0#G=}`#R
zgk>DZ;%Jr2{v;_AnX!Ya&1M$0S@`&?3A`<K6BZ}q=KXO#0R(>l!I)iME&$|Q3s7T>
zV~-&+qd8=P<F`dBZl+bFTBoO}wb}5%rG^fqwPi^AIsSf(ZcF_z@G49j7OR8D*>wXV
zqdl`HBrI$$6gz_(QWHf~Qi2k*)6?k>&j$fJo+ZHT81Wz0P&_|h!qY1InRgn&At9*n
zA`q*KQ$*GuZNxwCs_@#p4AV@P5ADy>;Y*abn0>b^obMz2H>=huJQY*=-jcW#*)=0!
zD=sBfGq?#nqqrjQ_C?No5^FB_pZs~}sdEer7s;m|GqkwW7C)voUGn1AM3$}NiE-pP
zX>9Ja$$|uB-8qyd`oNosQ4m%MX8=Qdlv`eeqZI$+j`E;P&-1>cXcX5sUM8_fJOFbP
z3Gu61=i752>OFb#4s=lt3O@iOL=V(c9*8)A1e7X~-LVM50~8aA)dvU=h2Z^6I+it}
z*unbnsWv);vxcgOcYW}b2h~d_$_YX{AR?M3cvPW^5;PU~uVbuv<5UbD*b@Cfkl)+&
zYzJ=+0{Il2ApGFHzQ>Q{d3bpxcP!1yfJn(NS#x}uLm!=m-&A`t8JM9BhwRSBY+HTc
zzqHyc!nvY^I$1O+p68I?uKkX?G&lX{Cytl@<?H`-_l2U|PGmL%E=-|a!20OV`Iv2#
zS4Ck~$P55MG9lRugP*UGjz-vN*BHCOTR3f%&#6~YH=zcZvTw3F<3T<gs8e|IP|ap>
zPCF@ddgu0^x+Q-E=cMGk_i^K`xHV%xr<>h;IRKt(o%PmYF2>vf5$7#}8!gc?$Zjsd
zI-e44zEtsz$tdFSiSrkuW1sm4e9~e&5p?Zb%XkrfgLZC6W@7zt+AnE-&wXiBLq<kM
zHB`cbkWde=e;{mGn-1pF#^?t1wkkiqdHZ%u|3o{bKPj(f&#tWS!-k9fUk?;br0gcH
zxBdLl`~D)x!(958kDHrYy<)<t=H{*VeCyIIq`(>e)lSots?z!B;bWdp)8*Xs$8G;3
z|G0*yNz5WSg0Ow>X7RW?{%N3B&i3zaTxzrFUpSAz)dS;yGwU9`uYUyS;1xEy$KS8K
zJNkE#b)iQ3+xp!NVNok}(?#pWCDPAQX0a{v?50aFNw2Alw6pX*n_cvjEG^KzGs0y_
zPojAPjhfo18>KPyYx=2}$>1+QZ#UK{(ATihEz-etG9sqmh6ZjIX;p+FaPPUx{5qB}
z-YJ_6WsCDFfCJ;wIOt~Q=RNp;8}j0RheK~_7mo6KlV9)X@J)%4DmN7B?l?LELw=E1
z22AQS`1uOs?Y$$@3tQ_Y;l%Y4HQMVHR;?!~Z_-JXul7fVd;G_dZw$R5BBGqd8=Y4h
z6X{Z2X2VndtjSa}BxZ+KLc+k$M>ZQ^`1WC&fr8X#r7QX1wNP5%EP}V6+*gLt(=A%A
zn;qi$idL5}R$fJM3P)P;;>|o<T=OOCpYP(av#&byG}5chBFewn?eVcQQdiaUPgz;n
zuF)?Sn3?m~{r6Ro{VTs(Fg`nk_F)kBDNvI!%zF8f2G%PjY-!z^wi6W}&j+jCn~Nv*
z_d$t=jYr4~;AXh_>^db%du^0>Q7*c9<!9eesU^0M=+|70o0Zqv&MJ{a`4pD5Q?EE#
zeUX`&vo!zpErbF$FR$V&xmT*huAe`&m6bnS*ygIN&S*+mc!kWEHND+%6C4$<1kX9X
z;Un!!XMg%M*go8q1kE>C{W&mR=OgMvxJ_BNu5En!`t*2h!NmR`J-k9pey99hZz;*!
zruR*&J<h)_Rw^LO*`*8dWUKqZK`qoSn4&yu+>%EPCQ$Ea^oddZjF^y6?;;S0PQE^Q
z>Dsl$C1-}&wz0IZL;?z;k|;!KbDuEm2O$So@0<0DMStoY*{;O7#T^bX!Frp*r87#*
z+f91rS6UsEkkvBX*hOYjRw;QpR4sm&G~^F#&#x@1;bcAzPEOvtcU#v1)sItdK1gmQ
zckb!cY<5bOnJwP58~OZLw#BX+#1$_wF=-=+)O)k{6<B@x7jM8idqH28$u(fjuSHn3
zT=ftMw-ta5*IJhX@ukqpVYn<70#;gKRgl!BGjCYy_<o;=&v&k$b=`q_@Q7KE%va7b
zvVqtH5~MBMaAV}|a`Z@uu?y(VFBCe6bLjV|)i$2@aI!~dgJ#nR3!=w|4zrwY?(MhC
zvca#Xs<z{;2;MjDM^NR*(OqeRXa~O`CJKT!s4HyTj;p{%P&{eeqC~(@19|7OD4nfo
z1*bd`Btz-4=WAzLboj6<pB1oavvtF$Vg<7If+-RrC@5Gb>Ha~?ZqFcp;4owU>^7=r
zx5cz&>zWqv@Z}M+?6ZXOmBBPd*`sh??VJ=~1X<(5?(rYYJk%+;YBU+=@FBohYy9Ze
zjDCshN8O@>*7bvTADcwYm&{ruQSJpyy7>^`9)>bQH4V!hKyIg~d!<7B_Es!eIvWz(
zVL`9Cffe~r7JywsnME9p+Yh}<xvYKFtVPSt9p9Nd=!vW8c2eb5a|Ho>ckZl}R3))@
zo2IvXa7L`4-qK_9*!3t?rYIeilPEP!{Mz^h==FlG$Lcg;qw-}Y0t0e_V6tg#8(e?3
zJjX~vvuAh2|Mo(y3l#gc$Amci4Ls9Hnk;Tc=x#Cao2$eMCQ>X^;%u7r_#iOBdXYyJ
z*lLw<51$8u=Yn-xRCn+__Tffiz^wO1bP?+Iai<vZ1BMRR60&~|*ox4>*&AL5yE%HR
zCZ`Lpu{S8WEe*r~tMUvul$Ahx51UMLPaGbu!?)Gg(B|tPDPj=df^I8^FBy)!)tltc
zx$H}Mu4jHhj1;2JFkH=GBkx3kw^q|eCj@`8?)Qcu`m)ex=zZnJ*!+}FRE+1rBDsvj
z>gW5QQO?qjjV0Z%uil&rEL>S#EeaTQ%l6-Wmf44)wp;ZJ(gIJbhy#WkFfP?3?qbrT
z{pK#4{v5(q0_xw$7cF6z2K}Y?Dl<Y_EQ9$#DSLT&mFO3=$wc#qG~WcJ0zLfkFizMe
z40$24%yNf~|6uKf+tTP_$!E~y7SkC~i$6Zz8ZLLM3A-+nc6#&5(%|P8l>LzxsK@Wv
zF;?TAkHF+@@Zi&zM88sm1wWsiToY0?GGo~7CT;EY$`rWZP_cjdWqzZAsOeldu@4a#
zr024<Cn$q&g5L>9Ew>vh{Pys@x5xNN(Ei&;MhqEcHzMY`;sNG$UzjWk5^>wm7Co>O
z4%B1x_<6p<D}*vsf0PJN6|%Z9e7XhPfGmTl&PGs8)s&1%-?K{ARj#r>KuDC%HasLG
z(sf;@qhg{*eO=1qlEj28p<3%p13QMNz@dZ%So3*rbbD09$1-A9f2!9*O?#Vf-HW|u
zfFeo(i*9<}H-4pL!cHvyL!gn2&tzkfWWZ7FO$1icljyiIR-=|(s+JFkt)ilILA-r6
zh@_G<3*uayov+IpqIIu@c8*zVa+C$z*Y8=d-gBG`E^pJ}&0khAcU!}~4(swKk>=Od
z5Q_Vxv{AmKng|U2nb%sP$E(^4R`v6cEfNBhC7`JP3f_K00$RC3EqLw3y-xbj9J)Rj
z8Q4hyx#!d0Dk1&%*SR>jxUN)Lz*;Me&*{jYW6vF{**RR?L>h3(%9{G@ENiVuVstjk
z<AzIpD6CD~t&%JQTBq^3r=5v{A2EgS_SG)$#fAV2jFHy|yylbVVm_CA&|qqZR@`;+
zv|&C+TXobv<%sqgJ)hd&F9OpHq`gGDB5b?RF6lD&fpTjdJ|2Y+9`Ks<WINBc#qAKT
zWApXYES>%P3@>IgNt$-A7Nxrr7v7KAH!SZ&MKR6J&hp&5r_a-(bv>ZBSCMdY0BFOx
zs$-If!Qxljgt2VX?G-(c4bjQAU%A;7L`GwcQArr@4!&^P%#4f$5O&JbD1AF!s)S-<
zm-bu>KGLH|9IR6Ad4S&*UZFOOmvmq*VPxo=oWG6gyJdjw&pa3q0pStKtI|c2U=zL#
z4_>s?Wq}>HL@}7d^8>n}p@feRB7UT(dyKD_y(q(7^eIe9Nz0Rn$TP|;S;7`^g*2y;
z{Wo&GWk-}$;>C3I^*`-z`{;JuA}{tHo*uf?U<mR=dZ0*HpWI7n3Sw;Un$QEE>nSu2
zWRKsUfRDK?8YHibD0qxI)qKql!Y^7Tfvjy57J+SPml-JaOU5R8jHiPB-yM<qaBH(m
zmTz4>O$lE|8W=(J6=F=hy>h_U@=ZwIad#F~M6CuJS1hT8lX8dcO7V3G=SN7_oiUFq
zLxQAlf7uO{VAp*-JuBI~ww}2WdSR*0Qp36`z|QL%k~ppMt2^aOb3g*SOu~$22bnaZ
z1>QffZpN7|?VTWTz#P<K^W1C(tcO43!na$4^7I-C!`RiFtdIU(9>;(-*xv&LPV>+D
zW)&mR5svri9UO9?HMA37G}Y0BxczMryZ0k$UUYLyvh25YIz|vnbMX#Hh)S~LLp-hs
zx9nBA^{a$2Dh8SL<+X{c7j@-!g#G$c<WBkR4{^bZt?B_Zi*hQNl=7h-+}wY?{q7<D
znO(}gZ17Z2c&(>LYgukz;*zZAlQM|)RQ$z|x`AZyK81eBgdsRPrs(QSGqvW!ylvVt
z+GT);rt9on@{>~#TX@l_`O|s9@P1T#1FvH}htic?jcNI%p1SOEd!Ail_c5kg<q;A2
zNmQ~IR<+>H@r}53&&mBmjhMr50f<nJInA%o{*}8>899p__wxqU#1~}7&!5U2{+40&
zq`8{*@kh5{&vcsl%k5a|c6<K@5C`VK3xPaxng3Z+U<xvE{W(szKowsoq+aah$tvGR
z++t$7KnK!W-L3igLAm++_v-3Vb1<4Ate5V!Y31_ffE`@wYcxGxeBo%DmDm&1UihaT
zWD%%B@XDtj2{s-L?zR&=FlHvG`pR%cO~J%Vxh~UkPU;03$<BN4)U9c0X?s2R8hSd!
zCkk3>psBlwH8bE?aR}Ij9~~W0TK&;#D_e7%%eoI8wi5PYc4j8$VNDotc%u-wTyz&I
zAWvArvcKN$thuG7Y5MQ?<C1s*!j?DmuRnL@p*2qPHh8DP+w%KuSnr2evhE)3EOGGS
z7`Tie%zG~r6#|{_zR|Qb;?MwoWrtokv`59?-#<=>$Peo+v+ny#@A!5vojdc|c5hEe
zr$BvOGz5Dy9t$59y`*&5>4~wM(0%qS6}(-UYoG5HfAZ8R3jE(920H{pCqOR3taPY<
zIk`i--Zx@+0;fvJm)6%Eh*)gd;3kjXiF%p6xxTG&&B5UsbH9z1cx+u%q@N-XTq4Ho
z1y_a>bZ=DtD`T|%idTo$$#>uwy4$1X^5N*b`h#ELj&r-~Owc_~kHjakzueKswetPh
zovSl|yvdSjh6*l&aqX<cuR<b+lsF?{oaya5<Hr{BA^+gUrg`a*St;DC0zYLFx8mN4
z`1}L$Im+FSIuH$8&<gMIPxk-$Wk!yDZkLKZy}ZD0IA}pjw8ik_T_;jf{sDN+SLHb2
zNh4nW{Os+>iKGnNgeMW+pXN=(FScMOuJA$jN<UAV=xjw?p|vAV3@e0QOATV%!-?kq
z)<mG6IC7gvE26-~<0nrk)j_@D(w#O&9CBfO^2{Pni$Ou%fJr*<zUxes^03PnVj2gD
z5-4<}q5_lye|~dba=>^7pbi?k7BL_6yiOt8F;_IyU|{Pe-9#uDtyhiN>&3^cM59nU
zIHuz{2yVU&tM~T@>_s({S$seJOMAhHgF_04ZM05Ibgl0JRzxQ192J6-b${olHT-DL
zxejVxO@WWlqetSgA2_qWqs&%7{tdWGV)5v2b06459&tt*n78jQG$aQU+)X#_+qRPe
zJ?VEbRfffKv`%!X0f9OAL$J1$RZbYIR2Bg9i`wqxoe2`@FKGf=UOG*;@QUvwZ~`j?
z?6bvaJ%!wZX=pTv;b(QkErg1j>U9yifSzJtAy}GrmQgc*03GEcrZ?fgiH^B#l6QFM
z3%jy<&&zu7@5h*Tn(*Pj(8i_ZKb{{jE@yvo`y3}R9iC#g^`kwUFIHLpF(VuJeIct_
z^72{Z*LSkE38dNJVLE4w_xDit{D)^5bDg8x04T(GZ+psvl*?n8>B{vN(Vsp+0s;cY
zaH(ULL`g#q4T;f^Sp$K$JXKXylt|HdoR9_x@M8z{S|s*WV<lm^xz^r38zP}BvbwPM
zFxo<WIY2Kc%r;9kwopaZp?(+T8{H6emTftYi@f^R-roLFcj_a4gTo{-hhV;q)!#My
zg&THs!e7!$=phA1q;u|oh;;RVfZaG#_Gqc)?EI?f<fM))dZ2LE*gI>d51C=&m(&x)
zep?H4TgYP75lI_(Q$(Ng2KjL2bg>n9z7no8Y#n%E1JM7cz3&W*D%-Zjrj=G3{Zs@*
zfo=hntf-)p6a*}goROg993)9B3>ZKJiX<pO6cCCaNkS1tK(d4)=bRBFhvLp%Ties$
z?R#FobMAZJ{c(S(WrbaPtu@0KV~+BhYpfBM8edY^Q~x?zw}-n0A_DbMewd3sBO#)I
zwCG?!Buo%ZdGqlqHT}7g3Er}B*L;z733m3xNs^&F8?#K&<XLH@oJuA|E%PK-2dwm?
zeM}rMK7sHQZVWx#_Xh?w<M@oK$Cm}I+n|+wCgLx;_I!C87n9(+*i=+lJdP~%^>99%
z56SJV%MEIX);9|UZSErPRa3mGbeJQUR}DR!n>|O^su~*fOd3Ybhbp3pt1AX#E8X^C
zgZwtF6U`SFcv%7UGqc&g!L21Z5w0s=)t2*;l(|TWp>vjXnC(`Sp)F1T3dnq$cO!fA
zD=+cA&e6*DeJeR_SW%M7{5&D}`E%qfaXUorDLo7-=U{NK8Kx|^>W%dm+0=lsYahRk
zn0HC9<giETTMW`QvJD&EE=9R(O9ss+HJrk9<;=85F?%L2W*yREzEKdtYPPlR2bL}`
zL0ZUyF~8#`-eJja)%U)vacs;_bjr7(SfzTNrtZbpRCf=LC4a(f?qr{M-t8hBPp^Cs
zIE*kC=_MZ4^H5G+CkLce0^!cpPknkO*bP~`7ej~)X6Mg8zjgJS?0j$aD1zV0wS$ps
zo0EOwU6D2^Bx-gkSl7cS8DQ++FrdT7Rrji>!Q@Gha32qbFI={|`Vt#v&o93O6c-oc
zaQg&DGrDHHQw?kv;yccoc-n7TW@{OpzOWL@YBJSPXyHHf6)!x*d_K}{(=xN;W$FYD
zptvlXIbQSbtia`&u7nENS;g+r3z}TCQqlHfJQqCWWMxCj^%cGQ`|+*0_LXwbQ>)qc
zMS}3;y~W6W(ecU<ypJj>C&fA&V-~)yG*2{}fME_UBQujYlsLO?)W?g&s04dCKz~M#
zpM3K8@#pZDXY)6r2-&Vg28>eC%-|N2P0^Vv#nUpji**e-(zxXJzT4a!Dr=dGiw;QP
zoJ^m*RXoi*YM>jLxulVzR@tELR)NbBaVk9iI>KW916zKAB8$gr^nRadn}928?>F(S
zra4{bAy+|Vn-A1e%BwP6mDd%1Ao6~25Q{21tX1gdZqBbXho4OVZI^&$6XkvfN9_RD
zb%$r0o0NMZW9-A8?fJDpReV?Rn+?I<1YmK%1gELlFkJ(Zq6^HrT_nA{5D0NS$X?Q<
zels3y<(}0%m-Q~DM*QsDoaTsy+_By6!}kv7FilK0XyEh>TJcbpC+P}ntCoo^eXWr7
zUQSVp^QqM}W;@C3oL?bMb?Cg!EFog4{#dqISEpSc3i#$;0-FGEx*2hRYhz_M)=37`
zpK*sw{lu}IG`C?h_33S`uMjs~_GWE|Z6W<3wF|ozgmpDvRXvZj?5JtdgV~}D9@SJO
zO$72CI)N?N<2tg!xpXZ@ibhdT4AenNb47(dTu#f$7xQx@f}@yMUw%k>$R6wTK(dfe
z3>tO2i2+`)&MPVTt><<PHxF^B_H1bIYbk&BHI0n*{o@I%t2$wf-nciTfpt(e$bzpz
z(vJJ<oDk>d1&7kKh8Up~AEOch7UKh`!XGmT6iHQ8;v#!B$s!F3)=U_-fLev+W=D|~
zfyr$VBZZiRt*x)oUdY|{8VioVB|&fuWrdjXN@R1@L-@;0I#5l9MF_e@fhXJI`f7T0
zLk_#ANkMDs))4pQy}deb`+W`r4tE{V?;;RyPCU)#9wQ92ZfOq-zQno;ciqro9zPD*
zH#e8Ny4Jy}@{><mQR~}A7VsjnceNNC)^B5(NJ1BGs%qO!tT~}BH+JUohaJLAGq;Fo
zdh(UePFU2wO@9{f<}E{WbCW9U3)ygfO@SVVpyk;PeB*1~LVZv2u~)btem|eOBdP!Q
zBsF}JgiJ57Y`;@;qB2(djiB|W#>i+?^MiSbM_0&C?(YP%CQAi#F>&#wNsF{`--b{<
z+HVzZlT^vzj>(pk*AjDuc&H}KXS>(bv&Ke7CP1MOr*MS)1_2k24<D#C&V)P4MJr<#
zhNhrBH+g1$vUOC$XJ^TfpGG0k{}`1CdcFA0DRh4y73@Fp-G@iuyAMz9lka4L>~}KZ
zM|l;bi8Bl+sMY0*xXyQ^O51iQV`Pl8&p4>uJa9Trhs*-@OWq2-!Uu&S^p|_->T9gW
z^dfWZ00DAw+xXPhr^hcw9^AdW`;2RNp~Iz(U_e8qa_=3D>alJEIuNNZjWgBF!pJB^
z?iG5!6G00wnKyIEtGLB`FP$M@xHN&>u9i@LFy&+D$h%j}Na(5e5J$eP*TJ&l)8zh+
z;%|OOV|6OYmS+koD#3ufMovb9nacC$^la1ewvJT?1)NF8VAY5L<FDd$P?_<Un*fMD
zdHU1=8M-Tpxhx?miL@RFak(O@ku)=ZBirt)t89djVrgmVvt^TwrN-6WHrkF+A3t6N
z5FzB6>`s@}z(+JWRzbTY_7@mikQd3TX>4I%Xv_jgxy+Ok1c_J_Wew(Z_NC>DNJEB!
z%k-FDm6Tjwa!-0RJM~$2j$4xhvuF2i#mkp3zp_Ux5q{lOc-}6f#&LCZ70EQ_eMOPR
z^>?Jy@We*iW5<%)`sneys3)fY3gAgYZ;{abnjJD`iJF=5kkzsssy2`eSsCO%RbGlc
zHos;^l!j=6!DaE@@|W~K_PRlIL%Z4Jy6OiK<sLjk{Dz>@XG$GX99=Bc<aCF&AHx|f
z+&Pa5l`Yr)n4hev722rs$3H$f#IUw-s=RUz$9mqXRYm+!Auv$ZK`;wmJ+BfMmyjrC
zyEE>Xyj_bzy8b()zLaAmm{!zcx4rw&sl`*&OwLtSN=>&A%P;vp-G#4T39(zRsF+*n
zqIugM!jN+^#y(}$-1~J(*N{I4M5;@DeX3IXnAvSfQSvJakhUEsT2!eH?lWzFd<*vm
zx`Dcgv&y0=o0u7`oVHH`uZ^-b&&6z))O<LM@$v7AdnK-`q-LzPTLhJ3=IXj=m*8M8
zU!ja8=Gq-fxlt@RR~w!<8o2F{1;%QpczNY%eN{UWGN89VW)&)gH*Sb{4MCGbRJ6Ec
zZHV8dG1l8RBmx&t#J8Rjf>I;VDe{-K)m78^K{hZ*ZC>CFt!ZeG=LT=U_xn-cHq{NV
ziEA)faFm!T>qgD+<sO`X&zn2HE^2Rksx#&oRPO7TSw4=8Ky=P$))@yin&McI+l~+m
zuq*)%^SJ?OX=wmOPFDn>8|>)a%AV5O7*Ee4{nm00NH0pt%`F6Z-nz)MX`d)Y8VH&`
zoKQffJ!N>E6Mwy_y-hEuT)*+@#Mx|vO1ZExG=dB4@~74yO@A)EikNqTErfZtu49-F
z{*uFV%x)XGmNw3!M#%da9hPl|s1UVZQ)u?FLP9-b_H@wx!$*kPzlU%nApk;iPMBJn
z*32E%+ge%?AMonRBOD>BX`Y1#ZwNx`uUeK=ZFmm4IM73)RB2S*&`+VDY%rJURx4bX
zH1#N65R~<F!vS3VHn2?U;`-)FkbFpZVBBFnY~mQzB5!)ON!?3CrNHhqbjhltNK<rk
zF>==GfU5pia%iDwFf?^{KD3c;AmuA;yZUM1AC4ZT>7&3&TDO#`D{<AoXAEmPE(~KZ
zIMAxcKKfOu%gKtEFVUrxx-az?YHAq&wKhY?J#%*;kZMKMuRBBb-~Ra|6z(eWLTe95
zc5RWaMlJM_mqJpYd)i4w#R-;@Su*HV#MO?lh^42egN@Pce5|o#Jn+LhI;kz(+V8F%
zIeB)GL?x5WZ=4;<rNagIEge;{N@pDT^m-|cE*$v1q}lE&SyG^&Bs7admEB%b)ZYzU
z{%<3`F=h`nHRsGF@}8j(3f06x`={0ENP)e0PXc-HGG^evF`IM{?;glzp*egw4kBs7
zx(T?$oH!x5d*hI{PR7T&NKvUg5Q?z6nto4iLeFaiHIB2eMD9~(Wj}pNAuup7mB%@J
z9m3ky{!nwbO*O0T0$os}s@U4Td;OjIQja_dCMQpxVAzzJqY>i8hY56-ZKhq%xFarF
zJp73=a{F7}HL-J{l2KUu4d9jUSee07!Rio##9rFEHc<a24Dk9Z+`Jj%Dnb-raWqX^
z3FQi5MP&ZJ{TB22BFK**8TUq9=oq;K+`mepq+_;CpBxa`EO_Fi=@g8!&RW@}MPaRD
zKvZ&mB4sy&0LgJ_Zl$0G1t+|w!v5z(<S%O`(Nl!EqUOAAd)ym;5v_O_+1gUoLxmfw
zzAoKtIT*~NkQXoTA|7tg?9KTsKVov%&~q=+KH6%dka?mmdk}y4*?Vh$(WGI6s!4aO
zI+ST|O4i&&dc*<AZi!P?M_}JuEDtfbd-v|0v#7tmm9=m(+oDGolwqMn<3=wMCGTE!
zo;(_uRgO-J&X6}DY>bmPXaA)UyB#vnpzor7tTt33n`OPlP<YlP9vI-}#waksDku=r
z<We(=t`05Nx69I8NRAM;c=n7*7|@5!pT_*V3M71KdDP6MBOnaD26lnW#0&#XQ0^NB
zb2PFw#Tq=oO=p`n4kn#<o@`R&#A-~829Zy#*HlR5PA(h_k4a`*H0$2qRS;9Km+l<4
zR7kk3xKFimrafKUiBR}SBtbsf32DO&&qc6eikU7T7Ee>8XN;^mLOjU$70aXjbQk-S
z<yTY`6aq%;!?8rPf%Gz2$Q9J!ft#vco3Bxx><go#5w0r}ae4NmhmXW;x(LYFA&SHh
zMT5oGU<|{?x<!TtSHu~Qc&F)h&GF~)vM<a_V#bI$@}fr3gWpc90LR~r-rmcFDQdE^
zTPrk$eCAJqdh(Q=L`#M?Du2P_Es6G02n{MBd0zcmoR4`+Zs7XjOdJufCa4?oGBh;)
z3&@4@q>YGpy|j<5`=Cf6zDwdeBD*j@&mOBMfgJ2PZe0WqW8J2}u2HQ*DhbixM%Q(H
zz-~uP1koa!%^I)k;+R#Ww-$M#SQrCas^!xFwqVy<akP6<?NhFl)pZ$R(HQKL6k&@|
zOPJ4+OF!CXR<m5UK9DfyVB`CibG@f+Km<;wslwuAtM07c`IG0)WqmaN_D%MS4-sCp
z`$K%2zFQ`>4|w($mwFOO_a9xA!JR0&nBH!ov!6qCUm#mUQqW0xX&jtr8ezi##OSY;
zyEF8*qpbu?Bf~+K`CgBfj0kG*{R0A?;5t}!auLvi7S7id<1LAnJAYo~1QrKI58`wf
z$Y1xA5fAeL#GC{)BF$T8&+;8UdUR^?cAWLVJ&EcI;jfUIGW!fZ+i*f3Dex|=#x?Pf
z%WR5ZzA*n^c0@g65kua{3pZ#NhPs7PXxsd&bv~brlBubNSsQwX`qOK14%6+8O{tWt
zH3zuJ{e|=%sR!f>|GV||f9@d;{gIq%Q};c6%lHbMns)Bmd5I``mPHv%1Ekxd`zLX1
z*0Y-ztLudEzc-4@5KwEg{!;DU(u1ImKxQI9x8Vb2xMN;Ti;aTb9QK_`*HDi6(CAVG
z_;B63f1h!Ntjjpmn>#54{(#cguX6-OkfwoFY5k<gLPM~Nd_UKnq-o!qmyEi(+Zy~o
zjsAGe%!k!$p~}%F>yu)=I}1gulPjLN$!<k?)Q>Dt+-2V(PeDObf&4}Qa(_#fYDT=`
zvRYuwps1)wD}UI7QC=i<sH$>luwV4q>SnHa9EW-$D+W<B08Z987VVJ=bH^OFws&}|
zQa5ig3Xb39*>2j~Na?t2nJVc=&kcG)gCtL3TZJ7tyWUPw>NpI3-0kP?5<EEMjKM|l
zTO=XyNs3kuSbedN+(`luL<PWorDq8j%pd#rjnK1T^wL(fJfdd4s=Eu5w?GWdo&Rqs
zfD{Jc7vdqpjMe^g6ow%=hI7AZ!N`4M{*uLF{mXE3rZssfH!#*-SXclcC&Yu}2CZ*-
zqj6QHIY1wmTo>O$6S4QDsP$J`n03}jWH&?R<3E()F*s0M!Tg**Ew64Y(6-#$`gxEo
zT4E)9#Vi;vnm^#T^a+8i^b}jHEY^n}gWTRb+9k&kY)OT{KXcx+WN$UW_6njwvftX!
zaqNcr3D9-lX?w6VschhXAq&oVxW%cv_Ys18cCP(OQ~8h>EHMxVwy*5#!q2Mj7hNR}
ztvS=xI*&GzKgEmc-%AI806K6?1sh+FPHZUNQ~@nU^T6?+P#B><T!{bXS|-3!V5-Os
zmX~;<D^Z6db@Fri{SyTj6=BsSO>WnJGS0ksKGjl_8N;!tagcmfUo4rNuf7G+5aBz0
zo|>lyNFOEwRR<G)GI;eArvi?P+}yGQL_7k~10SmUB!%(d!Pul*sz!VfkZF}oi$QbK
zwz_*rh2ZqS9AqZE``i>k<qk_^`h<w$8L7<B3nc?0d1LVXB;e0e8zFsW3td53x3(nx
zj)_z^j{W^~;IUq1var0|=(epHyY0SMBo}qz)!i+<Q|V~ETe4LCd3E;0vO%R!ai;$N
zb_B4<C9?l>Is$al_2&-*QUXroh{zIA9~v6I!PlS(OFIY24=N(paObQh9TL{}Gaa0v
zN82o0mVte!VH}0P5i1}|>3@yWq52XAe*%OR=0rXVUPm9C=`&V7Vl#)NJQ=VMt})IG
z`Q!f@^|L=Uh+YO2)3jkJbpx2+!HlgZX-9?@cbpCxt?1u%%TyufRnN_U>&Co@dBjug
zntMmXxJC8g;Pc}poZQUR$=8#s_?)fEIxLyiH<^~ij8Z0EgZcy%WBLc6Zdh@L43vf(
zYQOR<t_(szgDzAa6O5wUqzWO9n@7$YKVWhl*7GLmYA;Q;fh?m~y(7a&CRPYNqyk1o
zi{fCJx@g(go1~NQKA9HpNpcA?E<7ux8Lnx~yMPtWM>LzV^rDS3d8xa5<<R<~`)2SP
zfWs&I&L&K>Q5Gl3r+K=Xjl*0I4@*#^%5mSH)suh2Vv9fco@OxO3#rv2e;Nh$d?xD>
zbG!VRDgE>I6_!$E<AG_U#;Tg+vGo;I=$w%P8+Zltg&WF_5Rq8Tx>wU7HRB+<1F9u=
z<Vd)yefNt_!U~<oLI<Ds1Mu7_x7Nn*9_mAeFY!P;PJS*GN5mxv;vXd(r6?^e9XO@E
zbgQ7&he-%#!gG{nJG4DE>T<L)?j%B>XVWRLABnpqPD4B{ZDw{7=BGPxYqaDIQ_>gF
z_=83BcADIF7nSHa8V9x~Z*AtrhUb`L)GMAAB+LJqGi&m?Ldl9LjG9aVTuv9^cM`?b
zQ?&D%&csjM*9peql4j+XRTk0>6I$7;AqyaGx05TDSU;xol2)DBYA^)LIP({r-peS|
zSTW{w#Sc>J<|AO_kRa=doxo;sdgcHFWKlnzm!Hrm3@F^_c#cPxoX6rorL?@@tEKd#
zG-~Hna1NujHW?ZVhS{ACkaje=)hg+I%ahoAdz<HnhxlI9$)zaMXNGA4vfP3zpF>m;
zo!d2@+}oQn;B8#dm;f~QQlG!5s{1AnV3uaBg7!LFz32iTt9^*?{=%Tde$}WZR8>hI
zH<bD2V;SyElBzi3w}XL+e3N4{u-$i+5%m<Z+}fA`s?twUevbT(3@EzJdw#gk@s+pU
zjPFJ8(#Cu&sC(BoHV`Vzpf(Ow>QQvxk^|QPpu@|3O5wstoR>6GS~*3|Zbnr-zdp*@
z=`++|PawzvsU*z#jZ|74`nu`+Rm+NQTjnjAIHV)a{9tJIOb0}Troz-Xg1`-BewiG=
zIdoVD#9laLo=cDecMdLE-|8^lG*B>BMjtu}^Td$y#%sg-XlQ9C=?zBf$lwP>%+K^}
zX8$F95(uiKFg^FgSvXLh_g(kwPmA|#SR%HjbU6%vIgmhmG0;=#ma6z+$bpYL5A0mF
zGmhLl#(V1Y>1#RNw`)KO#jWPCG}7jpv2iGWW0rt)P)p+-I({$C-A+zl*a&fb)5YQg
z^)g8K6P^2Xtvy`OC25-5Q4`hUc7E8KPL&|X$gOcszByh#Ff1$#+zV$(&jp<C4)Rw{
zq*NU`Z$jdNwh`zIb>Ek(4QlR7*%^HDW)oDtOV`s9_RKhEJ0g$4%*7iLWfbR^6;RBZ
z!E~gn$EF4d*4`gCZ&QLs)L`?TGu<SjIRxf~8l<bx&1pSNLxWe$&4;@jkKShgrXM+)
zN=ibxLkgOI#w`5k3jWpR<qr)AkQBe}s<Py(G3Bo@WVJ_g!zmw#!fR^yWPbd#Qx(^J
zLq0claNVZkc?lbhcLsHYph&d<My<D31USyHI`hEBn(4UdX=NzYjtzVZ&4;F)2mrGt
zG$+Va87_ksjgNiqgLhLZ%Hov_X@4U821V5CDpSL;Z!g&kFS#zsV)N~H1Z?U4Xn_9x
zuli3Aw11W_bzT9D-<jc&`M%Odwc|x~=p8}wUh>NPiwvrK5ZZ%SP;7xD7RUl)ARm(s
zTA`{K+tAA1(xxOwP9TjXr&=rCs!oNn^`g*yKNsK)T3H3^Pz(~Gqetu%j`&slc&SwU
z9z@qy9nAiErt7^0j9IvZ%r}AVH1lI8x@taUER><(N`vsBmi#Xu|B*2lOJc^4XN<^L
z%P2!juF72yffUp@!*dLdi{py2QRm{W{_#G59>cUGdx2{%lgH{AAQPl<o9-S<1C`8g
zCF>?LG{5$ki#nW6Z;3*)-<=cwdo_Tm0Nd~ti@B)6Fpns(VuvfKGS1E;%6;?M^{JtR
z%j+y)pY>?~q+b!P!YFf;KqSo;>}-2uO}?eJLx>D}<IuKx|CoWEN8u;C+3!Zf6(e43
zLxvCURa2RL2^iP+L%(xwWA0|eC=ABKeBi|%q$vh?IQ6>+aL;XkC_j*X28QZX)wiT8
zgacR<jRm#`{`)r9UmQup&^PcaUQiv8Kdr<doNb~#{WVll=<bmM^mynyK1}$xclTdh
zz+VuT;W~`;z;7!mDndEvkVMAN_4R|_iIx)C&^wwc^Ou~RF3d=599%Y?BlUFVniclC
zVV^)~b&*|1Z>6>$+*Mc`3@%<a5*%Af#bX%_q(6k_rnufx$g<8`qeWb>&l+#t0Cl_)
z!Fh&f9t9F8B<8`+Fqz*sL*57>#{p+-MFvu0u54K}p?50g6kJiBF3@(49x-_20Mxw@
z&};to`knLbUU7=AB|Gz3<cz9=0fCBzagnLdr5}I1p6Tejk8|B|w|EP=K=};w`x-U{
z7v+pIHT695Vn1%6UxB9FfHHKi%UB3#<8i^k&)*<OdDX1EHto}36*MY)#on1_%h^U~
zL9FJtheW@&GjbURg>6!9s<u{2fvJp2C5+$R85sQ2=XW=cTLY}73qmSYVDsvt1@(OC
zxbse+<t&Ad{{M+zK@mT%DOJGHrv3z-p*&cLUj-a)0|Nsv4;XRS{x0-HM_WrAYepEk
zfGdENe#L5muJo5{0<{(q=-;LH{_jx$I_d8mg~Q-vzK>fo|5}%mWn+Wg(Q_~!ymRNy
z5RE@&1(H7fnIC=CQ0l`RftJ`6TJsL6n#J{Yf#DmeFA*uz^%KfZvd{oQ^9Z3cT+P}Q
zQr7LVPsl!>UY+*7dtW9QB?n_JAe<HG^tO;62`SspnTA0>@Uk6{u1e|p5`cMbVFjH7
znpJ4LcdTM*OiyZH9F*FBvd7q-!k><ykKCJ2(RS<U-qZO$xNEWTzfN3z$QvpaVCIr?
zNNV48y|}v(b1z6M!%_29c{yA)dBe!DaOCdxiaX_#H~PmvxN5GS2y(a-`Q!gbU;wwU
zSJBCtxsDAvv2XF|9UboWE^C=i1NCK*tW^VJWr_7RHlo*X#~k*X_bnv^&7JJP7KUXD
zTCmsffyqY)Hao2JW<aTbspuH;LCxlX0_BwASmAA>MBM`6=K<U|7k{7YEv^IKriTd$
zZ8+qfwWPZ@Q`b&Th1s|)tWI>_hKIb&>V$3{iP~pBM=G42DNa^Jp7*6VeC>%sPFLO9
z+FHw4!$Xq^Cqkw!a;df!bp@r6L=&*x#r3-I00nK6m?wFx)b<6J>X4mJZ0-a((9p^$
z(j-<4arxmn@O7iIE6Dr4vyH&lsi=m;H4;zF5T?wswfHrW_lQ@Zk<dy}>ztZOOqJBE
zXFamblyQ#1?_8*R4N`7@5g^`qnUtiY;|HdQ#S~T<@5Q#V0?A(-BO!M+RqW=y{{S68
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",