Skip to content

Commit

Permalink
Convert tag modal into drawer (#977)
Browse files Browse the repository at this point in the history
* change tag modal into drawer

* improve scroll handling

* teleport all side bar content

* improve naming

* fix focus trap in filter drawer
  • Loading branch information
sissbruecker authored Feb 2, 2025
1 parent 0d4c47e commit c5a300a
Show file tree
Hide file tree
Showing 14 changed files with 196 additions and 124 deletions.
4 changes: 2 additions & 2 deletions bookmarks/e2e/e2e_test_collapse_side_panel.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,13 @@ def setUp(self) -> None:
def assertSidePanelIsVisible(self):
expect(self.page.locator(".bookmarks-page .side-panel")).to_be_visible()
expect(
self.page.locator(".bookmarks-page [ld-tag-modal-trigger]")
self.page.locator(".bookmarks-page [ld-filter-drawer-trigger]")
).not_to_be_visible()

def assertSidePanelIsHidden(self):
expect(self.page.locator(".bookmarks-page .side-panel")).not_to_be_visible()
expect(
self.page.locator(".bookmarks-page [ld-tag-modal-trigger]")
self.page.locator(".bookmarks-page [ld-filter-drawer-trigger]")
).to_be_visible()

def test_side_panel_should_be_visible_by_default(self):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,39 +4,39 @@
from bookmarks.e2e.helpers import LinkdingE2ETestCase


class TagCloudModalE2ETestCase(LinkdingE2ETestCase):
class FilterDrawerE2ETestCase(LinkdingE2ETestCase):
def test_show_modal_close_modal(self):
self.setup_bookmark(tags=[self.setup_tag(name="cooking")])
self.setup_bookmark(tags=[self.setup_tag(name="hiking")])

with sync_playwright() as p:
page = self.open(reverse("bookmarks:index"), p)

# use smaller viewport to make tags button visible
# use smaller viewport to make filter button visible
page.set_viewport_size({"width": 375, "height": 812})

# open tag cloud modal
modal_trigger = page.locator(".content-area-header").get_by_role(
"button", name="Tags"
# open drawer
drawer_trigger = page.locator(".content-area-header").get_by_role(
"button", name="Filters"
)
modal_trigger.click()
drawer_trigger.click()

# verify modal is visible
modal = page.locator(".modal")
expect(modal).to_be_visible()
expect(modal.locator("h2")).to_have_text("Tags")
# verify drawer is visible
drawer = page.locator(".modal.drawer.filter-drawer")
expect(drawer).to_be_visible()
expect(drawer.locator("h2")).to_have_text("Filters")

# close with close button
modal.locator("button.close").click()
expect(modal).to_be_hidden()
drawer.locator("button.close").click()
expect(drawer).to_be_hidden()

# open modal again
modal_trigger.click()
# open drawer again
drawer_trigger.click()

# close with backdrop
backdrop = modal.locator(".modal-overlay")
backdrop = drawer.locator(".modal-overlay")
backdrop.click(position={"x": 0, "y": 0})
expect(modal).to_be_hidden()
expect(drawer).to_be_hidden()

def test_select_tag(self):
self.setup_bookmark(tags=[self.setup_tag(name="cooking")])
Expand All @@ -45,29 +45,29 @@ def test_select_tag(self):
with sync_playwright() as p:
page = self.open(reverse("bookmarks:index"), p)

# use smaller viewport to make tags button visible
# use smaller viewport to make filter button visible
page.set_viewport_size({"width": 375, "height": 812})

# open tag cloud modal
modal_trigger = page.locator(".content-area-header").get_by_role(
"button", name="Tags"
drawer_trigger = page.locator(".content-area-header").get_by_role(
"button", name="Filters"
)
modal_trigger.click()
drawer_trigger.click()

# verify tags are displayed
modal = page.locator(".modal")
unselected_tags = modal.locator(".unselected-tags")
drawer = page.locator(".modal.drawer.filter-drawer")
unselected_tags = drawer.locator(".unselected-tags")
expect(unselected_tags.get_by_text("cooking")).to_be_visible()
expect(unselected_tags.get_by_text("hiking")).to_be_visible()

# select tag
unselected_tags.get_by_text("cooking").click()

# open modal again
modal_trigger.click()
# open drawer again
drawer_trigger.click()

# verify tag is selected, other tag is not visible anymore
selected_tags = modal.locator(".selected-tags")
selected_tags = drawer.locator(".selected-tags")
expect(selected_tags.get_by_text("cooking")).to_be_visible()

expect(unselected_tags.get_by_text("cooking")).not_to_be_visible()
Expand Down
97 changes: 97 additions & 0 deletions bookmarks/frontend/behaviors/filter-drawer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import { Behavior, registerBehavior } from "./index";
import { ModalBehavior } from "./modal";
import { isKeyboardActive } from "./focus-utils";

class FilterDrawerTriggerBehavior extends Behavior {
constructor(element) {
super(element);

this.onClick = this.onClick.bind(this);

element.addEventListener("click", this.onClick);
}

destroy() {
this.element.removeEventListener("click", this.onClick);
}

onClick() {
const modal = document.createElement("div");
modal.classList.add("modal", "drawer", "filter-drawer");
modal.setAttribute("ld-filter-drawer", "");
modal.innerHTML = `
<div class="modal-overlay"></div>
<div class="modal-container" role="dialog" aria-modal="true">
<div class="modal-header">
<h2>Filters</h2>
<button class="close" aria-label="Close dialog">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" stroke-width="2"
stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M18 6l-12 12"></path>
<path d="M6 6l12 12"></path>
</svg>
</button>
</div>
<div class="modal-body">
<section class="content content-area"></div>
</div>
</div>
`;
document.body.querySelector(".modals").appendChild(modal);
}
}

class FilterDrawerBehavior extends ModalBehavior {
init() {
// Teleport content before creating focus trap, otherwise it will not detect
// focusable content elements
this.teleport();
super.init();
// Add active class to start slide-in animation
this.element.classList.add("active");
}

destroy() {
super.destroy();
// Always close on destroy to restore drawer content to original location
// before turbo caches DOM
this.doClose();
}

mapHeading(container, from, to) {
const headings = container.querySelectorAll(from);
headings.forEach((heading) => {
const newHeading = document.createElement(to);
newHeading.textContent = heading.textContent;
heading.replaceWith(newHeading);
});
}

teleport() {
const content = this.element.querySelector(".content");
const sidePanel = document.querySelector("section.side-panel");
content.append(...sidePanel.children);
this.mapHeading(content, "h2", "h3");
}

teleportBack() {
const sidePanel = document.querySelector("section.side-panel");
const content = this.element.querySelector(".content");
sidePanel.append(...content.children);
this.mapHeading(sidePanel, "h3", "h2");
}

doClose() {
super.doClose();
this.teleportBack();

// Try restore focus to drawer trigger
const restoreFocusElement =
document.querySelector("[ld-filter-drawer-trigger]") || document.body;
restoreFocusElement.focus({ focusVisible: isKeyboardActive() });
}
}

registerBehavior("ld-filter-drawer-trigger", FilterDrawerTriggerBehavior);
registerBehavior("ld-filter-drawer", FilterDrawerBehavior);
16 changes: 12 additions & 4 deletions bookmarks/frontend/behaviors/modal.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,7 @@ export class ModalBehavior extends Behavior {
this.closeButton.addEventListener("click", this.onClose);
document.addEventListener("keydown", this.onKeyDown);

this.setupInert();
this.focusTrap = new FocusTrapController(
element.querySelector(".modal-container"),
);
this.init();
}

destroy() {
Expand All @@ -30,18 +27,29 @@ export class ModalBehavior extends Behavior {
this.focusTrap.destroy();
}

init() {
this.setupInert();
this.focusTrap = new FocusTrapController(
this.element.querySelector(".modal-container"),
);
}

setupInert() {
// Inert all other elements on the page
document
.querySelectorAll("body > *:not(.modals)")
.forEach((el) => el.setAttribute("inert", ""));
// Lock scroll on the body
document.body.classList.add("scroll-lock");
}

clearInert() {
// Clear inert attribute from all elements to allow focus outside the modal again
document
.querySelectorAll("body > *")
.forEach((el) => el.removeAttribute("inert"));
// Remove scroll lock from the body
document.body.classList.remove("scroll-lock");
}

onKeyDown(event) {
Expand Down
76 changes: 0 additions & 76 deletions bookmarks/frontend/behaviors/tag-modal.js

This file was deleted.

4 changes: 2 additions & 2 deletions bookmarks/frontend/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,13 @@ import "./behaviors/bookmark-page";
import "./behaviors/bulk-edit";
import "./behaviors/clear-button";
import "./behaviors/confirm-button";
import "./behaviors/details-modal";
import "./behaviors/dropdown";
import "./behaviors/filter-drawer";
import "./behaviors/form";
import "./behaviors/details-modal";
import "./behaviors/global-shortcuts";
import "./behaviors/search-autocomplete";
import "./behaviors/tag-autocomplete";
import "./behaviors/tag-modal";

export { default as TagAutoComplete } from "./components/TagAutocomplete.svelte";
export { default as SearchAutoComplete } from "./components/SearchAutoComplete.svelte";
Expand Down
6 changes: 3 additions & 3 deletions bookmarks/styles/bookmark-page.css
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
grid-gap: var(--unit-9);
}

[ld-tag-modal-trigger] {
[ld-filter-drawer-trigger] {
display: none;
}

Expand All @@ -24,7 +24,7 @@
display: none;
}

[ld-tag-modal-trigger] {
[ld-filter-drawer-trigger] {
display: inline-block;
}
}
Expand All @@ -38,7 +38,7 @@
display: none;
}

[ld-tag-modal-trigger] {
[ld-filter-drawer-trigger] {
display: inline-block;
}
}
Expand Down
6 changes: 4 additions & 2 deletions bookmarks/styles/components.css
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@

/* Content area component */
section.content-area {
h2 {
h2,
h3 {
font-size: var(--font-size-lg);
}

Expand All @@ -14,7 +15,8 @@ section.content-area {
padding-bottom: var(--unit-2);
margin-bottom: var(--unit-4);

h2 {
h2,
h3 {
flex: 0 0 auto;
line-height: var(--unit-9);
margin: 0;
Expand Down
1 change: 0 additions & 1 deletion bookmarks/styles/theme/base.css
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ html {
font-size: var(--html-font-size);
line-height: var(--html-line-height);
-webkit-tap-highlight-color: transparent;
scrollbar-gutter: stable;
}

/* Reserve space for vert. scrollbar to avoid layout shifting when scrollbars are added */
Expand Down
Loading

0 comments on commit c5a300a

Please sign in to comment.