@atlaskit/media-viewer 54.8.44 → 54.9.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,46 @@
1
1
  # @atlaskit/media-viewer
2
2
 
3
+ ## 54.9.1
4
+
5
+ ### Patch Changes
6
+
7
+ - Updated dependencies
8
+
9
+ ## 54.9.0
10
+
11
+ ### Minor Changes
12
+
13
+ - [`a432278c5cb5c`](https://bitbucket.org/atlassian/atlassian-frontend-monorepo/commits/a432278c5cb5c) -
14
+ [ux] Add opt-in consumer extension points so a product can host its own sidebar content — such as
15
+ Confluence comments — inside Media Viewer, and restore that viewer state across remounts. All of
16
+ the new behaviour is gated behind the `cc_comments_media_viewer_sidebar` experiment; with the
17
+ experiment off both packages behave exactly as before.
18
+
19
+ `@atlaskit/media-viewer` adds nine optional fields to `MediaViewerExtensions` and exports the new
20
+ `MediaViewerNavigationDirection` type. Consumers can intercept the close and navigate paths with
21
+ `onPreviewClose`, `onSidebarClose` and `onNavigation` — each receives a `proceed()` callback, so a
22
+ consumer can defer or swallow the action while, for example, prompting about an unsaved comment.
23
+ Viewer state can be persisted and restored through `getMediaViewerSelectedItem`,
24
+ `onSelectedItemChange`, `defaultSidebarVisible` and `onSidebarVisibilityChange`, and the existing
25
+ `sidebar` extension accepts `label` and `title`. The previously shipped `sidebar` and
26
+ `headerActions` fields are unchanged and are not gated.
27
+
28
+ `@atlaskit/media-card` adds no new prop of its own, but `Card`/`FileCard` consumers can now pass
29
+ `getMediaViewerSelectedItem` and `onSelectedItemChange` through the existing
30
+ `mediaViewerExtensions` prop to seed the Media Viewer selected item on mount and be notified when
31
+ it opens or closes — new remount-restore behaviour that also requires this release of
32
+ `@atlaskit/media-viewer` to typecheck.
33
+
34
+ Both packages add `@atlaskit/platform-feature-experiments` as a runtime dependency.
35
+
36
+ ### Patch Changes
37
+
38
+ - [`ae39a248bd118`](https://bitbucket.org/atlassian/atlassian-frontend-monorepo/commits/ae39a248bd118) -
39
+ FFCLEANUP-147994 clean up fg relating to fallback media name fetcher
40
+ platform_editor_media_name_fallback, platform_editor_media_file_rename_on_fallback,
41
+ platform_editor_media_file_rename_on_fallback, platform_editor_media_name_fallback_viewer_card
42
+ - Updated dependencies
43
+
3
44
  ## 54.8.44
4
45
 
5
46
  ### Patch Changes
@@ -14,7 +14,7 @@ var _experienceTypes = require("@atlaskit/ufo/experience-types");
14
14
  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; }
15
15
  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) { (0, _defineProperty2.default)(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; }
16
16
  var packageName = "@atlaskit/media-viewer";
17
- var packageVersion = "54.8.43";
17
+ var packageVersion = "54.9.0";
18
18
  var ufoExperience;
19
19
  var getExperience = function getExperience() {
20
20
  if (!ufoExperience) {
@@ -8,7 +8,6 @@ exports.createItemDownloader = void 0;
8
8
  var _regenerator = _interopRequireDefault(require("@babel/runtime/regenerator"));
9
9
  var _asyncToGenerator2 = _interopRequireDefault(require("@babel/runtime/helpers/asyncToGenerator"));
10
10
  var _mediaClient = require("@atlaskit/media-client");
11
- var _expValEquals = require("@atlaskit/tmp-editor-statsig/exp-val-equals");
12
11
  var _createDownloadFailedEventPayload = require("./analytics/events/operational/createDownloadFailedEventPayload");
13
12
  var _createDownloadSucceededEventPayload = require("./analytics/events/operational/createDownloadSucceededEventPayload");
14
13
  var _fireAnalytics = require("./analytics/fireAnalytics");
@@ -22,7 +21,7 @@ var createItemDownloader = exports.createItemDownloader = function createItemDow
22
21
  collectionName = options.collectionName, traceContext = options.traceContext, createAnalyticsEvent = options.createAnalyticsEvent, fallbackMediaName = options.fallbackMediaName;
23
22
  id = file.id;
24
23
  fileStateName = !(0, _mediaClient.isErrorFileState)(file) ? file.name : undefined;
25
- name = fileStateName || ((0, _expValEquals.expValEquals)('platform_editor_media_download_fallback_name', 'isEnabled', true) ? fallbackMediaName : undefined);
24
+ name = fileStateName || fallbackMediaName;
26
25
  mediaClient.file.downloadBinary(id, name, collectionName, traceContext).then(function () {
27
26
  (0, _fireAnalytics.fireAnalytics)((0, _createDownloadSucceededEventPayload.createDownloadSucceededEventPayload)(file, traceContext), createAnalyticsEvent);
28
27
  }).catch(function (e) {
@@ -22,7 +22,6 @@ var _MediaButton = _interopRequireDefault(require("@atlaskit/media-ui/MediaButto
22
22
  var _messages = require("@atlaskit/media-ui/messages");
23
23
  var _mimeTypeIcon = require("@atlaskit/media-ui/mime-type-icon");
24
24
  var _fg = require("@atlaskit/platform-feature-flags/fg");
25
- var _expValEquals = require("@atlaskit/tmp-editor-statsig/exp-val-equals");
26
25
  var _outcome = require("./domain/outcome");
27
26
  var _download = require("./download");
28
27
  var _MediaViewerError = require("./MediaViewerError");
@@ -109,12 +108,12 @@ var Header = exports.Header = function Header(_ref) {
109
108
  (0, _react.useEffect)(function () {
110
109
  // Reset fetch state when the file identity changes (e.g. navigating in viewer)
111
110
  var currentId = (fileState === null || fileState === void 0 ? void 0 : fileState.status) !== 'error' ? fileState === null || fileState === void 0 ? void 0 : fileState.id : undefined;
112
- if (currentId && currentId !== lastFetchedFileId.current && (0, _expValEquals.expValEquals)('platform_editor_media_name_fallback_viewer_card', 'isEnabled', true)) {
111
+ if (currentId && currentId !== lastFetchedFileId.current) {
113
112
  fallbackMediaNameFetchAttempted.current = false;
114
113
  setFallbackMediaName(undefined);
115
114
  lastFetchedFileId.current = currentId;
116
115
  }
117
- if (fileState && fileState.status !== 'error' && !fileState.name && fallbackMediaNameFetcher && !fallbackMediaNameFetchAttempted.current && (0, _expValEquals.expValEquals)('platform_editor_media_name_fallback_viewer_card', 'isEnabled', true)) {
116
+ if (fileState && fileState.status !== 'error' && !fileState.name && fallbackMediaNameFetcher && !fallbackMediaNameFetchAttempted.current) {
118
117
  fallbackMediaNameFetchAttempted.current = true;
119
118
  fallbackMediaNameFetcher(fileState.id).then(function (name) {
120
119
  setFallbackMediaName(name);
@@ -170,7 +169,7 @@ var Header = exports.Header = function Header(_ref) {
170
169
  name: item.name
171
170
  })), /*#__PURE__*/_react.default.createElement(_styleWrappers.MedatadataTextWrapper, null, /*#__PURE__*/_react.default.createElement(_styleWrappers.MetadataFileName, {
172
171
  "data-testid": "media-viewer-file-name"
173
- }, item.name || (0, _expValEquals.expValEquals)('platform_editor_media_name_fallback_viewer_card', 'isEnabled', true) && fallbackMediaName || /*#__PURE__*/_react.default.createElement(_reactIntl.FormattedMessage, _messages.messages.unknown)), /*#__PURE__*/_react.default.createElement(_styleWrappers.MetadataSubText, {
172
+ }, item.name || fallbackMediaName || /*#__PURE__*/_react.default.createElement(_reactIntl.FormattedMessage, _messages.messages.unknown)), /*#__PURE__*/_react.default.createElement(_styleWrappers.MetadataSubText, {
174
173
  "data-testid": "media-viewer-file-metadata-text"
175
174
  }, /*#__PURE__*/_react.default.createElement(_styleWrappers.FormattedMessageWrapper, null, renderFileTypeText(item)), item.size ? ' · ' + (0, _humanReadableSize.toHumanReadableMediaSize)(item.size) : '')));
176
175
  },
package/dist/cjs/list.js CHANGED
@@ -25,6 +25,7 @@ var List = exports.List = function List(_ref) {
25
25
  featureFlags = _ref.featureFlags,
26
26
  isSidebarVisible = _ref.isSidebarVisible,
27
27
  onNavigationChange = _ref.onNavigationChange,
28
+ onNavigationRequest = _ref.onNavigationRequest,
28
29
  items = _ref.items,
29
30
  viewerOptions = _ref.viewerOptions,
30
31
  fallbackMediaNameFetcher = _ref.fallbackMediaNameFetcher;
@@ -71,11 +72,18 @@ var List = exports.List = function List(_ref) {
71
72
  }), /*#__PURE__*/_react.default.createElement(_navigation.Navigation, {
72
73
  items: items,
73
74
  selectedItem: selectedItem,
74
- onChange: function onChange(selectedItem) {
75
- onNavigationChange === null || onNavigationChange === void 0 || onNavigationChange(selectedItem);
76
- showControls === null || showControls === void 0 || showControls();
77
- setSelectedItem(selectedItem);
78
- setPreviewCount(previewCount + 1);
75
+ onChange: function onChange(nextSelectedItem) {
76
+ var commit = function commit() {
77
+ onNavigationChange === null || onNavigationChange === void 0 || onNavigationChange(nextSelectedItem);
78
+ showControls === null || showControls === void 0 || showControls();
79
+ setSelectedItem(nextSelectedItem);
80
+ setPreviewCount(previewCount + 1);
81
+ };
82
+ if (onNavigationRequest) {
83
+ onNavigationRequest(nextSelectedItem, commit);
84
+ } else {
85
+ commit();
86
+ }
79
87
  },
80
88
  isArchiveSideBarVisible: isArchiveSideBarVisible
81
89
  }));
@@ -14,7 +14,9 @@ var _reactFocusLock = _interopRequireDefault(require("react-focus-lock"));
14
14
  var _reactIntl = require("react-intl");
15
15
  var _reactScrolllock = _interopRequireDefault(require("react-scrolllock"));
16
16
  var _useAnalyticsEvents2 = require("@atlaskit/analytics-next/useAnalyticsEvents");
17
+ var _mediaClient = require("@atlaskit/media-client");
17
18
  var _shortcut = require("@atlaskit/media-ui/shortcut");
19
+ var _isExperimentEnabled = require("@atlaskit/platform-feature-experiments/is-experiment-enabled");
18
20
  var _modal = require("./analytics/events/screen/modal");
19
21
  var _closed = require("./analytics/events/ui/closed");
20
22
  var _fireAnalytics = require("./analytics/fireAnalytics");
@@ -23,21 +25,39 @@ var _content = require("./content");
23
25
  var _list = require("./list");
24
26
  var _styleWrappers = require("./styleWrappers");
25
27
  function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function _interopRequireWildcard(e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, default: e }; if (null === e || "object" != _typeof(e) && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (var _t in e) "default" !== _t && {}.hasOwnProperty.call(e, _t) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, _t)) && (i.get || i.set) ? o(f, _t, i) : f[_t] = e[_t]); return f; })(e, t); }
28
+ var getIdentifierId = function getIdentifierId(identifier) {
29
+ return (0, _mediaClient.isFileIdentifier)(identifier) ? identifier.id : undefined;
30
+ };
26
31
  var MediaViewerComponent = function MediaViewerComponent(_ref) {
27
32
  var featureFlags = _ref.featureFlags,
28
33
  items = _ref.items,
29
34
  extensions = _ref.extensions,
30
35
  contextId = _ref.contextId,
31
36
  innerRef = _ref.innerRef,
32
- _onClose = _ref.onClose,
37
+ onClose = _ref.onClose,
33
38
  selectedItem = _ref.selectedItem,
34
39
  intl = _ref.intl,
35
40
  viewerOptions = _ref.viewerOptions,
36
41
  fallbackMediaNameFetcher = _ref.fallbackMediaNameFetcher;
37
- var _useState = (0, _react.useState)(false),
42
+ // Experiment gate for the Confluence comments-in-media-viewer work. Only the
43
+ // fields introduced by that series are gated here — `extensions.sidebar` and
44
+ // `extensions.headerActions` shipped earlier and must keep working with the
45
+ // experiment off, so `extensions` itself is deliberately *not* nullified.
46
+ var _ref2 = (0, _isExperimentEnabled.isExperimentEnabled)('cc_comments_media_viewer_sidebar') ? extensions !== null && extensions !== void 0 ? extensions : {} : {},
47
+ onPreviewClose = _ref2.onPreviewClose,
48
+ onSidebarClose = _ref2.onSidebarClose,
49
+ onNavigation = _ref2.onNavigation,
50
+ onSelectedItemChange = _ref2.onSelectedItemChange,
51
+ defaultSidebarVisible = _ref2.defaultSidebarVisible,
52
+ onSidebarVisibilityChange = _ref2.onSidebarVisibilityChange;
53
+ var _useState = (0, _react.useState)(defaultSidebarVisible !== null && defaultSidebarVisible !== void 0 ? defaultSidebarVisible : false),
38
54
  _useState2 = (0, _slicedToArray2.default)(_useState, 2),
39
55
  isSidebarVisible = _useState2[0],
40
- setIsSidebarVisible = _useState2[1];
56
+ setIsSidebarVisibleState = _useState2[1];
57
+ var setIsSidebarVisible = (0, _react.useCallback)(function (isVisible) {
58
+ setIsSidebarVisibleState(isVisible);
59
+ onSidebarVisibilityChange === null || onSidebarVisibilityChange === void 0 || onSidebarVisibilityChange(isVisible);
60
+ }, [onSidebarVisibilityChange]);
41
61
  var _useState3 = (0, _react.useState)(),
42
62
  _useState4 = (0, _slicedToArray2.default)(_useState3, 2),
43
63
  selectedIdentifier = _useState4[0],
@@ -46,19 +66,95 @@ var MediaViewerComponent = function MediaViewerComponent(_ref) {
46
66
  createAnalyticsEvent = _useAnalyticsEvents.createAnalyticsEvent;
47
67
  var createAnalyticsEventRef = (0, _react.useRef)(createAnalyticsEvent);
48
68
  createAnalyticsEventRef.current = createAnalyticsEvent;
69
+
70
+ // Delegation helpers: if the consumer provides an interceptor, call it and
71
+ // let the consumer decide when (or whether) to invoke `proceed`. Otherwise
72
+ // proceed immediately so behavior is unchanged for consumers that don't
73
+ // supply the new callbacks.
74
+ var requestPreviewClose = (0, _react.useCallback)(function (proceed) {
75
+ if (onPreviewClose) {
76
+ onPreviewClose(proceed);
77
+ } else {
78
+ proceed();
79
+ }
80
+ }, [onPreviewClose]);
81
+ var requestSidebarClose = (0, _react.useCallback)(function (proceed) {
82
+ if (onSidebarClose) {
83
+ onSidebarClose(proceed);
84
+ } else {
85
+ proceed();
86
+ }
87
+ }, [onSidebarClose]);
88
+ var requestNavigation = (0, _react.useCallback)(function (direction, proceed) {
89
+ if (onNavigation) {
90
+ onNavigation(direction, proceed);
91
+ } else {
92
+ proceed();
93
+ }
94
+ }, [onNavigation]);
49
95
  (0, _react.useLayoutEffect)(function () {
50
96
  (0, _fireAnalytics.fireAnalytics)((0, _modal.createModalEvent)(), createAnalyticsEventRef.current);
51
97
  (0, _perfMarks.start)('MediaViewer.SessionDuration');
52
98
  }, []);
53
99
  var defaultSelectedItem = selectedItem || items[0];
100
+
101
+ // Toggling the sidebar: opening is unconditional; closing goes through the
102
+ // optional onSidebarClose interceptor.
103
+ var toggleSidebar = (0, _react.useCallback)(function () {
104
+ if (isSidebarVisible) {
105
+ requestSidebarClose(function () {
106
+ return setIsSidebarVisible(false);
107
+ });
108
+ } else {
109
+ setIsSidebarVisible(true);
110
+ }
111
+ }, [isSidebarVisible, requestSidebarClose, setIsSidebarVisible]);
112
+
113
+ // The full modal close — used by ESC, the close button, and click-outside.
114
+ // Always passed through requestPreviewClose so the consumer can intercept.
115
+ var handlePreviewClose = (0, _react.useCallback)(function (reason) {
116
+ requestPreviewClose(function () {
117
+ if (reason !== 'other') {
118
+ (0, _fireAnalytics.fireAnalytics)((0, _closed.createClosedEvent)(reason), createAnalyticsEventRef.current);
119
+ }
120
+ onClose && onClose();
121
+ });
122
+ }, [onClose, requestPreviewClose]);
123
+
124
+ // Compute the navigation direction by comparing the new identifier's index
125
+ // in `items` with the current selected identifier's index. Falls back to
126
+ // 'next' if we can't determine direction (e.g. random jump from a list).
127
+ var computeNavigationDirection = (0, _react.useCallback)(function (nextIdentifier) {
128
+ var current = selectedIdentifier || defaultSelectedItem;
129
+ var currentId = current && getIdentifierId(current);
130
+ var nextId = getIdentifierId(nextIdentifier);
131
+ if (!currentId || !nextId) {
132
+ return 'next';
133
+ }
134
+ var currentIndex = items.findIndex(function (it) {
135
+ return getIdentifierId(it) === currentId;
136
+ });
137
+ var nextIndex = items.findIndex(function (it) {
138
+ return getIdentifierId(it) === nextId;
139
+ });
140
+ if (currentIndex === -1 || nextIndex === -1) {
141
+ return 'next';
142
+ }
143
+ return nextIndex < currentIndex ? 'prev' : 'next';
144
+ }, [items, selectedIdentifier, defaultSelectedItem]);
54
145
  var renderSidebar = function renderSidebar() {
55
146
  var sidebarSelectedIdentifier = selectedIdentifier || defaultSelectedItem;
56
- if (sidebarSelectedIdentifier && isSidebarVisible && extensions && extensions.sidebar) {
147
+ if (sidebarSelectedIdentifier && isSidebarVisible && extensions !== null && extensions !== void 0 && extensions.sidebar) {
57
148
  return /*#__PURE__*/_react.default.createElement(_styleWrappers.SidebarWrapper, {
58
149
  "data-testid": "media-viewer-sidebar-content"
59
150
  }, extensions.sidebar.renderer(sidebarSelectedIdentifier, {
151
+ // `close` is what the sidebar renderer calls to dismiss itself.
152
+ // Route it through the same interceptor so confluence's
153
+ // unsaved-comment guard runs here too.
60
154
  close: function close() {
61
- return setIsSidebarVisible(!isSidebarVisible);
155
+ return requestSidebarClose(function () {
156
+ return setIsSidebarVisible(false);
157
+ });
62
158
  }
63
159
  }));
64
160
  }
@@ -73,27 +169,42 @@ var MediaViewerComponent = function MediaViewerComponent(_ref) {
73
169
  }, /*#__PURE__*/_react.default.createElement(_shortcut.Shortcut, {
74
170
  code: 'Escape',
75
171
  handler: function handler() {
76
- (0, _fireAnalytics.fireAnalytics)((0, _closed.createClosedEvent)('escKey'), createAnalyticsEventRef.current);
77
- _onClose && _onClose();
172
+ handlePreviewClose('escKey');
78
173
  }
79
174
  }), /*#__PURE__*/_react.default.createElement(_content.Content, {
80
175
  isSidebarVisible: isSidebarVisible,
81
176
  onClose: function onClose(_e, analyticsEvent) {
82
177
  var _analyticsEvent$paylo;
83
- if ((analyticsEvent === null || analyticsEvent === void 0 || (_analyticsEvent$paylo = analyticsEvent.payload) === null || _analyticsEvent$paylo === void 0 ? void 0 : _analyticsEvent$paylo.actionSubject) === 'button') {
84
- (0, _fireAnalytics.fireAnalytics)((0, _closed.createClosedEvent)('button'), createAnalyticsEventRef.current);
85
- }
86
- _onClose && _onClose();
178
+ var reason = (analyticsEvent === null || analyticsEvent === void 0 || (_analyticsEvent$paylo = analyticsEvent.payload) === null || _analyticsEvent$paylo === void 0 ? void 0 : _analyticsEvent$paylo.actionSubject) === 'button' ? 'button' : 'other';
179
+ handlePreviewClose(reason);
87
180
  }
88
181
  }, /*#__PURE__*/_react.default.createElement(_list.List, {
89
182
  defaultSelectedItem: defaultSelectedItem || items[0],
90
- items: items,
91
- onClose: _onClose,
92
- extensions: extensions,
93
- onNavigationChange: setSelectedIdentifier,
94
- onSidebarButtonClick: function onSidebarButtonClick() {
95
- return setIsSidebarVisible(!isSidebarVisible);
183
+ items: items
184
+ // Note: `onClose` here is what the prev/next list passes down for
185
+ // "close-the-modal" actions. Route through interceptor too.
186
+ ,
187
+ onClose: function onClose() {
188
+ return handlePreviewClose('other');
189
+ },
190
+ extensions: extensions
191
+ // Notification-only: List has already updated its displayed item
192
+ // by the time this fires. We use it to keep the parent's
193
+ // `selectedIdentifier` in sync (consumers may key off it).
194
+ ,
195
+ onNavigationChange: function onNavigationChange(identifier) {
196
+ setSelectedIdentifier(identifier);
197
+ onSelectedItemChange === null || onSelectedItemChange === void 0 || onSelectedItemChange(identifier);
198
+ }
199
+ // Pre-commit gate: consumer can swallow or defer the
200
+ // nav by not calling `commit`. The image only swaps
201
+ // once `commit` runs.
202
+ ,
203
+ onNavigationRequest: function onNavigationRequest(identifier, commit) {
204
+ var direction = computeNavigationDirection(identifier);
205
+ requestNavigation(direction, commit);
96
206
  },
207
+ onSidebarButtonClick: toggleSidebar,
97
208
  isSidebarVisible: isSidebarVisible,
98
209
  contextId: contextId,
99
210
  featureFlags: featureFlags,
@@ -4,7 +4,7 @@ import { fg } from '@atlaskit/platform-feature-flags/fg';
4
4
  import { UFOExperience } from '@atlaskit/ufo/experience';
5
5
  import { ExperiencePerformanceTypes, ExperienceTypes } from '@atlaskit/ufo/experience-types';
6
6
  const packageName = "@atlaskit/media-viewer";
7
- const packageVersion = "54.8.43";
7
+ const packageVersion = "54.9.0";
8
8
  let ufoExperience;
9
9
  const getExperience = () => {
10
10
  if (!ufoExperience) {
@@ -1,5 +1,4 @@
1
1
  import { isErrorFileState } from '@atlaskit/media-client';
2
- import { expValEquals } from '@atlaskit/tmp-editor-statsig/exp-val-equals';
3
2
  import { createDownloadFailedEventPayload } from './analytics/events/operational/createDownloadFailedEventPayload';
4
3
  import { createDownloadSucceededEventPayload } from './analytics/events/operational/createDownloadSucceededEventPayload';
5
4
  import { fireAnalytics } from './analytics/fireAnalytics';
@@ -13,7 +12,7 @@ export const createItemDownloader = (file, mediaClient, options) => async () =>
13
12
  } = options;
14
13
  const id = file.id;
15
14
  const fileStateName = !isErrorFileState(file) ? file.name : undefined;
16
- const name = fileStateName || (expValEquals('platform_editor_media_download_fallback_name', 'isEnabled', true) ? fallbackMediaName : undefined);
15
+ const name = fileStateName || fallbackMediaName;
17
16
  mediaClient.file.downloadBinary(id, name, collectionName, traceContext).then(() => {
18
17
  fireAnalytics(createDownloadSucceededEventPayload(file, traceContext), createAnalyticsEvent);
19
18
  }).catch(e => {
@@ -13,7 +13,6 @@ import MediaButton from '@atlaskit/media-ui/MediaButton';
13
13
  import { messages } from '@atlaskit/media-ui/messages';
14
14
  import { MimeTypeIcon } from '@atlaskit/media-ui/mime-type-icon';
15
15
  import { fg } from '@atlaskit/platform-feature-flags/fg';
16
- import { expValEquals } from '@atlaskit/tmp-editor-statsig/exp-val-equals';
17
16
  import { Outcome } from './domain/outcome';
18
17
  import { DisabledToolbarDownloadButton } from './download';
19
18
  import { MediaViewerError } from './MediaViewerError';
@@ -96,12 +95,12 @@ export const Header = ({
96
95
  useEffect(() => {
97
96
  // Reset fetch state when the file identity changes (e.g. navigating in viewer)
98
97
  const currentId = (fileState === null || fileState === void 0 ? void 0 : fileState.status) !== 'error' ? fileState === null || fileState === void 0 ? void 0 : fileState.id : undefined;
99
- if (currentId && currentId !== lastFetchedFileId.current && expValEquals('platform_editor_media_name_fallback_viewer_card', 'isEnabled', true)) {
98
+ if (currentId && currentId !== lastFetchedFileId.current) {
100
99
  fallbackMediaNameFetchAttempted.current = false;
101
100
  setFallbackMediaName(undefined);
102
101
  lastFetchedFileId.current = currentId;
103
102
  }
104
- if (fileState && fileState.status !== 'error' && !fileState.name && fallbackMediaNameFetcher && !fallbackMediaNameFetchAttempted.current && expValEquals('platform_editor_media_name_fallback_viewer_card', 'isEnabled', true)) {
103
+ if (fileState && fileState.status !== 'error' && !fileState.name && fallbackMediaNameFetcher && !fallbackMediaNameFetchAttempted.current) {
105
104
  fallbackMediaNameFetchAttempted.current = true;
106
105
  fallbackMediaNameFetcher(fileState.id).then(name => {
107
106
  setFallbackMediaName(name);
@@ -158,7 +157,7 @@ export const Header = ({
158
157
  name: item.name
159
158
  })), /*#__PURE__*/React.createElement(MedatadataTextWrapper, null, /*#__PURE__*/React.createElement(MetadataFileName, {
160
159
  "data-testid": "media-viewer-file-name"
161
- }, item.name || expValEquals('platform_editor_media_name_fallback_viewer_card', 'isEnabled', true) && fallbackMediaName || /*#__PURE__*/React.createElement(FormattedMessage, messages.unknown)), /*#__PURE__*/React.createElement(MetadataSubText, {
160
+ }, item.name || fallbackMediaName || /*#__PURE__*/React.createElement(FormattedMessage, messages.unknown)), /*#__PURE__*/React.createElement(MetadataSubText, {
162
161
  "data-testid": "media-viewer-file-metadata-text"
163
162
  }, /*#__PURE__*/React.createElement(FormattedMessageWrapper, null, renderFileTypeText(item)), item.size ? ' · ' + toHumanReadableMediaSize(item.size) : ''))),
164
163
  pending: () => null,
@@ -15,6 +15,7 @@ export const List = ({
15
15
  featureFlags,
16
16
  isSidebarVisible,
17
17
  onNavigationChange,
18
+ onNavigationRequest,
18
19
  items,
19
20
  viewerOptions,
20
21
  fallbackMediaNameFetcher
@@ -53,11 +54,18 @@ export const List = ({
53
54
  }), /*#__PURE__*/React.createElement(Navigation, {
54
55
  items: items,
55
56
  selectedItem: selectedItem,
56
- onChange: selectedItem => {
57
- onNavigationChange === null || onNavigationChange === void 0 ? void 0 : onNavigationChange(selectedItem);
58
- showControls === null || showControls === void 0 ? void 0 : showControls();
59
- setSelectedItem(selectedItem);
60
- setPreviewCount(previewCount + 1);
57
+ onChange: nextSelectedItem => {
58
+ const commit = () => {
59
+ onNavigationChange === null || onNavigationChange === void 0 ? void 0 : onNavigationChange(nextSelectedItem);
60
+ showControls === null || showControls === void 0 ? void 0 : showControls();
61
+ setSelectedItem(nextSelectedItem);
62
+ setPreviewCount(previewCount + 1);
63
+ };
64
+ if (onNavigationRequest) {
65
+ onNavigationRequest(nextSelectedItem, commit);
66
+ } else {
67
+ commit();
68
+ }
61
69
  },
62
70
  isArchiveSideBarVisible: isArchiveSideBarVisible
63
71
  }));
@@ -1,11 +1,13 @@
1
1
  import _extends from "@babel/runtime/helpers/extends";
2
- import React, { useState, useLayoutEffect, useRef } from 'react';
2
+ import React, { useState, useLayoutEffect, useRef, useCallback } from 'react';
3
3
  import { start } from 'perf-marks';
4
4
  import FocusLock from 'react-focus-lock';
5
5
  import { IntlProvider, injectIntl } from 'react-intl';
6
6
  import ScrollLock from 'react-scrolllock';
7
7
  import { useAnalyticsEvents } from '@atlaskit/analytics-next/useAnalyticsEvents';
8
+ import { isFileIdentifier } from '@atlaskit/media-client';
8
9
  import { Shortcut } from '@atlaskit/media-ui/shortcut';
10
+ import { isExperimentEnabled } from '@atlaskit/platform-feature-experiments/is-experiment-enabled';
9
11
  import { createModalEvent } from './analytics/events/screen/modal';
10
12
  import { createClosedEvent } from './analytics/events/ui/closed';
11
13
  import { fireAnalytics } from './analytics/fireAnalytics';
@@ -13,6 +15,7 @@ import { mediaViewerPopupClass } from './classnames';
13
15
  import { Content } from './content';
14
16
  import { List } from './list';
15
17
  import { Blanket, SidebarWrapper } from './styleWrappers';
18
+ const getIdentifierId = identifier => isFileIdentifier(identifier) ? identifier.id : undefined;
16
19
  const MediaViewerComponent = ({
17
20
  featureFlags,
18
21
  items,
@@ -25,25 +28,109 @@ const MediaViewerComponent = ({
25
28
  viewerOptions,
26
29
  fallbackMediaNameFetcher
27
30
  }) => {
28
- const [isSidebarVisible, setIsSidebarVisible] = useState(false);
31
+ // Experiment gate for the Confluence comments-in-media-viewer work. Only the
32
+ // fields introduced by that series are gated here — `extensions.sidebar` and
33
+ // `extensions.headerActions` shipped earlier and must keep working with the
34
+ // experiment off, so `extensions` itself is deliberately *not* nullified.
35
+ const {
36
+ onPreviewClose,
37
+ onSidebarClose,
38
+ onNavigation,
39
+ onSelectedItemChange,
40
+ defaultSidebarVisible,
41
+ onSidebarVisibilityChange
42
+ } = isExperimentEnabled('cc_comments_media_viewer_sidebar') ? extensions !== null && extensions !== void 0 ? extensions : {} : {};
43
+ const [isSidebarVisible, setIsSidebarVisibleState] = useState(defaultSidebarVisible !== null && defaultSidebarVisible !== void 0 ? defaultSidebarVisible : false);
44
+ const setIsSidebarVisible = useCallback(isVisible => {
45
+ setIsSidebarVisibleState(isVisible);
46
+ onSidebarVisibilityChange === null || onSidebarVisibilityChange === void 0 ? void 0 : onSidebarVisibilityChange(isVisible);
47
+ }, [onSidebarVisibilityChange]);
29
48
  const [selectedIdentifier, setSelectedIdentifier] = useState();
30
49
  const {
31
50
  createAnalyticsEvent
32
51
  } = useAnalyticsEvents();
33
52
  const createAnalyticsEventRef = useRef(createAnalyticsEvent);
34
53
  createAnalyticsEventRef.current = createAnalyticsEvent;
54
+
55
+ // Delegation helpers: if the consumer provides an interceptor, call it and
56
+ // let the consumer decide when (or whether) to invoke `proceed`. Otherwise
57
+ // proceed immediately so behavior is unchanged for consumers that don't
58
+ // supply the new callbacks.
59
+ const requestPreviewClose = useCallback(proceed => {
60
+ if (onPreviewClose) {
61
+ onPreviewClose(proceed);
62
+ } else {
63
+ proceed();
64
+ }
65
+ }, [onPreviewClose]);
66
+ const requestSidebarClose = useCallback(proceed => {
67
+ if (onSidebarClose) {
68
+ onSidebarClose(proceed);
69
+ } else {
70
+ proceed();
71
+ }
72
+ }, [onSidebarClose]);
73
+ const requestNavigation = useCallback((direction, proceed) => {
74
+ if (onNavigation) {
75
+ onNavigation(direction, proceed);
76
+ } else {
77
+ proceed();
78
+ }
79
+ }, [onNavigation]);
35
80
  useLayoutEffect(() => {
36
81
  fireAnalytics(createModalEvent(), createAnalyticsEventRef.current);
37
82
  start('MediaViewer.SessionDuration');
38
83
  }, []);
39
84
  const defaultSelectedItem = selectedItem || items[0];
85
+
86
+ // Toggling the sidebar: opening is unconditional; closing goes through the
87
+ // optional onSidebarClose interceptor.
88
+ const toggleSidebar = useCallback(() => {
89
+ if (isSidebarVisible) {
90
+ requestSidebarClose(() => setIsSidebarVisible(false));
91
+ } else {
92
+ setIsSidebarVisible(true);
93
+ }
94
+ }, [isSidebarVisible, requestSidebarClose, setIsSidebarVisible]);
95
+
96
+ // The full modal close — used by ESC, the close button, and click-outside.
97
+ // Always passed through requestPreviewClose so the consumer can intercept.
98
+ const handlePreviewClose = useCallback(reason => {
99
+ requestPreviewClose(() => {
100
+ if (reason !== 'other') {
101
+ fireAnalytics(createClosedEvent(reason), createAnalyticsEventRef.current);
102
+ }
103
+ onClose && onClose();
104
+ });
105
+ }, [onClose, requestPreviewClose]);
106
+
107
+ // Compute the navigation direction by comparing the new identifier's index
108
+ // in `items` with the current selected identifier's index. Falls back to
109
+ // 'next' if we can't determine direction (e.g. random jump from a list).
110
+ const computeNavigationDirection = useCallback(nextIdentifier => {
111
+ const current = selectedIdentifier || defaultSelectedItem;
112
+ const currentId = current && getIdentifierId(current);
113
+ const nextId = getIdentifierId(nextIdentifier);
114
+ if (!currentId || !nextId) {
115
+ return 'next';
116
+ }
117
+ const currentIndex = items.findIndex(it => getIdentifierId(it) === currentId);
118
+ const nextIndex = items.findIndex(it => getIdentifierId(it) === nextId);
119
+ if (currentIndex === -1 || nextIndex === -1) {
120
+ return 'next';
121
+ }
122
+ return nextIndex < currentIndex ? 'prev' : 'next';
123
+ }, [items, selectedIdentifier, defaultSelectedItem]);
40
124
  const renderSidebar = () => {
41
125
  const sidebarSelectedIdentifier = selectedIdentifier || defaultSelectedItem;
42
- if (sidebarSelectedIdentifier && isSidebarVisible && extensions && extensions.sidebar) {
126
+ if (sidebarSelectedIdentifier && isSidebarVisible && extensions !== null && extensions !== void 0 && extensions.sidebar) {
43
127
  return /*#__PURE__*/React.createElement(SidebarWrapper, {
44
128
  "data-testid": "media-viewer-sidebar-content"
45
129
  }, extensions.sidebar.renderer(sidebarSelectedIdentifier, {
46
- close: () => setIsSidebarVisible(!isSidebarVisible)
130
+ // `close` is what the sidebar renderer calls to dismiss itself.
131
+ // Route it through the same interceptor so confluence's
132
+ // unsaved-comment guard runs here too.
133
+ close: () => requestSidebarClose(() => setIsSidebarVisible(false))
47
134
  }));
48
135
  }
49
136
  };
@@ -57,25 +144,40 @@ const MediaViewerComponent = ({
57
144
  }, /*#__PURE__*/React.createElement(Shortcut, {
58
145
  code: 'Escape',
59
146
  handler: () => {
60
- fireAnalytics(createClosedEvent('escKey'), createAnalyticsEventRef.current);
61
- onClose && onClose();
147
+ handlePreviewClose('escKey');
62
148
  }
63
149
  }), /*#__PURE__*/React.createElement(Content, {
64
150
  isSidebarVisible: isSidebarVisible,
65
151
  onClose: (_e, analyticsEvent) => {
66
152
  var _analyticsEvent$paylo;
67
- if ((analyticsEvent === null || analyticsEvent === void 0 ? void 0 : (_analyticsEvent$paylo = analyticsEvent.payload) === null || _analyticsEvent$paylo === void 0 ? void 0 : _analyticsEvent$paylo.actionSubject) === 'button') {
68
- fireAnalytics(createClosedEvent('button'), createAnalyticsEventRef.current);
69
- }
70
- onClose && onClose();
153
+ const reason = (analyticsEvent === null || analyticsEvent === void 0 ? void 0 : (_analyticsEvent$paylo = analyticsEvent.payload) === null || _analyticsEvent$paylo === void 0 ? void 0 : _analyticsEvent$paylo.actionSubject) === 'button' ? 'button' : 'other';
154
+ handlePreviewClose(reason);
71
155
  }
72
156
  }, /*#__PURE__*/React.createElement(List, {
73
157
  defaultSelectedItem: defaultSelectedItem || items[0],
74
- items: items,
75
- onClose: onClose,
76
- extensions: extensions,
77
- onNavigationChange: setSelectedIdentifier,
78
- onSidebarButtonClick: () => setIsSidebarVisible(!isSidebarVisible),
158
+ items: items
159
+ // Note: `onClose` here is what the prev/next list passes down for
160
+ // "close-the-modal" actions. Route through interceptor too.
161
+ ,
162
+ onClose: () => handlePreviewClose('other'),
163
+ extensions: extensions
164
+ // Notification-only: List has already updated its displayed item
165
+ // by the time this fires. We use it to keep the parent's
166
+ // `selectedIdentifier` in sync (consumers may key off it).
167
+ ,
168
+ onNavigationChange: identifier => {
169
+ setSelectedIdentifier(identifier);
170
+ onSelectedItemChange === null || onSelectedItemChange === void 0 ? void 0 : onSelectedItemChange(identifier);
171
+ }
172
+ // Pre-commit gate: consumer can swallow or defer the
173
+ // nav by not calling `commit`. The image only swaps
174
+ // once `commit` runs.
175
+ ,
176
+ onNavigationRequest: (identifier, commit) => {
177
+ const direction = computeNavigationDirection(identifier);
178
+ requestNavigation(direction, commit);
179
+ },
180
+ onSidebarButtonClick: toggleSidebar,
79
181
  isSidebarVisible: isSidebarVisible,
80
182
  contextId: contextId,
81
183
  featureFlags: featureFlags,
@@ -7,7 +7,7 @@ import { fg } from '@atlaskit/platform-feature-flags/fg';
7
7
  import { UFOExperience } from '@atlaskit/ufo/experience';
8
8
  import { ExperiencePerformanceTypes, ExperienceTypes } from '@atlaskit/ufo/experience-types';
9
9
  var packageName = "@atlaskit/media-viewer";
10
- var packageVersion = "54.8.43";
10
+ var packageVersion = "54.9.0";
11
11
  var ufoExperience;
12
12
  var getExperience = function getExperience() {
13
13
  if (!ufoExperience) {
@@ -1,7 +1,6 @@
1
1
  import _asyncToGenerator from "@babel/runtime/helpers/asyncToGenerator";
2
2
  import _regeneratorRuntime from "@babel/runtime/regenerator";
3
3
  import { isErrorFileState } from '@atlaskit/media-client';
4
- import { expValEquals } from '@atlaskit/tmp-editor-statsig/exp-val-equals';
5
4
  import { createDownloadFailedEventPayload } from './analytics/events/operational/createDownloadFailedEventPayload';
6
5
  import { createDownloadSucceededEventPayload } from './analytics/events/operational/createDownloadSucceededEventPayload';
7
6
  import { fireAnalytics } from './analytics/fireAnalytics';
@@ -15,7 +14,7 @@ export var createItemDownloader = function createItemDownloader(file, mediaClien
15
14
  collectionName = options.collectionName, traceContext = options.traceContext, createAnalyticsEvent = options.createAnalyticsEvent, fallbackMediaName = options.fallbackMediaName;
16
15
  id = file.id;
17
16
  fileStateName = !isErrorFileState(file) ? file.name : undefined;
18
- name = fileStateName || (expValEquals('platform_editor_media_download_fallback_name', 'isEnabled', true) ? fallbackMediaName : undefined);
17
+ name = fileStateName || fallbackMediaName;
19
18
  mediaClient.file.downloadBinary(id, name, collectionName, traceContext).then(function () {
20
19
  fireAnalytics(createDownloadSucceededEventPayload(file, traceContext), createAnalyticsEvent);
21
20
  }).catch(function (e) {
@@ -14,7 +14,6 @@ import MediaButton from '@atlaskit/media-ui/MediaButton';
14
14
  import { messages } from '@atlaskit/media-ui/messages';
15
15
  import { MimeTypeIcon } from '@atlaskit/media-ui/mime-type-icon';
16
16
  import { fg } from '@atlaskit/platform-feature-flags/fg';
17
- import { expValEquals } from '@atlaskit/tmp-editor-statsig/exp-val-equals';
18
17
  import { Outcome } from './domain/outcome';
19
18
  import { DisabledToolbarDownloadButton } from './download';
20
19
  import { MediaViewerError } from './MediaViewerError';
@@ -100,12 +99,12 @@ export var Header = function Header(_ref) {
100
99
  useEffect(function () {
101
100
  // Reset fetch state when the file identity changes (e.g. navigating in viewer)
102
101
  var currentId = (fileState === null || fileState === void 0 ? void 0 : fileState.status) !== 'error' ? fileState === null || fileState === void 0 ? void 0 : fileState.id : undefined;
103
- if (currentId && currentId !== lastFetchedFileId.current && expValEquals('platform_editor_media_name_fallback_viewer_card', 'isEnabled', true)) {
102
+ if (currentId && currentId !== lastFetchedFileId.current) {
104
103
  fallbackMediaNameFetchAttempted.current = false;
105
104
  setFallbackMediaName(undefined);
106
105
  lastFetchedFileId.current = currentId;
107
106
  }
108
- if (fileState && fileState.status !== 'error' && !fileState.name && fallbackMediaNameFetcher && !fallbackMediaNameFetchAttempted.current && expValEquals('platform_editor_media_name_fallback_viewer_card', 'isEnabled', true)) {
107
+ if (fileState && fileState.status !== 'error' && !fileState.name && fallbackMediaNameFetcher && !fallbackMediaNameFetchAttempted.current) {
109
108
  fallbackMediaNameFetchAttempted.current = true;
110
109
  fallbackMediaNameFetcher(fileState.id).then(function (name) {
111
110
  setFallbackMediaName(name);
@@ -161,7 +160,7 @@ export var Header = function Header(_ref) {
161
160
  name: item.name
162
161
  })), /*#__PURE__*/React.createElement(MedatadataTextWrapper, null, /*#__PURE__*/React.createElement(MetadataFileName, {
163
162
  "data-testid": "media-viewer-file-name"
164
- }, item.name || expValEquals('platform_editor_media_name_fallback_viewer_card', 'isEnabled', true) && fallbackMediaName || /*#__PURE__*/React.createElement(FormattedMessage, messages.unknown)), /*#__PURE__*/React.createElement(MetadataSubText, {
163
+ }, item.name || fallbackMediaName || /*#__PURE__*/React.createElement(FormattedMessage, messages.unknown)), /*#__PURE__*/React.createElement(MetadataSubText, {
165
164
  "data-testid": "media-viewer-file-metadata-text"
166
165
  }, /*#__PURE__*/React.createElement(FormattedMessageWrapper, null, renderFileTypeText(item)), item.size ? ' · ' + toHumanReadableMediaSize(item.size) : '')));
167
166
  },
package/dist/esm/list.js CHANGED
@@ -16,6 +16,7 @@ export var List = function List(_ref) {
16
16
  featureFlags = _ref.featureFlags,
17
17
  isSidebarVisible = _ref.isSidebarVisible,
18
18
  onNavigationChange = _ref.onNavigationChange,
19
+ onNavigationRequest = _ref.onNavigationRequest,
19
20
  items = _ref.items,
20
21
  viewerOptions = _ref.viewerOptions,
21
22
  fallbackMediaNameFetcher = _ref.fallbackMediaNameFetcher;
@@ -62,11 +63,18 @@ export var List = function List(_ref) {
62
63
  }), /*#__PURE__*/React.createElement(Navigation, {
63
64
  items: items,
64
65
  selectedItem: selectedItem,
65
- onChange: function onChange(selectedItem) {
66
- onNavigationChange === null || onNavigationChange === void 0 || onNavigationChange(selectedItem);
67
- showControls === null || showControls === void 0 || showControls();
68
- setSelectedItem(selectedItem);
69
- setPreviewCount(previewCount + 1);
66
+ onChange: function onChange(nextSelectedItem) {
67
+ var commit = function commit() {
68
+ onNavigationChange === null || onNavigationChange === void 0 || onNavigationChange(nextSelectedItem);
69
+ showControls === null || showControls === void 0 || showControls();
70
+ setSelectedItem(nextSelectedItem);
71
+ setPreviewCount(previewCount + 1);
72
+ };
73
+ if (onNavigationRequest) {
74
+ onNavigationRequest(nextSelectedItem, commit);
75
+ } else {
76
+ commit();
77
+ }
70
78
  },
71
79
  isArchiveSideBarVisible: isArchiveSideBarVisible
72
80
  }));
@@ -1,12 +1,14 @@
1
1
  import _extends from "@babel/runtime/helpers/extends";
2
2
  import _slicedToArray from "@babel/runtime/helpers/slicedToArray";
3
- import React, { useState, useLayoutEffect, useRef } from 'react';
3
+ import React, { useState, useLayoutEffect, useRef, useCallback } from 'react';
4
4
  import { start } from 'perf-marks';
5
5
  import FocusLock from 'react-focus-lock';
6
6
  import { IntlProvider, injectIntl } from 'react-intl';
7
7
  import ScrollLock from 'react-scrolllock';
8
8
  import { useAnalyticsEvents } from '@atlaskit/analytics-next/useAnalyticsEvents';
9
+ import { isFileIdentifier } from '@atlaskit/media-client';
9
10
  import { Shortcut } from '@atlaskit/media-ui/shortcut';
11
+ import { isExperimentEnabled } from '@atlaskit/platform-feature-experiments/is-experiment-enabled';
10
12
  import { createModalEvent } from './analytics/events/screen/modal';
11
13
  import { createClosedEvent } from './analytics/events/ui/closed';
12
14
  import { fireAnalytics } from './analytics/fireAnalytics';
@@ -14,21 +16,39 @@ import { mediaViewerPopupClass } from './classnames';
14
16
  import { Content } from './content';
15
17
  import { List } from './list';
16
18
  import { Blanket, SidebarWrapper } from './styleWrappers';
19
+ var getIdentifierId = function getIdentifierId(identifier) {
20
+ return isFileIdentifier(identifier) ? identifier.id : undefined;
21
+ };
17
22
  var MediaViewerComponent = function MediaViewerComponent(_ref) {
18
23
  var featureFlags = _ref.featureFlags,
19
24
  items = _ref.items,
20
25
  extensions = _ref.extensions,
21
26
  contextId = _ref.contextId,
22
27
  innerRef = _ref.innerRef,
23
- _onClose = _ref.onClose,
28
+ onClose = _ref.onClose,
24
29
  selectedItem = _ref.selectedItem,
25
30
  intl = _ref.intl,
26
31
  viewerOptions = _ref.viewerOptions,
27
32
  fallbackMediaNameFetcher = _ref.fallbackMediaNameFetcher;
28
- var _useState = useState(false),
33
+ // Experiment gate for the Confluence comments-in-media-viewer work. Only the
34
+ // fields introduced by that series are gated here — `extensions.sidebar` and
35
+ // `extensions.headerActions` shipped earlier and must keep working with the
36
+ // experiment off, so `extensions` itself is deliberately *not* nullified.
37
+ var _ref2 = isExperimentEnabled('cc_comments_media_viewer_sidebar') ? extensions !== null && extensions !== void 0 ? extensions : {} : {},
38
+ onPreviewClose = _ref2.onPreviewClose,
39
+ onSidebarClose = _ref2.onSidebarClose,
40
+ onNavigation = _ref2.onNavigation,
41
+ onSelectedItemChange = _ref2.onSelectedItemChange,
42
+ defaultSidebarVisible = _ref2.defaultSidebarVisible,
43
+ onSidebarVisibilityChange = _ref2.onSidebarVisibilityChange;
44
+ var _useState = useState(defaultSidebarVisible !== null && defaultSidebarVisible !== void 0 ? defaultSidebarVisible : false),
29
45
  _useState2 = _slicedToArray(_useState, 2),
30
46
  isSidebarVisible = _useState2[0],
31
- setIsSidebarVisible = _useState2[1];
47
+ setIsSidebarVisibleState = _useState2[1];
48
+ var setIsSidebarVisible = useCallback(function (isVisible) {
49
+ setIsSidebarVisibleState(isVisible);
50
+ onSidebarVisibilityChange === null || onSidebarVisibilityChange === void 0 || onSidebarVisibilityChange(isVisible);
51
+ }, [onSidebarVisibilityChange]);
32
52
  var _useState3 = useState(),
33
53
  _useState4 = _slicedToArray(_useState3, 2),
34
54
  selectedIdentifier = _useState4[0],
@@ -37,19 +57,95 @@ var MediaViewerComponent = function MediaViewerComponent(_ref) {
37
57
  createAnalyticsEvent = _useAnalyticsEvents.createAnalyticsEvent;
38
58
  var createAnalyticsEventRef = useRef(createAnalyticsEvent);
39
59
  createAnalyticsEventRef.current = createAnalyticsEvent;
60
+
61
+ // Delegation helpers: if the consumer provides an interceptor, call it and
62
+ // let the consumer decide when (or whether) to invoke `proceed`. Otherwise
63
+ // proceed immediately so behavior is unchanged for consumers that don't
64
+ // supply the new callbacks.
65
+ var requestPreviewClose = useCallback(function (proceed) {
66
+ if (onPreviewClose) {
67
+ onPreviewClose(proceed);
68
+ } else {
69
+ proceed();
70
+ }
71
+ }, [onPreviewClose]);
72
+ var requestSidebarClose = useCallback(function (proceed) {
73
+ if (onSidebarClose) {
74
+ onSidebarClose(proceed);
75
+ } else {
76
+ proceed();
77
+ }
78
+ }, [onSidebarClose]);
79
+ var requestNavigation = useCallback(function (direction, proceed) {
80
+ if (onNavigation) {
81
+ onNavigation(direction, proceed);
82
+ } else {
83
+ proceed();
84
+ }
85
+ }, [onNavigation]);
40
86
  useLayoutEffect(function () {
41
87
  fireAnalytics(createModalEvent(), createAnalyticsEventRef.current);
42
88
  start('MediaViewer.SessionDuration');
43
89
  }, []);
44
90
  var defaultSelectedItem = selectedItem || items[0];
91
+
92
+ // Toggling the sidebar: opening is unconditional; closing goes through the
93
+ // optional onSidebarClose interceptor.
94
+ var toggleSidebar = useCallback(function () {
95
+ if (isSidebarVisible) {
96
+ requestSidebarClose(function () {
97
+ return setIsSidebarVisible(false);
98
+ });
99
+ } else {
100
+ setIsSidebarVisible(true);
101
+ }
102
+ }, [isSidebarVisible, requestSidebarClose, setIsSidebarVisible]);
103
+
104
+ // The full modal close — used by ESC, the close button, and click-outside.
105
+ // Always passed through requestPreviewClose so the consumer can intercept.
106
+ var handlePreviewClose = useCallback(function (reason) {
107
+ requestPreviewClose(function () {
108
+ if (reason !== 'other') {
109
+ fireAnalytics(createClosedEvent(reason), createAnalyticsEventRef.current);
110
+ }
111
+ onClose && onClose();
112
+ });
113
+ }, [onClose, requestPreviewClose]);
114
+
115
+ // Compute the navigation direction by comparing the new identifier's index
116
+ // in `items` with the current selected identifier's index. Falls back to
117
+ // 'next' if we can't determine direction (e.g. random jump from a list).
118
+ var computeNavigationDirection = useCallback(function (nextIdentifier) {
119
+ var current = selectedIdentifier || defaultSelectedItem;
120
+ var currentId = current && getIdentifierId(current);
121
+ var nextId = getIdentifierId(nextIdentifier);
122
+ if (!currentId || !nextId) {
123
+ return 'next';
124
+ }
125
+ var currentIndex = items.findIndex(function (it) {
126
+ return getIdentifierId(it) === currentId;
127
+ });
128
+ var nextIndex = items.findIndex(function (it) {
129
+ return getIdentifierId(it) === nextId;
130
+ });
131
+ if (currentIndex === -1 || nextIndex === -1) {
132
+ return 'next';
133
+ }
134
+ return nextIndex < currentIndex ? 'prev' : 'next';
135
+ }, [items, selectedIdentifier, defaultSelectedItem]);
45
136
  var renderSidebar = function renderSidebar() {
46
137
  var sidebarSelectedIdentifier = selectedIdentifier || defaultSelectedItem;
47
- if (sidebarSelectedIdentifier && isSidebarVisible && extensions && extensions.sidebar) {
138
+ if (sidebarSelectedIdentifier && isSidebarVisible && extensions !== null && extensions !== void 0 && extensions.sidebar) {
48
139
  return /*#__PURE__*/React.createElement(SidebarWrapper, {
49
140
  "data-testid": "media-viewer-sidebar-content"
50
141
  }, extensions.sidebar.renderer(sidebarSelectedIdentifier, {
142
+ // `close` is what the sidebar renderer calls to dismiss itself.
143
+ // Route it through the same interceptor so confluence's
144
+ // unsaved-comment guard runs here too.
51
145
  close: function close() {
52
- return setIsSidebarVisible(!isSidebarVisible);
146
+ return requestSidebarClose(function () {
147
+ return setIsSidebarVisible(false);
148
+ });
53
149
  }
54
150
  }));
55
151
  }
@@ -64,27 +160,42 @@ var MediaViewerComponent = function MediaViewerComponent(_ref) {
64
160
  }, /*#__PURE__*/React.createElement(Shortcut, {
65
161
  code: 'Escape',
66
162
  handler: function handler() {
67
- fireAnalytics(createClosedEvent('escKey'), createAnalyticsEventRef.current);
68
- _onClose && _onClose();
163
+ handlePreviewClose('escKey');
69
164
  }
70
165
  }), /*#__PURE__*/React.createElement(Content, {
71
166
  isSidebarVisible: isSidebarVisible,
72
167
  onClose: function onClose(_e, analyticsEvent) {
73
168
  var _analyticsEvent$paylo;
74
- if ((analyticsEvent === null || analyticsEvent === void 0 || (_analyticsEvent$paylo = analyticsEvent.payload) === null || _analyticsEvent$paylo === void 0 ? void 0 : _analyticsEvent$paylo.actionSubject) === 'button') {
75
- fireAnalytics(createClosedEvent('button'), createAnalyticsEventRef.current);
76
- }
77
- _onClose && _onClose();
169
+ var reason = (analyticsEvent === null || analyticsEvent === void 0 || (_analyticsEvent$paylo = analyticsEvent.payload) === null || _analyticsEvent$paylo === void 0 ? void 0 : _analyticsEvent$paylo.actionSubject) === 'button' ? 'button' : 'other';
170
+ handlePreviewClose(reason);
78
171
  }
79
172
  }, /*#__PURE__*/React.createElement(List, {
80
173
  defaultSelectedItem: defaultSelectedItem || items[0],
81
- items: items,
82
- onClose: _onClose,
83
- extensions: extensions,
84
- onNavigationChange: setSelectedIdentifier,
85
- onSidebarButtonClick: function onSidebarButtonClick() {
86
- return setIsSidebarVisible(!isSidebarVisible);
174
+ items: items
175
+ // Note: `onClose` here is what the prev/next list passes down for
176
+ // "close-the-modal" actions. Route through interceptor too.
177
+ ,
178
+ onClose: function onClose() {
179
+ return handlePreviewClose('other');
180
+ },
181
+ extensions: extensions
182
+ // Notification-only: List has already updated its displayed item
183
+ // by the time this fires. We use it to keep the parent's
184
+ // `selectedIdentifier` in sync (consumers may key off it).
185
+ ,
186
+ onNavigationChange: function onNavigationChange(identifier) {
187
+ setSelectedIdentifier(identifier);
188
+ onSelectedItemChange === null || onSelectedItemChange === void 0 || onSelectedItemChange(identifier);
189
+ }
190
+ // Pre-commit gate: consumer can swallow or defer the
191
+ // nav by not calling `commit`. The image only swaps
192
+ // once `commit` runs.
193
+ ,
194
+ onNavigationRequest: function onNavigationRequest(identifier, commit) {
195
+ var direction = computeNavigationDirection(identifier);
196
+ requestNavigation(direction, commit);
87
197
  },
198
+ onSidebarButtonClick: toggleSidebar,
88
199
  isSidebarVisible: isSidebarVisible,
89
200
  contextId: contextId,
90
201
  featureFlags: featureFlags,
@@ -10,9 +10,15 @@ export type FileStateFlags = {
10
10
  export interface MediaViewerExtensionsActions {
11
11
  close: () => void;
12
12
  }
13
+ export type MediaViewerNavigationDirection = 'next' | 'prev';
13
14
  export interface MediaViewerExtensions {
14
15
  sidebar?: {
15
16
  icon: ReactNode;
17
+ /** Accessible label for the sidebar toggle. Falls back to a generic localized string. */
18
+ label?: string;
19
+ /** Title rendered by Media Viewer in the inset sidebar header. */
20
+ title?: string;
21
+ /** Renders the sidebar body below the Platform-owned inset sidebar header. */
16
22
  renderer: (selectedIdentifier: Identifier, actions: MediaViewerExtensionsActions) => ReactNode;
17
23
  };
18
24
  headerActions?: Array<{
@@ -26,6 +32,55 @@ export interface MediaViewerExtensions {
26
32
  /** Optional callback to control per-item visibility. When omitted, button always shows. */
27
33
  isVisible?: (selectedIdentifier: Identifier) => boolean;
28
34
  }>;
35
+ /**
36
+ * Interceptor for the media viewer modal-close path (close button, ESC, click
37
+ * outside). When provided, the platform calls this instead of immediately
38
+ * closing — the consumer must invoke `proceed()` to actually close. If
39
+ * `proceed` is never called, the close is silently swallowed (e.g. while a
40
+ * confirm prompt is awaiting user input).
41
+ *
42
+ * When omitted, the platform proceeds immediately (default behavior).
43
+ */
44
+ onPreviewClose?: (proceed: () => void) => void;
45
+ /**
46
+ * Interceptor for the sidebar-close path (clicking the sidebar toggle button
47
+ * while the sidebar is open, or the sidebar's own internal close action).
48
+ * Called only when the sidebar is currently visible. Same delegation
49
+ * semantics as `onPreviewClose`.
50
+ */
51
+ onSidebarClose?: (proceed: () => void) => void;
52
+ /**
53
+ * Interceptor for the prev/next navigation buttons. Receives the requested
54
+ * direction so the consumer can decide per-direction. Same delegation
55
+ * semantics as `onPreviewClose`.
56
+ */
57
+ onNavigation?: (direction: MediaViewerNavigationDirection, proceed: () => void) => void;
58
+ /**
59
+ * Optional selected-item override for a given media card. Consumers can return
60
+ * an identifier to make this card render MediaViewer, or null to keep it
61
+ * closed. This lets consumers own remount-restore policy outside media-card.
62
+ */
63
+ getMediaViewerSelectedItem?: (identifier: Identifier) => Identifier | null;
64
+ /**
65
+ * Called whenever the selected preview item changes, including open, close,
66
+ * and prev/next navigation. Consumers can persist this value somewhere stable
67
+ * so a remounted owner card can restore the viewer to the same selected item.
68
+ */
69
+ onSelectedItemChange?: (selectedIdentifier: Identifier | null) => void;
70
+ /**
71
+ * Optional initial value for the sidebar's open/closed state. Used to persist
72
+ * the sidebar visibility across MediaViewer remounts (e.g. when the
73
+ * surrounding renderer re-mounts because its document was updated and the
74
+ * media node was re-inserted into the tree).
75
+ */
76
+ defaultSidebarVisible?: boolean;
77
+ /**
78
+ * Called whenever the sidebar's open/closed state changes inside the viewer
79
+ * (after the on{Sidebar,Preview}Close interceptors have proceeded). The
80
+ * consumer can persist this value somewhere stable so a subsequent remount
81
+ * can restore it via `defaultSidebarVisible`.
82
+ */
83
+ onSidebarVisibilityChange?: (isVisible: boolean) => void;
29
84
  }
30
85
  export interface MediaViewerProps {
31
86
  readonly mediaClient: MediaClient;
@@ -7,6 +7,15 @@ import { type ViewerOptionsProps } from './viewerOptions';
7
7
  export type Props = Readonly<{
8
8
  onClose?: () => void;
9
9
  onNavigationChange?: (selectedItem: Identifier) => void;
10
+ /**
11
+ * If provided, the List delegates the decision to advance to the next/prev
12
+ * item to the consumer. The consumer must call `proceed()` to actually
13
+ * commit the navigation. If `proceed` is never called, the underlying
14
+ * displayed item does not change. Used by consumers that need to show a
15
+ * confirmation prompt (e.g. unsaved comment changes) before allowing
16
+ * navigation between media items.
17
+ */
18
+ onNavigationRequest?: (selectedItem: Identifier, proceed: () => void) => void;
10
19
  defaultSelectedItem: Identifier;
11
20
  items: Identifier[];
12
21
  extensions?: MediaViewerExtensions;
@@ -22,4 +31,4 @@ export type State = {
22
31
  previewCount: number;
23
32
  isArchiveSideBarVisible: boolean;
24
33
  };
25
- export declare const List: ({ defaultSelectedItem, onClose, showControls, extensions, onSidebarButtonClick, contextId, featureFlags, isSidebarVisible, onNavigationChange, items, viewerOptions, fallbackMediaNameFetcher, }: Props) => React.JSX.Element;
34
+ export declare const List: ({ defaultSelectedItem, onClose, showControls, extensions, onSidebarButtonClick, contextId, featureFlags, isSidebarVisible, onNavigationChange, onNavigationRequest, items, viewerOptions, fallbackMediaNameFetcher, }: Props) => React.JSX.Element;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atlaskit/media-viewer",
3
- "version": "54.8.44",
3
+ "version": "54.9.1",
4
4
  "description": "MediaViewer is Atlassian's powerful solution for viewing files on the web. It's both powerful and extendable yet easy-to-integrate",
5
5
  "publishConfig": {
6
6
  "registry": "https://registry.npmjs.org/"
@@ -50,6 +50,7 @@
50
50
  "@atlaskit/media-svg": "^4.0.0",
51
51
  "@atlaskit/media-ui": "^31.0.0",
52
52
  "@atlaskit/menu": "^11.0.0",
53
+ "@atlaskit/platform-feature-experiments": "^0.3.0",
53
54
  "@atlaskit/platform-feature-flags": "^2.2.0",
54
55
  "@atlaskit/portal": "^7.0.0",
55
56
  "@atlaskit/primitives": "^22.5.0",
@@ -58,7 +59,7 @@
58
59
  "@atlaskit/spinner": "^20.4.0",
59
60
  "@atlaskit/textfield": "^10.2.0",
60
61
  "@atlaskit/theme": "^28.2.0",
61
- "@atlaskit/tmp-editor-statsig": "^205.0.0",
62
+ "@atlaskit/tmp-editor-statsig": "^207.0.0",
62
63
  "@atlaskit/tokens": "^20.0.0",
63
64
  "@atlaskit/tooltip": "^24.3.0",
64
65
  "@atlaskit/ufo": "^2.0.0",
@@ -95,10 +96,11 @@
95
96
  "@atlaskit/media-test-data": "^4.0.0",
96
97
  "@atlaskit/media-test-helpers": "^43.0.0",
97
98
  "@atlaskit/modal-dialog": "^17.0.0",
98
- "@atlaskit/select": "^23.0.0",
99
+ "@atlaskit/select": "^23.1.0",
99
100
  "@atlaskit/ssr": "workspace:^",
100
101
  "@atlaskit/toggle": "^17.3.0",
101
102
  "@atlassian/a11y-jest-testing": "^0.17.0",
103
+ "@atlassian/experiment-test-utils": "^0.2.0",
102
104
  "@atlassian/feature-flags-test-utils": "^1.3.0",
103
105
  "@atlassian/testing-library": "^0.11.0",
104
106
  "@atlassian/ufo": "^1.0.0",