@atlaskit/smart-card 43.11.3 → 43.12.1

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 (42) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/analytics.spec.yaml +7 -1
  3. package/dist/cjs/utils/analytics/analytics.js +1 -1
  4. package/dist/cjs/view/CardWithUrl/component.js +327 -3
  5. package/dist/cjs/view/EmbedCard/components/ExpandedFrame.js +93 -2
  6. package/dist/cjs/view/EmbedCard/components/Frame.js +121 -3
  7. package/dist/cjs/view/EmbedCard/components/IframeDwellTracker.js +25 -4
  8. package/dist/cjs/view/EmbedCard/index.js +204 -1
  9. package/dist/cjs/view/EmbedCard/views/ResolvedView.js +95 -2
  10. package/dist/cjs/view/EmbedCard/views/not-found-view/not-found-svg/index.js +5 -1
  11. package/dist/cjs/view/LinkUrl/index.js +1 -1
  12. package/dist/es2019/utils/analytics/analytics.js +1 -1
  13. package/dist/es2019/view/CardWithUrl/component.js +324 -2
  14. package/dist/es2019/view/EmbedCard/components/ExpandedFrame.js +87 -2
  15. package/dist/es2019/view/EmbedCard/components/Frame.js +112 -2
  16. package/dist/es2019/view/EmbedCard/components/IframeDwellTracker.js +25 -4
  17. package/dist/es2019/view/EmbedCard/index.js +208 -0
  18. package/dist/es2019/view/EmbedCard/views/ResolvedView.js +91 -3
  19. package/dist/es2019/view/EmbedCard/views/not-found-view/not-found-svg/index.js +5 -1
  20. package/dist/es2019/view/LinkUrl/index.js +1 -1
  21. package/dist/esm/utils/analytics/analytics.js +1 -1
  22. package/dist/esm/view/CardWithUrl/component.js +326 -2
  23. package/dist/esm/view/EmbedCard/components/ExpandedFrame.js +95 -2
  24. package/dist/esm/view/EmbedCard/components/Frame.js +122 -2
  25. package/dist/esm/view/EmbedCard/components/IframeDwellTracker.js +25 -4
  26. package/dist/esm/view/EmbedCard/index.js +203 -0
  27. package/dist/esm/view/EmbedCard/views/ResolvedView.js +97 -3
  28. package/dist/esm/view/EmbedCard/views/not-found-view/not-found-svg/index.js +5 -1
  29. package/dist/esm/view/LinkUrl/index.js +1 -1
  30. package/dist/types/common/analytics/generated/analytics.types.d.ts +1 -0
  31. package/dist/types/view/EmbedCard/components/ExpandedFrame.d.ts +8 -1
  32. package/dist/types/view/EmbedCard/components/Frame.d.ts +6 -0
  33. package/dist/types/view/EmbedCard/index.d.ts +4 -0
  34. package/dist/types/view/EmbedCard/types.d.ts +4 -0
  35. package/dist/types/view/EmbedCard/views/ResolvedView.d.ts +6 -1
  36. package/dist/types-ts4.5/common/analytics/generated/analytics.types.d.ts +1 -0
  37. package/dist/types-ts4.5/view/EmbedCard/components/ExpandedFrame.d.ts +8 -1
  38. package/dist/types-ts4.5/view/EmbedCard/components/Frame.d.ts +6 -0
  39. package/dist/types-ts4.5/view/EmbedCard/index.d.ts +4 -0
  40. package/dist/types-ts4.5/view/EmbedCard/types.d.ts +4 -0
  41. package/dist/types-ts4.5/view/EmbedCard/views/ResolvedView.d.ts +6 -1
  42. package/package.json +8 -1
@@ -2,6 +2,7 @@ import React, { useCallback, useEffect, useMemo } from 'react';
2
2
  import { useAnalyticsEvents as useAnalyticsEventsNext } from '@atlaskit/analytics-next';
3
3
  import { extractSmartLinkEmbed } from '@atlaskit/link-extractors';
4
4
  import { fg } from '@atlaskit/platform-feature-flags';
5
+ import { componentWithFG } from '@atlaskit/platform-feature-flags-react';
5
6
  import { expValEquals } from '@atlaskit/tmp-editor-statsig/exp-val-equals';
6
7
  import { editorExperiment } from '@atlaskit/tmp-editor-statsig/experiments';
7
8
  import { useAnalyticsEvents } from '../../common/analytics/generated/use-analytics-events';
@@ -17,11 +18,12 @@ import { SmartLinkAnalyticsContext } from '../../utils/analytics/SmartLinkAnalyt
17
18
  import { isFlexibleUiCard } from '../../utils/flexible';
18
19
  import * as measure from '../../utils/performance';
19
20
  import { BlockCard } from '../BlockCard';
20
- import { EmbedCard } from '../EmbedCard';
21
+ import { EmbedCard, EmbedCardUpdated } from '../EmbedCard';
21
22
  import FlexibleCard from '../FlexibleCard';
22
23
  import { InlineCard } from '../InlineCard';
23
24
  import { useFire3PWorkflowsClickEvent } from '../SmartLinkEvents/useSmartLinkEvents';
24
25
  const thirdPartyARIPrefix = 'ari:third-party';
26
+ const EmbedCardComponent = componentWithFG('rovo_chat_embed_card_dwell_and_hover_metrics', EmbedCardUpdated, EmbedCard);
25
27
  function Component({
26
28
  id,
27
29
  url,
@@ -322,11 +324,331 @@ function Component({
322
324
  });
323
325
  }
324
326
  }
327
+ function ComponentUpdated({
328
+ id,
329
+ url,
330
+ isSelected,
331
+ isHovered,
332
+ frameStyle,
333
+ platform,
334
+ onClick,
335
+ appearance,
336
+ onResolve,
337
+ onError,
338
+ testId,
339
+ actionOptions: actionOptionsProp,
340
+ inheritDimensions,
341
+ embedIframeRef,
342
+ embedIframeUrlType,
343
+ inlinePreloaderStyle,
344
+ ui,
345
+ children,
346
+ showHoverPreview,
347
+ hoverPreviewOptions,
348
+ removeTextHighlightingFromTitle,
349
+ resolvingPlaceholder,
350
+ truncateInline,
351
+ CompetitorPrompt,
352
+ hideIconLoadingSkeleton,
353
+ disablePreviewPanel,
354
+ placeholderData
355
+ }) {
356
+ const {
357
+ createAnalyticsEvent
358
+ } = useAnalyticsEventsNext();
359
+ const {
360
+ fireEvent
361
+ } = useAnalyticsEvents();
362
+ let isFlexibleUi = useMemo(() => isFlexibleUiCard(children, ui), [children, ui]);
363
+
364
+ // Get state, actions for this card.
365
+ const {
366
+ state,
367
+ actions,
368
+ config,
369
+ renderers,
370
+ error,
371
+ isPreviewPanelAvailable,
372
+ openPreviewPanel
373
+ } = useSmartLink(id, url);
374
+ const ari = getObjectAri(state.details);
375
+ const name = getObjectName(state.details);
376
+ const definitionId = getDefinitionId(state.details);
377
+ const extensionKey = getExtensionKey(state.details);
378
+ const resourceType = getResourceType(state.details);
379
+ const services = getServices(state.details);
380
+ const thirdPartyARI = getThirdPartyARI(state.details);
381
+ const firstPartyIdentifier = getFirstPartyIdentifier();
382
+ const actionOptions = combineActionOptions({
383
+ actionOptions: actionOptionsProp,
384
+ platform
385
+ });
386
+ const fire3PClickEvent = fg('platform_smartlink_3pclick_analytics') ?
387
+ // eslint-disable-next-line react-hooks/rules-of-hooks
388
+ useFire3PWorkflowsClickEvent(firstPartyIdentifier, thirdPartyARI) : undefined;
389
+
390
+ // Setup UI handlers.
391
+ const handleClickWrapper = useCallback(event => {
392
+ const isModifierKeyPressed = isSpecialKey(event) || isSpecialClick(event);
393
+ fireEvent('ui.smartLink.clicked', {
394
+ id,
395
+ display: isFlexibleUi ? CardDisplay.Flexible : appearance,
396
+ definitionId: definitionId !== null && definitionId !== void 0 ? definitionId : null,
397
+ isModifierKeyPressed
398
+ });
399
+ if (fg('platform_smartlink_3pclick_analytics')) {
400
+ if (thirdPartyARI && thirdPartyARI.startsWith(thirdPartyARIPrefix)) {
401
+ const clickURL = getClickUrl(url, state.details);
402
+ if (clickURL === url && fire3PClickEvent) {
403
+ // For questions or concerns about this event,
404
+ // please reach out to the 3P Workflows Team via Slack in #help-3p-connector-workflow
405
+ fire3PClickEvent();
406
+ }
407
+ }
408
+ }
409
+ const isDisablePreviewPanel = disablePreviewPanel && editorExperiment('platform_editor_preview_panel_linking_exp', true, {
410
+ exposure: true
411
+ });
412
+
413
+ // If preview panel is available and the user clicked on the link,
414
+ // delegate the click to the preview panel handler
415
+ if (!isModifierKeyPressed && ari && name && openPreviewPanel && isPreviewPanelAvailable !== null && isPreviewPanelAvailable !== void 0 && isPreviewPanelAvailable({
416
+ ari
417
+ }) && !isDisablePreviewPanel) {
418
+ var _extractSmartLinkEmbe2;
419
+ event.preventDefault();
420
+ event.stopPropagation();
421
+ openPreviewPanel({
422
+ url,
423
+ ari,
424
+ name,
425
+ iconUrl: getObjectIconUrl(state.details),
426
+ panelData: {
427
+ embedUrl: expValEquals('platform_hover_card_preview_panel', 'cohort', 'test') ? (_extractSmartLinkEmbe2 = extractSmartLinkEmbed(state.details)) === null || _extractSmartLinkEmbe2 === void 0 ? void 0 : _extractSmartLinkEmbe2.src : undefined
428
+ }
429
+ });
430
+ fireLinkClickedEvent(createAnalyticsEvent)(event, {
431
+ attributes: {
432
+ clickOutcome: 'previewPanel'
433
+ }
434
+ });
435
+ return;
436
+ } else if (!onClick && !isFlexibleUi) {
437
+ const clickUrl = getClickUrl(url, state.details);
438
+
439
+ // Ctrl+left click on mac typically doesn't trigger onClick
440
+ // The event could have potentially had `e.preventDefault()` called on it by now
441
+ // event by smart card internally
442
+ // If it has been called then only then can `isSpecialEvent` be true.
443
+ const target = isSpecialEvent(event) ? '_blank' : '_self';
444
+ window.open(clickUrl, target);
445
+ fireLinkClickedEvent(createAnalyticsEvent)(event, {
446
+ attributes: {
447
+ clickOutcome: target === '_blank' ? 'clickThroughNewTabOrWindow' : 'clickThrough'
448
+ }
449
+ });
450
+ } else {
451
+ if (onClick) {
452
+ onClick(event);
453
+ }
454
+ fireLinkClickedEvent(createAnalyticsEvent)(event);
455
+ }
456
+ }, [fireEvent, id, isFlexibleUi, appearance, definitionId, onClick, url, state.details, ari, name, fire3PClickEvent, isPreviewPanelAvailable, openPreviewPanel, createAnalyticsEvent, thirdPartyARI, disablePreviewPanel]);
457
+ const handleAuthorize = useCallback(() => actions.authorize(appearance), [actions, appearance]);
458
+ const handleRetry = useCallback(() => {
459
+ actions.reload();
460
+ }, [actions]);
461
+ const handleInvoke = useCallback(opts => actions.invoke(opts, appearance), [actions, appearance]);
462
+
463
+ // NB: for each status change in a Smart Link, a performance mark is created.
464
+ // Measures are sent relative to the first mark, matching what a user sees.
465
+ useEffect(() => {
466
+ measure.mark(id, state.status);
467
+ if (state.status !== 'pending' && state.status !== 'resolving') {
468
+ var _state$error3, _state$error4;
469
+ measure.create(id, state.status);
470
+ if (state.status === 'resolved') {
471
+ var _measure$getMeasure$d2, _measure$getMeasure2;
472
+ fireEvent('operational.smartLink.resolved', {
473
+ definitionId: definitionId !== null && definitionId !== void 0 ? definitionId : null,
474
+ duration: (_measure$getMeasure$d2 = (_measure$getMeasure2 = measure.getMeasure(id, state.status)) === null || _measure$getMeasure2 === void 0 ? void 0 : _measure$getMeasure2.duration) !== null && _measure$getMeasure$d2 !== void 0 ? _measure$getMeasure$d2 : null
475
+ });
476
+ } else if (((_state$error3 = state.error) === null || _state$error3 === void 0 ? void 0 : _state$error3.type) !== 'ResolveUnsupportedError' && ((_state$error4 = state.error) === null || _state$error4 === void 0 ? void 0 : _state$error4.type) !== 'UnsupportedError') {
477
+ fireEvent('operational.smartLink.unresolved', {
478
+ definitionId: definitionId !== null && definitionId !== void 0 ? definitionId : null,
479
+ reason: state.status,
480
+ error: state.error === undefined ? null : {
481
+ name: state.error.name,
482
+ kind: state.error.kind,
483
+ type: state.error.type
484
+ }
485
+ });
486
+ }
487
+ }
488
+ }, [id, appearance, state.status, state.error, definitionId, extensionKey, resourceType, fireEvent]);
489
+
490
+ // NB: once the smart-card has rendered into an end state, we capture
491
+ // this as a successful render. These can be one of:
492
+ // - the resolved state: when metadata is shown;
493
+ // - the unresolved states: viz. forbidden, not_found, unauthorized, errored.
494
+ useEffect(() => {
495
+ if (isFinalState(state.status)) {
496
+ succeedUfoExperience('smart-link-rendered', id || 'NULL', {
497
+ extensionKey,
498
+ display: isFlexibleUi ? 'flexible' : appearance
499
+ });
500
+
501
+ // UFO will disregard this if authentication experience has not yet been started
502
+ succeedUfoExperience('smart-link-authenticated', id || 'NULL', {
503
+ display: isFlexibleUi ? 'flexible' : appearance
504
+ });
505
+ fireEvent('ui.smartLink.renderSuccess', {
506
+ display: isFlexibleUi ? 'flexible' : appearance
507
+ });
508
+ }
509
+ }, [appearance, extensionKey, fireEvent, id, isFlexibleUi, state.status]);
510
+ const onIframeDwell = useCallback((dwellTime, dwellPercentVisible) => {
511
+ fireEvent('ui.smartLinkIframe.dwelled', {
512
+ id,
513
+ definitionId: definitionId !== null && definitionId !== void 0 ? definitionId : null,
514
+ display: isFlexibleUi ? 'flexible' : appearance,
515
+ dwellPercentVisible,
516
+ dwellTime
517
+ });
518
+ }, [id, appearance, definitionId, isFlexibleUi, fireEvent]);
519
+ const onIframeFocus = useCallback(() => {
520
+ fireEvent('ui.smartLinkIframe.focused', {
521
+ id,
522
+ definitionId: definitionId !== null && definitionId !== void 0 ? definitionId : null,
523
+ display: isFlexibleUi ? 'flexible' : appearance,
524
+ interactionType: 'focus'
525
+ });
526
+ }, [id, appearance, definitionId, isFlexibleUi, fireEvent]);
527
+ const onIframeMouseEnter = useCallback(() => {
528
+ fireEvent('ui.smartLinkIframe.focused', {
529
+ id,
530
+ definitionId: definitionId !== null && definitionId !== void 0 ? definitionId : null,
531
+ display: isFlexibleUi ? 'flexible' : appearance,
532
+ interactionType: 'mouseenter'
533
+ });
534
+ }, [id, appearance, definitionId, isFlexibleUi, fireEvent]);
535
+ const onIframeMouseLeave = useCallback(() => {
536
+ fireEvent('ui.smartLinkIframe.focused', {
537
+ id,
538
+ definitionId: definitionId !== null && definitionId !== void 0 ? definitionId : null,
539
+ display: isFlexibleUi ? 'flexible' : appearance,
540
+ interactionType: 'mouseleave'
541
+ });
542
+ }, [id, appearance, definitionId, isFlexibleUi, fireEvent]);
543
+ if (isFlexibleUi) {
544
+ let cardState = state;
545
+ if (error) {
546
+ if ((error === null || error === void 0 ? void 0 : error.name) === 'APIError') {
547
+ cardState = {
548
+ status: 'errored'
549
+ };
550
+ } else {
551
+ throw error;
552
+ }
553
+ }
554
+ return /*#__PURE__*/React.createElement(FlexibleCard, {
555
+ id: id,
556
+ cardState: cardState,
557
+ placeholderData: fg('platform_initial_data_for_smart_cards') ? placeholderData : undefined,
558
+ onAuthorize: services.length && handleAuthorize || undefined,
559
+ onClick: handleClickWrapper,
560
+ origin: "smartLinkCard",
561
+ renderers: renderers,
562
+ ui: ui,
563
+ showHoverPreview: showHoverPreview,
564
+ hoverPreviewOptions: hoverPreviewOptions,
565
+ actionOptions: actionOptions,
566
+ url: url,
567
+ testId: testId,
568
+ onResolve: onResolve,
569
+ onError: onError
570
+ }, children);
571
+ }
572
+
573
+ // We have to keep this last to prevent hook order from being violated
574
+ if (error) {
575
+ throw error;
576
+ }
577
+ switch (appearance) {
578
+ case 'inline':
579
+ return /*#__PURE__*/React.createElement(InlineCard, {
580
+ id: id,
581
+ url: url,
582
+ renderers: renderers,
583
+ cardState: state,
584
+ handleAuthorize: services.length && handleAuthorize || undefined,
585
+ handleFrameClick: handleClickWrapper,
586
+ isSelected: isSelected,
587
+ isHovered: isHovered,
588
+ onResolve: onResolve,
589
+ onError: onError,
590
+ testId: testId,
591
+ inlinePreloaderStyle: inlinePreloaderStyle,
592
+ showHoverPreview: showHoverPreview,
593
+ hoverPreviewOptions: hoverPreviewOptions,
594
+ actionOptions: actionOptions,
595
+ removeTextHighlightingFromTitle: removeTextHighlightingFromTitle,
596
+ resolvingPlaceholder: resolvingPlaceholder,
597
+ truncateInline: truncateInline,
598
+ hideIconLoadingSkeleton: hideIconLoadingSkeleton
599
+ });
600
+ case 'block':
601
+ return /*#__PURE__*/React.createElement(BlockCard, {
602
+ id: id,
603
+ url: url,
604
+ renderers: renderers,
605
+ authFlow: config && config.authFlow,
606
+ cardState: state,
607
+ handleAuthorize: services.length && handleAuthorize || undefined,
608
+ handleFrameClick: handleClickWrapper,
609
+ isSelected: isSelected,
610
+ onResolve: onResolve,
611
+ onError: onError,
612
+ testId: testId,
613
+ actionOptions: actionOptions,
614
+ CompetitorPrompt: CompetitorPrompt,
615
+ hideIconLoadingSkeleton: hideIconLoadingSkeleton
616
+ });
617
+ case 'embed':
618
+ return /*#__PURE__*/React.createElement(EmbedCardComponent, {
619
+ id: id,
620
+ url: url,
621
+ renderers: renderers,
622
+ cardState: state,
623
+ iframeUrlType: embedIframeUrlType,
624
+ handleAuthorize: services.length && handleAuthorize || undefined,
625
+ handleErrorRetry: handleRetry,
626
+ handleFrameClick: handleClickWrapper,
627
+ handleInvoke: handleInvoke,
628
+ isSelected: isSelected,
629
+ frameStyle: frameStyle,
630
+ platform: platform,
631
+ onResolve: onResolve,
632
+ onError: onError,
633
+ testId: testId,
634
+ inheritDimensions: inheritDimensions,
635
+ actionOptions: actionOptions,
636
+ ref: embedIframeRef,
637
+ onIframeDwell: onIframeDwell,
638
+ onIframeFocus: onIframeFocus,
639
+ onIframeMouseEnter: onIframeMouseEnter,
640
+ onIframeMouseLeave: onIframeMouseLeave,
641
+ CompetitorPrompt: CompetitorPrompt,
642
+ hideIconLoadingSkeleton: hideIconLoadingSkeleton
643
+ });
644
+ }
645
+ }
646
+ const CardWithUrlContentComponent = componentWithFG('rovo_chat_embed_card_dwell_and_hover_metrics', ComponentUpdated, Component);
325
647
  export const CardWithUrlContent = props => {
326
648
  const display = isFlexibleUiCard(props.children, props === null || props === void 0 ? void 0 : props.ui) ? CardDisplay.Flexible : props.appearance;
327
649
  return /*#__PURE__*/React.createElement(SmartLinkModalProvider, null, /*#__PURE__*/React.createElement(SmartLinkAnalyticsContext, {
328
650
  url: props.url,
329
651
  id: props.id,
330
652
  display: display
331
- }, /*#__PURE__*/React.createElement(Component, props)));
653
+ }, /*#__PURE__*/React.createElement(CardWithUrlContentComponent, props)));
332
654
  };
@@ -3,12 +3,15 @@ import _extends from "@babel/runtime/helpers/extends";
3
3
  import "./ExpandedFrame.compiled.css";
4
4
  import * as React from 'react';
5
5
  import { ax, ix } from "@compiled/react/runtime";
6
+ // eslint-disable-next-line no-unused-vars
7
+
6
8
  import { fg } from '@atlaskit/platform-feature-flags';
9
+ import { componentWithFG } from '@atlaskit/platform-feature-flags-react';
7
10
  import Tooltip from '@atlaskit/tooltip';
8
11
  import { useMouseDownEvent } from '../../../state/analytics/useLinkClicked';
9
12
  import { handleClickCommon } from '../../common/utils';
10
13
  import { className } from './styled';
11
- export const ExpandedFrame = ({
14
+ const ExpandedFrame = ({
12
15
  isPlaceholder = false,
13
16
  children,
14
17
  onClick,
@@ -96,4 +99,86 @@ const styles = {
96
99
  contentStyle: "_19itdlqj _2rko12b0 _1reo15vq _18m915vq _v56414au _bfhkhp5a _16jlkb7n _4t3i1osq _1pbykb7n",
97
100
  contentInteractiveActiveBorder: "_1jhm1tt7",
98
101
  contentOverflowAuto: "_1reo1wug _18m91wug"
99
- };
102
+ };
103
+ const ExpandedFrameUpdated = ({
104
+ isPlaceholder = false,
105
+ children,
106
+ onClick,
107
+ icon,
108
+ text,
109
+ isSelected,
110
+ frameStyle = 'showOnHover',
111
+ href,
112
+ minWidth,
113
+ maxWidth,
114
+ testId = 'expanded-frame',
115
+ inheritDimensions,
116
+ allowScrollBar = false,
117
+ setOverflow = true,
118
+ CompetitorPrompt,
119
+ onContentMouseEnter,
120
+ onContentMouseLeave
121
+ }) => {
122
+ const isInteractive = () => !isPlaceholder && (Boolean(href) || Boolean(onClick));
123
+ const handleClick = event => handleClickCommon(event, onClick);
124
+ const handleMouseDown = useMouseDownEvent();
125
+
126
+ // Note: cleanup fg based on results of prompt_whiteboard_competitor_link experiment
127
+ const CompetitorPromptComponent = CompetitorPrompt && href && fg('prompt_whiteboard_competitor_link_gate') ? /*#__PURE__*/React.createElement(CompetitorPrompt, {
128
+ sourceUrl: href,
129
+ linkType: "embed"
130
+ }) : null;
131
+ const renderHeader = () => {
132
+ return frameStyle !== 'hide' && /*#__PURE__*/React.createElement("div", {
133
+ className: ax([styles.header, "embed-header"])
134
+ }, /*#__PURE__*/React.createElement("div", {
135
+ className: ax([styles.leftSection])
136
+ }, /*#__PURE__*/React.createElement("div", {
137
+ className: ax([styles.headerIcon])
138
+ }, icon), /*#__PURE__*/React.createElement("div", {
139
+ className: ax([styles.tooltipWrapper])
140
+ }, !isPlaceholder && /*#__PURE__*/React.createElement(Tooltip, {
141
+ content: text,
142
+ hideTooltipOnMouseDown: true
143
+ }, /*#__PURE__*/React.createElement("a", {
144
+ href: href,
145
+ onClick: handleClick,
146
+ onMouseDown: handleMouseDown,
147
+ className: ax([styles.headerAnchor])
148
+ }, text)))), CompetitorPromptComponent);
149
+ };
150
+ const interactive = isInteractive();
151
+ const showBackgroundAlways = frameStyle === 'show' || isSelected && frameStyle !== 'hide';
152
+ const showBackgroundOnHover = interactive && frameStyle !== 'hide';
153
+ const renderContent = () => {
154
+ return /*#__PURE__*/React.createElement("div", {
155
+ "data-testid": "embed-content-wrapper",
156
+ // This fixes an issue with input fields in cross domain iframes (ie. databases and jira fields from different domains)
157
+ // See: HOT-107830
158
+ contentEditable: false,
159
+ onMouseEnter: onContentMouseEnter,
160
+ onMouseLeave: onContentMouseLeave,
161
+ onFocus: onContentMouseEnter,
162
+ onBlur: onContentMouseLeave,
163
+ className: ax([styles.contentStyle, setOverflow && allowScrollBar && styles.contentOverflowAuto, interactive && !showBackgroundAlways && !showBackgroundOnHover && styles.contentInteractiveActiveBorder])
164
+ }, children);
165
+ };
166
+ return /*#__PURE__*/React.createElement("div", _extends({
167
+ // eslint-disable-next-line @atlaskit/ui-styling-standard/no-classname-prop -- Ignored via go/DSP-18766
168
+ className: ax([styles.linkWrapper, inheritDimensions && styles.linkWrapperInheritDimensions, isSelected && frameStyle !== 'hide' && styles.linkWrapperSelected, showBackgroundAlways && styles.linkWrapperBorderAndBackground, showBackgroundOnHover && !showBackgroundAlways && styles.linkWrapperInteractiveNotHidden, className]),
169
+ style: {
170
+ minWidth: minWidth ? `${minWidth}px` : '',
171
+ maxWidth: maxWidth ? `${maxWidth}px` : ''
172
+ },
173
+ "data-testid": testId,
174
+ "data-trello-do-not-use-override": testId
175
+ // Due to limitations of testing library, we can't assert ::after
176
+ ,
177
+ "data-is-selected": isSelected
178
+ }, (isPlaceholder || !href) && {
179
+ 'data-wrapper-type': 'default',
180
+ 'data-is-interactive': isInteractive()
181
+ }), renderHeader(), renderContent());
182
+ };
183
+ const ExpandedFrameWithFG = componentWithFG('rovo_chat_embed_card_dwell_and_hover_metrics', ExpandedFrameUpdated, ExpandedFrame);
184
+ export { ExpandedFrameWithFG as ExpandedFrame };
@@ -2,6 +2,9 @@
2
2
  import "./Frame.compiled.css";
3
3
  import { ax, ix } from "@compiled/react/runtime";
4
4
  import React, { useEffect, useRef, useState } from 'react';
5
+
6
+ // eslint-disable-next-line no-unused-vars
7
+
5
8
  import { di } from 'react-magnetic-di';
6
9
  import { getIframeSandboxAttribute } from '../../../utils';
7
10
  import { IFrame } from './IFrame';
@@ -87,8 +90,115 @@ export const Frame = /*#__PURE__*/React.forwardRef(({
87
90
  src: url,
88
91
  "data-testid": `${testId}-frame`,
89
92
  "data-iframe-loaded": isIframeLoaded,
90
- onMouseEnter: () => setMouseOver(true),
91
- onMouseLeave: () => setMouseOver(false),
93
+ onMouseEnter: () => {
94
+ setMouseOver(true);
95
+ },
96
+ onMouseLeave: () => {
97
+ setMouseOver(false);
98
+ },
99
+ allowFullScreen: true,
100
+ scrolling: "yes",
101
+ allow: "autoplay; encrypted-media; clipboard-write",
102
+ onLoad: () => {
103
+ setIframeLoaded(true);
104
+ },
105
+ sandbox: getIframeSandboxAttribute(isTrusted),
106
+ title: title,
107
+ extensionKey: extensionKey,
108
+ className: ax(["_19itidpf _1reo15vq _18m915vq _2rkofajl _154iidpf _1ltvidpf _1bsb1osq _4t3i1osq _kqswh2mm"])
109
+ }));
110
+ });
111
+ export const FrameUpdated = /*#__PURE__*/React.forwardRef(({
112
+ url,
113
+ isTrusted = false,
114
+ testId,
115
+ onIframeDwell,
116
+ onIframeFocus,
117
+ onIframeMouseEnter,
118
+ onIframeMouseLeave,
119
+ isMouseOver: isMouseOverProp,
120
+ title,
121
+ extensionKey
122
+ }, iframeRef) => {
123
+ const [isIframeLoaded, setIframeLoaded] = useState(false);
124
+ const [isMouseOver, setMouseOver] = useState(false);
125
+ const [isWindowFocused, setWindowFocused] = useState(document.hasFocus());
126
+
127
+ // Use prop if provided (from wrapper), otherwise use local state (for backward compatibility)
128
+ const effectiveMouseOver = isMouseOverProp !== undefined ? isMouseOverProp : isMouseOver;
129
+ const ref = useRef();
130
+ const mergedRef = mergeRefs([iframeRef, ref]);
131
+ const [percentVisible, setPercentVisible] = useState(0);
132
+
133
+ /**
134
+ * These are the 'percent visible' thresholds at which the intersectionObserver will
135
+ * trigger a state change. Eg. when the user scrolls and moves from 74% to 76%, or
136
+ * vice versa. It's in a state object so that its static for the useEffect
137
+ */
138
+ const [threshold] = useState([0.75, 0.8, 0.85, 0.9, 0.95, 1]);
139
+ useEffect(() => {
140
+ if (!ref || !ref.current) {
141
+ return;
142
+ }
143
+ const observer = new IntersectionObserver(entries => {
144
+ entries.forEach(entry => {
145
+ setPercentVisible(entry === null || entry === void 0 ? void 0 : entry.intersectionRatio);
146
+ });
147
+ }, {
148
+ threshold
149
+ });
150
+ observer.observe(ref.current);
151
+ return () => {
152
+ observer.disconnect();
153
+ };
154
+ }, [threshold, mergedRef]);
155
+ useEffect(() => {
156
+ // Initialize with current focus state
157
+ setWindowFocused(document.hasFocus());
158
+ const onBlur = () => {
159
+ setWindowFocused(false);
160
+ if (document.activeElement === ref.current) {
161
+ onIframeFocus && onIframeFocus();
162
+ }
163
+ };
164
+ const onFocus = () => {
165
+ setWindowFocused(true);
166
+ };
167
+ window.addEventListener('blur', onBlur);
168
+ window.addEventListener('focus', onFocus);
169
+ return () => {
170
+ window.removeEventListener('blur', onBlur);
171
+ window.removeEventListener('focus', onFocus);
172
+ };
173
+ }, [ref, onIframeFocus]);
174
+ if (!url) {
175
+ return null;
176
+ }
177
+ return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(IframeDwellTracker, {
178
+ isIframeLoaded: isIframeLoaded,
179
+ isMouseOver: effectiveMouseOver,
180
+ isWindowFocused: isWindowFocused,
181
+ iframePercentVisible: percentVisible,
182
+ onIframeDwell: onIframeDwell
183
+ }), /*#__PURE__*/React.createElement(IFrame, {
184
+ childRef: mergedRef,
185
+ src: url,
186
+ "data-testid": `${testId}-frame`,
187
+ "data-iframe-loaded": isIframeLoaded,
188
+ onMouseEnter: () => {
189
+ onIframeMouseEnter === null || onIframeMouseEnter === void 0 ? void 0 : onIframeMouseEnter();
190
+ // Use local state if prop not provided, otherwise prop takes precedence
191
+ if (isMouseOverProp === undefined) {
192
+ setMouseOver(true);
193
+ }
194
+ },
195
+ onMouseLeave: () => {
196
+ onIframeMouseLeave === null || onIframeMouseLeave === void 0 ? void 0 : onIframeMouseLeave();
197
+ // Use local state if prop not provided, otherwise prop takes precedence
198
+ if (isMouseOverProp === undefined) {
199
+ setMouseOver(false);
200
+ }
201
+ },
92
202
  allowFullScreen: true,
93
203
  scrolling: "yes",
94
204
  allow: "autoplay; encrypted-media; clipboard-write",
@@ -3,6 +3,7 @@
3
3
  * @jsx jsx
4
4
  */
5
5
  import { useEffect, useRef, useState } from 'react';
6
+ import { fg } from '@atlaskit/platform-feature-flags';
6
7
  /**
7
8
  * A kind of cheap logarithmic backoff. Fire analytics after the user has
8
9
  * dwelled for 5 seconds, then 10 seconds, and so on.
@@ -42,11 +43,31 @@ export const IframeDwellTracker = ({
42
43
  };
43
44
  });
44
45
  };
45
- if (isIframeLoaded && isMouseOver && isWindowFocused && iframePercentVisible > 0.75) {
46
- if (dwellTimeoutId.current) {
47
- clearInterval(dwellTimeoutId.current);
46
+
47
+ // Require: iframe loaded, mouse over, and >75% visible
48
+ const isDwellAndHoverMetricsEnabled = fg('rovo_chat_embed_card_dwell_and_hover_metrics');
49
+ if (isDwellAndHoverMetricsEnabled) {
50
+ // Note: Removed isWindowFocused requirement as it's unreliable and prevents tracking
51
+ // The mouse over check is sufficient to indicate user engagement
52
+ const shouldTrack = isIframeLoaded && isMouseOver && iframePercentVisible > 0.75;
53
+ if (shouldTrack) {
54
+ if (dwellTimeoutId.current) {
55
+ clearInterval(dwellTimeoutId.current);
56
+ }
57
+ dwellTimeoutId.current = setInterval(incrementDwellTime, 1000);
58
+ } else {
59
+ if (dwellTimeoutId.current) {
60
+ clearInterval(dwellTimeoutId.current);
61
+ dwellTimeoutId.current = undefined;
62
+ }
63
+ }
64
+ } else {
65
+ if (isIframeLoaded && isMouseOver && isWindowFocused && iframePercentVisible > 0.75) {
66
+ if (dwellTimeoutId.current) {
67
+ clearInterval(dwellTimeoutId.current);
68
+ }
69
+ dwellTimeoutId.current = setInterval(incrementDwellTime, 1000);
48
70
  }
49
- dwellTimeoutId.current = setInterval(incrementDwellTime, 1000);
50
71
  }
51
72
  return () => {
52
73
  if (dwellTimeoutId.current) {