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
This commit is contained in:
Mike Clark
2026-07-01 11:10:30 -06:00
committed by Pionxzh
parent 5b56f354c7
commit 6b36cbb90a
4 changed files with 106 additions and 5 deletions

View File

@@ -29,7 +29,7 @@ function main() {
const container = getMenuContainer() const container = getMenuContainer()
injectionMap.set(target, container) injectionMap.set(target, container)
target.before(container) getNavMenuInsertionTarget(target).before(container)
} }
const selector = '[data-testid="accounts-profile-button"]' const selector = '[data-testid="accounts-profile-button"]'
@@ -99,3 +99,10 @@ function getMenuContainer() {
render(<Menu container={container} />, container) render(<Menu container={container} />, container)
return container return container
} }
function getNavMenuInsertionTarget(target: Element) {
const wrapper = target.parentElement
if (!wrapper || wrapper.children.length !== 1) return target
return wrapper
}

View File

@@ -54,6 +54,37 @@ html {
filter: brightness(0.5); 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 { .ce-card {
border-radius: 1rem; border-radius: 1rem;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.12), 0 2px 8px rgba(0, 0, 0, 0.08); box-shadow: 0 8px 32px rgba(0, 0, 0, 0.12), 0 2px 8px rgba(0, 0, 0, 0.08);

View File

@@ -18,6 +18,61 @@ import { SettingDialog } from './SettingDialog'
import '../style.css' import '../style.css'
import './Dialog.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<Element>()
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 }) { function MenuInner({ container }: { container: HTMLDivElement }) {
const { t } = useTranslation() const { t } = useTranslation()
@@ -59,6 +114,7 @@ function MenuInner({ container }: { container: HTMLDivElement }) {
const width = useWindowResize(() => window.innerWidth) const width = useWindowResize(() => window.innerWidth)
const isMobile = width < 768 const isMobile = width < 768
const isCollapsedSidebar = useCollapsedSidebar(container, isMobile)
const Portal = isMobile ? 'div' : HoverCard.Portal const Portal = isMobile ? 'div' : HoverCard.Portal
return ( return (
@@ -78,8 +134,11 @@ function MenuInner({ container }: { container: HTMLDivElement }) {
> >
<HoverCard.Trigger> <HoverCard.Trigger>
<MenuItem <MenuItem
className="border-0 ms-2 me-1.5 mb-2" className={isCollapsedSidebar
? 'ce-nav-trigger ce-nav-trigger-collapsed'
: 'ce-nav-trigger border-0 ms-2 me-1.5 mb-2'}
text={t('ExportHelper')} text={t('ExportHelper')}
ariaLabel={t('ExportHelper')}
icon={IconArrowRightFromBracket} icon={IconArrowRightFromBracket}
onClick={() => { onClick={() => {
setOpen(true) setOpen(true)
@@ -210,7 +269,7 @@ function MenuInner({ container }: { container: HTMLDivElement }) {
</HoverCard.Content> </HoverCard.Content>
</Portal> </Portal>
</HoverCard.Root> </HoverCard.Root>
<Divider /> {!isCollapsedSidebar && <Divider />}
</> </>
) )
} }

View File

@@ -10,11 +10,12 @@ export interface MenuItemProps {
successText?: string successText?: string
disabled?: boolean disabled?: boolean
title?: string title?: string
ariaLabel?: string
className?: string className?: string
onClick?: (() => boolean) | (() => Promise<boolean>) onClick?: (() => boolean) | (() => Promise<boolean>)
} }
export const MenuItem: FC<MenuItemProps> = ({ text, successText, disabled = false, title, icon: Icon, onClick, className }) => { export const MenuItem: FC<MenuItemProps> = ({ text, successText, disabled = false, title, ariaLabel, icon: Icon, onClick, className }) => {
const [loading, setLoading] = useState(false) const [loading, setLoading] = useState(false)
const [succeed, setSucceed] = useState(false) const [succeed, setSucceed] = useState(false)
@@ -52,6 +53,7 @@ export const MenuItem: FC<MenuItemProps> = ({ text, successText, disabled = fals
onClick={handleClick} onClick={handleClick}
onTouchStart={handleClick} onTouchStart={handleClick}
disabled={disabled} disabled={disabled}
aria-label={ariaLabel}
title={title} title={title}
> >
{loading {loading
@@ -63,7 +65,9 @@ export const MenuItem: FC<MenuItemProps> = ({ text, successText, disabled = fals
: ( : (
<> <>
{Icon && <Icon />} {Icon && <Icon />}
{(succeed && successText) ? successText : text} <span className="ce-menu-item-text">
{(succeed && successText) ? successText : text}
</span>
</> </>
)} )}
</div> </div>