@atlaskit/editor-plugin-card 0.13.5 → 0.14.0

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.
@@ -1,14 +1,23 @@
1
+ import _extends from "@babel/runtime/helpers/extends";
1
2
  /** @jsx jsx */
2
3
 
3
- import React, { useLayoutEffect, useMemo, useRef, useState } from 'react';
4
+ import React, { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
4
5
  import { css, jsx } from '@emotion/react';
6
+ import debounce from 'lodash/debounce';
5
7
  import { useIntl } from 'react-intl-next';
6
8
  import { browser } from '@atlaskit/editor-common/utils';
7
9
  import HipchatChevronDownIcon from '@atlaskit/icon/glyph/hipchat/chevron-down';
8
10
  import HipchatChevronUpIcon from '@atlaskit/icon/glyph/hipchat/chevron-up';
9
11
  import { N20A, N800 } from '@atlaskit/theme/colors';
10
12
  import { messages } from '../../messages';
13
+ import { getChildElement, getInlineCardAvailableWidth, getOverlayWidths } from './utils';
14
+ const DEBOUNCE_IN_MS = 5;
15
+ const ESTIMATED_MIN_WIDTH_IN_PX = 16;
11
16
  const PADDING_IN_PX = 2;
17
+ const OVERLAY_CLASSNAME = 'ak-editor-card-overlay';
18
+ const OVERLAY_LABEL_CLASSNAME = 'ak-editor-card-overlay-label';
19
+ const OVERLAY_MARKER_CLASSNAME = 'ak-editor-card-overlay-marker';
20
+ const TEXT_NODE_SELECTOR = ['p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6'].join(',');
12
21
  const containerStyles = css({
13
22
  position: 'relative',
14
23
  lineHeight: 'normal'
@@ -18,9 +27,10 @@ const linkStyles = css({
18
27
  textDecoration: 'none'
19
28
  });
20
29
  const overlayStyles = css({
30
+ // Visibility is set directly on element via style prop
31
+ display: 'inline-flex',
21
32
  // Positioning
22
33
  position: 'relative',
23
- display: 'inline-flex',
24
34
  flexWrap: 'nowrap',
25
35
  alignItems: 'center',
26
36
  alignSelf: 'stretch',
@@ -96,67 +106,126 @@ const markerStyles = css({
96
106
  });
97
107
  const InlineCardOverlay = ({
98
108
  children,
99
- isToolbarOpen = false,
109
+ isSelected = false,
100
110
  isVisible = false,
101
111
  testId = 'inline-card-overlay',
102
- url
112
+ url,
113
+ ...props
103
114
  }) => {
115
+ const [showOverlay, setShowOverlay] = useState(true);
104
116
  const [showLabel, setShowLabel] = useState(true);
105
117
  const [overlayWidth, setOverlayWidth] = useState(0);
118
+ const maxOverlayWidth = useRef(0);
119
+ const minOverlayWidth = useRef(ESTIMATED_MIN_WIDTH_IN_PX);
120
+ const parentWidth = useRef(0);
106
121
  const containerRef = useRef(null);
107
- const markerRef = useRef(null);
108
- const overlayRef = useRef(null);
109
- const labelRef = useRef(null);
110
- useLayoutEffect(() => {
111
- if (!isVisible) {
112
- // Reset to default state for width calculation when the component become visible.
113
- setShowLabel(true);
122
+ const setVisibility = useCallback(() => {
123
+ if (!containerRef.current || !maxOverlayWidth.current) {
124
+ return;
125
+ }
126
+ const marker = getChildElement(containerRef, `.${OVERLAY_MARKER_CLASSNAME}`);
127
+ if (!marker) {
114
128
  return;
115
129
  }
116
130
  try {
117
- var _containerRef$current, _containerRef$current2, _containerRef$current3, _markerRef$current$ge, _markerRef$current, _markerRef$current$ge2, _overlayRef$current$g, _overlayRef$current, _overlayRef$current$g2, _labelRef$current$get, _labelRef$current, _labelRef$current$get2;
118
- // Get the width of the available space to display overlay
119
- const start = (_containerRef$current = containerRef === null || containerRef === void 0 ? void 0 : (_containerRef$current2 = containerRef.current) === null || _containerRef$current2 === void 0 ? void 0 : (_containerRef$current3 = _containerRef$current2.getBoundingClientRect()) === null || _containerRef$current3 === void 0 ? void 0 : _containerRef$current3.left) !== null && _containerRef$current !== void 0 ? _containerRef$current : 0;
120
- const end = (_markerRef$current$ge = markerRef === null || markerRef === void 0 ? void 0 : (_markerRef$current = markerRef.current) === null || _markerRef$current === void 0 ? void 0 : (_markerRef$current$ge2 = _markerRef$current.getBoundingClientRect()) === null || _markerRef$current$ge2 === void 0 ? void 0 : _markerRef$current$ge2.left) !== null && _markerRef$current$ge !== void 0 ? _markerRef$current$ge : 0;
121
- const availableWidth = end - start - PADDING_IN_PX;
122
-
123
- // Get overlay width and label width
124
- const overlayWidth = (_overlayRef$current$g = overlayRef === null || overlayRef === void 0 ? void 0 : (_overlayRef$current = overlayRef.current) === null || _overlayRef$current === void 0 ? void 0 : (_overlayRef$current$g2 = _overlayRef$current.getBoundingClientRect()) === null || _overlayRef$current$g2 === void 0 ? void 0 : _overlayRef$current$g2.width) !== null && _overlayRef$current$g !== void 0 ? _overlayRef$current$g : 0;
131
+ // Get the width of the available space to display overlay.
132
+ // This is the width of the inline link itself. If the inline
133
+ // is wrapped to the next line, this is width of the last line.
134
+ const availableWidth = getInlineCardAvailableWidth(containerRef.current, marker) - PADDING_IN_PX;
125
135
 
126
- // Show label if there is enough space to display
127
- const shouldShowLabel = availableWidth > 0 && overlayWidth > 0 ? availableWidth > overlayWidth : false;
128
- setShowLabel(shouldShowLabel);
136
+ // If available width is less than the min width of overlay, don't show overlay.
137
+ const canShowOverlay = availableWidth > minOverlayWidth.current;
138
+ setShowOverlay(canShowOverlay);
139
+ if (!canShowOverlay) {
140
+ return;
141
+ }
129
142
 
130
- // We use relative positioning and need to set
131
- // negative margin left (ltr) as the width of the overlay
132
- // to make the overlay position on top of inline link.
133
- const labelWidth = (_labelRef$current$get = labelRef === null || labelRef === void 0 ? void 0 : (_labelRef$current = labelRef.current) === null || _labelRef$current === void 0 ? void 0 : (_labelRef$current$get2 = _labelRef$current.getBoundingClientRect()) === null || _labelRef$current$get2 === void 0 ? void 0 : _labelRef$current$get2.width) !== null && _labelRef$current$get !== void 0 ? _labelRef$current$get : 0;
134
- const newOverlayWidth = shouldShowLabel ? overlayWidth : overlayWidth - labelWidth;
135
- setOverlayWidth(newOverlayWidth);
143
+ // Otherwise, check if overlay can be show in full context with label and icon.
144
+ const canShowLabel = availableWidth > maxOverlayWidth.current + PADDING_IN_PX;
145
+ setShowLabel(canShowLabel);
146
+ setOverlayWidth(canShowLabel ? maxOverlayWidth.current : minOverlayWidth.current);
136
147
  } catch {
137
- // If something goes wrong, play it safe by hiding label so that
138
- // the component does not look too janky.
139
- setShowLabel(false);
148
+ // If something goes wrong, hide the overlay all together.
149
+ setShowOverlay(false);
150
+ }
151
+ }, []);
152
+ useLayoutEffect(() => {
153
+ // Using useLayoutEffect here.
154
+ // 1) We want all to be able to determine whether to display label before
155
+ // the overlay becomes visible.
156
+ // 2) We need to wait for the refs to be assigned to be able to do determine
157
+ // the width of the overlay.
158
+ if (!containerRef.current) {
159
+ return;
160
+ }
161
+ if (!maxOverlayWidth.current) {
162
+ const overlay = getChildElement(containerRef, `.${OVERLAY_CLASSNAME}`);
163
+ const label = getChildElement(containerRef, `.${OVERLAY_LABEL_CLASSNAME}`);
164
+ if (overlay && label) {
165
+ // Set overlay max (label + icon) and min (icon only) width.
166
+ // This should run only once.
167
+ const {
168
+ max,
169
+ min
170
+ } = getOverlayWidths(overlay, label);
171
+ maxOverlayWidth.current = max;
172
+ minOverlayWidth.current = min;
173
+ }
174
+ }
175
+ if (isVisible) {
176
+ setVisibility();
177
+ }
178
+ }, [setVisibility, isVisible]);
179
+ useEffect(() => {
180
+ var _containerRef$current;
181
+ // Find the closest block parent to observe size change
182
+ 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);
183
+ if (!parent) {
184
+ return;
140
185
  }
141
- }, [isVisible]);
186
+ const updateOverlay = debounce(entries => {
187
+ var _entries$, _entries$$contentBoxS, _entries$$contentBoxS2;
188
+ if (!isVisible) {
189
+ return;
190
+ }
191
+ 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;
192
+ if (!size) {
193
+ return;
194
+ }
195
+ if (!parentWidth.current) {
196
+ parentWidth.current = size;
197
+ }
198
+ if (parentWidth.current === size) {
199
+ return;
200
+ }
201
+ parentWidth.current = size;
202
+ setVisibility();
203
+ }, DEBOUNCE_IN_MS);
204
+ const observer = new ResizeObserver(updateOverlay);
205
+ observer.observe(parent);
206
+ return () => {
207
+ observer.disconnect();
208
+ };
209
+ }, [isVisible, setVisibility]);
142
210
  const intl = useIntl();
143
211
  const label = intl.formatMessage(messages.inlineOverlay);
144
212
  const icon = useMemo(() => {
145
- const IconComponent = isToolbarOpen ? HipchatChevronUpIcon : HipchatChevronDownIcon;
213
+ const IconComponent = isSelected ? HipchatChevronUpIcon : HipchatChevronDownIcon;
146
214
  return jsx(IconComponent, {
147
215
  label: label,
148
216
  size: "small",
149
- testId: `${testId}-${isToolbarOpen ? 'open' : 'close'}`
217
+ testId: `${testId}-${isSelected ? 'open' : 'close'}`
150
218
  });
151
- }, [isToolbarOpen, label, testId]);
152
- return jsx("span", {
219
+ }, [isSelected, label, testId]);
220
+ return jsx("span", _extends({}, props, {
153
221
  css: containerStyles,
154
222
  ref: containerRef
155
- }, children, isVisible && jsx(React.Fragment, null, jsx("span", {
223
+ }), children, isVisible && showOverlay && jsx(React.Fragment, null, jsx("span", {
156
224
  "aria-hidden": "true",
157
- css: markerStyles,
158
- ref: markerRef
225
+ className: OVERLAY_MARKER_CLASSNAME,
226
+ css: markerStyles
159
227
  }), jsx("a", {
228
+ className: OVERLAY_CLASSNAME,
160
229
  css: [overlayStyles, browser.safari && safariOverlayStyles],
161
230
  style: {
162
231
  marginLeft: -overlayWidth
@@ -164,11 +233,11 @@ const InlineCardOverlay = ({
164
233
  "data-testid": testId,
165
234
  href: url,
166
235
  onClick: e => e.preventDefault(),
167
- ref: overlayRef
236
+ tabIndex: -1
168
237
  }, showLabel && jsx("span", {
238
+ className: OVERLAY_LABEL_CLASSNAME,
169
239
  css: textStyles,
170
- "data-testid": `${testId}-label`,
171
- ref: labelRef
240
+ "data-testid": `${testId}-label`
172
241
  }, label), jsx("span", {
173
242
  css: iconStyles
174
243
  }, icon))));
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Find a child element inside a ref.
3
+ */
4
+ export const getChildElement = (ref, selector) => ref.current ? ref.current.querySelector(selector) : undefined;
5
+
6
+ /**
7
+ * Get the available width of the inline card.
8
+ * (Mainly here to make the component unit testable.)
9
+ */
10
+ export const getInlineCardAvailableWidth = (startEl, endEl) => {
11
+ const start = startEl.getBoundingClientRect().left;
12
+ const end = endEl.getBoundingClientRect().left;
13
+ return end - start;
14
+ };
15
+
16
+ /**
17
+ * Get max and min width of an overlay.
18
+ * (Mainly here to make the component unit testable.)
19
+ */
20
+ export const getOverlayWidths = (overlayEl, labelEl) => {
21
+ const max = overlayEl.getBoundingClientRect().width;
22
+ const min = max - labelEl.getBoundingClientRect().width;
23
+ return {
24
+ max,
25
+ min
26
+ };
27
+ };
@@ -133,6 +133,8 @@ export function InlineCardNodeView(props) {
133
133
  useAlternativePreloader: useAlternativePreloader
134
134
  });
135
135
  }
136
+
137
+ // BEGIN: Awareness (To be revisited in EDM-8508)
136
138
  var editorState = view.state;
137
139
  var linkPosition = getPos && typeof getPos() === 'number' ? getPos() : undefined;
138
140
  var canBeUpgradedToEmbed = !!linkPosition && allowEmbeds ? isEmbedSupportedAtPosition(linkPosition, editorState, 'inline') : false;
@@ -140,6 +142,8 @@ export function InlineCardNodeView(props) {
140
142
  var isPulseEnabled = enableInlineUpgradeFeatures && canBeUpgradedToEmbed;
141
143
  var isOverlayEnabled = enableInlineUpgradeFeatures && (canBeUpgradedToEmbed || canBeUpgradedToBlock);
142
144
  var isSelected = view.state.selection instanceof NodeSelection && ((_view$state$selection = view.state.selection) === null || _view$state$selection === void 0 || (_view$state$selection = _view$state$selection.node) === null || _view$state$selection === void 0 ? void 0 : _view$state$selection.type) === view.state.schema.nodes.inlineCard && ((_view$state$selection2 = view.state.selection) === null || _view$state$selection2 === void 0 ? void 0 : _view$state$selection2.from) === getPos();
145
+ // END: Awareness
146
+
143
147
  return /*#__PURE__*/React.createElement(WrappedInlineCardWithAwareness, {
144
148
  node: node,
145
149
  view: view,
@@ -1,4 +1,3 @@
1
- import _extends from "@babel/runtime/helpers/extends";
2
1
  import _slicedToArray from "@babel/runtime/helpers/slicedToArray";
3
2
  /** @jsx jsx */
4
3
  import { memo, useCallback, useMemo, useState } from 'react';
@@ -29,17 +28,17 @@ var InlineCard = function InlineCard(_ref) {
29
28
  isOverlayEnabled = _ref.isOverlayEnabled,
30
29
  isPulseEnabled = _ref.isPulseEnabled,
31
30
  pluginInjectionApi = _ref.pluginInjectionApi,
32
- isSelected = _ref.isSelected;
31
+ _ref$isSelected = _ref.isSelected,
32
+ isSelected = _ref$isSelected === void 0 ? false : _ref$isSelected;
33
33
  var _node$attrs = node.attrs,
34
34
  url = _node$attrs.url,
35
35
  data = _node$attrs.data;
36
36
 
37
- // A complete show/hide logic for the overlay will be implemented
38
- // in EDM-8239 and EDM-8241
37
+ // BEGIN: Awareness (To be revisited in EDM-8508)
39
38
  var _useState = useState(false),
40
39
  _useState2 = _slicedToArray(_useState, 2),
41
- isOverlayVisible = _useState2[0],
42
- setIsOverlayVisible = _useState2[1];
40
+ isHovered = _useState2[0],
41
+ setIsHovered = _useState2[1];
43
42
  var linkPosition = useMemo(function () {
44
43
  if (!getPos || typeof getPos === 'boolean') {
45
44
  return undefined;
@@ -64,6 +63,8 @@ var InlineCard = function InlineCard(_ref) {
64
63
  if (isSelected && shouldShowToolbarPulse && !isLocalStorageKeyDiscovered(LOCAL_STORAGE_DISCOVERY_KEY_TOOLBAR)) {
65
64
  markLocalStorageKeyDiscovered(LOCAL_STORAGE_DISCOVERY_KEY_SMART_LINK);
66
65
  }
66
+ // END: Awareness
67
+
67
68
  var scrollContainer = useMemo(function () {
68
69
  return findOverflowScrollParent(view.dom) || undefined;
69
70
  }, [view.dom]);
@@ -99,10 +100,7 @@ var InlineCard = function InlineCard(_ref) {
99
100
  });
100
101
  }, [onResolve]);
101
102
  var innerCard = useMemo(function () {
102
- return jsx(InlineCardOverlay, {
103
- isVisible: isOverlayVisible,
104
- url: url
105
- }, jsx(SmartCard, {
103
+ return jsx(SmartCard, {
106
104
  key: url,
107
105
  url: url,
108
106
  data: data,
@@ -113,25 +111,34 @@ var InlineCard = function InlineCard(_ref) {
113
111
  onError: onError,
114
112
  inlinePreloaderStyle: useAlternativePreloader ? 'on-right-without-skeleton' : undefined,
115
113
  showServerActions: showServerActions
116
- }));
117
- }, [data, isOverlayVisible, onError, onResolve, scrollContainer, showServerActions, url, useAlternativePreloader]);
118
- var card = useMemo(function () {
119
- return jsx("span", _extends({
120
- css: shouldShowLinkPulse && loaderWrapperStyles,
121
- className: "card"
122
- }, shouldShowLinkOverlay ? {
114
+ });
115
+ }, [data, onError, onResolve, scrollContainer, showServerActions, url, useAlternativePreloader]);
116
+
117
+ // BEGIN: Awareness (To be revisited in EDM-8508)
118
+ var cardWithOverlay = useMemo(function () {
119
+ return shouldShowLinkOverlay ? jsx(InlineCardOverlay, {
120
+ isSelected: isSelected,
121
+ isVisible: isHovered || isSelected,
123
122
  onMouseEnter: function onMouseEnter() {
124
- return setIsOverlayVisible(true);
123
+ return setIsHovered(true);
125
124
  },
126
125
  onMouseLeave: function onMouseLeave() {
127
- return setIsOverlayVisible(false);
128
- }
129
- } : {}), shouldShowLinkPulse ? jsx(DiscoveryPulse, {
126
+ return setIsHovered(false);
127
+ },
128
+ url: url
129
+ }, innerCard) : innerCard;
130
+ }, [innerCard, isHovered, isSelected, shouldShowLinkOverlay, url]);
131
+ var card = useMemo(function () {
132
+ return jsx("span", {
133
+ css: shouldShowLinkPulse && loaderWrapperStyles,
134
+ className: "card"
135
+ }, shouldShowLinkPulse ? jsx(DiscoveryPulse, {
130
136
  localStorageKey: LOCAL_STORAGE_DISCOVERY_KEY_SMART_LINK,
131
137
  localStorageKeyExpirationInMs: ONE_DAY_IN_MILLISECONDS,
132
- discoveryMode: 'start'
133
- }, innerCard) : innerCard);
134
- }, [shouldShowLinkPulse, shouldShowLinkOverlay, innerCard]);
138
+ discoveryMode: "start"
139
+ }, cardWithOverlay) : cardWithOverlay);
140
+ }, [shouldShowLinkPulse, cardWithOverlay]);
141
+ // END: Awareness
135
142
 
136
143
  // [WS-2307]: we only render card wrapped into a Provider when the value is ready,
137
144
  // otherwise if we got data, we can render the card directly since it doesn't need the Provider
@@ -20,7 +20,7 @@ var getClosestInlineCardPos = function getClosestInlineCardPos(state, editorView
20
20
  left: coord.left,
21
21
  top: direction === 'up' ? coord.top - lookupPixel : coord.bottom + lookupPixel
22
22
  })) === null || _editorView$posAtCoor === void 0 ? void 0 : _editorView$posAtCoor.inside;
23
- if (nearPos) {
23
+ if (typeof nearPos === 'number' && nearPos > -1) {
24
24
  var newNode = state.doc.nodeAt(nearPos);
25
25
  if (newNode) {
26
26
  if (newNode.type !== inlineCardType || findChildren(parent, function (node) {
@@ -241,7 +241,7 @@ var generateToolbarItems = function generateToolbarItems(state, featureFlags, in
241
241
  }, {
242
242
  type: 'separator'
243
243
  }]
244
- }], _toConsumableArray(getSettingsButtonGroup(state, featureFlags, intl, editorAnalyticsApi)), [{
244
+ }], _toConsumableArray(getSettingsButtonGroup(intl, editorAnalyticsApi)), [{
245
245
  id: 'editor.link.delete',
246
246
  focusEditoronEnter: true,
247
247
  type: 'button',
@@ -326,9 +326,8 @@ var getUnlinkButtonGroup = function getUnlinkButtonGroup(state, intl, node, inli
326
326
  type: 'separator'
327
327
  }] : [];
328
328
  };
329
- var getSettingsButtonGroup = function getSettingsButtonGroup(state, featureFlags, intl, editorAnalyticsApi) {
330
- var floatingToolbarLinkSettingsButton = featureFlags.floatingToolbarLinkSettingsButton;
331
- return floatingToolbarLinkSettingsButton === 'true' ? [{
329
+ var getSettingsButtonGroup = function getSettingsButtonGroup(intl, editorAnalyticsApi) {
330
+ return [{
332
331
  id: 'editor.link.settings',
333
332
  type: 'button',
334
333
  icon: CogIcon,
@@ -336,7 +335,7 @@ var getSettingsButtonGroup = function getSettingsButtonGroup(state, featureFlags
336
335
  onClick: openLinkSettings(editorAnalyticsApi)
337
336
  }, {
338
337
  type: 'separator'
339
- }] : [];
338
+ }];
340
339
  };
341
340
  var getDatasourceButtonGroup = function getDatasourceButtonGroup(metadata, intl, editorAnalyticsApi, node, hoverDecoration, datasourceId, state) {
342
341
  var _node$attrs3;
@@ -1,18 +1,29 @@
1
+ import _extends from "@babel/runtime/helpers/extends";
1
2
  import _slicedToArray from "@babel/runtime/helpers/slicedToArray";
3
+ import _objectWithoutProperties from "@babel/runtime/helpers/objectWithoutProperties";
2
4
  import _defineProperty from "@babel/runtime/helpers/defineProperty";
5
+ var _excluded = ["children", "isSelected", "isVisible", "testId", "url"];
3
6
  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
7
  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
8
  /** @jsx jsx */
6
9
 
7
- import React, { useLayoutEffect, useMemo, useRef, useState } from 'react';
10
+ import React, { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
8
11
  import { css, jsx } from '@emotion/react';
12
+ import debounce from 'lodash/debounce';
9
13
  import { useIntl } from 'react-intl-next';
10
14
  import { browser } from '@atlaskit/editor-common/utils';
11
15
  import HipchatChevronDownIcon from '@atlaskit/icon/glyph/hipchat/chevron-down';
12
16
  import HipchatChevronUpIcon from '@atlaskit/icon/glyph/hipchat/chevron-up';
13
17
  import { N20A, N800 } from '@atlaskit/theme/colors';
14
18
  import { messages } from '../../messages';
19
+ import { getChildElement, getInlineCardAvailableWidth, getOverlayWidths } from './utils';
20
+ var DEBOUNCE_IN_MS = 5;
21
+ var ESTIMATED_MIN_WIDTH_IN_PX = 16;
15
22
  var PADDING_IN_PX = 2;
23
+ var OVERLAY_CLASSNAME = 'ak-editor-card-overlay';
24
+ var OVERLAY_LABEL_CLASSNAME = 'ak-editor-card-overlay-label';
25
+ var OVERLAY_MARKER_CLASSNAME = 'ak-editor-card-overlay-marker';
26
+ var TEXT_NODE_SELECTOR = ['p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6'].join(',');
16
27
  var containerStyles = css({
17
28
  position: 'relative',
18
29
  lineHeight: 'normal'
@@ -22,9 +33,10 @@ var linkStyles = css({
22
33
  textDecoration: 'none'
23
34
  });
24
35
  var overlayStyles = css({
36
+ // Visibility is set directly on element via style prop
37
+ display: 'inline-flex',
25
38
  // Positioning
26
39
  position: 'relative',
27
- display: 'inline-flex',
28
40
  flexWrap: 'nowrap',
29
41
  alignItems: 'center',
30
42
  alignSelf: 'stretch',
@@ -90,75 +102,136 @@ var markerStyles = css({
90
102
  });
91
103
  var InlineCardOverlay = function InlineCardOverlay(_ref) {
92
104
  var children = _ref.children,
93
- _ref$isToolbarOpen = _ref.isToolbarOpen,
94
- isToolbarOpen = _ref$isToolbarOpen === void 0 ? false : _ref$isToolbarOpen,
105
+ _ref$isSelected = _ref.isSelected,
106
+ isSelected = _ref$isSelected === void 0 ? false : _ref$isSelected,
95
107
  _ref$isVisible = _ref.isVisible,
96
108
  isVisible = _ref$isVisible === void 0 ? false : _ref$isVisible,
97
109
  _ref$testId = _ref.testId,
98
110
  testId = _ref$testId === void 0 ? 'inline-card-overlay' : _ref$testId,
99
- url = _ref.url;
111
+ url = _ref.url,
112
+ props = _objectWithoutProperties(_ref, _excluded);
100
113
  var _useState = useState(true),
101
114
  _useState2 = _slicedToArray(_useState, 2),
102
- showLabel = _useState2[0],
103
- setShowLabel = _useState2[1];
104
- var _useState3 = useState(0),
115
+ showOverlay = _useState2[0],
116
+ setShowOverlay = _useState2[1];
117
+ var _useState3 = useState(true),
105
118
  _useState4 = _slicedToArray(_useState3, 2),
106
- overlayWidth = _useState4[0],
107
- setOverlayWidth = _useState4[1];
119
+ showLabel = _useState4[0],
120
+ setShowLabel = _useState4[1];
121
+ var _useState5 = useState(0),
122
+ _useState6 = _slicedToArray(_useState5, 2),
123
+ overlayWidth = _useState6[0],
124
+ setOverlayWidth = _useState6[1];
125
+ var maxOverlayWidth = useRef(0);
126
+ var minOverlayWidth = useRef(ESTIMATED_MIN_WIDTH_IN_PX);
127
+ var parentWidth = useRef(0);
108
128
  var containerRef = useRef(null);
109
- var markerRef = useRef(null);
110
- var overlayRef = useRef(null);
111
- var labelRef = useRef(null);
112
- useLayoutEffect(function () {
113
- if (!isVisible) {
114
- // Reset to default state for width calculation when the component become visible.
115
- setShowLabel(true);
129
+ var setVisibility = useCallback(function () {
130
+ if (!containerRef.current || !maxOverlayWidth.current) {
131
+ return;
132
+ }
133
+ var marker = getChildElement(containerRef, ".".concat(OVERLAY_MARKER_CLASSNAME));
134
+ if (!marker) {
116
135
  return;
117
136
  }
118
137
  try {
119
- var _containerRef$current, _containerRef$current2, _markerRef$current$ge, _markerRef$current, _overlayRef$current$g, _overlayRef$current, _labelRef$current$get, _labelRef$current;
120
- // Get the width of the available space to display overlay
121
- var start = (_containerRef$current = containerRef === null || containerRef === void 0 || (_containerRef$current2 = containerRef.current) === null || _containerRef$current2 === void 0 || (_containerRef$current2 = _containerRef$current2.getBoundingClientRect()) === null || _containerRef$current2 === void 0 ? void 0 : _containerRef$current2.left) !== null && _containerRef$current !== void 0 ? _containerRef$current : 0;
122
- var end = (_markerRef$current$ge = markerRef === null || markerRef === void 0 || (_markerRef$current = markerRef.current) === null || _markerRef$current === void 0 || (_markerRef$current = _markerRef$current.getBoundingClientRect()) === null || _markerRef$current === void 0 ? void 0 : _markerRef$current.left) !== null && _markerRef$current$ge !== void 0 ? _markerRef$current$ge : 0;
123
- var availableWidth = end - start - PADDING_IN_PX;
124
-
125
- // Get overlay width and label width
126
- var _overlayWidth = (_overlayRef$current$g = overlayRef === null || overlayRef === void 0 || (_overlayRef$current = overlayRef.current) === null || _overlayRef$current === void 0 || (_overlayRef$current = _overlayRef$current.getBoundingClientRect()) === null || _overlayRef$current === void 0 ? void 0 : _overlayRef$current.width) !== null && _overlayRef$current$g !== void 0 ? _overlayRef$current$g : 0;
138
+ // Get the width of the available space to display overlay.
139
+ // This is the width of the inline link itself. If the inline
140
+ // is wrapped to the next line, this is width of the last line.
141
+ var availableWidth = getInlineCardAvailableWidth(containerRef.current, marker) - PADDING_IN_PX;
127
142
 
128
- // Show label if there is enough space to display
129
- var shouldShowLabel = availableWidth > 0 && _overlayWidth > 0 ? availableWidth > _overlayWidth : false;
130
- setShowLabel(shouldShowLabel);
143
+ // If available width is less than the min width of overlay, don't show overlay.
144
+ var canShowOverlay = availableWidth > minOverlayWidth.current;
145
+ setShowOverlay(canShowOverlay);
146
+ if (!canShowOverlay) {
147
+ return;
148
+ }
131
149
 
132
- // We use relative positioning and need to set
133
- // negative margin left (ltr) as the width of the overlay
134
- // to make the overlay position on top of inline link.
135
- var labelWidth = (_labelRef$current$get = labelRef === null || labelRef === void 0 || (_labelRef$current = labelRef.current) === null || _labelRef$current === void 0 || (_labelRef$current = _labelRef$current.getBoundingClientRect()) === null || _labelRef$current === void 0 ? void 0 : _labelRef$current.width) !== null && _labelRef$current$get !== void 0 ? _labelRef$current$get : 0;
136
- var newOverlayWidth = shouldShowLabel ? _overlayWidth : _overlayWidth - labelWidth;
137
- setOverlayWidth(newOverlayWidth);
150
+ // Otherwise, check if overlay can be show in full context with label and icon.
151
+ var canShowLabel = availableWidth > maxOverlayWidth.current + PADDING_IN_PX;
152
+ setShowLabel(canShowLabel);
153
+ setOverlayWidth(canShowLabel ? maxOverlayWidth.current : minOverlayWidth.current);
138
154
  } catch (_unused) {
139
- // If something goes wrong, play it safe by hiding label so that
140
- // the component does not look too janky.
141
- setShowLabel(false);
155
+ // If something goes wrong, hide the overlay all together.
156
+ setShowOverlay(false);
157
+ }
158
+ }, []);
159
+ useLayoutEffect(function () {
160
+ // Using useLayoutEffect here.
161
+ // 1) We want all to be able to determine whether to display label before
162
+ // the overlay becomes visible.
163
+ // 2) We need to wait for the refs to be assigned to be able to do determine
164
+ // the width of the overlay.
165
+ if (!containerRef.current) {
166
+ return;
167
+ }
168
+ if (!maxOverlayWidth.current) {
169
+ var overlay = getChildElement(containerRef, ".".concat(OVERLAY_CLASSNAME));
170
+ var _label = getChildElement(containerRef, ".".concat(OVERLAY_LABEL_CLASSNAME));
171
+ if (overlay && _label) {
172
+ // Set overlay max (label + icon) and min (icon only) width.
173
+ // This should run only once.
174
+ var _getOverlayWidths = getOverlayWidths(overlay, _label),
175
+ max = _getOverlayWidths.max,
176
+ min = _getOverlayWidths.min;
177
+ maxOverlayWidth.current = max;
178
+ minOverlayWidth.current = min;
179
+ }
180
+ }
181
+ if (isVisible) {
182
+ setVisibility();
183
+ }
184
+ }, [setVisibility, isVisible]);
185
+ useEffect(function () {
186
+ var _containerRef$current;
187
+ // Find the closest block parent to observe size change
188
+ var parent = containerRef === null || containerRef === void 0 || (_containerRef$current = containerRef.current) === null || _containerRef$current === void 0 ? void 0 : _containerRef$current.closest(TEXT_NODE_SELECTOR);
189
+ if (!parent) {
190
+ return;
142
191
  }
143
- }, [isVisible]);
192
+ var updateOverlay = debounce(function (entries) {
193
+ var _entries$;
194
+ if (!isVisible) {
195
+ return;
196
+ }
197
+ var size = entries === null || entries === void 0 || (_entries$ = entries[0]) === null || _entries$ === void 0 || (_entries$ = _entries$.contentBoxSize) === null || _entries$ === void 0 || (_entries$ = _entries$[0]) === null || _entries$ === void 0 ? void 0 : _entries$.inlineSize;
198
+ if (!size) {
199
+ return;
200
+ }
201
+ if (!parentWidth.current) {
202
+ parentWidth.current = size;
203
+ }
204
+ if (parentWidth.current === size) {
205
+ return;
206
+ }
207
+ parentWidth.current = size;
208
+ setVisibility();
209
+ }, DEBOUNCE_IN_MS);
210
+ var observer = new ResizeObserver(updateOverlay);
211
+ observer.observe(parent);
212
+ return function () {
213
+ observer.disconnect();
214
+ };
215
+ }, [isVisible, setVisibility]);
144
216
  var intl = useIntl();
145
217
  var label = intl.formatMessage(messages.inlineOverlay);
146
218
  var icon = useMemo(function () {
147
- var IconComponent = isToolbarOpen ? HipchatChevronUpIcon : HipchatChevronDownIcon;
219
+ var IconComponent = isSelected ? HipchatChevronUpIcon : HipchatChevronDownIcon;
148
220
  return jsx(IconComponent, {
149
221
  label: label,
150
222
  size: "small",
151
- testId: "".concat(testId, "-").concat(isToolbarOpen ? 'open' : 'close')
223
+ testId: "".concat(testId, "-").concat(isSelected ? 'open' : 'close')
152
224
  });
153
- }, [isToolbarOpen, label, testId]);
154
- return jsx("span", {
225
+ }, [isSelected, label, testId]);
226
+ return jsx("span", _extends({}, props, {
155
227
  css: containerStyles,
156
228
  ref: containerRef
157
- }, children, isVisible && jsx(React.Fragment, null, jsx("span", {
229
+ }), children, isVisible && showOverlay && jsx(React.Fragment, null, jsx("span", {
158
230
  "aria-hidden": "true",
159
- css: markerStyles,
160
- ref: markerRef
231
+ className: OVERLAY_MARKER_CLASSNAME,
232
+ css: markerStyles
161
233
  }), jsx("a", {
234
+ className: OVERLAY_CLASSNAME,
162
235
  css: [overlayStyles, browser.safari && safariOverlayStyles],
163
236
  style: {
164
237
  marginLeft: -overlayWidth
@@ -168,11 +241,11 @@ var InlineCardOverlay = function InlineCardOverlay(_ref) {
168
241
  onClick: function onClick(e) {
169
242
  return e.preventDefault();
170
243
  },
171
- ref: overlayRef
244
+ tabIndex: -1
172
245
  }, showLabel && jsx("span", {
246
+ className: OVERLAY_LABEL_CLASSNAME,
173
247
  css: textStyles,
174
- "data-testid": "".concat(testId, "-label"),
175
- ref: labelRef
248
+ "data-testid": "".concat(testId, "-label")
176
249
  }, label), jsx("span", {
177
250
  css: iconStyles
178
251
  }, icon))));