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 610ca3dc59fe9c4515c83fffb23504b9126ad855 Author: Evan Rusackas <[email protected]> AuthorDate: Thu Jan 8 19:56:55 2026 -0800 feat(mobile): add drawer menus for nav and filters on mobile - Add hamburger menu in global nav that opens a Drawer with menu items - Add "Filters" button on mobile that opens a bottom Drawer with FilterBar - Replace horizontal menus with mobile-friendly drawer pattern 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <[email protected]> --- .../DashboardBuilder/DashboardBuilder.tsx | 44 +++++++- superset-frontend/src/features/home/RightMenu.tsx | 116 ++++++++++++++------- 2 files changed, 121 insertions(+), 39 deletions(-) diff --git a/superset-frontend/src/dashboard/components/DashboardBuilder/DashboardBuilder.tsx b/superset-frontend/src/dashboard/components/DashboardBuilder/DashboardBuilder.tsx index 0afe5222e5..6ad97aeead 100644 --- a/superset-frontend/src/dashboard/components/DashboardBuilder/DashboardBuilder.tsx +++ b/superset-frontend/src/dashboard/components/DashboardBuilder/DashboardBuilder.tsx @@ -22,7 +22,13 @@ import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { addAlpha, JsonObject, t, useElementOnScreen } from '@superset-ui/core'; import { css, styled, useTheme } from '@apache-superset/core/ui'; import { useDispatch, useSelector } from 'react-redux'; -import { EmptyState, Grid, Loading } from '@superset-ui/core/components'; +import { + Button, + Drawer, + EmptyState, + Grid, + Loading, +} from '@superset-ui/core/components'; import { ErrorBoundary, BasicErrorAlert } from 'src/components'; import BuilderComponentPane from 'src/dashboard/components/BuilderComponentPane'; import DashboardHeader from 'src/dashboard/components/Header'; @@ -364,6 +370,7 @@ const DashboardBuilder = () => { const uiConfig = useUiConfig(); const theme = useTheme(); const { md: isNotMobile } = Grid.useBreakpoint(); + const [mobileFiltersOpen, setMobileFiltersOpen] = useState(false); const dashboardId = useSelector<RootState, string>( ({ dashboardInfo }) => `${dashboardInfo.id}`, @@ -512,6 +519,24 @@ const DashboardBuilder = () => { ({ 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> + )} {showFilterBar && filterBarOrientation === FilterBarOrientation.Horizontal && ( <FilterBar @@ -557,6 +582,8 @@ const DashboardBuilder = () => { isReport, topLevelTabs, uiConfig.hideNav, + isNotMobile, + theme, ], ); @@ -719,6 +746,21 @@ const DashboardBuilder = () => { `} /> )} + {/* Mobile filters drawer */} + {!isNotMobile && nativeFiltersEnabled && ( + <Drawer + title={t('Filters')} + placement="bottom" + onClose={() => setMobileFiltersOpen(false)} + open={mobileFiltersOpen} + height="70vh" + > + <FilterBar + orientation={FilterBarOrientation.Horizontal} + hidden={false} + /> + </Drawer> + )} </DashboardWrapper> ); }; diff --git a/superset-frontend/src/features/home/RightMenu.tsx b/superset-frontend/src/features/home/RightMenu.tsx index b0dc717d67..756267e2de 100644 --- a/superset-frontend/src/features/home/RightMenu.tsx +++ b/superset-frontend/src/features/home/RightMenu.tsx @@ -31,6 +31,9 @@ import { Icons, Typography, TelemetryPixel, + Drawer, + Grid, + Button, } from '@superset-ui/core/components'; import type { ItemType, MenuItem } from '@superset-ui/core/components/Menu'; import { ensureAppRoot, makeUrl } from 'src/utils/pathUtils'; @@ -90,6 +93,8 @@ const StyledMenuItem = styled.div<{ disabled?: boolean }>` `} `; +const { useBreakpoint } = Grid; + const RightMenu = ({ align, settings, @@ -107,6 +112,8 @@ const RightMenu = ({ }) => void; }) => { const theme = useTheme(); + const screens = useBreakpoint(); + const [mobileMenuOpen, setMobileMenuOpen] = useState(false); const user = useSelector<any, UserWithPermissionsAndRoles>( state => state.user, ); @@ -662,48 +669,81 @@ const RightMenu = ({ </Tag> ); })()} - <Menu - css={css` - display: flex; - flex-direction: row; - align-items: center; - height: 100%; - border-bottom: none !important; - - /* Remove the underline from menu items */ - .ant-menu-item:after, - .ant-menu-submenu:after { - content: none !important; - } - - .submenu-with-caret { + {/* Mobile: hamburger menu with drawer */} + {!screens.md && ( + <> + <Button + buttonStyle="link" + onClick={() => setMobileMenuOpen(true)} + aria-label={t('Menu')} + > + <Icons.MenuOutlined iconSize="l" /> + </Button> + <Drawer + title={t('Menu')} + placement="right" + onClose={() => setMobileMenuOpen(false)} + open={mobileMenuOpen} + width={280} + > + <Menu + mode="inline" + selectable={false} + onClick={info => { + handleMenuSelection(info); + setMobileMenuOpen(false); + }} + onOpenChange={onMenuOpen} + items={menuItems} + /> + </Drawer> + </> + )} + {/* Desktop: horizontal menu */} + {screens.md && ( + <Menu + css={css` + display: flex; + flex-direction: row; + align-items: center; height: 100%; - padding: 0; - .ant-menu-submenu-title { - align-items: center; - display: flex; - gap: ${theme.sizeUnit * 2}px; - flex-direction: row-reverse; - height: 100%; - } - &.ant-menu-submenu::after { - inset-inline: ${theme.sizeUnit}px; + border-bottom: none !important; + + /* Remove the underline from menu items */ + .ant-menu-item:after, + .ant-menu-submenu:after { + content: none !important; } - &.ant-menu-submenu:hover, - &.ant-menu-submenu-active { - .ant-menu-title-content { - color: ${theme.colorPrimary}; + + .submenu-with-caret { + height: 100%; + padding: 0; + .ant-menu-submenu-title { + align-items: center; + display: flex; + gap: ${theme.sizeUnit * 2}px; + flex-direction: row-reverse; + height: 100%; + } + &.ant-menu-submenu::after { + inset-inline: ${theme.sizeUnit}px; + } + &.ant-menu-submenu:hover, + &.ant-menu-submenu-active { + .ant-menu-title-content { + color: ${theme.colorPrimary}; + } } } - } - `} - selectable={false} - mode="horizontal" - onClick={handleMenuSelection} - onOpenChange={onMenuOpen} - disabledOverflow - items={menuItems} - /> + `} + selectable={false} + mode="horizontal" + onClick={handleMenuSelection} + onOpenChange={onMenuOpen} + disabledOverflow + items={menuItems} + /> + )} {navbarRight.documentation_url && ( <> <StyledAnchor
