UNPKG

@atlaskit/editor-common

Version:

A package that contains common classes and components for editor and renderer

249 lines (243 loc) 10.1 kB
/* eslint-disable @atlaskit/design-system/no-nested-styles */ /* eslint-disable @atlaskit/design-system/prefer-primitives */ /** * @jsxRuntime classic * @jsx jsx */ import React, { useLayoutEffect, useMemo, useRef, useState } from 'react'; // eslint-disable-next-line @typescript-eslint/consistent-type-imports -- jsx required at runtime for @jsxRuntime classic import { css, jsx } from '@emotion/react'; // eslint-disable-line @atlaskit/ui-styling-standard/use-compiled import { useIntl } from 'react-intl'; import { NodeSelection } from '@atlaskit/editor-prosemirror/state'; import { akEditorFullPageDefaultFontSize, akEditorFullPageDenseFontSize } from '@atlaskit/editor-shared-styles'; // eslint-disable-next-line @atlaskit/ui-styling-standard/use-compiled -- Ignored via go/DSP-18766 import GrowDiagonalIcon from '@atlaskit/icon/core/grow-diagonal'; import LinkExternalIcon from '@atlaskit/icon/core/link-external'; import PanelRightIcon from '@atlaskit/icon/core/panel-right'; import { fg } from '@atlaskit/platform-feature-flags'; // eslint-disable-next-line @atlaskit/design-system/no-emotion-primitives -- to be migrated to @atlaskit/primitives/compiled – go/akcss import { Anchor, Box, Text, xcss } from '@atlaskit/primitives'; import { expValEquals } from '@atlaskit/tmp-editor-statsig/exp-val-equals'; import { editorExperiment } from '@atlaskit/tmp-editor-statsig/experiments'; import { buildVisitedNonHyperLinkPayload, INPUT_METHOD } from '../../analytics'; import { cardMessages } from '../../messages'; /** * Dynamic padding value that adjusts based on the editor's base font size. * When base font size is <= akEditorFullPageDenseFontSize (13px), uses 1px padding. * Otherwise, uses default value with token('space.025'). */ const DYNAMIC_PADDING_BLOCK = `clamp(1px, calc((var(--ak-editor-base-font-size, ${akEditorFullPageDefaultFontSize}px) - ${akEditorFullPageDenseFontSize}px) * 999), ${"var(--ds-space-025, 2px)"})`; const containerStyles = css({ position: 'relative' }); const iconWrapperStyles = xcss({ display: 'inline-flex', justifyContent: 'center', alignItems: 'center', height: '17px', width: '17px' }); const hiddenTextStyle = css({ overflow: 'hidden', whiteSpace: 'nowrap', position: 'absolute', visibility: 'hidden' }); const linkStylesCommon = xcss({ position: 'absolute', left: 'space.025', display: 'inline-flex', alignItems: 'center', verticalAlign: 'middle', paddingBlock: 'space.025', paddingInline: 'space.050', gap: 'space.025', overflow: 'hidden', zIndex: 'card', backgroundColor: 'color.background.accent.gray.subtlest', borderRadius: "var(--ds-radius-small, 3px)", color: 'color.text.subtle', textDecoration: 'none', whiteSpace: 'nowrap', top: '0px', height: '1.2em', ':hover': { backgroundColor: 'elevation.surface.hovered', color: 'color.text.subtle', textDecoration: 'none' } }); const MIN_AVAILABLE_SPACE_WITH_LABEL_OVERLAY = 45; const ICON_WIDTH = 16; const DEFAULT_OPEN_TEXT_WIDTH = 28; // Default open text width in English const visitCardLinkAnalytics = (editorAnalyticsApi, inputMethod) => (state, dispatch) => { if (!(state.selection instanceof NodeSelection)) { return false; } const { type } = state.selection.node; if (dispatch) { const { tr } = state; editorAnalyticsApi === null || editorAnalyticsApi === void 0 ? void 0 : editorAnalyticsApi.attachAnalyticsEvent(buildVisitedNonHyperLinkPayload(type.name, inputMethod))(tr); dispatch(tr); } return true; }; const HoverLinkOverlay = ({ children, isVisible = false, url, compactPadding = false, editorAnalyticsApi, view, onClick, showPanelButton = false, showPanelButtonIcon }) => { const { formatMessage } = useIntl(); const containerRef = useRef(null); const hoverLinkButtonRef = useRef(null); const hiddenTextRef = useRef(null); const [showLabel, setShowLabel] = useState(true); const [isHovered, setHovered] = useState(false); const openTextWidthRef = useRef(null); const regularPadding = expValEquals('confluence_compact_text_format', 'isEnabled', true) || expValEquals('cc_editor_ai_content_mode', 'variant', 'test') && fg('platform_editor_content_mode_button_mvp') ? DYNAMIC_PADDING_BLOCK : "var(--ds-space-025, 2px)"; const memoizedHoverLinkStyles = useMemo(() => ({ paddingBlock: compactPadding ? '1px' : regularPadding }), [compactPadding, regularPadding]); const hoverLinkStyles = expValEquals('platform_editor_perf_lint_cleanup', 'isEnabled', true) ? memoizedHoverLinkStyles : { paddingBlock: compactPadding ? '1px' : expValEquals('confluence_compact_text_format', 'isEnabled', true) || expValEquals('cc_editor_ai_content_mode', 'variant', 'test') && fg('platform_editor_content_mode_button_mvp') ? DYNAMIC_PADDING_BLOCK : "var(--ds-space-025, 2px)" }; useLayoutEffect(() => { var _containerRef$current, _hoverLinkButtonRef$c; if (!isVisible || !isHovered) { return; } const cardWidth = (_containerRef$current = containerRef.current) === null || _containerRef$current === void 0 ? void 0 : _containerRef$current.offsetWidth; const openButtonWidth = (_hoverLinkButtonRef$c = hoverLinkButtonRef.current) === null || _hoverLinkButtonRef$c === void 0 ? void 0 : _hoverLinkButtonRef$c.offsetWidth; // Get the hidden text width if (!openTextWidthRef.current) { const hiddenText = hiddenTextRef.current; if (hiddenText) { // Measure the width of the hidden text // Temporarily make the element visible to measure its width hiddenText.style.visibility = 'hidden'; hiddenText.style.display = 'inline'; openTextWidthRef.current = hiddenText.offsetWidth; // Reset the hiddenText's display property hiddenText.style.display = 'none'; hiddenText.style.visibility = 'inherit'; } else { openTextWidthRef.current = DEFAULT_OPEN_TEXT_WIDTH; } } if (!cardWidth || !openButtonWidth) { return; } const openTextWidth = openTextWidthRef.current || DEFAULT_OPEN_TEXT_WIDTH; let canShowLabel = cardWidth - openTextWidth > MIN_AVAILABLE_SPACE_WITH_LABEL_OVERLAY + ICON_WIDTH; // When a smart link wraps to multiple lines in a constrained container (e.g. table cell), // the hover button can overflow beyond the container bounds. We detect this by comparing // the button's right edge to the container's right edge, and hide the label if it overflows. if (editorExperiment('cc_editor_hover_link_overlay_css_fix', true)) { if (containerRef.current && hoverLinkButtonRef.current) { const containerRight = containerRef.current.getBoundingClientRect().right; const buttonRight = hoverLinkButtonRef.current.getBoundingClientRect().right; if (buttonRight > containerRight) { canShowLabel = false; } } } setShowLabel(canShowLabel); }, [isVisible, isHovered]); const handleOverlayChange = isHovered => { setHovered(isHovered); // Reset label visibility on hover start so we can measure if it overflows. // Without this, the label stays hidden from a previous hover and won't be re-measured. if (editorExperiment('cc_editor_hover_link_overlay_css_fix', true)) { if (isHovered) { setShowLabel(true); } } }; const sendVisitLinkAnalytics = inputMethod => { if (editorAnalyticsApi && view) { visitCardLinkAnalytics(editorAnalyticsApi, inputMethod)(view.state, view.dispatch); } }; const handleDoubleClick = () => { if (!showPanelButton) { sendVisitLinkAnalytics(INPUT_METHOD.DOUBLE_CLICK); // Double click opens the link in a new tab window.open(url, '_blank'); } }; const handleClick = event => { if (showPanelButton && onClick) { onClick(event); } else { sendVisitLinkAnalytics(INPUT_METHOD.BUTTON); } }; const isPreviewButton = showPanelButton && editorExperiment('platform_editor_preview_panel_linking_exp', true); const label = isPreviewButton ? formatMessage(cardMessages.previewButtonTitle) : formatMessage(cardMessages.openButtonTitle); let icon = null; if (isPreviewButton && showPanelButtonIcon === 'panel') { icon = jsx(PanelRightIcon, { label: "" }); } else if (isPreviewButton && showPanelButtonIcon === 'modal') { icon = jsx(GrowDiagonalIcon, { label: "" }); } else { icon = jsx(LinkExternalIcon, { label: "" }); } return jsx("span", { ref: containerRef, css: containerStyles, onDoubleClick: handleDoubleClick, onMouseEnter: () => handleOverlayChange(true), onMouseLeave: () => handleOverlayChange(false) // No-op focus/blur handlers to satisfy a11y/mouse-events-have-key-events rule. // The hover overlay is a mouse convenience — keyboard users can access link actions // (open, preview) via the floating toolbar that appears on selection. , onFocus: expValEquals('editor_a11y__enghealth-46814_fy26', 'isEnabled', true) ? () => {} : undefined, onBlur: expValEquals('editor_a11y__enghealth-46814_fy26', 'isEnabled', true) ? () => {} : undefined }, children, jsx("span", { css: hiddenTextStyle, "aria-hidden": "true" }, jsx(Text, { ref: hiddenTextRef, size: "small", maxLines: 1 }, label)), isHovered && jsx(Anchor, { ref: hoverLinkButtonRef, xcss: linkStylesCommon, href: url, target: "_blank" // eslint-disable-next-line @atlaskit/ui-styling-standard/enforce-style-prop -- dynamic paddingBlock value based on experiment flags , style: hoverLinkStyles, onClick: handleClick, testId: "inline-card-hoverlink-overlay" }, jsx(Box, { xcss: iconWrapperStyles, "data-inlinecard-button-overlay": "icon-wrapper-line-height", testId: "inline-card-hoverlink-overlay-icon" }, icon), showLabel && jsx(Text, { size: "small", color: "color.text.subtle", maxLines: 1, testId: "inline-card-hoverlink-overlay-label" }, label))); }; export default HoverLinkOverlay;