Skip to content

Commit fad979f

Browse files
committed
Preventing the banner from obscuring focus
When an element is focused under the banner, we're moving the banner out of the way, either by scrolling the page or by moving it. WCAG 2.4.12
1 parent 605973e commit fad979f

4 files changed

Lines changed: 94 additions & 8 deletions

File tree

src/ui/themes/dsfr/Banner.tsx

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
import {useTranslations} from '../../utils/hooks';
2-
import {template} from '../../utils/template';
1+
import {useRef} from 'preact/hooks';
32
import type {BannerComponent} from '../../components/types/Banner';
3+
import {useNonObscuringElement, useTranslations} from '../../utils/hooks';
4+
import {template} from '../../utils/template';
45

56
const Banner: BannerComponent = ({
67
needsUpdate,
@@ -11,10 +12,13 @@ const Banner: BannerComponent = ({
1112
onDecline,
1213
onConfigure
1314
}) => {
15+
const ref = useRef<HTMLDivElement>();
1416
const t = useTranslations();
1517

18+
useNonObscuringElement(ref);
19+
1620
return (
17-
<div className="fr-consent-banner" aria-hidden={isHidden}>
21+
<div className="fr-consent-banner" aria-hidden={isHidden} ref={ref}>
1822
{t.banner.title ? <h2 className="fr-h6">{t.banner.title}</h2> : null}
1923

2024
<div className="fr-consent-banner__content">

src/ui/themes/standard/Banner.tsx

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1+
import {useRef} from 'preact/hooks';
2+
import {BannerComponent} from '../../components/types/Banner';
13
import {imageAttributes} from '../../utils/config';
2-
import {useTranslations} from '../../utils/hooks';
4+
import {useNonObscuringElement, useTranslations} from '../../utils/hooks';
35
import {template} from '../../utils/template';
4-
import {BannerComponent} from '../../components/types/Banner';
56

67
const Banner: BannerComponent = ({
78
isHidden,
@@ -13,11 +14,14 @@ const Banner: BannerComponent = ({
1314
onDecline: onDeclineRequest,
1415
onConfigure: onConfigRequest
1516
}) => {
17+
const bodyRef = useRef<HTMLDivElement>();
1618
const t = useTranslations();
1719

20+
useNonObscuringElement(bodyRef);
21+
1822
return (
1923
<div aria-hidden={isHidden} className="orejime-Banner">
20-
<div className="orejime-Banner-body">
24+
<div className="orejime-Banner-body" ref={bodyRef}>
2125
{logo && (
2226
<div className="orejime-Banner-logoContainer">
2327
<img

src/ui/utils/dom.ts

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,3 +53,66 @@ export const findFirstFocusableChild = (element: HTMLElement) =>
5353
element.querySelector<HTMLElement>(
5454
'a[href], button:not([disabled]):not([aria-hidden]), [tabindex]:not([tabindex^="-"])'
5555
);
56+
57+
// Translates an element vertically by a given offset,
58+
// relatively to its current translation.
59+
const translateElementY = (element: HTMLElement, offset: number) => {
60+
const base = Number(element.dataset.translation) || 0;
61+
const translation = base + offset;
62+
63+
if (translation > 0) {
64+
element.dataset.translation = translation.toString();
65+
element.style.transform = `translateY(${translation}px)`;
66+
} else {
67+
delete element.dataset.translation;
68+
element.style.transform = '';
69+
}
70+
};
71+
72+
// Resolves a visual collision between two elements, either
73+
// by scrolling or hiding one of them.
74+
// We're only resolving collisions on the vertical axis, as
75+
// it is the main direction of web pages.
76+
export const resolveCollision = (fixed: HTMLElement, mobile: HTMLElement) => {
77+
if (mobile.contains(fixed)) {
78+
translateElementY(mobile, 0);
79+
return;
80+
}
81+
82+
const fixedRect = fixed.getBoundingClientRect();
83+
const mobileRect = mobile.getBoundingClientRect();
84+
const isColliding =
85+
mobileRect.left < fixedRect.right
86+
&& mobileRect.right > fixedRect.x
87+
&& mobileRect.top < fixedRect.bottom
88+
&& mobileRect.bottom > fixedRect.top;
89+
90+
const mobileCenterY = mobileRect.top + mobileRect.height / 2;
91+
const direction = mobileCenterY > window.innerHeight / 2 ? 1 : -1;
92+
const overlap =
93+
direction > 0
94+
? fixedRect.bottom - mobileRect.top
95+
: mobileRect.bottom - fixedRect.top;
96+
97+
if (!isColliding) {
98+
translateElementY(mobile, overlap);
99+
return;
100+
}
101+
102+
const doc = document.documentElement;
103+
const leeway =
104+
direction > 0
105+
? Math.abs(doc.scrollHeight - doc.clientHeight - doc.scrollTop)
106+
: doc.scrollTop;
107+
108+
// We're scrolling as much possible first.
109+
window.scrollBy({
110+
top: overlap * direction
111+
});
112+
113+
// If scrolling isn't enough to get out of trouble,
114+
// we're moving the mobile element out of the way.
115+
if (overlap > leeway) {
116+
translateElementY(mobile, (overlap - leeway) * direction);
117+
}
118+
};

src/ui/utils/hooks.ts

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
1-
import {useContext, useEffect, useState} from 'preact/hooks';
2-
import Context from '../components/Context';
1+
import {MutableRef, useContext, useEffect, useState} from 'preact/hooks';
32
import {Purpose} from '../../core/types';
43
import {
54
acceptedConsents,
65
areAllPurposesDisabled,
76
areAllPurposesEnabled,
87
declinedConsents
98
} from '../../core/utils/purposes';
9+
import Context from '../components/Context';
10+
import {resolveCollision} from './dom';
1011

1112
export const useConfig = () => {
1213
const {config} = useContext(Context);
@@ -107,3 +108,17 @@ export const useConsent = (
107108
const manager = useManager();
108109
return [manager.getConsent(id), manager.setConsent.bind(manager, id)];
109110
};
111+
112+
export const useNonObscuringElement = (ref: MutableRef<HTMLElement>): void => {
113+
useEffect(() => {
114+
const resolve = (event: FocusEvent) => {
115+
resolveCollision(event.target as HTMLElement, ref.current);
116+
};
117+
118+
document.addEventListener('focusin', resolve);
119+
120+
return () => {
121+
document.removeEventListener('focusin', resolve);
122+
};
123+
}, [ref]);
124+
};

0 commit comments

Comments
 (0)