@atlaskit/editor-plugin-card
Version:
Card plugin for @atlaskit/editor-core
256 lines (253 loc) • 10.1 kB
JavaScript
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;