UNPKG

@atlaskit/editor-plugin-card

Version:

Card plugin for @atlaskit/editor-core

256 lines (253 loc) 10.1 kB
import _extends from "@babel/runtime/helpers/extends"; /* eslint-disable @atlaskit/design-system/no-nested-styles */ /* eslint-disable @atlaskit/design-system/prefer-primitives */ /** * @jsxRuntime classic * @jsx jsx */ import React, { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react'; // eslint-disable-next-line @atlaskit/ui-styling-standard/use-compiled, @typescript-eslint/consistent-type-imports import { css, jsx } from '@emotion/react'; import debounce from 'lodash/debounce'; import { useIntl } from 'react-intl'; import { cardMessages as messages } from '@atlaskit/editor-common/messages'; import { ZERO_WIDTH_JOINER } from '@atlaskit/editor-common/whitespace'; import CustomizeIcon from '@atlaskit/icon/core/customize'; import { getChildElement, getInlineCardAvailableWidth, getOverlayWidths, isOneLine } from './utils'; const DEBOUNCE_IN_MS = 5; const ESTIMATED_MIN_WIDTH_IN_PX = 16; const PADDING_IN_PX = 4; const ICON_WIDTH_IN_PX = 14; const ICON_AND_LABEL_CLASSNAME = 'ak-editor-card-overlay-icon-and-label'; const OVERLAY_LABEL_CLASSNAME = 'ak-editor-card-overlay-label'; const OVERLAY_GRADIENT_CLASSNAME = 'ak-editor-card-overlay-gradient'; const OVERLAY_MARKER_CLASSNAME = 'ak-editor-card-overlay-marker'; const TEXT_NODE_SELECTOR = ['p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6'].join(','); const SMART_LINK_BACKGROUND_COLOR = "var(--ds-surface-raised, #FFFFFF)"; const SMART_LINK_ACTIVE_COLOR = "var(--ds-background-selected, #E9F2FE)"; const getGradientWithColor = color => { return `linear-gradient(270deg, ${color} 0%, rgba(255, 255, 255, 0.00) 100%)`; }; const containerStyles = css({ position: 'relative', // eslint-disable-next-line @atlaskit/design-system/use-tokens-typography lineHeight: 'normal', // eslint-disable-next-line @atlaskit/ui-styling-standard/no-unsafe-selectors -- Ignored via go/DSP-18766 ':active': { // eslint-disable-next-line @atlaskit/ui-styling-standard/no-nested-selectors, @atlaskit/ui-styling-standard/no-unsafe-values -- Ignored via go/DSP-18766 [`.${ICON_AND_LABEL_CLASSNAME}`]: { background: SMART_LINK_ACTIVE_COLOR }, // eslint-disable-next-line @atlaskit/ui-styling-standard/no-nested-selectors, @atlaskit/ui-styling-standard/no-unsafe-values -- Ignored via go/DSP-18766 [`.${OVERLAY_GRADIENT_CLASSNAME}`]: { // eslint-disable-next-line @atlaskit/ui-styling-standard/no-unsafe-values -- Ignored via go/DSP-18766 background: getGradientWithColor(SMART_LINK_ACTIVE_COLOR) } } }); const overlayStyles = css({ // Set default styling to be invisible but available in dom for width calculation. visibility: 'hidden', position: 'absolute', display: 'inline-flex', justifyContent: 'flex-end', alignItems: 'center', verticalAlign: 'text-top', height: '1lh', '@supports not (height: 1lh)': { height: '1.2em' }, overflow: 'hidden', // EDM-1717: box-shadow Safari fix bring load wrapper zIndex to 1 zIndex: 2, pointerEvents: 'none' }); const showOverlayStyles = css({ position: 'relative', visibility: 'visible' }); const iconStyles = css({ // Position icon in the middle // eslint-disable-next-line @atlaskit/ui-styling-standard/no-nested-selectors -- Ignored via go/DSP-18766 span: { display: 'flex' } }); const labelStyles = css({ font: "var(--ds-font-body, normal 400 14px/20px \"Atlassian Sans\", ui-sans-serif, -apple-system, BlinkMacSystemFont, \"Segoe UI\", Ubuntu, \"Helvetica Neue\", sans-serif)", fontWeight: "var(--ds-font-weight-semibold, 600)", width: 'max-content' }); const iconAndLabelStyles = css({ display: 'flex', alignItems: 'center', height: '100%', gap: "var(--ds-space-050, 4px)", paddingRight: "var(--ds-space-050, 4px)", // Margin to avoid the background covering the link border marginRight: "var(--ds-space-025, 2px)", background: SMART_LINK_BACKGROUND_COLOR, color: "var(--ds-text-subtlest, #6B6E76)" }); const overflowingContainerStyles = css({ display: 'flex', flexDirection: 'row-reverse', alignItems: 'center', width: 'max-content', height: '100%' }); const gradientStyles = css({ width: '2.5rem', height: '100%', // eslint-disable-next-line @atlaskit/ui-styling-standard/no-unsafe-values -- Ignored via go/DSP-18766 background: getGradientWithColor(SMART_LINK_BACKGROUND_COLOR) }); const InlineCardOverlay = ({ children, isSelected = false, isVisible = false, testId = 'inline-card-overlay', url, ...props }) => { const [showOverlay, setShowOverlay] = useState(false); const [showLabel, setShowLabel] = useState(true); const [availableWidth, setAvailableWidth] = useState(undefined); const maxOverlayWidth = useRef(0); const minOverlayWidth = useRef(ESTIMATED_MIN_WIDTH_IN_PX); const parentWidth = useRef(0); const containerRef = useRef(null); const setVisibility = useCallback(() => { if (!containerRef.current || !maxOverlayWidth.current) { return; } const marker = getChildElement(containerRef, `.${OVERLAY_MARKER_CLASSNAME}`); if (!marker) { return; } try { const oneLine = isOneLine(containerRef.current, marker); // Get the width of the available space to display overlay. // This is the width of the inline link itself. If the inline // is wrapped to the next line, this is width of the last line. const availableWidth = getInlineCardAvailableWidth(containerRef.current, marker) - PADDING_IN_PX - ( // Always leave at least the icon visible oneLine ? ICON_WIDTH_IN_PX + PADDING_IN_PX : 0); setAvailableWidth(availableWidth); const canShowLabel = availableWidth > maxOverlayWidth.current; setShowLabel(canShowLabel); const canShowOverlay = availableWidth > minOverlayWidth.current && !isSelected; setShowOverlay(canShowOverlay); } catch { // If something goes wrong, hide the overlay all together. setShowOverlay(false); } }, [isSelected]); useLayoutEffect(() => { // Using useLayoutEffect here. // 1) We want all to be able to determine whether to display label before // the overlay becomes visible. // 2) We need to wait for the refs to be assigned to be able to do determine // the width of the overlay. if (!containerRef.current) { return; } // This should run only once if (!maxOverlayWidth.current) { const iconAndLabel = getChildElement(containerRef, `.${ICON_AND_LABEL_CLASSNAME}`); const label = getChildElement(containerRef, `.${OVERLAY_LABEL_CLASSNAME}`); if (iconAndLabel && label) { // Set overlay max (label + icon) and min (icon only) width. const { max, min } = getOverlayWidths(iconAndLabel, label); maxOverlayWidth.current = max; minOverlayWidth.current = min; } } if (isVisible) { setVisibility(); } }, [setVisibility, isVisible]); useEffect(() => { var _containerRef$current; // Find the closest block parent to observe size change const parent = containerRef === null || containerRef === void 0 ? void 0 : (_containerRef$current = containerRef.current) === null || _containerRef$current === void 0 ? void 0 : _containerRef$current.closest(TEXT_NODE_SELECTOR); if (!parent) { return; } const updateOverlay = debounce(entries => { var _entries$, _entries$$contentBoxS, _entries$$contentBoxS2; if (!isVisible) { return; } const size = entries === null || entries === void 0 ? void 0 : (_entries$ = entries[0]) === null || _entries$ === void 0 ? void 0 : (_entries$$contentBoxS = _entries$.contentBoxSize) === null || _entries$$contentBoxS === void 0 ? void 0 : (_entries$$contentBoxS2 = _entries$$contentBoxS[0]) === null || _entries$$contentBoxS2 === void 0 ? void 0 : _entries$$contentBoxS2.inlineSize; if (!size) { return; } if (!parentWidth.current) { parentWidth.current = size; } if (parentWidth.current === size) { return; } parentWidth.current = size; setVisibility(); }, DEBOUNCE_IN_MS); const observer = new ResizeObserver(updateOverlay); observer.observe(parent); return () => { observer.disconnect(); }; }, [isVisible, setVisibility]); const intl = useIntl(); const label = intl.formatMessage(messages.inlineOverlay); return ( // Ignored via go/ees005 // eslint-disable-next-line react/jsx-props-no-spreading jsx("span", _extends({}, props, { css: containerStyles, ref: containerRef }), children, isVisible && jsx(React.Fragment, null, jsx("span", { "aria-hidden": "true", className: OVERLAY_MARKER_CLASSNAME }, ZERO_WIDTH_JOINER), jsx("a", { css: [overlayStyles, showOverlay && showOverlayStyles], style: { // eslint-disable-next-line @atlaskit/design-system/ensure-design-token-usage/preview marginLeft: availableWidth && -availableWidth, width: availableWidth }, "data-testid": testId, href: url, onClick: e => e.preventDefault(), tabIndex: -1 }, jsx("span", { css: overflowingContainerStyles }, jsx("span", { css: iconAndLabelStyles // eslint-disable-next-line @atlaskit/ui-styling-standard/no-classname-prop -- Ignored via go/DSP-18766 , className: ICON_AND_LABEL_CLASSNAME }, jsx("span", { css: iconStyles }, jsx(CustomizeIcon, { label: label, testId: `${testId}-icon` })), showLabel && jsx("span", { css: labelStyles // eslint-disable-next-line @atlaskit/ui-styling-standard/no-classname-prop -- Ignored via go/DSP-18766 , className: OVERLAY_LABEL_CLASSNAME, "data-testid": `${testId}-label` }, label)), jsx("span", { css: gradientStyles // eslint-disable-next-line @atlaskit/ui-styling-standard/no-classname-prop -- Ignored via go/DSP-18766 , className: OVERLAY_GRADIENT_CLASSNAME, "data-testid": `${testId}-gradient` }))))) ); }; export default InlineCardOverlay;