@atlaskit/editor-plugin-table 1.6.1 → 1.6.3

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 (35) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/cjs/plugins/table/index.js +2 -1
  3. package/dist/cjs/plugins/table/pm-plugins/sticky-headers/nodeviews/tableRow.js +3 -1
  4. package/dist/cjs/plugins/table/types.js +1 -0
  5. package/dist/cjs/plugins/table/ui/FloatingContextualButton/FixedButton.js +133 -0
  6. package/dist/cjs/plugins/table/ui/FloatingContextualButton/index.js +73 -128
  7. package/dist/cjs/version.json +1 -1
  8. package/dist/es2019/plugins/table/index.js +2 -1
  9. package/dist/es2019/plugins/table/pm-plugins/sticky-headers/nodeviews/tableRow.js +3 -1
  10. package/dist/es2019/plugins/table/types.js +1 -0
  11. package/dist/es2019/plugins/table/ui/FloatingContextualButton/FixedButton.js +120 -0
  12. package/dist/es2019/plugins/table/ui/FloatingContextualButton/index.js +76 -108
  13. package/dist/es2019/version.json +1 -1
  14. package/dist/esm/plugins/table/index.js +2 -1
  15. package/dist/esm/plugins/table/pm-plugins/sticky-headers/nodeviews/tableRow.js +3 -1
  16. package/dist/esm/plugins/table/types.js +1 -0
  17. package/dist/esm/plugins/table/ui/FloatingContextualButton/FixedButton.js +118 -0
  18. package/dist/esm/plugins/table/ui/FloatingContextualButton/index.js +73 -129
  19. package/dist/esm/version.json +1 -1
  20. package/dist/types/plugins/table/types.d.ts +1 -0
  21. package/dist/types/plugins/table/ui/FloatingContextualButton/FixedButton.d.ts +23 -0
  22. package/dist/types/plugins/table/ui/FloatingContextualButton/index.d.ts +1 -9
  23. package/dist/types-ts4.5/plugins/table/types.d.ts +1 -0
  24. package/dist/types-ts4.5/plugins/table/ui/FloatingContextualButton/FixedButton.d.ts +23 -0
  25. package/dist/types-ts4.5/plugins/table/ui/FloatingContextualButton/index.d.ts +1 -9
  26. package/package.json +5 -4
  27. package/src/__tests__/playwright/__fixtures__/base-adfs.ts +1486 -0
  28. package/src/__tests__/playwright/extensions.spec.ts +67 -0
  29. package/src/__tests__/unit/nodeviews/cell.ts +0 -14
  30. package/src/__tests__/unit/ui/FixedButton.tsx +214 -0
  31. package/src/plugins/table/index.tsx +1 -0
  32. package/src/plugins/table/pm-plugins/sticky-headers/nodeviews/tableRow.ts +2 -1
  33. package/src/plugins/table/types.ts +1 -0
  34. package/src/plugins/table/ui/FloatingContextualButton/FixedButton.tsx +175 -0
  35. package/src/plugins/table/ui/FloatingContextualButton/index.tsx +41 -95
@@ -0,0 +1,67 @@
1
+ import {
2
+ EditorTableModel,
3
+ EditorNodeContainerModel,
4
+ EditorExtensionModel,
5
+ editorTestCase as test,
6
+ expect,
7
+ } from '@af/editor-libra';
8
+ import { tableInsideLayoutAfterExtension } from './__fixtures__/base-adfs';
9
+
10
+ test.use({
11
+ editorProps: {
12
+ appearance: 'full-page',
13
+ allowTables: {
14
+ advanced: true,
15
+ },
16
+ allowLayouts: true,
17
+ allowExtension: true,
18
+ },
19
+
20
+ editorMountOptions: {
21
+ withConfluenceMacrosExtensionProvider: true,
22
+ },
23
+
24
+ adf: tableInsideLayoutAfterExtension,
25
+ });
26
+
27
+ test.describe('when the extension context panel opens', () => {
28
+ test('should not overflow the table', async ({ editor }) => {
29
+ const nodes = EditorNodeContainerModel.from(editor);
30
+ const extensionModel = EditorExtensionModel.from(nodes.bodiedExtension);
31
+ const tableModel = EditorTableModel.from(nodes.table);
32
+
33
+ await editor.selection.set({ anchor: 1, head: 1 });
34
+
35
+ await test.step('make sure the table is not overflowed already', async () => {
36
+ expect(await tableModel.hasOverflowed()).toBeFalsy();
37
+ });
38
+
39
+ await extensionModel.configuration(editor).openContextPanel();
40
+
41
+ expect(await tableModel.hasOverflowed()).toBeFalsy();
42
+ });
43
+ });
44
+
45
+ test('should not overflow the table when changing viewport', async ({
46
+ editor,
47
+ }) => {
48
+ const nodes = EditorNodeContainerModel.from(editor);
49
+ const tableModel = EditorTableModel.from(nodes.table);
50
+ await editor.page.setViewportSize({ width: 1000, height: 1024 });
51
+ await editor.selection.set({ anchor: 1, head: 1 });
52
+ await tableModel.isSelected();
53
+ const cell = await tableModel.cell(0);
54
+ await cell.click();
55
+ await cell.resize({
56
+ mouse: editor.page.mouse,
57
+ cellSide: 'right',
58
+ moveDirection: 'right',
59
+ moveDistance: 100,
60
+ });
61
+ await test.step('make sure the table is not overflowed already', async () => {
62
+ expect(await tableModel.hasOverflowed()).toBeFalsy();
63
+ });
64
+ await editor.page.setViewportSize({ width: 1050, height: 1024 });
65
+ await tableModel.isSelected();
66
+ expect(await tableModel.hasOverflowed()).toBeFalsy();
67
+ });
@@ -43,20 +43,6 @@ jest.mock('@atlaskit/editor-common/utils', () => ({
43
43
  },
44
44
  }));
45
45
 
46
- jest.mock('@atlaskit/editor-palette', () => ({
47
- ...jest.requireActual<Object>('@atlaskit/editor-palette'),
48
- hexToEditorBackgroundPaletteColorTokenName: jest.fn((hexColor: string) => {
49
- return hexColor;
50
- }),
51
- }));
52
-
53
- jest.mock('@atlaskit/tokens', () => ({
54
- ...jest.requireActual<Object>('@atlaskit/tokens'),
55
- getTokenValue: jest.fn((tokenId: string, fallback: string = '') => {
56
- return tokenId || fallback;
57
- }),
58
- }));
59
-
60
46
  describe('table -> nodeviews -> tableCell.tsx', () => {
61
47
  const TABLE_LOCAL_ID = 'test-table-local-id';
62
48
  const createEditor = createProsemirrorEditorFactory();
@@ -0,0 +1,214 @@
1
+ import React from 'react';
2
+ import { createPortal } from 'react-dom';
3
+ import { render, screen, cleanup } from '@testing-library/react';
4
+ import { akEditorFloatingPanelZIndex } from '@atlaskit/editor-shared-styles';
5
+ import FixedButton, {
6
+ BUTTON_WIDTH,
7
+ calcLeftPos,
8
+ calcObserverTargetMargin,
9
+ } from '../../../plugins/table/ui/FloatingContextualButton/FixedButton';
10
+
11
+ jest.mock('react-dom', () => ({
12
+ createPortal: jest.fn((node: React.ReactNode) => node),
13
+ }));
14
+
15
+ describe('calcLeftPos()', () => {
16
+ it('should calculate left position', () => {
17
+ let result = calcLeftPos({
18
+ buttonWidth: 0,
19
+ cellRectLeft: 0,
20
+ cellRefWidth: 0,
21
+ offset: 0,
22
+ });
23
+ expect(result).toEqual(0);
24
+ result = calcLeftPos({
25
+ buttonWidth: 20,
26
+ cellRectLeft: 400,
27
+ cellRefWidth: 250,
28
+ offset: 3,
29
+ });
30
+ expect(result).toEqual(627);
31
+ });
32
+ });
33
+
34
+ describe('calcObserverTargetMargin()', () => {
35
+ it('should calculate the margin', () => {
36
+ const mockTableWrapper = document.createElement('div');
37
+ mockTableWrapper.scrollLeft = 50;
38
+ const tableSpy = jest.spyOn(mockTableWrapper, 'getBoundingClientRect');
39
+ tableSpy.mockImplementation(() => ({ left: 800 } as DOMRect));
40
+
41
+ const mockButton = document.createElement('div');
42
+ const buttonSpy = jest.spyOn(mockButton, 'getBoundingClientRect');
43
+ buttonSpy.mockImplementation(() => ({ left: 1200 } as DOMRect));
44
+
45
+ const result = calcObserverTargetMargin(mockTableWrapper, mockButton);
46
+ expect(result).toEqual(450);
47
+ });
48
+ });
49
+
50
+ describe('<FixedButton />', () => {
51
+ const targetCellRef = document.createElement('div');
52
+ const fixedButtonRef = {
53
+ current: document.createElement('div'),
54
+ };
55
+ const observerTargetRef = {
56
+ current: document.createElement('div'),
57
+ };
58
+ const props = {
59
+ offset: 10,
60
+ stickyHeader: {
61
+ pos: 0,
62
+ top: 0,
63
+ padding: 0,
64
+ sticky: true,
65
+ },
66
+ targetCellPosition: 0,
67
+ targetCellRef,
68
+ mountTo: document.createElement('div'),
69
+ tableWrapper: document.createElement('div'),
70
+ fixedButtonRef,
71
+ observerTargetRef,
72
+ isContextualMenuOpen: false,
73
+ };
74
+ const MockChildren = () => <div data-testid="mock-children" />;
75
+
76
+ it('should render children', () => {
77
+ render(
78
+ <FixedButton {...props}>
79
+ <MockChildren />
80
+ </FixedButton>,
81
+ );
82
+ expect(screen.getByTestId('mock-children')).toBeInTheDocument();
83
+ });
84
+
85
+ it('should render using createPortal', () => {
86
+ render(
87
+ <FixedButton {...props}>
88
+ <MockChildren />
89
+ </FixedButton>,
90
+ );
91
+ expect(createPortal).toBeCalled();
92
+ });
93
+
94
+ describe('observerTargetRef', () => {
95
+ it('should have correct inital styles', () => {
96
+ const { container } = render(
97
+ <FixedButton {...props}>
98
+ <MockChildren />
99
+ </FixedButton>,
100
+ );
101
+ expect(container.firstChild).toHaveStyle(
102
+ `top: 0px; left: 0px; position: absolute; width: ${BUTTON_WIDTH}px; height: ${BUTTON_WIDTH}px`,
103
+ );
104
+ });
105
+
106
+ it('should have correct "margin-left" style', () => {
107
+ const mockTableWrapper = document.createElement('div');
108
+ mockTableWrapper.scrollLeft = 123;
109
+ const { container } = render(
110
+ <FixedButton {...props} tableWrapper={mockTableWrapper}>
111
+ <MockChildren />
112
+ </FixedButton>,
113
+ );
114
+
115
+ expect(container.firstChild).toHaveStyle(`margin-left: 123px;`);
116
+ });
117
+ });
118
+
119
+ describe('fixedButtonRef', () => {
120
+ it('should have correct "top" style', () => {
121
+ const { container, rerender } = render(
122
+ <FixedButton {...props} offset={10}>
123
+ <MockChildren />
124
+ </FixedButton>,
125
+ );
126
+ expect(container.firstChild?.firstChild).toHaveStyle(`top: 20px;`);
127
+
128
+ rerender(
129
+ <FixedButton
130
+ {...props}
131
+ stickyHeader={{ pos: 0, top: 3, padding: 0, sticky: true }}
132
+ >
133
+ <MockChildren />
134
+ </FixedButton>,
135
+ );
136
+ expect(container.firstChild?.firstChild).toHaveStyle(`top: 23px;`);
137
+
138
+ rerender(
139
+ <FixedButton
140
+ {...props}
141
+ stickyHeader={{ pos: 0, top: 0, padding: 2, sticky: true }}
142
+ >
143
+ <MockChildren />
144
+ </FixedButton>,
145
+ );
146
+ expect(container.firstChild?.firstChild).toHaveStyle(`top: 22px;`);
147
+ });
148
+
149
+ it('should have correct "left" style', () => {
150
+ const mockRef = document.createElement('div');
151
+ const mockRefSpy = jest.spyOn(mockRef, 'getBoundingClientRect');
152
+ mockRefSpy.mockImplementation(() => ({ left: 3 } as DOMRect));
153
+
154
+ const { container } = render(
155
+ <FixedButton {...props} targetCellRef={mockRef}>
156
+ <MockChildren />
157
+ </FixedButton>,
158
+ );
159
+ expect(container.firstChild?.firstChild).toHaveStyle(`left: -27px;`);
160
+ });
161
+
162
+ it('should have correct "z-index" style', () => {
163
+ const { container } = render(
164
+ <FixedButton {...props}>
165
+ <MockChildren />
166
+ </FixedButton>,
167
+ );
168
+ expect(container.firstChild?.firstChild).toHaveStyle(
169
+ `z-index: ${akEditorFloatingPanelZIndex}`,
170
+ );
171
+ });
172
+ });
173
+
174
+ describe('useEffect()', () => {
175
+ const observe = jest.fn();
176
+ const unobserve = jest.fn();
177
+
178
+ window.IntersectionObserver = jest.fn((callback) => {
179
+ callback([{ isIntersecting: true }]);
180
+ return {
181
+ observe,
182
+ unobserve,
183
+ };
184
+ }) as any;
185
+
186
+ it('should add / remove event listener to the table wrapper', () => {
187
+ const mockTableWrapper = document.createElement('div');
188
+ const addSpy = jest.spyOn(mockTableWrapper, 'addEventListener');
189
+ const removeSpy = jest.spyOn(mockTableWrapper, 'removeEventListener');
190
+
191
+ render(
192
+ <FixedButton {...props} tableWrapper={mockTableWrapper}>
193
+ <MockChildren />
194
+ </FixedButton>,
195
+ );
196
+
197
+ expect(addSpy).toBeCalled();
198
+ cleanup();
199
+ expect(removeSpy).toBeCalled();
200
+ });
201
+
202
+ it('should observe / unobserve an InterSectionObserver', () => {
203
+ render(
204
+ <FixedButton {...props}>
205
+ <MockChildren />
206
+ </FixedButton>,
207
+ );
208
+
209
+ expect(observe).toHaveBeenCalled();
210
+ cleanup();
211
+ expect(unobserve).toHaveBeenCalled();
212
+ });
213
+ });
214
+ });
@@ -387,6 +387,7 @@ const tablesPlugin: NextEditorPlugin<
387
387
  isContextualMenuOpen={isContextualMenuOpen}
388
388
  layout={layout}
389
389
  stickyHeader={stickyHeader}
390
+ tableWrapper={tableWrapperTarget}
390
391
  />
391
392
  )}
392
393
  {allowControls && (
@@ -611,7 +611,7 @@ export class TableRowNodeView implements NodeView {
611
611
  return;
612
612
  }
613
613
 
614
- const { table } = tree;
614
+ const { table, wrapper } = tree;
615
615
 
616
616
  // ED-16035 Make sure sticky header is only applied to first row
617
617
  const tbody = this.dom.parentElement;
@@ -639,6 +639,7 @@ export class TableRowNodeView implements NodeView {
639
639
 
640
640
  this.dom.style.top = `${domTop}px`;
641
641
  updateTableMargin(table);
642
+ this.dom.scrollLeft = wrapper.scrollLeft;
642
643
 
643
644
  this.emitOn(domTop, this.colControlsOffset);
644
645
  };
@@ -289,6 +289,7 @@ export const TableCssClassName = {
289
289
  CONTEXTUAL_SUBMENU: `${tablePrefixSelector}-contextual-submenu`,
290
290
  CONTEXTUAL_MENU_BUTTON_WRAP: `${tablePrefixSelector}-contextual-menu-button-wrap`,
291
291
  CONTEXTUAL_MENU_BUTTON: `${tablePrefixSelector}-contextual-menu-button`,
292
+ CONTEXTUAL_MENU_BUTTON_FIXED: `${tablePrefixSelector}-contextual-menu-button-fixed`,
292
293
  CONTEXTUAL_MENU_ICON: `${tablePrefixSelector}-contextual-submenu-icon`,
293
294
 
294
295
  // come from prosemirror-table
@@ -0,0 +1,175 @@
1
+ import React, { useEffect, useRef } from 'react';
2
+ import { createPortal } from 'react-dom';
3
+ import rafSchedule from 'raf-schd';
4
+
5
+ import { akEditorFloatingPanelZIndex } from '@atlaskit/editor-shared-styles';
6
+ import { RowStickyState } from '../../pm-plugins/sticky-headers';
7
+ import { insertColumnButtonOffset } from '../common-styles';
8
+ import { TableCssClassName as ClassName } from '../../types';
9
+
10
+ export const BUTTON_WIDTH = 20;
11
+
12
+ export interface Props {
13
+ children: React.ReactNode;
14
+ mountTo: HTMLElement;
15
+ offset: number;
16
+ stickyHeader: RowStickyState;
17
+ targetCellPosition: number;
18
+ targetCellRef: HTMLElement;
19
+ tableWrapper: HTMLElement;
20
+ isContextualMenuOpen: boolean | undefined;
21
+ }
22
+
23
+ interface CalcLeftPosData {
24
+ buttonWidth: number;
25
+ cellRectLeft: number;
26
+ cellRefWidth: number;
27
+ offset: number;
28
+ }
29
+
30
+ export const calcLeftPos = ({
31
+ buttonWidth,
32
+ cellRectLeft,
33
+ cellRefWidth,
34
+ offset,
35
+ }: CalcLeftPosData) => {
36
+ return cellRectLeft + cellRefWidth - buttonWidth - offset;
37
+ };
38
+
39
+ export const calcObserverTargetMargin = (
40
+ tableWrapper: HTMLElement,
41
+ fixedButtonRefCurrent: HTMLElement,
42
+ ) => {
43
+ const tableWrapperRect = tableWrapper.getBoundingClientRect();
44
+ const fixedButtonRect = fixedButtonRefCurrent.getBoundingClientRect();
45
+ const scrollLeft = tableWrapper.scrollLeft;
46
+ return fixedButtonRect.left - tableWrapperRect.left + scrollLeft;
47
+ };
48
+
49
+ export const FixedButton = ({
50
+ children,
51
+ isContextualMenuOpen,
52
+ mountTo,
53
+ offset,
54
+ stickyHeader,
55
+ tableWrapper,
56
+ targetCellPosition,
57
+ targetCellRef,
58
+ }: Props) => {
59
+ const fixedButtonRef = useRef<HTMLDivElement | null>(null);
60
+ const observerTargetRef = useRef<HTMLDivElement | null>(null);
61
+
62
+ // Using refs here rather than state to prevent heaps of renders on scroll
63
+ const scrollDataRef = useRef(0);
64
+ const leftPosDataRef = useRef(0);
65
+
66
+ useEffect(() => {
67
+ const observerTargetRefCurrent = observerTargetRef.current;
68
+ const fixedButtonRefCurrent = fixedButtonRef.current;
69
+
70
+ if (fixedButtonRefCurrent && observerTargetRefCurrent) {
71
+ scrollDataRef.current = tableWrapper.scrollLeft;
72
+ leftPosDataRef.current = 0;
73
+ // Hide the button initially in case there's a flash of the button being
74
+ // outside the table before the Intersection Observer fires
75
+ fixedButtonRefCurrent.style.visibility = 'hidden';
76
+
77
+ const margin = calcObserverTargetMargin(
78
+ tableWrapper,
79
+ fixedButtonRefCurrent,
80
+ );
81
+
82
+ // Much more simple and predictable to add this margin to the observer target
83
+ // rather than using it to calculate the rootMargin values
84
+ observerTargetRefCurrent.style.marginLeft = `${margin}px`;
85
+
86
+ const observer = new IntersectionObserver(
87
+ (entries) => {
88
+ entries.forEach((entry) => {
89
+ if (entry.isIntersecting) {
90
+ fixedButtonRefCurrent.style.visibility = 'visible';
91
+ } else {
92
+ fixedButtonRefCurrent.style.visibility = 'hidden';
93
+ }
94
+ });
95
+ },
96
+ {
97
+ root: tableWrapper,
98
+ rootMargin: `0px ${insertColumnButtonOffset}px 0px 0px`,
99
+ threshold: 1,
100
+ },
101
+ );
102
+
103
+ const handleScroll = rafSchedule((event) => {
104
+ if (fixedButtonRef.current) {
105
+ const delta = event.target.scrollLeft - scrollDataRef.current;
106
+ const style = `translateX(${leftPosDataRef.current - delta}px)`;
107
+ fixedButtonRef.current.style.transform = style;
108
+
109
+ scrollDataRef.current = event.target.scrollLeft;
110
+ leftPosDataRef.current = leftPosDataRef.current - delta;
111
+ }
112
+ });
113
+
114
+ observer.observe(observerTargetRefCurrent);
115
+ tableWrapper.addEventListener('scroll', handleScroll);
116
+
117
+ return () => {
118
+ tableWrapper.removeEventListener('scroll', handleScroll);
119
+ fixedButtonRefCurrent.style.transform = '';
120
+ observer.unobserve(observerTargetRefCurrent);
121
+ };
122
+ }
123
+ }, [
124
+ fixedButtonRef,
125
+ observerTargetRef,
126
+ tableWrapper,
127
+ targetCellPosition,
128
+ targetCellRef,
129
+ isContextualMenuOpen,
130
+ ]);
131
+
132
+ const targetCellRect = targetCellRef.getBoundingClientRect();
133
+
134
+ // Using a portal here to ensure wrapperRef has the tableWrapper as an
135
+ // ancestor. This is required to make the Intersection Observer work.
136
+ return createPortal(
137
+ // Using observerTargetRef here for our Intersection Observer. There is issues
138
+ // getting the observer to work just using the fixedButtonRef, possible due
139
+ // to using position fixed on this Element, or possibly due to its position
140
+ // being changed on scroll.
141
+ <div
142
+ ref={observerTargetRef}
143
+ style={{
144
+ position: 'absolute',
145
+ top: '0px',
146
+ left: '0px',
147
+ width: `${BUTTON_WIDTH}px`,
148
+ height: `${BUTTON_WIDTH}px`,
149
+ }}
150
+ >
151
+ <div
152
+ ref={fixedButtonRef}
153
+ style={{
154
+ position: 'fixed',
155
+ top: stickyHeader.top + stickyHeader.padding + offset * 2,
156
+ zIndex: akEditorFloatingPanelZIndex,
157
+ left: calcLeftPos({
158
+ buttonWidth: BUTTON_WIDTH,
159
+ cellRectLeft: targetCellRect.left,
160
+ cellRefWidth: targetCellRef.clientWidth,
161
+ offset,
162
+ }),
163
+ width: `${BUTTON_WIDTH}px`,
164
+ height: `${BUTTON_WIDTH}px`,
165
+ }}
166
+ className={ClassName.CONTEXTUAL_MENU_BUTTON_FIXED}
167
+ >
168
+ {children}
169
+ </div>
170
+ </div>,
171
+ mountTo,
172
+ );
173
+ };
174
+
175
+ export default FixedButton;