@atlaskit/editor-plugin-text-color 12.3.18 → 12.3.19
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +9 -0
- package/dist/cjs/pm-plugins/main.js +9 -13
- package/dist/cjs/pm-plugins/utils/color.js +32 -1
- package/dist/cjs/ui/ColorAccessibilityMenuItem.js +163 -17
- package/dist/es2019/pm-plugins/main.js +11 -15
- package/dist/es2019/pm-plugins/utils/color.js +35 -0
- package/dist/es2019/ui/ColorAccessibilityMenuItem.js +148 -13
- package/dist/esm/pm-plugins/main.js +11 -15
- package/dist/esm/pm-plugins/utils/color.js +31 -0
- package/dist/esm/ui/ColorAccessibilityMenuItem.js +164 -18
- package/dist/types/pm-plugins/main.d.ts +5 -0
- package/dist/types/pm-plugins/utils/color.d.ts +12 -0
- package/package.json +5 -5
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
# @atlaskit/editor-plugin-text-color
|
|
2
2
|
|
|
3
|
+
## 12.3.19
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- [`4821d5874a821`](https://bitbucket.org/atlassian/atlassian-frontend-monorepo/commits/4821d5874a821) -
|
|
8
|
+
EDITOR-7869: Show worst accessibility status when selection spans multiple text or highlight
|
|
9
|
+
colors
|
|
10
|
+
- Updated dependencies
|
|
11
|
+
|
|
3
12
|
## 12.3.18
|
|
4
13
|
|
|
5
14
|
### Patch Changes
|
|
@@ -11,7 +11,6 @@ var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/de
|
|
|
11
11
|
var _toConsumableArray2 = _interopRequireDefault(require("@babel/runtime/helpers/toConsumableArray"));
|
|
12
12
|
var _safePlugin = require("@atlaskit/editor-common/safe-plugin");
|
|
13
13
|
var _uiColor = require("@atlaskit/editor-common/ui-color");
|
|
14
|
-
var _text = require("@atlaskit/editor-palette/text");
|
|
15
14
|
var _state = require("@atlaskit/editor-prosemirror/state");
|
|
16
15
|
var _platformFeatureFlags = require("@atlaskit/platform-feature-flags");
|
|
17
16
|
var _expValEqualsNoExposure = require("@atlaskit/tmp-editor-statsig/exp-val-equals-no-exposure");
|
|
@@ -20,13 +19,6 @@ var _constants = require("./utils/constants");
|
|
|
20
19
|
var _disabled = require("./utils/disabled");
|
|
21
20
|
function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; }
|
|
22
21
|
function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { (0, _defineProperty2.default)(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; }
|
|
23
|
-
var getColorInNonActiveTheme = function getColorInNonActiveTheme(color, defaultColor) {
|
|
24
|
-
if (!color || color === defaultColor) {
|
|
25
|
-
return (0, _uiColor.getTokenCSSVariableValueForNonActiveTheme)("var(--ds-text, #292A2E)", defaultColor);
|
|
26
|
-
}
|
|
27
|
-
var colorValue = (0, _text.hexToEditorTextPaletteColor)(color) || color;
|
|
28
|
-
return (0, _uiColor.getTokenCSSVariableValueForNonActiveTheme)(colorValue, color);
|
|
29
|
-
};
|
|
30
22
|
function createInitialPluginState(editorState, pluginConfig) {
|
|
31
23
|
var defaultColor = (pluginConfig === null || pluginConfig === void 0 ? void 0 : pluginConfig.defaultColor) || _constants.DEFAULT_COLOR;
|
|
32
24
|
var paletteColors = (0, _expValEqualsNoExposure.expValEqualsNoExposure)('platform_editor_lovability_text_bg_color', 'isEnabled', true) ? _uiColor.textColorPaletteNew : _uiColor.textColorPalette;
|
|
@@ -43,7 +35,8 @@ function createInitialPluginState(editorState, pluginConfig) {
|
|
|
43
35
|
defaultColor: defaultColor.color,
|
|
44
36
|
isPaletteOpen: false
|
|
45
37
|
}, (0, _platformFeatureFlags.fg)('platform_editor_lovability_text_bg_color_patch_1') && (0, _expValEqualsNoExposure.expValEqualsNoExposure)('platform_editor_lovability_text_bg_color', 'isEnabled', true) && {
|
|
46
|
-
colorInNonActiveTheme:
|
|
38
|
+
colorInNonActiveTheme: (0, _uiColor.getTextColorInNonActiveTheme)(color, defaultColor.color),
|
|
39
|
+
isMultiTextColor: (0, _color.isMultiTextColorSelection)(editorState)
|
|
47
40
|
});
|
|
48
41
|
return state;
|
|
49
42
|
}
|
|
@@ -70,7 +63,8 @@ function createPlugin(dispatch, pluginConfig) {
|
|
|
70
63
|
nextState = _objectSpread(_objectSpread({}, pluginState), {}, {
|
|
71
64
|
color: pluginState.defaultColor
|
|
72
65
|
}, (0, _platformFeatureFlags.fg)('platform_editor_lovability_text_bg_color_patch_1') && (0, _expValEqualsNoExposure.expValEqualsNoExposure)('platform_editor_lovability_text_bg_color', 'isEnabled', true) && {
|
|
73
|
-
colorInNonActiveTheme:
|
|
66
|
+
colorInNonActiveTheme: (0, _uiColor.getTextColorInNonActiveTheme)(pluginState.defaultColor, pluginState.defaultColor),
|
|
67
|
+
isMultiTextColor: false
|
|
74
68
|
});
|
|
75
69
|
break;
|
|
76
70
|
case ACTIONS.SET_COLOR:
|
|
@@ -79,7 +73,8 @@ function createPlugin(dispatch, pluginConfig) {
|
|
|
79
73
|
disabled: false,
|
|
80
74
|
isPaletteOpen: false
|
|
81
75
|
}, (0, _platformFeatureFlags.fg)('platform_editor_lovability_text_bg_color_patch_1') && (0, _expValEqualsNoExposure.expValEqualsNoExposure)('platform_editor_lovability_text_bg_color', 'isEnabled', true) && {
|
|
82
|
-
colorInNonActiveTheme:
|
|
76
|
+
colorInNonActiveTheme: (0, _uiColor.getTextColorInNonActiveTheme)(meta.color, pluginState.defaultColor),
|
|
77
|
+
isMultiTextColor: false
|
|
83
78
|
});
|
|
84
79
|
break;
|
|
85
80
|
case ACTIONS.DISABLE:
|
|
@@ -98,10 +93,11 @@ function createPlugin(dispatch, pluginConfig) {
|
|
|
98
93
|
color: color,
|
|
99
94
|
disabled: (0, _disabled.getDisabledState)(newState)
|
|
100
95
|
}, color !== pluginState.color && (0, _platformFeatureFlags.fg)('platform_editor_lovability_text_bg_color_patch_1') && (0, _expValEqualsNoExposure.expValEqualsNoExposure)('platform_editor_lovability_text_bg_color', 'isEnabled', true) && {
|
|
101
|
-
colorInNonActiveTheme:
|
|
96
|
+
colorInNonActiveTheme: (0, _uiColor.getTextColorInNonActiveTheme)(color, pluginState.defaultColor),
|
|
97
|
+
isMultiTextColor: (0, _color.isMultiTextColorSelection)(newState)
|
|
102
98
|
});
|
|
103
99
|
}
|
|
104
|
-
if (pluginState && pluginState.color !== nextState.color || pluginState && pluginState.colorInNonActiveTheme !== nextState.colorInNonActiveTheme || pluginState && pluginState.disabled !== nextState.disabled || pluginState && pluginState.isPaletteOpen !== nextState.isPaletteOpen) {
|
|
100
|
+
if (pluginState && pluginState.color !== nextState.color || pluginState && pluginState.colorInNonActiveTheme !== nextState.colorInNonActiveTheme || pluginState && pluginState.disabled !== nextState.disabled || pluginState && pluginState.isMultiTextColor !== nextState.isMultiTextColor || pluginState && pluginState.isPaletteOpen !== nextState.isPaletteOpen) {
|
|
105
101
|
dispatch(pluginKey, nextState);
|
|
106
102
|
return nextState;
|
|
107
103
|
}
|
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
Object.defineProperty(exports, "__esModule", {
|
|
4
4
|
value: true
|
|
5
5
|
});
|
|
6
|
-
exports.getActiveColorNew = exports.getActiveColor = void 0;
|
|
6
|
+
exports.isMultiTextColorSelection = exports.getActiveColorNew = exports.getActiveColor = void 0;
|
|
7
|
+
var _state = require("@atlaskit/editor-prosemirror/state");
|
|
7
8
|
var _constants = require("./constants");
|
|
8
9
|
/**
|
|
9
10
|
* Use getDisabledStateNew instead and pass in `tr`
|
|
@@ -49,6 +50,36 @@ var getActiveColor = exports.getActiveColor = function getActiveColor(state) {
|
|
|
49
50
|
}
|
|
50
51
|
return marksWithColor.length ? marksWithColor[0].attrs.color : _constants.DEFAULT_COLOR.color;
|
|
51
52
|
};
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Returns true only when the selection contains two or more *distinct* text
|
|
56
|
+
* colors.
|
|
57
|
+
*
|
|
58
|
+
* This is deliberately narrower than `getActiveColor() === null`: `getActiveColor`
|
|
59
|
+
* also returns `null` for a mixed selection where a single colored region sits
|
|
60
|
+
* alongside uncolored text (`marksWithColor.length === 1 && marks.length > 1`).
|
|
61
|
+
* That mixed case is not a multi-color selection and must not trigger the
|
|
62
|
+
* expensive worst-status selection walk, so we key the `isMultiTextColor` flag
|
|
63
|
+
* off this predicate instead.
|
|
64
|
+
*/
|
|
65
|
+
var isMultiTextColorSelection = exports.isMultiTextColorSelection = function isMultiTextColorSelection(state) {
|
|
66
|
+
var selection = state.selection,
|
|
67
|
+
doc = state.doc;
|
|
68
|
+
if (!(selection instanceof _state.TextSelection)) {
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
var textColor = doc.type.schema.marks.textColor;
|
|
72
|
+
var colors = new Set();
|
|
73
|
+
doc.nodesBetween(selection.from, selection.to, function (node) {
|
|
74
|
+
if (node.isLeaf) {
|
|
75
|
+
var mark = textColor.isInSet(node.marks);
|
|
76
|
+
if (mark) {
|
|
77
|
+
colors.add(mark.attrs.color);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
return colors.size > 1;
|
|
82
|
+
};
|
|
52
83
|
var getActiveColorNew = exports.getActiveColorNew = function getActiveColorNew(tr) {
|
|
53
84
|
var _ref3 = tr.selection,
|
|
54
85
|
$from = _ref3.$from,
|
|
@@ -15,6 +15,7 @@ var _hooks = require("@atlaskit/editor-common/hooks");
|
|
|
15
15
|
var _messages = require("@atlaskit/editor-common/messages");
|
|
16
16
|
var _uiColor = require("@atlaskit/editor-common/ui-color");
|
|
17
17
|
var _editorPalette = require("@atlaskit/editor-palette");
|
|
18
|
+
var _state = require("@atlaskit/editor-prosemirror/state");
|
|
18
19
|
var _accessibility = _interopRequireDefault(require("@atlaskit/icon/core/accessibility"));
|
|
19
20
|
var _questionCircle = _interopRequireDefault(require("@atlaskit/icon/core/question-circle"));
|
|
20
21
|
var _platformFeatureFlags = require("@atlaskit/platform-feature-flags");
|
|
@@ -22,6 +23,9 @@ var _compiled = require("@atlaskit/primitives/compiled");
|
|
|
22
23
|
var _tokens = require("@atlaskit/tokens");
|
|
23
24
|
var _colorContrast = require("../pm-plugins/utils/color-contrast");
|
|
24
25
|
var _constants = require("../pm-plugins/utils/constants");
|
|
26
|
+
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; } } }; }
|
|
27
|
+
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; } }
|
|
28
|
+
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; }
|
|
25
29
|
var resolveColorValue = function resolveColorValue(color, fallback) {
|
|
26
30
|
return (0, _uiColor.getTokenCSSVariableValue)(color) || (color.startsWith('var(') ? fallback : color);
|
|
27
31
|
};
|
|
@@ -41,13 +45,15 @@ var getBackgroundColor = function getBackgroundColor(highlightColor) {
|
|
|
41
45
|
};
|
|
42
46
|
var useColorAccessibilityState = function useColorAccessibilityState(api) {
|
|
43
47
|
return (0, _hooks.useSharedPluginStateWithSelector)(api, ['textColor', 'highlight'], function (states) {
|
|
44
|
-
var _states$textColorStat, _states$highlightStat, _states$highlightStat2, _states$textColorStat2, _states$textColorStat3;
|
|
48
|
+
var _states$textColorStat, _states$highlightStat, _states$highlightStat2, _states$highlightStat3, _states$textColorStat2, _states$textColorStat3, _states$textColorStat4;
|
|
45
49
|
return {
|
|
46
50
|
defaultColor: (_states$textColorStat = states.textColorState) === null || _states$textColorStat === void 0 ? void 0 : _states$textColorStat.defaultColor,
|
|
47
51
|
highlightColor: (_states$highlightStat = states.highlightState) === null || _states$highlightStat === void 0 ? void 0 : _states$highlightStat.activeColor,
|
|
48
52
|
highlightColorInNonActiveTheme: (_states$highlightStat2 = states.highlightState) === null || _states$highlightStat2 === void 0 ? void 0 : _states$highlightStat2.activeColorInNonActiveTheme,
|
|
49
|
-
|
|
50
|
-
|
|
53
|
+
isMultiHighlightColor: (_states$highlightStat3 = states.highlightState) === null || _states$highlightStat3 === void 0 ? void 0 : _states$highlightStat3.isMultiHighlightColor,
|
|
54
|
+
isMultiTextColor: (_states$textColorStat2 = states.textColorState) === null || _states$textColorStat2 === void 0 ? void 0 : _states$textColorStat2.isMultiTextColor,
|
|
55
|
+
textColor: (_states$textColorStat3 = states.textColorState) === null || _states$textColorStat3 === void 0 ? void 0 : _states$textColorStat3.color,
|
|
56
|
+
textColorInNonActiveTheme: (_states$textColorStat4 = states.textColorState) === null || _states$textColorStat4 === void 0 ? void 0 : _states$textColorStat4.colorInNonActiveTheme
|
|
51
57
|
};
|
|
52
58
|
});
|
|
53
59
|
};
|
|
@@ -60,6 +66,22 @@ var getContrastRatio = function getContrastRatio(defaultColor, highlightColor, t
|
|
|
60
66
|
return null;
|
|
61
67
|
}
|
|
62
68
|
};
|
|
69
|
+
|
|
70
|
+
// Computes the contrast ratio for a (textColor, highlightColor) pair as it would
|
|
71
|
+
// appear in the non-active theme, using the shared resolvers so the walker
|
|
72
|
+
// evaluates the same colors the single-pair plugin-state path does. Returns null
|
|
73
|
+
// if it cannot be calculated.
|
|
74
|
+
var getNonActiveThemeContrastRatio = function getNonActiveThemeContrastRatio(defaultColor, highlightColor, textColor) {
|
|
75
|
+
try {
|
|
76
|
+
return (0, _colorContrast.getContrastRatio)((0, _uiColor.getTextColorInNonActiveTheme)(textColor !== null && textColor !== void 0 ? textColor : null, defaultColor || _constants.DEFAULT_COLOR.color), (0, _uiColor.getHighlightColorInNonActiveTheme)(highlightColor !== null && highlightColor !== void 0 ? highlightColor : null, {
|
|
77
|
+
defaultBackgroundColor: _constants.DEFAULT_BACKGROUND_COLOR,
|
|
78
|
+
transparentColor: _constants.TRANSPARENT_HIGHLIGHT_COLOR
|
|
79
|
+
}));
|
|
80
|
+
} catch (_unused2) {
|
|
81
|
+
// if we failed to calculate the contrast ratio, return null
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
};
|
|
63
85
|
var getAccessibilityStatus = function getAccessibilityStatus(contrastRatio) {
|
|
64
86
|
if (contrastRatio >= _constants.ACCESSIBLE_CONTRAST_RATIO) {
|
|
65
87
|
return 'accessible';
|
|
@@ -69,9 +91,98 @@ var getAccessibilityStatus = function getAccessibilityStatus(contrastRatio) {
|
|
|
69
91
|
return 'inaccessible';
|
|
70
92
|
}
|
|
71
93
|
};
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Collects the worst accessibility status across all unique (textColor, highlightColor)
|
|
97
|
+
* combinations in the current selection.
|
|
98
|
+
*
|
|
99
|
+
* When the selection spans multiple text colors and/or highlight colors,
|
|
100
|
+
* this walks the document between selection boundaries and checks every
|
|
101
|
+
* text node's color marks, computing the contrast ratio for each unique pair
|
|
102
|
+
* (across both the active and non-active theme) and returning the worst
|
|
103
|
+
* status found.
|
|
104
|
+
*/
|
|
105
|
+
var getWorstAccessibilityStatusFromSelection = function getWorstAccessibilityStatusFromSelection(api, defaultColor) {
|
|
106
|
+
if (!(api !== null && api !== void 0 && api.core)) {
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Track the lowest (worst) contrast ratio across all pairs; a lower ratio
|
|
111
|
+
// means a worse accessibility status, so a single numeric comparison is
|
|
112
|
+
// enough to find the worst pair.
|
|
113
|
+
var worstContrastRatio = null;
|
|
114
|
+
api.core.actions.execute(function (_ref) {
|
|
115
|
+
var tr = _ref.tr;
|
|
116
|
+
// Text/highlight colors only apply to a text range. If the selection is
|
|
117
|
+
// not a text selection (e.g. a node, cell or all selection) there is no
|
|
118
|
+
// meaningful range of colored text to evaluate.
|
|
119
|
+
if (!(tr.selection instanceof _state.TextSelection)) {
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
var _tr$selection = tr.selection,
|
|
123
|
+
from = _tr$selection.from,
|
|
124
|
+
to = _tr$selection.to;
|
|
125
|
+
var seen = new Set();
|
|
126
|
+
tr.doc.nodesBetween(from, to, function (node) {
|
|
127
|
+
// Text and highlight color marks only apply to text nodes, so skip
|
|
128
|
+
// any non-text leaves (e.g. emoji, mentions, inline cards). Continue
|
|
129
|
+
// descending into container nodes to reach their text children.
|
|
130
|
+
if (!node.isText) {
|
|
131
|
+
return !node.isLeaf;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Extract both color marks in a single pass over the node's marks
|
|
135
|
+
// rather than two `Array.find` scans.
|
|
136
|
+
var textColor = null;
|
|
137
|
+
var highlightColor = null;
|
|
138
|
+
var _iterator = _createForOfIteratorHelper(node.marks),
|
|
139
|
+
_step;
|
|
140
|
+
try {
|
|
141
|
+
for (_iterator.s(); !(_step = _iterator.n()).done;) {
|
|
142
|
+
var mark = _step.value;
|
|
143
|
+
if (mark.type.name === 'textColor') {
|
|
144
|
+
var _mark$attrs$color;
|
|
145
|
+
textColor = (_mark$attrs$color = mark.attrs.color) !== null && _mark$attrs$color !== void 0 ? _mark$attrs$color : null;
|
|
146
|
+
} else if (mark.type.name === 'backgroundColor') {
|
|
147
|
+
var _mark$attrs$color2;
|
|
148
|
+
highlightColor = (_mark$attrs$color2 = mark.attrs.color) !== null && _mark$attrs$color2 !== void 0 ? _mark$attrs$color2 : null;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
} catch (err) {
|
|
152
|
+
_iterator.e(err);
|
|
153
|
+
} finally {
|
|
154
|
+
_iterator.f();
|
|
155
|
+
}
|
|
156
|
+
var pairKey = "".concat(textColor !== null && textColor !== void 0 ? textColor : 'default', "|").concat(highlightColor !== null && highlightColor !== void 0 ? highlightColor : 'default');
|
|
157
|
+
// Skip pairs we have already evaluated so the expensive contrast
|
|
158
|
+
// calculations run at most once per unique (text, highlight) pair.
|
|
159
|
+
if (seen.has(pairKey)) {
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
seen.add(pairKey);
|
|
163
|
+
var contrastRatio = getContrastRatio(defaultColor, highlightColor, textColor);
|
|
164
|
+
if (contrastRatio === null) {
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Account for the non-active theme so the worst-case (least
|
|
169
|
+
// accessible) contrast across both themes is used for each pair,
|
|
170
|
+
// keeping multi-color selections consistent with the single-pair path.
|
|
171
|
+
var nonActiveThemeContrastRatio = getNonActiveThemeContrastRatio(defaultColor, highlightColor, textColor);
|
|
172
|
+
var mostCriticalContrastRatio = nonActiveThemeContrastRatio !== null ? Math.min(contrastRatio, nonActiveThemeContrastRatio) : contrastRatio;
|
|
173
|
+
if (worstContrastRatio === null || mostCriticalContrastRatio < worstContrastRatio) {
|
|
174
|
+
worstContrastRatio = mostCriticalContrastRatio;
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
// Read-only operation — do not dispatch
|
|
179
|
+
return null;
|
|
180
|
+
});
|
|
181
|
+
return worstContrastRatio === null ? null : getAccessibilityStatus(worstContrastRatio);
|
|
182
|
+
};
|
|
183
|
+
var AccessibilityStatus = function AccessibilityStatus(_ref2) {
|
|
184
|
+
var accessibilityStatus = _ref2.accessibilityStatus,
|
|
185
|
+
formatMessage = _ref2.formatMessage;
|
|
75
186
|
if (accessibilityStatus === 'accessible') {
|
|
76
187
|
return /*#__PURE__*/_react.default.createElement(_compiled.Text, {
|
|
77
188
|
as: "span",
|
|
@@ -95,30 +206,65 @@ var AccessibilityStatus = function AccessibilityStatus(_ref) {
|
|
|
95
206
|
var styles = {
|
|
96
207
|
container: "_19itglyw _19pku2gc _ca0q1b66 _n3td1b66 _19bvu2gc _u5f31b66 _1e0c1txw _4cvr1h6o _1bah1yb4"
|
|
97
208
|
};
|
|
98
|
-
var ColorAccessibilityMenuItem = exports.ColorAccessibilityMenuItem = function ColorAccessibilityMenuItem(
|
|
99
|
-
var api =
|
|
209
|
+
var ColorAccessibilityMenuItem = exports.ColorAccessibilityMenuItem = function ColorAccessibilityMenuItem(_ref3) {
|
|
210
|
+
var api = _ref3.api;
|
|
100
211
|
var _useIntl = (0, _reactIntl.useIntl)(),
|
|
101
212
|
formatMessage = _useIntl.formatMessage;
|
|
102
213
|
var _useColorAccessibilit = useColorAccessibilityState(api),
|
|
103
214
|
defaultColor = _useColorAccessibilit.defaultColor,
|
|
104
215
|
highlightColor = _useColorAccessibilit.highlightColor,
|
|
105
216
|
highlightColorInNonActiveTheme = _useColorAccessibilit.highlightColorInNonActiveTheme,
|
|
217
|
+
isMultiHighlightColor = _useColorAccessibilit.isMultiHighlightColor,
|
|
218
|
+
isMultiTextColor = _useColorAccessibilit.isMultiTextColor,
|
|
106
219
|
textColor = _useColorAccessibilit.textColor,
|
|
107
220
|
textColorInNonActiveTheme = _useColorAccessibilit.textColorInNonActiveTheme;
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
221
|
+
// The multi-color selection handling (walking every unique text/highlight
|
|
222
|
+
// pair across the active and non-active theme) is only performed when the
|
|
223
|
+
// patch gate is on. When the gate is off we fall back to the legacy behavior
|
|
224
|
+
// of only evaluating the single active text/highlight color in the current
|
|
225
|
+
// active theme
|
|
226
|
+
var isMultiColorSelection = Boolean(isMultiTextColor || isMultiHighlightColor) && (0, _platformFeatureFlags.fg)('platform_editor_lovability_text_bg_color_patch_1');
|
|
227
|
+
|
|
228
|
+
// The complement of `isMultiColorSelection`: the single active text/highlight
|
|
229
|
+
// pair is evaluated whenever we are not walking a multi-color selection. This
|
|
230
|
+
// covers both a genuinely single-color selection (gate on) and every
|
|
231
|
+
// selection when the gate is off (legacy behavior).
|
|
232
|
+
var isSingleColorSelection = !isMultiColorSelection;
|
|
233
|
+
|
|
234
|
+
// Memoize the selection walk so we don't re-traverse the entire ProseMirror
|
|
235
|
+
// document on every render. It only needs to recompute when the multi-color
|
|
236
|
+
// selection state or the inputs to the walk change (the selection flags,
|
|
237
|
+
// default color, or the plugin api).
|
|
238
|
+
var multiColorStatus = _react.default.useMemo(function () {
|
|
239
|
+
return isMultiColorSelection ? getWorstAccessibilityStatusFromSelection(api, defaultColor) : null;
|
|
240
|
+
},
|
|
241
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
242
|
+
[isMultiColorSelection, isMultiTextColor, isMultiHighlightColor, defaultColor, api]);
|
|
243
|
+
|
|
244
|
+
// The single active text/highlight pair path. Everything below only applies
|
|
245
|
+
// when we are not walking a multi-color selection
|
|
246
|
+
var singleColorStatus = null;
|
|
247
|
+
if (isSingleColorSelection) {
|
|
248
|
+
var singleColorContrastRatio = getContrastRatio(defaultColor, highlightColor, textColor);
|
|
249
|
+
|
|
250
|
+
// Also account for the non-active theme (gate on only) so the worst-case
|
|
251
|
+
// (least accessible) status across both themes is shown.
|
|
252
|
+
var nonActiveThemeSingleColorRatio = null;
|
|
111
253
|
if (textColorInNonActiveTheme && highlightColorInNonActiveTheme && (0, _platformFeatureFlags.fg)('platform_editor_lovability_text_bg_color_patch_1')) {
|
|
112
|
-
|
|
254
|
+
try {
|
|
255
|
+
nonActiveThemeSingleColorRatio = (0, _colorContrast.getContrastRatio)(textColorInNonActiveTheme, highlightColorInNonActiveTheme);
|
|
256
|
+
} catch (_unused3) {
|
|
257
|
+
// if we failed to calculate the contrast ratio, leave as null
|
|
258
|
+
nonActiveThemeSingleColorRatio = null;
|
|
259
|
+
}
|
|
113
260
|
}
|
|
114
|
-
|
|
115
|
-
|
|
261
|
+
var mostCriticalContrastRatio = singleColorContrastRatio !== null && nonActiveThemeSingleColorRatio !== null ? Math.min(singleColorContrastRatio, nonActiveThemeSingleColorRatio) : singleColorContrastRatio;
|
|
262
|
+
singleColorStatus = mostCriticalContrastRatio !== null ? getAccessibilityStatus(mostCriticalContrastRatio) : null;
|
|
116
263
|
}
|
|
117
|
-
var
|
|
118
|
-
if (
|
|
264
|
+
var accessibilityStatus = isSingleColorSelection ? singleColorStatus : multiColorStatus;
|
|
265
|
+
if (accessibilityStatus === null) {
|
|
119
266
|
return /*#__PURE__*/_react.default.createElement(_react.default.Fragment, null);
|
|
120
267
|
}
|
|
121
|
-
var accessibilityStatus = getAccessibilityStatus(mostCriticalContrastRatio);
|
|
122
268
|
var tooltipContent = function tooltipContent(accessibilityStatus) {
|
|
123
269
|
if (accessibilityStatus === 'accessible') {
|
|
124
270
|
return formatMessage(_messages.colorAccessibilityMessages.accessibleTooltip);
|
|
@@ -1,19 +1,11 @@
|
|
|
1
1
|
import { SafePlugin } from '@atlaskit/editor-common/safe-plugin';
|
|
2
|
-
import {
|
|
3
|
-
import { hexToEditorTextPaletteColor } from '@atlaskit/editor-palette/text';
|
|
2
|
+
import { getTextColorInNonActiveTheme, textColorPalette, textColorPaletteNew } from '@atlaskit/editor-common/ui-color';
|
|
4
3
|
import { PluginKey } from '@atlaskit/editor-prosemirror/state';
|
|
5
4
|
import { fg } from '@atlaskit/platform-feature-flags';
|
|
6
5
|
import { expValEqualsNoExposure } from '@atlaskit/tmp-editor-statsig/exp-val-equals-no-exposure';
|
|
7
|
-
import { getActiveColor } from './utils/color';
|
|
6
|
+
import { getActiveColor, isMultiTextColorSelection } from './utils/color';
|
|
8
7
|
import { DEFAULT_COLOR } from './utils/constants';
|
|
9
8
|
import { getDisabledState } from './utils/disabled';
|
|
10
|
-
const getColorInNonActiveTheme = (color, defaultColor) => {
|
|
11
|
-
if (!color || color === defaultColor) {
|
|
12
|
-
return getTokenCSSVariableValueForNonActiveTheme("var(--ds-text, #292A2E)", defaultColor);
|
|
13
|
-
}
|
|
14
|
-
const colorValue = hexToEditorTextPaletteColor(color) || color;
|
|
15
|
-
return getTokenCSSVariableValueForNonActiveTheme(colorValue, color);
|
|
16
|
-
};
|
|
17
9
|
function createInitialPluginState(editorState, pluginConfig) {
|
|
18
10
|
const defaultColor = (pluginConfig === null || pluginConfig === void 0 ? void 0 : pluginConfig.defaultColor) || DEFAULT_COLOR;
|
|
19
11
|
const paletteColors = expValEqualsNoExposure('platform_editor_lovability_text_bg_color', 'isEnabled', true) ? textColorPaletteNew : textColorPalette;
|
|
@@ -31,7 +23,8 @@ function createInitialPluginState(editorState, pluginConfig) {
|
|
|
31
23
|
isPaletteOpen: false,
|
|
32
24
|
// eslint-disable-next-line @atlaskit/platform/no-preconditioning
|
|
33
25
|
...(fg('platform_editor_lovability_text_bg_color_patch_1') && expValEqualsNoExposure('platform_editor_lovability_text_bg_color', 'isEnabled', true) && {
|
|
34
|
-
colorInNonActiveTheme:
|
|
26
|
+
colorInNonActiveTheme: getTextColorInNonActiveTheme(color, defaultColor.color),
|
|
27
|
+
isMultiTextColor: isMultiTextColorSelection(editorState)
|
|
35
28
|
})
|
|
36
29
|
};
|
|
37
30
|
return state;
|
|
@@ -61,7 +54,8 @@ export function createPlugin(dispatch, pluginConfig) {
|
|
|
61
54
|
color: pluginState.defaultColor,
|
|
62
55
|
// eslint-disable-next-line @atlaskit/platform/no-preconditioning
|
|
63
56
|
...(fg('platform_editor_lovability_text_bg_color_patch_1') && expValEqualsNoExposure('platform_editor_lovability_text_bg_color', 'isEnabled', true) && {
|
|
64
|
-
colorInNonActiveTheme:
|
|
57
|
+
colorInNonActiveTheme: getTextColorInNonActiveTheme(pluginState.defaultColor, pluginState.defaultColor),
|
|
58
|
+
isMultiTextColor: false
|
|
65
59
|
})
|
|
66
60
|
};
|
|
67
61
|
break;
|
|
@@ -73,7 +67,8 @@ export function createPlugin(dispatch, pluginConfig) {
|
|
|
73
67
|
isPaletteOpen: false,
|
|
74
68
|
// eslint-disable-next-line @atlaskit/platform/no-preconditioning
|
|
75
69
|
...(fg('platform_editor_lovability_text_bg_color_patch_1') && expValEqualsNoExposure('platform_editor_lovability_text_bg_color', 'isEnabled', true) && {
|
|
76
|
-
colorInNonActiveTheme:
|
|
70
|
+
colorInNonActiveTheme: getTextColorInNonActiveTheme(meta.color, pluginState.defaultColor),
|
|
71
|
+
isMultiTextColor: false
|
|
77
72
|
})
|
|
78
73
|
};
|
|
79
74
|
break;
|
|
@@ -97,11 +92,12 @@ export function createPlugin(dispatch, pluginConfig) {
|
|
|
97
92
|
disabled: getDisabledState(newState),
|
|
98
93
|
// eslint-disable-next-line @atlaskit/platform/no-preconditioning
|
|
99
94
|
...(color !== pluginState.color && fg('platform_editor_lovability_text_bg_color_patch_1') && expValEqualsNoExposure('platform_editor_lovability_text_bg_color', 'isEnabled', true) && {
|
|
100
|
-
colorInNonActiveTheme:
|
|
95
|
+
colorInNonActiveTheme: getTextColorInNonActiveTheme(color, pluginState.defaultColor),
|
|
96
|
+
isMultiTextColor: isMultiTextColorSelection(newState)
|
|
101
97
|
})
|
|
102
98
|
};
|
|
103
99
|
}
|
|
104
|
-
if (pluginState && pluginState.color !== nextState.color || pluginState && pluginState.colorInNonActiveTheme !== nextState.colorInNonActiveTheme || pluginState && pluginState.disabled !== nextState.disabled || pluginState && pluginState.isPaletteOpen !== nextState.isPaletteOpen) {
|
|
100
|
+
if (pluginState && pluginState.color !== nextState.color || pluginState && pluginState.colorInNonActiveTheme !== nextState.colorInNonActiveTheme || pluginState && pluginState.disabled !== nextState.disabled || pluginState && pluginState.isMultiTextColor !== nextState.isMultiTextColor || pluginState && pluginState.isPaletteOpen !== nextState.isPaletteOpen) {
|
|
105
101
|
dispatch(pluginKey, nextState);
|
|
106
102
|
return nextState;
|
|
107
103
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { TextSelection } from '@atlaskit/editor-prosemirror/state';
|
|
1
2
|
import { DEFAULT_COLOR } from './constants';
|
|
2
3
|
|
|
3
4
|
/**
|
|
@@ -44,6 +45,40 @@ export const getActiveColor = state => {
|
|
|
44
45
|
}
|
|
45
46
|
return marksWithColor.length ? marksWithColor[0].attrs.color : DEFAULT_COLOR.color;
|
|
46
47
|
};
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Returns true only when the selection contains two or more *distinct* text
|
|
51
|
+
* colors.
|
|
52
|
+
*
|
|
53
|
+
* This is deliberately narrower than `getActiveColor() === null`: `getActiveColor`
|
|
54
|
+
* also returns `null` for a mixed selection where a single colored region sits
|
|
55
|
+
* alongside uncolored text (`marksWithColor.length === 1 && marks.length > 1`).
|
|
56
|
+
* That mixed case is not a multi-color selection and must not trigger the
|
|
57
|
+
* expensive worst-status selection walk, so we key the `isMultiTextColor` flag
|
|
58
|
+
* off this predicate instead.
|
|
59
|
+
*/
|
|
60
|
+
export const isMultiTextColorSelection = state => {
|
|
61
|
+
const {
|
|
62
|
+
selection,
|
|
63
|
+
doc
|
|
64
|
+
} = state;
|
|
65
|
+
if (!(selection instanceof TextSelection)) {
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
const {
|
|
69
|
+
textColor
|
|
70
|
+
} = doc.type.schema.marks;
|
|
71
|
+
const colors = new Set();
|
|
72
|
+
doc.nodesBetween(selection.from, selection.to, node => {
|
|
73
|
+
if (node.isLeaf) {
|
|
74
|
+
const mark = textColor.isInSet(node.marks);
|
|
75
|
+
if (mark) {
|
|
76
|
+
colors.add(mark.attrs.color);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
return colors.size > 1;
|
|
81
|
+
};
|
|
47
82
|
export const getActiveColorNew = tr => {
|
|
48
83
|
const {
|
|
49
84
|
$from,
|
|
@@ -6,8 +6,9 @@ import { useIntl } from 'react-intl';
|
|
|
6
6
|
import { IconButton } from '@atlaskit/button/new';
|
|
7
7
|
import { useSharedPluginStateWithSelector } from '@atlaskit/editor-common/hooks';
|
|
8
8
|
import { colorAccessibilityMessages as messages } from '@atlaskit/editor-common/messages';
|
|
9
|
-
import { getTokenCSSVariableValue } from '@atlaskit/editor-common/ui-color';
|
|
9
|
+
import { getHighlightColorInNonActiveTheme, getTextColorInNonActiveTheme, getTokenCSSVariableValue } from '@atlaskit/editor-common/ui-color';
|
|
10
10
|
import { hexToEditorTextBackgroundPaletteColor, hexToEditorTextPaletteColor } from '@atlaskit/editor-palette';
|
|
11
|
+
import { TextSelection } from '@atlaskit/editor-prosemirror/state';
|
|
11
12
|
import AccessibilityIcon from '@atlaskit/icon/core/accessibility';
|
|
12
13
|
import QuestionCircleIcon from '@atlaskit/icon/core/question-circle';
|
|
13
14
|
import { fg } from '@atlaskit/platform-feature-flags';
|
|
@@ -34,13 +35,15 @@ const getBackgroundColor = highlightColor => {
|
|
|
34
35
|
};
|
|
35
36
|
const useColorAccessibilityState = api => {
|
|
36
37
|
return useSharedPluginStateWithSelector(api, ['textColor', 'highlight'], states => {
|
|
37
|
-
var _states$textColorStat, _states$highlightStat, _states$highlightStat2, _states$textColorStat2, _states$textColorStat3;
|
|
38
|
+
var _states$textColorStat, _states$highlightStat, _states$highlightStat2, _states$highlightStat3, _states$textColorStat2, _states$textColorStat3, _states$textColorStat4;
|
|
38
39
|
return {
|
|
39
40
|
defaultColor: (_states$textColorStat = states.textColorState) === null || _states$textColorStat === void 0 ? void 0 : _states$textColorStat.defaultColor,
|
|
40
41
|
highlightColor: (_states$highlightStat = states.highlightState) === null || _states$highlightStat === void 0 ? void 0 : _states$highlightStat.activeColor,
|
|
41
42
|
highlightColorInNonActiveTheme: (_states$highlightStat2 = states.highlightState) === null || _states$highlightStat2 === void 0 ? void 0 : _states$highlightStat2.activeColorInNonActiveTheme,
|
|
42
|
-
|
|
43
|
-
|
|
43
|
+
isMultiHighlightColor: (_states$highlightStat3 = states.highlightState) === null || _states$highlightStat3 === void 0 ? void 0 : _states$highlightStat3.isMultiHighlightColor,
|
|
44
|
+
isMultiTextColor: (_states$textColorStat2 = states.textColorState) === null || _states$textColorStat2 === void 0 ? void 0 : _states$textColorStat2.isMultiTextColor,
|
|
45
|
+
textColor: (_states$textColorStat3 = states.textColorState) === null || _states$textColorStat3 === void 0 ? void 0 : _states$textColorStat3.color,
|
|
46
|
+
textColorInNonActiveTheme: (_states$textColorStat4 = states.textColorState) === null || _states$textColorStat4 === void 0 ? void 0 : _states$textColorStat4.colorInNonActiveTheme
|
|
44
47
|
};
|
|
45
48
|
});
|
|
46
49
|
};
|
|
@@ -53,6 +56,22 @@ const getContrastRatio = (defaultColor, highlightColor, textColor) => {
|
|
|
53
56
|
return null;
|
|
54
57
|
}
|
|
55
58
|
};
|
|
59
|
+
|
|
60
|
+
// Computes the contrast ratio for a (textColor, highlightColor) pair as it would
|
|
61
|
+
// appear in the non-active theme, using the shared resolvers so the walker
|
|
62
|
+
// evaluates the same colors the single-pair plugin-state path does. Returns null
|
|
63
|
+
// if it cannot be calculated.
|
|
64
|
+
const getNonActiveThemeContrastRatio = (defaultColor, highlightColor, textColor) => {
|
|
65
|
+
try {
|
|
66
|
+
return calcContrastRatio(getTextColorInNonActiveTheme(textColor !== null && textColor !== void 0 ? textColor : null, defaultColor || DEFAULT_COLOR.color), getHighlightColorInNonActiveTheme(highlightColor !== null && highlightColor !== void 0 ? highlightColor : null, {
|
|
67
|
+
defaultBackgroundColor: DEFAULT_BACKGROUND_COLOR,
|
|
68
|
+
transparentColor: TRANSPARENT_HIGHLIGHT_COLOR
|
|
69
|
+
}));
|
|
70
|
+
} catch {
|
|
71
|
+
// if we failed to calculate the contrast ratio, return null
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
};
|
|
56
75
|
const getAccessibilityStatus = contrastRatio => {
|
|
57
76
|
if (contrastRatio >= ACCESSIBLE_CONTRAST_RATIO) {
|
|
58
77
|
return 'accessible';
|
|
@@ -62,6 +81,89 @@ const getAccessibilityStatus = contrastRatio => {
|
|
|
62
81
|
return 'inaccessible';
|
|
63
82
|
}
|
|
64
83
|
};
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Collects the worst accessibility status across all unique (textColor, highlightColor)
|
|
87
|
+
* combinations in the current selection.
|
|
88
|
+
*
|
|
89
|
+
* When the selection spans multiple text colors and/or highlight colors,
|
|
90
|
+
* this walks the document between selection boundaries and checks every
|
|
91
|
+
* text node's color marks, computing the contrast ratio for each unique pair
|
|
92
|
+
* (across both the active and non-active theme) and returning the worst
|
|
93
|
+
* status found.
|
|
94
|
+
*/
|
|
95
|
+
const getWorstAccessibilityStatusFromSelection = (api, defaultColor) => {
|
|
96
|
+
if (!(api !== null && api !== void 0 && api.core)) {
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Track the lowest (worst) contrast ratio across all pairs; a lower ratio
|
|
101
|
+
// means a worse accessibility status, so a single numeric comparison is
|
|
102
|
+
// enough to find the worst pair.
|
|
103
|
+
let worstContrastRatio = null;
|
|
104
|
+
api.core.actions.execute(({
|
|
105
|
+
tr
|
|
106
|
+
}) => {
|
|
107
|
+
// Text/highlight colors only apply to a text range. If the selection is
|
|
108
|
+
// not a text selection (e.g. a node, cell or all selection) there is no
|
|
109
|
+
// meaningful range of colored text to evaluate.
|
|
110
|
+
if (!(tr.selection instanceof TextSelection)) {
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
const {
|
|
114
|
+
from,
|
|
115
|
+
to
|
|
116
|
+
} = tr.selection;
|
|
117
|
+
const seen = new Set();
|
|
118
|
+
tr.doc.nodesBetween(from, to, node => {
|
|
119
|
+
var _textColor, _highlightColor;
|
|
120
|
+
// Text and highlight color marks only apply to text nodes, so skip
|
|
121
|
+
// any non-text leaves (e.g. emoji, mentions, inline cards). Continue
|
|
122
|
+
// descending into container nodes to reach their text children.
|
|
123
|
+
if (!node.isText) {
|
|
124
|
+
return !node.isLeaf;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Extract both color marks in a single pass over the node's marks
|
|
128
|
+
// rather than two `Array.find` scans.
|
|
129
|
+
let textColor = null;
|
|
130
|
+
let highlightColor = null;
|
|
131
|
+
for (const mark of node.marks) {
|
|
132
|
+
if (mark.type.name === 'textColor') {
|
|
133
|
+
var _mark$attrs$color;
|
|
134
|
+
textColor = (_mark$attrs$color = mark.attrs.color) !== null && _mark$attrs$color !== void 0 ? _mark$attrs$color : null;
|
|
135
|
+
} else if (mark.type.name === 'backgroundColor') {
|
|
136
|
+
var _mark$attrs$color2;
|
|
137
|
+
highlightColor = (_mark$attrs$color2 = mark.attrs.color) !== null && _mark$attrs$color2 !== void 0 ? _mark$attrs$color2 : null;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
const pairKey = `${(_textColor = textColor) !== null && _textColor !== void 0 ? _textColor : 'default'}|${(_highlightColor = highlightColor) !== null && _highlightColor !== void 0 ? _highlightColor : 'default'}`;
|
|
141
|
+
// Skip pairs we have already evaluated so the expensive contrast
|
|
142
|
+
// calculations run at most once per unique (text, highlight) pair.
|
|
143
|
+
if (seen.has(pairKey)) {
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
seen.add(pairKey);
|
|
147
|
+
const contrastRatio = getContrastRatio(defaultColor, highlightColor, textColor);
|
|
148
|
+
if (contrastRatio === null) {
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Account for the non-active theme so the worst-case (least
|
|
153
|
+
// accessible) contrast across both themes is used for each pair,
|
|
154
|
+
// keeping multi-color selections consistent with the single-pair path.
|
|
155
|
+
const nonActiveThemeContrastRatio = getNonActiveThemeContrastRatio(defaultColor, highlightColor, textColor);
|
|
156
|
+
const mostCriticalContrastRatio = nonActiveThemeContrastRatio !== null ? Math.min(contrastRatio, nonActiveThemeContrastRatio) : contrastRatio;
|
|
157
|
+
if (worstContrastRatio === null || mostCriticalContrastRatio < worstContrastRatio) {
|
|
158
|
+
worstContrastRatio = mostCriticalContrastRatio;
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
// Read-only operation — do not dispatch
|
|
163
|
+
return null;
|
|
164
|
+
});
|
|
165
|
+
return worstContrastRatio === null ? null : getAccessibilityStatus(worstContrastRatio);
|
|
166
|
+
};
|
|
65
167
|
const AccessibilityStatus = ({
|
|
66
168
|
accessibilityStatus,
|
|
67
169
|
formatMessage
|
|
@@ -99,23 +201,56 @@ export const ColorAccessibilityMenuItem = ({
|
|
|
99
201
|
defaultColor,
|
|
100
202
|
highlightColor,
|
|
101
203
|
highlightColorInNonActiveTheme,
|
|
204
|
+
isMultiHighlightColor,
|
|
205
|
+
isMultiTextColor,
|
|
102
206
|
textColor,
|
|
103
207
|
textColorInNonActiveTheme
|
|
104
208
|
} = useColorAccessibilityState(api);
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
209
|
+
// The multi-color selection handling (walking every unique text/highlight
|
|
210
|
+
// pair across the active and non-active theme) is only performed when the
|
|
211
|
+
// patch gate is on. When the gate is off we fall back to the legacy behavior
|
|
212
|
+
// of only evaluating the single active text/highlight color in the current
|
|
213
|
+
// active theme
|
|
214
|
+
const isMultiColorSelection = Boolean(isMultiTextColor || isMultiHighlightColor) && fg('platform_editor_lovability_text_bg_color_patch_1');
|
|
215
|
+
|
|
216
|
+
// The complement of `isMultiColorSelection`: the single active text/highlight
|
|
217
|
+
// pair is evaluated whenever we are not walking a multi-color selection. This
|
|
218
|
+
// covers both a genuinely single-color selection (gate on) and every
|
|
219
|
+
// selection when the gate is off (legacy behavior).
|
|
220
|
+
const isSingleColorSelection = !isMultiColorSelection;
|
|
221
|
+
|
|
222
|
+
// Memoize the selection walk so we don't re-traverse the entire ProseMirror
|
|
223
|
+
// document on every render. It only needs to recompute when the multi-color
|
|
224
|
+
// selection state or the inputs to the walk change (the selection flags,
|
|
225
|
+
// default color, or the plugin api).
|
|
226
|
+
const multiColorStatus = React.useMemo(() => isMultiColorSelection ? getWorstAccessibilityStatusFromSelection(api, defaultColor) : null,
|
|
227
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
228
|
+
[isMultiColorSelection, isMultiTextColor, isMultiHighlightColor, defaultColor, api]);
|
|
229
|
+
|
|
230
|
+
// The single active text/highlight pair path. Everything below only applies
|
|
231
|
+
// when we are not walking a multi-color selection
|
|
232
|
+
let singleColorStatus = null;
|
|
233
|
+
if (isSingleColorSelection) {
|
|
234
|
+
const singleColorContrastRatio = getContrastRatio(defaultColor, highlightColor, textColor);
|
|
235
|
+
|
|
236
|
+
// Also account for the non-active theme (gate on only) so the worst-case
|
|
237
|
+
// (least accessible) status across both themes is shown.
|
|
238
|
+
let nonActiveThemeSingleColorRatio = null;
|
|
108
239
|
if (textColorInNonActiveTheme && highlightColorInNonActiveTheme && fg('platform_editor_lovability_text_bg_color_patch_1')) {
|
|
109
|
-
|
|
240
|
+
try {
|
|
241
|
+
nonActiveThemeSingleColorRatio = calcContrastRatio(textColorInNonActiveTheme, highlightColorInNonActiveTheme);
|
|
242
|
+
} catch {
|
|
243
|
+
// if we failed to calculate the contrast ratio, leave as null
|
|
244
|
+
nonActiveThemeSingleColorRatio = null;
|
|
245
|
+
}
|
|
110
246
|
}
|
|
111
|
-
|
|
112
|
-
|
|
247
|
+
const mostCriticalContrastRatio = singleColorContrastRatio !== null && nonActiveThemeSingleColorRatio !== null ? Math.min(singleColorContrastRatio, nonActiveThemeSingleColorRatio) : singleColorContrastRatio;
|
|
248
|
+
singleColorStatus = mostCriticalContrastRatio !== null ? getAccessibilityStatus(mostCriticalContrastRatio) : null;
|
|
113
249
|
}
|
|
114
|
-
const
|
|
115
|
-
if (
|
|
250
|
+
const accessibilityStatus = isSingleColorSelection ? singleColorStatus : multiColorStatus;
|
|
251
|
+
if (accessibilityStatus === null) {
|
|
116
252
|
return /*#__PURE__*/React.createElement(React.Fragment, null);
|
|
117
253
|
}
|
|
118
|
-
const accessibilityStatus = getAccessibilityStatus(mostCriticalContrastRatio);
|
|
119
254
|
const tooltipContent = accessibilityStatus => {
|
|
120
255
|
if (accessibilityStatus === 'accessible') {
|
|
121
256
|
return formatMessage(messages.accessibleTooltip);
|
|
@@ -3,21 +3,13 @@ import _toConsumableArray from "@babel/runtime/helpers/toConsumableArray";
|
|
|
3
3
|
function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; }
|
|
4
4
|
function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; }
|
|
5
5
|
import { SafePlugin } from '@atlaskit/editor-common/safe-plugin';
|
|
6
|
-
import {
|
|
7
|
-
import { hexToEditorTextPaletteColor } from '@atlaskit/editor-palette/text';
|
|
6
|
+
import { getTextColorInNonActiveTheme, textColorPalette, textColorPaletteNew } from '@atlaskit/editor-common/ui-color';
|
|
8
7
|
import { PluginKey } from '@atlaskit/editor-prosemirror/state';
|
|
9
8
|
import { fg } from '@atlaskit/platform-feature-flags';
|
|
10
9
|
import { expValEqualsNoExposure } from '@atlaskit/tmp-editor-statsig/exp-val-equals-no-exposure';
|
|
11
|
-
import { getActiveColor } from './utils/color';
|
|
10
|
+
import { getActiveColor, isMultiTextColorSelection } from './utils/color';
|
|
12
11
|
import { DEFAULT_COLOR } from './utils/constants';
|
|
13
12
|
import { getDisabledState } from './utils/disabled';
|
|
14
|
-
var getColorInNonActiveTheme = function getColorInNonActiveTheme(color, defaultColor) {
|
|
15
|
-
if (!color || color === defaultColor) {
|
|
16
|
-
return getTokenCSSVariableValueForNonActiveTheme("var(--ds-text, #292A2E)", defaultColor);
|
|
17
|
-
}
|
|
18
|
-
var colorValue = hexToEditorTextPaletteColor(color) || color;
|
|
19
|
-
return getTokenCSSVariableValueForNonActiveTheme(colorValue, color);
|
|
20
|
-
};
|
|
21
13
|
function createInitialPluginState(editorState, pluginConfig) {
|
|
22
14
|
var defaultColor = (pluginConfig === null || pluginConfig === void 0 ? void 0 : pluginConfig.defaultColor) || DEFAULT_COLOR;
|
|
23
15
|
var paletteColors = expValEqualsNoExposure('platform_editor_lovability_text_bg_color', 'isEnabled', true) ? textColorPaletteNew : textColorPalette;
|
|
@@ -34,7 +26,8 @@ function createInitialPluginState(editorState, pluginConfig) {
|
|
|
34
26
|
defaultColor: defaultColor.color,
|
|
35
27
|
isPaletteOpen: false
|
|
36
28
|
}, fg('platform_editor_lovability_text_bg_color_patch_1') && expValEqualsNoExposure('platform_editor_lovability_text_bg_color', 'isEnabled', true) && {
|
|
37
|
-
colorInNonActiveTheme:
|
|
29
|
+
colorInNonActiveTheme: getTextColorInNonActiveTheme(color, defaultColor.color),
|
|
30
|
+
isMultiTextColor: isMultiTextColorSelection(editorState)
|
|
38
31
|
});
|
|
39
32
|
return state;
|
|
40
33
|
}
|
|
@@ -61,7 +54,8 @@ export function createPlugin(dispatch, pluginConfig) {
|
|
|
61
54
|
nextState = _objectSpread(_objectSpread({}, pluginState), {}, {
|
|
62
55
|
color: pluginState.defaultColor
|
|
63
56
|
}, fg('platform_editor_lovability_text_bg_color_patch_1') && expValEqualsNoExposure('platform_editor_lovability_text_bg_color', 'isEnabled', true) && {
|
|
64
|
-
colorInNonActiveTheme:
|
|
57
|
+
colorInNonActiveTheme: getTextColorInNonActiveTheme(pluginState.defaultColor, pluginState.defaultColor),
|
|
58
|
+
isMultiTextColor: false
|
|
65
59
|
});
|
|
66
60
|
break;
|
|
67
61
|
case ACTIONS.SET_COLOR:
|
|
@@ -70,7 +64,8 @@ export function createPlugin(dispatch, pluginConfig) {
|
|
|
70
64
|
disabled: false,
|
|
71
65
|
isPaletteOpen: false
|
|
72
66
|
}, fg('platform_editor_lovability_text_bg_color_patch_1') && expValEqualsNoExposure('platform_editor_lovability_text_bg_color', 'isEnabled', true) && {
|
|
73
|
-
colorInNonActiveTheme:
|
|
67
|
+
colorInNonActiveTheme: getTextColorInNonActiveTheme(meta.color, pluginState.defaultColor),
|
|
68
|
+
isMultiTextColor: false
|
|
74
69
|
});
|
|
75
70
|
break;
|
|
76
71
|
case ACTIONS.DISABLE:
|
|
@@ -89,10 +84,11 @@ export function createPlugin(dispatch, pluginConfig) {
|
|
|
89
84
|
color: color,
|
|
90
85
|
disabled: getDisabledState(newState)
|
|
91
86
|
}, color !== pluginState.color && fg('platform_editor_lovability_text_bg_color_patch_1') && expValEqualsNoExposure('platform_editor_lovability_text_bg_color', 'isEnabled', true) && {
|
|
92
|
-
colorInNonActiveTheme:
|
|
87
|
+
colorInNonActiveTheme: getTextColorInNonActiveTheme(color, pluginState.defaultColor),
|
|
88
|
+
isMultiTextColor: isMultiTextColorSelection(newState)
|
|
93
89
|
});
|
|
94
90
|
}
|
|
95
|
-
if (pluginState && pluginState.color !== nextState.color || pluginState && pluginState.colorInNonActiveTheme !== nextState.colorInNonActiveTheme || pluginState && pluginState.disabled !== nextState.disabled || pluginState && pluginState.isPaletteOpen !== nextState.isPaletteOpen) {
|
|
91
|
+
if (pluginState && pluginState.color !== nextState.color || pluginState && pluginState.colorInNonActiveTheme !== nextState.colorInNonActiveTheme || pluginState && pluginState.disabled !== nextState.disabled || pluginState && pluginState.isMultiTextColor !== nextState.isMultiTextColor || pluginState && pluginState.isPaletteOpen !== nextState.isPaletteOpen) {
|
|
96
92
|
dispatch(pluginKey, nextState);
|
|
97
93
|
return nextState;
|
|
98
94
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { TextSelection } from '@atlaskit/editor-prosemirror/state';
|
|
1
2
|
import { DEFAULT_COLOR } from './constants';
|
|
2
3
|
|
|
3
4
|
/**
|
|
@@ -44,6 +45,36 @@ export var getActiveColor = function getActiveColor(state) {
|
|
|
44
45
|
}
|
|
45
46
|
return marksWithColor.length ? marksWithColor[0].attrs.color : DEFAULT_COLOR.color;
|
|
46
47
|
};
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Returns true only when the selection contains two or more *distinct* text
|
|
51
|
+
* colors.
|
|
52
|
+
*
|
|
53
|
+
* This is deliberately narrower than `getActiveColor() === null`: `getActiveColor`
|
|
54
|
+
* also returns `null` for a mixed selection where a single colored region sits
|
|
55
|
+
* alongside uncolored text (`marksWithColor.length === 1 && marks.length > 1`).
|
|
56
|
+
* That mixed case is not a multi-color selection and must not trigger the
|
|
57
|
+
* expensive worst-status selection walk, so we key the `isMultiTextColor` flag
|
|
58
|
+
* off this predicate instead.
|
|
59
|
+
*/
|
|
60
|
+
export var isMultiTextColorSelection = function isMultiTextColorSelection(state) {
|
|
61
|
+
var selection = state.selection,
|
|
62
|
+
doc = state.doc;
|
|
63
|
+
if (!(selection instanceof TextSelection)) {
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
var textColor = doc.type.schema.marks.textColor;
|
|
67
|
+
var colors = new Set();
|
|
68
|
+
doc.nodesBetween(selection.from, selection.to, function (node) {
|
|
69
|
+
if (node.isLeaf) {
|
|
70
|
+
var mark = textColor.isInSet(node.marks);
|
|
71
|
+
if (mark) {
|
|
72
|
+
colors.add(mark.attrs.color);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
return colors.size > 1;
|
|
77
|
+
};
|
|
47
78
|
export var getActiveColorNew = function getActiveColorNew(tr) {
|
|
48
79
|
var _ref3 = tr.selection,
|
|
49
80
|
$from = _ref3.$from,
|
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
/* ColorAccessibilityMenuItem.tsx generated by @compiled/babel-plugin v0.39.1 */
|
|
2
2
|
import "./ColorAccessibilityMenuItem.compiled.css";
|
|
3
3
|
import { ax, ix } from "@compiled/react/runtime";
|
|
4
|
+
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; } } }; }
|
|
5
|
+
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; } }
|
|
6
|
+
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; }
|
|
4
7
|
import React from 'react';
|
|
5
8
|
import { useIntl } from 'react-intl';
|
|
6
9
|
import { IconButton } from '@atlaskit/button/new';
|
|
7
10
|
import { useSharedPluginStateWithSelector } from '@atlaskit/editor-common/hooks';
|
|
8
11
|
import { colorAccessibilityMessages as messages } from '@atlaskit/editor-common/messages';
|
|
9
|
-
import { getTokenCSSVariableValue } from '@atlaskit/editor-common/ui-color';
|
|
12
|
+
import { getHighlightColorInNonActiveTheme, getTextColorInNonActiveTheme, getTokenCSSVariableValue } from '@atlaskit/editor-common/ui-color';
|
|
10
13
|
import { hexToEditorTextBackgroundPaletteColor, hexToEditorTextPaletteColor } from '@atlaskit/editor-palette';
|
|
14
|
+
import { TextSelection } from '@atlaskit/editor-prosemirror/state';
|
|
11
15
|
import AccessibilityIcon from '@atlaskit/icon/core/accessibility';
|
|
12
16
|
import QuestionCircleIcon from '@atlaskit/icon/core/question-circle';
|
|
13
17
|
import { fg } from '@atlaskit/platform-feature-flags';
|
|
@@ -34,13 +38,15 @@ var getBackgroundColor = function getBackgroundColor(highlightColor) {
|
|
|
34
38
|
};
|
|
35
39
|
var useColorAccessibilityState = function useColorAccessibilityState(api) {
|
|
36
40
|
return useSharedPluginStateWithSelector(api, ['textColor', 'highlight'], function (states) {
|
|
37
|
-
var _states$textColorStat, _states$highlightStat, _states$highlightStat2, _states$textColorStat2, _states$textColorStat3;
|
|
41
|
+
var _states$textColorStat, _states$highlightStat, _states$highlightStat2, _states$highlightStat3, _states$textColorStat2, _states$textColorStat3, _states$textColorStat4;
|
|
38
42
|
return {
|
|
39
43
|
defaultColor: (_states$textColorStat = states.textColorState) === null || _states$textColorStat === void 0 ? void 0 : _states$textColorStat.defaultColor,
|
|
40
44
|
highlightColor: (_states$highlightStat = states.highlightState) === null || _states$highlightStat === void 0 ? void 0 : _states$highlightStat.activeColor,
|
|
41
45
|
highlightColorInNonActiveTheme: (_states$highlightStat2 = states.highlightState) === null || _states$highlightStat2 === void 0 ? void 0 : _states$highlightStat2.activeColorInNonActiveTheme,
|
|
42
|
-
|
|
43
|
-
|
|
46
|
+
isMultiHighlightColor: (_states$highlightStat3 = states.highlightState) === null || _states$highlightStat3 === void 0 ? void 0 : _states$highlightStat3.isMultiHighlightColor,
|
|
47
|
+
isMultiTextColor: (_states$textColorStat2 = states.textColorState) === null || _states$textColorStat2 === void 0 ? void 0 : _states$textColorStat2.isMultiTextColor,
|
|
48
|
+
textColor: (_states$textColorStat3 = states.textColorState) === null || _states$textColorStat3 === void 0 ? void 0 : _states$textColorStat3.color,
|
|
49
|
+
textColorInNonActiveTheme: (_states$textColorStat4 = states.textColorState) === null || _states$textColorStat4 === void 0 ? void 0 : _states$textColorStat4.colorInNonActiveTheme
|
|
44
50
|
};
|
|
45
51
|
});
|
|
46
52
|
};
|
|
@@ -53,6 +59,22 @@ var getContrastRatio = function getContrastRatio(defaultColor, highlightColor, t
|
|
|
53
59
|
return null;
|
|
54
60
|
}
|
|
55
61
|
};
|
|
62
|
+
|
|
63
|
+
// Computes the contrast ratio for a (textColor, highlightColor) pair as it would
|
|
64
|
+
// appear in the non-active theme, using the shared resolvers so the walker
|
|
65
|
+
// evaluates the same colors the single-pair plugin-state path does. Returns null
|
|
66
|
+
// if it cannot be calculated.
|
|
67
|
+
var getNonActiveThemeContrastRatio = function getNonActiveThemeContrastRatio(defaultColor, highlightColor, textColor) {
|
|
68
|
+
try {
|
|
69
|
+
return calcContrastRatio(getTextColorInNonActiveTheme(textColor !== null && textColor !== void 0 ? textColor : null, defaultColor || DEFAULT_COLOR.color), getHighlightColorInNonActiveTheme(highlightColor !== null && highlightColor !== void 0 ? highlightColor : null, {
|
|
70
|
+
defaultBackgroundColor: DEFAULT_BACKGROUND_COLOR,
|
|
71
|
+
transparentColor: TRANSPARENT_HIGHLIGHT_COLOR
|
|
72
|
+
}));
|
|
73
|
+
} catch (_unused2) {
|
|
74
|
+
// if we failed to calculate the contrast ratio, return null
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
};
|
|
56
78
|
var getAccessibilityStatus = function getAccessibilityStatus(contrastRatio) {
|
|
57
79
|
if (contrastRatio >= ACCESSIBLE_CONTRAST_RATIO) {
|
|
58
80
|
return 'accessible';
|
|
@@ -62,9 +84,98 @@ var getAccessibilityStatus = function getAccessibilityStatus(contrastRatio) {
|
|
|
62
84
|
return 'inaccessible';
|
|
63
85
|
}
|
|
64
86
|
};
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Collects the worst accessibility status across all unique (textColor, highlightColor)
|
|
90
|
+
* combinations in the current selection.
|
|
91
|
+
*
|
|
92
|
+
* When the selection spans multiple text colors and/or highlight colors,
|
|
93
|
+
* this walks the document between selection boundaries and checks every
|
|
94
|
+
* text node's color marks, computing the contrast ratio for each unique pair
|
|
95
|
+
* (across both the active and non-active theme) and returning the worst
|
|
96
|
+
* status found.
|
|
97
|
+
*/
|
|
98
|
+
var getWorstAccessibilityStatusFromSelection = function getWorstAccessibilityStatusFromSelection(api, defaultColor) {
|
|
99
|
+
if (!(api !== null && api !== void 0 && api.core)) {
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Track the lowest (worst) contrast ratio across all pairs; a lower ratio
|
|
104
|
+
// means a worse accessibility status, so a single numeric comparison is
|
|
105
|
+
// enough to find the worst pair.
|
|
106
|
+
var worstContrastRatio = null;
|
|
107
|
+
api.core.actions.execute(function (_ref) {
|
|
108
|
+
var tr = _ref.tr;
|
|
109
|
+
// Text/highlight colors only apply to a text range. If the selection is
|
|
110
|
+
// not a text selection (e.g. a node, cell or all selection) there is no
|
|
111
|
+
// meaningful range of colored text to evaluate.
|
|
112
|
+
if (!(tr.selection instanceof TextSelection)) {
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
var _tr$selection = tr.selection,
|
|
116
|
+
from = _tr$selection.from,
|
|
117
|
+
to = _tr$selection.to;
|
|
118
|
+
var seen = new Set();
|
|
119
|
+
tr.doc.nodesBetween(from, to, function (node) {
|
|
120
|
+
// Text and highlight color marks only apply to text nodes, so skip
|
|
121
|
+
// any non-text leaves (e.g. emoji, mentions, inline cards). Continue
|
|
122
|
+
// descending into container nodes to reach their text children.
|
|
123
|
+
if (!node.isText) {
|
|
124
|
+
return !node.isLeaf;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Extract both color marks in a single pass over the node's marks
|
|
128
|
+
// rather than two `Array.find` scans.
|
|
129
|
+
var textColor = null;
|
|
130
|
+
var highlightColor = null;
|
|
131
|
+
var _iterator = _createForOfIteratorHelper(node.marks),
|
|
132
|
+
_step;
|
|
133
|
+
try {
|
|
134
|
+
for (_iterator.s(); !(_step = _iterator.n()).done;) {
|
|
135
|
+
var mark = _step.value;
|
|
136
|
+
if (mark.type.name === 'textColor') {
|
|
137
|
+
var _mark$attrs$color;
|
|
138
|
+
textColor = (_mark$attrs$color = mark.attrs.color) !== null && _mark$attrs$color !== void 0 ? _mark$attrs$color : null;
|
|
139
|
+
} else if (mark.type.name === 'backgroundColor') {
|
|
140
|
+
var _mark$attrs$color2;
|
|
141
|
+
highlightColor = (_mark$attrs$color2 = mark.attrs.color) !== null && _mark$attrs$color2 !== void 0 ? _mark$attrs$color2 : null;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
} catch (err) {
|
|
145
|
+
_iterator.e(err);
|
|
146
|
+
} finally {
|
|
147
|
+
_iterator.f();
|
|
148
|
+
}
|
|
149
|
+
var pairKey = "".concat(textColor !== null && textColor !== void 0 ? textColor : 'default', "|").concat(highlightColor !== null && highlightColor !== void 0 ? highlightColor : 'default');
|
|
150
|
+
// Skip pairs we have already evaluated so the expensive contrast
|
|
151
|
+
// calculations run at most once per unique (text, highlight) pair.
|
|
152
|
+
if (seen.has(pairKey)) {
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
seen.add(pairKey);
|
|
156
|
+
var contrastRatio = getContrastRatio(defaultColor, highlightColor, textColor);
|
|
157
|
+
if (contrastRatio === null) {
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Account for the non-active theme so the worst-case (least
|
|
162
|
+
// accessible) contrast across both themes is used for each pair,
|
|
163
|
+
// keeping multi-color selections consistent with the single-pair path.
|
|
164
|
+
var nonActiveThemeContrastRatio = getNonActiveThemeContrastRatio(defaultColor, highlightColor, textColor);
|
|
165
|
+
var mostCriticalContrastRatio = nonActiveThemeContrastRatio !== null ? Math.min(contrastRatio, nonActiveThemeContrastRatio) : contrastRatio;
|
|
166
|
+
if (worstContrastRatio === null || mostCriticalContrastRatio < worstContrastRatio) {
|
|
167
|
+
worstContrastRatio = mostCriticalContrastRatio;
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
// Read-only operation — do not dispatch
|
|
172
|
+
return null;
|
|
173
|
+
});
|
|
174
|
+
return worstContrastRatio === null ? null : getAccessibilityStatus(worstContrastRatio);
|
|
175
|
+
};
|
|
176
|
+
var AccessibilityStatus = function AccessibilityStatus(_ref2) {
|
|
177
|
+
var accessibilityStatus = _ref2.accessibilityStatus,
|
|
178
|
+
formatMessage = _ref2.formatMessage;
|
|
68
179
|
if (accessibilityStatus === 'accessible') {
|
|
69
180
|
return /*#__PURE__*/React.createElement(Text, {
|
|
70
181
|
as: "span",
|
|
@@ -88,30 +199,65 @@ var AccessibilityStatus = function AccessibilityStatus(_ref) {
|
|
|
88
199
|
var styles = {
|
|
89
200
|
container: "_19itglyw _19pku2gc _ca0q1b66 _n3td1b66 _19bvu2gc _u5f31b66 _1e0c1txw _4cvr1h6o _1bah1yb4"
|
|
90
201
|
};
|
|
91
|
-
export var ColorAccessibilityMenuItem = function ColorAccessibilityMenuItem(
|
|
92
|
-
var api =
|
|
202
|
+
export var ColorAccessibilityMenuItem = function ColorAccessibilityMenuItem(_ref3) {
|
|
203
|
+
var api = _ref3.api;
|
|
93
204
|
var _useIntl = useIntl(),
|
|
94
205
|
formatMessage = _useIntl.formatMessage;
|
|
95
206
|
var _useColorAccessibilit = useColorAccessibilityState(api),
|
|
96
207
|
defaultColor = _useColorAccessibilit.defaultColor,
|
|
97
208
|
highlightColor = _useColorAccessibilit.highlightColor,
|
|
98
209
|
highlightColorInNonActiveTheme = _useColorAccessibilit.highlightColorInNonActiveTheme,
|
|
210
|
+
isMultiHighlightColor = _useColorAccessibilit.isMultiHighlightColor,
|
|
211
|
+
isMultiTextColor = _useColorAccessibilit.isMultiTextColor,
|
|
99
212
|
textColor = _useColorAccessibilit.textColor,
|
|
100
213
|
textColorInNonActiveTheme = _useColorAccessibilit.textColorInNonActiveTheme;
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
214
|
+
// The multi-color selection handling (walking every unique text/highlight
|
|
215
|
+
// pair across the active and non-active theme) is only performed when the
|
|
216
|
+
// patch gate is on. When the gate is off we fall back to the legacy behavior
|
|
217
|
+
// of only evaluating the single active text/highlight color in the current
|
|
218
|
+
// active theme
|
|
219
|
+
var isMultiColorSelection = Boolean(isMultiTextColor || isMultiHighlightColor) && fg('platform_editor_lovability_text_bg_color_patch_1');
|
|
220
|
+
|
|
221
|
+
// The complement of `isMultiColorSelection`: the single active text/highlight
|
|
222
|
+
// pair is evaluated whenever we are not walking a multi-color selection. This
|
|
223
|
+
// covers both a genuinely single-color selection (gate on) and every
|
|
224
|
+
// selection when the gate is off (legacy behavior).
|
|
225
|
+
var isSingleColorSelection = !isMultiColorSelection;
|
|
226
|
+
|
|
227
|
+
// Memoize the selection walk so we don't re-traverse the entire ProseMirror
|
|
228
|
+
// document on every render. It only needs to recompute when the multi-color
|
|
229
|
+
// selection state or the inputs to the walk change (the selection flags,
|
|
230
|
+
// default color, or the plugin api).
|
|
231
|
+
var multiColorStatus = React.useMemo(function () {
|
|
232
|
+
return isMultiColorSelection ? getWorstAccessibilityStatusFromSelection(api, defaultColor) : null;
|
|
233
|
+
},
|
|
234
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
235
|
+
[isMultiColorSelection, isMultiTextColor, isMultiHighlightColor, defaultColor, api]);
|
|
236
|
+
|
|
237
|
+
// The single active text/highlight pair path. Everything below only applies
|
|
238
|
+
// when we are not walking a multi-color selection
|
|
239
|
+
var singleColorStatus = null;
|
|
240
|
+
if (isSingleColorSelection) {
|
|
241
|
+
var singleColorContrastRatio = getContrastRatio(defaultColor, highlightColor, textColor);
|
|
242
|
+
|
|
243
|
+
// Also account for the non-active theme (gate on only) so the worst-case
|
|
244
|
+
// (least accessible) status across both themes is shown.
|
|
245
|
+
var nonActiveThemeSingleColorRatio = null;
|
|
104
246
|
if (textColorInNonActiveTheme && highlightColorInNonActiveTheme && fg('platform_editor_lovability_text_bg_color_patch_1')) {
|
|
105
|
-
|
|
247
|
+
try {
|
|
248
|
+
nonActiveThemeSingleColorRatio = calcContrastRatio(textColorInNonActiveTheme, highlightColorInNonActiveTheme);
|
|
249
|
+
} catch (_unused3) {
|
|
250
|
+
// if we failed to calculate the contrast ratio, leave as null
|
|
251
|
+
nonActiveThemeSingleColorRatio = null;
|
|
252
|
+
}
|
|
106
253
|
}
|
|
107
|
-
|
|
108
|
-
|
|
254
|
+
var mostCriticalContrastRatio = singleColorContrastRatio !== null && nonActiveThemeSingleColorRatio !== null ? Math.min(singleColorContrastRatio, nonActiveThemeSingleColorRatio) : singleColorContrastRatio;
|
|
255
|
+
singleColorStatus = mostCriticalContrastRatio !== null ? getAccessibilityStatus(mostCriticalContrastRatio) : null;
|
|
109
256
|
}
|
|
110
|
-
var
|
|
111
|
-
if (
|
|
257
|
+
var accessibilityStatus = isSingleColorSelection ? singleColorStatus : multiColorStatus;
|
|
258
|
+
if (accessibilityStatus === null) {
|
|
112
259
|
return /*#__PURE__*/React.createElement(React.Fragment, null);
|
|
113
260
|
}
|
|
114
|
-
var accessibilityStatus = getAccessibilityStatus(mostCriticalContrastRatio);
|
|
115
261
|
var tooltipContent = function tooltipContent(accessibilityStatus) {
|
|
116
262
|
if (accessibilityStatus === 'accessible') {
|
|
117
263
|
return formatMessage(messages.accessibleTooltip);
|
|
@@ -8,6 +8,11 @@ export type TextColorPluginState = {
|
|
|
8
8
|
colorInNonActiveTheme?: string;
|
|
9
9
|
defaultColor: string;
|
|
10
10
|
disabled?: boolean;
|
|
11
|
+
/**
|
|
12
|
+
* True when the current selection spans more than one text color. Only
|
|
13
|
+
* populated behind the lovability text/bg color patch gate + experiment.
|
|
14
|
+
*/
|
|
15
|
+
isMultiTextColor?: boolean;
|
|
11
16
|
isPaletteOpen?: boolean;
|
|
12
17
|
palette: Array<PaletteColor>;
|
|
13
18
|
};
|
|
@@ -3,4 +3,16 @@ import type { EditorState, Transaction } from '@atlaskit/editor-prosemirror/stat
|
|
|
3
3
|
* Use getDisabledStateNew instead and pass in `tr`
|
|
4
4
|
*/
|
|
5
5
|
export declare const getActiveColor: (state: EditorState) => string | null;
|
|
6
|
+
/**
|
|
7
|
+
* Returns true only when the selection contains two or more *distinct* text
|
|
8
|
+
* colors.
|
|
9
|
+
*
|
|
10
|
+
* This is deliberately narrower than `getActiveColor() === null`: `getActiveColor`
|
|
11
|
+
* also returns `null` for a mixed selection where a single colored region sits
|
|
12
|
+
* alongside uncolored text (`marksWithColor.length === 1 && marks.length > 1`).
|
|
13
|
+
* That mixed case is not a multi-color selection and must not trigger the
|
|
14
|
+
* expensive worst-status selection walk, so we key the `isMultiTextColor` flag
|
|
15
|
+
* off this predicate instead.
|
|
16
|
+
*/
|
|
17
|
+
export declare const isMultiTextColorSelection: (state: EditorState) => boolean;
|
|
6
18
|
export declare const getActiveColorNew: (tr: Transaction) => string | null;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@atlaskit/editor-plugin-text-color",
|
|
3
|
-
"version": "12.3.
|
|
3
|
+
"version": "12.3.19",
|
|
4
4
|
"description": "Text color plugin for @atlaskit/editor-core",
|
|
5
5
|
"author": "Atlassian Pty Ltd",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -33,21 +33,21 @@
|
|
|
33
33
|
"@atlaskit/editor-plugin-toolbar": "^9.0.0",
|
|
34
34
|
"@atlaskit/editor-prosemirror": "^8.0.0",
|
|
35
35
|
"@atlaskit/editor-shared-styles": "^4.0.0",
|
|
36
|
-
"@atlaskit/editor-toolbar": "^2.
|
|
36
|
+
"@atlaskit/editor-toolbar": "^2.2.0",
|
|
37
37
|
"@atlaskit/editor-toolbar-model": "^1.1.0",
|
|
38
38
|
"@atlaskit/heading": "^6.2.0",
|
|
39
39
|
"@atlaskit/icon": "^37.0.0",
|
|
40
40
|
"@atlaskit/icon-lab": "^7.3.0",
|
|
41
41
|
"@atlaskit/platform-feature-flags": "^2.0.0",
|
|
42
42
|
"@atlaskit/platform-feature-flags-react": "^1.1.0",
|
|
43
|
-
"@atlaskit/primitives": "^20.
|
|
43
|
+
"@atlaskit/primitives": "^20.6.0",
|
|
44
44
|
"@atlaskit/tmp-editor-statsig": "^124.1.0",
|
|
45
|
-
"@atlaskit/tokens": "^15.
|
|
45
|
+
"@atlaskit/tokens": "^15.6.0",
|
|
46
46
|
"@babel/runtime": "^7.0.0",
|
|
47
47
|
"@emotion/react": "^11.7.1"
|
|
48
48
|
},
|
|
49
49
|
"peerDependencies": {
|
|
50
|
-
"@atlaskit/editor-common": "^116.
|
|
50
|
+
"@atlaskit/editor-common": "^116.26.0",
|
|
51
51
|
"react": "^18.2.0",
|
|
52
52
|
"react-dom": "^18.2.0",
|
|
53
53
|
"react-intl": "^5.25.1 || ^6.0.0 || ^7.0.0"
|