Repository: deephaven/deephaven-plugins (language: python)

Issue:
Cleanup component mappings and utils
We currently map some components via `SPECTRUM_ELEMENT_TYPE_PREFIX` and others more directly. https://github.com/deephaven/deephaven-plugins/pull/512 will remove our last Spectrum dependencies, so we should clean things up and make the mappings more explicit / consistent.


Discussion / hints:

Fix patch:
diff --git a/plugins/ui/src/js/src/elements/spectrum/ActionButton.tsx b/plugins/ui/src/js/src/elements/ActionButton.tsx
similarity index 71%
rename from plugins/ui/src/js/src/elements/spectrum/ActionButton.tsx
rename to plugins/ui/src/js/src/elements/ActionButton.tsx
index 064fce34a..77f1e1f58 100644
--- a/plugins/ui/src/js/src/elements/spectrum/ActionButton.tsx
+++ b/plugins/ui/src/js/src/elements/ActionButton.tsx
@@ -3,10 +3,10 @@ import {
   ActionButton as DHCActionButton,
   ActionButtonProps as DHCActionButtonProps,
 } from '@deephaven/components';
-import { useButtonProps } from './useButtonProps';
-import { SerializedButtonEventProps } from '../SerializedPropTypes';
+import { useButtonProps } from './hooks/useButtonProps';
+import { SerializedButtonEventProps } from './model/SerializedPropTypes';
 
-function ActionButton(
+export function ActionButton(
   props: SerializedButtonEventProps<DHCActionButtonProps>
 ): JSX.Element {
   const buttonProps = useButtonProps(props);
diff --git a/plugins/ui/src/js/src/elements/ActionGroup.tsx b/plugins/ui/src/js/src/elements/ActionGroup.tsx
index 4b435b20c..0f5515215 100644
--- a/plugins/ui/src/js/src/elements/ActionGroup.tsx
+++ b/plugins/ui/src/js/src/elements/ActionGroup.tsx
@@ -5,7 +5,7 @@ import {
 import {
   SerializedSelectionProps,
   useSelectionProps,
-} from './spectrum/useSelectionProps';
+} from './hooks/useSelectionProps';
 
 export type SerializedActionGroupProps<T> = Omit<
   DHActionGroupProps<T>,
@@ -13,7 +13,7 @@ export type SerializedActionGroupProps<T> = Omit<
 > &
   SerializedSelectionProps;
 
-function ActionGroup<T>({
+export function ActionGroup<T>({
   selectionMode: selectionModeMaybeUppercase,
   onChange: serializedOnChange,
   onSelectionChange: serializedOnSelectionChange,
diff --git a/plugins/ui/src/js/src/elements/spectrum/Button.tsx b/plugins/ui/src/js/src/elements/Button.tsx
similarity index 71%
rename from plugins/ui/src/js/src/elements/spectrum/Button.tsx
rename to plugins/ui/src/js/src/elements/Button.tsx
index d6643c359..cc34581f2 100644
--- a/plugins/ui/src/js/src/elements/spectrum/Button.tsx
+++ b/plugins/ui/src/js/src/elements/Button.tsx
@@ -1,9 +1,9 @@
 import React from 'react';
 import { SpectrumButton, SpectrumButtonProps } from '@deephaven/components';
-import { useButtonProps } from './useButtonProps';
-import { SerializedButtonEventProps } from '../SerializedPropTypes';
+import { useButtonProps } from './hooks/useButtonProps';
+import { SerializedButtonEventProps } from './model/SerializedPropTypes';
 
-function Button(
+export function Button(
   props: SerializedButtonEventProps<SpectrumButtonProps>
 ): JSX.Element {
   const buttonProps = useButtonProps(props);
diff --git a/plugins/ui/src/js/src/elements/spectrum/Form.tsx b/plugins/ui/src/js/src/elements/Form.tsx
similarity index 97%
rename from plugins/ui/src/js/src/elements/spectrum/Form.tsx
rename to plugins/ui/src/js/src/elements/Form.tsx
index 11937f1f8..4cfc6c8d2 100644
--- a/plugins/ui/src/js/src/elements/spectrum/Form.tsx
+++ b/plugins/ui/src/js/src/elements/Form.tsx
@@ -4,7 +4,7 @@ import {
   FormProps as DHCFormProps,
 } from '@deephaven/components';
 
-function Form(
+export function Form(
   props: DHCFormProps & {
     onSubmit?: (data: { [key: string]: FormDataEntryValue }) => void;
   }
diff --git a/plugins/ui/src/js/src/elements/HTMLElementView.tsx b/plugins/ui/src/js/src/elements/HTMLElementView.tsx
index 903e7ddd0..329847937 100644
--- a/plugins/ui/src/js/src/elements/HTMLElementView.tsx
+++ b/plugins/ui/src/js/src/elements/HTMLElementView.tsx
@@ -1,6 +1,6 @@
 import React from 'react';
-import { HTMLElementNode, getHTMLTag } from './HTMLElementUtils';
-import { ELEMENT_KEY } from './ElementUtils';
+import { HTMLElementNode, getHTMLTag } from './utils/HTMLElementUtils';
+import { ELEMENT_KEY } from './utils/ElementUtils';
 
 export type HTMLElementViewProps = {
   element: HTMLElementNode;
diff --git a/plugins/ui/src/js/src/elements/IconElementView.tsx b/plugins/ui/src/js/src/elements/IconElementView.tsx
index 930e338ad..0b56d01cd 100644
--- a/plugins/ui/src/js/src/elements/IconElementView.tsx
+++ b/plugins/ui/src/js/src/elements/IconElementView.tsx
@@ -1,8 +1,8 @@
 import React from 'react';
 import { Icon } from '@deephaven/components';
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { getIcon, IconElementNode } from './IconElementUtils';
-import { ELEMENT_KEY } from './ElementUtils';
+import { getIcon, IconElementNode } from './utils/IconElementUtils';
+import { ELEMENT_KEY } from './utils/ElementUtils';
 
 export type IconElementViewProps = {
   element: IconElementNode;
diff --git a/plugins/ui/src/js/src/elements/ListView.tsx b/plugins/ui/src/js/src/elements/ListView.tsx
index 56c28df1e..21e052f84 100644
--- a/plugins/ui/src/js/src/elements/ListView.tsx
+++ b/plugins/ui/src/js/src/elements/ListView.tsx
@@ -3,11 +3,14 @@ import { isElementOfType } from '@deephaven/react-hooks';
 import { getSettings, RootState } from '@deephaven/redux';
 import { ListView as DHListView } from '@deephaven/components';
 import { ListView as DHListViewJSApi } from '@deephaven/jsapi-components';
-import { SerializedListViewProps, useListViewProps } from './useListViewProps';
+import {
+  SerializedListViewProps,
+  useListViewProps,
+} from './hooks/useListViewProps';
 import ObjectView from './ObjectView';
-import useReExportedTable from './useReExportedTable';
+import useReExportedTable from './hooks/useReExportedTable';
 
-function ListView(props: SerializedListViewProps): JSX.Element | null {
+export function ListView(props: SerializedListViewProps): JSX.Element | null {
   const settings = useSelector(getSettings<RootState>);
   const { children, ...listViewProps } = useListViewProps(props);
 
diff --git a/plugins/ui/src/js/src/elements/Picker.tsx b/plugins/ui/src/js/src/elements/Picker.tsx
index f4f860c55..79ce0ada4 100644
--- a/plugins/ui/src/js/src/elements/Picker.tsx
+++ b/plugins/ui/src/js/src/elements/Picker.tsx
@@ -3,11 +3,11 @@ import { Picker as DHPicker } from '@deephaven/components';
 import { Picker as DHPickerJSApi } from '@deephaven/jsapi-components';
 import { isElementOfType } from '@deephaven/react-hooks';
 import { getSettings, RootState } from '@deephaven/redux';
-import { SerializedPickerProps, usePickerProps } from './usePickerProps';
+import { SerializedPickerProps, usePickerProps } from './hooks/usePickerProps';
 import ObjectView from './ObjectView';
-import useReExportedTable from './useReExportedTable';
+import useReExportedTable from './hooks/useReExportedTable';
 
-function Picker(props: SerializedPickerProps): JSX.Element | null {
+export function Picker(props: SerializedPickerProps): JSX.Element | null {
   const settings = useSelector(getSettings<RootState>);
   const { children, ...pickerProps } = usePickerProps(props);
 
diff --git a/plugins/ui/src/js/src/elements/Radio.tsx b/plugins/ui/src/js/src/elements/Radio.tsx
index 313f5c2a3..5fc41f855 100644
--- a/plugins/ui/src/js/src/elements/Radio.tsx
+++ b/plugins/ui/src/js/src/elements/Radio.tsx
@@ -5,15 +5,15 @@ import {
 import {
   SerializedFocusEventProps,
   SerializedKeyboardEventProps,
-} from './SerializedPropTypes';
-import { useFocusEventCallback } from './spectrum/useFocusEventCallback';
-import { useKeyboardEventCallback } from './spectrum/useKeyboardEventCallback';
+} from './model/SerializedPropTypes';
+import { useFocusEventCallback } from './hooks/useFocusEventCallback';
+import { useKeyboardEventCallback } from './hooks/useKeyboardEventCallback';
 
 export type SerializedRadioProps = SerializedFocusEventProps<
   SerializedKeyboardEventProps<DHRadioProps>
 >;
 
-function Radio({
+export function Radio({
   onFocus: serializedOnFocus,
   onBlur: serializedOnBlur,
   onKeyDown: serializedOnKeyDown,
diff --git a/plugins/ui/src/js/src/elements/RadioGroup.tsx b/plugins/ui/src/js/src/elements/RadioGroup.tsx
index b44031e55..b30e41393 100644
--- a/plugins/ui/src/js/src/elements/RadioGroup.tsx
+++ b/plugins/ui/src/js/src/elements/RadioGroup.tsx
@@ -3,8 +3,8 @@ import {
   RadioGroupProps as DHRadioGroupProps,
 } from '@deephaven/components';
 import { Orientation } from '@react-types/shared';
-import { SerializedFocusEventProps } from './SerializedPropTypes';
-import { useFocusEventCallback } from './spectrum/useFocusEventCallback';
+import { SerializedFocusEventProps } from './model/SerializedPropTypes';
+import { useFocusEventCallback } from './hooks/useFocusEventCallback';
 
 export type SerializedRadioGroupProps = Omit<
   SerializedFocusEventProps<DHRadioGroupProps>,
@@ -13,7 +13,7 @@ export type SerializedRadioGroupProps = Omit<
   orientation?: Orientation | Uppercase<Orientation>;
 };
 
-function RadioGroup({
+export function RadioGroup({
   onFocus: serializedOnFocus,
   onBlur: serializedOnBlur,
   orientation: orientationMaybeUppercase,
diff --git a/plugins/ui/src/js/src/elements/spectrum/RangeSlider.tsx b/plugins/ui/src/js/src/elements/RangeSlider.tsx
similarity index 93%
rename from plugins/ui/src/js/src/elements/spectrum/RangeSlider.tsx
rename to plugins/ui/src/js/src/elements/RangeSlider.tsx
index 77eafa255..bb7423ae5 100644
--- a/plugins/ui/src/js/src/elements/spectrum/RangeSlider.tsx
+++ b/plugins/ui/src/js/src/elements/RangeSlider.tsx
@@ -9,7 +9,7 @@ const VALUE_CHANGE_DEBOUNCE = 250;
 
 const EMPTY_FUNCTION = () => undefined;
 
-function RangeSlider(props: DHCRangeSliderProps): JSX.Element {
+export function RangeSlider(props: DHCRangeSliderProps): JSX.Element {
   const {
     defaultValue = { start: 0, end: 0 },
     value: propValue,
diff --git a/plugins/ui/src/js/src/elements/spectrum/Slider.tsx b/plugins/ui/src/js/src/elements/Slider.tsx
similarity index 93%
rename from plugins/ui/src/js/src/elements/spectrum/Slider.tsx
rename to plugins/ui/src/js/src/elements/Slider.tsx
index 28f8ecda2..005e28498 100644
--- a/plugins/ui/src/js/src/elements/spectrum/Slider.tsx
+++ b/plugins/ui/src/js/src/elements/Slider.tsx
@@ -9,7 +9,7 @@ const VALUE_CHANGE_DEBOUNCE = 250;
 
 const EMPTY_FUNCTION = () => undefined;
 
-function Slider(props: DHCSliderProps): JSX.Element {
+export function Slider(props: DHCSliderProps): JSX.Element {
   const {
     defaultValue = 0,
     value: propValue,
diff --git a/plugins/ui/src/js/src/elements/SpectrumElementUtils.ts b/plugins/ui/src/js/src/elements/SpectrumElementUtils.ts
deleted file mode 100644
index ffb624b3b..000000000
--- a/plugins/ui/src/js/src/elements/SpectrumElementUtils.ts
+++ /dev/null
@@ -1,109 +0,0 @@
-import { ValueOf } from '@deephaven/utils';
-import {
-  ButtonGroup,
-  Content,
-  ContextualHelp,
-  Grid,
-  Heading,
-  Icon,
-  IllustratedMessage,
-  NumberField,
-  SpectrumCheckbox as Checkbox,
-  Switch,
-  Tabs,
-  TabList,
-  Text,
-  ToggleButton,
-  Flex,
-  View,
-} from '@deephaven/components';
-import {
-  ActionButton,
-  Button,
-  Form,
-  RangeSlider,
-  Slider,
-  TabPanels,
-  TextField,
-} from './spectrum';
-import { ELEMENT_KEY, ElementNode, isElementNode } from './ElementUtils';
-
-// TODO: #425 will be removing Spectrum utils altogether. Just keeping this for
-// now to keep the initial PR smaller.
-export const SPECTRUM_ELEMENT_TYPE_PREFIX = 'deephaven.ui.components.';
-
-export const SpectrumSupportedTypes = {
-  ActionButton,
-  Button,
-  ButtonGroup,
-  Checkbox,
-  Content,
-  ContextualHelp,
-  Flex,
-  Form,
-  Grid,
-  Heading,
-  Icon,
-  IllustratedMessage,
-  NumberField,
-  RangeSlider,
-  Slider,
-  Switch,
-  Tabs,
-  TabList,
-  TabPanels,
-  Text,
-  TextField,
-  ToggleButton,
-  View,
-} as const;
-
-export type SpectrumElementName =
-  `${typeof SPECTRUM_ELEMENT_TYPE_PREFIX}${keyof typeof SpectrumSupportedTypes}`;
-
-/**
- * Describes a Spectrum element that can be rendered in the UI.
- * The type of Spectrum element loaded is the name of the element without the prefix.
- * For example, `deephaven.ui.spectrum.Text` will render the Text component.
- * The props are passed directly to the Spectrum component.
- * @see SpectrumSupportedTypes
- */
-export type SpectrumElementNode = ElementNode<SpectrumElementName>;
-
-export function isSpectrumElementNode(
-  obj: unknown
-): obj is SpectrumElementNode {
-  if (!isElementNode(obj)) {
-    return false;
-  }
-
-  const name = (obj as SpectrumElementNode)[ELEMENT_KEY];
-
-  return (
-    name.startsWith(SPECTRUM_ELEMENT_TYPE_PREFIX) &&
-    name.substring(SPECTRUM_ELEMENT_TYPE_PREFIX.length) in
-      SpectrumSupportedTypes
-  );
-}
-
-/**
- * Get the Spectrum Component for the element
- * @param name Name of the element
- * @returns The Spectrum Component name for the element
- */
-export function getSpectrumComponent(
-  name: SpectrumElementName
-): ValueOf<typeof SpectrumSupportedTypes> {
-  const Component =
-    SpectrumSupportedTypes[
-      name.substring(
-        SPECTRUM_ELEMENT_TYPE_PREFIX.length
-      ) as keyof typeof SpectrumSupportedTypes
-    ];
-
-  if (Component == null) {
-    throw new Error(`Unknown Spectrum component '${name}'`);
-  }
-
-  return Component;
-}
diff --git a/plugins/ui/src/js/src/elements/SpectrumElementView.tsx b/plugins/ui/src/js/src/elements/SpectrumElementView.tsx
deleted file mode 100644
index a610a21a3..000000000
--- a/plugins/ui/src/js/src/elements/SpectrumElementView.tsx
+++ /dev/null
@@ -1,30 +0,0 @@
-import React from 'react';
-import {
-  getSpectrumComponent,
-  SpectrumElementNode,
-} from './SpectrumElementUtils';
-import { ELEMENT_KEY } from './ElementUtils';
-import { mapSpectrumProps } from './spectrum/mapSpectrumProps';
-
-export type SpectrumElementViewProps = {
-  element: SpectrumElementNode;
-};
-
-export function SpectrumElementView({
-  element,
-}: SpectrumElementViewProps): JSX.Element | null {
-  const { [ELEMENT_KEY]: name, props = {} } = element;
-
-  const Component = getSpectrumComponent(name) as React.ComponentType<unknown>;
-
-  if (Component == null) {
-    throw new Error(`Unknown Spectrum component ${name}`);
-  }
-
-  return (
-    // eslint-disable-next-line react/jsx-props-no-spreading, @typescript-eslint/no-explicit-any
-    <Component {...mapSpectrumProps(props)} />
-  );
-}
-
-export default SpectrumElementView;
diff --git a/plugins/ui/src/js/src/elements/spectrum/TabPanels.tsx b/plugins/ui/src/js/src/elements/TabPanels.tsx
similarity index 83%
rename from plugins/ui/src/js/src/elements/spectrum/TabPanels.tsx
rename to plugins/ui/src/js/src/elements/TabPanels.tsx
index c5958d835..a68bde3aa 100644
--- a/plugins/ui/src/js/src/elements/spectrum/TabPanels.tsx
+++ b/plugins/ui/src/js/src/elements/TabPanels.tsx
@@ -4,7 +4,9 @@ import {
   TabPanelsProps as DHCTabPanelsProps,
 } from '@deephaven/components';
 
-function TabPanels(props: DHCTabPanelsProps<React.ReactNode>): JSX.Element {
+export function TabPanels(
+  props: DHCTabPanelsProps<React.ReactNode>
+): JSX.Element {
   const { UNSAFE_style: unsafeStyle, ...otherProps } = props;
 
   return (
diff --git a/plugins/ui/src/js/src/elements/spectrum/TextField.tsx b/plugins/ui/src/js/src/elements/TextField.tsx
similarity index 96%
rename from plugins/ui/src/js/src/elements/spectrum/TextField.tsx
rename to plugins/ui/src/js/src/elements/TextField.tsx
index a9aad7b9e..ae8800d01 100644
--- a/plugins/ui/src/js/src/elements/spectrum/TextField.tsx
+++ b/plugins/ui/src/js/src/elements/TextField.tsx
@@ -16,7 +16,7 @@ interface TextFieldProps extends DHCTextFieldProps {
   onChange?: (value: string) => Promise<void>;
 }
 
-function TextField(props: TextFieldProps): JSX.Element {
+export function TextField(props: TextFieldProps): JSX.Element {
   const {
     defaultValue = '',
     value: propValue,
diff --git a/plugins/ui/src/js/src/elements/UITable.tsx b/plugins/ui/src/js/src/elements/UITable.tsx
index 7127e67b4..53e2b008d 100644
--- a/plugins/ui/src/js/src/elements/UITable.tsx
+++ b/plugins/ui/src/js/src/elements/UITable.tsx
@@ -13,12 +13,12 @@ import type { dh } from '@deephaven/jsapi-types';
 import Log from '@deephaven/log';
 import { getSettings, RootState } from '@deephaven/redux';
 import { EMPTY_ARRAY } from '@deephaven/utils';
-import { UITableProps } from './UITableUtils';
-import UITableMouseHandler from './UITableMouseHandler';
+import { UITableProps } from './utils/UITableUtils';
+import UITableMouseHandler from './utils/UITableMouseHandler';
 
 const log = Log.module('@deephaven/js-plugin-ui/UITable');
 
-function UITable({
+export function UITable({
   onCellPress,
   onCellDoublePress,
   onColumnPress,
diff --git a/plugins/ui/src/js/src/elements/hooks/index.ts b/plugins/ui/src/js/src/elements/hooks/index.ts
new file mode 100644
index 000000000..b678c77a3
--- /dev/null
+++ b/plugins/ui/src/js/src/elements/hooks/index.ts
@@ -0,0 +1,8 @@
+export * from './useButtonProps';
+export * from './useFocusEventCallback';
+export * from './useKeyboardEventCallback';
+export * from './useListViewProps';
+export * from './usePickerProps';
+export * from './usePressEventCallback';
+export * from './useReExportedTable';
+export * from './useSelectionProps';
diff --git a/plugins/ui/src/js/src/elements/spectrum/useButtonProps.ts b/plugins/ui/src/js/src/elements/hooks/useButtonProps.ts
similarity index 86%
rename from plugins/ui/src/js/src/elements/spectrum/useButtonProps.ts
rename to plugins/ui/src/js/src/elements/hooks/useButtonProps.ts
index a7f67fd81..38d193c26 100644
--- a/plugins/ui/src/js/src/elements/spectrum/useButtonProps.ts
+++ b/plugins/ui/src/js/src/elements/hooks/useButtonProps.ts
@@ -1,8 +1,8 @@
 import { usePressEventCallback } from './usePressEventCallback';
 import { useFocusEventCallback } from './useFocusEventCallback';
 import { useKeyboardEventCallback } from './useKeyboardEventCallback';
-import { mapSpectrumProps } from './mapSpectrumProps';
-import { SerializedButtonEventProps } from '../SerializedPropTypes';
+import { SerializedButtonEventProps } from '../model/SerializedPropTypes';
+import { wrapTextChildren } from '../utils';
 
 // returns SpectrumButtonProps
 export function useButtonProps<T>(props: SerializedButtonEventProps<T>): T {
@@ -15,6 +15,7 @@ export function useButtonProps<T>(props: SerializedButtonEventProps<T>): T {
     onBlur: propOnBlur,
     onKeyDown: propOnKeyDown,
     onKeyUp: propOnKeyUp,
+    children,
     ...otherProps
   } = props;
 
@@ -36,7 +37,8 @@ export function useButtonProps<T>(props: SerializedButtonEventProps<T>): T {
     onBlur,
     onKeyDown,
     onKeyUp,
-    ...mapSpectrumProps(otherProps),
+    children: wrapTextChildren(children),
+    ...otherProps,
   } as T;
 }
 
diff --git a/plugins/ui/src/js/src/elements/spectrum/useFocusEventCallback.ts b/plugins/ui/src/js/src/elements/hooks/useFocusEventCallback.ts
similarity index 96%
rename from plugins/ui/src/js/src/elements/spectrum/useFocusEventCallback.ts
rename to plugins/ui/src/js/src/elements/hooks/useFocusEventCallback.ts
index adb1565b8..17b480ecc 100644
--- a/plugins/ui/src/js/src/elements/spectrum/useFocusEventCallback.ts
+++ b/plugins/ui/src/js/src/elements/hooks/useFocusEventCallback.ts
@@ -1,5 +1,5 @@
 import { FocusEvent, useCallback } from 'react';
-import getTargetName from './EventUtils';
+import { getTargetName } from '../utils';
 
 export function serializeFocusEvent(event: FocusEvent): SerializedFocusEvent {
   const { relatedTarget, target, type } = event;
diff --git a/plugins/ui/src/js/src/elements/spectrum/useKeyboardEventCallback.ts b/plugins/ui/src/js/src/elements/hooks/useKeyboardEventCallback.ts
similarity index 100%
rename from plugins/ui/src/js/src/elements/spectrum/useKeyboardEventCallback.ts
rename to plugins/ui/src/js/src/elements/hooks/useKeyboardEventCallback.ts
diff --git a/plugins/ui/src/js/src/elements/useListViewProps.ts b/plugins/ui/src/js/src/elements/hooks/useListViewProps.ts
similarity index 96%
rename from plugins/ui/src/js/src/elements/useListViewProps.ts
rename to plugins/ui/src/js/src/elements/hooks/useListViewProps.ts
index 53df13fed..415dd1035 100644
--- a/plugins/ui/src/js/src/elements/useListViewProps.ts
+++ b/plugins/ui/src/js/src/elements/hooks/useListViewProps.ts
@@ -2,11 +2,11 @@ import { ReactElement } from 'react';
 import type { SelectionMode } from '@react-types/shared';
 import { ListViewProps as DHListViewProps } from '@deephaven/components';
 import { ListViewProps as DHListViewJSApiProps } from '@deephaven/jsapi-components';
-import { ObjectViewProps } from './ObjectView';
+import { ObjectViewProps } from '../ObjectView';
 import {
   SerializedSelectionProps,
   useSelectionProps,
-} from './spectrum/useSelectionProps';
+} from './useSelectionProps';
 
 type Density = Required<DHListViewProps>['density'];
 
diff --git a/plugins/ui/src/js/src/elements/usePickerProps.ts b/plugins/ui/src/js/src/elements/hooks/usePickerProps.ts
similarity index 93%
rename from plugins/ui/src/js/src/elements/usePickerProps.ts
rename to plugins/ui/src/js/src/elements/hooks/usePickerProps.ts
index 06bed3d4c..55666d769 100644
--- a/plugins/ui/src/js/src/elements/usePickerProps.ts
+++ b/plugins/ui/src/js/src/elements/hooks/usePickerProps.ts
@@ -1,15 +1,15 @@
 import { PickerProps as DHPickerProps } from '@deephaven/components';
 import { PickerProps as DHPickerJSApiProps } from '@deephaven/jsapi-components';
 import { ReactElement } from 'react';
-import { ObjectViewProps } from './ObjectView';
+import { ObjectViewProps } from '../ObjectView';
 import {
   SerializedFocusEventCallback,
   useFocusEventCallback,
-} from './spectrum/useFocusEventCallback';
+} from './useFocusEventCallback';
 import {
   SerializedKeyboardEventCallback,
   useKeyboardEventCallback,
-} from './spectrum/useKeyboardEventCallback';
+} from './useKeyboardEventCallback';
 
 export interface SerializedPickerEventProps {
   /** Handler that is called when the element receives focus. */
diff --git a/plugins/ui/src/js/src/elements/spectrum/usePressEventCallback.ts b/plugins/ui/src/js/src/elements/hooks/usePressEventCallback.ts
similarity index 96%
rename from plugins/ui/src/js/src/elements/spectrum/usePressEventCallback.ts
rename to plugins/ui/src/js/src/elements/hooks/usePressEventCallback.ts
index 563660f0a..1aff194c5 100644
--- a/plugins/ui/src/js/src/elements/spectrum/usePressEventCallback.ts
+++ b/plugins/ui/src/js/src/elements/hooks/usePressEventCallback.ts
@@ -1,6 +1,6 @@
 import { useCallback } from 'react';
 import { PressEvent } from '@react-types/shared';
-import getTargetName from './EventUtils';
+import { getTargetName } from '../utils';
 
 export function serializePressEvent(event: PressEvent): SerializedPressEvent {
   const { target, type, pointerType, shiftKey, ctrlKey, metaKey, altKey } =
diff --git a/plugins/ui/src/js/src/elements/useReExportedTable.ts b/plugins/ui/src/js/src/elements/hooks/useReExportedTable.ts
similarity index 87%
rename from plugins/ui/src/js/src/elements/useReExportedTable.ts
rename to plugins/ui/src/js/src/elements/hooks/useReExportedTable.ts
index a09134307..9123ba970 100644
--- a/plugins/ui/src/js/src/elements/useReExportedTable.ts
+++ b/plugins/ui/src/js/src/elements/hooks/useReExportedTable.ts
@@ -2,8 +2,8 @@ import { ReactNode } from 'react';
 import { useTableClose } from '@deephaven/jsapi-components';
 import type { dh } from '@deephaven/jsapi-types';
 import { isElementOfType, usePromiseFactory } from '@deephaven/react-hooks';
-import ObjectView from './ObjectView';
-import { fetchReexportedTable } from './ElementUtils';
+import ObjectView from '../ObjectView';
+import { fetchReexportedTable } from '../utils';
 
 export function useReExportedTable(node: ReactNode): dh.Table | null {
   const isObjectView = isElementOfType(node, ObjectView);
diff --git a/plugins/ui/src/js/src/elements/spectrum/useSelectionProps.ts b/plugins/ui/src/js/src/elements/hooks/useSelectionProps.ts
similarity index 100%
rename from plugins/ui/src/js/src/elements/spectrum/useSelectionProps.ts
rename to plugins/ui/src/js/src/elements/hooks/useSelectionProps.ts
diff --git a/plugins/ui/src/js/src/elements/index.ts b/plugins/ui/src/js/src/elements/index.ts
new file mode 100644
index 000000000..d05cabf01
--- /dev/null
+++ b/plugins/ui/src/js/src/elements/index.ts
@@ -0,0 +1,19 @@
+export * from './ActionButton';
+export * from './ActionGroup';
+export * from './Button';
+export * from './Form';
+export * from './hooks';
+export * from './HTMLElementView';
+export * from './IconElementView';
+export * from './ListView';
+export * from './model';
+export * from './ObjectView';
+export * from './Picker';
+export * from './Radio';
+export * from './RadioGroup';
+export * from './RangeSlider';
+export * from './Slider';
+export * from './TabPanels';
+export * from './TextField';
+export * from './UITable';
+export * from './utils';
diff --git a/plugins/ui/src/js/src/elements/ElementConstants.ts b/plugins/ui/src/js/src/elements/model/ElementConstants.ts
similarity index 63%
rename from plugins/ui/src/js/src/elements/ElementConstants.ts
rename to plugins/ui/src/js/src/elements/model/ElementConstants.ts
index 6fffd69de..22d4610ce 100644
--- a/plugins/ui/src/js/src/elements/ElementConstants.ts
+++ b/plugins/ui/src/js/src/elements/model/ElementConstants.ts
@@ -23,17 +23,39 @@ export const ELEMENT_NAME = {
   stack: uiComponentName('Stack'),
 
   /** Other Components */
+  actionButton: uiComponentName('ActionButton'),
   actionGroup: uiComponentName('ActionGroup'),
   actionMenu: uiComponentName('ActionMenu'),
+  button: uiComponentName('Button'),
+  buttonGroup: uiComponentName('ButtonGroup'),
+  checkbox: uiComponentName('Checkbox'),
+  content: uiComponentName('Content'),
+  contextualHelp: uiComponentName('ContextualHelp'),
+  flex: uiComponentName('Flex'),
+  form: uiComponentName('Form'),
   fragment: uiComponentName('Fragment'),
+  grid: uiComponentName('Grid'),
+  heading: uiComponentName('Heading'),
+  illustratedMessage: uiComponentName('IllustratedMessage'),
   item: uiComponentName('Item'),
   listActionGroup: uiComponentName('ListActionGroup'),
   listActionMenu: uiComponentName('ListActionMenu'),
   listView: uiComponentName('ListView'),
+  numberField: uiComponentName('NumberField'),
   picker: uiComponentName('Picker'),
   radio: uiComponentName('Radio'),
   radioGroup: uiComponentName('RadioGroup'),
+  rangeSlider: uiComponentName('RangeSlider'),
   section: uiComponentName('Section'),
+  slider: uiComponentName('Slider'),
+  switch: uiComponentName('Switch'),
+  tabList: uiComponentName('TabList'),
+  tabPanels: uiComponentName('TabPanels'),
+  tabs: uiComponentName('Tabs'),
+  text: uiComponentName('Text'),
+  textField: uiComponentName('TextField'),
+  toggleButton: uiComponentName('ToggleButton'),
+  view: uiComponentName('View'),
 } as const;
 
 export type ElementName = typeof ELEMENT_NAME;
diff --git a/plugins/ui/src/js/src/elements/SerializedPropTypes.ts b/plugins/ui/src/js/src/elements/model/SerializedPropTypes.ts
similarity index 82%
rename from plugins/ui/src/js/src/elements/SerializedPropTypes.ts
rename to plugins/ui/src/js/src/elements/model/SerializedPropTypes.ts
index ad8a8ecb7..16914c1d5 100644
--- a/plugins/ui/src/js/src/elements/SerializedPropTypes.ts
+++ b/plugins/ui/src/js/src/elements/model/SerializedPropTypes.ts
@@ -1,6 +1,7 @@
-import { SerializedFocusEventCallback } from './spectrum/useFocusEventCallback';
-import { SerializedKeyboardEventCallback } from './spectrum/useKeyboardEventCallback';
-import { SerializedPressEventCallback } from './spectrum/usePressEventCallback';
+import React from 'react';
+import { SerializedFocusEventCallback } from '../hooks/useFocusEventCallback';
+import { SerializedKeyboardEventCallback } from '../hooks/useKeyboardEventCallback';
+import { SerializedPressEventCallback } from '../hooks/usePressEventCallback';
 
 export type SerializedFocusEventProps<T> = Omit<T, 'onFocus' | 'onBlur'> & {
   /** Handler that is called when the element receives focus. */
@@ -45,4 +46,4 @@ export type SerializedPressEventProps<T> = Omit<
 
 export type SerializedButtonEventProps<T> = SerializedFocusEventProps<
   SerializedKeyboardEventProps<SerializedPressEventProps<T>>
->;
+> & { children: React.ReactNode };
diff --git a/plugins/ui/src/js/src/elements/model/index.ts b/plugins/ui/src/js/src/elements/model/index.ts
new file mode 100644
index 000000000..f70b1d838
--- /dev/null
+++ b/plugins/ui/src/js/src/elements/model/index.ts
@@ -0,0 +1,2 @@
+export * from './ElementConstants';
+export * from './SerializedPropTypes';
diff --git a/plugins/ui/src/js/src/elements/spectrum/index.ts b/plugins/ui/src/js/src/elements/spectrum/index.ts
deleted file mode 100644
index a3e908593..000000000
--- a/plugins/ui/src/js/src/elements/spectrum/index.ts
+++ /dev/null
@@ -1,13 +0,0 @@
-/**
- * Wrappers for Spectrum components.
- * Used if we want to mediate some of the props values passed to the Spectrum components.
- * For example, we may want to debounce sending the change for a text field, and also keep the value on the client side until the change is sent.
- * Or in the case of event handlers, we may want to wrap the event handler to serialize the event correctly.
- */
-export { default as ActionButton } from './ActionButton';
-export { default as Button } from './Button';
-export { default as Form } from './Form';
-export { default as RangeSlider } from './RangeSlider';
-export { default as Slider } from './Slider';
-export { default as TabPanels } from './TabPanels';
-export { default as TextField } from './TextField';
diff --git a/plugins/ui/src/js/src/elements/spectrum/mapSpectrumChildren.tsx b/plugins/ui/src/js/src/elements/spectrum/mapSpectrumChildren.tsx
deleted file mode 100644
index 12dedb10a..000000000
--- a/plugins/ui/src/js/src/elements/spectrum/mapSpectrumChildren.tsx
+++ /dev/null
@@ -1,20 +0,0 @@
-import React from 'react';
-import { Text } from '@deephaven/components';
-
-/**
- * Map the children of an element to Spectrum children, automatically wrapping strings and numbers in `Text` elements.
- * @param children Children to map as spectrum children
- */
-export function mapSpectrumChildren(
-  children: React.ReactNode
-): React.ReactNode {
-  const childrenArray = Array.isArray(children) ? children : [children];
-  return childrenArray.map(child => {
-    if (typeof child === 'string') {
-      return <Text key={child}>{child}</Text>;
-    }
-    return child;
-  });
-}
-
-export default mapSpectrumChildren;
diff --git a/plugins/ui/src/js/src/elements/spectrum/mapSpectrumProps.ts b/plugins/ui/src/js/src/elements/spectrum/mapSpectrumProps.ts
deleted file mode 100644
index c7c693473..000000000
--- a/plugins/ui/src/js/src/elements/spectrum/mapSpectrumProps.ts
+++ /dev/null
@@ -1,18 +0,0 @@
-import { PropsWithChildren } from 'react';
-import mapSpectrumChildren from './mapSpectrumChildren';
-
-/**
- * Map the props of an element to Spectrum props, automatically wrapping children strings and numbers in `Text` elements.
- * @param props Props to map as spectrum props
- */
-export function mapSpectrumProps<
-  T extends PropsWithChildren<Record<string, unknown>>,
->(props: T): T {
-  return {
-    ...props,
-    children:
-      props?.children != null ? mapSpectrumChildren(props.children) : undefined,
-  };
-}
-
-export default mapSpectrumChildren;
diff --git a/plugins/ui/src/js/src/elements/ElementUtils.tsx b/plugins/ui/src/js/src/elements/utils/ElementUtils.tsx
similarity index 91%
rename from plugins/ui/src/js/src/elements/ElementUtils.tsx
rename to plugins/ui/src/js/src/elements/utils/ElementUtils.tsx
index b5fa2759e..9e8b05f38 100644
--- a/plugins/ui/src/js/src/elements/ElementUtils.tsx
+++ b/plugins/ui/src/js/src/elements/utils/ElementUtils.tsx
@@ -1,8 +1,8 @@
 import React from 'react';
 import { Text } from '@deephaven/components';
 import type { dh } from '@deephaven/jsapi-types';
-import { ELEMENT_NAME } from './ElementConstants';
-import ObjectView from './ObjectView';
+import { ELEMENT_NAME } from '../model/ElementConstants';
+import ObjectView from '../ObjectView';
 
 export const CALLABLE_KEY = '__dhCbid';
 export const OBJECT_KEY = '__dhObid';
@@ -212,3 +212,18 @@ export function wrapElementChildren(element: ElementNode): ElementNode {
     props: newProps,
   };
 }
+
+/**
+ * Map the children of an element to an array, automatically wrapping primitive
+ * values in `Text` elements.
+ * @param children Children to map as an array.
+ */
+export function wrapTextChildren(children: React.ReactNode): React.ReactNode {
+  const childrenArray = Array.isArray(children) ? children : [children];
+  return childrenArray.map(child => {
+    if (isPrimitive(child)) {
+      return <Text key={String(child)}>{String(child)}</Text>;
+    }
+    return child;
+  });
+}
diff --git a/plugins/ui/src/js/src/elements/spectrum/EventUtils.ts b/plugins/ui/src/js/src/elements/utils/EventUtils.ts
similarity index 100%
rename from plugins/ui/src/js/src/elements/spectrum/EventUtils.ts
rename to plugins/ui/src/js/src/elements/utils/EventUtils.ts
diff --git a/plugins/ui/src/js/src/elements/HTMLElementUtils.ts b/plugins/ui/src/js/src/elements/utils/HTMLElementUtils.ts
similarity index 92%
rename from plugins/ui/src/js/src/elements/HTMLElementUtils.ts
rename to plugins/ui/src/js/src/elements/utils/HTMLElementUtils.ts
index 9530a0e69..1c59ebd93 100644
--- a/plugins/ui/src/js/src/elements/HTMLElementUtils.ts
+++ b/plugins/ui/src/js/src/elements/utils/HTMLElementUtils.ts
@@ -1,5 +1,5 @@
 import { ReactHTML } from 'react';
-import { ELEMENT_PREFIX, ElementPrefix } from './ElementConstants';
+import { ELEMENT_PREFIX, ElementPrefix } from '../model/ElementConstants';
 import { ELEMENT_KEY, ElementNode, isElementNode } from './ElementUtils';
 
 /**
diff --git a/plugins/ui/src/js/src/elements/IconElementUtils.ts b/plugins/ui/src/js/src/elements/utils/IconElementUtils.ts
similarity index 91%
rename from plugins/ui/src/js/src/elements/IconElementUtils.ts
rename to plugins/ui/src/js/src/elements/utils/IconElementUtils.ts
index 1bdfe0974..781aea8d1 100644
--- a/plugins/ui/src/js/src/elements/IconElementUtils.ts
+++ b/plugins/ui/src/js/src/elements/utils/IconElementUtils.ts
@@ -1,5 +1,5 @@
 import * as icons from '@deephaven/icons';
-import { ELEMENT_PREFIX, ElementPrefix } from './ElementConstants';
+import { ELEMENT_PREFIX, ElementPrefix } from '../model/ElementConstants';
 import { ELEMENT_KEY, ElementNode, isElementNode } from './ElementUtils';
 
 /**
diff --git a/plugins/ui/src/js/src/elements/UITableMouseHandler.ts b/plugins/ui/src/js/src/elements/utils/UITableMouseHandler.ts
similarity index 100%
rename from plugins/ui/src/js/src/elements/UITableMouseHandler.ts
rename to plugins/ui/src/js/src/elements/utils/UITableMouseHandler.ts
diff --git a/plugins/ui/src/js/src/elements/UITableUtils.tsx b/plugins/ui/src/js/src/elements/utils/UITableUtils.tsx
similarity index 95%
rename from plugins/ui/src/js/src/elements/UITableUtils.tsx
rename to plugins/ui/src/js/src/elements/utils/UITableUtils.tsx
index 21769a190..1abb6aa1c 100644
--- a/plugins/ui/src/js/src/elements/UITableUtils.tsx
+++ b/plugins/ui/src/js/src/elements/utils/UITableUtils.tsx
@@ -1,7 +1,7 @@
 import type { dh } from '@deephaven/jsapi-types';
 import { ColumnName, DehydratedSort, RowIndex } from '@deephaven/iris-grid';
 import { ELEMENT_KEY, ElementNode, isElementNode } from './ElementUtils';
-import { ELEMENT_NAME, ElementName } from './ElementConstants';
+import { ELEMENT_NAME, ElementName } from '../model/ElementConstants';
 
 export type CellData = {
   type: string;
diff --git a/plugins/ui/src/js/src/elements/utils/index.ts b/plugins/ui/src/js/src/elements/utils/index.ts
new file mode 100644
index 000000000..69419497a
--- /dev/null
+++ b/plugins/ui/src/js/src/elements/utils/index.ts
@@ -0,0 +1,6 @@
+export * from './ElementUtils';
+export * from './EventUtils';
+export * from './HTMLElementUtils';
+export * from './IconElementUtils';
+export * from './UITableMouseHandler';
+export * from './UITableUtils';
diff --git a/plugins/ui/src/js/src/layout/LayoutUtils.tsx b/plugins/ui/src/js/src/layout/LayoutUtils.tsx
index 6cca370dd..b2d78ec49 100644
--- a/plugins/ui/src/js/src/layout/LayoutUtils.tsx
+++ b/plugins/ui/src/js/src/layout/LayoutUtils.tsx
@@ -8,12 +8,12 @@ import {
   ELEMENT_KEY,
   ElementNode,
   isElementNode,
-} from '../elements/ElementUtils';
+} from '../elements/utils/ElementUtils';
 import Column from './Column';
 import Row from './Row';
 import Stack from './Stack';
 import ReactPanel from './ReactPanel';
-import { ElementName, ELEMENT_NAME } from '../elements/ElementConstants';
+import { ElementName, ELEMENT_NAME } from '../elements/model/ElementConstants';
 
 export type GoldenLayoutParent = RowOrColumn | GLStack | Root;
 
diff --git a/plugins/ui/src/js/src/widget/WidgetHandler.tsx b/plugins/ui/src/js/src/widget/WidgetHandler.tsx
index 918ba0bf1..c15f4b17d 100644
--- a/plugins/ui/src/js/src/widget/WidgetHandler.tsx
+++ b/plugins/ui/src/js/src/widget/WidgetHandler.tsx
@@ -24,7 +24,7 @@ import {
   isCallableNode,
   isElementNode,
   isObjectNode,
-} from '../elements/ElementUtils';
+} from '../elements/utils/ElementUtils';
 import {
   ReadonlyWidgetData,
   WidgetDataUpdate,
diff --git a/plugins/ui/src/js/src/widget/WidgetUtils.tsx b/plugins/ui/src/js/src/widget/WidgetUtils.tsx
index 524e6f763..57b01821e 100644
--- a/plugins/ui/src/js/src/widget/WidgetUtils.tsx
+++ b/plugins/ui/src/js/src/widget/WidgetUtils.tsx
@@ -5,10 +5,25 @@ import React, { ComponentType } from 'react';
 // wrapped due to how Spectrum collection components consume them.
 import {
   ActionMenu,
+  ButtonGroup,
+  SpectrumCheckbox as Checkbox,
+  Content,
+  ContextualHelp,
+  Flex,
+  Grid,
+  Heading,
+  IllustratedMessage,
   Item,
   ListActionGroup,
   ListActionMenu,
+  NumberField,
   Section,
+  Switch,
+  TabList,
+  Tabs,
+  Text,
+  ToggleButton,
+  View,
 } from '@deephaven/components';
 import { ValueOf } from '@deephaven/utils';
 import { ReadonlyWidgetData } from './WidgetTypes';
@@ -17,25 +32,43 @@ import {
   ELEMENT_KEY,
   isElementNode,
   wrapElementChildren,
-} from '../elements/ElementUtils';
+  wrapTextChildren,
+} from '../elements/utils/ElementUtils';
 import HTMLElementView from '../elements/HTMLElementView';
-import { isHTMLElementNode } from '../elements/HTMLElementUtils';
-import { isSpectrumElementNode } from '../elements/SpectrumElementUtils';
-import SpectrumElementView from '../elements/SpectrumElementView';
-import { isIconElementNode } from '../elements/IconElementUtils';
+import { isHTMLElementNode } from '../elements/utils/HTMLElementUtils';
+import { isIconElementNode } from '../elements/utils/IconElementUtils';
 import IconElementView from '../elements/IconElementView';
-import UITable from '../elements/UITable';
-import { ELEMENT_NAME, ElementName } from '../elements/ElementConstants';
+import { ELEMENT_NAME, ElementName } from '../elements/model/ElementConstants';
 import ReactPanel from '../layout/ReactPanel';
 import Row from '../layout/Row';
 import Stack from '../layout/Stack';
 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';
-import Radio from '../elements/Radio';
-import RadioGroup from '../elements/RadioGroup';
+import {
+  ActionButton,
+  ActionGroup,
+  Button,
+  Form,
+  ListView,
+  Picker,
+  Radio,
+  RadioGroup,
+  RangeSlider,
+  Slider,
+  TabPanels,
+  TextField,
+  UITable,
+} from '../elements';
+
+/**
+ * Elements to implicitly wrap primitive children in <Text> components.
+ */
+const shouldWrapTextChildren = new Set<string>([
+  ELEMENT_NAME.column,
+  ELEMENT_NAME.flex,
+  ELEMENT_NAME.grid,
+  ELEMENT_NAME.view,
+]);
 
 /*
  * Map element node names to their corresponding React components
@@ -52,17 +85,39 @@ export const elementComponentMap = {
   [ELEMENT_NAME.stack]: Stack,
 
   // Other components
+  [ELEMENT_NAME.actionButton]: ActionButton,
   [ELEMENT_NAME.actionGroup]: ActionGroup,
   [ELEMENT_NAME.actionMenu]: ActionMenu,
+  [ELEMENT_NAME.button]: Button,
+  [ELEMENT_NAME.buttonGroup]: ButtonGroup,
+  [ELEMENT_NAME.checkbox]: Checkbox,
+  [ELEMENT_NAME.content]: Content,
+  [ELEMENT_NAME.contextualHelp]: ContextualHelp,
+  [ELEMENT_NAME.flex]: Flex,
+  [ELEMENT_NAME.form]: Form,
   [ELEMENT_NAME.fragment]: React.Fragment,
+  [ELEMENT_NAME.grid]: Grid,
+  [ELEMENT_NAME.heading]: Heading,
+  [ELEMENT_NAME.illustratedMessage]: IllustratedMessage,
   [ELEMENT_NAME.item]: Item,
   [ELEMENT_NAME.listActionGroup]: ListActionGroup,
   [ELEMENT_NAME.listActionMenu]: ListActionMenu,
   [ELEMENT_NAME.listView]: ListView,
+  [ELEMENT_NAME.numberField]: NumberField,
   [ELEMENT_NAME.picker]: Picker,
   [ELEMENT_NAME.radio]: Radio,
   [ELEMENT_NAME.radioGroup]: RadioGroup,
+  [ELEMENT_NAME.rangeSlider]: RangeSlider,
   [ELEMENT_NAME.section]: Section,
+  [ELEMENT_NAME.slider]: Slider,
+  [ELEMENT_NAME.switch]: Switch,
+  [ELEMENT_NAME.tabList]: TabList,
+  [ELEMENT_NAME.tabPanels]: TabPanels,
+  [ELEMENT_NAME.tabs]: Tabs,
+  [ELEMENT_NAME.text]: Text,
+  [ELEMENT_NAME.textField]: TextField,
+  [ELEMENT_NAME.toggleButton]: ToggleButton,
+  [ELEMENT_NAME.view]: View,
 } as const satisfies Record<ValueOf<ElementName>, unknown>;
 
 export function getComponentTypeForElement<P extends Record<string, unknown>>(
@@ -79,9 +134,6 @@ export function getComponentForElement(element: ElementNode): React.ReactNode {
   if (isHTMLElementNode(newElement)) {
     return HTMLElementView({ element: newElement });
   }
-  if (isSpectrumElementNode(newElement)) {
-    return SpectrumElementView({ element: newElement });
-  }
   if (isIconElementNode(newElement)) {
     return IconElementView({ element: newElement });
   }
@@ -89,7 +141,16 @@ export function getComponentForElement(element: ElementNode): React.ReactNode {
     const Component = getComponentTypeForElement(newElement);
 
     if (Component != null) {
-      return <Component {...newElement.props} />;
+      const props =
+        shouldWrapTextChildren.has(newElement[ELEMENT_KEY]) &&
+        newElement.props?.children != null
+          ? {
+              ...newElement.props,
+              children: wrapTextChildren(newElement.props.children),
+            }
+          : newElement.props;
+
+      return <Component {...props} />;
     }
   }
 


Test patch:
diff --git a/plugins/ui/src/js/src/elements/ElementUtils.test.tsx b/plugins/ui/src/js/src/elements/utils/ElementUtils.test.tsx
similarity index 97%
rename from plugins/ui/src/js/src/elements/ElementUtils.test.tsx
rename to plugins/ui/src/js/src/elements/utils/ElementUtils.test.tsx
index 88bfbef6a..c3a0767a3 100644
--- a/plugins/ui/src/js/src/elements/ElementUtils.test.tsx
+++ b/plugins/ui/src/js/src/elements/utils/ElementUtils.test.tsx
@@ -8,8 +8,8 @@ import {
   isPrimitive,
   wrapElementChildren,
 } from './ElementUtils';
-import ObjectView from './ObjectView';
-import { ELEMENT_NAME } from './ElementConstants';
+import ObjectView from '../ObjectView';
+import { ELEMENT_NAME } from '../model/ElementConstants';
 
 const { asMock, createMockProxy } = TestUtils;
 
diff --git a/plugins/ui/src/js/src/elements/spectrum/EventUtils.test.ts b/plugins/ui/src/js/src/elements/utils/EventUtils.test.ts
similarity index 100%
rename from plugins/ui/src/js/src/elements/spectrum/EventUtils.test.ts
rename to plugins/ui/src/js/src/elements/utils/EventUtils.test.ts
diff --git a/plugins/ui/src/js/src/layout/LayoutUtils.test.tsx b/plugins/ui/src/js/src/layout/LayoutUtils.test.tsx
index 8063bc8e2..a8b452c6a 100644
--- a/plugins/ui/src/js/src/layout/LayoutUtils.test.tsx
+++ b/plugins/ui/src/js/src/layout/LayoutUtils.test.tsx
@@ -15,7 +15,7 @@ import {
 import Row from './Row';
 import Stack from './Stack';
 import ReactPanel from './ReactPanel';
-import { ELEMENT_NAME } from '../elements/ElementConstants';
+import { ELEMENT_NAME } from '../elements/model/ElementConstants';
 
 beforeEach(() => {
   TestUtils.disableConsoleOutput();
diff --git a/plugins/ui/src/js/src/widget/DocumentHandler.test.tsx b/plugins/ui/src/js/src/widget/DocumentHandler.test.tsx
index 43aae2c39..0cf6037ce 100644
--- a/plugins/ui/src/js/src/widget/DocumentHandler.test.tsx
+++ b/plugins/ui/src/js/src/widget/DocumentHandler.test.tsx
@@ -6,7 +6,7 @@ import DocumentHandler, { DocumentHandlerProps } from './DocumentHandler';
 import { ReactPanelProps } from '../layout/LayoutUtils';
 import { MixedPanelsError, NoChildrenError } from '../errors';
 import { getComponentForElement } from './WidgetUtils';
-import { ELEMENT_NAME } from '../elements/ElementConstants';
+import { ELEMENT_NAME } from '../elements/model/ElementConstants';
 
 const mockReactPanel = jest.fn((props: ReactPanelProps) => (
   <div>ReactPanel</div>
diff --git a/plugins/ui/src/js/src/widget/WidgetUtils.test.tsx b/plugins/ui/src/js/src/widget/WidgetUtils.test.tsx
index ee6665160..a9e8fba29 100644
--- a/plugins/ui/src/js/src/widget/WidgetUtils.test.tsx
+++ b/plugins/ui/src/js/src/widget/WidgetUtils.test.tsx
@@ -1,11 +1,12 @@
 import React from 'react';
 import { Text } from '@deephaven/components';
-import { ELEMENT_NAME, ELEMENT_PREFIX } from '../elements/ElementConstants';
-import { ElementNode, ELEMENT_KEY } from '../elements/ElementUtils';
+import {
+  ELEMENT_NAME,
+  ELEMENT_PREFIX,
+} from '../elements/model/ElementConstants';
+import { ElementNode, ELEMENT_KEY } from '../elements/utils/ElementUtils';
 import HTMLElementView from '../elements/HTMLElementView';
 import IconElementView from '../elements/IconElementView';
-import { SPECTRUM_ELEMENT_TYPE_PREFIX } from '../elements/SpectrumElementUtils';
-import SpectrumElementView from '../elements/SpectrumElementView';
 import {
   elementComponentMap,
   getComponentForElement,
@@ -29,7 +30,6 @@ describe('getComponentForElement', () => {
   it.each([
     /* eslint-disable react/jsx-key */
     [`${ELEMENT_PREFIX.html}div`, HTMLElementView],
-    [`${SPECTRUM_ELEMENT_TYPE_PREFIX}ActionButton`, SpectrumElementView],
     [`${ELEMENT_PREFIX.icon}vsAdd`, IconElementView],
     /* eslint-enable react/jsx-key */
   ] as [string, ({ element }: { element: unknown }) => JSX.Element][])(
diff --git a/tests/ui.spec.ts-snapshots/UI-all-components-render-1-chromium-linux.png b/tests/ui.spec.ts-snapshots/UI-all-components-render-1-chromium-linux.png
index 1172b696f..497c29423 100644
Binary files a/tests/ui.spec.ts-snapshots/UI-all-components-render-1-chromium-linux.png and b/tests/ui.spec.ts-snapshots/UI-all-components-render-1-chromium-linux.png differ
diff --git a/tests/ui.spec.ts-snapshots/UI-all-components-render-1-firefox-linux.png b/tests/ui.spec.ts-snapshots/UI-all-components-render-1-firefox-linux.png
index 92294a517..84b32377c 100644
Binary files a/tests/ui.spec.ts-snapshots/UI-all-components-render-1-firefox-linux.png and b/tests/ui.spec.ts-snapshots/UI-all-components-render-1-firefox-linux.png differ
diff --git a/tests/ui.spec.ts-snapshots/UI-all-components-render-1-webkit-linux.png b/tests/ui.spec.ts-snapshots/UI-all-components-render-1-webkit-linux.png
index 377493e73..456fb28df 100644
Binary files a/tests/ui.spec.ts-snapshots/UI-all-components-render-1-webkit-linux.png and b/tests/ui.spec.ts-snapshots/UI-all-components-render-1-webkit-linux.png differ