Add collapsible sidebar and redesign app layout#1748
Add collapsible sidebar and redesign app layout#1748
Conversation
|
As this MR handles persistent state via local storage, it will result in showing the sidebar for a short amount of time on page load. I suggest that we work on a more generic "UI state" solution in a dedicated PR. |
There was a problem hiding this comment.
Pull request overview
This pull request adds a collapsible sidebar and redesigns the application layout. The changes replace the previous drawer-based sidebar with a new implementation that supports both mobile overlay and desktop push-aside modes, with state persistence for the desktop view.
Changes:
- Replaced
BackpexSidebarSectionshook with a newBackpexSidebarhook that manages both sidebar visibility and section expansion - Redesigned app shell layout to use a fixed sidebar with overlay on mobile and content-shifting on desktop
- Moved branding component from topbar to sidebar (breaking change from
topbar_brandingtosidebar_branding)
Reviewed changes
Copilot reviewed 9 out of 11 changed files in this pull request and generated 18 comments.
Show a summary per file
| File | Description |
|---|---|
| assets/js/hooks/_sidebar.js | New hook implementation combining sidebar visibility management with section expansion logic |
| assets/js/hooks/_sidebar_sections.js | Removed old sections-only hook |
| assets/js/hooks/index.js | Updated export to reference new BackpexSidebar hook |
| priv/static/js/backpex.esm.js | Compiled ESM bundle with new sidebar implementation |
| priv/static/js/backpex.cjs.js | Compiled CJS bundle with new sidebar implementation |
| lib/backpex/html/layout.ex | Redesigned app shell layout with fixed sidebar and renamed branding component |
| demo/lib/demo_web/components/layouts/admin.html.heex | Updated demo to use new sidebar structure with sidebar_branding |
| demo/assets/css/app.css | Added CSS custom property for sidebar width |
| priv/gettext/backpex.pot | Updated translation strings for simplified navigation labels |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
The priv generator template and installation guide still referenced the removed topbar_branding component and the old sidebar slot shape. Move the branding into the sidebar slot as sidebar_branding and wrap nav content in <nav><ul> so copy-pasted layouts compile and render correctly under the new app_shell API.
Document the breaking sidebar API changes (topbar_branding rename and slot move, required <nav>/<ul> wrapper, --sidebar-width CSS variable, and BackpexSidebarSections hook rename) so existing users can migrate on the next release.
Use Tailwind's arbitrary-value syntax with a var() fallback so the sidebar renders at a sane width even when the consumer hasn't defined --sidebar-width. Consumers can still override by setting the variable in their own stylesheet.
Replace the non-interactive <span> with a <button>, add aria-expanded and aria-controls pointing at the content <ul>, and keep the ARIA state in sync in the hook on both initial mount and click. Users can now focus the toggle with Tab and expand/collapse it with Enter or Space, fixing WCAG 2.1.1 and 4.1.2 violations.
The mobile sidebar is a modal overlay but was leaking Tab focus into the hidden content underneath and never returned focus to the toggle on close. Store the previously focused element when opening, move focus into the sidebar, trap Tab within it while open, and restore focus on close. Apply role="dialog"/aria-modal dynamically so the desktop collapsible panel is not mislabeled as a modal.
When the sidebar is translated off-canvas, its links and buttons stay in the DOM and keyboard focus can land on invisible elements. Toggle the inert attribute alongside the transform so the off-canvas subtree is removed from the tab order and the accessibility tree.
Server-side renders the sidebar hidden on mobile and visible on desktop via -translate-x-full md:translate-x-0 so the CSS output matches the default state before JS loads. The hook now writes inline transform and margin styles (which win over the Tailwind responsive classes) and clears a data-suppress-transition attribute on the first animation frame so the snap to a stored non-default preference is instant rather than animated on page load. A remaining animation can occur on desktop hard-reload when the user previously collapsed the sidebar, because the server does not yet know the stored preference. A future cookie-backed UI state system will close that gap.
Store bound handlers as instance references during mount so the hook can remove its document keydown, matchMedia change, toggle click, overlay click, and per-section click listeners when the element is destroyed. Matches the cleanup pattern used by every other hook in this directory and prevents handlers from leaking across LiveView navigations.
Attach the BackpexSidebar hook conditionally on the presence of the sidebar slot, and early-return from mounted()/updated() when the sidebar DOM is missing. This prevents a TypeError on layouts that use app_shell without a sidebar.
Change the outer sidebar element from <aside aria-label="Main navigation"> to <nav> and drop the nested <nav> inside the slot. This avoids a double landmark and keeps screen reader navigation unambiguous.
Tailwind v4's -translate-x-full and md:translate-x-0 utilities emit the CSS `translate` property rather than `transform`. Setting the element's inline `transform` therefore did not override the SSR state, leaving the mobile drawer off-canvas. Writing to `style.translate` puts the hook on the same property it needs to win against.
Apply sidebar, overlay, and main-margin transition utilities only when the user has not requested reduced motion. Users who set prefers-reduced-motion: reduce no longer see the 300 ms slide and fade animations, satisfying WCAG 2.3.3.
Switch the mobile/desktop boundary from 768 px to 1024 px so iPad portrait and similar narrow-tablet viewports render the sidebar as a drawer instead of eating roughly half of the content area. Updates the Tailwind responsive classes in app_shell and the matching JS breakpoint in the sidebar hook.
Switch the mobile drawer backdrop from the hard-coded bg-black/50 to bg-neutral/50 so the scrim adapts to the active daisyUI theme instead of always rendering as cold black.
Switch the main container from transition-[margin] to transition-[margin-left] so only the single property that actually changes is animated. Same visual result, smaller animation scope and fewer styles forced onto the transition list.
b764bb6 to
5ecfabe
Compare
Drop the "section" default id. The id is used as the localStorage key for the collapsed state and as the basis for aria-controls, so two sections sharing the default would toggle together and announce conflicting relationships to assistive tech. Making the attribute required forces every section on a page to have its own identity, and the v0.19 upgrade guide documents the breaking change.
standard 17.1.2 does not recognize `requestAnimationFrame` as a browser global, matching prior gaps for `localStorage` and `IntersectionObserver`. Add it to the explicit globals list so CI lint passes.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 14 out of 16 changed files in this pull request and generated 3 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Replaces the `toggle._handler` expando property with a WeakMap keyed by
the toggle element. This keeps handler references off DOM nodes and
removes the awkward first-mount `removeEventListener('click', undefined)`
call by guarding the removal on a previously stored handler.
Tailwind v4 exposes breakpoints as CSS custom properties. Reading --breakpoint-lg in the sidebar hook keeps the `lg:` utilities in the layout and the JS media query in sync when consumers customize it, instead of hardcoding 1024px in two places.
collapsible_sidebar.mov