gaufre: fix popup placement issues, simplify styling

this is done following up Tchap integration.

- the popup placement script was really dumb and assumed the gaufre
button was always placed at the top right of the page. Tchap can't do
that and uses it at the bottom left. Now the popup places itself
correctly wherever the button is on the page.
On mobile now we have a "modal" mode for the popup where it takes all
the viewport.
- Tchap uses the gaufre inside their own popup component. This was not
something we handled before. Now you can set up a
'lasuite--gaufre-borderless' class on your html or body tag so that the
gaufre doesn't render its box shadow or blue border, making it easier to
integrate in a already made popup.
This commit is contained in:
Emmanuel Pelletier
2024-06-14 15:21:40 +02:00
parent b2b8588ac6
commit 62b74a5445
4 changed files with 246 additions and 86 deletions

View File

@@ -1,5 +1,7 @@
;(function () { ;(function () {
const BUTTON_CLASS = "js-lasuite-gaufre-btn" const BUTTON_CLASS = "js-lasuite-gaufre-btn"
const DIMENSIONS = { width: 304, height: 352, margin: 8 }
let lastFocusedButton = null let lastFocusedButton = null
window.document.documentElement.classList.add("lasuite--gaufre-loaded") window.document.documentElement.classList.add("lasuite--gaufre-loaded")
@@ -42,7 +44,7 @@
if (!button) { if (!button) {
return return
} }
popup.style.cssText = getPopupPositionStyle(button) updatePopupStyle(popup, button)
}) })
const appendPopup = () => { const appendPopup = () => {
@@ -58,8 +60,6 @@
} }
const popup = document.createElement("div") const popup = document.createElement("div")
popup.id = "lasuite-gaufre-popup" popup.id = "lasuite-gaufre-popup"
popup.width = "304"
popup.height = "360"
popup.style.cssText = "display: none !important" popup.style.cssText = "display: none !important"
const { host, protocol, searchParams, origin } = new URL(scriptTag.src) const { host, protocol, searchParams, origin } = new URL(scriptTag.src)
@@ -79,35 +79,111 @@
}) })
} }
const getPopupPositionStyle = (button) => { const getPopupCoords = (button) => {
const buttonCoords = button.getBoundingClientRect() const buttonCoords = button.getBoundingClientRect()
const isSmallScreen = window.innerWidth <= 400
let leftPos = buttonCoords.right - 304 + document.documentElement.scrollLeft const documentWidth = document.body.clientWidth
leftPos = leftPos < 5 ? 5 : leftPos const spaceLeft = buttonCoords.left + window.scrollX
return ` const spaceRight = documentWidth - buttonCoords.right + window.scrollX
position: absolute !important; const hasHorizontalSpace = spaceLeft > DIMENSIONS.width || spaceRight > DIMENSIONS.width
top: ${buttonCoords.top + buttonCoords.height + 8}px; let modalMode = false
${
isSmallScreen // 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
left: 5px; let isAnchoredToRight = true
right: 5px; // if there is not enough space on the left, or if there is more space on the right,
margin: 0 auto; // 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: 100vw;
height: 100vh;
margin: auto;
` `
: ` }
left: ${leftPos}px; return `
width: 304px;` ${defaultPopupStyle}
} position: absolute !important;
border: 0 !important; top: ${coords.top}px !important;
display: block !important; left: ${coords.left}px !important;
z-index: 100000; 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) => { const showPopup = (button) => {
let popup = document.querySelector(`#lasuite-gaufre-popup`) let popup = document.querySelector(`#lasuite-gaufre-popup`)
const show = (el) => { const show = (el) => {
el.style.cssText = getPopupPositionStyle(button) updatePopupStyle(popup, button)
el.classList.add("lasuite--gaufre-opened") el.classList.add("lasuite--gaufre-opened")
lastFocusedButton = button lastFocusedButton = button
setTimeout(() => { setTimeout(() => {
@@ -132,5 +208,6 @@
lastFocusedButton.focus() lastFocusedButton.focus()
lastFocusedButton = null lastFocusedButton = null
} }
window.document.documentElement.classList.remove("lasuite--gaufre-modal-opened")
} }
})() })()

View File

@@ -42,22 +42,62 @@ const { services } = Astro.props
} }
.lagaufre { .lagaufre {
height: 22rem !important; --lagaufre-border-color: #01018f !important;
max-height: 22rem !important; --lagaufre-bg-color: #fff !important;
height: 100% !important;
width: 100% !important;
font-size: 100% !important; font-size: 100% !important;
font-family: "La Gaufre", "La Gaufre fallback", BlinkMacSystemFont, "Segoe UI", font-family: "La Gaufre", "La Gaufre fallback", BlinkMacSystemFont, "Segoe UI",
"Noto Sans", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji" !important; "Noto Sans", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji" !important;
margin: 0 0 0.5rem !important; margin: 0 0 8px !important;
background-color: #01018fcc !important; border: 2px solid var(--lagaufre-border-color) !important;
padding: 2px !important; background-color: var(--lagaufre-bg-color) !important;
width: 100% !important;
/* border-radius: 8px !important; */
filter: drop-shadow(0 4px 3px rgb(0 0 0 / 0.07)) drop-shadow(0 2px 2px rgb(0 0 0 / 0.06)) !important; filter: drop-shadow(0 4px 3px rgb(0 0 0 / 0.07)) drop-shadow(0 2px 2px rgb(0 0 0 / 0.06)) !important;
overflow: hidden !important; overflow: hidden !important;
display: flex !important;
flex-direction: column !important;
} }
[data-fr-scheme="dark"] .lagaufre { [data-fr-scheme="dark"] .lagaufre {
background-color: #8585f6 !important; --lagaufre-bg-color: #1e1e1e !important;
--lagaufre-border-color: #8585f6 !important;
}
.lasuite--gaufre-borderless .lagaufre {
border-color: var(--lagaufre-bg-color) !important;
filter: none !important;
margin-bottom: 0 !important;
}
.lasuite--gaufre-modal-opened body {
overflow: hidden !important;
}
.lasuite--gaufre-modal .lagaufre {
filter: none !important;
border: 0 !important;
padding: 0 !important;
margin: 0 !important;
font-weight: bold !important;
}
.lagaufre-close-button-container {
display: flex !important;
justify-content: flex-end !important;
}
.lagaufre-close-button {
display: none !important;
padding: 8px !important;
margin: 4px !important;
border: 1px solid rgb(221, 221, 221) !important;
color: rgb(0, 0, 145) !important;
font-weight: bold !important;
background: none !important;
}
.lasuite--gaufre-modal .lagaufre-close-button {
display: block !important;
} }
.lagaufre-sr-only { .lagaufre-sr-only {
@@ -73,31 +113,20 @@ const { services } = Astro.props
display: block !important; display: block !important;
} }
.lagaufre-border {
height: 100% !important;
/* border-radius: 6px !important; */
overflow: hidden !important;
}
.lagaufre-list { .lagaufre-list {
margin: 0 !important; margin: 0 !important;
padding: 0 !important; padding: 0 !important;
list-style: none !important; list-style: none !important;
/* border-radius: 2px !important; */ /* border-radius: 2px !important; */
background-color: white !important;
height: 100% !important; height: 100% !important;
overflow: auto !important; overflow: auto !important;
} }
[data-fr-scheme="dark"] .lagaufre-list {
background-color: #1e1e1e !important;
}
.lagaufre-service { .lagaufre-service {
position: relative !important; position: relative !important;
display: flex !important; display: flex !important;
align-items: center !important; align-items: center !important;
padding: 1rem 2rem !important; padding: 16px 32px !important;
border-top: 1px solid transparent !important; border-top: 1px solid transparent !important;
border-bottom: 1px solid transparent !important; border-bottom: 1px solid transparent !important;
} }
@@ -139,6 +168,7 @@ const { services } = Astro.props
} }
.lagaufre-service__icon { .lagaufre-service__icon {
flex-shrink: 0 !important;
display: flex !important; display: flex !important;
align-items: center !important; align-items: center !important;
width: 40px !important; width: 40px !important;
@@ -157,7 +187,7 @@ const { services } = Astro.props
} }
.lagaufre-service__name { .lagaufre-service__name {
margin-left: 1.5rem !important; margin-left: 24px !important;
text-decoration: none !important; text-decoration: none !important;
color: #161616 !important; color: #161616 !important;
} }
@@ -197,47 +227,50 @@ const { services } = Astro.props
background-color: transparent !important; background-color: transparent !important;
} }
</style> </style>
<div class="lagaufre-border"> <h1 id="lagaufre-title" class="lagaufre-sr-only">Liste des services de La Suite numérique</h1>
<h1 id="lagaufre-title" class="lagaufre-sr-only"> <div class="lagaufre-close-button-container">
Liste des services de La Suite numérique <button type="button" class="lagaufre-close-button">
</h1> <span class="lagaufre-sr-only">Fermer la liste des services</span>
<ul <span aria-hidden="true">✕</span>
class="lagaufre-list lagaufre-scrollbars js-lagaufre-keyboard-anchor" <span>Fermer</span>
aria-labelledby="lagaufre-title" </button>
tabindex="-1"
>
{
services
.filter(({ enabled }) => !!enabled)
.map(({ id, name, url }, i) => {
const logo =
logos[`/src/assets/logos/${id}.svg`] ||
logos[`/src/assets/logos/${id}.jpg`] ||
logos[`/src/assets/logos/${id}.png`]
return (
<li>
<div class="lagaufre-service lagaufre-enlarge-link">
<div class="lagaufre-service__icon">
{!!logo ? (
<Image src={logo()} width="40" height="40" alt="" loading="eager" />
) : null}
</div>
<a
target="_parent"
class="lagaufre-service__name"
href={url}
id={`lagaufre-service-${id}`}
{...((i === 0 && { autofocus: true }) || {})}
>
{name}
</a>
</div>
</li>
)
})
}
</ul>
</div> </div>
<ul
class="lagaufre-list lagaufre-scrollbars js-lagaufre-keyboard-anchor"
aria-labelledby="lagaufre-title"
tabindex="-1"
>
{
services
.filter(({ enabled }) => !!enabled)
.map(({ id, name, url }, i) => {
const logo =
logos[`/src/assets/logos/${id}.svg`] ||
logos[`/src/assets/logos/${id}.jpg`] ||
logos[`/src/assets/logos/${id}.png`]
return (
<li>
<div class="lagaufre-service lagaufre-enlarge-link">
<div class="lagaufre-service__icon">
{!!logo ? (
<Image src={logo()} width="40" height="40" alt="" loading="eager" />
) : null}
</div>
<a
target="_parent"
class="lagaufre-service__name"
href={url}
id={`lagaufre-service-${id}`}
{...((i === 0 && { autofocus: true }) || {})}
>
{name}
</a>
</div>
</li>
)
})
}
</ul>
</div> </div>
</body> </body>
</html> </html>

View File

@@ -167,6 +167,31 @@ burger : aucun HTML n'est proposé pour ça, tant il dépend de votre site.
même comportement sur votre propre implémentation, vous pouvez vous appuyer sur la présence de la même comportement sur votre propre implémentation, vous pouvez vous appuyer sur la présence de la
classe `lasuite--gaufre-loaded` sur la balise `html`. classe `lasuite--gaufre-loaded` sur la balise `html`.
### Popup customisée
Au delà du bouton, suivant votre stack technique, vous voudrez peut-être utiliser un composant de
popup déjà en place dans votre app. Dans ce cas, une solution est de ne pas charger le script
proposé, et de gérer vous-même le comportement du bouton.
Le contenu à afficher dans la popup est requêtable directement sur
[{import.meta.env.PUBLIC_LASUITE_API_URL}/api/v1/gaufre](/api/v1/gaufre). La liste est déjà
construite pour vous en HTML/CSS car son affichage est amené à évoluer au fil du temps suivant le
nombre de services. Il n'est pas souhaitable de demander à chaque service de La Suite de devoir se
mettre à jour régulièrement. L'idée est donc de requêter cette URL, récupérer son `<body>` et de
l'intégrer dans votre DOM dans le contenu de votre composant de popup.
Appliquez la classe `lasuite--gaufre-borderless` sur votre balise `html` ou `body`. Ceci fera que la
liste fournie via l'URL ci-dessus, une fois dans votre DOM, n'affichera pas de bordure bleue avec
une ombre, et s'intégrera mieux dans votre composant de popup.
:::caution
Il est vivement déconseillé de surcharger les CSS du contenu de la popup de la Gaufre, car les
contenus de la popup et leur disposition visuelle évoluent avec le temps.
[Contactez-nous](/about/help) si vous avez des besoins particuliers par rapport à votre service.
:::
## Exemple ## Exemple
[La Gaufre : exemple HTML](/examples/gaufre/html). [La Gaufre : exemple HTML](/examples/gaufre/html).

View File

@@ -7,6 +7,7 @@ import gaufreCssUrl from "@gouvfr-lasuite/integration/dist/css/gaufre.css?url"
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<title>Exemple intégration bouton La Gaufre - La Suite numérique</title> <title>Exemple intégration bouton La Gaufre - La Suite numérique</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<style is:inline> <style is:inline>
.wrapper { .wrapper {
margin: 2rem auto; margin: 2rem auto;
@@ -30,6 +31,12 @@ import gaufreCssUrl from "@gouvfr-lasuite/integration/dist/css/gaufre.css?url"
.example > p { .example > p {
margin-top: 0; margin-top: 0;
} }
.example > .footer {
display: flex;
gap: 1em;
justify-content: flex-start;
font-style: italic;
}
</style> </style>
<!-- code à ajouter pour faire marcher le bouton Gaufre --> <!-- code à ajouter pour faire marcher le bouton Gaufre -->
@@ -60,6 +67,13 @@ import gaufreCssUrl from "@gouvfr-lasuite/integration/dist/css/gaufre.css?url"
</button> </button>
<!-- fin du code à copier --> <!-- fin du code à copier -->
</div> </div>
<button
type="button"
class="lasuite-gaufre-btn lasuite-gaufre-btn--vanilla js-lasuite-gaufre-btn"
title="Les services de La Suite numérique"
>
Les services de La Suite numérique
</button>
<p> <p>
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec ac diam a libero posuere Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec ac diam a libero posuere
ornare facilisis in mi. Nullam eu vulputate augue, in auctor nibh. Praesent ac tempus dui. ornare facilisis in mi. Nullam eu vulputate augue, in auctor nibh. Praesent ac tempus dui.
@@ -88,6 +102,17 @@ import gaufreCssUrl from "@gouvfr-lasuite/integration/dist/css/gaufre.css?url"
Vestibulum feugiat pulvinar fermentum. Vivamus imperdiet dapibus ornare. Donec venenatis, Vestibulum feugiat pulvinar fermentum. Vivamus imperdiet dapibus ornare. Donec venenatis,
lectus id faucibus tempus, sapien urna molestie augue, at egestas enim lectus quis nisi. lectus id faucibus tempus, sapien urna molestie augue, at egestas enim lectus quis nisi.
</p> </p>
<div class="footer">
<button
type="button"
class="lasuite-gaufre-btn lasuite-gaufre-btn--vanilla js-lasuite-gaufre-btn"
title="Les services de La Suite numérique"
>
Les services de La Suite numérique
</button>
<p>la popup s'affiche correctement quelque soit la position du bouton dans la fenêtre</p>
</div>
</div> </div>
</div> </div>
</body> </body>