@gitlab/ui
Version:
GitLab UI Components
96 lines (89 loc) • 4.08 kB
JavaScript
import { LIGHT_SCOPE_CLASS, DARK_SCOPE_CLASS, DARK_ROOT_CLASS } from '../../tokens/color_mode';
const SCOPE_CLASSES = [DARK_SCOPE_CLASS, LIGHT_SCOPE_CLASS, DARK_ROOT_CLASS];
const SCOPE_SELECTOR = SCOPE_CLASSES.map(scopeClass => `.${scopeClass}`).join(', ');
/**
* Whether the given element sits in dark mode.
*
* Colour mode is not a document-wide fact. Tokens are declared for the root dark class and for a
* dark scope class, so any subtree can be flipped independently, and a light scope can flip one
* back inside a dark document. The nearest scope wins, which is what `closest` gives us.
*
* This lives under `charts` rather than in the general utilities because charts are the only thing
* in the package that needs it. Everywhere else, colour follows tokens and CSS re-resolves them on
* its own. Charts cannot always do that: ECharts parses colours numerically for its visual maps,
* and canvas cannot read custom properties at all, so those values have to be chosen in JavaScript
* and re-chosen when the mode changes.
*/
const isDarkMode = element => {
var _element$closest;
const scope = element === null || element === void 0 ? void 0 : (_element$closest = element.closest) === null || _element$closest === void 0 ? void 0 : _element$closest.call(element, SCOPE_SELECTOR);
return Boolean(scope) && !scope.classList.contains(LIGHT_SCOPE_CLASS);
};
// Keyed by subscription so that each subscribed element's last known mode is tracked separately.
const subscriptions = new Map();
let observer = null;
// Most class mutations (hover states, dropdowns opening and closing) have nothing to do with the
// colour mode. Only mutations that added or removed a scope class can change it, so anything else
// can be dismissed with this cheap check instead of re-checking every subscriber.
const touchesColorMode = _ref => {
let target = _ref.target,
oldValue = _ref.oldValue;
return SCOPE_CLASSES.some(scopeClass => {
var _target$classList;
return ((_target$classList = target.classList) === null || _target$classList === void 0 ? void 0 : _target$classList.contains(scopeClass)) || (oldValue === null || oldValue === void 0 ? void 0 : oldValue.includes(scopeClass));
});
};
const notify = () => {
subscriptions.forEach((_ref2, key) => {
let element = _ref2.element,
subscriber = _ref2.subscriber,
mode = _ref2.mode;
const current = isDarkMode(element);
if (current !== mode) {
subscriptions.set(key, {
element,
subscriber,
mode: current
});
subscriber(current);
}
});
};
/**
* Calls back when the colour mode of `element` changes, and returns a function that unsubscribes.
*
* Most colour can be left to CSS, which re-resolves custom properties on its own. This exists for
* the places that cannot: colours ECharts parses numerically, such as the interpolated stops of a
* continuous `visualMap`, and colours handed to a canvas context. Both need a concrete value picked
* in JavaScript, so both need telling when the mode flips.
*
* Because a scope class can live on any ancestor, this watches the whole document for class
* changes, then re-checks each subscribed element and only calls back when its own mode changed.
*/
const onColorModeChange = (element, subscriber) => {
if (!subscriptions.size && typeof MutationObserver !== 'undefined') {
observer = new MutationObserver(mutations => {
if (mutations.some(mutation => touchesColorMode(mutation))) notify();
});
observer.observe(document.documentElement, {
attributeFilter: ['class'],
attributeOldValue: true,
subtree: true
});
}
const key = Symbol('colorModeSubscription');
subscriptions.set(key, {
element,
subscriber,
mode: isDarkMode(element)
});
return () => {
subscriptions.delete(key);
if (!subscriptions.size) {
var _observer;
(_observer = observer) === null || _observer === void 0 ? void 0 : _observer.disconnect();
observer = null;
}
};
};
export { isDarkMode, onColorModeChange };