UNPKG

@atlaskit/editor-plugin-text-color

Version:

Text color plugin for @atlaskit/editor-core

301 lines (291 loc) 16.9 kB
/* 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" })); };