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

rusackas pushed a commit to branch mobile-dashboard-support
in repository https://gitbox.apache.org/repos/asf/superset.git

commit a8a6d5f37aaf2e7447a8fc78ad913711c254093b
Author: Evan Rusackas <[email protected]>
AuthorDate: Fri Jan 9 17:15:21 2026 -0800

    feat(mobile): Add filter drawer and chart consumption mode for mobile 
dashboards
    
    - Add left-side filter drawer with vertical filter layout on mobile
    - Hide Actions header and show Apply/Clear buttons side by side
    - Add filter button to dashboard header (only when filters exist)
    - Support leftPanelItems prop in PageHeaderWithActions
    - Hide chart kebab menu and disable title links on mobile
    - Show full chart titles without truncation on mobile
    - Center dashboard title on mobile with filter icon on left
    
    🤖 Generated with [Claude Code](https://claude.com/claude-code)
    
    Co-Authored-By: Claude Opus 4.5 <[email protected]>
---
 .../src/components/PageHeaderWithActions/index.tsx |  12 +++
 .../DashboardBuilder/DashboardBuilder.tsx          | 102 ++++++++++++++++-----
 .../DashboardBuilder/DashboardWrapper.tsx          |  23 +++++
 .../dashboard/components/DashboardBuilder/state.ts |   1 +
 .../src/dashboard/components/Header/index.jsx      |  26 +++++-
 5 files changed, 140 insertions(+), 24 deletions(-)

diff --git 
a/superset-frontend/packages/superset-ui-core/src/components/PageHeaderWithActions/index.tsx
 
b/superset-frontend/packages/superset-ui-core/src/components/PageHeaderWithActions/index.tsx
index cd523ea6ea..4c48594c63 100644
--- 
a/superset-frontend/packages/superset-ui-core/src/components/PageHeaderWithActions/index.tsx
+++ 
b/superset-frontend/packages/superset-ui-core/src/components/PageHeaderWithActions/index.tsx
@@ -81,6 +81,15 @@ const headerStyles = (theme: SupersetTheme) => css`
     display: flex;
     align-items: center;
   }
+
+  /* Mobile: center the title between left and right panels */
+  @media (max-width: 767px) {
+    .title-panel {
+      flex: 1;
+      justify-content: center;
+      margin-right: 0;
+    }
+  }
 `;
 
 const buttonsStyles = (theme: SupersetTheme) => css`
@@ -108,6 +117,7 @@ export type PageHeaderWithActionsProps = {
   showFaveStar: boolean;
   showMenuDropdown?: boolean;
   faveStarProps: FaveStarProps;
+  leftPanelItems?: ReactNode;
   titlePanelAdditionalItems: ReactNode;
   rightPanelAdditionalItems: ReactNode;
   additionalActionsMenu: ReactElement;
@@ -124,6 +134,7 @@ export const PageHeaderWithActions = ({
   certificatiedBadgeProps,
   showFaveStar,
   faveStarProps,
+  leftPanelItems,
   titlePanelAdditionalItems,
   rightPanelAdditionalItems,
   additionalActionsMenu,
@@ -134,6 +145,7 @@ export const PageHeaderWithActions = ({
   const theme = useTheme();
   return (
     <div css={headerStyles} className="header-with-actions">
+      {leftPanelItems}
       <div className="title-panel">
         <DynamicEditableTitle {...editableTitleProps} />
         {showTitlePanelItems && (
diff --git 
a/superset-frontend/src/dashboard/components/DashboardBuilder/DashboardBuilder.tsx
 
b/superset-frontend/src/dashboard/components/DashboardBuilder/DashboardBuilder.tsx
index 82721d386d..8a99960a6a 100644
--- 
a/superset-frontend/src/dashboard/components/DashboardBuilder/DashboardBuilder.tsx
+++ 
b/superset-frontend/src/dashboard/components/DashboardBuilder/DashboardBuilder.tsx
@@ -461,6 +461,7 @@ const DashboardBuilder = () => {
     dashboardFiltersOpen,
     toggleDashboardFiltersOpen,
     nativeFiltersEnabled,
+    hasFilters,
   } = useNativeFilters();
 
   const [containerRef, isSticky] = useElementOnScreen<HTMLDivElement>(
@@ -520,24 +521,14 @@ const DashboardBuilder = () => {
   const renderDraggableContent = useCallback(
     ({ dropIndicatorProps }: { dropIndicatorProps: JsonObject }) => (
       <div>
-        {!hideDashboardHeader && <DashboardHeader />}
-        {/* Mobile filter button */}
-        {!isNotMobile && !editMode && nativeFiltersEnabled && (
-          <div
-            css={css`
-              padding: ${theme.sizeUnit * 2}px ${theme.sizeUnit * 4}px;
-              background: ${theme.colorBgBase};
-              border-bottom: 1px solid ${theme.colorBorderSecondary};
-            `}
-          >
-            <Button
-              buttonStyle="secondary"
-              onClick={() => setMobileFiltersOpen(true)}
-            >
-              <Icons.FilterOutlined iconSize="m" />
-              {t('Filters')}
-            </Button>
-          </div>
+        {!hideDashboardHeader && (
+          <DashboardHeader
+            onOpenMobileFilters={
+              !isNotMobile && nativeFiltersEnabled && hasFilters
+                ? () => setMobileFiltersOpen(true)
+                : undefined
+            }
+          />
         )}
         {showFilterBar &&
           filterBarOrientation === FilterBarOrientation.Horizontal && (
@@ -576,6 +567,7 @@ const DashboardBuilder = () => {
     ),
     [
       nativeFiltersEnabled,
+      hasFilters,
       filterBarOrientation,
       editMode,
       handleChangeTab,
@@ -752,13 +744,81 @@ const DashboardBuilder = () => {
       {!isNotMobile && nativeFiltersEnabled && (
         <Drawer
           title={t('Filters')}
-          placement="bottom"
+          placement="left"
           onClose={() => setMobileFiltersOpen(false)}
           open={mobileFiltersOpen}
-          height="70vh"
+          width="85vw"
+          styles={{
+            body: {
+              padding: 0,
+              display: 'flex',
+              flexDirection: 'column',
+            },
+          }}
+          css={css`
+            /* Mobile filter drawer overrides */
+
+            /* Hide the Header component (contains Actions title, settings, 
collapse button) */
+            /* Target the parent div that contains the collapse button using 
:has() */
+            div:has([data-test='filter-bar-collapse-button']) {
+              display: none !important;
+            }
+
+            /* Hide the collapsed bar */
+            [data-test='filter-bar-collapsable'] {
+              display: none !important;
+            }
+
+            /* Action buttons: side by side, not fixed position */
+            [data-test='filterbar-action-buttons'] {
+              position: relative !important;
+              flex-direction: row !important;
+              width: 100% !important;
+              padding: ${theme.sizeUnit * 4}px !important;
+              background: ${theme.colorBgContainer} !important;
+              border-top: 1px solid ${theme.colorBorderSecondary} !important;
+              gap: ${theme.sizeUnit * 2}px !important;
+              bottom: auto !important;
+              left: auto !important;
+
+              .filter-apply-button {
+                margin-bottom: 0 !important;
+                flex: 1;
+              }
+              .filter-clear-all-button {
+                flex: 1;
+              }
+            }
+
+            /* Remove border-right and make full width */
+            [data-test='filter-bar'] {
+              position: relative;
+              width: 100% !important;
+              height: 100%;
+              border-right: none;
+
+              & > .open {
+                position: relative;
+                width: 100% !important;
+                height: 100%;
+                min-height: 100%;
+                border-right: none !important;
+                border-bottom: none !important;
+                display: flex;
+                flex-direction: column;
+              }
+            }
+          `}
         >
           <FilterBar
-            orientation={FilterBarOrientation.Horizontal}
+            orientation={FilterBarOrientation.Vertical}
+            verticalConfig={{
+              filtersOpen: true,
+              toggleFiltersBar: () => {},
+              width: 300,
+              height: '100%',
+              offset: 0,
+            }}
             hidden={false}
           />
         </Drawer>
diff --git 
a/superset-frontend/src/dashboard/components/DashboardBuilder/DashboardWrapper.tsx
 
b/superset-frontend/src/dashboard/components/DashboardBuilder/DashboardWrapper.tsx
index ec423b7bb0..bb8f2b08b7 100644
--- 
a/superset-frontend/src/dashboard/components/DashboardBuilder/DashboardWrapper.tsx
+++ 
b/superset-frontend/src/dashboard/components/DashboardBuilder/DashboardWrapper.tsx
@@ -110,6 +110,29 @@ const StyledDiv = styled.div`
     i.warning {
       color: ${theme.colorWarning};
     }
+
+    /* Mobile: consumption-only mode */
+    @media (max-width: 767px) {
+      /* Hide chart kebab menu (SliceHeaderControls) */
+      [data-test='slice-header'] .header-controls [id$='-controls'] {
+        display: none;
+      }
+
+      /* Disable chart title links - make them plain text */
+      [data-test='slice-header'] .header-title a {
+        pointer-events: none;
+        text-decoration: none !important;
+      }
+
+      /* Show full chart title without truncation - no tooltip needed */
+      [data-test='slice-header'] .header-title {
+        -webkit-line-clamp: unset;
+        display: block;
+        white-space: normal;
+        overflow: visible;
+        text-overflow: unset;
+      }
+    }
   `}
 `;
 
diff --git 
a/superset-frontend/src/dashboard/components/DashboardBuilder/state.ts 
b/superset-frontend/src/dashboard/components/DashboardBuilder/state.ts
index c7b7a56fdf..028283d22d 100644
--- a/superset-frontend/src/dashboard/components/DashboardBuilder/state.ts
+++ b/superset-frontend/src/dashboard/components/DashboardBuilder/state.ts
@@ -102,5 +102,6 @@ export const useNativeFilters = () => {
     dashboardFiltersOpen,
     toggleDashboardFiltersOpen,
     nativeFiltersEnabled,
+    hasFilters: filterValues.length > 0,
   };
 };
diff --git a/superset-frontend/src/dashboard/components/Header/index.jsx 
b/superset-frontend/src/dashboard/components/Header/index.jsx
index 9ee450bfe6..86242a1387 100644
--- a/superset-frontend/src/dashboard/components/Header/index.jsx
+++ b/superset-frontend/src/dashboard/components/Header/index.jsx
@@ -25,7 +25,7 @@ import {
   t,
   getExtensionsRegistry,
 } from '@superset-ui/core';
-import { styled, css } from '@apache-superset/core/ui';
+import { styled, css, useTheme } from '@apache-superset/core/ui';
 import { Global } from '@emotion/react';
 import { shallowEqual, useDispatch, useSelector } from 'react-redux';
 import { bindActionCreators } from 'redux';
@@ -60,7 +60,10 @@ import setPeriodicRunner, {
 } from 'src/dashboard/util/setPeriodicRunner';
 import ReportModal from 'src/features/reports/ReportModal';
 import { deleteActiveReport } from 'src/features/reports/ReportModal/actions';
-import { PageHeaderWithActions } from 
'@superset-ui/core/components/PageHeaderWithActions';
+import {
+  PageHeaderWithActions,
+  menuTriggerStyles,
+} from '@superset-ui/core/components/PageHeaderWithActions';
 import { useUnsavedChangesPrompt } from 'src/hooks/useUnsavedChangesPrompt';
 import DashboardEmbedModal from '../EmbeddedModal';
 import OverwriteConfirm from '../OverwriteConfirm';
@@ -166,8 +169,9 @@ const discardChanges = () => {
 
 const { useBreakpoint } = Grid;
 
-const Header = () => {
+const Header = ({ onOpenMobileFilters }) => {
   const dispatch = useDispatch();
+  const theme = useTheme();
   const screens = useBreakpoint();
   const isMobile = !screens.md;
   const [didNotifyMaxUndoHistoryToast, setDidNotifyMaxUndoHistoryToast] =
@@ -810,6 +814,22 @@ const Header = () => {
         editableTitleProps={editableTitleProps}
         certificatiedBadgeProps={certifiedBadgeProps}
         faveStarProps={faveStarProps}
+        leftPanelItems={
+          onOpenMobileFilters && (
+            <Button
+              css={menuTriggerStyles}
+              buttonStyle="tertiary"
+              aria-label={t('Open filters')}
+              onClick={onOpenMobileFilters}
+              data-test="mobile-filters-trigger"
+            >
+              <Icons.FilterOutlined
+                iconColor={theme.colorPrimary}
+                iconSize="l"
+              />
+            </Button>
+          )
+        }
         titlePanelAdditionalItems={titlePanelAdditionalItems}
         rightPanelAdditionalItems={rightPanelAdditionalItems}
         menuDropdownProps={{

Reply via email to