Add OlDrawer component for hamburger menu#12326
Conversation
Remove lightDismiss prop (always allow backdrop dismiss), tune animation durations, improve swipe-to-close backdrop opacity calculation, and move drawer to document.body in demo page. Simplify header_dropdown template by removing hamburger-specific logic now handled by the drawer. Add aria-expanded toggling and auto-close on link/button click to nav_head. Fix drawer menu CSS: use design tokens, scope :hover behind media query, fix login link underlines via :link/:visited specificity, and add last-child border removal.
Replace the loop-based hamburgerLinks rendering with hardcoded markup. Remove unused loginLinks, contributeLinks, and resourceLinks arrays.
for more information, see https://pre-commit.ci
There was a problem hiding this comment.
Pull request overview
This PR replaces the header hamburger menu’s <details> dropdown with a new ol-drawer Lit web component that renders as a root-level overlay (solving stacking-context/z-index issues) and adds a design pattern library page documenting the drawer.
Changes:
- Added
OlDrawerLit component (<ol-drawer>) with backdrop, animation, focus trapping, scroll lock, and swipe-to-dismiss. - Reworked the header hamburger markup/CSS to use the new drawer + new light-DOM menu styles.
- Added design system documentation/demo for the drawer and bumped the
globalsnpm dependency.
Reviewed changes
Copilot reviewed 13 out of 14 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| static/css/legacy.css | Extends existing header icon sizing to the new hamburger markup; removes unused drawer/mask positioning rules. |
| static/css/components/header-bar.css | Removes <details> hamburger drawer styles and adds new .hamburger-trigger + .drawer-menu* light-DOM styling. |
| static/css/components/header-bar--desktop.css | Removes old drawer/mask sizing rules; updates icon sizing selectors. |
| package.json | Bumps globals dependency version. |
| package-lock.json | Updates lockfile for globals bump and related metadata normalization. |
| openlibrary/templates/site/head.html | Hides <ol-drawer> until defined to prevent flash of unstyled content. |
| openlibrary/templates/lib/nav_head.html | Replaces hamburger <details> dropdown with <ol-drawer> + inline wiring script. |
| openlibrary/templates/lib/header_dropdown.html | Removes hamburger-specific rendering/mask/drawer logic now that hamburger is handled separately. |
| openlibrary/templates/design/drawer.html | Adds drawer component documentation + live demos. |
| openlibrary/templates/design.html | Adds “Drawer” section to the design system index and renders the new drawer template. |
| openlibrary/plugins/openlibrary/js/index.js | Removes old hamburger mask click handler tied to the <details> implementation. |
| openlibrary/i18n/messages.pot | Updates message catalog references/strings for the new drawer docs and hamburger markup. |
| openlibrary/components/lit/OlDrawer.js | Introduces the new <ol-drawer> Lit component implementation. |
| openlibrary/components/lit/index.js | Exports/registers OlDrawer in the Lit components entrypoint. |
| var trigger = document.querySelector('.hamburger-trigger'); | ||
| var drawer = document.getElementById('hamburger-drawer'); | ||
| if (trigger && drawer) { | ||
| trigger.addEventListener('click', function() { | ||
| drawer.open = !drawer.open; | ||
| }); |
There was a problem hiding this comment.
ol-components.js (which defines ol-drawer) is loaded in the footer, but this click handler toggles the drawer via the open property immediately. If a user clicks before the custom element upgrades, this will set an expando property on the undefined element and the value will be lost on upgrade (menu appears to not open until a second click). Prefer toggling the open attribute (toggleAttribute/setAttribute/removeAttribute) or waiting for customElements.whenDefined('ol-drawer') before wiring the handler.
| disconnectedCallback() { | ||
| super.disconnectedCallback(); | ||
| this._removeListeners(); | ||
| this._unlockBodyScroll(); | ||
| } |
There was a problem hiding this comment.
disconnectedCallback() unconditionally calls _unlockBodyScroll(), which will call window.scrollTo(0, this._savedScrollY) even if the drawer was never opened/locked. If the element is removed/reparented while closed, this can unexpectedly scroll the page to the top. Guard this so unlock/scroll restoration only happens if a scroll lock is currently active.
|
Thank you for this detailed PR, @lokesh! The description is clear and well-structured. 🤖 Copilot has been assigned for an initial review. There's no assignee on this PR or a linked issue yet, so it will enter the general review queue. There are currently 76 open non-draft PRs ahead of yours. Possible improvements for this PR
PR triage checklist (maintainers / Pam)
Note This comment was automatically generated by Pam, Open Library's Project AI Manager, on behalf of @mekarpeles. Pam is designed to provide status visibility, perform basic project management functions and relevant codebase research, and provide actionable feedback so contributors aren't left waiting. |
Previously, the drawer panel itself received focus on open. Pressing Tab then hit the start focus-sentinel, which incorrectly wrapped to the last focusable element. Now the first focusable slotted element is focused instead, so Tab moves forward naturally.
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
for more information, see https://pre-commit.ci
# Conflicts: # openlibrary/plugins/openlibrary/js/index.js
for more information, see https://pre-commit.ci
Previously _get_pr_drift only returned head_sha/drift/merged, so stale
titles ("PR #12326") and missing author/assignee persisted indefinitely.
Now on a cache miss, _get_drift_info also writes fresh metadata back to
the state file, making "Refresh Status" actually update what you see.
- Staged UX: Add/Fetch Latest/Enable/Disable/Remove all stage changes;
only "Perform Deploy" applies them and triggers Jenkins
- Memcache drift cache (5-min TTL) to avoid hitting GitHub API on every
page load; "Refresh Status" evicts the cache explicitly
- On cache miss, refreshes title/author/assignee from GitHub and writes
back to state file so stale data ("PR #12326") self-corrects
- GitHub API token support via github_api_token in openlibrary.yml
- PR author + assignee with avatars (no extra API calls)
- Columns: Enabled (was Active), Pinned Commit with pending arrow,
Branch HEAD, Drift, Added (with added_by tooltip)
- Jenkins job URL in deploy banner; rel="noopener noreferrer" on links
- Backward-compat: old bare-array state file format still loads cleanly
- Staged UX: Add/Fetch Latest/Enable/Disable/Remove all stage changes;
only "Perform Deploy" applies them and triggers Jenkins
- Memcache drift cache (5-min TTL) to avoid hitting GitHub API on every
page load; "Refresh Status" evicts the cache explicitly
- On cache miss, refreshes title/author/assignee from GitHub and writes
back to state file so stale data ("PR #12326") self-corrects
- GitHub API token support via github_api_token in openlibrary.yml
- PR author + assignee with avatars (no extra API calls)
- Columns: Enabled (was Active), Pinned Commit with pending arrow,
Branch HEAD, Drift, Added (with added_by tooltip)
- Jenkins job URL in deploy banner; rel="noopener noreferrer" on links
- Backward-compat: old bare-array state file format still loads cleanly
Resolve conflicts and update OlDrawer to adopted component conventions: - Use shared scroll-lock.js and focus-utils.js (keydown Tab-trap like OlDialog) instead of hand-rolled scroll lock and focus sentinels - Design tokens without fallbacks; rename --size to --ol-drawer-width, add --ol-drawer-* cssprops; use --z-index-modal and semantic colors - Guard customElements.define; regenerate custom-elements manifest - Move ol-drawer pre-hydration FOUC rule into ol-components.css - Drop dead .app-drawer/.mask-menu CSS; instant hover on drawer login links - Regenerate messages.pot
Pressing a menu link no longer closes the drawer. Instead the pressed row holds full opacity while the rest of the menu dims and a spinner shows at its trailing edge — mirroring the search modal's result loading treatment — so the drawer stays open until the new page takes over. Modified/new-tab clicks are left untreated, and the state is cleared on bfcache restore.
Match the <ol-button> press affordance: menu links and the logout button scale to 0.97 on :active with an 0.08s transform transition. Hover color changes stay instant per docs/ai/design.md.
A fast toggle could set open=false after _show() scheduled its updateComplete callback but before it ran. The callback then set _animState back to 'entering', re-opening the drawer while open was false — leaving it stuck open and scroll-locked, since Escape and backdrop dismissal both gate on this.open. Bail out of the open sequence if open is no longer true.
Pinning <body> to lock background scroll removes the document scrollbar; on platforms with classic (space-consuming) scrollbars, reclaiming that width shifted the page. Measure the scrollbar width before locking and hold it as body padding (border-box) so the layout stays put. No-op on overlay-scrollbar platforms where the width is 0.
When open was toggled true then false within the same tick (before the enter transition was armed), _hide() moved the still-parked panel to 'exiting'. That's a closed→closed no-op transition that never fires transitionend, so cleanup never ran and the drawer was left scroll-locked with its keydown/touch listeners attached. Finish synchronously from the 'preparing' state, mirroring the reduced-motion path.
# Conflicts: # openlibrary/components/lit/index.js
|
|
||
| $:render_template("lib/header_dropdown", hamburgerProps, track_prefix="Hamburger") | ||
| <div class="hamburger-component" data-ol-link-track="HeaderBar|Hamburger"> | ||
| <button class="hamburger-trigger" aria-label="$_('Menu')" aria-expanded="false"> |
There was a problem hiding this comment.
Best Practice: Accessible name "$_('Menu')" does not contain visible text "$if ctx.user:
$if is_privileged_user and cached_get_counts_by_mode(mode='open') > 0:
$(cached_get_counts_by_mode(mode='open'))".
Interactive elements with visible text must have accessible names that contain that text.
Details
For voice control users who activate controls by speaking their visible label, the accessible name must include the visible text. If aria-label is 'Submit form' but the button shows 'Send', voice users saying 'click Send' won't activate it. Ensure aria-label/aria-labelledby contains or matches the visible text.
| </header> | ||
|
|
||
| <ol-drawer id="hamburger-drawer" label="$_('Menu')"> | ||
| <ul class="drawer-menu hamburger-drawer-menu"> |
There was a problem hiding this comment.
WCAG 1.3.1: <ul> contains direct text content. Wrap in <li>.
<ul> and <ol> must only contain <li>, <script>, or <template> as direct children.
Details
Screen readers announce list structure ('list with 5 items') based on proper markup. Placing non-<li> elements directly inside <ul> or <ol> breaks this structure. Wrap content in <li> elements, or if you need wrapper divs for styling, restructure your CSS to style the <li> elements directly.
| } | ||
|
|
||
| /* The row holding the pressed link stays lit. `is-target-row` is set on the | ||
| <li> (not the link) because opacity on the row compounds onto its children, |
There was a problem hiding this comment.
WCAG 1.3.1: <li> is not contained in a <ul>, <ol>, or <menu>.
<li> elements must be contained in a <ul>, <ol>, or <menu>.
Details
List items (<li>) only have semantic meaning inside a list container (<ul>, <ol>, or <menu>). Outside of these containers, assistive technologies cannot convey the list relationship. Wrap <li> elements in the appropriate list container.
drawer.mp4
Replaces the hamburger menu's
<details>-based dropdown with a newol-drawerLit web component that slides in from the viewport edge.Motivation: As we flatten stacking contexts across the site, overlays like drawers and popovers need to render reliably above all page content, including the header. The old dropdown was tied to the header's stacking context, which made that hard. The drawer renders at the document root, sidestepping the problem.
Improvements:
aria-expandedstate on the hamburger triggerprefers-reduced-motionTesting
Stakeholders
@cdrini