@atlaskit/editor-plugin-table 5.3.39 → 5.4.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 (29) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/dist/cjs/plugins/table/nodeviews/TableComponent.js +3 -2
  3. package/dist/cjs/plugins/table/nodeviews/TableRow.js +158 -45
  4. package/dist/cjs/plugins/table/ui/TableFloatingControls/CornerControls/DragCornerControls.js +0 -6
  5. package/dist/cjs/plugins/table/ui/TableFloatingControls/RowDropTarget/index.js +3 -0
  6. package/dist/cjs/plugins/table/ui/common-styles.js +18 -7
  7. package/dist/cjs/plugins/table/ui/ui-styles.js +1 -1
  8. package/dist/es2019/plugins/table/nodeviews/TableComponent.js +3 -2
  9. package/dist/es2019/plugins/table/nodeviews/TableRow.js +163 -50
  10. package/dist/es2019/plugins/table/ui/TableFloatingControls/CornerControls/DragCornerControls.js +1 -9
  11. package/dist/es2019/plugins/table/ui/TableFloatingControls/RowDropTarget/index.js +1 -0
  12. package/dist/es2019/plugins/table/ui/common-styles.js +17 -0
  13. package/dist/es2019/plugins/table/ui/ui-styles.js +25 -15
  14. package/dist/esm/plugins/table/nodeviews/TableComponent.js +3 -2
  15. package/dist/esm/plugins/table/nodeviews/TableRow.js +158 -46
  16. package/dist/esm/plugins/table/ui/TableFloatingControls/CornerControls/DragCornerControls.js +1 -7
  17. package/dist/esm/plugins/table/ui/TableFloatingControls/RowDropTarget/index.js +3 -0
  18. package/dist/esm/plugins/table/ui/common-styles.js +18 -7
  19. package/dist/esm/plugins/table/ui/ui-styles.js +1 -1
  20. package/dist/types/plugins/table/nodeviews/TableRow.d.ts +3 -0
  21. package/dist/types-ts4.5/plugins/table/nodeviews/TableRow.d.ts +3 -0
  22. package/package.json +5 -2
  23. package/src/plugins/table/nodeviews/TableComponent.tsx +3 -2
  24. package/src/plugins/table/nodeviews/TableRow.ts +199 -52
  25. package/src/plugins/table/ui/TableFloatingControls/CornerControls/DragCornerControls.tsx +1 -7
  26. package/src/plugins/table/ui/TableFloatingControls/RowDropTarget/index.tsx +1 -0
  27. package/src/plugins/table/ui/common-styles.ts +23 -0
  28. package/src/plugins/table/ui/ui-styles.ts +28 -15
  29. package/src/__tests__/integration/horizontal-scroll.ts +0 -491
@@ -6,6 +6,7 @@ import { findOverflowScrollParent } from '@atlaskit/editor-common/ui';
6
6
  import { browser } from '@atlaskit/editor-common/utils';
7
7
  import type { Node as PMNode } from '@atlaskit/editor-prosemirror/model';
8
8
  import type { EditorView, NodeView } from '@atlaskit/editor-prosemirror/view';
9
+ import { getBooleanFF } from '@atlaskit/platform-feature-flags';
9
10
 
10
11
  import { getPluginState } from '../pm-plugins/plugin-factory';
11
12
  import { pluginKey as tablePluginKey } from '../pm-plugins/plugin-key';
@@ -29,6 +30,12 @@ import { supportedHeaderRow } from '../utils/nodes';
29
30
 
30
31
  import TableNodeView from './TableNodeViewBase';
31
32
 
33
+ interface SentinelData {
34
+ isIntersecting: boolean;
35
+ boundingClientRect: DOMRectReadOnly | null;
36
+ rootBounds: DOMRectReadOnly | null;
37
+ }
38
+
32
39
  // limit scroll event calls
33
40
  const HEADER_ROW_SCROLL_THROTTLE_TIMEOUT = 200;
34
41
 
@@ -80,6 +87,21 @@ export default class TableRow
80
87
  top?: HTMLElement | null;
81
88
  bottom?: HTMLElement | null;
82
89
  } = {};
90
+ private sentinelData: {
91
+ top: SentinelData;
92
+ bottom: SentinelData;
93
+ } = {
94
+ top: {
95
+ isIntersecting: false,
96
+ boundingClientRect: null,
97
+ rootBounds: null,
98
+ },
99
+ bottom: {
100
+ isIntersecting: false,
101
+ boundingClientRect: null,
102
+ rootBounds: null,
103
+ },
104
+ };
83
105
  private stickyRowHeight?: number;
84
106
  private listening = false;
85
107
  private padding: number = 0;
@@ -327,71 +349,196 @@ export default class TableRow
327
349
  }
328
350
 
329
351
  private createIntersectionObserver() {
330
- this.intersectionObserver = new IntersectionObserver(
331
- (entries: IntersectionObserverEntry[], _: IntersectionObserver) => {
332
- const tree = getTree(this.dom);
333
- if (!tree) {
334
- return;
335
- }
336
- const { table } = tree;
337
-
338
- if (table.rows.length < 2) {
339
- // ED-19307 - When there's only one row in a table the top & bottom sentinels become inverted. This creates some nasty visiblity
340
- // toggling side-effects because the intersection observers gets confused.
341
- return;
342
- }
343
-
344
- entries.forEach((entry) => {
345
- const target = entry.target as HTMLElement;
352
+ if (getBooleanFF('platform.editor.table.alternative-sticky-header-logic')) {
353
+ this.intersectionObserver = new IntersectionObserver(
354
+ (entries: IntersectionObserverEntry[], _: IntersectionObserver) => {
355
+ // IMPORTANT: please try and avoid using entry.rootBounds it's terribly inconsistent. For example; sometimes it may return
356
+ // 0 height. In safari it will multiply all values by the window scale factor, however chrome & firfox won't.
357
+ // This is why i just get the scroll view bounding rect here and use it, and fallback to the entry.rootBounds if needed.
358
+ const rootBounds = (
359
+ this.editorScrollableElement as Element
360
+ )?.getBoundingClientRect?.();
361
+
362
+ entries.forEach((entry) => {
363
+ const { target, isIntersecting, boundingClientRect } = entry;
364
+ // This observer only every looks at the top/bottom sentinels, we can assume if it's not one then it's the other.
365
+ const targetKey = target.classList.contains(
366
+ ClassName.TABLE_STICKY_SENTINEL_TOP,
367
+ )
368
+ ? 'top'
369
+ : 'bottom';
370
+ // Cache the latest sentinel information
371
+ this.sentinelData[targetKey] = {
372
+ isIntersecting,
373
+ boundingClientRect,
374
+ rootBounds: rootBounds ?? entry.rootBounds,
375
+ };
376
+ // Keep the other sentinels rootBounds in sync with this latest one
377
+ this.sentinelData[
378
+ targetKey === 'top' ? 'bottom' : targetKey
379
+ ].rootBounds = rootBounds ?? entry.rootBounds;
380
+ });
381
+ this.refreshStickyState();
382
+ },
383
+ { threshold: 0, root: this.editorScrollableElement as Element },
384
+ );
385
+ } else {
386
+ this.intersectionObserver = new IntersectionObserver(
387
+ (entries: IntersectionObserverEntry[], _: IntersectionObserver) => {
388
+ const tree = getTree(this.dom);
389
+ if (!tree) {
390
+ return;
391
+ }
392
+ const { table } = tree;
346
393
 
347
- // if the rootBounds has 0 height, e.g. confluence preview mode, we do nothing.
348
- if (entry.rootBounds?.height === 0) {
394
+ if (table.rows.length < 2) {
395
+ // ED-19307 - When there's only one row in a table the top & bottom sentinels become inverted. This creates some nasty visiblity
396
+ // toggling side-effects because the intersection observers gets confused.
349
397
  return;
350
398
  }
351
399
 
352
- if (target.classList.contains(ClassName.TABLE_STICKY_SENTINEL_TOP)) {
353
- const sentinelIsBelowScrollArea =
354
- (entry.rootBounds?.bottom || 0) < entry.boundingClientRect.bottom;
400
+ entries.forEach((entry) => {
401
+ const target = entry.target as HTMLElement;
355
402
 
356
- if (!entry.isIntersecting && !sentinelIsBelowScrollArea) {
357
- tree && this.makeHeaderRowSticky(tree, entry.rootBounds?.top);
358
- this.lastStickyTimestamp = Date.now();
359
- } else {
360
- table && this.makeRowHeaderNotSticky(table);
403
+ // if the rootBounds has 0 height, e.g. confluence preview mode, we do nothing.
404
+ if (entry.rootBounds?.height === 0) {
405
+ return;
361
406
  }
362
- }
363
407
 
364
- if (
365
- target.classList.contains(ClassName.TABLE_STICKY_SENTINEL_BOTTOM)
366
- ) {
367
- const sentinelIsAboveScrollArea =
368
- entry.boundingClientRect.top - this.dom.offsetHeight <
369
- (entry.rootBounds?.top || 0);
370
-
371
- if (table && !entry.isIntersecting && sentinelIsAboveScrollArea) {
372
- // Not a perfect solution, but need to this code specific for FireFox ED-19177
373
- if (browser.gecko) {
374
- if (
375
- this.lastStickyTimestamp &&
376
- Date.now() - this.lastStickyTimestamp >
377
- STICKY_HEADER_TOGGLE_TOLERANCE_MS
378
- ) {
408
+ if (
409
+ target.classList.contains(ClassName.TABLE_STICKY_SENTINEL_TOP)
410
+ ) {
411
+ const sentinelIsBelowScrollArea =
412
+ (entry.rootBounds?.bottom || 0) <
413
+ entry.boundingClientRect.bottom;
414
+
415
+ if (!entry.isIntersecting && !sentinelIsBelowScrollArea) {
416
+ tree && this.makeHeaderRowSticky(tree, entry.rootBounds?.top);
417
+ this.lastStickyTimestamp = Date.now();
418
+ } else {
419
+ table && this.makeRowHeaderNotSticky(table);
420
+ }
421
+ }
422
+
423
+ if (
424
+ target.classList.contains(ClassName.TABLE_STICKY_SENTINEL_BOTTOM)
425
+ ) {
426
+ const sentinelIsAboveScrollArea =
427
+ entry.boundingClientRect.top - this.dom.offsetHeight <
428
+ (entry.rootBounds?.top || 0);
429
+
430
+ if (table && !entry.isIntersecting && sentinelIsAboveScrollArea) {
431
+ // Not a perfect solution, but need to this code specific for FireFox ED-19177
432
+ if (browser.gecko) {
433
+ if (
434
+ this.lastStickyTimestamp &&
435
+ Date.now() - this.lastStickyTimestamp >
436
+ STICKY_HEADER_TOGGLE_TOLERANCE_MS
437
+ ) {
438
+ this.makeRowHeaderNotSticky(table);
439
+ }
440
+ } else {
379
441
  this.makeRowHeaderNotSticky(table);
380
442
  }
381
- } else {
382
- this.makeRowHeaderNotSticky(table);
443
+ } else if (entry.isIntersecting && sentinelIsAboveScrollArea) {
444
+ tree && this.makeHeaderRowSticky(tree, entry?.rootBounds?.top);
445
+ this.lastStickyTimestamp = Date.now();
383
446
  }
384
- } else if (entry.isIntersecting && sentinelIsAboveScrollArea) {
385
- tree && this.makeHeaderRowSticky(tree, entry?.rootBounds?.top);
386
- this.lastStickyTimestamp = Date.now();
387
447
  }
388
- }
389
- return;
390
- });
391
- },
392
- { root: this.editorScrollableElement as Element },
448
+ return;
449
+ });
450
+ },
451
+ { root: this.editorScrollableElement as Element },
452
+ );
453
+ }
454
+ }
455
+
456
+ private refreshStickyState() {
457
+ const tree = getTree(this.dom);
458
+ if (!tree) {
459
+ return;
460
+ }
461
+ const { table } = tree;
462
+ const shouldStick = this.isHeaderSticky();
463
+ if (this.isSticky !== shouldStick) {
464
+ if (shouldStick) {
465
+ // The rootRect is kept in sync across sentinels so it doesn't matter which one we use.
466
+ const rootRect =
467
+ this.sentinelData.top.rootBounds ??
468
+ this.sentinelData.bottom.rootBounds;
469
+ this.makeHeaderRowSticky(tree, rootRect?.top);
470
+ } else {
471
+ this.makeRowHeaderNotSticky(table);
472
+ }
473
+ }
474
+ }
475
+
476
+ private isHeaderSticky() {
477
+ /*
478
+ # Overview
479
+ I'm going to list all the view states associated with the sentinels and when they should trigger sticky headers.
480
+ The format of the states are; {top|bottom}:{in|above|below}
481
+ ie sentinel:view-position -- both "above" and "below" are equal to out of the viewport
482
+
483
+ For example; "top:in" means top sentinel is within the viewport. "bottom:above" means the bottom sentinel is
484
+ above and out of the viewport
485
+
486
+ This will hopefully simplify things and make it easier to determine when sticky should/shouldn't be triggered.
487
+
488
+ # States
489
+ top:in / bottom:in - NOT sticky
490
+ top:in / bottom:above - NOT sticky - NOTE: This is an inversion clause
491
+ top:in / bottom:below - NOT sticky
492
+ top:above / bottom:in - STICKY
493
+ top:above / bottom:above - NOT sticky
494
+ top:above / bottom:below - STICKY
495
+ top:below / bottom:in - NOT sticky - NOTE: This is an inversion clause
496
+ top:below / bottom:above - NOT sticky - NOTE: This is an inversion clause
497
+ top:below / bottom:below - NOT sticky
498
+
499
+ # Summary
500
+ The only time the header should be sticky is when the top sentinel is above the view and the bottom sentinel
501
+ is in or below it.
502
+ */
503
+
504
+ const { top: sentinelTop, bottom: sentinelBottom } = this.sentinelData;
505
+ // The rootRect is kept in sync across sentinels so it doesn't matter which one we use.
506
+ const rootBounds = sentinelTop.rootBounds ?? sentinelBottom.rootBounds;
507
+
508
+ if (
509
+ !rootBounds ||
510
+ !sentinelTop.boundingClientRect ||
511
+ !sentinelBottom.boundingClientRect
512
+ ) {
513
+ return false;
514
+ }
515
+
516
+ // Normalize the sentinels to y points
517
+ // Since the sentinels are actually rects 1px high we want make sure we normalise the inner most values closest to the table.
518
+ const sentinelTopY = sentinelTop.boundingClientRect.bottom;
519
+ const sentinelBottomY = sentinelBottom.boundingClientRect.top;
520
+
521
+ // If header row height is more than 50% of viewport height don't do this
522
+ const isRowHeaderTooLarge =
523
+ this.stickyRowHeight && this.stickyRowHeight > window.innerHeight * 0.5;
524
+
525
+ const isTopSentinelAboveScrollArea =
526
+ !sentinelTop.isIntersecting && sentinelTopY <= rootBounds.top;
527
+
528
+ const isBottomSentinelInOrBelowScrollArea =
529
+ sentinelBottom.isIntersecting || sentinelBottomY > rootBounds.bottom;
530
+
531
+ // This makes sure that the top sentinel is actually above the bottom sentinel, and that they havn't inverted.
532
+ const isTopSentinelAboveBottomSentinel = sentinelTopY < sentinelBottomY;
533
+
534
+ return (
535
+ isTopSentinelAboveScrollArea &&
536
+ isBottomSentinelInOrBelowScrollArea &&
537
+ isTopSentinelAboveBottomSentinel &&
538
+ !isRowHeaderTooLarge
393
539
  );
394
540
  }
541
+
395
542
  /* receive external events */
396
543
 
397
544
  private onTablePluginState(state: TablePluginState) {
@@ -11,7 +11,7 @@ import {
11
11
  selectTable,
12
12
  } from '@atlaskit/editor-tables/utils';
13
13
 
14
- import { clearHoverSelection, hoverTable } from '../../../commands';
14
+ import { clearHoverSelection } from '../../../commands';
15
15
  import { TableCssClassName as ClassName } from '../../../types';
16
16
 
17
17
  import type { CornerControlProps } from './types';
@@ -26,11 +26,6 @@ const DragCornerControlsComponent = ({
26
26
  dispatch(selectTable(state.tr).setMeta('addToHistory', false));
27
27
  };
28
28
 
29
- const handleMouseOver = () => {
30
- const { state, dispatch } = editorView;
31
- hoverTable()(state, dispatch);
32
- };
33
-
34
29
  const handleMouseOut = () => {
35
30
  const { state, dispatch } = editorView;
36
31
  clearHoverSelection()(state, dispatch);
@@ -54,7 +49,6 @@ const DragCornerControlsComponent = ({
54
49
  aria-label={formatMessage(messages.cornerControl)}
55
50
  type="button"
56
51
  onClick={handleOnClick}
57
- onMouseOver={handleMouseOver}
58
52
  onMouseOut={handleMouseOut}
59
53
  contentEditable={false}
60
54
  >
@@ -33,6 +33,7 @@ const RowDropTarget = ({ index, localId, style }: RowDropTargetProps) => {
33
33
  data.indexes?.indexOf(index) === -1
34
34
  );
35
35
  },
36
+ getIsSticky: () => true,
36
37
  getData({ input, element }) {
37
38
  const data = {
38
39
  localId,
@@ -5,6 +5,7 @@ import {
5
5
  tableSharedStyle,
6
6
  } from '@atlaskit/editor-common/styles';
7
7
  import type { FeatureFlags } from '@atlaskit/editor-common/types';
8
+ import { browser } from '@atlaskit/editor-common/utils';
8
9
  import {
9
10
  akEditorSelectedNodeClassName,
10
11
  akEditorSmallZIndex,
@@ -246,6 +247,26 @@ const tableStickyHeaderColumnControlsDecorationsStyle = (
246
247
  }
247
248
  };
248
249
 
250
+ const tableStickyHeaderFirefoxFixStyle = (
251
+ props: ThemeProps & { featureFlags?: FeatureFlags },
252
+ ) => {
253
+ /*
254
+ This is MAGIC!
255
+ This fixes a bug which occurs in firefox when the first row becomes sticky.
256
+ see https://product-fabric.atlassian.net/browse/ED-19177
257
+ */
258
+ if (
259
+ browser.gecko &&
260
+ getBooleanFF('platform.editor.table.alternative-sticky-header-logic')
261
+ ) {
262
+ return css`
263
+ .${ClassName.TABLE_STICKY} > tbody::before {
264
+ content: '';
265
+ }
266
+ `;
267
+ }
268
+ };
269
+
249
270
  const tableRowControlStyles = () => {
250
271
  return getBooleanFF('platform.editor.table.drag-and-drop')
251
272
  ? css`
@@ -402,6 +423,8 @@ export const tableStyles = (
402
423
 
403
424
  ${tableStickyHeaderColumnControlsDecorationsStyle(props)}
404
425
 
426
+ ${tableStickyHeaderFirefoxFixStyle(props)}
427
+
405
428
  .${ClassName.TABLE_STICKY}
406
429
  .${ClassName.ROW_CONTROLS}
407
430
  .${ClassName.ROW_CONTROLS_BUTTON_WRAP}.sticky {
@@ -222,16 +222,15 @@ export const dragInsertButtonWrapper = (props: ThemeProps) => css`
222
222
 
223
223
  export const dragCornerControlButton = (props: ThemeProps) => css`
224
224
  .${ClassName.DRAG_CORNER_BUTTON} {
225
- width: 12px;
226
- height: 12px;
225
+ width: 24px;
226
+ height: 24px;
227
227
  display: flex;
228
- justify-content: center;
229
- align-items: center;
228
+ justify-content: flex-end;
229
+ align-items: flex-end;
230
230
  position: absolute;
231
- top: ${token('space.negative.075', '-6px')};
232
- left: ${token('space.075', '6px')};
233
- background-color: ${token('elevation.surface', '#FFF')};
234
- border-radius: 50%;
231
+ top: ${token('space.negative.250', '-20px')};
232
+ left: ${token('space.negative.100', '-8px')};
233
+ background-color: transparent;
235
234
  border: none;
236
235
  padding: 0;
237
236
  outline: none;
@@ -239,24 +238,38 @@ export const dragCornerControlButton = (props: ThemeProps) => css`
239
238
 
240
239
  &.active .${ClassName.DRAG_CORNER_BUTTON_INNER} {
241
240
  background-color: ${token('color.border.selected', '#0C66E4')};
242
- border-color: ${token('color.border.selected', '#0C66E4')};
241
+ width: 10px;
242
+ height: 10px;
243
+ border-width: 2px;
244
+ border-radius: 4px;
245
+ top: ${token('space.075', '6px')};
246
+ left: ${token('space.075', '6px')};
243
247
  }
244
248
 
245
249
  &:hover {
246
250
  cursor: pointer;
247
251
 
248
252
  .${ClassName.DRAG_CORNER_BUTTON_INNER} {
249
- border-color: ${token('color.border.selected', '#0C66E4')};
253
+ width: 10px;
254
+ height: 10px;
255
+ border-width: 2px;
256
+ border-radius: 4px;
257
+ top: ${token('space.075', '6px')};
258
+ left: ${token('space.075', '6px')};
250
259
  }
251
260
  }
252
261
  }
253
262
 
254
263
  .${ClassName.DRAG_CORNER_BUTTON_INNER} {
255
- border: 1px solid
256
- ${token('color.background.accent.gray.subtler', '#DCDFE4')};
257
- border-radius: 50%;
258
- width: 6px;
259
- height: 6px;
264
+ border: 1px solid ${token('color.border.inverse', '#FFF')};
265
+ background-color: ${token(
266
+ 'color.background.accent.gray.subtler',
267
+ '#DCDFE4',
268
+ )};
269
+ border-radius: 2px;
270
+ width: 5px;
271
+ height: 5px;
272
+ position: relative;
260
273
  }
261
274
  `;
262
275