@atlaskit/editor-plugin-table 1.6.2 → 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.
- package/CHANGELOG.md +6 -0
- package/dist/cjs/plugins/table/index.js +2 -1
- package/dist/cjs/plugins/table/pm-plugins/sticky-headers/nodeviews/tableRow.js +3 -1
- package/dist/cjs/plugins/table/types.js +1 -0
- package/dist/cjs/plugins/table/ui/FloatingContextualButton/FixedButton.js +133 -0
- package/dist/cjs/plugins/table/ui/FloatingContextualButton/index.js +73 -128
- package/dist/cjs/version.json +1 -1
- package/dist/es2019/plugins/table/index.js +2 -1
- package/dist/es2019/plugins/table/pm-plugins/sticky-headers/nodeviews/tableRow.js +3 -1
- package/dist/es2019/plugins/table/types.js +1 -0
- package/dist/es2019/plugins/table/ui/FloatingContextualButton/FixedButton.js +120 -0
- package/dist/es2019/plugins/table/ui/FloatingContextualButton/index.js +76 -108
- package/dist/es2019/version.json +1 -1
- package/dist/esm/plugins/table/index.js +2 -1
- package/dist/esm/plugins/table/pm-plugins/sticky-headers/nodeviews/tableRow.js +3 -1
- package/dist/esm/plugins/table/types.js +1 -0
- package/dist/esm/plugins/table/ui/FloatingContextualButton/FixedButton.js +118 -0
- package/dist/esm/plugins/table/ui/FloatingContextualButton/index.js +73 -129
- package/dist/esm/version.json +1 -1
- package/dist/types/plugins/table/types.d.ts +1 -0
- package/dist/types/plugins/table/ui/FloatingContextualButton/FixedButton.d.ts +23 -0
- package/dist/types/plugins/table/ui/FloatingContextualButton/index.d.ts +1 -9
- package/dist/types-ts4.5/plugins/table/types.d.ts +1 -0
- package/dist/types-ts4.5/plugins/table/ui/FloatingContextualButton/FixedButton.d.ts +23 -0
- package/dist/types-ts4.5/plugins/table/ui/FloatingContextualButton/index.d.ts +1 -9
- package/package.json +4 -3
- package/src/__tests__/playwright/__fixtures__/base-adfs.ts +1486 -0
- package/src/__tests__/playwright/extensions.spec.ts +67 -0
- package/src/__tests__/unit/ui/FixedButton.tsx +214 -0
- package/src/plugins/table/index.tsx +1 -0
- package/src/plugins/table/pm-plugins/sticky-headers/nodeviews/tableRow.ts +2 -1
- package/src/plugins/table/types.ts +1 -0
- package/src/plugins/table/ui/FloatingContextualButton/FixedButton.tsx +175 -0
- 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
|
+
});
|
|
@@ -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
|
+
});
|
|
@@ -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;
|
|
@@ -8,30 +8,19 @@ import { WrappedComponentProps, injectIntl } from 'react-intl-next';
|
|
|
8
8
|
|
|
9
9
|
import { TableLayout } from '@atlaskit/adf-schema';
|
|
10
10
|
import { Popup } from '@atlaskit/editor-common/ui';
|
|
11
|
-
import {
|
|
12
|
-
akEditorFloatingOverlapPanelZIndex,
|
|
13
|
-
akEditorSmallZIndex,
|
|
14
|
-
} from '@atlaskit/editor-shared-styles';
|
|
11
|
+
import { akEditorSmallZIndex } from '@atlaskit/editor-shared-styles';
|
|
15
12
|
import ExpandIcon from '@atlaskit/icon/glyph/chevron-down';
|
|
16
13
|
|
|
17
14
|
import { ToolbarButton } from '@atlaskit/editor-common/ui-menu';
|
|
18
15
|
|
|
19
|
-
import { closestElement } from '@atlaskit/editor-common/utils';
|
|
20
16
|
import { toggleContextualMenu } from '../../commands';
|
|
21
17
|
import { RowStickyState } from '../../pm-plugins/sticky-headers';
|
|
22
18
|
import { TableCssClassName as ClassName } from '../../types';
|
|
23
19
|
import messages from '../../ui/messages';
|
|
20
|
+
import FixedButton from './FixedButton';
|
|
24
21
|
|
|
25
|
-
import {
|
|
26
|
-
|
|
27
|
-
AnalyticsEventPayload,
|
|
28
|
-
CONTENT_COMPONENT,
|
|
29
|
-
} from '@atlaskit/editor-common/analytics';
|
|
30
|
-
import {
|
|
31
|
-
ACTION,
|
|
32
|
-
ACTION_SUBJECT,
|
|
33
|
-
EVENT_TYPE,
|
|
34
|
-
} from '@atlaskit/editor-common/analytics';
|
|
22
|
+
import { DispatchAnalyticsEvent } from '@atlaskit/editor-common/analytics';
|
|
23
|
+
import { ACTION_SUBJECT } from '@atlaskit/editor-common/analytics';
|
|
35
24
|
import {
|
|
36
25
|
tableFloatingCellButtonStyles,
|
|
37
26
|
tableFloatingCellButtonSelectedStyles,
|
|
@@ -42,6 +31,7 @@ import { ThemeProps } from '@atlaskit/theme/types';
|
|
|
42
31
|
|
|
43
32
|
export interface Props {
|
|
44
33
|
editorView: EditorView;
|
|
34
|
+
tableWrapper?: HTMLElement;
|
|
45
35
|
tableNode?: PMNode;
|
|
46
36
|
targetCellPosition: number;
|
|
47
37
|
isContextualMenuOpen?: boolean;
|
|
@@ -54,58 +44,41 @@ export interface Props {
|
|
|
54
44
|
dispatchAnalyticsEvent?: DispatchAnalyticsEvent;
|
|
55
45
|
}
|
|
56
46
|
|
|
57
|
-
|
|
58
|
-
Props & WrappedComponentProps,
|
|
59
|
-
any
|
|
60
|
-
> {
|
|
61
|
-
static displayName = 'FloatingContextualButton';
|
|
47
|
+
const BUTTON_OFFSET = 3;
|
|
62
48
|
|
|
63
|
-
|
|
49
|
+
const FloatingContextualButtonInner = React.memo(
|
|
50
|
+
(props: Props & WrappedComponentProps) => {
|
|
64
51
|
const {
|
|
52
|
+
editorView,
|
|
53
|
+
isContextualMenuOpen,
|
|
65
54
|
mountPoint,
|
|
66
55
|
scrollableElement,
|
|
67
|
-
|
|
56
|
+
stickyHeader,
|
|
57
|
+
tableWrapper,
|
|
68
58
|
targetCellPosition,
|
|
69
|
-
isContextualMenuOpen,
|
|
70
59
|
intl: { formatMessage },
|
|
71
|
-
|
|
72
|
-
|
|
60
|
+
} = props; // : Props & WrappedComponentProps
|
|
61
|
+
|
|
62
|
+
const handleClick = () => {
|
|
63
|
+
const { state, dispatch } = editorView;
|
|
64
|
+
// Clicking outside the dropdown handles toggling the menu closed
|
|
65
|
+
// (otherwise these two toggles combat each other).
|
|
66
|
+
// In the event a user clicks the chevron button again
|
|
67
|
+
// That will count as clicking outside the dropdown and
|
|
68
|
+
// will be toggled appropriately
|
|
69
|
+
if (!isContextualMenuOpen) {
|
|
70
|
+
toggleContextualMenu()(state, dispatch);
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
|
|
73
74
|
const domAtPos = editorView.domAtPos.bind(editorView);
|
|
74
75
|
let targetCellRef: Node | undefined;
|
|
75
|
-
|
|
76
|
-
targetCellRef = findDomRefAtPos(targetCellPosition, domAtPos);
|
|
77
|
-
} catch (error) {
|
|
78
|
-
// eslint-disable-next-line no-console
|
|
79
|
-
console.warn(error);
|
|
80
|
-
if (dispatchAnalyticsEvent) {
|
|
81
|
-
const payload: AnalyticsEventPayload = {
|
|
82
|
-
action: ACTION.ERRORED,
|
|
83
|
-
actionSubject: ACTION_SUBJECT.CONTENT_COMPONENT,
|
|
84
|
-
eventType: EVENT_TYPE.OPERATIONAL,
|
|
85
|
-
attributes: {
|
|
86
|
-
component: CONTENT_COMPONENT.FLOATING_CONTEXTUAL_BUTTON,
|
|
87
|
-
selection: editorView.state.selection.toJSON(),
|
|
88
|
-
position: targetCellPosition,
|
|
89
|
-
docSize: editorView.state.doc.nodeSize,
|
|
90
|
-
error: error instanceof Error ? error.message : String(error),
|
|
91
|
-
},
|
|
92
|
-
nonPrivacySafeAttributes: {
|
|
93
|
-
errorStack: error instanceof Error ? error.stack : undefined,
|
|
94
|
-
},
|
|
95
|
-
};
|
|
96
|
-
dispatchAnalyticsEvent(payload);
|
|
97
|
-
}
|
|
98
|
-
}
|
|
76
|
+
targetCellRef = findDomRefAtPos(targetCellPosition, domAtPos);
|
|
99
77
|
|
|
100
78
|
if (!targetCellRef || !(targetCellRef instanceof HTMLElement)) {
|
|
101
79
|
return null;
|
|
102
80
|
}
|
|
103
81
|
|
|
104
|
-
const tableWrapper = closestElement(
|
|
105
|
-
targetCellRef,
|
|
106
|
-
`.${ClassName.TABLE_NODE_WRAPPER}`,
|
|
107
|
-
);
|
|
108
|
-
|
|
109
82
|
const labelCellOptions = formatMessage(messages.cellOptions);
|
|
110
83
|
|
|
111
84
|
const button = (
|
|
@@ -120,7 +93,7 @@ export class FloatingContextualButtonInner extends React.Component<
|
|
|
120
93
|
className={ClassName.CONTEXTUAL_MENU_BUTTON}
|
|
121
94
|
selected={isContextualMenuOpen}
|
|
122
95
|
title={labelCellOptions}
|
|
123
|
-
onClick={
|
|
96
|
+
onClick={handleClick}
|
|
124
97
|
iconBefore={<ExpandIcon label="" />}
|
|
125
98
|
aria-label={labelCellOptions}
|
|
126
99
|
/>
|
|
@@ -130,24 +103,19 @@ export class FloatingContextualButtonInner extends React.Component<
|
|
|
130
103
|
const parentSticky =
|
|
131
104
|
targetCellRef.parentElement &&
|
|
132
105
|
targetCellRef.parentElement.className.indexOf('sticky') > -1;
|
|
133
|
-
if (
|
|
134
|
-
const pos = targetCellRef.getBoundingClientRect();
|
|
135
|
-
|
|
106
|
+
if (stickyHeader && parentSticky && tableWrapper) {
|
|
136
107
|
return (
|
|
137
|
-
<
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
zIndex: akEditorFloatingOverlapPanelZIndex,
|
|
146
|
-
left: pos.left + targetCellRef.clientWidth - 20 - 3,
|
|
147
|
-
}}
|
|
108
|
+
<FixedButton
|
|
109
|
+
offset={BUTTON_OFFSET}
|
|
110
|
+
stickyHeader={stickyHeader}
|
|
111
|
+
tableWrapper={tableWrapper}
|
|
112
|
+
targetCellPosition={targetCellPosition}
|
|
113
|
+
targetCellRef={targetCellRef}
|
|
114
|
+
mountTo={tableWrapper}
|
|
115
|
+
isContextualMenuOpen={isContextualMenuOpen}
|
|
148
116
|
>
|
|
149
117
|
{button}
|
|
150
|
-
</
|
|
118
|
+
</FixedButton>
|
|
151
119
|
);
|
|
152
120
|
}
|
|
153
121
|
|
|
@@ -159,7 +127,7 @@ export class FloatingContextualButtonInner extends React.Component<
|
|
|
159
127
|
mountTo={tableWrapper || mountPoint}
|
|
160
128
|
boundariesElement={targetCellRef}
|
|
161
129
|
scrollableElement={scrollableElement}
|
|
162
|
-
offset={[
|
|
130
|
+
offset={[BUTTON_OFFSET, -BUTTON_OFFSET]}
|
|
163
131
|
forcePlacement
|
|
164
132
|
allowOutOfBounds
|
|
165
133
|
zIndex={akEditorSmallZIndex}
|
|
@@ -167,30 +135,8 @@ export class FloatingContextualButtonInner extends React.Component<
|
|
|
167
135
|
{button}
|
|
168
136
|
</Popup>
|
|
169
137
|
);
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
shouldComponentUpdate(nextProps: Props) {
|
|
173
|
-
return (
|
|
174
|
-
this.props.tableNode !== nextProps.tableNode ||
|
|
175
|
-
this.props.targetCellPosition !== nextProps.targetCellPosition ||
|
|
176
|
-
this.props.layout !== nextProps.layout ||
|
|
177
|
-
this.props.isContextualMenuOpen !== nextProps.isContextualMenuOpen ||
|
|
178
|
-
this.props.isNumberColumnEnabled !== nextProps.isNumberColumnEnabled ||
|
|
179
|
-
this.props.stickyHeader !== nextProps.stickyHeader
|
|
180
|
-
);
|
|
181
|
-
}
|
|
182
|
-
private handleClick = () => {
|
|
183
|
-
const { state, dispatch } = this.props.editorView;
|
|
184
|
-
// Clicking outside the dropdown handles toggling the menu closed
|
|
185
|
-
// (otherwise these two toggles combat each other).
|
|
186
|
-
// In the event a user clicks the chevron button again
|
|
187
|
-
// That will count as clicking outside the dropdown and
|
|
188
|
-
// will be toggled appropriately
|
|
189
|
-
if (!this.props.isContextualMenuOpen) {
|
|
190
|
-
toggleContextualMenu()(state, dispatch);
|
|
191
|
-
}
|
|
192
|
-
};
|
|
193
|
-
}
|
|
138
|
+
},
|
|
139
|
+
);
|
|
194
140
|
|
|
195
141
|
const FloatingContextualButton = injectIntl(FloatingContextualButtonInner);
|
|
196
142
|
|