Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
Outlook.com darkModeHandler
import Color from 'color';
import ContentHandler from 'owa-controls-content-handler/lib/schema/ContentHandler';
import { getPaletteAsRawColors } from 'owa-theme';
export const DARK_MODE_HANDLER_NAME = 'darkModeHandler';
const VALID_CONTRAST_VALUE = 4.5;
const DARK_MODE_SELECTOR = '*';
const EMPTY_STRING = '';
const ATTR_COLOR = 'color';
const ATTR_BGCOLOR = 'bgcolor';
const DATA_OG_STYLE_COLOR = 'data-ogsc';
const DATA_OG_ATTR_COLOR = 'data-ogac';
const DATA_OG_STYLE_BACKGROUNDCOLOR = 'data-ogsb';
const DATA_OG_ATTR_BGCOLOR = 'data-ogab';
export class DarkModeHandler implements ContentHandler {
public readonly cssSelector = DARK_MODE_SELECTOR;
public readonly keywords = null;
private readonly baseBGColor: Color;
private alteredElements: {
element: HTMLElement;
styleColor: string;
attrColor: string;
styleBGColor: string;
attrBGColor: string;
}[];
constructor() {
this.baseBGColor = Color(getPaletteAsRawColors().white);
this.alteredElements = [];
}
public readonly handler = (element: HTMLElement, keyword?: string) => {
// If any element has an invalid color value, Color will throw an exception when trying to create a color object.
try {
let elementAltered = false;
const computedStyles = window.getComputedStyle(element);
// The color mapped to "white" is always used for the background in the reading pane.
// The actual color will be flipped to a dark value when darkMode is enabled.
const styleColor = element.style.color;
let textColor = Color(computedStyles.color);
// Legacy color support, do not remove even though HTML5 deprecates it
let attrColor = element.getAttribute(ATTR_COLOR);
if (!this.isValidContrast(textColor, this.baseBGColor)) {
textColor = this.fixContrast(textColor, this.baseBGColor);
element.style.color = textColor.hex();
element.setAttribute(DATA_OG_STYLE_COLOR, styleColor ? styleColor : EMPTY_STRING);
// Word seems to prioritize color over CSS color so copy/paste has issues without this
if (attrColor) {
element.setAttribute(ATTR_COLOR, textColor.hex());
element.setAttribute(DATA_OG_ATTR_COLOR, attrColor);
}
elementAltered = true;
}
// Get the background color from the element giving priority to style.
// If the element contains no background color, default to dark mode background to past contrast checks.
const styleBGColor = element.style.backgroundColor;
let bgColor = computedStyles.backgroundColor
? Color(computedStyles.backgroundColor)
: Color(styleBGColor);
// Legacy bgcolor support, do not remove even though HTML5 deprecates it
let attrBGColor = element.getAttribute(ATTR_BGCOLOR);
if (!this.isValidContrast(bgColor, textColor)) {
bgColor = this.fixContrast(bgColor, textColor);
element.style.backgroundColor = bgColor.hex();
element.setAttribute(
DATA_OG_STYLE_BACKGROUNDCOLOR,
styleBGColor ? styleBGColor : EMPTY_STRING
);
// Word seems to prioritize bgcolor over CSS background-color so copy/paste has issues without this
if (attrBGColor) {
element.setAttribute(ATTR_BGCOLOR, bgColor.hex());
element.setAttribute(DATA_OG_ATTR_BGCOLOR, attrBGColor);
}
elementAltered = true;
}
// If we altered the element cache its attributes for restoring via the undoHandler.
if (elementAltered) {
this.alteredElements.push({
element: element,
styleColor: styleColor,
attrColor: attrColor,
styleBGColor: styleBGColor,
attrBGColor: attrBGColor,
});
}
} catch (e) {
// Simply swallow malformed CSS and move on as to avoid component errors.
}
};
public readonly undoHandler = (elements: HTMLElement[]) => {
this.alteredElements.forEach(alteredElement => {
const { element, styleColor, attrColor, styleBGColor, attrBGColor } = alteredElement;
// It's possible we set an attribute based on default assumptions, and don't have a cached value to return to.
// Therefore, reset all attributes to their cached values or empty strings.
element.style.color = styleColor ? styleColor : EMPTY_STRING;
attrColor && element.setAttribute(ATTR_COLOR, attrColor);
element.style.backgroundColor = styleBGColor ? styleBGColor : EMPTY_STRING;
attrBGColor && element.setAttribute(ATTR_BGCOLOR, attrBGColor);
// Clean up our custom attr from DOM
element.removeAttribute(DATA_OG_STYLE_COLOR);
element.removeAttribute(DATA_OG_ATTR_COLOR);
element.removeAttribute(DATA_OG_STYLE_BACKGROUNDCOLOR);
element.removeAttribute(DATA_OG_ATTR_BGCOLOR);
});
this.alteredElements = [];
};
private isValidContrast(color1: Color, color2: Color): boolean {
return color1.contrast(color2) >= VALID_CONTRAST_VALUE;
}
private fixContrast(color: Color, comparisonColor: Color): Color {
const baseLValue = this.baseBGColor.lab().array()[0];
const ceilingDarkLValue = 50; // values above this can't contrast against 100
const floorLightLValue = 50 + baseLValue; // values below this can't contrast against baseLValue
const midpointLValue = (floorLightLValue + ceilingDarkLValue) / 2;
// L value for the color we need to contrast against
const comparisonLValue = comparisonColor.lab().array()[0];
// Create the CIELAB color array from the provided color
const colorLab = color.lab().array();
// Scaled L value because baseLValue reduced the range from [0, 100] to [baseLValue, 100]
let newLValue = colorLab[0] * ((100 - baseLValue) / 100) + baseLValue;
// Flip, or not flip, depends on whichever value is more contrasting
// Also avoid the dead zone in the middle caused by baseLValue not being 0
if (comparisonLValue > midpointLValue) {
// comparisonLValue isLight, newLValue should isDark
newLValue = Math.min(newLValue, 2 * midpointLValue - newLValue);
newLValue =
((newLValue - baseLValue) * (ceilingDarkLValue - baseLValue)) /
(midpointLValue - baseLValue) +
baseLValue;
} else {
// comparisonLValue isDark, newLValue should isLight
newLValue = Math.max(newLValue, 2 * midpointLValue - newLValue);
newLValue =
100 - ((100 - newLValue) * (100 - floorLightLValue)) / (100 - midpointLValue);
}
// Create a new color from the old color values and new luminance value.
return Color.lab(newLValue, colorLab[1], colorLab[2]).rgb();
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.