UNPKG

@atlaskit/editor-plugin-text-color

Version:

Text color plugin for @atlaskit/editor-core

342 lines (335 loc) 15.3 kB
/* ColorAccessibilityMenuItem.tsx generated by @compiled/babel-plugin v2.0.0 */ import "./ColorAccessibilityMenuItem.compiled.css"; import { ax, ix } from "@compiled/react/runtime"; import React from 'react'; import { useIntl } from 'react-intl'; import { PanelType } from '@atlaskit/adf-schema/panel'; import IconButton from '@atlaskit/button/icon/button'; import { cx } from '@atlaskit/css'; import { useSharedPluginStateWithSelector } from '@atlaskit/editor-common/hooks'; import { colorAccessibilityMessages as messages } from '@atlaskit/editor-common/messages'; import { getPanelTypeBackgroundNoTokens } from '@atlaskit/editor-common/panel'; import { useEditorToolbar } from '@atlaskit/editor-common/toolbar'; import { getTextColorInNonActiveTheme, getTokenCSSVariableValue, getTokenCSSVariableValueForNonActiveTheme } from '@atlaskit/editor-common/ui-color'; import { hexToEditorBackgroundPaletteColor, 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/fg'; 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, ACCESSIBLE_CONTRAST_RATIO, DIFFICULT_CONTRAST_RATIO } from '../pm-plugins/utils/constants'; const resolveColorValue = (color, fallback) => { return getTokenCSSVariableValue(color) || (color.startsWith('var(') ? fallback : color); }; const getForegroundColor = (textColor, defaultColor) => { if (!textColor || defaultColor && textColor === defaultColor) { return getTokenValue('color.text', defaultColor || DEFAULT_COLOR.color); } const colorValue = hexToEditorTextPaletteColor(textColor) || textColor; return resolveColorValue(colorValue, textColor); }; const getBackgroundColor = (backgroundColor, palette = 'text') => { if (!backgroundColor || backgroundColor === 'none') { return getTokenValue('elevation.surface', DEFAULT_BACKGROUND_COLOR); } const colorValue = palette === 'element' ? hexToEditorBackgroundPaletteColor(backgroundColor) || backgroundColor : hexToEditorTextBackgroundPaletteColor(backgroundColor) || backgroundColor; return resolveColorValue(colorValue, backgroundColor); }; const getBackgroundColorInNonActiveTheme = (backgroundColor, palette = 'text') => { if (!backgroundColor || backgroundColor === 'none') { return getTokenCSSVariableValueForNonActiveTheme("var(--ds-surface, #FFFFFF)", DEFAULT_BACKGROUND_COLOR); } const colorValue = palette === 'element' ? hexToEditorBackgroundPaletteColor(backgroundColor) || backgroundColor : hexToEditorTextBackgroundPaletteColor(backgroundColor) || backgroundColor; return getTokenCSSVariableValueForNonActiveTheme(colorValue, backgroundColor); }; const STANDARD_PANEL_BACKGROUND_COLORS = new Map([[PanelType.INFO, getPanelTypeBackgroundNoTokens(PanelType.INFO)], [PanelType.NOTE, getPanelTypeBackgroundNoTokens(PanelType.NOTE)], [PanelType.SUCCESS, getPanelTypeBackgroundNoTokens(PanelType.SUCCESS)], [PanelType.WARNING, getPanelTypeBackgroundNoTokens(PanelType.WARNING)], [PanelType.ERROR, getPanelTypeBackgroundNoTokens(PanelType.ERROR)]]); const getResolvedBackgroundFromHighlight = highlightColor => { if (!highlightColor) { return { color: null, source: 'text' }; } return { color: highlightColor, source: 'text' }; }; /** * Resolves a background color from a container node that paints behind text. * Callers walk ancestors so the nearest painted container wins. */ const getSupportedAncestorBackgroundColor = node => { let backgroundColor; if (node.type.name === 'tableCell' || node.type.name === 'tableHeader') { backgroundColor = node.attrs.background; } else if (node.type.name === 'panel' || node.type.name === 'panel_c1') { if (node.attrs.panelType === PanelType.CUSTOM) { backgroundColor = node.attrs.panelColor; } else { backgroundColor = STANDARD_PANEL_BACKGROUND_COLORS.get(node.attrs.panelType); } } return backgroundColor ? { color: backgroundColor, source: 'element' } : null; }; /** * Resolves the visible background behind a text node. * Text highlight wins before falling back to the nearest element background. */ export const getResolvedBackgroundColorForTextNode = ($pos, node) => { for (const mark of node.marks) { if (mark.type.name === 'backgroundColor' && mark.attrs.color) { return { color: mark.attrs.color, source: 'text' }; } } for (let depth = $pos.depth; depth > 0; depth--) { const resolvedBackgroundColor = getSupportedAncestorBackgroundColor($pos.node(depth)); if (resolvedBackgroundColor) { return resolvedBackgroundColor; } } return { color: null, source: 'text' }; }; const getResolvedBackgroundColorForPosition = ($pos, highlightColor) => { const highlightBackground = getResolvedBackgroundFromHighlight(highlightColor); if (highlightColor) { return highlightBackground; } for (let depth = $pos.depth; depth > 0; depth--) { const resolvedBackgroundColor = getSupportedAncestorBackgroundColor($pos.node(depth)); if (resolvedBackgroundColor) { return resolvedBackgroundColor; } } return highlightBackground; }; const getResolvedBackgroundColorValue = backgroundColor => { if (backgroundColor.source === 'element') { return getBackgroundColor(backgroundColor.color, 'element'); } return getBackgroundColor(backgroundColor.color); }; const getResolvedBackgroundColorValueInNonActiveTheme = backgroundColor => { if (backgroundColor.source === 'element') { return getBackgroundColorInNonActiveTheme(backgroundColor.color, 'element'); } return getBackgroundColorInNonActiveTheme(backgroundColor.color); }; const useColorAccessibilityState = api => { return useSharedPluginStateWithSelector(api, ['textColor', 'highlight'], states => { var _states$textColorStat, _states$highlightStat, _states$textColorStat2; 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, textColor: (_states$textColorStat2 = states.textColorState) === null || _states$textColorStat2 === void 0 ? void 0 : _states$textColorStat2.color }; }); }; const getContrastRatio = (defaultColor, backgroundColor, textColor) => { try { const contrastRatio = calcContrastRatio(getForegroundColor(textColor, defaultColor), getResolvedBackgroundColorValue(backgroundColor)); return contrastRatio; } catch { // if we failed to calculate the contrast ratio, return null return null; } }; // Computes the contrast ratio as it would appear in the non-active theme. const getNonActiveThemeContrastRatio = (defaultColor, backgroundColor, textColor) => { try { return calcContrastRatio(getTextColorInNonActiveTheme(textColor !== null && textColor !== void 0 ? textColor : null, defaultColor || DEFAULT_COLOR.color), getResolvedBackgroundColorValueInNonActiveTheme(backgroundColor)); } catch { // if we failed to calculate the contrast ratio, return null return null; } }; const getAccessibilityStatus = contrastRatio => { if (contrastRatio >= ACCESSIBLE_CONTRAST_RATIO) { return 'accessible'; } else if (contrastRatio >= DIFFICULT_CONTRAST_RATIO) { return 'difficultToRead'; } else { return 'inaccessible'; } }; /** * Computes status from active foreground/background colors only. * Used by the gate-off path so legacy behavior stays isolated. */ const getAccessibilityStatusFromColorPair = (defaultColor, backgroundColor, textColor) => { const contrastRatio = getContrastRatio(defaultColor, backgroundColor, textColor); return contrastRatio === null ? null : getAccessibilityStatus(contrastRatio); }; /** * Returns the lowest contrast ratio across active and non-active themes. */ const getMostCriticalContrastRatioAcrossThemes = (defaultColor, backgroundColor, textColor) => { const contrastRatio = getContrastRatio(defaultColor, backgroundColor, textColor); const nonActiveThemeContrastRatio = getNonActiveThemeContrastRatio(defaultColor, backgroundColor, textColor); if (contrastRatio !== null && nonActiveThemeContrastRatio !== null) { return Math.min(contrastRatio, nonActiveThemeContrastRatio); } return contrastRatio; }; const getAccessibilityStatusFromColorPairAcrossThemes = (defaultColor, backgroundColor, textColor) => { const mostCriticalContrastRatio = getMostCriticalContrastRatioAcrossThemes(defaultColor, backgroundColor, textColor); return mostCriticalContrastRatio === null ? null : getAccessibilityStatus(mostCriticalContrastRatio); }; /** * Collects the worst accessibility status across all unique foreground/background * combinations in the current selection. * * When the selection spans multiple text colors and/or resolved backgrounds, * this checks each unique pair across both the active and non-active theme and * returns the worst status found. */ const getWorstAccessibilityStatusFromEditorState = (state, defaultColor) => { let worstContrastRatio = null; // 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 (!(state.selection instanceof TextSelection) || state.selection.empty) { return null; } const { from, to } = state.selection; const seen = new Set(); state.doc.nodesBetween(from, to, (node, pos) => { var _textColor, _backgroundColor$colo; if (!node.isText) { return !node.isLeaf; } let textColor = null; for (const mark of node.marks) { 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; } } const backgroundColor = getResolvedBackgroundColorForTextNode(state.doc.resolve(pos), node); const pairKey = `${(_textColor = textColor) !== null && _textColor !== void 0 ? _textColor : 'default'}|${backgroundColor.source}|${(_backgroundColor$colo = backgroundColor.color) !== null && _backgroundColor$colo !== void 0 ? _backgroundColor$colo : 'default'}`; // Skip foreground/background pairs already evaluated. The source is // part of the key because text highlights and element backgrounds use // different editor palettes. if (seen.has(pairKey)) { return; } seen.add(pairKey); const mostCriticalContrastRatio = getMostCriticalContrastRatioAcrossThemes(defaultColor, backgroundColor, textColor); if (mostCriticalContrastRatio === null) { return; } if (worstContrastRatio === null || mostCriticalContrastRatio < worstContrastRatio) { worstContrastRatio = mostCriticalContrastRatio; } }); return worstContrastRatio === null ? null : getAccessibilityStatus(worstContrastRatio); }; export const getAccessibilityStatusForCurrentSelection = (state, defaultColor, highlightColor, textColor) => { if (state.selection instanceof TextSelection) { if (!state.selection.empty) { return getWorstAccessibilityStatusFromEditorState(state, defaultColor); } return getAccessibilityStatusFromColorPairAcrossThemes(defaultColor, getResolvedBackgroundColorForPosition(state.selection.$from, highlightColor), textColor); } return null; }; const AccessibilityStatus = ({ accessibilityStatus, 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)); } }; const styles = { container: "_19itglyw _19pku2gc _ca0q1b66 _n3td1b66 _19bvu2gc _u5f31b66 _1e0c1txw _4cvr1h6o _1bah1yb4", containerPatch: "_19pkze3t _ca0qze3t _n3tdze3t _19bvze3t _u5f3ze3t" }; export const ColorAccessibilityMenuItem = ({ api }) => { const { formatMessage } = useIntl(); const { defaultColor, highlightColor, textColor } = useColorAccessibilityState(api); const { editorView } = useEditorToolbar(); const editorState = editorView === null || editorView === void 0 ? void 0 : editorView.state; const accessibilityStatus = React.useMemo(() => editorState && fg('platform_editor_lovability_text_bg_color_patch_1') ? getAccessibilityStatusForCurrentSelection(editorState, defaultColor, highlightColor, textColor) : getAccessibilityStatusFromColorPair(defaultColor, getResolvedBackgroundFromHighlight(highlightColor), textColor), [defaultColor, editorState, highlightColor, textColor]); if (accessibilityStatus === null) { return /*#__PURE__*/React.createElement(React.Fragment, null); } const 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" })); };