diff --git a/bookmarks/e2e/e2e_test_collapse_side_panel.py b/bookmarks/e2e/e2e_test_collapse_side_panel.py
index 407b4f7..a5b03d9 100644
--- a/bookmarks/e2e/e2e_test_collapse_side_panel.py
+++ b/bookmarks/e2e/e2e_test_collapse_side_panel.py
@@ -12,13 +12,13 @@ class CollapseSidePanelE2ETestCase(LinkdingE2ETestCase):
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):
diff --git a/bookmarks/e2e/e2e_test_tag_cloud_modal.py b/bookmarks/e2e/e2e_test_filter_drawer.py
similarity index 56%
rename from bookmarks/e2e/e2e_test_tag_cloud_modal.py
rename to bookmarks/e2e/e2e_test_filter_drawer.py
index 7d60dcc..aabdd49 100644
--- a/bookmarks/e2e/e2e_test_tag_cloud_modal.py
+++ b/bookmarks/e2e/e2e_test_filter_drawer.py
@@ -4,7 +4,7 @@ from playwright.sync_api import sync_playwright, expect
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")])
@@ -12,31 +12,31 @@ class TagCloudModalE2ETestCase(LinkdingE2ETestCase):
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")])
@@ -45,29 +45,29 @@ class TagCloudModalE2ETestCase(LinkdingE2ETestCase):
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()
diff --git a/bookmarks/frontend/behaviors/filter-drawer.js b/bookmarks/frontend/behaviors/filter-drawer.js
new file mode 100644
index 0000000..8645b73
--- /dev/null
+++ b/bookmarks/frontend/behaviors/filter-drawer.js
@@ -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 = `
+
+
+
+ `;
+ 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);
diff --git a/bookmarks/frontend/behaviors/modal.js b/bookmarks/frontend/behaviors/modal.js
index 78d788c..bfad237 100644
--- a/bookmarks/frontend/behaviors/modal.js
+++ b/bookmarks/frontend/behaviors/modal.js
@@ -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() {
@@ -30,11 +27,20 @@ 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() {
@@ -42,6 +48,8 @@ export class ModalBehavior extends Behavior {
document
.querySelectorAll("body > *")
.forEach((el) => el.removeAttribute("inert"));
+ // Remove scroll lock from the body
+ document.body.classList.remove("scroll-lock");
}
onKeyDown(event) {
diff --git a/bookmarks/frontend/behaviors/tag-modal.js b/bookmarks/frontend/behaviors/tag-modal.js
deleted file mode 100644
index f515d5d..0000000
--- a/bookmarks/frontend/behaviors/tag-modal.js
+++ /dev/null
@@ -1,76 +0,0 @@
-import { Behavior, registerBehavior } from "./index";
-import { ModalBehavior } from "./modal";
-import { isKeyboardActive } from "./focus-utils";
-
-class TagModalTriggerBehavior 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() {
- // Creates a new modal and teleports the tag cloud into it
- const modal = document.createElement("div");
- modal.classList.add("modal", "active");
- modal.setAttribute("ld-tag-modal", "");
- modal.innerHTML = `
-
-
- `;
- modal.addEventListener("modal:close", this.onClose);
-
- modal.tagCloud = document.querySelector(".tag-cloud");
- modal.tagCloudContainer = modal.tagCloud.parentElement;
-
- const content = modal.querySelector(".content");
- content.appendChild(modal.tagCloud);
-
- document.body.querySelector(".modals").appendChild(modal);
- }
-}
-
-class TagModalBehavior extends ModalBehavior {
- destroy() {
- super.destroy();
- // Always close on destroy to restore tag cloud to original parent before
- // turbo caches DOM
- this.doClose();
- }
-
- doClose() {
- super.doClose();
-
- // Restore tag cloud to original parent
- this.element.tagCloudContainer.appendChild(this.element.tagCloud);
-
- // Try restore focus to tag cloud trigger
- const restoreFocusElement =
- document.querySelector("[ld-tag-modal-trigger]") || document.body;
- restoreFocusElement.focus({ focusVisible: isKeyboardActive() });
- }
-}
-
-registerBehavior("ld-tag-modal-trigger", TagModalTriggerBehavior);
-registerBehavior("ld-tag-modal", TagModalBehavior);
diff --git a/bookmarks/frontend/index.js b/bookmarks/frontend/index.js
index 789ff11..1f036bd 100644
--- a/bookmarks/frontend/index.js
+++ b/bookmarks/frontend/index.js
@@ -3,13 +3,13 @@ import "./behaviors/bookmark-page";
import "./behaviors/bulk-edit";
import "./behaviors/clear-button";
import "./behaviors/confirm-button";
-import "./behaviors/dropdown";
-import "./behaviors/form";
import "./behaviors/details-modal";
+import "./behaviors/dropdown";
+import "./behaviors/filter-drawer";
+import "./behaviors/form";
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";
diff --git a/bookmarks/styles/bookmark-page.css b/bookmarks/styles/bookmark-page.css
index 45f81e3..223ec49 100644
--- a/bookmarks/styles/bookmark-page.css
+++ b/bookmarks/styles/bookmark-page.css
@@ -15,7 +15,7 @@
grid-gap: var(--unit-9);
}
- [ld-tag-modal-trigger] {
+ [ld-filter-drawer-trigger] {
display: none;
}
@@ -24,7 +24,7 @@
display: none;
}
- [ld-tag-modal-trigger] {
+ [ld-filter-drawer-trigger] {
display: inline-block;
}
}
@@ -38,7 +38,7 @@
display: none;
}
- [ld-tag-modal-trigger] {
+ [ld-filter-drawer-trigger] {
display: inline-block;
}
}
diff --git a/bookmarks/styles/components.css b/bookmarks/styles/components.css
index d625196..b3c6f87 100644
--- a/bookmarks/styles/components.css
+++ b/bookmarks/styles/components.css
@@ -2,7 +2,8 @@
/* Content area component */
section.content-area {
- h2 {
+ h2,
+ h3 {
font-size: var(--font-size-lg);
}
@@ -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;
diff --git a/bookmarks/styles/theme/base.css b/bookmarks/styles/theme/base.css
index 4bd64d8..4a269b8 100644
--- a/bookmarks/styles/theme/base.css
+++ b/bookmarks/styles/theme/base.css
@@ -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 */
diff --git a/bookmarks/styles/theme/modals.css b/bookmarks/styles/theme/modals.css
index b5db0fb..102b663 100644
--- a/bookmarks/styles/theme/modals.css
+++ b/bookmarks/styles/theme/modals.css
@@ -62,13 +62,14 @@
gap: var(--unit-4);
max-height: 75vh;
max-width: var(--control-width-md);
- padding: var(--unit-6);
width: 100%;
& .modal-header {
display: flex;
align-items: flex-start;
gap: var(--unit-2);
+ padding: var(--unit-6);
+ padding-bottom: 0;
color: var(--text-color);
& h2 {
@@ -95,10 +96,53 @@
& .modal-body {
overflow-y: auto;
- position: relative;
+ padding: 0 var(--unit-6);
+ }
+
+ & .modal-body:not(:has(+ .modal-footer)) {
+ margin-bottom: var(--unit-6);
}
& .modal-footer {
+ padding: var(--unit-6);
+ padding-top: 0;
text-align: right;
}
}
+
+.modal.drawer {
+ display: block;
+
+ & .modal-container {
+ position: fixed;
+ top: 0;
+ right: 0;
+ width: 400px;
+ max-width: 100%;
+ height: 100%;
+ max-height: 100%;
+ border: none;
+ border-left: solid 1px var(--modal-container-border-color);
+ border-radius: 0;
+ transform: translateX(100%);
+ animation: fade-in 0.25s ease 1;
+ transition: transform 0.25s ease;
+ }
+
+ &.active {
+ & .modal-container {
+ transform: translateX(0);
+ }
+ }
+
+ &.active.closing {
+ & .modal-container {
+ animation: fade-out 0.25s ease 1;
+ transform: translateX(100%);
+ }
+ }
+}
+
+.scroll-lock {
+ overflow: hidden !important;
+}
diff --git a/bookmarks/templates/bookmarks/archive.html b/bookmarks/templates/bookmarks/archive.html
index 4ae8dee..d7a6a22 100644
--- a/bookmarks/templates/bookmarks/archive.html
+++ b/bookmarks/templates/bookmarks/archive.html
@@ -14,8 +14,7 @@
diff --git a/bookmarks/templates/bookmarks/index.html b/bookmarks/templates/bookmarks/index.html
index 7cb6c9a..e17f5e6 100644
--- a/bookmarks/templates/bookmarks/index.html
+++ b/bookmarks/templates/bookmarks/index.html
@@ -14,7 +14,7 @@
diff --git a/bookmarks/templates/bookmarks/shared.html b/bookmarks/templates/bookmarks/shared.html
index a9195de..bc40d06 100644
--- a/bookmarks/templates/bookmarks/shared.html
+++ b/bookmarks/templates/bookmarks/shared.html
@@ -13,8 +13,7 @@
Shared bookmarks
diff --git a/bookmarks/templates/settings/general.html b/bookmarks/templates/settings/general.html
index 866d0fa..427ca33 100644
--- a/bookmarks/templates/settings/general.html
+++ b/bookmarks/templates/settings/general.html
@@ -127,11 +127,11 @@