✨ feature: Add independent chrome tint elements for iOS 26+ compatibility and enhance related tests
This commit is contained in:
@@ -4,26 +4,26 @@
|
||||
* Tint the browser chrome (status bar + home indicator area) to match
|
||||
* the site header / footer when those elements are in view.
|
||||
*
|
||||
* Implementation notes — what each browser honors:
|
||||
* How it works on each browser:
|
||||
*
|
||||
* - Chrome / older iOS Safari (≤18):
|
||||
* - Chrome and iOS ≤ 18:
|
||||
* <meta name="theme-color"> drives the top status bar tint. We
|
||||
* update it dynamically. (Ignored by iOS 26+, kept for compatibility.)
|
||||
* update it dynamically. (Ignored by iOS 26+, but kept for
|
||||
* compatibility with older browsers and Chrome on Android.)
|
||||
*
|
||||
* - iOS 26+ Safari:
|
||||
* The <meta name="theme-color"> tag is ignored. WebKit instead
|
||||
* samples the <body> background-color via a live observer, and
|
||||
* tints the chrome to match. We set body.style.backgroundColor
|
||||
* dynamically to the same color the meta tag would carry.
|
||||
* The <meta name="theme-color"> tag is ignored. Instead, iOS
|
||||
* samples the background-color of `position: fixed` elements
|
||||
* within 4px of the top or 3px of the bottom of the viewport
|
||||
* (at least 80% wide, at least 12px tall) and tints the chrome
|
||||
* to match. We set the inline `style.backgroundColor` of two
|
||||
* real fixed <div>s (`.chrome-tint--top` and
|
||||
* `.chrome-tint--bottom`) declared in header.php.
|
||||
*
|
||||
* - Bottom of the screen (home indicator area, behind the address bar):
|
||||
* A fixed pseudo-element filled with `env(safe-area-inset-bottom)`
|
||||
* takes its background from a body class that this module toggles
|
||||
* when the footer is in view. (iOS 26 only samples ONE color for
|
||||
* the entire chrome, so the bottom of the screen will share the
|
||||
* same color as the top — we don't get a true top/bottom split on
|
||||
* iOS 26. The CSS pseudo-element still serves as a no-op fallback
|
||||
* for browsers that do support per-region tinting.)
|
||||
* Importantly, iOS 26 only takes ONE color per region (top OR
|
||||
* bottom), so we can run the two regions independently. This
|
||||
* lets the top go header-color while the bottom is footer-color
|
||||
* when both elements are in view (e.g. on short pages).
|
||||
*
|
||||
* Colors:
|
||||
* - HEADER_COLOR (#032F46) is the darker end of the .site-header
|
||||
@@ -31,16 +31,15 @@
|
||||
* - FOOTER_COLOR (#102C45) is the sRGB equivalent of the
|
||||
* --color-cwc-blue-02 token used by .site-footer.
|
||||
*
|
||||
* "In view" = any pixel of the element overlaps the viewport.
|
||||
* "In view" = any pixel of the element overlaps the viewport.
|
||||
*
|
||||
* No-ops if neither the header nor footer is found, or if
|
||||
* IntersectionObserver isn't supported.
|
||||
* No-ops if either the header or footer is missing, or if
|
||||
* IntersectionObserver isn't supported (older browsers).
|
||||
*/
|
||||
|
||||
const HEADER_COLOR = '#032F46';
|
||||
const FOOTER_COLOR = '#102C45';
|
||||
const THEME_META_NAME = 'theme-color';
|
||||
const FOOTER_IN_VIEW_CLASS = 'footer-in-view';
|
||||
|
||||
function ensureThemeColorMeta() {
|
||||
let meta = document.head.querySelector(`meta[name="${THEME_META_NAME}"]`);
|
||||
@@ -61,21 +60,15 @@ function setThemeColor(color) {
|
||||
}
|
||||
}
|
||||
|
||||
function setBodyBackground(color) {
|
||||
// For iOS 26+: WebKit's live observer on <body> background-color
|
||||
// drives the chrome tint. Removing the inline style lets any CSS
|
||||
// background (or the default transparent) show through.
|
||||
function setFixedBackground(el, color) {
|
||||
if (!el) return;
|
||||
if (color == null) {
|
||||
document.body.style.removeProperty('background-color');
|
||||
el.style.removeProperty('background-color');
|
||||
} else {
|
||||
document.body.style.backgroundColor = color;
|
||||
el.style.backgroundColor = color;
|
||||
}
|
||||
}
|
||||
|
||||
function setBottomTintClass(active) {
|
||||
document.body.classList.toggle(FOOTER_IN_VIEW_CLASS, active);
|
||||
}
|
||||
|
||||
function initChromeTint() {
|
||||
if (typeof IntersectionObserver === 'undefined') return;
|
||||
|
||||
@@ -83,22 +76,23 @@ function initChromeTint() {
|
||||
const footer = document.querySelector('.site-footer');
|
||||
if (!header && !footer) return;
|
||||
|
||||
const topEl = document.querySelector('.chrome-tint--top');
|
||||
const bottomEl = document.querySelector('.chrome-tint--bottom');
|
||||
|
||||
// Top tint state. Truthy means the header is currently in view.
|
||||
let headerInView = false;
|
||||
// Bottom tint state. Truthy means the footer is currently in view.
|
||||
let footerInView = false;
|
||||
|
||||
const apply = () => {
|
||||
// Top tint is the header color ONLY when the header is in view.
|
||||
if (headerInView) {
|
||||
setThemeColor(HEADER_COLOR);
|
||||
setBodyBackground(HEADER_COLOR);
|
||||
} else {
|
||||
setThemeColor(null);
|
||||
setBodyBackground(null);
|
||||
}
|
||||
// Bottom tint is the footer color ONLY when the footer is in view.
|
||||
setBottomTintClass(footerInView);
|
||||
// Top tint = header color ONLY when the header is in view.
|
||||
const topColor = headerInView ? HEADER_COLOR : null;
|
||||
setFixedBackground(topEl, topColor);
|
||||
setThemeColor(topColor);
|
||||
|
||||
// Bottom tint = footer color ONLY when the footer is in view.
|
||||
const bottomColor = footerInView ? FOOTER_COLOR : null;
|
||||
setFixedBackground(bottomEl, bottomColor);
|
||||
};
|
||||
|
||||
const buildObserver = (el, onChange) => {
|
||||
|
||||
Reference in New Issue
Block a user