From 6b36cbb90ae8117647e74cea49356054ec835a6e Mon Sep 17 00:00:00 2001 From: Mike Clark Date: Wed, 1 Jul 2026 11:10:30 -0600 Subject: [PATCH] fix: improve collapsed sidebar exporter UX - Render the sidebar trigger as an icon-only button when collapsed - Insert outside the profile trigger wrapper to avoid native tooltip overlap - Add an accessible label while hiding collapsed trigger text --- src/main.tsx | 9 ++++++- src/style.css | 31 ++++++++++++++++++++++ src/ui/Menu.tsx | 63 +++++++++++++++++++++++++++++++++++++++++++-- src/ui/MenuItem.tsx | 8 ++++-- 4 files changed, 106 insertions(+), 5 deletions(-) diff --git a/src/main.tsx b/src/main.tsx index fdb3239..91597e6 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -29,7 +29,7 @@ function main() { const container = getMenuContainer() injectionMap.set(target, container) - target.before(container) + getNavMenuInsertionTarget(target).before(container) } const selector = '[data-testid="accounts-profile-button"]' @@ -99,3 +99,10 @@ function getMenuContainer() { render(, container) return container } + +function getNavMenuInsertionTarget(target: Element) { + const wrapper = target.parentElement + if (!wrapper || wrapper.children.length !== 1) return target + + return wrapper +} diff --git a/src/style.css b/src/style.css index cafad35..4a4f45f 100644 --- a/src/style.css +++ b/src/style.css @@ -54,6 +54,37 @@ html { filter: brightness(0.5); } +.ce-nav-trigger { + min-width: 0; + border: 0; + color: var(--ce-text-primary); +} + +.ce-nav-trigger .ce-menu-item-text { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.ce-nav-trigger-collapsed { + width: 32px; + height: 32px; + margin: 0 auto 0.5rem; + padding: 0; + justify-content: center; + gap: 0; + border-radius: 8px; + color: var(--text-secondary, var(--ce-text-primary)); +} + +.ce-nav-trigger-collapsed:hover { + background-color: var(--sidebar-surface-secondary, rgba(255, 255, 255, 0.1)); +} + +.ce-nav-trigger-collapsed .ce-menu-item-text { + display: none; +} + .ce-card { border-radius: 1rem; box-shadow: 0 8px 32px rgba(0, 0, 0, 0.12), 0 2px 8px rgba(0, 0, 0, 0.08); diff --git a/src/ui/Menu.tsx b/src/ui/Menu.tsx index 117e86a..9c259d7 100644 --- a/src/ui/Menu.tsx +++ b/src/ui/Menu.tsx @@ -18,6 +18,61 @@ import { SettingDialog } from './SettingDialog' import '../style.css' import './Dialog.css' +function useCollapsedSidebar(container: HTMLDivElement, isMobile: boolean) { + const [isCollapsed, setIsCollapsed] = useState(false) + + useEffect(() => { + if (isMobile) { + setIsCollapsed(false) + return + } + + let frame = 0 + const observed = new Set() + const observer = typeof ResizeObserver === 'undefined' ? null : new ResizeObserver(update) + + function sidebarElement() { + return container.closest('nav, aside, [aria-label="Sidebar"], [data-testid="sidebar"]') + ?? container.parentElement + } + + function observe(element: Element | null | undefined) { + if (!observer || !element || observed.has(element)) return + observer.observe(element) + observed.add(element) + } + + function update() { + cancelAnimationFrame(frame) + frame = requestAnimationFrame(() => { + const parentWidth = container.parentElement?.getBoundingClientRect().width ?? 0 + const sidebarWidth = sidebarElement()?.getBoundingClientRect().width ?? parentWidth + const nextCollapsed = (parentWidth > 0 && parentWidth < 96) + || (sidebarWidth > 0 && sidebarWidth < 96) + + setIsCollapsed(nextCollapsed) + container.toggleAttribute('data-ce-sidebar-collapsed', nextCollapsed) + observe(container.parentElement) + observe(sidebarElement()) + }) + } + + observe(container.parentElement) + observe(sidebarElement()) + update() + + window.addEventListener('resize', update) + return () => { + cancelAnimationFrame(frame) + window.removeEventListener('resize', update) + observer?.disconnect() + container.removeAttribute('data-ce-sidebar-collapsed') + } + }, [container, isMobile]) + + return isCollapsed +} + function MenuInner({ container }: { container: HTMLDivElement }) { const { t } = useTranslation() @@ -59,6 +114,7 @@ function MenuInner({ container }: { container: HTMLDivElement }) { const width = useWindowResize(() => window.innerWidth) const isMobile = width < 768 + const isCollapsedSidebar = useCollapsedSidebar(container, isMobile) const Portal = isMobile ? 'div' : HoverCard.Portal return ( @@ -78,8 +134,11 @@ function MenuInner({ container }: { container: HTMLDivElement }) { > { setOpen(true) @@ -210,7 +269,7 @@ function MenuInner({ container }: { container: HTMLDivElement }) { - + {!isCollapsedSidebar && } ) } diff --git a/src/ui/MenuItem.tsx b/src/ui/MenuItem.tsx index 0a17e0d..70e0c94 100644 --- a/src/ui/MenuItem.tsx +++ b/src/ui/MenuItem.tsx @@ -10,11 +10,12 @@ export interface MenuItemProps { successText?: string disabled?: boolean title?: string + ariaLabel?: string className?: string onClick?: (() => boolean) | (() => Promise) } -export const MenuItem: FC = ({ text, successText, disabled = false, title, icon: Icon, onClick, className }) => { +export const MenuItem: FC = ({ text, successText, disabled = false, title, ariaLabel, icon: Icon, onClick, className }) => { const [loading, setLoading] = useState(false) const [succeed, setSucceed] = useState(false) @@ -52,6 +53,7 @@ export const MenuItem: FC = ({ text, successText, disabled = fals onClick={handleClick} onTouchStart={handleClick} disabled={disabled} + aria-label={ariaLabel} title={title} > {loading @@ -63,7 +65,9 @@ export const MenuItem: FC = ({ text, successText, disabled = fals : ( <> {Icon && } - {(succeed && successText) ? successText : text} + + {(succeed && successText) ? successText : text} + )}