From c11bb1c811c32ebe5d0b42a0d1875326da02a06c Mon Sep 17 00:00:00 2001
From: Brian Ingles <brianingles@deephaven.io>
Date: Fri, 3 May 2024 11:42:44 -0500
Subject: [PATCH] ActionGroup wrapper (#445)

---
 .../ui/src/js/src/elements/ActionGroup.tsx    | 39 ++++++++++++++++++
 ...nEventCallback.ts => useSelectionProps.ts} | 41 ++++++++++++++++++-
 .../src/js/src/elements/useListViewProps.ts   | 41 +++++++------------
 plugins/ui/src/js/src/widget/WidgetUtils.tsx  |  2 +-
 4 files changed, 95 insertions(+), 28 deletions(-)
 create mode 100644 plugins/ui/src/js/src/elements/ActionGroup.tsx
 rename plugins/ui/src/js/src/elements/spectrum/{useSelectionEventCallback.ts => useSelectionProps.ts} (50%)

diff --git a/plugins/ui/src/js/src/elements/ActionGroup.tsx b/plugins/ui/src/js/src/elements/ActionGroup.tsx
new file mode 100644
index 000000000..4b435b20c
--- /dev/null
+++ b/plugins/ui/src/js/src/elements/ActionGroup.tsx
@@ -0,0 +1,39 @@
+import {
+  ActionGroup as DHActionGroup,
+  ActionGroupProps as DHActionGroupProps,
+} from '@deephaven/components';
+import {
+  SerializedSelectionProps,
+  useSelectionProps,
+} from './spectrum/useSelectionProps';
+
+export type SerializedActionGroupProps<T> = Omit<
+  DHActionGroupProps<T>,
+  'selectionMode' | 'onChange' | 'onSelectionChange'
+> &
+  SerializedSelectionProps;
+
+function ActionGroup<T>({
+  selectionMode: selectionModeMaybeUppercase,
+  onChange: serializedOnChange,
+  onSelectionChange: serializedOnSelectionChange,
+  ...props
+}: SerializedActionGroupProps<T>): JSX.Element {
+  const { selectionMode, onChange, onSelectionChange } = useSelectionProps({
+    selectionMode: selectionModeMaybeUppercase,
+    onChange: serializedOnChange,
+    onSelectionChange: serializedOnSelectionChange,
+  });
+
+  return (
+    <DHActionGroup
+      // eslint-disable-next-line react/jsx-props-no-spreading
+      {...props}
+      selectionMode={selectionMode}
+      onChange={onChange}
+      onSelectionChange={onSelectionChange}
+    />
+  );
+}
+
+export default ActionGroup;
diff --git a/plugins/ui/src/js/src/elements/spectrum/useSelectionEventCallback.ts b/plugins/ui/src/js/src/elements/spectrum/useSelectionProps.ts
similarity index 50%
rename from plugins/ui/src/js/src/elements/spectrum/useSelectionEventCallback.ts
rename to plugins/ui/src/js/src/elements/spectrum/useSelectionProps.ts
index c64c95226..8f6e770ed 100644
--- a/plugins/ui/src/js/src/elements/spectrum/useSelectionEventCallback.ts
+++ b/plugins/ui/src/js/src/elements/spectrum/useSelectionProps.ts
@@ -1,5 +1,6 @@
-import type { ItemKey, ItemSelection } from '@deephaven/components';
 import { useCallback } from 'react';
+import type { SelectionMode } from '@react-types/shared';
+import type { ItemKey, ItemSelection } from '@deephaven/components';
 
 export type SerializedSelection = 'all' | ItemKey[];
 
@@ -7,6 +8,25 @@ export type SerializedSelectionEventCallback = (
   event: SerializedSelection
 ) => void;
 
+export interface SerializedSelectionProps {
+  selectionMode?: SelectionMode | Uppercase<SelectionMode>;
+
+  /** Handler that is called when selection changes */
+  onChange?: SerializedSelectionEventCallback;
+
+  /**
+   * Handler that is called when the selection changes.
+   * @deprecated Use `onChange` instead
+   */
+  onSelectionChange?: SerializedSelectionEventCallback;
+}
+
+export interface SelectionProps {
+  selectionMode?: SelectionMode;
+  onChange?: (selection: ItemSelection) => void;
+  onSelectionChange?: (selection: ItemSelection) => void;
+}
+
 /**
  * Selection can be 'all' or a Set of keys. If it is a Set, serialize it as an
  * array.
@@ -40,3 +60,22 @@ export function useSelectionEventCallback(
     [callback]
   );
 }
+
+export function useSelectionProps({
+  selectionMode,
+  onChange,
+  onSelectionChange,
+}: SerializedSelectionProps): SelectionProps {
+  const selectionModeLc = selectionMode?.toLowerCase() as SelectionMode;
+
+  const serializedOnChange = useSelectionEventCallback(onChange);
+  const serializedOnSelectionChange =
+    useSelectionEventCallback(onSelectionChange);
+
+  return {
+    selectionMode: selectionModeLc,
+    onChange: onChange == null ? undefined : serializedOnChange,
+    onSelectionChange:
+      onSelectionChange == null ? undefined : serializedOnSelectionChange,
+  };
+}
diff --git a/plugins/ui/src/js/src/elements/useListViewProps.ts b/plugins/ui/src/js/src/elements/useListViewProps.ts
index 8b976f7b0..467030437 100644
--- a/plugins/ui/src/js/src/elements/useListViewProps.ts
+++ b/plugins/ui/src/js/src/elements/useListViewProps.ts
@@ -4,9 +4,9 @@ import { ListViewProps as DHListViewProps } from '@deephaven/components';
 import { ListViewProps as DHListViewJSApiProps } from '@deephaven/jsapi-components';
 import { ObjectViewProps } from './ObjectView';
 import {
-  SerializedSelectionEventCallback,
-  useSelectionEventCallback,
-} from './spectrum/useSelectionEventCallback';
+  SerializedSelectionProps,
+  useSelectionProps,
+} from './spectrum/useSelectionProps';
 
 type Density = Required<DHListViewProps>['density'];
 
@@ -25,22 +25,11 @@ type WrappedDHListViewProps = Omit<
   selectionMode?: SelectionMode | Uppercase<SelectionMode>;
 };
 
-export interface SerializedListViewEventProps {
-  /** Handler that is called when selection changes */
-  onChange?: SerializedSelectionEventCallback;
-
-  /**
-   * Handler that is called when the selection changes.
-   * @deprecated Use `onChange` instead
-   */
-  onSelectionChange?: SerializedSelectionEventCallback;
-}
-
 export type SerializedListViewProps = (
   | WrappedDHListViewProps
   | WrappedDHListViewJSApiProps
 ) &
-  SerializedListViewEventProps;
+  SerializedSelectionProps;
 
 /**
  * Wrap ListView props with the appropriate serialized event callbacks.
@@ -49,24 +38,24 @@ export type SerializedListViewProps = (
  */
 export function useListViewProps({
   density,
-  selectionMode,
-  onChange,
-  onSelectionChange,
+  selectionMode: selectionModeMaybeUppercase,
+  onChange: serializedOnChange,
+  onSelectionChange: serializedOnSelectionChange,
   ...otherProps
 }: SerializedListViewProps): DHListViewProps | WrappedDHListViewJSApiProps {
   const densityLc = density?.toLowerCase() as Density;
-  const selectionModeLc = selectionMode?.toLowerCase() as SelectionMode;
 
-  const serializedOnChange = useSelectionEventCallback(onChange);
-  const serializedOnSelectionChange =
-    useSelectionEventCallback(onSelectionChange);
+  const { selectionMode, onChange, onSelectionChange } = useSelectionProps({
+    selectionMode: selectionModeMaybeUppercase,
+    onChange: serializedOnChange,
+    onSelectionChange: serializedOnSelectionChange,
+  });
 
   return {
     density: densityLc,
-    selectionMode: selectionModeLc,
-    onChange: onChange == null ? undefined : serializedOnChange,
-    onSelectionChange:
-      onSelectionChange == null ? undefined : serializedOnSelectionChange,
+    selectionMode,
+    onChange,
+    onSelectionChange,
     // The @deephaven/components `ListView` has its own normalization logic that
     // handles primitive children types (string, number, boolean). It also
     // handles nested children inside of `Item` and `Section` components, so
diff --git a/plugins/ui/src/js/src/widget/WidgetUtils.tsx b/plugins/ui/src/js/src/widget/WidgetUtils.tsx
index 5ebfd2219..76c1ed142 100644
--- a/plugins/ui/src/js/src/widget/WidgetUtils.tsx
+++ b/plugins/ui/src/js/src/widget/WidgetUtils.tsx
@@ -4,7 +4,6 @@ import React, { ComponentType } from 'react';
 // Importing `Item` and `Section` compnents directly since they should not be
 // wrapped due to how Spectrum collection components consume them.
 import {
-  ActionGroup,
   ActionMenu,
   Item,
   ListActionGroup,
@@ -34,6 +33,7 @@ import Column from '../layout/Column';
 import Dashboard from '../layout/Dashboard';
 import ListView from '../elements/ListView';
 import Picker from '../elements/Picker';
+import ActionGroup from '../elements/ActionGroup';
 
 /*
  * Map element node names to their corresponding React components