Initial implementation of collapsible sections and re-order feature (#355)

* Initial implementation

* Add support for reports section too

* UI Improvement

now it looks a lot nicer :)

* Remove duplicate section titles

* FIX malformed DIV

* Add accessibility and touch support

WCAG 2.1 Level AA Compliant
  - Keyboard operable (Success Criterion 2.1.1)
  - Focus visible (Success Criterion 2.4.7)
  - Name, Role, Value (Success Criterion 4.1.2)

 Screen Reader Support
  - Clear instructions in aria-label
  - Proper semantic roles
  - State changes announced via aria-grabbed

* Add proper UI for tab highlight

* Add keyboard support to collapse also

* FIX js errors

* Fix rabbit

* FIX we don't need the html

* FIX CSRF and error handling

* Simplify into one single DB migration

---------

Co-authored-by: Juan José Mata <juanjo.mata@gmail.com>
This commit is contained in:
soky srm
2025-11-26 17:51:38 +01:00
committed by GitHub
parent 6e6fce1737
commit db8353e895
21 changed files with 1179 additions and 83 deletions

View File

@@ -0,0 +1,97 @@
import { Controller } from "@hotwired/stimulus";
export default class extends Controller {
static targets = ["content", "chevron", "container", "button"];
static values = {
sectionKey: String,
collapsed: Boolean,
};
connect() {
if (this.collapsedValue) {
this.collapse(false);
}
}
toggle(event) {
event.preventDefault();
if (this.collapsedValue) {
this.expand();
} else {
this.collapse();
}
}
handleToggleKeydown(event) {
// Handle Enter and Space keys for keyboard accessibility
if (event.key === "Enter" || event.key === " ") {
event.preventDefault();
event.stopPropagation(); // Prevent section's keyboard handler from firing
this.toggle(event);
}
}
collapse(persist = true) {
this.contentTarget.classList.add("hidden");
this.chevronTarget.classList.add("rotate-180");
this.collapsedValue = true;
if (this.hasButtonTarget) {
this.buttonTarget.setAttribute("aria-expanded", "false");
}
if (persist) {
this.savePreference(true);
}
}
expand() {
this.contentTarget.classList.remove("hidden");
this.chevronTarget.classList.remove("rotate-180");
this.collapsedValue = false;
if (this.hasButtonTarget) {
this.buttonTarget.setAttribute("aria-expanded", "true");
}
this.savePreference(false);
}
async savePreference(collapsed) {
const preferences = {
collapsed_sections: {
[this.sectionKeyValue]: collapsed,
},
};
// Safely obtain CSRF token
const csrfToken = document.querySelector('meta[name="csrf-token"]');
if (!csrfToken) {
console.error(
"[Dashboard Section] CSRF token not found. Cannot save preferences.",
);
return;
}
try {
const response = await fetch("/dashboard/preferences", {
method: "PATCH",
headers: {
"Content-Type": "application/json",
"X-CSRF-Token": csrfToken.content,
},
body: JSON.stringify({ preferences }),
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
console.error(
"[Dashboard Section] Failed to save preferences:",
response.status,
errorData,
);
}
} catch (error) {
console.error(
"[Dashboard Section] Network error saving preferences:",
error,
);
}
}
}