Skip to content

Commit 3c709c5

Browse files
lokeshpre-commit-ci[bot]Copilotcdrini
authored
Add OlDrawer component for hamburger menu (#12326)
* feat: add OlDrawer Lit component and update header bar navigation * feat: refine OlDrawer component and integrate with header navigation 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. * style: improve readability of hamburgerLinks list formatting * refactor: hardcode drawer menu links for readability Replace the loop-based hamburgerLinks rendering with hardcoded markup. Remove unused loginLinks, contributeLinks, and resourceLinks arrays. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * fix: focus first slotted element on drawer open to fix tab order 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. * Update openlibrary/templates/lib/nav_head.html Co-authored-by: Copilot <[email protected]> * Update openlibrary/components/lit/OlDrawer.js Co-authored-by: Copilot <[email protected]> * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Replace hamburger auto-close with in-place loading feedback 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. * Add press-scale feedback to drawer menu items 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. * Guard drawer re-entry when open flips false before enter microtask 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. * Prevent page shift when drawer opens by compensating for scrollbar width 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. * Fix ol-drawer stranding on synchronous open→close 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. * fix: repair drawer after the master merge - OlDrawer imported getFocusableFromSlot, which master renamed to getTabbableFromSlot. A focus trap wants tab stops, not every focusable node, so the rename is also the correct semantics here. - Port the drawer's design-system docs onto master's rebuilt /developers/design: a registry row in design.py plus a Jinja partial, replacing the old hand-written Templetor section. Claude-Session: https://claude.ai/code/session_016RMfy95Q9ritR7KF727ouR * refactor(drawer): build ol-drawer on native <dialog>.showModal() The drawer was a fifth overlay mechanism — position: fixed plus z-index: var(--z-index-modal) — where docs/ai/web-components.md asks new overlays to join one of the four that already work. It is a modal by every test there (focus trap, aria-modal, scrim, Escape, scroll lock), so it belongs on native <dialog>. The <dialog> fills the viewport and is transparent; the scrim and the sliding panel are ordinary children. That costs one element over ::backdrop and buys direct control of scrim opacity from the swipe gesture, which a pseudo-element can't take. What the browser now does for us: - Top layer. The PR's whole motivation. Verified above a z-index 2147483647 overlay from inside a transform / contain: paint / isolation ancestor — the containing-block trap that no z-index can escape. The "place it near the document root" caveat in the old docblock is gone. - Background inerting, so aria-modal="true" is finally true. Previously the JS trap caught Tab but a screen reader's virtual cursor still browsed the page behind. Verified: a header link can't take focus while open. - Escape, via the cancel event. Deleted along the way: the hand-rolled backdrop and its opacity choreography, the z-index, the document-level Escape listener, the closed/preparing/entering/open/exiting state machine (the dialog is either open or not), and the swipe path's duplicate close implementation — a swipe now just sets a reason and flips `open`, so all four dismissals share one exit. Two behavioral changes worth naming: - ol-drawer-hide is now cancelable and fires for every close with a reason ('escape' | 'scrim' | 'swipe' | 'programmatic'), matching ol-dialog's ol-close. It previously skipped programmatic closes entirely, and the swipe path fired it after the animation rather than before. - The dismissal reason 'backdrop' is now 'scrim', matching the element. Reduced motion is handled by reading the resolved transition-duration (ol-dialog's trick, applied to transitions) rather than a matchMedia call in JS, so the media query lives in one place. Initial focus runs synchronously instead of in a requestAnimationFrame — the dialog is laid out by the time showModal() returns, and rAF is paused in an occluded tab, which stranded focus on the dialog itself. nav_head.html needs no change: it only listens for ol-drawer-show and ol-drawer-after-hide. Claude-Session: https://claude.ai/code/session_016RMfy95Q9ritR7KF727ouR * refactor(nav): share Browse destinations between the drawer and popover The Browse links were in three places after the drawer landed: browseLinks in nav_head.html (9 items, and orphaned by this branch — its only consumer was the old hamburger dropdown), browse_popover.html's own hardcoded list, and the drawer's hand-written rows. They had already drifted: the popover was missing Advanced Search. nav.py holds the destination table — what exists, where it points, its analytics slug — and nothing else. Presentation stays in the templates: the popover still owns the icons and blurbs for the rows it features, the drawer still renders one flat list. Sharing the markup instead would push icons into Python and buy nothing, since the two surfaces genuinely differ. Both now derive their featured/simple split and positional analytics ranks from one ordered list, so adding a destination is a single edit and the popover picks up Advanced Search at rank 9. The drawer keeps its two-part track values rather than inheriting the popover's rank — mixing formats within one menu would muddy the data. Labels are translated per call rather than at import, so the list is built inside the function; a module-level _() would freeze one request's locale process-wide. Claude-Session: https://claude.ai/code/session_016RMfy95Q9ritR7KF727ouR * fix(header): show Sign Up on phones, not just in the hamburger .auth-component was display:none below 30em (480px), so on most phones in portrait — 360-430px covers iPhone SE through Pro Max and the common Android widths — a logged-out patron saw no account entry point in the header at all. The hamburger was the only route, and it does nothing until the Lit bundle executes. This branch lengthens that window (the old menu was a JS-free <details>), which is what surfaced it. Only Sign Up appears below 768px; .hide-me still holds Log In back, exactly as it did between 480 and 767px. That's deliberate: measured at 360px and 390px, Sign Up alone is ~77px and sits on the header's first row beside the logo, search and hamburger, leaving header height at 52px. Showing both wraps the hamburger onto a second row and takes the header to 74px. No horizontal overflow at 360, 390, 500 or 800px. Claude-Session: https://claude.ai/code/session_016RMfy95Q9ritR7KF727ouR * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * fix(drawer): stop the end-placement slide from jumping and snapping back The dialog clipped its off-screen panel with `overflow: hidden`, which still creates a scroll container — it only hides the scrollbars. A panel parked at translateX(100%) therefore sat in the dialog's scrollable overflow (scrollWidth 1652 against a 1352 viewport), and focusing a link inside it scrolled the dialog by the panel's full width to bring it into view. That 300px offset then fought the transform transition: the drawer appeared already open, overshot, and snapped back. Only `end` placement showed it. Overflow to the left of the origin isn't scrollable in LTR, so `start` never had anything to scroll and always slid cleanly — which is what pointed at the cause. `overflow: clip` clips without ever becoming scrollable, with `hidden` left as the preceding declaration for Safari < 16. For that fallback the dialog's scrollLeft is also reset after the focus calls, in the same synchronous block so nothing paints in between. Verified: scrollLeft now stays 0 for both end-placement demos and the header drawer, forcing scrollLeft to 999 leaves it at 0, focusing inside the panel no longer moves it, and the panel carries a real 400ms transform CSSTransition. Claude-Session: https://claude.ai/code/session_016RMfy95Q9ritR7KF727ouR * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * chore: untrack _testing-prs.json Local scratch file swept in by a `git add -A`. It stays on disk and is now listed in .git/info/exclude so it can't be re-added. Claude-Session: https://claude.ai/code/session_016RMfy95Q9ritR7KF727ouR * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Fix drawer registry row after upstream dropped Component.description The merge kept the drawer's description string as a third positional arg, which landed on `group` and collided with the keyword. * Stop swipe-dismiss from hijacking an in-flight scroll Both the drawer's horizontal swipe and the popover tray's vertical one armed on displacement alone, so scrolling the panel with a little drift in the dismiss direction started a drag the browser had already claimed for its own scroll — every preventDefault() on those non-cancelable touchmoves logged an intervention. The drawer now locks to whichever axis dominates the gesture, both components bail out when the touch is no longer cancelable, and preventDefault() is guarded either way. * fix(drawer): let a reopen during the exit transition take _openDrawer() bailed on an already-open dialog, which is exactly the state a reopen mid-exit lands in: showModal() is still in effect, so the early return skipped the _cycle bump and the queued close callback ran anyway. `open` stayed true against a closed dialog, and it took two more taps to get the drawer back — an accidental double-tap on the hamburger reads as a broken menu. Bump the cycle first so the pending close cancels, and re-run the entry. showModal() and the _previouslyFocusedElement capture are skipped on a reopen: the first throws on an open dialog, and the second would record an element inside the drawer, losing the real restore target. * fix(drawer): drop the subsection divider that never rendered --dark-beige is defined nowhere in the repo, so the border-top shorthand was invalid at computed-value time and the style fell back to none. Rather than pick a replacement token, remove the rule: the subsections read fine on spacing alone. * fix(nav): don't 500 the site on a Browse link with no icon navIcons[link.track] was a hard subscript on a dict only browse_popover maintains, while nav.py told you adding a destination is one edit there. Following that advice raised KeyError in a template nav_head renders on every page. Fall back to a generic icon, and say so in the docstring. * fix(nav): keep the merge-request count super-librarian only The deleted header_dropdown.html gated both merge-count badges on is_super_librarian_or_higher(); folding the markup into nav_head reused its is_librarian_or_higher() and quietly showed the counts to every librarian. The /merges link itself stays librarian-gated as before. * chore(nav): drop header_dropdown's dead loginClass branch The hamburger's loginLinks was its only caller, and the drawer rewrite replaced that with drawer-menu__login-links in nav_head. The .login-links* rules went with it, so the branch survived only to render unstyled markup nothing reaches. * Update openlibrary/components/lit/OlDrawer.js Co-authored-by: Drini Cami <[email protected]> * docs(lit): type enum props as unions, not {String} The API tables on /developers/design print the JSDoc type verbatim, so {String} rendered a useless "String" while the real values sat in prose that nothing checks. Converting the closed sets means editors autocomplete them, tsc --checkJs catches typos, and the table lists them for free. Follows cdrini's review suggestion on ol-drawer's placement. Popover and tooltip placement stay {String} — they're a {side}-{align} grammar, not a short list worth enumerating. Also writes the rule down in docs/ai/web-components.md so new components keep doing it. * fix(drawer): scroll a Tab target into the panel's view The focus trap preventDefault()s every Tab and moves focus itself, which also cancels the browser's native scroll-into-view. In a drawer taller than its own scroller, Tab could land on a control below the fold — focus invisible to the user, a WCAG 2.4.11 failure. focus({preventScroll:true}) stays, because letting focus() scroll would walk every ancestor and push the panel itself off-screen; scrollIntoView with block:'nearest' moves only the panel's scroller, and only when the target isn't already visible. Consumers with a sticky header or footer can inset the resting position through the new --ol-drawer-scroll-padding custom property, since the panel is shadow DOM and scroll-padding is otherwise out of reach. * refactor(nav): move the hamburger drawer's inline script into a module The wiring lived as an inline <script> in nav_head.html, so it shipped on every page render, escaped lint and bundling, and had to stay ES5-flavoured. It now lives in js/hamburger-drawer.js, dynamically imported from index.js only when the trigger and drawer are both present. * fix(nav): stop the hamburger icon rendering blue on iOS Safari .hamburger-trigger reset the button's background and border but never set a color, and a <button> doesn't inherit one — the UA's ButtonText wins, and iOS Safari resolves that to its system blue, which the currentColor sprite picks up. Pin it to --color-text, the same token the Browse trigger uses. The magnifier and barcode move to --color-text too, so the three header icons read as a set. The magnifier had #8a7f71 baked into search-icon.svg and could not take a token at all, so it becomes a sprite icon; the file stays for the legacy search box in searchbox.css. --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Copilot <[email protected]> Co-authored-by: Drini Cami <[email protected]>
1 parent 38f397b commit 3c709c5

28 files changed

Lines changed: 1516 additions & 427 deletions

docs/ai/web-components.md

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ The API reference tables on `/developers/design` are **generated, not hand-writt
9999

100100
To make a component's API appear in the tables:
101101

102-
- **Properties** — declare each public property in `static properties` and document it with `@prop {Type} name - description`. The *Attribute* column comes from the property's `attribute` mapping: use `{ attribute: 'kebab-name' }` for multi-word names; single-word props map 1:1.
102+
- **Properties** — declare each public property in `static properties` and document it with `@prop {Type} name - description`. The *Attribute* column comes from the property's `attribute` mapping: use `{ attribute: 'kebab-name' }` for multi-word names; single-word props map 1:1. **A prop with a closed set of string values is typed as a union, not `{String}`** — see below.
103103
- **Events**`@fires event-name - description`. Describe the `detail` payload in the description (e.g. `detail: { selected: Boolean }`).
104104
- **Slots**`@slot - description` for the default slot; `@slot name - description` for named slots.
105105
- **CSS custom properties**`@cssprop [--name=default] - description`. The bracketed default fills the *Default* column.
@@ -108,6 +108,28 @@ To make a component's API appear in the tables:
108108

109109
Intentionally **excluded** from the tables: internal reactive state (Lit `state: true`, conventionally `_`-prefixed) and any non-public member — keep those out of `@prop`.
110110

111+
### Type the enum, don't describe it
112+
113+
The *Type* column is `type.text` copied verbatim out of the JSDoc, so `@prop {String} variant` renders the useless word `String` and the real answer ends up buried in prose that nothing checks. **Write the allowed values as a union type.** Editors then autocomplete them, `tsc --checkJs` catches typos, and the design page's table lists them without anyone maintaining a second copy.
114+
115+
```js
116+
// Good — the values ARE the type
117+
* @prop {"small" | "medium"} size - Default: "medium"
118+
* @prop {"neutral" | "success" | "warning" | "danger"} variant - Default: "neutral"
119+
120+
// Bad — the type says nothing and the values drift out of sync with the code
121+
* @prop {String} size - Chip size: "small" or "medium" (default)
122+
```
123+
124+
Where the line falls:
125+
126+
- **Closed set of string literals** → union (`variant`, `size`, `mode`, `placement`, `appearance`, `type`). One legal value is still a union: `@prop {"floating"} elevation`, `@prop {"button"} variant` — omitting the attribute is the other state.
127+
- **Open-ended string**`{String}` (`label`, `href`, `name`, every `label*` i18n override, a CSS length like `maxHeight`).
128+
- **Composed grammar rather than a fixed list**`{String}`, with the grammar spelled out in the description. `ol-popover` / `ol-tooltip` `placement` is `"{side}"` or `"{side}-{align}"`; enumerating all 16 combinations would be noise in the table. Prefer the union whenever the list is short enough to read at a glance (roughly six or fewer).
129+
- **State the default in the description** (`Default: "medium"`), never in the type — the manifest reads the *Default* column from the field's initializer, and repeating it in the type just makes the type wrong.
130+
- **Don't restate the values in the description** once they're in the type. That duplication is exactly what drifts.
131+
- **Quote style follows the file.** Both `{"start" | "end"}` and `{'start' | 'end'}` render fine; match whichever the component's own JSDoc already uses rather than churning it.
132+
111133
Example (from `OLChip.js`):
112134

113135
```js
@@ -538,7 +560,7 @@ _onPopoverOpen() {
538560
539561
1. Create a file in `openlibrary/components/lit/` named after the class (e.g., `OlMyWidget.js`).
540562
2. Register the component by adding an export to `openlibrary/components/lit/index.js`.
541-
3. Add JSDoc to the class documenting the public API — `@prop`, `@fires`, `@slot`, `@cssprop`, `@csspart` (see [Documenting the API](#documenting-the-api-custom-elements-manifest)). This drives the generated API tables; no hand-written prop tables.
563+
3. Add JSDoc to the class documenting the public API — `@prop`, `@fires`, `@slot`, `@cssprop`, `@csspart` (see [Documenting the API](#documenting-the-api-custom-elements-manifest)). This drives the generated API tables; no hand-written prop tables. Type any closed set of values as a union, not `{String}` — see [Type the enum, don't describe it](#type-the-enum-dont-describe-it).
542564
4. Regenerate the Custom Elements Manifest (`npm run build-assets:lit-manifest`) so the API table renders locally; the JSON is gitignored and rebuilt by `make lit-components` in CI/deploy.
543565
5. Add a demo partial at `openlibrary/templates/design/components/<id>.html.jinja` defining a `{% macro demos() %}` of `ex.example(...)` calls, and register a `Component(...)` row in `COMPONENTS` in `openlibrary/plugins/openlibrary/design.py`. The row drives the sidebar, section order, and the *Avoid* line; the API table renders from the manifest. Nothing on the page is hand-listed — `openlibrary/templates/design.html` is only a shim into `design/layout.html.jinja`, so there is no section markup to add there.
544566
6. If it renders an anchored overlay panel, promote it to the top layer — see [Overlays and the top layer](#overlays-and-the-top-layer).

openlibrary/components/lit/OLChip.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,9 @@ import './OlIcon.js';
99
* click events, and optional link behavior via href.
1010
*
1111
* @prop {Boolean} selected - Whether the chip is in a selected state
12-
* @prop {String} size - Chip size: "small" or "medium" (default)
13-
* @prop {String} variant - Domain category that tints the chip:
14-
* "language" | "subject" | "genre" | "author" | "place" | "neutral".
12+
* @prop {"small" | "medium"} size - Default: "medium"
13+
* @prop {"language" | "subject" | "genre" | "author" | "place" | "neutral"} variant -
14+
* Domain category that tints the chip.
1515
* Omit for the default (white / solid-blue-when-selected) chip. The chip
1616
* maps the variant to a soft-tint palette internally (see colors.css); a
1717
* variant chip keeps its tint when `selected` and just gains a close icon.

openlibrary/components/lit/OLChipGroup.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@ import { LitElement, html, css } from 'lit';
55
*
66
* Provides consistent spacing and wrapping behavior for groups of chips.
77
*
8-
* @prop {String} gap - Gap size: "small" (4px), "medium" (8px, default), or "large" (12px)
8+
* @prop {"small" | "medium" | "large"} gap - "small" (4px), "medium" (8px,
9+
* default), or "large" (12px)
910
*
1011
* @slot - One or more <ol-chip> elements
1112
*

openlibrary/components/lit/OLReadMore.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import './OlIcon.js';
1919
* @prop {String} moreText - Label for the expand toggle (default: "Read more")
2020
* @prop {String} lessText - Label for the collapse toggle (default: "Read less")
2121
* @prop {String} backgroundColor - Background color for the gradient fade (default: white)
22-
* @prop {String} labelSize - Size of the toggle button text: "medium" (default) or "small" (12px)
22+
* @prop {"medium" | "small"} labelSize - Size of the toggle button text: "medium" (default) or "small" (12px)
2323
*
2424
* @slot - The collapsible content
2525
*

openlibrary/components/lit/OlBanner.js

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -31,14 +31,14 @@ import './OlIcon.js';
3131
*
3232
* @element ol-banner
3333
*
34-
* @prop {String} variant - "neutral" (default) | "success" | "warning" | "danger"
35-
* @prop {String} appearance - "outlined" (default) shows a border and rounded
36-
* corners; "plain" removes both, for banners that
37-
* abut the edges of other UI.
38-
* @prop {String} dismissId - Opaque identifier passed in the dismiss event's
39-
* detail, for the host app's persistence layer.
34+
* @prop {"neutral" | "success" | "warning" | "danger"} variant - Default: "neutral"
35+
* @prop {"outlined" | "plain"} appearance - "outlined" (default) shows a border and
36+
* rounded corners; "plain" removes both, for banners that abut the edges of
37+
* other UI.
38+
* @prop {String} dismissId - Opaque identifier passed in the dismiss event's
39+
* detail, for the host app's persistence layer.
4040
* @prop {Boolean} dismissible - Show a close button.
41-
* @prop {String} labelClose - Aria label for the close button (default: "Close")
41+
* @prop {String} labelClose - Aria label for the close button (default: "Close")
4242
*
4343
* @slot - The (already translated, server-rendered) announcement content.
4444
* @slot icon - Optional custom icon (a child with slot="icon"), replacing the

openlibrary/components/lit/OlDialog.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,16 +20,16 @@ import { slotHasContent } from './utils/slot-utils.js';
2020
* accessible name when `withoutHeader` is true.
2121
* @prop {Boolean} withoutHeader - Hide the default header (title + close
2222
* button). The `header` slot still works.
23-
* @prop {String} width - Width preset: `'small'` (400px), `'medium'` (550px,
24-
* default), or `'large'` (800px). Override per-instance via
25-
* `--ol-dialog-width-*` host CSS variables.
23+
* @prop {'small' | 'medium' | 'large'} width - Width preset: `'small'` (400px),
24+
* `'medium'` (550px, default), or `'large'` (800px). Override per-instance
25+
* via `--ol-dialog-width-*` host CSS variables.
2626
* @prop {Boolean} closeOnBackdropClick - Whether clicking the backdrop closes
2727
* the dialog. Default `true`. Attribute: `close-on-backdrop-click`.
2828
* @prop {Boolean} closeOnEscape - Whether pressing Escape closes the dialog.
2929
* Default `true`. Attribute: `close-on-escape`.
3030
* @prop {Boolean} fullscreenOnMobile - At viewports ≤767px, render edge-to-edge
3131
* (full viewport, no border-radius). Attribute: `fullscreen-on-mobile`.
32-
* @prop {String} placement - `'center'` (default) keeps the dialog vertically
32+
* @prop {'center' | 'top'} placement - `'center'` (default) keeps the dialog vertically
3333
* centered like a normal modal. `'top'` anchors it a fixed distance from
3434
* the top of the viewport so the top edge stays put as content grows or
3535
* shrinks (command-palette / search-modal pattern).

0 commit comments

Comments
 (0)