mirror of
https://github.com/discourse/discourse.git
synced 2026-08-08 17:53:55 +08:00
`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
105 lines
2.4 KiB
JavaScript
Vendored
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);
|
|
}
|
|
});
|
|
}
|