@atlaskit/smart-card 23.11.2 → 23.12.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.
Files changed (51) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/dist/cjs/messages.js +5 -0
  3. package/dist/cjs/state/analytics/useSmartLinkAnalytics.js +216 -146
  4. package/dist/cjs/utils/analytics/analytics.js +134 -74
  5. package/dist/cjs/utils/analytics/index.js +6 -0
  6. package/dist/cjs/version.json +1 -1
  7. package/dist/cjs/view/CardWithUrl/component.js +27 -1
  8. package/dist/cjs/view/EmbedCard/components/Frame.js +110 -5
  9. package/dist/cjs/view/EmbedCard/components/IframeDwellTracker.js +75 -0
  10. package/dist/cjs/view/EmbedCard/index.js +6 -2
  11. package/dist/cjs/view/EmbedCard/views/ResolvedView.js +6 -2
  12. package/dist/cjs/view/HoverCard/components/HoverCardContent.js +49 -2
  13. package/dist/es2019/messages.js +5 -0
  14. package/dist/es2019/state/analytics/useSmartLinkAnalytics.js +69 -1
  15. package/dist/es2019/utils/analytics/analytics.js +52 -0
  16. package/dist/es2019/utils/analytics/index.js +1 -1
  17. package/dist/es2019/version.json +1 -1
  18. package/dist/es2019/view/CardWithUrl/component.js +27 -1
  19. package/dist/es2019/view/EmbedCard/components/Frame.js +91 -6
  20. package/dist/es2019/view/EmbedCard/components/IframeDwellTracker.js +57 -0
  21. package/dist/es2019/view/EmbedCard/index.js +6 -2
  22. package/dist/es2019/view/EmbedCard/views/ResolvedView.js +6 -2
  23. package/dist/es2019/view/HoverCard/components/HoverCardContent.js +13 -1
  24. package/dist/esm/messages.js +5 -0
  25. package/dist/esm/state/analytics/useSmartLinkAnalytics.js +217 -147
  26. package/dist/esm/utils/analytics/analytics.js +127 -73
  27. package/dist/esm/utils/analytics/index.js +1 -1
  28. package/dist/esm/version.json +1 -1
  29. package/dist/esm/view/CardWithUrl/component.js +27 -1
  30. package/dist/esm/view/EmbedCard/components/Frame.js +110 -6
  31. package/dist/esm/view/EmbedCard/components/IframeDwellTracker.js +62 -0
  32. package/dist/esm/view/EmbedCard/index.js +6 -2
  33. package/dist/esm/view/EmbedCard/views/ResolvedView.js +6 -2
  34. package/dist/esm/view/HoverCard/components/HoverCardContent.js +43 -1
  35. package/dist/types/messages.d.ts +1 -1
  36. package/dist/types/state/analytics/useSmartLinkAnalytics.d.ts +27 -1
  37. package/dist/types/state/flexible-ui-context/index.d.ts +4 -0
  38. package/dist/types/state/hooks/useSmartLink.d.ts +2 -0
  39. package/dist/types/utils/analytics/analytics.d.ts +3 -1
  40. package/dist/types/utils/analytics/index.d.ts +1 -1
  41. package/dist/types/utils/analytics/types.d.ts +12 -0
  42. package/dist/types/utils/mocks.d.ts +2 -0
  43. package/dist/types/utils/types.d.ts +2 -2
  44. package/dist/types/view/EmbedCard/components/Frame.d.ts +2 -0
  45. package/dist/types/view/EmbedCard/components/IframeDwellTracker.d.ts +8 -0
  46. package/dist/types/view/EmbedCard/types.d.ts +2 -0
  47. package/dist/types/view/EmbedCard/views/ResolvedView.d.ts +4 -0
  48. package/dist/types/view/HoverCard/components/HoverCardContent.d.ts +1 -0
  49. package/package.json +2 -2
  50. package/report.api.md +42 -1
  51. package/tmp/api-report-tmp.d.ts +20 -2
@@ -7,12 +7,18 @@ var _typeof = require("@babel/runtime/helpers/typeof");
7
7
  Object.defineProperty(exports, "__esModule", {
8
8
  value: true
9
9
  });
10
- exports.hoverCardClassName = exports.getOpenAction = exports.default = void 0;
10
+ exports.hoverCardClassName = exports.getOpenAction = exports.getCopyAction = exports.default = void 0;
11
+
12
+ var _regenerator = _interopRequireDefault(require("@babel/runtime/regenerator"));
13
+
14
+ var _asyncToGenerator2 = _interopRequireDefault(require("@babel/runtime/helpers/asyncToGenerator"));
11
15
 
12
16
  var _react = require("@emotion/react");
13
17
 
14
18
  var _shortcut = _interopRequireDefault(require("@atlaskit/icon/glyph/shortcut"));
15
19
 
20
+ var _copy = _interopRequireDefault(require("@atlaskit/icon/glyph/copy"));
21
+
16
22
  var _extractors = require("@atlaskit/linking-common/extractors");
17
23
 
18
24
  var _react2 = _interopRequireWildcard(require("react"));
@@ -74,6 +80,47 @@ var getOpenAction = function getOpenAction(url, analytics, onActionClick) {
74
80
 
75
81
  exports.getOpenAction = getOpenAction;
76
82
 
83
+ var getCopyAction = function getCopyAction(url) {
84
+ return {
85
+ name: _constants.ActionName.CustomAction,
86
+ icon: (0, _react.jsx)(_copy.default, {
87
+ label: "copy url",
88
+ size: "medium"
89
+ }),
90
+ iconPosition: 'before',
91
+ onClick: function () {
92
+ var _onClick = (0, _asyncToGenerator2.default)( /*#__PURE__*/_regenerator.default.mark(function _callee() {
93
+ return _regenerator.default.wrap(function _callee$(_context) {
94
+ while (1) {
95
+ switch (_context.prev = _context.next) {
96
+ case 0:
97
+ _context.next = 2;
98
+ return navigator.clipboard.writeText(url);
99
+
100
+ case 2:
101
+ return _context.abrupt("return", _context.sent);
102
+
103
+ case 3:
104
+ case "end":
105
+ return _context.stop();
106
+ }
107
+ }
108
+ }, _callee);
109
+ }));
110
+
111
+ function onClick() {
112
+ return _onClick.apply(this, arguments);
113
+ }
114
+
115
+ return onClick;
116
+ }(),
117
+ tooltipMessage: (0, _react.jsx)(_reactIntlNext.FormattedMessage, _messages.messages.copy_url_to_clipboard),
118
+ testId: 'hover-card-copy-button'
119
+ };
120
+ };
121
+
122
+ exports.getCopyAction = getCopyAction;
123
+
77
124
  var HoverCardContent = function HoverCardContent(_ref) {
78
125
  var _cardState$details;
79
126
 
@@ -107,7 +154,7 @@ var HoverCardContent = function HoverCardContent(_ref) {
107
154
  });
108
155
  }, [cardState.status, analytics.ui, id]);
109
156
  var titleActions = (0, _react2.useMemo)(function () {
110
- return [getOpenAction(url, analytics, onActionClick)];
157
+ return [getCopyAction(url), getOpenAction(url, analytics, onActionClick)];
111
158
  }, [url, analytics, onActionClick]);
112
159
  var data = (_cardState$details = cardState.details) === null || _cardState$details === void 0 ? void 0 : _cardState$details.data;
113
160
  var types = data ? (0, _extractors.extractType)(data) : undefined;
@@ -85,6 +85,11 @@ export const messages = defineMessages({
85
85
  defaultMessage: 'Connect to Atlassian to view more details from your work and collaboration from one place.',
86
86
  description: 'Shown when a user does not have access to a link, but can connect their external account to view the link on card view and we do not have the providers name.'
87
87
  },
88
+ copy_url_to_clipboard: {
89
+ id: 'fabric.linking.download',
90
+ defaultMessage: 'Copy link',
91
+ description: ''
92
+ },
88
93
  could_not_load_link: {
89
94
  id: 'fabric.linking.couldnt_load_link',
90
95
  defaultMessage: "We couldn't load this link for an unknown reason.",
@@ -4,7 +4,7 @@ import { chunkloadFailedEvent, connectFailedEvent, connectSucceededEvent, contex
4
4
  import { failUfoExperience, startUfoExperience, succeedUfoExperience } from './ufoExperiences';
5
5
  import { getDefinitionId, getExtensionKey, getProduct, getResourceType, getSubproduct } from '../helpers';
6
6
  import { useSmartLinkContext } from '@atlaskit/link-provider';
7
- import { trackLinkUpdated } from '../../utils/analytics/analytics';
7
+ import { trackLinkUpdated, uiIframeDwelledEvent, uiIframeFocusedEvent } from '../../utils/analytics/analytics';
8
8
 
9
9
  const applyCommonAttributes = (event, commonAttributes) => {
10
10
  if (event && event.attributes) {
@@ -156,6 +156,74 @@ export const useSmartLinkAnalytics = (url, dispatchAnalytics, id, defaultLocatio
156
156
  actionSubjectId
157
157
  }), commonAttributes)),
158
158
 
159
+ /**
160
+ * This fires an event that represents when a user dwells their cursor on a Smart Link's iframe.
161
+ * @param id The unique ID for this Smart Link.
162
+ * @param display Whether the card was an Inline, Block, Embed or Flexible UI.
163
+ * @param status What status the Smart Link is currently in (e.g. resolved, unresolved)
164
+ * @param definitionId The definitionId of the Smart Link resolver invoked.
165
+ * @param extensionKey The extensionKey of the Smart Link resovler invoked.
166
+ * @param location Where the Smart Link is currently rendered.
167
+ * @param destinationProduct The product the Smart Link is linked to.
168
+ * @param dwellTime Total seconds that the user has dwelled on this iframe
169
+ * @param dwellPercentVisible Percentage element was visible at end of dwell
170
+ * @returns
171
+ */
172
+ iframeDwelledEvent: ({
173
+ id,
174
+ display,
175
+ status,
176
+ definitionId,
177
+ extensionKey,
178
+ location,
179
+ destinationProduct,
180
+ destinationSubproduct,
181
+ dwellTime,
182
+ dwellPercentVisible
183
+ }) => dispatchAnalytics(applyCommonAttributes(uiIframeDwelledEvent({
184
+ id,
185
+ display,
186
+ status,
187
+ definitionId,
188
+ extensionKey,
189
+ location,
190
+ destinationProduct,
191
+ destinationSubproduct,
192
+ dwellTime,
193
+ dwellPercentVisible
194
+ }), commonAttributes)),
195
+
196
+ /**
197
+ * This fires an event that represents when a user clicks or tabs into a Smart Link's iframe.
198
+ * @param id The unique ID for this Smart Link.
199
+ * @param display Whether the card was an Inline, Block, Embed or Flexible UI.
200
+ * @param status What status the Smart Link is currently in (e.g. resolved, unresolved)
201
+ * @param definitionId The definitionId of the Smart Link resolver invoked.
202
+ * @param extensionKey The extensionKey of the Smart Link resovler invoked.
203
+ * @param location Where the Smart Link is currently rendered.
204
+ * @param destinationProduct The product the Smart Link is linked to.
205
+ * @returns
206
+ */
207
+ iframeFocusedEvent: ({
208
+ id,
209
+ display,
210
+ status,
211
+ definitionId,
212
+ extensionKey,
213
+ location,
214
+ destinationProduct,
215
+ destinationSubproduct
216
+ }) => dispatchAnalytics(applyCommonAttributes(uiIframeFocusedEvent({
217
+ id,
218
+ display,
219
+ status,
220
+ definitionId,
221
+ extensionKey,
222
+ location,
223
+ destinationProduct,
224
+ destinationSubproduct
225
+ }), commonAttributes)),
226
+
159
227
  /**
160
228
  * This fires an event that represents when a user clicks on a Smart Link action.
161
229
  * Note: This also starts the UFO smart-link-action-invocation experience.
@@ -297,6 +297,58 @@ export const uiCardClickedEvent = ({
297
297
  destinationSubproduct
298
298
  }
299
299
  });
300
+ export const uiIframeDwelledEvent = ({
301
+ id,
302
+ display,
303
+ status,
304
+ definitionId,
305
+ extensionKey,
306
+ location,
307
+ destinationProduct,
308
+ destinationSubproduct,
309
+ dwellTime,
310
+ dwellPercentVisible
311
+ }) => ({
312
+ action: 'dwelled',
313
+ actionSubject: 'smartLinkIframe',
314
+ eventType: 'ui',
315
+ attributes: { ...context,
316
+ id,
317
+ status,
318
+ definitionId,
319
+ extensionKey,
320
+ display,
321
+ location,
322
+ destinationProduct,
323
+ destinationSubproduct,
324
+ dwellTime,
325
+ dwellPercentVisible
326
+ }
327
+ });
328
+ export const uiIframeFocusedEvent = ({
329
+ id,
330
+ display,
331
+ status,
332
+ definitionId,
333
+ extensionKey,
334
+ location,
335
+ destinationProduct,
336
+ destinationSubproduct
337
+ }) => ({
338
+ action: 'focused',
339
+ actionSubject: 'smartLinkIframe',
340
+ eventType: 'ui',
341
+ attributes: { ...context,
342
+ id,
343
+ status,
344
+ definitionId,
345
+ extensionKey,
346
+ display,
347
+ location,
348
+ destinationProduct,
349
+ destinationSubproduct
350
+ }
351
+ });
300
352
  export const uiActionClickedEvent = ({
301
353
  id,
302
354
  actionType,
@@ -1,6 +1,6 @@
1
1
  import { getMeasure } from '../performance';
2
2
  import { resolvedEvent, unresolvedEvent } from './analytics';
3
- export { ANALYTICS_CHANNEL, context, fireSmartLinkEvent, resolvedEvent, unresolvedEvent, invokeSucceededEvent, invokeFailedEvent, chunkloadFailedEvent, connectSucceededEvent, connectFailedEvent, trackAppAccountAuthStarted, trackAppAccountConnected, uiAuthEvent, uiAuthAlternateAccountEvent, uiCardClickedEvent, uiActionClickedEvent, uiClosedAuthEvent, screenAuthPopupEvent, uiRenderSuccessEvent, uiRenderFailedEvent, uiHoverCardViewedEvent, uiHoverCardDismissedEvent, uiHoverCardOpenLinkClickedEvent, uiLearnMoreLinkClickedEvent } from './analytics';
3
+ export { ANALYTICS_CHANNEL, context, fireSmartLinkEvent, resolvedEvent, unresolvedEvent, invokeSucceededEvent, invokeFailedEvent, chunkloadFailedEvent, connectSucceededEvent, connectFailedEvent, trackAppAccountAuthStarted, trackAppAccountConnected, uiAuthEvent, uiAuthAlternateAccountEvent, uiCardClickedEvent, uiActionClickedEvent, uiClosedAuthEvent, screenAuthPopupEvent, uiRenderSuccessEvent, uiRenderFailedEvent, uiHoverCardViewedEvent, uiHoverCardDismissedEvent, uiHoverCardOpenLinkClickedEvent, uiLearnMoreLinkClickedEvent, uiIframeDwelledEvent } from './analytics';
4
4
  export const instrumentEvent = ({
5
5
  id,
6
6
  status,
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "@atlaskit/smart-card",
3
- "version": "23.11.2",
3
+ "version": "23.12.0",
4
4
  "sideEffects": false
5
5
  }
@@ -113,6 +113,30 @@ export function CardWithUrlContent({
113
113
  });
114
114
  }
115
115
  }, [appearance, state.status, url, definitionId, extensionKey, analytics.ui, id]);
116
+ const onIframeDwell = useCallback((dwellTime, dwellPercentVisible) => {
117
+ analytics.ui.iframeDwelledEvent({
118
+ id,
119
+ display: isFlexibleUi ? 'flexible' : appearance,
120
+ status: state.status,
121
+ definitionId,
122
+ extensionKey,
123
+ destinationProduct: product,
124
+ destinationSubproduct: subproduct,
125
+ dwellTime: dwellTime,
126
+ dwellPercentVisible: dwellPercentVisible
127
+ });
128
+ }, [id, state.status, analytics.ui, appearance, definitionId, extensionKey, isFlexibleUi, product, subproduct]);
129
+ const onIframeFocus = useCallback(() => {
130
+ analytics.ui.iframeFocusedEvent({
131
+ id,
132
+ display: isFlexibleUi ? 'flexible' : appearance,
133
+ status: state.status,
134
+ definitionId,
135
+ extensionKey,
136
+ destinationProduct: product,
137
+ destinationSubproduct: subproduct
138
+ });
139
+ }, [id, state.status, analytics.ui, appearance, definitionId, extensionKey, isFlexibleUi, product, subproduct]);
116
140
 
117
141
  if (isFlexibleUi) {
118
142
  let cardState = state;
@@ -204,7 +228,9 @@ export function CardWithUrlContent({
204
228
  testId: testId,
205
229
  inheritDimensions: inheritDimensions,
206
230
  showActions: showActions,
207
- ref: embedIframeRef
231
+ ref: embedIframeRef,
232
+ onIframeDwell: onIframeDwell,
233
+ onIframeFocus: onIframeFocus
208
234
  });
209
235
  }
210
236
  }
@@ -1,20 +1,101 @@
1
1
  /** @jsx jsx */
2
2
  import { jsx } from '@emotion/react';
3
- import React, { useState } from 'react';
3
+ import React, { useEffect, useState, useRef } from 'react';
4
4
  import { getIframeSandboxAttribute } from '../../../utils';
5
+ import { IframeDwellTracker } from './IframeDwellTracker';
6
+ import { useFeatureFlag } from '@atlaskit/link-provider';
7
+
8
+ function mergeRefs(refs) {
9
+ return value => {
10
+ refs.forEach(ref => {
11
+ if (typeof ref === 'function') {
12
+ ref(value);
13
+ } else if (ref !== null) {
14
+ ref.current = value;
15
+ }
16
+ });
17
+ };
18
+ }
19
+
5
20
  export const Frame = /*#__PURE__*/React.forwardRef(({
6
21
  url,
7
22
  isTrusted = false,
8
- testId
23
+ testId,
24
+ onIframeDwell,
25
+ onIframeFocus
9
26
  }, iframeRef) => {
10
27
  const [isIframeLoaded, setIframeLoaded] = useState(false);
28
+ const [isMouseOver, setMouseOver] = useState(false);
29
+ const [isWindowFocused, setWindowFocused] = useState(true);
30
+ const ref = useRef();
31
+ const mergedRef = mergeRefs([iframeRef, ref]);
32
+ const trackIframeDwellEvents = useFeatureFlag('trackIframeDwellEvents');
33
+ const [percentVisible, setPercentVisible] = useState(0);
34
+ /**
35
+ * These are the 'percent visible' thresholds at which the intersectionObserver will
36
+ * trigger a state change. Eg. when the user scrolls and moves from 74% to 76%, or
37
+ * vice versa. It's in a state object so that its static for the useEffect
38
+ */
39
+
40
+ const [threshold] = useState([0.75, 0.8, 0.85, 0.9, 0.95, 1]);
41
+ useEffect(() => {
42
+ if (!trackIframeDwellEvents) {
43
+ return;
44
+ }
45
+
46
+ if (!ref || !ref.current) {
47
+ return;
48
+ }
49
+
50
+ const observer = new IntersectionObserver(entries => {
51
+ entries.forEach(entry => {
52
+ setPercentVisible(entry === null || entry === void 0 ? void 0 : entry.intersectionRatio);
53
+ });
54
+ }, {
55
+ threshold
56
+ });
57
+ observer.observe(ref.current);
58
+ return () => {
59
+ observer.disconnect();
60
+ };
61
+ }, [trackIframeDwellEvents, threshold, mergedRef]);
62
+ useEffect(() => {
63
+ if (!trackIframeDwellEvents) {
64
+ return;
65
+ }
66
+
67
+ const onBlur = () => {
68
+ setWindowFocused(false);
69
+
70
+ if (document.activeElement === ref.current) {
71
+ onIframeFocus && onIframeFocus();
72
+ }
73
+ };
74
+
75
+ const onFocus = () => {
76
+ setWindowFocused(true);
77
+ };
78
+
79
+ window.addEventListener('blur', onBlur);
80
+ window.addEventListener('focus', onFocus);
81
+ return () => {
82
+ window.removeEventListener('blur', onBlur);
83
+ window.removeEventListener('focus', onFocus);
84
+ };
85
+ }, [trackIframeDwellEvents, ref, onIframeFocus]);
11
86
 
12
87
  if (!url) {
13
88
  return null;
14
89
  }
15
90
 
16
- return jsx("iframe", {
17
- ref: iframeRef,
91
+ return jsx(React.Fragment, null, trackIframeDwellEvents ? jsx(IframeDwellTracker, {
92
+ isIframeLoaded: isIframeLoaded,
93
+ isMouseOver: isMouseOver,
94
+ isWindowFocused: isWindowFocused,
95
+ iframePercentVisible: percentVisible,
96
+ onIframeDwell: onIframeDwell
97
+ }) : null, jsx("iframe", {
98
+ ref: mergedRef,
18
99
  src: url,
19
100
  "data-testid": `${testId}-frame`,
20
101
  "data-iframe-loaded": isIframeLoaded,
@@ -28,10 +109,14 @@ export const Frame = /*#__PURE__*/React.forwardRef(({
28
109
  overflow: 'hidden',
29
110
  borderRadius: '3px'
30
111
  },
112
+ onMouseEnter: trackIframeDwellEvents ? () => setMouseOver(true) : undefined,
113
+ onMouseLeave: trackIframeDwellEvents ? () => setMouseOver(false) : undefined,
31
114
  allowFullScreen: true,
32
115
  scrolling: "yes",
33
116
  allow: "autoplay; encrypted-media; clipboard-write",
34
- onLoad: () => setIframeLoaded(true),
117
+ onLoad: () => {
118
+ setIframeLoaded(true);
119
+ },
35
120
  sandbox: getIframeSandboxAttribute(isTrusted)
36
- });
121
+ }));
37
122
  });
@@ -0,0 +1,57 @@
1
+ /** @jsx jsx */
2
+ import { useEffect, useRef, useState } from 'react';
3
+
4
+ /**
5
+ * A kind of cheap logarithmic backoff. Fire analytics after the user has
6
+ * dwelled for 5 seconds, then 10 seconds, and so on.
7
+ */
8
+ const INTERVALS_TO_LOG = [5, 10, 15, 20, 25, 30, 45, 60, 90, 120, 180];
9
+ export const IframeDwellTracker = ({
10
+ isIframeLoaded,
11
+ isMouseOver,
12
+ isWindowFocused,
13
+ iframePercentVisible,
14
+ onIframeDwell
15
+ }) => {
16
+ const [dwell, setDwell] = useState({
17
+ time: 0,
18
+ percentVisible: 0
19
+ });
20
+ const dwellTimeoutId = useRef(); // fire analytics when the dwell timer reaches 5 seconds
21
+
22
+ useEffect(() => {
23
+ if (dwell.time > 0 && INTERVALS_TO_LOG.includes(dwell.time)) {
24
+ onIframeDwell && onIframeDwell(dwell.time, dwell.percentVisible);
25
+ }
26
+ }, [dwell, onIframeDwell]); // start and stop tracking dwell time
27
+
28
+ useEffect(() => {
29
+ const incrementDwellTime = () => {
30
+ // callback is called on dwell state changes, so change percentVisible and time together
31
+ setDwell(({
32
+ time
33
+ }) => {
34
+ return {
35
+ time: time + 1,
36
+ // snapshot of percent visible, rounded down to nearest 5%
37
+ percentVisible: Math.floor((iframePercentVisible || 0) * 20) * 5
38
+ };
39
+ });
40
+ };
41
+
42
+ if (isIframeLoaded && isMouseOver && isWindowFocused && iframePercentVisible > 0.75) {
43
+ if (dwellTimeoutId.current) {
44
+ clearInterval(dwellTimeoutId.current);
45
+ }
46
+
47
+ dwellTimeoutId.current = setInterval(incrementDwellTime, 1000);
48
+ }
49
+
50
+ return () => {
51
+ if (dwellTimeoutId.current) {
52
+ clearInterval(dwellTimeoutId.current);
53
+ }
54
+ };
55
+ }, [setDwell, isMouseOver, isWindowFocused, isIframeLoaded, iframePercentVisible]);
56
+ return null;
57
+ };
@@ -31,7 +31,9 @@ export const EmbedCard = /*#__PURE__*/React.forwardRef(({
31
31
  onResolve,
32
32
  onError,
33
33
  testId,
34
- inheritDimensions
34
+ inheritDimensions,
35
+ onIframeDwell,
36
+ onIframeFocus
35
37
  }, iframeRef) => {
36
38
  var _details$meta, _forbiddenViewProps$c;
37
39
 
@@ -67,7 +69,9 @@ export const EmbedCard = /*#__PURE__*/React.forwardRef(({
67
69
  isFrameVisible: isFrameVisible,
68
70
  inheritDimensions: inheritDimensions,
69
71
  onClick: handleFrameClick,
70
- ref: iframeRef
72
+ ref: iframeRef,
73
+ onIframeDwell: onIframeDwell,
74
+ onIframeFocus: onIframeFocus
71
75
  }));
72
76
  } else {
73
77
  if (platform === 'mobile') {
@@ -15,7 +15,9 @@ export const EmbedCardResolvedView = /*#__PURE__*/React.forwardRef(({
15
15
  title,
16
16
  isTrusted,
17
17
  testId = 'embed-card-resolved-view',
18
- inheritDimensions
18
+ inheritDimensions,
19
+ onIframeDwell,
20
+ onIframeFocus
19
21
  }, embedIframeRef) => {
20
22
  const iconFromContext = context === null || context === void 0 ? void 0 : context.icon;
21
23
  const src = typeof iconFromContext === 'string' ? iconFromContext : undefined;
@@ -48,6 +50,8 @@ export const EmbedCardResolvedView = /*#__PURE__*/React.forwardRef(({
48
50
  url: preview === null || preview === void 0 ? void 0 : preview.src,
49
51
  isTrusted: isTrusted,
50
52
  testId: testId,
51
- ref: embedIframeRef
53
+ ref: embedIframeRef,
54
+ onIframeDwell: onIframeDwell,
55
+ onIframeFocus: onIframeFocus
52
56
  }));
53
57
  });
@@ -1,6 +1,7 @@
1
1
  /** @jsx jsx */
2
2
  import { jsx } from '@emotion/react';
3
3
  import ShortcutIcon from '@atlaskit/icon/glyph/shortcut';
4
+ import CopyIcon from '@atlaskit/icon/glyph/copy';
4
5
  import { extractType } from '@atlaskit/linking-common/extractors';
5
6
  import React, { useCallback, useEffect, useMemo } from 'react';
6
7
  import { ActionName, CardDisplay, SmartLinkPosition, SmartLinkSize } from '../../../constants';
@@ -36,6 +37,17 @@ export const getOpenAction = (url, analytics, onActionClick) => ({
36
37
  tooltipMessage: jsx(FormattedMessage, messages.open_link_in_a_new_tab),
37
38
  testId: 'hover-card-open-button'
38
39
  });
40
+ export const getCopyAction = url => ({
41
+ name: ActionName.CustomAction,
42
+ icon: jsx(CopyIcon, {
43
+ label: "copy url",
44
+ size: "medium"
45
+ }),
46
+ iconPosition: 'before',
47
+ onClick: async () => await navigator.clipboard.writeText(url),
48
+ tooltipMessage: jsx(FormattedMessage, messages.copy_url_to_clipboard),
49
+ testId: 'hover-card-copy-button'
50
+ });
39
51
 
40
52
  const HoverCardContent = ({
41
53
  id = '',
@@ -66,7 +78,7 @@ const HoverCardContent = ({
66
78
  actionSubjectId: 'titleGoToLink'
67
79
  });
68
80
  }, [cardState.status, analytics.ui, id]);
69
- const titleActions = useMemo(() => [getOpenAction(url, analytics, onActionClick)], [url, analytics, onActionClick]);
81
+ const titleActions = useMemo(() => [getCopyAction(url), getOpenAction(url, analytics, onActionClick)], [url, analytics, onActionClick]);
70
82
  const data = (_cardState$details = cardState.details) === null || _cardState$details === void 0 ? void 0 : _cardState$details.data;
71
83
  const types = data ? extractType(data) : undefined;
72
84
  const {
@@ -85,6 +85,11 @@ export var messages = defineMessages({
85
85
  defaultMessage: 'Connect to Atlassian to view more details from your work and collaboration from one place.',
86
86
  description: 'Shown when a user does not have access to a link, but can connect their external account to view the link on card view and we do not have the providers name.'
87
87
  },
88
+ copy_url_to_clipboard: {
89
+ id: 'fabric.linking.download',
90
+ defaultMessage: 'Copy link',
91
+ description: ''
92
+ },
88
93
  could_not_load_link: {
89
94
  id: 'fabric.linking.couldnt_load_link',
90
95
  defaultMessage: "We couldn't load this link for an unknown reason.",