Skip to content

feat(drawer): rework drawer + use it for mobile docs navigation#3230

Draft
jpzwarte wants to merge 5 commits into
docs/website-v2from
claude/mobile-sidebar-drawer-qofFy
Draft

feat(drawer): rework drawer + use it for mobile docs navigation#3230
jpzwarte wants to merge 5 commits into
docs/website-v2from
claude/mobile-sidebar-drawer-qofFy

Conversation

@jpzwarte
Copy link
Copy Markdown
Member

Summary

Makes the v2 documentation website (docs/website) work better on mobile by hiding the sidebar behind a hamburger button that opens an sl-drawer sliding in from the left. To support that UX properly, the sl-drawer component was reworked first.

Changes

sl-drawer rework (packages/components/drawer)

  • Native <dialog>-based implementation with modern CSS transitions using @starting-style and transition-behavior: allow-discrete — no JS animation library, no lifecycle juggling.
  • show() method for non-modal use in addition to showModal() (useful for persistent/docked drawers).
  • Re-open guard: calling showModal()/show() on an already-open drawer is a no-op instead of throwing.
  • Built-in close button in the header (rendered with sl-icon "xmark"), with a configurable close-button-size.
  • Proper events:
    • sl-close fires whenever the drawer has closed.
    • sl-cancel fires when the user presses Escape or clicks the backdrop.
  • disable-close attribute/property to suppress Escape + backdrop dismissal (useful for mandatory flows).
  • Exposed ::part(dialog), ::part(header), ::part(body) so consumers can theme the drawer from the outside without piercing shadow DOM.
  • CSS custom properties for sizing: --sl-drawer-inline-size (left/right) and --sl-drawer-block-size (top/bottom).
  • display: contents on the host so the drawer participates transparently in its parent layout while the <dialog> lives in the top layer.
  • Tests updated to cover show(), the re-open guard, sl-close/sl-cancel emission, and disableClose behaviour on cancel + backdrop click.

Mobile navigation on the v2 docs site (docs/website)

  • New mobile-only header with a hamburger button + logo, shown below 640px and sticky at the top of the page.
  • Sidebar is hidden in the mobile layout and made accessible via the hamburger, which calls drawer.showModal() on an sl-drawer attached to the left.
  • The drawer hosts a second doc-sidebar (the same sidebar.njk partial), with ::part() styling to match the docs theme (padding reset, raised-sunken background).
  • Grid layout updated: mobile uses 'content' / 'toc'; ≥640px restores the original 'sidebar content' / 'sidebar content toc' layouts.
  • The drawer closes itself when a nav link inside it is clicked, and toggles aria-expanded on the hamburger via the sl-close event.
  • far-bars icon registered and @sl-design-system/drawer/register.js imported in main.js.

Test plan

  • Verify yarn build + yarn test pass for the drawer package.
  • On desktop (≥640px) the docs site is unchanged: sidebar in-grid, no mobile header, no drawer visible.
  • On mobile (<640px) the sidebar is hidden, the hamburger header is sticky, and tapping the hamburger slides the sidebar in from the left on top of the content.
  • Drawer closes on: Escape, backdrop click, close button click, and clicking a nav link inside the drawer.
  • aria-expanded on the hamburger toggles correctly when the drawer opens and closes.
  • Page scrolling is locked while the drawer is open and restored after it closes.
  • Enter/leave animations play (and are suppressed under prefers-reduced-motion).
  • disable-close prevents Escape + backdrop from closing the drawer (covered by unit tests).

https://claude.ai/code/session_015uB4f5pofTvdYH4Xkw1r4q

Rewrites sl-drawer to use a native <dialog> with modern CSS
transitions (@starting-style + allow-discrete), adds a show() method
for non-modal use, a built-in close button with proper events
(sl-close, sl-cancel), a disable-close option, and external styling
via ::part(dialog|header|body).

Uses the drawer on the v2 docs website to provide a mobile
hamburger menu that slides in the sidebar from the left on top of
the content, while keeping the in-grid sidebar on desktop.
Copilot AI review requested due to automatic review settings April 16, 2026 19:06
@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented Apr 16, 2026

⚠️ No Changeset found

Latest commit: eee2475

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

💥 An error occurred when fetching the changed packages and changesets in this PR
Some errors occurred when validating the changesets config:
The package "@sl-design-system/grid" depends on the ignored package "@sl-design-system/example-data", but "@sl-design-system/grid" is not being ignored. Please add "@sl-design-system/grid" to the `ignore` option.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Reworks the sl-drawer web component to use a native <dialog> foundation and updates the v2 docs website to use sl-drawer for mobile navigation (hamburger-triggered sidebar).

Changes:

  • Reimplemented sl-drawer with <dialog>, added show() (non-modal), new close button/header layout, and sl-close/sl-cancel events.
  • Updated drawer styling to support directional slide-in transitions and exposed theming via ::part(...) + CSS custom properties.
  • Added a mobile header + hamburger-driven navigation drawer to docs/website and adjusted the responsive layout/CSS accordingly.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
packages/components/drawer/src/drawer.ts New <dialog>-based drawer implementation, events, close behavior, and public API updates (show()/showModal()).
packages/components/drawer/src/drawer.spec.ts Updated/added tests for modal vs non-modal open, re-open guard, and emitted close/cancel events.
packages/components/drawer/src/drawer.scss New top-layer/backdrop styling, attachment sizing, and enter/leave transitions; exposes ::part(...) styling hooks.
packages/components/drawer/package.json Adds dependencies needed for new close button/icon and shared event utilities.
docs/website/src/js/main.js Registers drawer and hamburger icon; wires hamburger ↔ drawer open/close and closes on nav link click.
docs/website/src/includes/base.njk Adds mobile-only header and a left-attached sl-drawer containing the sidebar for mobile navigation.
docs/website/src/css/main.css Responsive grid/layout updates plus mobile header + drawer theming via ::part(...).

});

it('should close the drawer when the cancel event is fired and close isn`t disabled', async () => {
it('should emit sl-cancel when the cancel event is fired and close isn`t disabled', async () => {
Copy link

Copilot AI Apr 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Test name contains a typo: use "isn't" instead of "isn`t".

Suggested change
it('should emit sl-cancel when the cancel event is fired and close isn`t disabled', async () => {
it("should emit sl-cancel when the cancel event is fired and close isn't disabled", async () => {

Copilot uses AI. Check for mistakes.
Comment on lines 95 to +105
@@ -60,68 +99,121 @@ export class Drawer extends ScopedElementsMixin(LitElement) {
aria-labelledby="title"
part="dialog"
>
<div>
<sl-button-bar>
<sl-button
sl-dialog-close
.size=${this.closeButtonSize}
tab-index="0"
aria-label="back to page"
title="close"
>x</sl-button
>
<slot name="actions"></slot>
</sl-button-bar>
<slot name="title" id="title"></slot>
<div part="header">
<slot name="header">
<slot name="title" id="title"></slot>
<sl-button-bar>
Copy link

Copilot AI Apr 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

aria-labelledby="title" points at the slot#title, but when consumers provide their own slot="header" content the default title slot may be omitted, leaving the internal <dialog> without a valid accessible label. Consider always rendering an element with id="title" outside the overridable header slot (or conditionally switch to aria-label/aria-labelledby based on slotted content), and/or document that custom header content must include an element with id="title" to preserve labeling.

Copilot uses AI. Check for mistakes.
Comment on lines 210 to +216
#onClose(): void {
// Reenable scrolling after the dialog has closed
document.documentElement.style.overflow = '';

this.inert = true;

this.closeEvent.emit();
Copy link

Copilot AI Apr 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

#onClose() always resets document.documentElement.style.overflow to an empty string, but show() (non-modal) never sets it. This can inadvertently clear an app’s pre-existing overflow style when the drawer was opened non-modally. Track whether scrolling was actually locked (or store/restore the previous overflow value) and only restore it when the drawer set it.

Copilot uses AI. Check for mistakes.
Comment on lines +12 to +15
flex-direction: column;
margin: 0;
overflow: hidden;
padding: 0;
Copy link

Copilot AI Apr 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new dialog styles rely on inset but no longer set an explicit positioning context (e.g. position: fixed). The old implementation used position: fixed; without it, non-modal show() drawers can end up positioned relative to the initial containing block and scroll with the page depending on UA styles. Explicitly set position: fixed (and any required inset defaults) so drawers stay anchored to the viewport in both modal and non-modal modes.

Suggested change
flex-direction: column;
margin: 0;
overflow: hidden;
padding: 0;
flex-direction: column;
inset: auto;
margin: 0;
overflow: hidden;
padding: 0;
position: fixed;

Copilot uses AI. Check for mistakes.
Comment on lines +140 to +141
slot[name='title']::slotted(*),
slot[name='title'] ::slotted(*) {
Copy link

Copilot AI Apr 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

slot[name='title'] ::slotted(*) (with a space) is not a valid ::slotted() selector form and will be ignored by the browser; the preceding slot[name='title']::slotted(*) already covers the intended styling. Remove the invalid selector to avoid confusion and keep the stylesheet standards-compliant.

Suggested change
slot[name='title']::slotted(*),
slot[name='title'] ::slotted(*) {
slot[name='title']::slotted(*) {

Copilot uses AI. Check for mistakes.
Copilot AI review requested due to automatic review settings April 17, 2026 08:09
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 7 out of 8 changed files in this pull request and generated 3 comments.

Comment on lines +127 to 138
/** Show the drawer as a modal, in the top layer, with a backdrop. */
showModal(): void {
if (this.dialog?.open) {
return;
}

this.inert = false;
this.dialog?.showModal();

// Disable scrolling while the dialog is open
// Disable scrolling while the drawer is open
document.documentElement.style.overflow = 'hidden';
}
Copy link

Copilot AI Apr 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

showModal() (and similarly show()) can be called before the first render when this.dialog is still undefined. In that case this method still sets inert = false and locks document.documentElement.style.overflow = 'hidden', leaving the page unscrollable even though no dialog opened (and disconnectedCallback() won’t reset it because dialog?.open is false). Consider guarding on !this.dialog (return early) or deferring the open until updateComplete so scroll locking only happens after the <dialog> exists and is actually shown.

Copilot uses AI. Check for mistakes.
});

it('should close the drawer when the cancel event is fired and close isn`t disabled', async () => {
it('should emit sl-cancel when the cancel event is fired and close isn`t disabled', async () => {
Copy link

Copilot AI Apr 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Test description uses isn\t` (backtick) instead of the apostrophe in “isn't”, which reads like a typo in the test output. Consider renaming the test to use a proper apostrophe (or avoid contractions).

Suggested change
it('should emit sl-cancel when the cancel event is fired and close isn`t disabled', async () => {
it('should emit sl-cancel when the cancel event is fired and close is not disabled', async () => {

Copilot uses AI. Check for mistakes.
*/
close(returnValue?: string): void {
if (this.dialog?.open) {
this.dialog.close(returnValue);
Copy link

Copilot AI Apr 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

close(returnValue?: string) currently always forwards one argument to HTMLDialogElement.close(). If returnValue is undefined (e.g. drawer.close() or close(button.getAttribute(...) || undefined)), WebIDL will coerce it to the string "undefined" and set dialog.returnValue unexpectedly. Consider only passing an argument when a real string is provided (otherwise call dialog.close() with no args).

Suggested change
this.dialog.close(returnValue);
if (returnValue === undefined) {
this.dialog.close();
} else {
this.dialog.close(returnValue);
}

Copilot uses AI. Check for mistakes.
@github-actions
Copy link
Copy Markdown
Contributor

🕸 Website preview

You can view a preview here (commit eee2475994e3cd9a80107407aadb107fbb7db772).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants