Skip to content

Commit 2a93e4d

Browse files
committed
[FIX] Spreadsheet: Make zoom work on Safari
The new zoom feature relies on the zoom css property, however there is a long standing bug in webkit (https://bugs.webkit.org/show_bug.cgi?id=77998) that impacts the computation of `Element.getBoundingClientRect`. This revision adds a check on safari browser to correct the boundingRect at specific points (pretty much where we handle mouseEvents). Task: 5232092
1 parent 2894172 commit 2a93e4d

File tree

9 files changed

+66
-27
lines changed

9 files changed

+66
-27
lines changed

packages/o-spreadsheet-engine/src/types/rendering.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,11 @@ import { GridIcon } from "../registries/icons_on_cell_registry";
33
import { ImageSVG } from "./image";
44
import { Alias, Align, BorderDescr, Color, DataBarFill, Pixel, Style, Zone } from "./misc";
55

6+
export interface DOMRectPosition {
7+
top: Pixel;
8+
left: Pixel;
9+
}
10+
611
/**
712
* Coordinate in pixels
813
*/

src/components/helpers/dom_helpers.ts

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { Ref } from "@odoo/o-spreadsheet-engine/types/misc";
2-
import { Rect } from "@odoo/o-spreadsheet-engine/types/rendering";
2+
import { DOMRectPosition, Rect } from "@odoo/o-spreadsheet-engine/types/rendering";
33

44
const macRegex = /Mac/i;
55

@@ -14,12 +14,27 @@ export function isChildEvent(parent: HTMLElement | null | undefined, ev: Event):
1414
return !!ev.target && parent!.contains(ev.target as Node);
1515
}
1616

17-
export function gridOverlayPosition() {
17+
export function gridOverlayPosition(zoom = 1): DOMRectPosition {
1818
const spreadsheetElement = document.querySelector(".o-grid-overlay");
19-
if (spreadsheetElement) {
20-
return spreadsheetElement.getBoundingClientRect();
19+
const result = spreadsheetElement && zoomCorrectedElementPosition(spreadsheetElement, zoom);
20+
if (!result) {
21+
throw new Error("Can't find spreadsheet position");
2122
}
22-
throw new Error("Can't find spreadsheet position");
23+
return result;
24+
}
25+
26+
export function zoomCorrectedElementPosition(
27+
el: Element,
28+
zoomLevel: number
29+
): DOMRectPosition | null {
30+
const targetEl = el.classList.contains("o-zoomable") ? el : el.closest(".o-zoomable");
31+
if (!targetEl) return null;
32+
const rect = targetEl!.getBoundingClientRect();
33+
const zoom = isBrowserSafari() ? zoomLevel : 1;
34+
return {
35+
top: rect.top * zoom,
36+
left: rect.left * zoom,
37+
};
2338
}
2439

2540
export function getRefBoundingRect(ref: Ref<HTMLElement>): Rect {
@@ -218,6 +233,10 @@ export function isBrowserFirefox() {
218233
return /Firefox/i.test(navigator.userAgent);
219234
}
220235

236+
export function isBrowserSafari() {
237+
return /Safari/i.test(navigator.userAgent);
238+
}
239+
221240
// Mobile detection
222241
function maxTouchPoints() {
223242
return navigator.maxTouchPoints || 1;

src/components/helpers/drag_and_drop_grid_hook.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,8 @@ export function useDragAndDropBeyondTheViewport(env: SpreadsheetChildEnv) {
4545
}
4646

4747
const sheetId = getters.getActiveSheetId();
48-
const position = gridOverlayPosition();
48+
const zoomLevel = env.model.getters.getViewportZoomLevel();
49+
const position = gridOverlayPosition(zoomLevel);
4950
const zoomedMouseEvent = withZoom(env, currentEv, position);
5051
const { x: offsetCorrectionX, y: offsetCorrectionY } = getters.getMainViewportCoordinates();
5152
const { top, left, bottom, right } = getters.getActiveMainViewport();
@@ -145,7 +146,8 @@ export function useDragAndDropBeyondTheViewport(env: SpreadsheetChildEnv) {
145146
startScrollDirection: DnDDirection = "all"
146147
) => {
147148
cleanUp();
148-
const position = gridOverlayPosition();
149+
const zoomLevel = env.model.getters.getViewportZoomLevel();
150+
const position = gridOverlayPosition(zoomLevel);
149151
scrollDirection = startScrollDirection;
150152
startingX = initialPointerCoordinates.clientX - position.left;
151153
startingY = initialPointerCoordinates.clientY - position.top;

src/components/helpers/zoom.ts

Lines changed: 11 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { SpreadsheetChildEnv } from "@odoo/o-spreadsheet-engine/types/spreadsheet_env";
2-
import { Pixel, Rect } from "../..";
2+
import { DOMRectPosition, Pixel, Rect } from "../..";
3+
import { zoomCorrectedElementPosition } from "./dom_helpers";
34

45
export type ZoomedMouseEvent<T extends MouseEvent | PointerEvent> = {
56
clientX: Pixel;
@@ -13,21 +14,21 @@ export type ZoomedMouseEvent<T extends MouseEvent | PointerEvent> = {
1314
* Return a POJO containing the original event as well as the client position and the client offset
1415
* where the event would target if the spreadsheet was not zoomed
1516
* @param ev unzoomed mouse event
16-
* @param originalTargetRect The original target bounding rect the resulting ZoomedMouseEvent offset must refer to
17+
* @param originalTargetPosition The original target bounding rect the resulting ZoomedMouseEvent offset must refer to
1718
* @returns a ZoomedMouseEvent
1819
*/
1920
export function withZoom<T extends MouseEvent>(
2021
env: SpreadsheetChildEnv,
2122
ev: T,
22-
originalTargetRect?: DOMRect | null
23+
originalTargetPosition?: DOMRectPosition | null
2324
): ZoomedMouseEvent<T> {
2425
const zoomLevel = env.model.getters.getViewportZoomLevel();
25-
if (originalTargetRect === undefined) {
26-
originalTargetRect = getZoomTargetBoundingRect(ev);
26+
if (originalTargetPosition === undefined) {
27+
originalTargetPosition = getZoomTargetPosition(ev, zoomLevel);
2728
}
28-
if (!originalTargetRect) return withNoZoom(ev);
29-
const baseOffsetX = ev.clientX - originalTargetRect.left;
30-
const baseOffsetY = ev.clientY - originalTargetRect.top;
29+
if (!originalTargetPosition) return withNoZoom(ev);
30+
const baseOffsetX = ev.clientX - originalTargetPosition.left;
31+
const baseOffsetY = ev.clientY - originalTargetPosition.top;
3132
const offsetX = baseOffsetX / zoomLevel;
3233
const offsetY = baseOffsetY / zoomLevel;
3334
return {
@@ -64,12 +65,10 @@ export function getZoomedRect(zoom: number, rect: Rect): Rect {
6465
/**
6566
* Returns the bounding rect of the closest or self element who is targetable by a ZoomedMouseEvent
6667
*/
67-
function getZoomTargetBoundingRect(ev: MouseEvent): DOMRect | null {
68+
function getZoomTargetPosition(ev: MouseEvent, zoom: number): DOMRectPosition | null {
6869
const target = ev.target;
6970
if (!target || !("classList" in target) || !(target instanceof Element)) {
7071
return null;
7172
}
72-
const targetEl = target.classList.contains("o-zoomable") ? target : target.closest(".o-zoomable");
73-
if (!targetEl) return null;
74-
return targetEl.getBoundingClientRect();
73+
return zoomCorrectedElementPosition(target, zoom);
7574
}

src/components/highlight/highlight/highlight.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -117,9 +117,11 @@ export class Highlight extends Component<HighlightProps, SpreadsheetChildEnv> {
117117
onMoveHighlight(ev: PointerEvent) {
118118
this.highlightState.shiftingMode = "isMoving";
119119
const z = this.props.range.zone;
120-
const zoomedMouseEvent = withZoom(this.env, ev);
121120

122-
const position = gridOverlayPosition();
121+
const zoomLevel = this.env.model.getters.getViewportZoomLevel();
122+
const position = gridOverlayPosition(zoomLevel);
123+
const zoomedMouseEvent = withZoom(this.env, ev, position);
124+
123125
const activeSheetId = this.env.model.getters.getActiveSheetId();
124126

125127
const initCol = this.env.model.getters.getColIndex(zoomedMouseEvent.clientX - position.left);

src/components/spreadsheet/spreadsheet.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -291,7 +291,8 @@ export class Spreadsheet extends Component<SpreadsheetProps, SpreadsheetChildEnv
291291
?.getBoundingClientRect().height || 0;
292292

293293
const gridWidth =
294-
this.spreadsheetRef.el?.querySelector(".o-grid")?.getBoundingClientRect().width || 0;
294+
(this.spreadsheetRef.el?.getBoundingClientRect().width || 0) -
295+
(this.spreadsheetRef.el?.querySelector(".o-sidePanel")?.getBoundingClientRect().width || 0);
295296
const gridHeight =
296297
(this.spreadsheetRef.el?.getBoundingClientRect().height || 0) -
297298
(this.spreadsheetRef.el?.querySelector(".o-column-groups")?.getBoundingClientRect().height ||

tests/figures/figure_component.test.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import {
22
DEFAULT_CELL_HEIGHT,
33
DEFAULT_CELL_WIDTH,
44
FIGURE_BORDER_WIDTH,
5+
HEADER_HEIGHT,
6+
HEADER_WIDTH,
57
MENU_WIDTH,
68
ZOOM_VALUES,
79
} from "@odoo/o-spreadsheet-engine/constants";
@@ -155,7 +157,12 @@ beforeEach(() => {
155157
describe("figures", () => {
156158
beforeEach(async () => {
157159
notifyUser = jest.fn();
158-
mockSpreadsheetRect = { top: 100, left: 200, height: 1000, width: 1000 };
160+
mockSpreadsheetRect = {
161+
top: 100,
162+
left: 200,
163+
height: 1000 + HEADER_HEIGHT,
164+
width: 1000 + HEADER_WIDTH,
165+
};
159166
mockFigureMenuItemRect = { top: 500, left: 500 };
160167
({ model, parent, fixture, env } = await mountSpreadsheet(undefined, { notifyUser }));
161168
sheetId = model.getters.getActiveSheetId();

tests/spreadsheet/__snapshots__/spreadsheet_component.test.ts.snap

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -872,8 +872,8 @@ exports[`Simple Spreadsheet Component simple rendering snapshot 1`] = `
872872
873873
<canvas
874874
height="985"
875-
style="width:1033px;height:985px;zoom:1"
876-
width="1033"
875+
style="width:985px;height:985px;zoom:1"
876+
width="985"
877877
/>
878878
879879
@@ -1844,8 +1844,8 @@ exports[`components take the small screen into account 1`] = `
18441844
18451845
<canvas
18461846
height="985"
1847-
style="width:1033px;height:985px;zoom:1"
1848-
width="1033"
1847+
style="width:485px;height:985px;zoom:1"
1848+
width="485"
18491849
/>
18501850
18511851

tests/spreadsheet/spreadsheet_component.test.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,9 @@ beforeEach(() => {
6767
"o-spreadsheet": () => {
6868
return { x: 0, y: 0, width: spreadsheetWidth, height: 1000 };
6969
},
70+
".o-grid": () => {
71+
return { x: 0, y: 0, width: spreadsheetWidth, height: 1000 };
72+
},
7073
});
7174
});
7275

@@ -493,6 +496,7 @@ test("*isSmall* is properly recomputed when changing window size", async () => {
493496

494497
test("components take the small screen into account", async () => {
495498
const model = new Model();
499+
spreadsheetWidth = 500;
496500
const { fixture } = await mountSpreadsheet({ model }, { isSmall: true });
497501
expect(fixture.querySelector(".o-spreadsheet")).toMatchSnapshot();
498502
expect(fixture.querySelector(".o-spreadsheet-mobile")).not.toBeNull();

0 commit comments

Comments
 (0)