@atlaskit/editor-plugin-text-color
Version:
Text color plugin for @atlaskit/editor-core
301 lines (291 loc) • 16.9 kB
JavaScript
/* ColorAccessibilityMenuItem.tsx generated by @compiled/babel-plugin v0.39.1 */
import "./ColorAccessibilityMenuItem.compiled.css";
import { ax, ix } from "@compiled/react/runtime";
function _createForOfIteratorHelper(r, e) { var t = "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"]; if (!t) { if (Array.isArray(r) || (t = _unsupportedIterableToArray(r)) || e && r && "number" == typeof r.length) { t && (r = t); var _n = 0, F = function F() {}; return { s: F, n: function n() { return _n >= r.length ? { done: !0 } : { done: !1, value: r[_n++] }; }, e: function e(r) { throw r; }, f: F }; } throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } var o, a = !0, u = !1; return { s: function s() { t = t.call(r); }, n: function n() { var r = t.next(); return a = r.done, r; }, e: function e(r) { u = !0, o = r; }, f: function f() { try { a || null == t.return || t.return(); } finally { if (u) throw o; } } }; }
function _unsupportedIterableToArray(r, a) { if (r) { if ("string" == typeof r) return _arrayLikeToArray(r, a); var t = {}.toString.call(r).slice(8, -1); return "Object" === t && r.constructor && (t = r.constructor.name), "Map" === t || "Set" === t ? Array.from(r) : "Arguments" === t || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(t) ? _arrayLikeToArray(r, a) : void 0; } }
function _arrayLikeToArray(r, a) { (null == a || a > r.length) && (a = r.length); for (var e = 0, n = Array(a); e < a; e++) n[e] = r[e]; return n; }
import React from 'react';
import { useIntl } from 'react-intl';
import { IconButton } from '@atlaskit/button/new';
import { cx } from '@atlaskit/css';
import { useSharedPluginStateWithSelector } from '@atlaskit/editor-common/hooks';
import { colorAccessibilityMessages as messages } from '@atlaskit/editor-common/messages';
import { getHighlightColorInNonActiveTheme, getTextColorInNonActiveTheme, getTokenCSSVariableValue } from '@atlaskit/editor-common/ui-color';
import { hexToEditorTextBackgroundPaletteColor, hexToEditorTextPaletteColor } from '@atlaskit/editor-palette';
import { TextSelection } from '@atlaskit/editor-prosemirror/state';
import AccessibilityIcon from '@atlaskit/icon/core/accessibility';
import QuestionCircleIcon from '@atlaskit/icon/core/question-circle';
import { fg } from '@atlaskit/platform-feature-flags';
import { Box, Inline, Text } from '@atlaskit/primitives/compiled';
import { getTokenValue } from '@atlaskit/tokens/get-token-value';
import { getContrastRatio as calcContrastRatio } from '../pm-plugins/utils/color-contrast';
import { DEFAULT_COLOR, DEFAULT_BACKGROUND_COLOR, TRANSPARENT_HIGHLIGHT_COLOR, ACCESSIBLE_CONTRAST_RATIO, DIFFICULT_CONTRAST_RATIO } from '../pm-plugins/utils/constants';
var resolveColorValue = function resolveColorValue(color, fallback) {
return getTokenCSSVariableValue(color) || (color.startsWith('var(') ? fallback : color);
};
var getForegroundColor = function getForegroundColor(textColor, defaultColor) {
if (!textColor || defaultColor && textColor === defaultColor) {
return getTokenValue('color.text', defaultColor || DEFAULT_COLOR.color);
}
var colorValue = hexToEditorTextPaletteColor(textColor) || textColor;
return resolveColorValue(colorValue, textColor);
};
var getBackgroundColor = function getBackgroundColor(highlightColor) {
if (!highlightColor || highlightColor === TRANSPARENT_HIGHLIGHT_COLOR) {
return getTokenValue('elevation.surface', DEFAULT_BACKGROUND_COLOR);
}
var colorValue = hexToEditorTextBackgroundPaletteColor(highlightColor) || highlightColor;
return resolveColorValue(colorValue, highlightColor);
};
var useColorAccessibilityState = function useColorAccessibilityState(api) {
return useSharedPluginStateWithSelector(api, ['textColor', 'highlight'], function (states) {
var _states$textColorStat, _states$highlightStat, _states$highlightStat2, _states$highlightStat3, _states$textColorStat2, _states$textColorStat3, _states$textColorStat4;
return {
defaultColor: (_states$textColorStat = states.textColorState) === null || _states$textColorStat === void 0 ? void 0 : _states$textColorStat.defaultColor,
highlightColor: (_states$highlightStat = states.highlightState) === null || _states$highlightStat === void 0 ? void 0 : _states$highlightStat.activeColor,
highlightColorInNonActiveTheme: (_states$highlightStat2 = states.highlightState) === null || _states$highlightStat2 === void 0 ? void 0 : _states$highlightStat2.activeColorInNonActiveTheme,
isMultiHighlightColor: (_states$highlightStat3 = states.highlightState) === null || _states$highlightStat3 === void 0 ? void 0 : _states$highlightStat3.isMultiHighlightColor,
isMultiTextColor: (_states$textColorStat2 = states.textColorState) === null || _states$textColorStat2 === void 0 ? void 0 : _states$textColorStat2.isMultiTextColor,
textColor: (_states$textColorStat3 = states.textColorState) === null || _states$textColorStat3 === void 0 ? void 0 : _states$textColorStat3.color,
textColorInNonActiveTheme: (_states$textColorStat4 = states.textColorState) === null || _states$textColorStat4 === void 0 ? void 0 : _states$textColorStat4.colorInNonActiveTheme
};
});
};
var getContrastRatio = function getContrastRatio(defaultColor, highlightColor, textColor) {
try {
var contrastRatio = calcContrastRatio(getForegroundColor(textColor, defaultColor), getBackgroundColor(highlightColor));
return contrastRatio;
} catch (_unused) {
// if we failed to calculate the contrast ratio, return null
return null;
}
};
// Computes the contrast ratio for a (textColor, highlightColor) pair as it would
// appear in the non-active theme, using the shared resolvers so the walker
// evaluates the same colors the single-pair plugin-state path does. Returns null
// if it cannot be calculated.
var getNonActiveThemeContrastRatio = function getNonActiveThemeContrastRatio(defaultColor, highlightColor, textColor) {
try {
return calcContrastRatio(getTextColorInNonActiveTheme(textColor !== null && textColor !== void 0 ? textColor : null, defaultColor || DEFAULT_COLOR.color), getHighlightColorInNonActiveTheme(highlightColor !== null && highlightColor !== void 0 ? highlightColor : null, {
defaultBackgroundColor: DEFAULT_BACKGROUND_COLOR,
transparentColor: TRANSPARENT_HIGHLIGHT_COLOR
}));
} catch (_unused2) {
// if we failed to calculate the contrast ratio, return null
return null;
}
};
var getAccessibilityStatus = function getAccessibilityStatus(contrastRatio) {
if (contrastRatio >= ACCESSIBLE_CONTRAST_RATIO) {
return 'accessible';
} else if (contrastRatio >= DIFFICULT_CONTRAST_RATIO) {
return 'difficultToRead';
} else {
return 'inaccessible';
}
};
/**
* Collects the worst accessibility status across all unique (textColor, highlightColor)
* combinations in the current selection.
*
* When the selection spans multiple text colors and/or highlight colors,
* this walks the document between selection boundaries and checks every
* text node's color marks, computing the contrast ratio for each unique pair
* (across both the active and non-active theme) and returning the worst
* status found.
*/
var getWorstAccessibilityStatusFromSelection = function getWorstAccessibilityStatusFromSelection(api, defaultColor) {
if (!(api !== null && api !== void 0 && api.core)) {
return null;
}
// Track the lowest (worst) contrast ratio across all pairs; a lower ratio
// means a worse accessibility status, so a single numeric comparison is
// enough to find the worst pair.
var worstContrastRatio = null;
api.core.actions.execute(function (_ref) {
var tr = _ref.tr;
// Text/highlight colors only apply to a text range. If the selection is
// not a text selection (e.g. a node, cell or all selection) there is no
// meaningful range of colored text to evaluate.
if (!(tr.selection instanceof TextSelection)) {
return null;
}
var _tr$selection = tr.selection,
from = _tr$selection.from,
to = _tr$selection.to;
var seen = new Set();
tr.doc.nodesBetween(from, to, function (node) {
// Text and highlight color marks only apply to text nodes, so skip
// any non-text leaves (e.g. emoji, mentions, inline cards). Continue
// descending into container nodes to reach their text children.
if (!node.isText) {
return !node.isLeaf;
}
// Extract both color marks in a single pass over the node's marks
// rather than two `Array.find` scans.
var textColor = null;
var highlightColor = null;
var _iterator = _createForOfIteratorHelper(node.marks),
_step;
try {
for (_iterator.s(); !(_step = _iterator.n()).done;) {
var mark = _step.value;
if (mark.type.name === 'textColor') {
var _mark$attrs$color;
textColor = (_mark$attrs$color = mark.attrs.color) !== null && _mark$attrs$color !== void 0 ? _mark$attrs$color : null;
} else if (mark.type.name === 'backgroundColor') {
var _mark$attrs$color2;
highlightColor = (_mark$attrs$color2 = mark.attrs.color) !== null && _mark$attrs$color2 !== void 0 ? _mark$attrs$color2 : null;
}
}
} catch (err) {
_iterator.e(err);
} finally {
_iterator.f();
}
var pairKey = "".concat(textColor !== null && textColor !== void 0 ? textColor : 'default', "|").concat(highlightColor !== null && highlightColor !== void 0 ? highlightColor : 'default');
// Skip pairs we have already evaluated so the expensive contrast
// calculations run at most once per unique (text, highlight) pair.
if (seen.has(pairKey)) {
return;
}
seen.add(pairKey);
var contrastRatio = getContrastRatio(defaultColor, highlightColor, textColor);
if (contrastRatio === null) {
return;
}
// Account for the non-active theme so the worst-case (least
// accessible) contrast across both themes is used for each pair,
// keeping multi-color selections consistent with the single-pair path.
var nonActiveThemeContrastRatio = getNonActiveThemeContrastRatio(defaultColor, highlightColor, textColor);
var mostCriticalContrastRatio = nonActiveThemeContrastRatio !== null ? Math.min(contrastRatio, nonActiveThemeContrastRatio) : contrastRatio;
if (worstContrastRatio === null || mostCriticalContrastRatio < worstContrastRatio) {
worstContrastRatio = mostCriticalContrastRatio;
}
});
// Read-only operation — do not dispatch
return null;
});
return worstContrastRatio === null ? null : getAccessibilityStatus(worstContrastRatio);
};
var AccessibilityStatus = function AccessibilityStatus(_ref2) {
var accessibilityStatus = _ref2.accessibilityStatus,
formatMessage = _ref2.formatMessage;
if (accessibilityStatus === 'accessible') {
return /*#__PURE__*/React.createElement(Text, {
as: "span",
size: "small",
color: "color.text.success"
}, formatMessage(messages.accessibleLabel));
} else if (accessibilityStatus === 'difficultToRead') {
return /*#__PURE__*/React.createElement(Text, {
as: "span",
size: "small",
color: "color.text.warning"
}, formatMessage(messages.difficultToReadLabel));
} else {
return /*#__PURE__*/React.createElement(Text, {
as: "span",
size: "small",
color: "color.text.danger"
}, formatMessage(messages.inaccessibleLabel));
}
};
var styles = {
container: "_19itglyw _19pku2gc _ca0q1b66 _n3td1b66 _19bvu2gc _u5f31b66 _1e0c1txw _4cvr1h6o _1bah1yb4",
containerPatch: "_19pkze3t _ca0qze3t _n3tdze3t _19bvze3t _u5f3ze3t"
};
export var ColorAccessibilityMenuItem = function ColorAccessibilityMenuItem(_ref3) {
var api = _ref3.api;
var _useIntl = useIntl(),
formatMessage = _useIntl.formatMessage;
var _useColorAccessibilit = useColorAccessibilityState(api),
defaultColor = _useColorAccessibilit.defaultColor,
highlightColor = _useColorAccessibilit.highlightColor,
highlightColorInNonActiveTheme = _useColorAccessibilit.highlightColorInNonActiveTheme,
isMultiHighlightColor = _useColorAccessibilit.isMultiHighlightColor,
isMultiTextColor = _useColorAccessibilit.isMultiTextColor,
textColor = _useColorAccessibilit.textColor,
textColorInNonActiveTheme = _useColorAccessibilit.textColorInNonActiveTheme;
// The multi-color selection handling (walking every unique text/highlight
// pair across the active and non-active theme) is only performed when the
// patch gate is on. When the gate is off we fall back to the legacy behavior
// of only evaluating the single active text/highlight color in the current
// active theme
var isMultiColorSelection = Boolean(isMultiTextColor || isMultiHighlightColor) && fg('platform_editor_lovability_text_bg_color_patch_1');
// The complement of `isMultiColorSelection`: the single active text/highlight
// pair is evaluated whenever we are not walking a multi-color selection. This
// covers both a genuinely single-color selection (gate on) and every
// selection when the gate is off (legacy behavior).
var isSingleColorSelection = !isMultiColorSelection;
// Memoize the selection walk so we don't re-traverse the entire ProseMirror
// document on every render. It only needs to recompute when the multi-color
// selection state or the inputs to the walk change (the selection flags,
// default color, or the plugin api).
var multiColorStatus = React.useMemo(function () {
return isMultiColorSelection ? getWorstAccessibilityStatusFromSelection(api, defaultColor) : null;
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[isMultiColorSelection, isMultiTextColor, isMultiHighlightColor, defaultColor, api]);
// The single active text/highlight pair path. Everything below only applies
// when we are not walking a multi-color selection
var singleColorStatus = null;
if (isSingleColorSelection) {
var singleColorContrastRatio = getContrastRatio(defaultColor, highlightColor, textColor);
// Also account for the non-active theme (gate on only) so the worst-case
// (least accessible) status across both themes is shown.
var nonActiveThemeSingleColorRatio = null;
if (textColorInNonActiveTheme && highlightColorInNonActiveTheme && fg('platform_editor_lovability_text_bg_color_patch_1')) {
try {
nonActiveThemeSingleColorRatio = calcContrastRatio(textColorInNonActiveTheme, highlightColorInNonActiveTheme);
} catch (_unused3) {
// if we failed to calculate the contrast ratio, leave as null
nonActiveThemeSingleColorRatio = null;
}
}
var mostCriticalContrastRatio = singleColorContrastRatio !== null && nonActiveThemeSingleColorRatio !== null ? Math.min(singleColorContrastRatio, nonActiveThemeSingleColorRatio) : singleColorContrastRatio;
singleColorStatus = mostCriticalContrastRatio !== null ? getAccessibilityStatus(mostCriticalContrastRatio) : null;
}
var accessibilityStatus = isSingleColorSelection ? singleColorStatus : multiColorStatus;
if (accessibilityStatus === null) {
return /*#__PURE__*/React.createElement(React.Fragment, null);
}
var tooltipContent = function tooltipContent(accessibilityStatus) {
if (accessibilityStatus === 'accessible') {
return formatMessage(messages.accessibleTooltip);
} else if (accessibilityStatus === 'difficultToRead') {
return formatMessage(messages.difficultToReadTooltip);
} else {
return formatMessage(messages.inaccessibleTooltip);
}
};
return /*#__PURE__*/React.createElement(Box, {
xcss: cx(styles.container, fg('platform_editor_lovability_text_bg_color_patch_1') && styles.containerPatch)
}, /*#__PURE__*/React.createElement(Inline, {
alignBlock: "center",
space: "space.050"
}, /*#__PURE__*/React.createElement(AccessibilityIcon, {
label: "",
size: "medium",
color: fg('platform_editor_lovability_text_bg_color_patch_1') ? "var(--ds-icon-subtle, #505258)" : undefined
}), /*#__PURE__*/React.createElement(Text, {
as: "span",
size: "small",
color: "color.text.subtle"
}, formatMessage(messages.accessibility)), /*#__PURE__*/React.createElement(Text, {
as: "span",
size: "small",
color: "color.text.subtle",
"aria-hidden": "true"
}, "\u2022"), /*#__PURE__*/React.createElement(AccessibilityStatus, {
accessibilityStatus: accessibilityStatus,
formatMessage: formatMessage
})), /*#__PURE__*/React.createElement(IconButton, {
icon: QuestionCircleIcon,
shape: "circle",
label: tooltipContent(accessibilityStatus),
isTooltipDisabled: false,
spacing: "compact",
appearance: "subtle"
}));
};