@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.
- package/CHANGELOG.md +16 -0
- package/dist/cjs/messages.js +5 -0
- package/dist/cjs/state/analytics/useSmartLinkAnalytics.js +216 -146
- package/dist/cjs/utils/analytics/analytics.js +134 -74
- package/dist/cjs/utils/analytics/index.js +6 -0
- package/dist/cjs/version.json +1 -1
- package/dist/cjs/view/CardWithUrl/component.js +27 -1
- package/dist/cjs/view/EmbedCard/components/Frame.js +110 -5
- package/dist/cjs/view/EmbedCard/components/IframeDwellTracker.js +75 -0
- package/dist/cjs/view/EmbedCard/index.js +6 -2
- package/dist/cjs/view/EmbedCard/views/ResolvedView.js +6 -2
- package/dist/cjs/view/HoverCard/components/HoverCardContent.js +49 -2
- package/dist/es2019/messages.js +5 -0
- package/dist/es2019/state/analytics/useSmartLinkAnalytics.js +69 -1
- package/dist/es2019/utils/analytics/analytics.js +52 -0
- package/dist/es2019/utils/analytics/index.js +1 -1
- package/dist/es2019/version.json +1 -1
- package/dist/es2019/view/CardWithUrl/component.js +27 -1
- package/dist/es2019/view/EmbedCard/components/Frame.js +91 -6
- package/dist/es2019/view/EmbedCard/components/IframeDwellTracker.js +57 -0
- package/dist/es2019/view/EmbedCard/index.js +6 -2
- package/dist/es2019/view/EmbedCard/views/ResolvedView.js +6 -2
- package/dist/es2019/view/HoverCard/components/HoverCardContent.js +13 -1
- package/dist/esm/messages.js +5 -0
- package/dist/esm/state/analytics/useSmartLinkAnalytics.js +217 -147
- package/dist/esm/utils/analytics/analytics.js +127 -73
- package/dist/esm/utils/analytics/index.js +1 -1
- package/dist/esm/version.json +1 -1
- package/dist/esm/view/CardWithUrl/component.js +27 -1
- package/dist/esm/view/EmbedCard/components/Frame.js +110 -6
- package/dist/esm/view/EmbedCard/components/IframeDwellTracker.js +62 -0
- package/dist/esm/view/EmbedCard/index.js +6 -2
- package/dist/esm/view/EmbedCard/views/ResolvedView.js +6 -2
- package/dist/esm/view/HoverCard/components/HoverCardContent.js +43 -1
- package/dist/types/messages.d.ts +1 -1
- package/dist/types/state/analytics/useSmartLinkAnalytics.d.ts +27 -1
- package/dist/types/state/flexible-ui-context/index.d.ts +4 -0
- package/dist/types/state/hooks/useSmartLink.d.ts +2 -0
- package/dist/types/utils/analytics/analytics.d.ts +3 -1
- package/dist/types/utils/analytics/index.d.ts +1 -1
- package/dist/types/utils/analytics/types.d.ts +12 -0
- package/dist/types/utils/mocks.d.ts +2 -0
- package/dist/types/utils/types.d.ts +2 -2
- package/dist/types/view/EmbedCard/components/Frame.d.ts +2 -0
- package/dist/types/view/EmbedCard/components/IframeDwellTracker.d.ts +8 -0
- package/dist/types/view/EmbedCard/types.d.ts +2 -0
- package/dist/types/view/EmbedCard/views/ResolvedView.d.ts +4 -0
- package/dist/types/view/HoverCard/components/HoverCardContent.d.ts +1 -0
- package/package.json +2 -2
- package/report.api.md +42 -1
- 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;
|
package/dist/es2019/messages.js
CHANGED
|
@@ -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,
|
package/dist/es2019/version.json
CHANGED
|
@@ -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(
|
|
17
|
-
|
|
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: () =>
|
|
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 {
|
package/dist/esm/messages.js
CHANGED
|
@@ -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.",
|