0
0
Fork 0
mirror of https://github.com/discourse/discourse.git synced 2026-08-08 17:53:55 +08:00
discourse/plugins/spoiler-alert/assets/javascripts/lib/apply-spoiler.js
Bannon Tanner 65d162b350
FIX: Allow re-blur on spoiler when in details view (#41266)
`INTERACTIVE_SELECTOR` includes "details" so that clicking a `<details>`
inside a spoiler expands it without also re-blurring the surrounding
spoiler. But because `closest()` walks up the DOM tree, every click
inside a `[spoiler]` that happens to be nested inside a `[details]` also
matches `closest("details")` against the parent `<details>` — making
such a spoiler impossible to re-blur once revealed.

Fix: scope the `closest()` result with `element.contains(...)` so only
interactive ancestors that live inside the spoiler block re-blurring.
Interactive ancestors above the spoiler are ignored.

Tests cover both directions: a spoiler-inside-details can re-blur, and
clicking an interactive descendant of a revealed spoiler still does not
re-blur it.

Context:
https://meta.discourse.org/t/spoilers-don-t-toggle-when-inside-details/194499
2026-06-29 12:47:49 -05:00

105 lines
2.4 KiB
JavaScript
Vendored

import { i18n } from "discourse-i18n";
const INTERACTIVE_SELECTOR = [
"a",
"area",
"audio",
"button",
"details",
"embed",
"iframe",
"img.animated",
"input",
"map",
"object",
"option",
"portal",
"select",
"textarea",
"track",
"video",
".lightbox",
].join(", ");
function isInteractive(event, element) {
if (event.defaultPrevented) {
return true;
}
const interactive = event.target.closest(INTERACTIVE_SELECTOR);
return interactive && element.contains(interactive);
}
function noTextSelected() {
return window.getSelection() + "" === "";
}
function setAttributes(element, attributes) {
Object.entries(attributes).forEach(([key, value]) => {
if (value === null) {
element.removeAttribute(key);
} else {
element.setAttribute(key, value);
}
});
}
function _setSpoilerHidden(element) {
const spoilerHiddenAttributes = {
role: "button",
tabindex: "0",
"data-spoiler-state": "blurred",
"aria-expanded": false,
"aria-label": i18n("spoiler.label.show"),
"aria-live": "polite",
};
// Set default attributes & classes on spoiler
setAttributes(element, spoilerHiddenAttributes);
element.classList.add("spoiler-blurred");
// Set aria-hidden for all children of the spoiler
Array.from(element.children).forEach((e) => {
e.setAttribute("aria-hidden", true);
});
}
function _setSpoilerVisible(element) {
const spoilerVisibleAttributes = {
"data-spoiler-state": "revealed",
"aria-expanded": true,
"aria-label": null,
role: null,
};
// Set attributes & classes for when spoiler is visible
setAttributes(element, spoilerVisibleAttributes);
element.classList.remove("spoiler-blurred");
// Remove aria-hidden for all children of the spoiler when visible
Array.from(element.children).forEach((e) => {
e.removeAttribute("aria-hidden");
});
}
function toggleSpoiler(event, element) {
if (element.getAttribute("data-spoiler-state") === "blurred") {
_setSpoilerVisible(element);
event.preventDefault();
} else if (!isInteractive(event, element) && noTextSelected()) {
_setSpoilerHidden(element);
}
}
export default function applySpoiler(element) {
_setSpoilerHidden(element);
element.addEventListener("click", (event) => {
toggleSpoiler(event, element);
});
element.addEventListener("keydown", (event) => {
if (event.key === "Enter") {
toggleSpoiler(event, element);
}
});
}