@atlaskit/editor-plugin-table 3.1.3 → 3.2.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 (34) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/dist/cjs/plugins/table/nodeviews/TableComponent.js +36 -2
  3. package/dist/cjs/plugins/table/nodeviews/TableStickyScrollbar.js +154 -0
  4. package/dist/cjs/plugins/table/pm-plugins/sticky-headers/nodeviews/tableRow.js +37 -4
  5. package/dist/cjs/plugins/table/toolbar.js +6 -0
  6. package/dist/cjs/plugins/table/ui/common-styles.js +9 -2
  7. package/dist/es2019/plugins/table/nodeviews/TableComponent.js +37 -3
  8. package/dist/es2019/plugins/table/nodeviews/TableStickyScrollbar.js +112 -0
  9. package/dist/es2019/plugins/table/pm-plugins/sticky-headers/nodeviews/tableRow.js +39 -6
  10. package/dist/es2019/plugins/table/toolbar.js +6 -0
  11. package/dist/es2019/plugins/table/ui/common-styles.js +33 -1
  12. package/dist/esm/plugins/table/nodeviews/TableComponent.js +37 -3
  13. package/dist/esm/plugins/table/nodeviews/TableStickyScrollbar.js +146 -0
  14. package/dist/esm/plugins/table/pm-plugins/sticky-headers/nodeviews/tableRow.js +37 -4
  15. package/dist/esm/plugins/table/toolbar.js +6 -0
  16. package/dist/esm/plugins/table/ui/common-styles.js +8 -2
  17. package/dist/types/plugins/table/nodeviews/TableComponent.d.ts +1 -0
  18. package/dist/types/plugins/table/nodeviews/TableStickyScrollbar.d.ts +24 -0
  19. package/dist/types/plugins/table/pm-plugins/sticky-headers/nodeviews/tableRow.d.ts +5 -0
  20. package/dist/types/plugins/table/toolbar.d.ts +4 -4
  21. package/dist/types/plugins/table/types.d.ts +3 -0
  22. package/dist/types/plugins/table/ui/common-styles.d.ts +1 -0
  23. package/dist/types-ts4.5/plugins/table/nodeviews/TableComponent.d.ts +1 -0
  24. package/dist/types-ts4.5/plugins/table/nodeviews/TableStickyScrollbar.d.ts +24 -0
  25. package/dist/types-ts4.5/plugins/table/pm-plugins/sticky-headers/nodeviews/tableRow.d.ts +5 -0
  26. package/dist/types-ts4.5/plugins/table/toolbar.d.ts +4 -4
  27. package/dist/types-ts4.5/plugins/table/types.d.ts +3 -0
  28. package/dist/types-ts4.5/plugins/table/ui/common-styles.d.ts +1 -0
  29. package/package.json +6 -3
  30. package/src/plugins/table/nodeviews/TableComponent.tsx +54 -3
  31. package/src/plugins/table/nodeviews/TableStickyScrollbar.ts +204 -0
  32. package/src/plugins/table/pm-plugins/sticky-headers/nodeviews/tableRow.ts +40 -3
  33. package/src/plugins/table/toolbar.tsx +10 -6
  34. package/src/plugins/table/ui/common-styles.ts +38 -0
@@ -0,0 +1,112 @@
1
+ import _defineProperty from "@babel/runtime/helpers/defineProperty";
2
+ import rafSchedule from 'raf-schd';
3
+ import { findOverflowScrollParent } from '@atlaskit/editor-common/ui';
4
+ import { TableCssClassName as ClassName } from '../types';
5
+ export class TableStickyScrollbar {
6
+ constructor(wrapper, view) {
7
+ _defineProperty(this, "sentinels", {});
8
+ _defineProperty(this, "handleScroll", event => {
9
+ if (!this.stickyScrollbarContainerElement || !this.wrapper || event.target !== this.stickyScrollbarContainerElement) {
10
+ return;
11
+ }
12
+ this.wrapper.scrollLeft = this.stickyScrollbarContainerElement.scrollLeft;
13
+ });
14
+ _defineProperty(this, "handleScrollDebounced", rafSchedule(this.handleScroll));
15
+ this.wrapper = wrapper;
16
+ this.view = view;
17
+ this.init();
18
+ }
19
+ dispose() {
20
+ if (this.stickyScrollbarContainerElement) {
21
+ this.stickyScrollbarContainerElement.removeEventListener('scroll', this.handleScrollDebounced);
22
+ this.handleScrollDebounced.cancel();
23
+ }
24
+ this.deleteIntesactionObserver();
25
+ }
26
+ scrollLeft(left) {
27
+ if (this.stickyScrollbarContainerElement) {
28
+ this.stickyScrollbarContainerElement.scrollLeft = left;
29
+ }
30
+ }
31
+ init() {
32
+ var _this$wrapper$parentE;
33
+ if (!this.wrapper) {
34
+ return;
35
+ }
36
+ this.stickyScrollbarContainerElement = (_this$wrapper$parentE = this.wrapper.parentElement) === null || _this$wrapper$parentE === void 0 ? void 0 : _this$wrapper$parentE.querySelector(`.${ClassName.TABLE_STICKY_SCROLLBAR_CONTAINER}`);
37
+ if (this.stickyScrollbarContainerElement) {
38
+ this.stickyScrollbarContainerElement.addEventListener('scroll', this.handleScrollDebounced, {
39
+ passive: true
40
+ });
41
+ }
42
+ this.createIntersectionObserver();
43
+ }
44
+ createIntersectionObserver() {
45
+ var _this$wrapper, _this$wrapper$parentE2, _this$wrapper$parentE3, _this$wrapper2, _this$wrapper2$parent, _this$wrapper2$parent2;
46
+ this.editorScrollableElement = findOverflowScrollParent(this.view.dom) || window.document;
47
+ if (!this.editorScrollableElement || !this.wrapper) {
48
+ return;
49
+ }
50
+ this.intersectionObserver = new IntersectionObserver((entries, _) => {
51
+ if (!this.stickyScrollbarContainerElement) {
52
+ return;
53
+ }
54
+ entries.forEach(entry => {
55
+ var _entry$rootBounds;
56
+ const target = entry.target;
57
+ // if the rootBounds has 0 height, e.g. confluence preview mode, we do nothing.
58
+ if (((_entry$rootBounds = entry.rootBounds) === null || _entry$rootBounds === void 0 ? void 0 : _entry$rootBounds.height) === 0) {
59
+ return;
60
+ }
61
+ if (target.classList.contains(ClassName.TABLE_STICKY_SCROLLBAR_SENTINEL_BOTTOM)) {
62
+ this.sentenialBottomCallback(entry);
63
+ }
64
+ if (target.classList.contains(ClassName.TABLE_STICKY_SCROLLBAR_SENTINEL_TOP)) {
65
+ this.sentenialTopCallback(entry);
66
+ }
67
+ });
68
+ }, {
69
+ root: this.editorScrollableElement
70
+ });
71
+ this.sentinels.bottom = (_this$wrapper = this.wrapper) === null || _this$wrapper === void 0 ? void 0 : (_this$wrapper$parentE2 = _this$wrapper.parentElement) === null || _this$wrapper$parentE2 === void 0 ? void 0 : (_this$wrapper$parentE3 = _this$wrapper$parentE2.getElementsByClassName(ClassName.TABLE_STICKY_SCROLLBAR_SENTINEL_BOTTOM)) === null || _this$wrapper$parentE3 === void 0 ? void 0 : _this$wrapper$parentE3.item(0);
72
+ this.sentinels.top = (_this$wrapper2 = this.wrapper) === null || _this$wrapper2 === void 0 ? void 0 : (_this$wrapper2$parent = _this$wrapper2.parentElement) === null || _this$wrapper2$parent === void 0 ? void 0 : (_this$wrapper2$parent2 = _this$wrapper2$parent.getElementsByClassName(ClassName.TABLE_STICKY_SCROLLBAR_SENTINEL_TOP)) === null || _this$wrapper2$parent2 === void 0 ? void 0 : _this$wrapper2$parent2.item(0);
73
+ [this.sentinels.bottom, this.sentinels.top].forEach(el => this.intersectionObserver.observe(el));
74
+ }
75
+ deleteIntesactionObserver() {
76
+ if (this.intersectionObserver) {
77
+ if (this.sentinels.bottom) {
78
+ this.intersectionObserver.unobserve(this.sentinels.bottom);
79
+ }
80
+ this.intersectionObserver.disconnect();
81
+ }
82
+ }
83
+ sentenialBottomCallback(entry) {
84
+ var _entry$rootBounds2;
85
+ const sentinelIsAboveScrollArea = entry.boundingClientRect.top < (((_entry$rootBounds2 = entry.rootBounds) === null || _entry$rootBounds2 === void 0 ? void 0 : _entry$rootBounds2.top) || 0);
86
+ this.bottomSentinelState = sentinelIsAboveScrollArea ? 'above' : entry.isIntersecting ? 'visible' : 'below';
87
+ this.toggle();
88
+ }
89
+ sentenialTopCallback(entry) {
90
+ var _entry$rootBounds3;
91
+ const sentinelIsBelowScrollArea = (((_entry$rootBounds3 = entry.rootBounds) === null || _entry$rootBounds3 === void 0 ? void 0 : _entry$rootBounds3.bottom) || 0) < entry.boundingClientRect.top;
92
+ this.topSentinelState = sentinelIsBelowScrollArea ? 'below' : entry.isIntersecting ? 'visible' : 'above';
93
+ this.toggle();
94
+ }
95
+ toggle() {
96
+ if ((this.topSentinelState === 'visible' || this.topSentinelState === 'above') && this.bottomSentinelState === 'below') {
97
+ this.show();
98
+ } else {
99
+ this.hide();
100
+ }
101
+ }
102
+ hide() {
103
+ if (this.stickyScrollbarContainerElement && this.stickyScrollbarContainerElement.style.display !== 'none') {
104
+ this.stickyScrollbarContainerElement.style.display = 'none';
105
+ }
106
+ }
107
+ show() {
108
+ if (this.stickyScrollbarContainerElement && this.stickyScrollbarContainerElement.style.display !== 'block') {
109
+ this.stickyScrollbarContainerElement.style.display = 'block';
110
+ }
111
+ }
112
+ }
@@ -137,10 +137,24 @@ export class TableRowNodeView {
137
137
  // otherwise make it non-sticky
138
138
  return false;
139
139
  });
140
+ /**
141
+ * Manually refire the intersection observers.
142
+ * Useful when the header may have detached from the table.
143
+ */
144
+ _defineProperty(this, "refireIntersectionObservers", () => {
145
+ if (this.isSticky) {
146
+ [this.sentinels.top, this.sentinels.bottom].forEach(el => {
147
+ if (el && this.intersectionObserver) {
148
+ this.intersectionObserver.unobserve(el);
149
+ this.intersectionObserver.observe(el);
150
+ }
151
+ });
152
+ }
153
+ });
140
154
  _defineProperty(this, "makeHeaderRowSticky", (tree, scrollTop) => {
141
155
  var _tbody$firstChild;
142
156
  // If header row height is more than 50% of viewport height don't do this
143
- if (this.stickyRowHeight && this.stickyRowHeight > window.innerHeight / 2) {
157
+ if (this.isSticky || this.stickyRowHeight && this.stickyRowHeight > window.innerHeight / 2) {
144
158
  return;
145
159
  }
146
160
  const {
@@ -160,10 +174,25 @@ export class TableRowNodeView {
160
174
  }
161
175
  const domTop = currentTableTop > 0 ? scrollTop : scrollTop + currentTableTop;
162
176
  if (!this.isSticky) {
177
+ var _this$editorScrollabl;
163
178
  syncStickyRowToTable(table);
164
179
  this.dom.classList.add('sticky');
165
180
  table.classList.add(ClassName.TABLE_STICKY);
166
181
  this.isSticky = true;
182
+
183
+ /**
184
+ * The logic below is not desirable, but acts as a fail safe for scenarios where the sticky header
185
+ * detaches from the table. This typically happens during a fast scroll by the user which causes
186
+ * the intersection observer logic to not fire as expected.
187
+ */
188
+ (_this$editorScrollabl = this.editorScrollableElement) === null || _this$editorScrollabl === void 0 ? void 0 : _this$editorScrollabl.addEventListener('scrollend', this.refireIntersectionObservers, {
189
+ passive: true,
190
+ once: true
191
+ });
192
+ const fastScrollThresholdMs = 500;
193
+ setTimeout(() => {
194
+ this.refireIntersectionObservers();
195
+ }, fastScrollThresholdMs);
167
196
  }
168
197
  this.dom.style.top = `${domTop}px`;
169
198
  updateTableMargin(table);
@@ -245,8 +274,12 @@ export class TableRowNodeView {
245
274
  this.eventDispatcher.on('widthPlugin', this.updateStickyHeaderWidth);
246
275
  this.eventDispatcher.on(tablePluginKey.key, this.onTablePluginState);
247
276
  this.listening = true;
248
- this.dom.addEventListener('wheel', this.headerRowMouseScroll.bind(this));
249
- this.dom.addEventListener('touchmove', this.headerRowMouseScroll.bind(this));
277
+ this.dom.addEventListener('wheel', this.headerRowMouseScroll.bind(this), {
278
+ passive: true
279
+ });
280
+ this.dom.addEventListener('touchmove', this.headerRowMouseScroll.bind(this), {
281
+ passive: true
282
+ });
250
283
  }
251
284
  unsubscribe() {
252
285
  if (!this.listening) {
@@ -255,7 +288,7 @@ export class TableRowNodeView {
255
288
  if (this.intersectionObserver) {
256
289
  this.intersectionObserver.disconnect();
257
290
  // ED-16211 Once intersection observer is disconnected, we need to remove the isObserved from the sentinels
258
- // Otherwise when new intersection observer is created it will not observe because it thinks its already being observed
291
+ // Otherwise when newer intersection observer is created it will not observe because it thinks its already being observed
259
292
  [this.sentinels.top, this.sentinels.bottom].forEach(el => {
260
293
  if (el) {
261
294
  delete el.dataset.isObserved;
@@ -316,10 +349,10 @@ export class TableRowNodeView {
316
349
  table
317
350
  } = this.tree;
318
351
  entries.forEach(entry => {
319
- var _this$editorScrollabl;
352
+ var _this$editorScrollabl2;
320
353
  // On resize of the parent scroll element we need to adjust the width
321
354
  // of the sticky header
322
- if (entry.target.className === ((_this$editorScrollabl = this.editorScrollableElement) === null || _this$editorScrollabl === void 0 ? void 0 : _this$editorScrollabl.className)) {
355
+ if (entry.target.className === ((_this$editorScrollabl2 = this.editorScrollableElement) === null || _this$editorScrollabl2 === void 0 ? void 0 : _this$editorScrollabl2.className)) {
323
356
  this.updateStickyHeaderWidth();
324
357
  } else {
325
358
  const newHeight = entry.contentRect ? entry.contentRect.height : entry.target.offsetHeight;
@@ -12,6 +12,7 @@ import { akEditorFloatingPanelZIndex } from '@atlaskit/editor-shared-styles';
12
12
  import { shortcutStyle } from '@atlaskit/editor-shared-styles/shortcut';
13
13
  import { findCellRectClosestToPos, findTable, getSelectionRect, isSelectionType, splitCell } from '@atlaskit/editor-tables/utils';
14
14
  import RemoveIcon from '@atlaskit/icon/glyph/editor/remove';
15
+ import { getBooleanFF } from '@atlaskit/platform-feature-flags';
15
16
  import { clearHoverSelection, hoverColumns, hoverMergedCells, hoverRows, hoverTable, removeDescendantNodes } from './commands';
16
17
  import { deleteColumnsWithAnalytics, deleteRowsWithAnalytics, deleteTableWithAnalytics, distributeColumnsWidthsWithAnalytics, emptyMultipleCellsWithAnalytics, insertColumnWithAnalytics, insertRowWithAnalytics, mergeCellsWithAnalytics, setColorWithAnalytics, sortColumnWithAnalytics, splitCellWithAnalytics, toggleHeaderColumnWithAnalytics, toggleHeaderRowWithAnalytics, toggleNumberColumnWithAnalytics, wrapTableInExpandWithAnalytics } from './commands-with-analytics';
17
18
  import { getPluginState } from './pm-plugins/plugin-factory';
@@ -337,6 +338,11 @@ export const getToolbarConfig = (getEditorContainerWidth, editorAnalyticsAPI, ge
337
338
  getDomRef,
338
339
  nodeType,
339
340
  offset: [0, 18],
341
+ absoluteOffset: getBooleanFF('platform.editor.table-sticky-scrollbar') ? {
342
+ top: -6
343
+ } : {
344
+ top: 0
345
+ },
340
346
  zIndex: akEditorFloatingPanelZIndex + 1,
341
347
  // Place the context menu slightly above the others
342
348
  items: [menu, separator(menu.hidden), ...cellItems, ...colorPicker, {
@@ -1,6 +1,6 @@
1
1
  import { css } from '@emotion/react';
2
2
  import { tableMarginTop, tableSharedStyle } from '@atlaskit/editor-common/styles';
3
- import { akEditorSelectedNodeClassName, akEditorSmallZIndex, akEditorStickyHeaderZIndex, akEditorTableCellOnStickyHeaderZIndex, akEditorTableNumberColumnWidth, akEditorTableToolbarSize, akEditorUnitZIndex, getSelectionStyles, relativeFontSizeToBase16, SelectionStyle } from '@atlaskit/editor-shared-styles';
3
+ import { akEditorSelectedNodeClassName, akEditorSmallZIndex, akEditorStickyHeaderZIndex, akEditorTableCellOnStickyHeaderZIndex, akEditorTableNumberColumnWidth, akEditorTableToolbarSize, akEditorUnitZIndex, getSelectionStyles, MAX_BROWSER_SCROLLBAR_HEIGHT, relativeFontSizeToBase16, SelectionStyle } from '@atlaskit/editor-shared-styles';
4
4
  import { scrollbarStyles } from '@atlaskit/editor-shared-styles/scrollbar';
5
5
  import { getBooleanFF } from '@atlaskit/platform-feature-flags';
6
6
  import { B300, N0, N20A, N300, N40A, R500 } from '@atlaskit/theme/colors';
@@ -16,6 +16,7 @@ const cornerControlHeight = tableToolbarSize + 1;
16
16
  its center should be aligned with the edge
17
17
  */
18
18
  export const insertColumnButtonOffset = tableInsertColumnButtonSize / 2;
19
+ export const tableRowHeight = 44;
19
20
  const rangeSelectionStyles = `
20
21
  .${ClassName.NODEVIEW_WRAPPER}.${akEditorSelectedNodeClassName} table tbody tr {
21
22
  th,td {
@@ -48,6 +49,36 @@ const sentinelStyles = `.${ClassName.TABLE_CONTAINER} {
48
49
  }
49
50
  }
50
51
  }`;
52
+ const stickyScrollbarSentinelStyles = `.${ClassName.TABLE_CONTAINER} {
53
+ > .${ClassName.TABLE_STICKY_SCROLLBAR_SENTINEL_BOTTOM},
54
+ > .${ClassName.TABLE_STICKY_SCROLLBAR_SENTINEL_TOP} {
55
+ position: absolute;
56
+ width: 100%;
57
+ height: 1px;
58
+ margin-top: -1px;
59
+ // need this to avoid sentinel being focused via keyboard
60
+ // this still allows it to be detected by intersection observer
61
+ visibility: hidden;
62
+ }
63
+ > .${ClassName.TABLE_STICKY_SCROLLBAR_SENTINEL_TOP} {
64
+ top: ${columnControlsDecorationHeight + tableRowHeight * 3}px;
65
+ }
66
+ > .${ClassName.TABLE_STICKY_SCROLLBAR_SENTINEL_BOTTOM} {
67
+ bottom: ${MAX_BROWSER_SCROLLBAR_HEIGHT}px;
68
+ }
69
+ }`;
70
+ const stickyScrollbarContainerStyles = `.${ClassName.TABLE_CONTAINER} {
71
+ > .${ClassName.TABLE_STICKY_SCROLLBAR_CONTAINER} {
72
+ width: 100%;
73
+ display: none;
74
+ overflow-x: auto;
75
+ position: sticky;
76
+ bottom: 0;
77
+ }
78
+ }`;
79
+ const stickyScrollbarStyles = () => {
80
+ return getBooleanFF('platform.editor.table-sticky-scrollbar') ? `${stickyScrollbarContainerStyles} ${stickyScrollbarSentinelStyles}` : '';
81
+ };
51
82
  const shadowSentinelStyles = `
52
83
  .${ClassName.TABLE_SHADOW_SENTINEL_LEFT},
53
84
  .${ClassName.TABLE_SHADOW_SENTINEL_RIGHT} {
@@ -349,6 +380,7 @@ export const tableStyles = props => {
349
380
 
350
381
  ${sentinelStyles}
351
382
  ${OverflowShadow(props)}
383
+ ${stickyScrollbarStyles()}
352
384
 
353
385
  .${ClassName.TABLE_STICKY} .${ClassName.TABLE_STICKY_SHADOW} {
354
386
  height: 0; // stop overflow flash & set correct height in update-overflow-shadows.ts
@@ -18,7 +18,7 @@ import { createDispatch } from '@atlaskit/editor-common/event-dispatcher';
18
18
  import { getParentNodeWidth } from '@atlaskit/editor-common/node-width';
19
19
  import { tableMarginSides } from '@atlaskit/editor-common/styles';
20
20
  import { analyticsEventKey, browser, isValidPosition } from '@atlaskit/editor-common/utils';
21
- import { akEditorTableToolbarSize as tableToolbarSize } from '@atlaskit/editor-shared-styles';
21
+ import { MAX_BROWSER_SCROLLBAR_HEIGHT, akEditorTableToolbarSize as tableToolbarSize } from '@atlaskit/editor-shared-styles';
22
22
  import { findTable, isTableSelected } from '@atlaskit/editor-tables/utils';
23
23
  import { getBooleanFF } from '@atlaskit/platform-feature-flags';
24
24
  import { autoSizeTable, clearHoverSelection } from '../commands';
@@ -34,6 +34,7 @@ import TableFloatingControls from '../ui/TableFloatingControls';
34
34
  import { containsHeaderRow, isTableNested, tablesHaveDifferentColumnWidths, tablesHaveDifferentNoOfColumns } from '../utils';
35
35
  import { OverflowShadowsObserver } from './OverflowShadowsObserver';
36
36
  import { TableContainer } from './TableContainer';
37
+ import { TableStickyScrollbar } from './TableStickyScrollbar';
37
38
  var isIE11 = browser.ie_version === 11;
38
39
  // When table is inserted via paste, keyboard shortcut or quickInsert,
39
40
  // componentDidUpdate is called multiple times. The isOverflowing value is correct only on the last update.
@@ -87,6 +88,11 @@ var TableComponent = /*#__PURE__*/function (_React$Component) {
87
88
  if (!_this.wrapper || event.target !== _this.wrapper) {
88
89
  return;
89
90
  }
91
+ if (getBooleanFF('platform.editor.table-sticky-scrollbar')) {
92
+ if (_this.stickyScrollbar) {
93
+ _this.stickyScrollbar.scrollLeft(_this.wrapper.scrollLeft);
94
+ }
95
+ }
90
96
  if (_this.table) {
91
97
  // sync sticky header row to table scroll
92
98
  var headers = _this.table.querySelectorAll('tr[data-header-row]');
@@ -297,7 +303,14 @@ var TableComponent = /*#__PURE__*/function (_React$Component) {
297
303
  eventDispatcher = _this$props7.eventDispatcher,
298
304
  options = _this$props7.options;
299
305
  if (allowColumnResizing && this.wrapper && !isIE11) {
300
- this.wrapper.addEventListener('scroll', this.handleScrollDebounced);
306
+ this.wrapper.addEventListener('scroll', this.handleScrollDebounced, {
307
+ passive: true
308
+ });
309
+ if (getBooleanFF('platform.editor.table-sticky-scrollbar')) {
310
+ if (this.table) {
311
+ this.stickyScrollbar = new TableStickyScrollbar(this.wrapper, this.props.view);
312
+ }
313
+ }
301
314
  }
302
315
  if (allowColumnResizing) {
303
316
  /**
@@ -329,6 +342,11 @@ var TableComponent = /*#__PURE__*/function (_React$Component) {
329
342
  if (this.wrapper && !isIE11) {
330
343
  this.wrapper.removeEventListener('scroll', this.handleScrollDebounced);
331
344
  }
345
+ if (getBooleanFF('platform.editor.table-sticky-scrollbar')) {
346
+ if (this.stickyScrollbar) {
347
+ this.stickyScrollbar.dispose();
348
+ }
349
+ }
332
350
  this.handleScrollDebounced.cancel();
333
351
  this.scaleTableDebounced.cancel();
334
352
  this.handleTableResizingDebounced.cancel();
@@ -501,6 +519,9 @@ var TableComponent = /*#__PURE__*/function (_React$Component) {
501
519
  }, /*#__PURE__*/React.createElement("div", {
502
520
  className: ClassName.TABLE_STICKY_SENTINEL_TOP,
503
521
  "data-testid": "sticky-sentinel-top"
522
+ }), getBooleanFF('platform.editor.table-sticky-scrollbar') && /*#__PURE__*/React.createElement("div", {
523
+ className: ClassName.TABLE_STICKY_SCROLLBAR_SENTINEL_TOP,
524
+ "data-testid": "sticky-scrollbar-sentinel-top"
504
525
  }), allowControls && rowControls, /*#__PURE__*/React.createElement("div", {
505
526
  style: shadowStyle(showBeforeShadow),
506
527
  className: ClassName.TABLE_LEFT_SHADOW
@@ -523,7 +544,17 @@ var TableComponent = /*#__PURE__*/function (_React$Component) {
523
544
  }
524
545
  }
525
546
  }
526
- }), /*#__PURE__*/React.createElement("div", {
547
+ }), getBooleanFF('platform.editor.table-sticky-scrollbar') && /*#__PURE__*/React.createElement("div", {
548
+ className: ClassName.TABLE_STICKY_SCROLLBAR_CONTAINER,
549
+ style: {
550
+ height: MAX_BROWSER_SCROLLBAR_HEIGHT,
551
+ display: 'none'
552
+ }
553
+ }, /*#__PURE__*/React.createElement("div", {
554
+ style: {
555
+ width: tableRef === null || tableRef === void 0 ? void 0 : tableRef.clientWidth
556
+ }
557
+ })), /*#__PURE__*/React.createElement("div", {
527
558
  style: shadowStyle(showAfterShadow),
528
559
  className: ClassName.TABLE_RIGHT_SHADOW
529
560
  }), this.state.stickyHeader && /*#__PURE__*/React.createElement("div", {
@@ -540,6 +571,9 @@ var TableComponent = /*#__PURE__*/function (_React$Component) {
540
571
  })), /*#__PURE__*/React.createElement("div", {
541
572
  className: ClassName.TABLE_STICKY_SENTINEL_BOTTOM,
542
573
  "data-testid": "sticky-sentinel-bottom"
574
+ }), getBooleanFF('platform.editor.table-sticky-scrollbar') && /*#__PURE__*/React.createElement("div", {
575
+ className: ClassName.TABLE_STICKY_SCROLLBAR_SENTINEL_BOTTOM,
576
+ "data-testid": "sticky-scrollbar-sentinel-bottom"
543
577
  }));
544
578
  }
545
579
  }]);
@@ -0,0 +1,146 @@
1
+ import _classCallCheck from "@babel/runtime/helpers/classCallCheck";
2
+ import _createClass from "@babel/runtime/helpers/createClass";
3
+ import _defineProperty from "@babel/runtime/helpers/defineProperty";
4
+ import rafSchedule from 'raf-schd';
5
+ import { findOverflowScrollParent } from '@atlaskit/editor-common/ui';
6
+ import { TableCssClassName as ClassName } from '../types';
7
+ export var TableStickyScrollbar = /*#__PURE__*/function () {
8
+ function TableStickyScrollbar(wrapper, view) {
9
+ var _this = this;
10
+ _classCallCheck(this, TableStickyScrollbar);
11
+ _defineProperty(this, "sentinels", {});
12
+ _defineProperty(this, "handleScroll", function (event) {
13
+ if (!_this.stickyScrollbarContainerElement || !_this.wrapper || event.target !== _this.stickyScrollbarContainerElement) {
14
+ return;
15
+ }
16
+ _this.wrapper.scrollLeft = _this.stickyScrollbarContainerElement.scrollLeft;
17
+ });
18
+ _defineProperty(this, "handleScrollDebounced", rafSchedule(this.handleScroll));
19
+ this.wrapper = wrapper;
20
+ this.view = view;
21
+ this.init();
22
+ }
23
+ _createClass(TableStickyScrollbar, [{
24
+ key: "dispose",
25
+ value: function dispose() {
26
+ if (this.stickyScrollbarContainerElement) {
27
+ this.stickyScrollbarContainerElement.removeEventListener('scroll', this.handleScrollDebounced);
28
+ this.handleScrollDebounced.cancel();
29
+ }
30
+ this.deleteIntesactionObserver();
31
+ }
32
+ }, {
33
+ key: "scrollLeft",
34
+ value: function scrollLeft(left) {
35
+ if (this.stickyScrollbarContainerElement) {
36
+ this.stickyScrollbarContainerElement.scrollLeft = left;
37
+ }
38
+ }
39
+ }, {
40
+ key: "init",
41
+ value: function init() {
42
+ var _this$wrapper$parentE;
43
+ if (!this.wrapper) {
44
+ return;
45
+ }
46
+ this.stickyScrollbarContainerElement = (_this$wrapper$parentE = this.wrapper.parentElement) === null || _this$wrapper$parentE === void 0 ? void 0 : _this$wrapper$parentE.querySelector(".".concat(ClassName.TABLE_STICKY_SCROLLBAR_CONTAINER));
47
+ if (this.stickyScrollbarContainerElement) {
48
+ this.stickyScrollbarContainerElement.addEventListener('scroll', this.handleScrollDebounced, {
49
+ passive: true
50
+ });
51
+ }
52
+ this.createIntersectionObserver();
53
+ }
54
+ }, {
55
+ key: "createIntersectionObserver",
56
+ value: function createIntersectionObserver() {
57
+ var _this2 = this,
58
+ _this$wrapper,
59
+ _this$wrapper$parentE2,
60
+ _this$wrapper$parentE3,
61
+ _this$wrapper2,
62
+ _this$wrapper2$parent,
63
+ _this$wrapper2$parent2;
64
+ this.editorScrollableElement = findOverflowScrollParent(this.view.dom) || window.document;
65
+ if (!this.editorScrollableElement || !this.wrapper) {
66
+ return;
67
+ }
68
+ this.intersectionObserver = new IntersectionObserver(function (entries, _) {
69
+ if (!_this2.stickyScrollbarContainerElement) {
70
+ return;
71
+ }
72
+ entries.forEach(function (entry) {
73
+ var _entry$rootBounds;
74
+ var target = entry.target;
75
+ // if the rootBounds has 0 height, e.g. confluence preview mode, we do nothing.
76
+ if (((_entry$rootBounds = entry.rootBounds) === null || _entry$rootBounds === void 0 ? void 0 : _entry$rootBounds.height) === 0) {
77
+ return;
78
+ }
79
+ if (target.classList.contains(ClassName.TABLE_STICKY_SCROLLBAR_SENTINEL_BOTTOM)) {
80
+ _this2.sentenialBottomCallback(entry);
81
+ }
82
+ if (target.classList.contains(ClassName.TABLE_STICKY_SCROLLBAR_SENTINEL_TOP)) {
83
+ _this2.sentenialTopCallback(entry);
84
+ }
85
+ });
86
+ }, {
87
+ root: this.editorScrollableElement
88
+ });
89
+ this.sentinels.bottom = (_this$wrapper = this.wrapper) === null || _this$wrapper === void 0 ? void 0 : (_this$wrapper$parentE2 = _this$wrapper.parentElement) === null || _this$wrapper$parentE2 === void 0 ? void 0 : (_this$wrapper$parentE3 = _this$wrapper$parentE2.getElementsByClassName(ClassName.TABLE_STICKY_SCROLLBAR_SENTINEL_BOTTOM)) === null || _this$wrapper$parentE3 === void 0 ? void 0 : _this$wrapper$parentE3.item(0);
90
+ this.sentinels.top = (_this$wrapper2 = this.wrapper) === null || _this$wrapper2 === void 0 ? void 0 : (_this$wrapper2$parent = _this$wrapper2.parentElement) === null || _this$wrapper2$parent === void 0 ? void 0 : (_this$wrapper2$parent2 = _this$wrapper2$parent.getElementsByClassName(ClassName.TABLE_STICKY_SCROLLBAR_SENTINEL_TOP)) === null || _this$wrapper2$parent2 === void 0 ? void 0 : _this$wrapper2$parent2.item(0);
91
+ [this.sentinels.bottom, this.sentinels.top].forEach(function (el) {
92
+ return _this2.intersectionObserver.observe(el);
93
+ });
94
+ }
95
+ }, {
96
+ key: "deleteIntesactionObserver",
97
+ value: function deleteIntesactionObserver() {
98
+ if (this.intersectionObserver) {
99
+ if (this.sentinels.bottom) {
100
+ this.intersectionObserver.unobserve(this.sentinels.bottom);
101
+ }
102
+ this.intersectionObserver.disconnect();
103
+ }
104
+ }
105
+ }, {
106
+ key: "sentenialBottomCallback",
107
+ value: function sentenialBottomCallback(entry) {
108
+ var _entry$rootBounds2;
109
+ var sentinelIsAboveScrollArea = entry.boundingClientRect.top < (((_entry$rootBounds2 = entry.rootBounds) === null || _entry$rootBounds2 === void 0 ? void 0 : _entry$rootBounds2.top) || 0);
110
+ this.bottomSentinelState = sentinelIsAboveScrollArea ? 'above' : entry.isIntersecting ? 'visible' : 'below';
111
+ this.toggle();
112
+ }
113
+ }, {
114
+ key: "sentenialTopCallback",
115
+ value: function sentenialTopCallback(entry) {
116
+ var _entry$rootBounds3;
117
+ var sentinelIsBelowScrollArea = (((_entry$rootBounds3 = entry.rootBounds) === null || _entry$rootBounds3 === void 0 ? void 0 : _entry$rootBounds3.bottom) || 0) < entry.boundingClientRect.top;
118
+ this.topSentinelState = sentinelIsBelowScrollArea ? 'below' : entry.isIntersecting ? 'visible' : 'above';
119
+ this.toggle();
120
+ }
121
+ }, {
122
+ key: "toggle",
123
+ value: function toggle() {
124
+ if ((this.topSentinelState === 'visible' || this.topSentinelState === 'above') && this.bottomSentinelState === 'below') {
125
+ this.show();
126
+ } else {
127
+ this.hide();
128
+ }
129
+ }
130
+ }, {
131
+ key: "hide",
132
+ value: function hide() {
133
+ if (this.stickyScrollbarContainerElement && this.stickyScrollbarContainerElement.style.display !== 'none') {
134
+ this.stickyScrollbarContainerElement.style.display = 'none';
135
+ }
136
+ }
137
+ }, {
138
+ key: "show",
139
+ value: function show() {
140
+ if (this.stickyScrollbarContainerElement && this.stickyScrollbarContainerElement.style.display !== 'block') {
141
+ this.stickyScrollbarContainerElement.style.display = 'block';
142
+ }
143
+ }
144
+ }]);
145
+ return TableStickyScrollbar;
146
+ }();
@@ -142,10 +142,24 @@ export var TableRowNodeView = /*#__PURE__*/function () {
142
142
  // otherwise make it non-sticky
143
143
  return false;
144
144
  });
145
+ /**
146
+ * Manually refire the intersection observers.
147
+ * Useful when the header may have detached from the table.
148
+ */
149
+ _defineProperty(this, "refireIntersectionObservers", function () {
150
+ if (_this.isSticky) {
151
+ [_this.sentinels.top, _this.sentinels.bottom].forEach(function (el) {
152
+ if (el && _this.intersectionObserver) {
153
+ _this.intersectionObserver.unobserve(el);
154
+ _this.intersectionObserver.observe(el);
155
+ }
156
+ });
157
+ }
158
+ });
145
159
  _defineProperty(this, "makeHeaderRowSticky", function (tree, scrollTop) {
146
160
  var _tbody$firstChild;
147
161
  // If header row height is more than 50% of viewport height don't do this
148
- if (_this.stickyRowHeight && _this.stickyRowHeight > window.innerHeight / 2) {
162
+ if (_this.isSticky || _this.stickyRowHeight && _this.stickyRowHeight > window.innerHeight / 2) {
149
163
  return;
150
164
  }
151
165
  var table = tree.table,
@@ -163,10 +177,25 @@ export var TableRowNodeView = /*#__PURE__*/function () {
163
177
  }
164
178
  var domTop = currentTableTop > 0 ? scrollTop : scrollTop + currentTableTop;
165
179
  if (!_this.isSticky) {
180
+ var _this$editorScrollabl;
166
181
  syncStickyRowToTable(table);
167
182
  _this.dom.classList.add('sticky');
168
183
  table.classList.add(ClassName.TABLE_STICKY);
169
184
  _this.isSticky = true;
185
+
186
+ /**
187
+ * The logic below is not desirable, but acts as a fail safe for scenarios where the sticky header
188
+ * detaches from the table. This typically happens during a fast scroll by the user which causes
189
+ * the intersection observer logic to not fire as expected.
190
+ */
191
+ (_this$editorScrollabl = _this.editorScrollableElement) === null || _this$editorScrollabl === void 0 ? void 0 : _this$editorScrollabl.addEventListener('scrollend', _this.refireIntersectionObservers, {
192
+ passive: true,
193
+ once: true
194
+ });
195
+ var fastScrollThresholdMs = 500;
196
+ setTimeout(function () {
197
+ _this.refireIntersectionObservers();
198
+ }, fastScrollThresholdMs);
170
199
  }
171
200
  _this.dom.style.top = "".concat(domTop, "px");
172
201
  updateTableMargin(table);
@@ -263,8 +292,12 @@ export var TableRowNodeView = /*#__PURE__*/function () {
263
292
  this.eventDispatcher.on('widthPlugin', this.updateStickyHeaderWidth);
264
293
  this.eventDispatcher.on(tablePluginKey.key, this.onTablePluginState);
265
294
  this.listening = true;
266
- this.dom.addEventListener('wheel', this.headerRowMouseScroll.bind(this));
267
- this.dom.addEventListener('touchmove', this.headerRowMouseScroll.bind(this));
295
+ this.dom.addEventListener('wheel', this.headerRowMouseScroll.bind(this), {
296
+ passive: true
297
+ });
298
+ this.dom.addEventListener('touchmove', this.headerRowMouseScroll.bind(this), {
299
+ passive: true
300
+ });
268
301
  }
269
302
  }, {
270
303
  key: "unsubscribe",
@@ -275,7 +308,7 @@ export var TableRowNodeView = /*#__PURE__*/function () {
275
308
  if (this.intersectionObserver) {
276
309
  this.intersectionObserver.disconnect();
277
310
  // ED-16211 Once intersection observer is disconnected, we need to remove the isObserved from the sentinels
278
- // Otherwise when new intersection observer is created it will not observe because it thinks its already being observed
311
+ // Otherwise when newer intersection observer is created it will not observe because it thinks its already being observed
279
312
  [this.sentinels.top, this.sentinels.bottom].forEach(function (el) {
280
313
  if (el) {
281
314
  delete el.dataset.isObserved;
@@ -12,6 +12,7 @@ import { akEditorFloatingPanelZIndex } from '@atlaskit/editor-shared-styles';
12
12
  import { shortcutStyle } from '@atlaskit/editor-shared-styles/shortcut';
13
13
  import { findCellRectClosestToPos, findTable, getSelectionRect, isSelectionType, splitCell } from '@atlaskit/editor-tables/utils';
14
14
  import RemoveIcon from '@atlaskit/icon/glyph/editor/remove';
15
+ import { getBooleanFF } from '@atlaskit/platform-feature-flags';
15
16
  import { clearHoverSelection, hoverColumns, hoverMergedCells, hoverRows, hoverTable, removeDescendantNodes } from './commands';
16
17
  import { deleteColumnsWithAnalytics, deleteRowsWithAnalytics, deleteTableWithAnalytics, distributeColumnsWidthsWithAnalytics, emptyMultipleCellsWithAnalytics, insertColumnWithAnalytics, insertRowWithAnalytics, mergeCellsWithAnalytics, setColorWithAnalytics, sortColumnWithAnalytics, splitCellWithAnalytics, toggleHeaderColumnWithAnalytics, toggleHeaderRowWithAnalytics, toggleNumberColumnWithAnalytics, wrapTableInExpandWithAnalytics } from './commands-with-analytics';
17
18
  import { getPluginState } from './pm-plugins/plugin-factory';
@@ -343,6 +344,11 @@ export var getToolbarConfig = function getToolbarConfig(getEditorContainerWidth,
343
344
  getDomRef: getDomRef,
344
345
  nodeType: nodeType,
345
346
  offset: [0, 18],
347
+ absoluteOffset: getBooleanFF('platform.editor.table-sticky-scrollbar') ? {
348
+ top: -6
349
+ } : {
350
+ top: 0
351
+ },
346
352
  zIndex: akEditorFloatingPanelZIndex + 1,
347
353
  // Place the context menu slightly above the others
348
354
  items: [menu, separator(menu.hidden)].concat(cellItems, colorPicker, [{