This is an automated email from the ASF dual-hosted git repository.

rusackas pushed a commit to branch feat/granular-theming-v2
in repository https://gitbox.apache.org/repos/asf/superset.git

commit 1be84f1769716cfd920ae1a1b7a5bba216a179e5
Author: Claude <[email protected]>
AuthorDate: Thu May 14 13:59:43 2026 -0700

    feat(dashboard): component theming for Tabs, Row, Column, Markdown — Phase 
4b-4e
    
    Same three-step recipe applied to each grid-component type:
    
      (a) wrap body in <ComponentThemeProvider layoutId={id}>
      (b) add "Apply theme" item to the component's menu via
          ComponentHeaderControls
      (c) mount <ThemeSelectorModal> gated on editMode
    
    - TabsRenderer (4b): wraps StyledTabsContainer; dots menu lands in the
      existing left HoverMenu next to drag/delete.
    - Row (4c): wraps WithPopoverMenu body; dots menu in the left HoverMenu
      next to drag/delete/setting-icon. The existing gear icon (opens the
      BackgroundStyleDropdown focus popover) is preserved as-is.
    - Column (4d): same recipe as Row, top-positioned HoverMenu.
    - Markdown (4e): class component, so themeModalOpen lives on
      this.state. Dots menu lands inside the existing WithPopoverMenu
      menuItems array next to MarkdownModeDropdown; the Edit/Preview
      toggle is intentionally preserved unchanged.
    
    Note on scope: the SIP originally imagined Phase 4 would also converge
    MarkdownModeDropdown and the Row/Column gear icon onto the shared dots
    menu. Those user-visible UX displacements are intentionally deferred
    so this phase adds the theming affordance *additively* — every existing
    menu control is untouched. The menu-pattern unification can be picked
    up later without coupling it to theming.
    
    Functional outcome: every grid-component type (Chart, Markdown, Row,
    Column, Tabs) now supports the full inheritance chain end-to-end:
    Instance -> Dashboard -> Tab -> Row/Col -> Chart/Markdown. Setting a
    themeId at any level applies to that subtree; clearing it falls
    through to the parent.
    
    Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
---
 SIP.md                                             |  60 +++--
 .../components/gridComponents/Column/Column.tsx    | 267 +++++++++++----------
 .../gridComponents/Markdown/Markdown.tsx           | 129 ++++++----
 .../components/gridComponents/Row/Row.tsx          | 259 +++++++++++---------
 .../gridComponents/TabsRenderer/TabsRenderer.tsx   | 141 ++++++-----
 5 files changed, 487 insertions(+), 369 deletions(-)

diff --git a/SIP.md b/SIP.md
index d73bafcd000..306beff3ad3 100644
--- a/SIP.md
+++ b/SIP.md
@@ -196,26 +196,46 @@ Each phase brings its own tests; the cumulative bar:
   3 passing tests on `setComponentThemeId`: preserves other meta keys
   + sets numeric `themeId`; stores explicit `null` for the clear path;
   no-op when the component id isn't in the layout.
-- _(Phase 4)_ — in progress.
-  - **Chart (4a)**: ✅ landed locally. End-to-end demo on Chart works
-    now: `SliceHeaderControls` has a new "Apply theme" item (gated on
-    dashboard edit mode); clicking it opens the Phase-3
-    `ThemeSelectorModal` keyed to the component's layoutId; on save the
-    Phase-3 action updates `meta.themeId`; the Phase-1
-    `ComponentThemeProvider` (already wrapping ChartHolder) re-resolves
-    and re-renders the chart with the new theme tokens. The full
-    Instance → Dashboard → Tab → Row/Col → Chart inheritance chain is
-    functionally complete for Chart.
-
-    Open follow-ups for the **Markdown / Row / Column / Tabs** PRs:
-    - Each gets the menu-pattern conversion (`MarkdownModeDropdown`,
-      gear icon, none → shared `ComponentHeaderControls`).
-    - Each wraps its body in `<ComponentThemeProvider layoutId=...>`.
-    - Each mounts a `<ThemeSelectorModal>` with an "Apply theme" menu
-      item that opens it.
-    - Each per-component PR can be reviewed in isolation for the menu/UX
-      change without dragging in the theming framework changes (those
-      are already merged in Phases 1-3).
+- _(Phase 4)_ — ✅ landed locally for all five grid-component types.
+  Same three-step recipe applied to each:
+  (a) wrap body in `<ComponentThemeProvider layoutId={id}>`,
+  (b) add "Apply theme" item to the component's menu via
+      `ComponentHeaderControls`,
+  (c) mount `<ThemeSelectorModal>` gated on `editMode`.
+
+  - **Chart (4a)**: `SliceHeaderControls` gets the menu item; the
+    provider was already wrapping `ChartHolder` from Phase 1.
+  - **Tabs (4b)**: `TabsRenderer` wraps `<StyledTabsContainer>` in the
+    provider; adds the dots-menu trigger inside the existing left
+    `HoverMenu` next to the drag handle and delete button.
+  - **Row (4c)**: wraps the `<WithPopoverMenu>` body; adds the
+    dots-menu trigger to the left `HoverMenu` next to drag/delete/
+    setting-icon. The existing gear icon (which opens the
+    BackgroundStyleDropdown focus popover) is preserved as-is.
+  - **Column (4d)**: same recipe as Row, wrapping its
+    `<WithPopoverMenu>` body and adding the dots menu to the top
+    `HoverMenu` next to drag/delete/setting-icon.
+  - **Markdown (4e)**: class component, so theme-modal state goes
+    through `this.state.themeModalOpen`. Adds a second
+    `ComponentHeaderControls` to the existing `<WithPopoverMenu
+    menuItems>` array next to the `MarkdownModeDropdown`
+    (Edit/Preview toggle is preserved as-is — the full menu-pattern
+    convergence onto a single dots menu is intentionally deferred so
+    Markdown's Edit/Preview UX is not changed in this phase).
+
+  Functional outcome: every grid-component type now supports the full
+  Instance → Dashboard → Tab → Row/Col → Chart/Markdown inheritance
+  chain end-to-end. Setting a `themeId` at any level applies to that
+  subtree; clearing it falls through to the parent.
+
+  Note on the broader menu-pattern unification: the SIP originally
+  imagined Phase 4 PRs would also converge `MarkdownModeDropdown`
+  (Edit/Preview popover) and the Row/Column gear icon into the shared
+  dots menu. We deferred those user-visible UX displacements so each
+  Phase-4 PR adds the theming affordance *additively* — i.e. the
+  existing menu controls are untouched, the dots menu sits alongside.
+  A follow-up SIP (or single sweep PR) can take the menu unification
+  later without coupling it to the theming work.
 
 ### Phase 1 status
 
diff --git 
a/superset-frontend/src/dashboard/components/gridComponents/Column/Column.tsx 
b/superset-frontend/src/dashboard/components/gridComponents/Column/Column.tsx
index 72be4b59a67..3cbc4ec5176 100644
--- 
a/superset-frontend/src/dashboard/components/gridComponents/Column/Column.tsx
+++ 
b/superset-frontend/src/dashboard/components/gridComponents/Column/Column.tsx
@@ -35,6 +35,9 @@ import IconButton from 'src/dashboard/components/IconButton';
 import ResizableContainer from 
'src/dashboard/components/resizable/ResizableContainer';
 import BackgroundStyleDropdown from 
'src/dashboard/components/menu/BackgroundStyleDropdown';
 import WithPopoverMenu from 'src/dashboard/components/menu/WithPopoverMenu';
+import ComponentThemeProvider from 
'src/dashboard/components/ComponentThemeProvider';
+import ComponentHeaderControls from 
'src/dashboard/components/menu/ComponentHeaderControls';
+import ThemeSelectorModal from 'src/dashboard/components/ThemeSelectorModal';
 import backgroundStyleOptions from 'src/dashboard/util/backgroundStyleOptions';
 import { BACKGROUND_TRANSPARENT } from 'src/dashboard/util/constants';
 import { EMPTY_CONTAINER_Z_INDEX } from 'src/dashboard/constants';
@@ -161,6 +164,7 @@ const Column = (props: ColumnProps) => {
   } = props;
 
   const [isFocused, setIsFocused] = useState(false);
+  const [themeModalOpen, setThemeModalOpen] = useState(false);
 
   const handleDeleteComponent = useCallback(() => {
     deleteComponent(id, parentId);
@@ -216,135 +220,154 @@ const Column = (props: ColumnProps) => {
         onResizeStop={onResizeStop}
         editMode={editMode}
       >
-        <WithPopoverMenu
-          isFocused={isFocused}
-          onChangeFocus={handleChangeFocus}
-          disableClick
-          menuItems={[
-            <BackgroundStyleDropdown
-              key={`${columnComponent.id}-background`}
-              id={`${columnComponent.id}-background`}
-              value={
-                (columnComponent.meta.background as string) ||
-                BACKGROUND_TRANSPARENT
-              }
-              onChange={handleChangeBackground}
-            />,
-          ]}
-          editMode={editMode}
-        >
-          {editMode && (
-            <HoverMenu
-              innerRef={
-                dragSourceRef as unknown as React.RefObject<HTMLDivElement>
-              }
-              position="top"
-            >
-              <DragHandle position="top" />
-              <DeleteComponentButton
-                iconSize="m"
-                onDelete={handleDeleteComponent}
-              />
-              <IconButton
-                onClick={() => handleChangeFocus(true)}
-                icon={<Icons.SettingOutlined iconSize="m" />}
-              />
-            </HoverMenu>
-          )}
-          <ColumnStyles
-            className={cx('grid-column', backgroundStyle?.className)}
+        <ComponentThemeProvider layoutId={columnComponent.id}>
+          <WithPopoverMenu
+            isFocused={isFocused}
+            onChangeFocus={handleChangeFocus}
+            disableClick
+            menuItems={[
+              <BackgroundStyleDropdown
+                key={`${columnComponent.id}-background`}
+                id={`${columnComponent.id}-background`}
+                value={
+                  (columnComponent.meta.background as string) ||
+                  BACKGROUND_TRANSPARENT
+                }
+                onChange={handleChangeBackground}
+              />,
+            ]}
             editMode={editMode}
           >
             {editMode && (
-              <Droppable
-                component={columnComponent}
-                parentComponent={columnComponent}
-                {...(columnItems.length === 0
-                  ? {
-                      dropToChild: true,
-                    }
-                  : {
-                      component: columnItems[0],
-                    })}
-                depth={depth}
-                index={0}
-                orientation="column"
-                onDrop={handleComponentDrop}
-                className={cx(
-                  'empty-droptarget',
-                  columnItems.length > 0 && 'droptarget-edge',
-                )}
-                editMode
-              >
-                {({ dropIndicatorProps }: DropIndicatorChildProps) =>
-                  dropIndicatorProps && <div {...dropIndicatorProps} />
+              <HoverMenu
+                innerRef={
+                  dragSourceRef as unknown as React.RefObject<HTMLDivElement>
                 }
-              </Droppable>
+                position="top"
+              >
+                <DragHandle position="top" />
+                <DeleteComponentButton
+                  iconSize="m"
+                  onDelete={handleDeleteComponent}
+                />
+                <IconButton
+                  onClick={() => handleChangeFocus(true)}
+                  icon={<Icons.SettingOutlined iconSize="m" />}
+                />
+                <ComponentHeaderControls
+                  items={[
+                    {
+                      key: 'apply-theme',
+                      label: t('Apply theme'),
+                      onClick: () => setThemeModalOpen(true),
+                    },
+                  ]}
+                  ariaLabel={t('Column options')}
+                />
+              </HoverMenu>
+            )}
+            {editMode && (
+              <ThemeSelectorModal
+                layoutId={columnComponent.id}
+                show={themeModalOpen}
+                onHide={() => setThemeModalOpen(false)}
+              />
             )}
-            {columnItems.length === 0 ? (
-              <div css={emptyColumnContentStyles}>{t('Empty column')}</div>
-            ) : (
-              columnItems.map((componentId: string, itemIndex: number) => (
-                <Fragment key={componentId}>
-                  <DashboardComponent
-                    id={componentId}
-                    parentId={columnComponent.id}
-                    depth={depth + 1}
-                    index={itemIndex}
-                    availableColumnCount={columnComponent.meta.width ?? 0}
-                    columnWidth={columnWidth}
-                    onResizeStart={
-                      onResizeStart as unknown as (
-                        event: MouseEvent | TouchEvent,
-                        direction: string,
-                        elementRef: HTMLElement,
-                      ) => void
-                    }
-                    onResize={
-                      onResize as unknown as (
-                        event: MouseEvent | TouchEvent,
-                        direction: string,
-                        elementRef: HTMLElement,
-                        delta: { width: number; height: number },
-                      ) => void
-                    }
-                    onResizeStop={
-                      onResizeStop as unknown as (
-                        event: MouseEvent | TouchEvent,
-                        direction: string,
-                        elementRef: HTMLElement,
-                        delta: { width: number; height: number },
-                        id: string,
-                      ) => void
-                    }
-                    isComponentVisible={isComponentVisible}
-                    onChangeTab={onChangeTab}
-                  />
-                  {editMode && (
-                    <Droppable
-                      component={columnItems}
-                      parentComponent={columnComponent}
-                      depth={depth}
-                      index={itemIndex + 1}
-                      orientation="column"
-                      onDrop={handleComponentDrop}
-                      className={cx(
-                        'empty-droptarget',
-                        itemIndex === columnItems.length - 1 &&
-                          'droptarget-edge',
-                      )}
-                      editMode
-                    >
-                      {({ dropIndicatorProps }: DropIndicatorChildProps) =>
-                        dropIndicatorProps && <div {...dropIndicatorProps} />
+            <ColumnStyles
+              className={cx('grid-column', backgroundStyle?.className)}
+              editMode={editMode}
+            >
+              {editMode && (
+                <Droppable
+                  component={columnComponent}
+                  parentComponent={columnComponent}
+                  {...(columnItems.length === 0
+                    ? {
+                        dropToChild: true,
                       }
-                    </Droppable>
+                    : {
+                        component: columnItems[0],
+                      })}
+                  depth={depth}
+                  index={0}
+                  orientation="column"
+                  onDrop={handleComponentDrop}
+                  className={cx(
+                    'empty-droptarget',
+                    columnItems.length > 0 && 'droptarget-edge',
                   )}
-                </Fragment>
-              ))
-            )}
-          </ColumnStyles>
-        </WithPopoverMenu>
+                  editMode
+                >
+                  {({ dropIndicatorProps }: DropIndicatorChildProps) =>
+                    dropIndicatorProps && <div {...dropIndicatorProps} />
+                  }
+                </Droppable>
+              )}
+              {columnItems.length === 0 ? (
+                <div css={emptyColumnContentStyles}>{t('Empty column')}</div>
+              ) : (
+                columnItems.map((componentId: string, itemIndex: number) => (
+                  <Fragment key={componentId}>
+                    <DashboardComponent
+                      id={componentId}
+                      parentId={columnComponent.id}
+                      depth={depth + 1}
+                      index={itemIndex}
+                      availableColumnCount={columnComponent.meta.width ?? 0}
+                      columnWidth={columnWidth}
+                      onResizeStart={
+                        onResizeStart as unknown as (
+                          event: MouseEvent | TouchEvent,
+                          direction: string,
+                          elementRef: HTMLElement,
+                        ) => void
+                      }
+                      onResize={
+                        onResize as unknown as (
+                          event: MouseEvent | TouchEvent,
+                          direction: string,
+                          elementRef: HTMLElement,
+                          delta: { width: number; height: number },
+                        ) => void
+                      }
+                      onResizeStop={
+                        onResizeStop as unknown as (
+                          event: MouseEvent | TouchEvent,
+                          direction: string,
+                          elementRef: HTMLElement,
+                          delta: { width: number; height: number },
+                          id: string,
+                        ) => void
+                      }
+                      isComponentVisible={isComponentVisible}
+                      onChangeTab={onChangeTab}
+                    />
+                    {editMode && (
+                      <Droppable
+                        component={columnItems}
+                        parentComponent={columnComponent}
+                        depth={depth}
+                        index={itemIndex + 1}
+                        orientation="column"
+                        onDrop={handleComponentDrop}
+                        className={cx(
+                          'empty-droptarget',
+                          itemIndex === columnItems.length - 1 &&
+                            'droptarget-edge',
+                        )}
+                        editMode
+                      >
+                        {({ dropIndicatorProps }: DropIndicatorChildProps) =>
+                          dropIndicatorProps && <div {...dropIndicatorProps} />
+                        }
+                      </Droppable>
+                    )}
+                  </Fragment>
+                ))
+              )}
+            </ColumnStyles>
+          </WithPopoverMenu>
+        </ComponentThemeProvider>
       </ResizableContainer>
     ),
     [
diff --git 
a/superset-frontend/src/dashboard/components/gridComponents/Markdown/Markdown.tsx
 
b/superset-frontend/src/dashboard/components/gridComponents/Markdown/Markdown.tsx
index 55f7426348d..6ab65e78798 100644
--- 
a/superset-frontend/src/dashboard/components/gridComponents/Markdown/Markdown.tsx
+++ 
b/superset-frontend/src/dashboard/components/gridComponents/Markdown/Markdown.tsx
@@ -34,6 +34,9 @@ import HoverMenu from 
'src/dashboard/components/menu/HoverMenu';
 import ResizableContainer from 
'src/dashboard/components/resizable/ResizableContainer';
 import MarkdownModeDropdown from 
'src/dashboard/components/menu/MarkdownModeDropdown';
 import WithPopoverMenu from 'src/dashboard/components/menu/WithPopoverMenu';
+import ComponentThemeProvider from 
'src/dashboard/components/ComponentThemeProvider';
+import ComponentHeaderControls from 
'src/dashboard/components/menu/ComponentHeaderControls';
+import ThemeSelectorModal from 'src/dashboard/components/ThemeSelectorModal';
 import type { LayoutItem } from 'src/dashboard/types';
 import type { DropResult } from 
'src/dashboard/components/dnd/dragDroppableConfig';
 import { ROW_TYPE, COLUMN_TYPE } from 'src/dashboard/util/componentTypes';
@@ -90,6 +93,7 @@ export interface MarkdownState {
   undoLength: number;
   redoLength: number;
   hasError?: boolean;
+  themeModalOpen: boolean;
 }
 
 // TODO: localize
@@ -152,6 +156,7 @@ class Markdown extends PureComponent<MarkdownProps, 
MarkdownState> {
       editorMode: 'preview',
       undoLength: props.undoLength,
       redoLength: props.redoLength,
+      themeModalOpen: false,
     };
     this.renderStartTime = Logger.getTimestamp();
 
@@ -396,62 +401,82 @@ class Markdown extends PureComponent<MarkdownProps, 
MarkdownState> {
         editMode={editMode}
       >
         {({ dragSourceRef }: DragChildProps) => (
-          <WithPopoverMenu
-            onChangeFocus={this.handleChangeFocus}
-            shouldFocus={this.shouldFocusMarkdown}
-            menuItems={[
-              <MarkdownModeDropdown
-                key={`${component.id}-mode`}
-                id={`${component.id}-mode`}
-                value={this.state.editorMode}
-                onChange={this.handleChangeEditorMode}
-              />,
-            ]}
-            editMode={editMode}
-          >
-            <MarkdownStyles
-              data-test="dashboard-markdown-editor"
-              className={cx(
-                'dashboard-markdown',
-                isEditing && 'dashboard-markdown--editing',
-              )}
-              id={component.id}
+          <ComponentThemeProvider layoutId={component.id}>
+            <WithPopoverMenu
+              onChangeFocus={this.handleChangeFocus}
+              shouldFocus={this.shouldFocusMarkdown}
+              menuItems={[
+                <MarkdownModeDropdown
+                  key={`${component.id}-mode`}
+                  id={`${component.id}-mode`}
+                  value={this.state.editorMode}
+                  onChange={this.handleChangeEditorMode}
+                />,
+                <ComponentHeaderControls
+                  key={`${component.id}-options`}
+                  items={[
+                    {
+                      key: 'apply-theme',
+                      label: t('Apply theme'),
+                      onClick: () => this.setState({ themeModalOpen: true }),
+                    },
+                  ]}
+                  ariaLabel={t('Markdown options')}
+                />,
+              ]}
+              editMode={editMode}
             >
-              <ResizableContainer
+              {editMode && (
+                <ThemeSelectorModal
+                  layoutId={component.id}
+                  show={this.state.themeModalOpen}
+                  onHide={() => this.setState({ themeModalOpen: false })}
+                />
+              )}
+              <MarkdownStyles
+                data-test="dashboard-markdown-editor"
+                className={cx(
+                  'dashboard-markdown',
+                  isEditing && 'dashboard-markdown--editing',
+                )}
                 id={component.id}
-                adjustableWidth={parentComponent.type === ROW_TYPE}
-                adjustableHeight
-                widthStep={columnWidth}
-                widthMultiple={widthMultiple}
-                heightStep={GRID_BASE_UNIT}
-                heightMultiple={component.meta.height ?? GRID_MIN_ROW_UNITS}
-                minWidthMultiple={GRID_MIN_COLUMN_COUNT}
-                minHeightMultiple={GRID_MIN_ROW_UNITS}
-                maxWidthMultiple={availableColumnCount + widthMultiple}
-                onResizeStart={this.handleResizeStart}
-                onResize={onResize}
-                onResizeStop={onResizeStop}
-                editMode={isFocused ? false : editMode}
               >
-                <div
-                  ref={dragSourceRef}
-                  className="dashboard-component 
dashboard-component-chart-holder"
-                  data-test="dashboard-component-chart-holder"
+                <ResizableContainer
+                  id={component.id}
+                  adjustableWidth={parentComponent.type === ROW_TYPE}
+                  adjustableHeight
+                  widthStep={columnWidth}
+                  widthMultiple={widthMultiple}
+                  heightStep={GRID_BASE_UNIT}
+                  heightMultiple={component.meta.height ?? GRID_MIN_ROW_UNITS}
+                  minWidthMultiple={GRID_MIN_COLUMN_COUNT}
+                  minHeightMultiple={GRID_MIN_ROW_UNITS}
+                  maxWidthMultiple={availableColumnCount + widthMultiple}
+                  onResizeStart={this.handleResizeStart}
+                  onResize={onResize}
+                  onResizeStop={onResizeStop}
+                  editMode={isFocused ? false : editMode}
                 >
-                  {editMode && (
-                    <HoverMenu position="top">
-                      <DeleteComponentButton
-                        onDelete={this.handleDeleteComponent}
-                      />
-                    </HoverMenu>
-                  )}
-                  {editMode && isEditing
-                    ? this.renderEditMode()
-                    : this.renderPreviewMode()}
-                </div>
-              </ResizableContainer>
-            </MarkdownStyles>
-          </WithPopoverMenu>
+                  <div
+                    ref={dragSourceRef}
+                    className="dashboard-component 
dashboard-component-chart-holder"
+                    data-test="dashboard-component-chart-holder"
+                  >
+                    {editMode && (
+                      <HoverMenu position="top">
+                        <DeleteComponentButton
+                          onDelete={this.handleDeleteComponent}
+                        />
+                      </HoverMenu>
+                    )}
+                    {editMode && isEditing
+                      ? this.renderEditMode()
+                      : this.renderPreviewMode()}
+                  </div>
+                </ResizableContainer>
+              </MarkdownStyles>
+            </WithPopoverMenu>
+          </ComponentThemeProvider>
         )}
       </Draggable>
     );
diff --git 
a/superset-frontend/src/dashboard/components/gridComponents/Row/Row.tsx 
b/superset-frontend/src/dashboard/components/gridComponents/Row/Row.tsx
index c5ec0c497f5..d4ff6f00ca8 100644
--- a/superset-frontend/src/dashboard/components/gridComponents/Row/Row.tsx
+++ b/superset-frontend/src/dashboard/components/gridComponents/Row/Row.tsx
@@ -43,6 +43,9 @@ import HoverMenu from 
'src/dashboard/components/menu/HoverMenu';
 import IconButton from 'src/dashboard/components/IconButton';
 import BackgroundStyleDropdown from 
'src/dashboard/components/menu/BackgroundStyleDropdown';
 import WithPopoverMenu from 'src/dashboard/components/menu/WithPopoverMenu';
+import ComponentThemeProvider from 
'src/dashboard/components/ComponentThemeProvider';
+import ComponentHeaderControls from 
'src/dashboard/components/menu/ComponentHeaderControls';
+import ThemeSelectorModal from 'src/dashboard/components/ThemeSelectorModal';
 import backgroundStyleOptions from 'src/dashboard/util/backgroundStyleOptions';
 import { BACKGROUND_TRANSPARENT } from 'src/dashboard/util/constants';
 import { isEmbedded } from 'src/dashboard/util/isEmbedded';
@@ -158,6 +161,7 @@ const Row = memo((props: RowProps) => {
   const [isInView, setIsInView] = useState(false);
   const [hoverMenuHovered, setHoverMenuHovered] = useState(false);
   const [containerHeight, setContainerHeight] = useState<number | null>(null);
+  const [themeModalOpen, setThemeModalOpen] = useState(false);
   const containerRef = useRef<HTMLDivElement | null>(null);
   const isComponentVisibleRef = useRef(isComponentVisible);
 
@@ -270,130 +274,151 @@ const Row = memo((props: RowProps) => {
   const remainColumnCount = availableColumnCount - occupiedColumnCount;
   const renderChild = useCallback(
     ({ dragSourceRef }: { dragSourceRef: RefObject<HTMLDivElement> }) => (
-      <WithPopoverMenu
-        isFocused={isFocused}
-        onChangeFocus={handleChangeFocus}
-        disableClick
-        menuItems={[
-          <BackgroundStyleDropdown
-            id={`${rowComponent.id}-background`}
-            value={backgroundStyle.value}
-            onChange={handleChangeBackground}
-          />,
-        ]}
-        editMode={editMode}
-      >
-        {editMode && (
-          <HoverMenu
-            onHover={handleMenuHover}
-            innerRef={dragSourceRef}
-            position="left"
-          >
-            <DragHandle position="left" />
-            <DeleteComponentButton onDelete={handleDeleteComponent} />
-            <IconButton
-              onClick={() => handleChangeFocus(true)}
-              icon={<Icons.SettingOutlined iconSize="l" />}
-            />
-          </HoverMenu>
-        )}
-        <GridRow
-          className={cx(
-            'grid-row',
-            rowItems.length === 0 && 'grid-row--empty',
-            hoverMenuHovered && 'grid-row--hovered',
-            backgroundStyle.className,
-          )}
-          data-test={`grid-row-${backgroundStyle.className}`}
-          ref={containerRef}
+      <ComponentThemeProvider layoutId={rowComponent.id}>
+        <WithPopoverMenu
+          isFocused={isFocused}
+          onChangeFocus={handleChangeFocus}
+          disableClick
+          menuItems={[
+            <BackgroundStyleDropdown
+              id={`${rowComponent.id}-background`}
+              value={backgroundStyle.value}
+              onChange={handleChangeBackground}
+            />,
+          ]}
           editMode={editMode}
         >
           {editMode && (
-            <Droppable
-              {...(rowItems.length === 0
-                ? {
-                    component: rowComponent,
-                    parentComponent: rowComponent,
-                    dropToChild: true,
-                  }
-                : {
-                    component: rowItems[0],
-                    parentComponent: rowComponent,
-                  })}
-              depth={depth}
-              index={0}
-              orientation="row"
-              onDrop={handleComponentDrop}
-              className={cx(
-                'empty-droptarget',
-                'empty-droptarget--vertical',
-                rowItems.length > 0 && 'droptarget-side',
-              )}
-              editMode
-              style={{
-                height: rowItems.length > 0 ? containerHeight : '100%',
-                ...(rowItems.length > 0 && { width: 16 }),
-              }}
+            <HoverMenu
+              onHover={handleMenuHover}
+              innerRef={dragSourceRef}
+              position="left"
             >
-              {({ dropIndicatorProps }: { dropIndicatorProps: JsonObject }) =>
-                dropIndicatorProps && <div {...dropIndicatorProps} />
-              }
-            </Droppable>
+              <DragHandle position="left" />
+              <DeleteComponentButton onDelete={handleDeleteComponent} />
+              <IconButton
+                onClick={() => handleChangeFocus(true)}
+                icon={<Icons.SettingOutlined iconSize="l" />}
+              />
+              <ComponentHeaderControls
+                items={[
+                  {
+                    key: 'apply-theme',
+                    label: t('Apply theme'),
+                    onClick: () => setThemeModalOpen(true),
+                  },
+                ]}
+                ariaLabel={t('Row options')}
+              />
+            </HoverMenu>
           )}
-          {rowItems.length === 0 && (
-            <div css={emptyRowContentStyles as any}>{t('Empty row')}</div>
+          {editMode && (
+            <ThemeSelectorModal
+              layoutId={rowComponent.id}
+              show={themeModalOpen}
+              onHide={() => setThemeModalOpen(false)}
+            />
           )}
-          {rowItems.length > 0 &&
-            rowItems.map((componentId, itemIndex) => (
-              <Fragment key={componentId}>
-                <DashboardComponent
-                  key={componentId}
-                  id={componentId}
-                  parentId={rowComponent.id as string}
-                  depth={depth + 1}
-                  index={itemIndex}
-                  availableColumnCount={remainColumnCount}
-                  columnWidth={columnWidth}
-                  onResizeStart={onResizeStart}
-                  onResize={onResize}
-                  onResizeStop={onResizeStop}
-                  isComponentVisible={isComponentVisible}
-                  onChangeTab={onChangeTab}
-                  isInView={isInView}
-                />
-                {editMode && (
-                  <Droppable
-                    component={rowItems}
-                    parentComponent={rowComponent}
-                    depth={depth}
-                    index={itemIndex + 1}
-                    orientation="row"
-                    onDrop={handleComponentDrop}
-                    className={cx(
-                      'empty-droptarget',
-                      'empty-droptarget--vertical',
-                      remainColumnCount === 0 &&
-                        itemIndex === rowItems.length - 1 &&
-                        'droptarget-side',
-                    )}
-                    editMode
-                    style={{
-                      height: containerHeight,
-                      ...(remainColumnCount === 0 &&
-                        itemIndex === rowItems.length - 1 && { width: 16 }),
-                    }}
-                  >
-                    {({
-                      dropIndicatorProps,
-                    }: {
-                      dropIndicatorProps: JsonObject;
-                    }) => dropIndicatorProps && <div {...dropIndicatorProps} 
/>}
-                  </Droppable>
+          <GridRow
+            className={cx(
+              'grid-row',
+              rowItems.length === 0 && 'grid-row--empty',
+              hoverMenuHovered && 'grid-row--hovered',
+              backgroundStyle.className,
+            )}
+            data-test={`grid-row-${backgroundStyle.className}`}
+            ref={containerRef}
+            editMode={editMode}
+          >
+            {editMode && (
+              <Droppable
+                {...(rowItems.length === 0
+                  ? {
+                      component: rowComponent,
+                      parentComponent: rowComponent,
+                      dropToChild: true,
+                    }
+                  : {
+                      component: rowItems[0],
+                      parentComponent: rowComponent,
+                    })}
+                depth={depth}
+                index={0}
+                orientation="row"
+                onDrop={handleComponentDrop}
+                className={cx(
+                  'empty-droptarget',
+                  'empty-droptarget--vertical',
+                  rowItems.length > 0 && 'droptarget-side',
                 )}
-              </Fragment>
-            ))}
-        </GridRow>
-      </WithPopoverMenu>
+                editMode
+                style={{
+                  height: rowItems.length > 0 ? containerHeight : '100%',
+                  ...(rowItems.length > 0 && { width: 16 }),
+                }}
+              >
+                {({ dropIndicatorProps }: { dropIndicatorProps: JsonObject }) 
=>
+                  dropIndicatorProps && <div {...dropIndicatorProps} />
+                }
+              </Droppable>
+            )}
+            {rowItems.length === 0 && (
+              <div css={emptyRowContentStyles as any}>{t('Empty row')}</div>
+            )}
+            {rowItems.length > 0 &&
+              rowItems.map((componentId, itemIndex) => (
+                <Fragment key={componentId}>
+                  <DashboardComponent
+                    key={componentId}
+                    id={componentId}
+                    parentId={rowComponent.id as string}
+                    depth={depth + 1}
+                    index={itemIndex}
+                    availableColumnCount={remainColumnCount}
+                    columnWidth={columnWidth}
+                    onResizeStart={onResizeStart}
+                    onResize={onResize}
+                    onResizeStop={onResizeStop}
+                    isComponentVisible={isComponentVisible}
+                    onChangeTab={onChangeTab}
+                    isInView={isInView}
+                  />
+                  {editMode && (
+                    <Droppable
+                      component={rowItems}
+                      parentComponent={rowComponent}
+                      depth={depth}
+                      index={itemIndex + 1}
+                      orientation="row"
+                      onDrop={handleComponentDrop}
+                      className={cx(
+                        'empty-droptarget',
+                        'empty-droptarget--vertical',
+                        remainColumnCount === 0 &&
+                          itemIndex === rowItems.length - 1 &&
+                          'droptarget-side',
+                      )}
+                      editMode
+                      style={{
+                        height: containerHeight,
+                        ...(remainColumnCount === 0 &&
+                          itemIndex === rowItems.length - 1 && { width: 16 }),
+                      }}
+                    >
+                      {({
+                        dropIndicatorProps,
+                      }: {
+                        dropIndicatorProps: JsonObject;
+                      }) =>
+                        dropIndicatorProps && <div {...dropIndicatorProps} />
+                      }
+                    </Droppable>
+                  )}
+                </Fragment>
+              ))}
+          </GridRow>
+        </WithPopoverMenu>
+      </ComponentThemeProvider>
     ),
     [
       backgroundStyle.className,
diff --git 
a/superset-frontend/src/dashboard/components/gridComponents/TabsRenderer/TabsRenderer.tsx
 
b/superset-frontend/src/dashboard/components/gridComponents/TabsRenderer/TabsRenderer.tsx
index 6d030b06a56..ff1a38ec017 100644
--- 
a/superset-frontend/src/dashboard/components/gridComponents/TabsRenderer/TabsRenderer.tsx
+++ 
b/superset-frontend/src/dashboard/components/gridComponents/TabsRenderer/TabsRenderer.tsx
@@ -45,6 +45,10 @@ import {
 import HoverMenu from '../../menu/HoverMenu';
 import DragHandle from '../../dnd/DragHandle';
 import DeleteComponentButton from '../../DeleteComponentButton';
+import ComponentThemeProvider from '../../ComponentThemeProvider';
+import ComponentHeaderControls from '../../menu/ComponentHeaderControls';
+import ThemeSelectorModal from '../../ThemeSelectorModal';
+import { t } from '@apache-superset/core/translation';
 
 const StyledTabsContainer = styled.div<{ isDragging?: boolean }>`
   width: 100%;
@@ -173,6 +177,7 @@ const TabsRenderer = memo<TabsRendererProps>(
     onTabTitleEditingChange,
   }) => {
     const [activeId, setActiveId] = useState<string | null>(null);
+    const [themeModalOpen, setThemeModalOpen] = useState(false);
 
     // Use ref to always have access to the current tabIds in callbacks
     const tabIdsRef = useRef(tabIds);
@@ -209,66 +214,86 @@ const TabsRenderer = memo<TabsRendererProps>(
     const isDragging = activeId !== null;
 
     return (
-      <StyledTabsContainer
-        className="dashboard-component dashboard-component-tabs"
-        data-test="dashboard-component-tabs"
-        isDragging={isDragging}
-      >
-        {editMode && renderHoverMenu && tabsDragSourceRef && (
-          <HoverMenu innerRef={tabsDragSourceRef} position="left">
-            <DragHandle position="left" />
-            <DeleteComponentButton onDelete={handleDeleteComponent} />
-          </HoverMenu>
-        )}
+      <ComponentThemeProvider layoutId={tabsComponent.id}>
+        <StyledTabsContainer
+          className="dashboard-component dashboard-component-tabs"
+          data-test="dashboard-component-tabs"
+          isDragging={isDragging}
+        >
+          {editMode && renderHoverMenu && tabsDragSourceRef && (
+            <HoverMenu innerRef={tabsDragSourceRef} position="left">
+              <DragHandle position="left" />
+              <DeleteComponentButton onDelete={handleDeleteComponent} />
+              <ComponentHeaderControls
+                items={[
+                  {
+                    key: 'apply-theme',
+                    label: t('Apply theme'),
+                    onClick: () => setThemeModalOpen(true),
+                  },
+                ]}
+                ariaLabel={t('Tabs options')}
+              />
+            </HoverMenu>
+          )}
+          {editMode && (
+            <ThemeSelectorModal
+              layoutId={tabsComponent.id}
+              show={themeModalOpen}
+              onHide={() => setThemeModalOpen(false)}
+            />
+          )}
 
-        <LineEditableTabs
-          id={tabsComponent.id}
-          activeKey={activeKey}
-          onChange={key => {
-            if (typeof key === 'string') {
-              const tabIndex = tabIds.indexOf(key);
-              if (tabIndex !== -1) handleClickTab(tabIndex);
-            }
-          }}
-          onEdit={handleEdit}
-          data-test="nav-list"
-          type={editMode ? 'editable-card' : 'card'}
-          items={tabItems}
-          tabBarStyle={{ paddingLeft: tabBarPaddingLeft }}
-          fullHeight
-          {...(editMode && {
-            renderTabBar: (tabBarProps, DefaultTabBar) => (
-              <DndContext
-                key={tabIds.join('-')}
-                sensors={[sensor]}
-                onDragStart={onDragStart}
-                onDragEnd={onDragEnd}
-                onDragCancel={onDragCancel}
-                collisionDetection={closestCenter}
-              >
-                <SortableContext
-                  items={tabIds}
-                  strategy={horizontalListSortingStrategy}
+          <LineEditableTabs
+            id={tabsComponent.id}
+            activeKey={activeKey}
+            onChange={key => {
+              if (typeof key === 'string') {
+                const tabIndex = tabIds.indexOf(key);
+                if (tabIndex !== -1) handleClickTab(tabIndex);
+              }
+            }}
+            onEdit={handleEdit}
+            data-test="nav-list"
+            type={editMode ? 'editable-card' : 'card'}
+            items={tabItems}
+            tabBarStyle={{ paddingLeft: tabBarPaddingLeft }}
+            fullHeight
+            {...(editMode && {
+              renderTabBar: (tabBarProps, DefaultTabBar) => (
+                <DndContext
+                  key={tabIds.join('-')}
+                  sensors={[sensor]}
+                  onDragStart={onDragStart}
+                  onDragEnd={onDragEnd}
+                  onDragCancel={onDragCancel}
+                  collisionDetection={closestCenter}
                 >
-                  <DefaultTabBar {...tabBarProps}>
-                    {(node: React.ReactElement) => (
-                      <DraggableTabNode
-                        {...(node as React.ReactElement<DraggableTabNodeProps>)
-                          .props}
-                        key={node.key}
-                        data-node-key={node.key as string}
-                        disabled={isEditingTabTitle}
-                      >
-                        {node}
-                      </DraggableTabNode>
-                    )}
-                  </DefaultTabBar>
-                </SortableContext>
-              </DndContext>
-            ),
-          })}
-        />
-      </StyledTabsContainer>
+                  <SortableContext
+                    items={tabIds}
+                    strategy={horizontalListSortingStrategy}
+                  >
+                    <DefaultTabBar {...tabBarProps}>
+                      {(node: React.ReactElement) => (
+                        <DraggableTabNode
+                          {...(
+                            node as React.ReactElement<DraggableTabNodeProps>
+                          ).props}
+                          key={node.key}
+                          data-node-key={node.key as string}
+                          disabled={isEditingTabTitle}
+                        >
+                          {node}
+                        </DraggableTabNode>
+                      )}
+                    </DefaultTabBar>
+                  </SortableContext>
+                </DndContext>
+              ),
+            })}
+          />
+        </StyledTabsContainer>
+      </ComponentThemeProvider>
     );
   },
 );


Reply via email to