Files
integration/website/public/api/v1/gaufre.js
Emmanuel Pelletier e47ac65e80 gaufre: follow disclosure aria pattern
- add aria attributes on load with the gaufre api script so that people
already using la gaufre don't necessarely *have* to update their code
- add the aria patterns in given code examples/react components. In some
cases, our small page load JS code isn't enough: for example on SPAs
where gaufre buttons might be loaded after page load.

thanks @inseo
2025-04-17 17:27:58 +02:00

228 lines
7.5 KiB
JavaScript

;(function () {
const BUTTON_CLASS = "js-lasuite-gaufre-btn"
const POPUP_ID = "lasuite-gaufre-popup"
const DIMENSIONS = { width: 304, height: 352, margin: 8 }
let lastFocusedButton = null
window.document.documentElement.classList.add("lasuite--gaufre-loaded")
if ("requestIdleCallback" in window) {
requestIdleCallback(() => {
appendPopup()
enhanceButtonsA11y()
})
}
const enhanceButtonsA11y = () => {
const buttons = document.querySelectorAll(`.${BUTTON_CLASS}`)
buttons.forEach((b) => {
b.setAttribute("aria-controls", POPUP_ID)
b.setAttribute("aria-expanded", "false")
})
}
document.body.addEventListener("click", (event) => {
if (!event.target.classList || !event.target.classList.contains(BUTTON_CLASS)) {
hidePopup()
return
}
const button = event.target
button.classList.toggle("lasuite--gaufre-opened")
if (button.classList.contains("lasuite--gaufre-opened")) {
showPopup(button)
} else {
hidePopup()
}
})
document.addEventListener("keyup", (event) => {
if (event.key === "Escape" && document.activeElement.closest(".lagaufre")) {
hidePopup()
}
})
window.addEventListener("resize", () => {
const popup = document.querySelector(`#${POPUP_ID}.lasuite--gaufre-opened`)
if (!popup) {
return
}
const button = document.querySelector(`.${BUTTON_CLASS}.lasuite--gaufre-opened`)
if (!button) {
return
}
updatePopupStyle(popup, button)
})
const appendPopup = () => {
if (document.getElementById(POPUP_ID)) {
return Promise.resolve(document.getElementById(POPUP_ID))
}
const scriptTag = document.querySelector(`#lasuite-gaufre-script`)
if (!scriptTag) {
console.log(
"La Suite numérique: Gaufre script tag not found, make sure the script has id 'lasuite-gaufre-script'.",
)
return
}
const popup = document.createElement("div")
popup.id = POPUP_ID
popup.style.cssText = "display: none !important"
const { host, protocol, searchParams, origin } = new URL(scriptTag.src)
const local = searchParams.get("type") === "local"
const lang = ["en"].includes(searchParams.get("lang")) ? searchParams.get("lang") : null
return fetch(
`${protocol}//${host}/api/v1/${(!!lang && `${lang}/`) || ""}gaufre${(!!local && "/local") || ""}`,
)
.then((res) => res.text())
.then((html) => {
html = html.replace(/(src=|href=|url\()"\//g, `$1"${origin}/`)
const parser = new DOMParser()
const popupDocument = parser.parseFromString(html, "text/html")
popup.innerHTML = popupDocument.body.innerHTML
document.body.appendChild(popup)
return popup
})
}
const getPopupCoords = (button) => {
const buttonCoords = button.getBoundingClientRect()
const documentWidth = document.body.clientWidth
const spaceLeft = buttonCoords.left + window.scrollX
const spaceRight = documentWidth - buttonCoords.right + window.scrollX
const hasHorizontalSpace = spaceLeft > DIMENSIONS.width || spaceRight > DIMENSIONS.width
let modalMode = false
// by default, the popup is displayed anchored to the right of the button (taking space on the left)
let leftPos = buttonCoords.right - DIMENSIONS.width + document.documentElement.scrollLeft
let isAnchoredToRight = true
// if there is not enough space on the left, or if there is more space on the right,
// we anchor it to the left of the button, taking space on the right
if (spaceLeft < DIMENSIONS.width || spaceRight > spaceLeft) {
leftPos = buttonCoords.left + document.documentElement.scrollLeft
isAnchoredToRight = false
}
// if there is no space at all, we use a "modal" mode, taking all the screen space
if (!hasHorizontalSpace) {
modalMode = true
}
const spaceTop = buttonCoords.top
const spaceBottom = window.innerHeight - buttonCoords.bottom
const hasVerticalSpace = spaceTop > DIMENSIONS.height || spaceBottom > DIMENSIONS.height
// by default, the popup is displayed anchored to the bottom of the button (taking space on the bottom)
let topPos = buttonCoords.bottom + 8 + document.documentElement.scrollTop
// if there is not enough space on the bottom, or if there is more space on the top,
// we anchor it to the top of the button, taking space on the top
if (spaceBottom < DIMENSIONS.height || spaceTop > spaceBottom) {
topPos =
buttonCoords.top -
DIMENSIONS.height -
DIMENSIONS.margin +
document.documentElement.scrollTop
}
// if there is no space below or above the button, but there is space on the sides,
// we show the popup next to the button
if (!hasVerticalSpace && window.innerHeight > DIMENSIONS.height) {
topPos = (window.innerHeight - DIMENSIONS.height) / 2
leftPos = isAnchoredToRight
? leftPos - buttonCoords.width - DIMENSIONS.margin
: leftPos + buttonCoords.width + DIMENSIONS.margin
}
// if there is no space at all, we use a "modal" mode, taking all the screen space
if (!hasVerticalSpace && window.innerHeight <= DIMENSIONS.height) {
modalMode = true
}
return {
modalMode,
top: !modalMode ? topPos : null,
left: !modalMode ? leftPos : null,
}
}
const defaultPopupStyle = `
border: 0 !important;
display: block !important;
z-index: 100000;
box-sizing: border-box !important;
`
const getPopupPositionStyle = (coords) => {
if (coords.modalMode) {
return `
${defaultPopupStyle}
position: fixed !important;
top: 0;
left: 0;
bottom: 0;
right: 0;
width: 100%;
width: 100svw;
height: 100%;
height: 100svh;
margin: auto;
`
}
return `
${defaultPopupStyle}
position: absolute !important;
top: ${coords.top}px !important;
left: ${coords.left}px !important;
max-width: calc(100vw - 30px);
width: ${DIMENSIONS.width}px;
height: ${DIMENSIONS.height}px;
margin: 0;
`
}
const updatePopupStyle = (popup, button) => {
const popupCoords = getPopupCoords(button)
popup.style.cssText = getPopupPositionStyle(popupCoords)
popup.classList[popupCoords.modalMode ? "add" : "remove"]("lasuite--gaufre-modal")
window.document.documentElement.classList[popupCoords.modalMode ? "add" : "remove"](
"lasuite--gaufre-modal-opened",
)
}
const showPopup = (button) => {
let popup = document.getElementById(POPUP_ID)
const show = (el) => {
updatePopupStyle(el, button)
el.classList.add("lasuite--gaufre-opened")
button.setAttribute("aria-expanded", "true")
lastFocusedButton = button
setTimeout(() => {
el.querySelector(".js-lagaufre-keyboard-anchor").focus()
}, 0)
}
if (popup) {
show(popup)
} else {
appendPopup().then(show)
}
}
const hidePopup = () => {
const popup = document.getElementById(POPUP_ID)
if (popup) {
popup.style.cssText = "display: none !important"
popup.classList.remove("lasuite--gaufre-opened")
}
document.querySelectorAll(`.${BUTTON_CLASS}`).forEach((b) => {
b.classList.remove("lasuite--gaufre-opened")
b.setAttribute("aria-expanded", "false")
})
if (lastFocusedButton) {
lastFocusedButton.focus()
lastFocusedButton = null
}
window.document.documentElement.classList.remove("lasuite--gaufre-modal-opened")
}
})()