@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.
Files changed (34) hide show
  1. package/CHANGELOG.md +6 -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 +4 -3
  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/ui/FixedButton.tsx +214 -0
  30. package/src/plugins/table/index.tsx +1 -0
  31. package/src/plugins/table/pm-plugins/sticky-headers/nodeviews/tableRow.ts +2 -1
  32. package/src/plugins/table/types.ts +1 -0
  33. package/src/plugins/table/ui/FloatingContextualButton/FixedButton.tsx +175 -0
  34. 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
+ });
@@ -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;
@@ -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
- DispatchAnalyticsEvent,
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
- export class FloatingContextualButtonInner extends React.Component<
58
- Props & WrappedComponentProps,
59
- any
60
- > {
61
- static displayName = 'FloatingContextualButton';
47
+ const BUTTON_OFFSET = 3;
62
48
 
63
- render() {
49
+ const FloatingContextualButtonInner = React.memo(
50
+ (props: Props & WrappedComponentProps) => {
64
51
  const {
52
+ editorView,
53
+ isContextualMenuOpen,
65
54
  mountPoint,
66
55
  scrollableElement,
67
- editorView,
56
+ stickyHeader,
57
+ tableWrapper,
68
58
  targetCellPosition,
69
- isContextualMenuOpen,
70
59
  intl: { formatMessage },
71
- dispatchAnalyticsEvent,
72
- } = this.props; // : Props & WrappedComponentProps
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
- try {
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={this.handleClick}
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 (this.props.stickyHeader && parentSticky) {
134
- const pos = targetCellRef.getBoundingClientRect();
135
-
106
+ if (stickyHeader && parentSticky && tableWrapper) {
136
107
  return (
137
- <div
138
- style={{
139
- position: 'fixed',
140
- top:
141
- this.props.stickyHeader.top +
142
- this.props.stickyHeader.padding +
143
- 3 +
144
- 3,
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
- </div>
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={[3, -3]}
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