@elementor/editor-canvas 4.2.0-913 → 4.2.0-914
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/dist/index.js +197 -87
- package/dist/index.mjs +178 -68
- package/package.json +18 -18
- package/src/components/grid-outline/__tests__/grid-outline-overlay.test.tsx +18 -1
- package/src/components/grid-outline/__tests__/grid-outline.test.tsx +106 -83
- package/src/components/grid-outline/{grid-outline-cell.tsx → cell.tsx} +2 -3
- package/src/components/grid-outline/grid-outline.tsx +44 -13
- package/src/components/grid-outline/line.tsx +26 -0
- package/src/hooks/__tests__/use-grid-children.test.ts +158 -0
- package/src/hooks/__tests__/use-grid-tracks.test.ts +58 -4
- package/src/hooks/use-grid-children.ts +48 -0
- package/src/hooks/use-grid-tracks.ts +3 -1
- package/src/utils/__tests__/grid-outline-utils.test.ts +72 -1
- package/src/utils/grid-outline-utils.ts +48 -0
|
@@ -36,97 +36,120 @@ describe( '<GridOutline />', () => {
|
|
|
36
36
|
expect( svg ).toHaveAttribute( 'height', '200' );
|
|
37
37
|
} );
|
|
38
38
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
const { container } = renderWithTheme(
|
|
53
|
-
<GridOutline
|
|
54
|
-
tracks={ makeTracks( {
|
|
55
|
-
columns: [ 100, 100, 100 ],
|
|
56
|
-
rows: [ 80, 80 ],
|
|
57
|
-
columnGap: 10,
|
|
58
|
-
rowGap: 8,
|
|
59
|
-
} ) }
|
|
60
|
-
width={ 320 }
|
|
61
|
-
height={ 176 }
|
|
62
|
-
/>
|
|
63
|
-
);
|
|
64
|
-
|
|
65
|
-
const rects = Array.from( container.querySelectorAll( 'rect' ) );
|
|
66
|
-
expect( rects ).toHaveLength( 3 * 2 );
|
|
67
|
-
|
|
68
|
-
const xs = rects.map( ( rect ) => rect.getAttribute( 'x' ) );
|
|
69
|
-
expect( xs ).toContain( '0.5' );
|
|
70
|
-
expect( xs ).toContain( '110.5' );
|
|
71
|
-
expect( xs ).toContain( '220.5' );
|
|
72
|
-
} );
|
|
39
|
+
describe( 'no gap', () => {
|
|
40
|
+
it( 'draws one line per unique boundary for an N×M grid', () => {
|
|
41
|
+
const { container } = renderWithTheme(
|
|
42
|
+
<GridOutline
|
|
43
|
+
tracks={ makeTracks( { columns: [ 100, 100, 100 ], rows: [ 80, 80 ] } ) }
|
|
44
|
+
width={ 300 }
|
|
45
|
+
height={ 160 }
|
|
46
|
+
/>
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
expect( container.querySelectorAll( 'line' ) ).toHaveLength( 4 + 3 );
|
|
50
|
+
expect( container.querySelectorAll( 'rect' ) ).toHaveLength( 0 );
|
|
51
|
+
} );
|
|
73
52
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
53
|
+
it( 'snaps line coordinates to half pixels for crisp 1px strokes', () => {
|
|
54
|
+
const { container } = renderWithTheme(
|
|
55
|
+
<GridOutline
|
|
56
|
+
tracks={ makeTracks( {
|
|
57
|
+
columns: [ 100, 100 ],
|
|
58
|
+
rows: [ 80 ],
|
|
59
|
+
padding: { top: 10, right: 10, bottom: 10, left: 10 },
|
|
60
|
+
} ) }
|
|
61
|
+
width={ 220 }
|
|
62
|
+
height={ 100 }
|
|
63
|
+
/>
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
const verticalXs = Array.from( container.querySelectorAll( 'line' ) )
|
|
67
|
+
.filter( ( line ) => line.getAttribute( 'x1' ) === line.getAttribute( 'x2' ) )
|
|
68
|
+
.map( ( line ) => line.getAttribute( 'x1' ) );
|
|
69
|
+
expect( verticalXs ).toEqual( [ '10.5', '110.5', '210.5' ] );
|
|
70
|
+
} );
|
|
86
71
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
72
|
+
it( 'passes the resolved iframe border color through to each line', () => {
|
|
73
|
+
const { container } = renderWithTheme(
|
|
74
|
+
<GridOutline
|
|
75
|
+
tracks={ makeTracks( { columns: [ 100 ], rows: [ 100 ], borderColor: '#abcdef' } ) }
|
|
76
|
+
width={ 100 }
|
|
77
|
+
height={ 100 }
|
|
78
|
+
/>
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
const lines = container.querySelectorAll( 'line' );
|
|
82
|
+
expect( lines.length ).toBeGreaterThan( 0 );
|
|
83
|
+
lines.forEach( ( line ) => {
|
|
84
|
+
expect( line ).toHaveAttribute( 'stroke', '#abcdef' );
|
|
85
|
+
} );
|
|
86
|
+
} );
|
|
90
87
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
rows: [ 50, 50 ],
|
|
96
|
-
padding: { top: 8, right: 12, bottom: 6, left: 4 },
|
|
97
|
-
} ) }
|
|
98
|
-
width={ 300 }
|
|
99
|
-
height={ 120 }
|
|
100
|
-
/>
|
|
101
|
-
);
|
|
88
|
+
it( 'renders nothing when there are no tracks on either axis', () => {
|
|
89
|
+
const { container } = renderWithTheme(
|
|
90
|
+
<GridOutline tracks={ makeTracks() } width={ 100 } height={ 100 } />
|
|
91
|
+
);
|
|
102
92
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
expect( rect ).toHaveAttribute( 'x', '4.5' );
|
|
107
|
-
expect( rect ).toHaveAttribute( 'width', '284' );
|
|
108
|
-
}
|
|
93
|
+
expect( container.querySelectorAll( 'line' ) ).toHaveLength( 0 );
|
|
94
|
+
expect( container.querySelectorAll( 'rect' ) ).toHaveLength( 0 );
|
|
95
|
+
} );
|
|
109
96
|
} );
|
|
110
97
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
98
|
+
describe( 'with a gap', () => {
|
|
99
|
+
it( 'draws one rect per cell so each cell has its own framed perimeter', () => {
|
|
100
|
+
const { container } = renderWithTheme(
|
|
101
|
+
<GridOutline
|
|
102
|
+
tracks={ makeTracks( {
|
|
103
|
+
columns: [ 100, 100, 100 ],
|
|
104
|
+
rows: [ 80, 80 ],
|
|
105
|
+
columnGap: 10,
|
|
106
|
+
rowGap: 8,
|
|
107
|
+
} ) }
|
|
108
|
+
width={ 320 }
|
|
109
|
+
height={ 176 }
|
|
110
|
+
/>
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
expect( container.querySelectorAll( 'rect' ) ).toHaveLength( 3 * 2 );
|
|
114
|
+
expect( container.querySelectorAll( 'line' ) ).toHaveLength( 0 );
|
|
124
115
|
} );
|
|
125
|
-
} );
|
|
126
116
|
|
|
127
|
-
|
|
128
|
-
|
|
117
|
+
it( 'offsets cells past the gap', () => {
|
|
118
|
+
const { container } = renderWithTheme(
|
|
119
|
+
<GridOutline
|
|
120
|
+
tracks={ makeTracks( {
|
|
121
|
+
columns: [ 100, 100, 100 ],
|
|
122
|
+
rows: [ 80 ],
|
|
123
|
+
columnGap: 10,
|
|
124
|
+
} ) }
|
|
125
|
+
width={ 320 }
|
|
126
|
+
height={ 80 }
|
|
127
|
+
/>
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
const xs = Array.from( container.querySelectorAll( 'rect' ) ).map( ( rect ) => rect.getAttribute( 'x' ) );
|
|
131
|
+
expect( xs ).toEqual( [ '0.5', '110.5', '220.5' ] );
|
|
132
|
+
} );
|
|
129
133
|
|
|
130
|
-
|
|
134
|
+
it( 'passes the resolved iframe border color through to each cell', () => {
|
|
135
|
+
const { container } = renderWithTheme(
|
|
136
|
+
<GridOutline
|
|
137
|
+
tracks={ makeTracks( {
|
|
138
|
+
columns: [ 100, 100 ],
|
|
139
|
+
rows: [ 100 ],
|
|
140
|
+
columnGap: 10,
|
|
141
|
+
borderColor: '#abcdef',
|
|
142
|
+
} ) }
|
|
143
|
+
width={ 210 }
|
|
144
|
+
height={ 100 }
|
|
145
|
+
/>
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
const rects = container.querySelectorAll( 'rect' );
|
|
149
|
+
expect( rects.length ).toBeGreaterThan( 0 );
|
|
150
|
+
rects.forEach( ( rect ) => {
|
|
151
|
+
expect( rect ).toHaveAttribute( 'stroke', '#abcdef' );
|
|
152
|
+
} );
|
|
153
|
+
} );
|
|
131
154
|
} );
|
|
132
155
|
} );
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import * as React from 'react';
|
|
2
2
|
|
|
3
3
|
const FALLBACK_COLOR = 'rgba(0, 0, 0, 0.12)';
|
|
4
|
-
export const DASH = '2 2';
|
|
5
4
|
|
|
6
5
|
type Props = {
|
|
7
6
|
x: number;
|
|
@@ -11,7 +10,7 @@ type Props = {
|
|
|
11
10
|
color?: string;
|
|
12
11
|
};
|
|
13
12
|
|
|
14
|
-
export function
|
|
13
|
+
export function Cell( { x, y, width, height, color }: Props ) {
|
|
15
14
|
return (
|
|
16
15
|
<rect
|
|
17
16
|
x={ x }
|
|
@@ -21,7 +20,7 @@ export function GridOutlineCell( { x, y, width, height, color }: Props ) {
|
|
|
21
20
|
fill="none"
|
|
22
21
|
stroke={ color || FALLBACK_COLOR }
|
|
23
22
|
strokeWidth={ 1 }
|
|
24
|
-
strokeDasharray=
|
|
23
|
+
strokeDasharray="2 2"
|
|
25
24
|
vectorEffect="non-scaling-stroke"
|
|
26
25
|
/>
|
|
27
26
|
);
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import * as React from 'react';
|
|
2
2
|
|
|
3
3
|
import { type GridTracks } from '../../hooks/use-grid-tracks';
|
|
4
|
-
import { computeCellRects, snapToHalfPixel } from '../../utils/grid-outline-utils';
|
|
5
|
-
import {
|
|
4
|
+
import { computeCellRects, computeGridLines, snapToHalfPixel } from '../../utils/grid-outline-utils';
|
|
5
|
+
import { Cell } from './cell';
|
|
6
|
+
import { Line } from './line';
|
|
6
7
|
|
|
7
8
|
type Props = {
|
|
8
9
|
tracks: GridTracks;
|
|
@@ -10,8 +11,47 @@ type Props = {
|
|
|
10
11
|
height: number;
|
|
11
12
|
};
|
|
12
13
|
|
|
14
|
+
const renderCells = ( tracks: GridTracks, width: number, height: number ) =>
|
|
15
|
+
computeCellRects( tracks, width, height ).map( ( cell, i ) => (
|
|
16
|
+
<Cell
|
|
17
|
+
key={ i }
|
|
18
|
+
x={ snapToHalfPixel( cell.x ) }
|
|
19
|
+
y={ snapToHalfPixel( cell.y ) }
|
|
20
|
+
width={ Math.round( cell.width ) }
|
|
21
|
+
height={ Math.round( cell.height ) }
|
|
22
|
+
color={ tracks.borderColor }
|
|
23
|
+
/>
|
|
24
|
+
) );
|
|
25
|
+
|
|
26
|
+
const renderLines = ( tracks: GridTracks, width: number, height: number ) => {
|
|
27
|
+
const { vertical, horizontal } = computeGridLines( tracks, width, height );
|
|
28
|
+
|
|
29
|
+
return [
|
|
30
|
+
...vertical.map( ( line, i ) => (
|
|
31
|
+
<Line
|
|
32
|
+
key={ `v${ i }` }
|
|
33
|
+
x1={ snapToHalfPixel( line.x1 ) }
|
|
34
|
+
y1={ Math.round( line.y1 ) }
|
|
35
|
+
x2={ snapToHalfPixel( line.x2 ) }
|
|
36
|
+
y2={ Math.round( line.y2 ) }
|
|
37
|
+
color={ tracks.borderColor }
|
|
38
|
+
/>
|
|
39
|
+
) ),
|
|
40
|
+
...horizontal.map( ( line, i ) => (
|
|
41
|
+
<Line
|
|
42
|
+
key={ `h${ i }` }
|
|
43
|
+
x1={ Math.round( line.x1 ) }
|
|
44
|
+
y1={ snapToHalfPixel( line.y1 ) }
|
|
45
|
+
x2={ Math.round( line.x2 ) }
|
|
46
|
+
y2={ snapToHalfPixel( line.y2 ) }
|
|
47
|
+
color={ tracks.borderColor }
|
|
48
|
+
/>
|
|
49
|
+
) ),
|
|
50
|
+
];
|
|
51
|
+
};
|
|
52
|
+
|
|
13
53
|
export function GridOutline( { tracks, width, height }: Props ) {
|
|
14
|
-
const
|
|
54
|
+
const hasGap = tracks.columnGap > 0 || tracks.rowGap > 0;
|
|
15
55
|
|
|
16
56
|
return (
|
|
17
57
|
<svg
|
|
@@ -20,16 +60,7 @@ export function GridOutline( { tracks, width, height }: Props ) {
|
|
|
20
60
|
style={ { position: 'absolute', inset: 0, overflow: 'visible' } }
|
|
21
61
|
xmlns="http://www.w3.org/2000/svg"
|
|
22
62
|
>
|
|
23
|
-
{
|
|
24
|
-
<GridOutlineCell
|
|
25
|
-
key={ i }
|
|
26
|
-
x={ snapToHalfPixel( cell.x ) }
|
|
27
|
-
y={ snapToHalfPixel( cell.y ) }
|
|
28
|
-
width={ Math.round( cell.width ) }
|
|
29
|
-
height={ Math.round( cell.height ) }
|
|
30
|
-
color={ tracks.borderColor }
|
|
31
|
-
/>
|
|
32
|
-
) ) }
|
|
63
|
+
{ hasGap ? renderCells( tracks, width, height ) : renderLines( tracks, width, height ) }
|
|
33
64
|
</svg>
|
|
34
65
|
);
|
|
35
66
|
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
|
|
3
|
+
const FALLBACK_COLOR = 'rgba(0, 0, 0, 0.12)';
|
|
4
|
+
|
|
5
|
+
type Props = {
|
|
6
|
+
x1: number;
|
|
7
|
+
y1: number;
|
|
8
|
+
x2: number;
|
|
9
|
+
y2: number;
|
|
10
|
+
color?: string;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export function Line( { x1, y1, x2, y2, color }: Props ) {
|
|
14
|
+
return (
|
|
15
|
+
<line
|
|
16
|
+
x1={ x1 }
|
|
17
|
+
y1={ y1 }
|
|
18
|
+
x2={ x2 }
|
|
19
|
+
y2={ y2 }
|
|
20
|
+
stroke={ color || FALLBACK_COLOR }
|
|
21
|
+
strokeWidth={ 1 }
|
|
22
|
+
strokeDasharray="2 2"
|
|
23
|
+
vectorEffect="non-scaling-stroke"
|
|
24
|
+
/>
|
|
25
|
+
);
|
|
26
|
+
}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/* eslint-disable testing-library/no-node-access */
|
|
2
|
+
import { act, renderHook } from '@testing-library/react';
|
|
3
|
+
|
|
4
|
+
import { useGridChildren } from '../use-grid-children';
|
|
5
|
+
|
|
6
|
+
type ResizeCallback = ( entries: unknown[] ) => void;
|
|
7
|
+
|
|
8
|
+
class MockResizeObserver {
|
|
9
|
+
static instances: MockResizeObserver[] = [];
|
|
10
|
+
callback: ResizeCallback;
|
|
11
|
+
observed: Element[] = [];
|
|
12
|
+
disconnected = false;
|
|
13
|
+
|
|
14
|
+
constructor( callback: ResizeCallback ) {
|
|
15
|
+
this.callback = callback;
|
|
16
|
+
MockResizeObserver.instances.push( this );
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
observe( element: Element ) {
|
|
20
|
+
this.observed.push( element );
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
unobserve( element: Element ) {
|
|
24
|
+
this.observed = this.observed.filter( ( item ) => item !== element );
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
disconnect() {
|
|
28
|
+
this.disconnected = true;
|
|
29
|
+
this.observed = [];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
trigger() {
|
|
33
|
+
this.callback( [] );
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function createGridWithChildren( count: number ): HTMLElement {
|
|
38
|
+
const grid = document.createElement( 'div' );
|
|
39
|
+
for ( let i = 0; i < count; i++ ) {
|
|
40
|
+
grid.appendChild( document.createElement( 'div' ) );
|
|
41
|
+
}
|
|
42
|
+
document.body.appendChild( grid );
|
|
43
|
+
return grid;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
describe( 'useGridChildren', () => {
|
|
47
|
+
const originalResizeObserver = globalThis.ResizeObserver;
|
|
48
|
+
|
|
49
|
+
beforeEach( () => {
|
|
50
|
+
MockResizeObserver.instances = [];
|
|
51
|
+
globalThis.ResizeObserver = MockResizeObserver as unknown as typeof ResizeObserver;
|
|
52
|
+
} );
|
|
53
|
+
|
|
54
|
+
afterEach( () => {
|
|
55
|
+
globalThis.ResizeObserver = originalResizeObserver;
|
|
56
|
+
document.body.innerHTML = '';
|
|
57
|
+
} );
|
|
58
|
+
|
|
59
|
+
it( 'returns 0 for a null element', () => {
|
|
60
|
+
const { result } = renderHook( () => useGridChildren( null ) );
|
|
61
|
+
|
|
62
|
+
expect( result.current ).toBe( 0 );
|
|
63
|
+
expect( MockResizeObserver.instances ).toHaveLength( 0 );
|
|
64
|
+
} );
|
|
65
|
+
|
|
66
|
+
it( 'observes existing children on mount', () => {
|
|
67
|
+
const grid = createGridWithChildren( 2 );
|
|
68
|
+
|
|
69
|
+
renderHook( () => useGridChildren( grid ) );
|
|
70
|
+
|
|
71
|
+
expect( MockResizeObserver.instances ).toHaveLength( 1 );
|
|
72
|
+
expect( MockResizeObserver.instances[ 0 ].observed ).toHaveLength( 2 );
|
|
73
|
+
} );
|
|
74
|
+
|
|
75
|
+
it( 'increments when a child is appended', async () => {
|
|
76
|
+
const grid = createGridWithChildren( 1 );
|
|
77
|
+
|
|
78
|
+
const { result } = renderHook( () => useGridChildren( grid ) );
|
|
79
|
+
const before = result.current;
|
|
80
|
+
|
|
81
|
+
await act( async () => {
|
|
82
|
+
grid.appendChild( document.createElement( 'div' ) );
|
|
83
|
+
await Promise.resolve();
|
|
84
|
+
} );
|
|
85
|
+
|
|
86
|
+
expect( result.current ).toBeGreaterThan( before );
|
|
87
|
+
expect( MockResizeObserver.instances[ 0 ].observed ).toHaveLength( 2 );
|
|
88
|
+
} );
|
|
89
|
+
|
|
90
|
+
it( 'increments when a child is removed', async () => {
|
|
91
|
+
const grid = createGridWithChildren( 2 );
|
|
92
|
+
|
|
93
|
+
const { result } = renderHook( () => useGridChildren( grid ) );
|
|
94
|
+
const before = result.current;
|
|
95
|
+
|
|
96
|
+
await act( async () => {
|
|
97
|
+
grid.removeChild( grid.children[ 0 ] );
|
|
98
|
+
await Promise.resolve();
|
|
99
|
+
} );
|
|
100
|
+
|
|
101
|
+
expect( result.current ).toBeGreaterThan( before );
|
|
102
|
+
expect( MockResizeObserver.instances[ 0 ].observed ).toHaveLength( 1 );
|
|
103
|
+
} );
|
|
104
|
+
|
|
105
|
+
it( 'increments when children are reordered', async () => {
|
|
106
|
+
const grid = createGridWithChildren( 3 );
|
|
107
|
+
|
|
108
|
+
const { result } = renderHook( () => useGridChildren( grid ) );
|
|
109
|
+
const before = result.current;
|
|
110
|
+
|
|
111
|
+
await act( async () => {
|
|
112
|
+
grid.insertBefore( grid.children[ 2 ], grid.children[ 0 ] );
|
|
113
|
+
await Promise.resolve();
|
|
114
|
+
} );
|
|
115
|
+
|
|
116
|
+
expect( result.current ).toBeGreaterThan( before );
|
|
117
|
+
} );
|
|
118
|
+
|
|
119
|
+
it( 'increments when a child resizes', () => {
|
|
120
|
+
const grid = createGridWithChildren( 1 );
|
|
121
|
+
|
|
122
|
+
const { result } = renderHook( () => useGridChildren( grid ) );
|
|
123
|
+
const before = result.current;
|
|
124
|
+
|
|
125
|
+
act( () => {
|
|
126
|
+
MockResizeObserver.instances[ 0 ].trigger();
|
|
127
|
+
} );
|
|
128
|
+
|
|
129
|
+
expect( result.current ).toBeGreaterThan( before );
|
|
130
|
+
} );
|
|
131
|
+
|
|
132
|
+
it( 'disconnects observers on unmount', () => {
|
|
133
|
+
const grid = createGridWithChildren( 2 );
|
|
134
|
+
|
|
135
|
+
const { unmount } = renderHook( () => useGridChildren( grid ) );
|
|
136
|
+
|
|
137
|
+
unmount();
|
|
138
|
+
|
|
139
|
+
expect( MockResizeObserver.instances[ 0 ].disconnected ).toBe( true );
|
|
140
|
+
} );
|
|
141
|
+
|
|
142
|
+
it( 'switches observation when the element changes', () => {
|
|
143
|
+
const first = createGridWithChildren( 1 );
|
|
144
|
+
const second = createGridWithChildren( 2 );
|
|
145
|
+
|
|
146
|
+
const { rerender } = renderHook( ( { element } ) => useGridChildren( element ), {
|
|
147
|
+
initialProps: { element: first as HTMLElement | null },
|
|
148
|
+
} );
|
|
149
|
+
|
|
150
|
+
const initialObserver = MockResizeObserver.instances[ 0 ];
|
|
151
|
+
|
|
152
|
+
rerender( { element: second } );
|
|
153
|
+
|
|
154
|
+
expect( initialObserver.disconnected ).toBe( true );
|
|
155
|
+
expect( MockResizeObserver.instances ).toHaveLength( 2 );
|
|
156
|
+
expect( MockResizeObserver.instances[ 1 ].observed ).toHaveLength( 2 );
|
|
157
|
+
} );
|
|
158
|
+
} );
|
|
@@ -45,8 +45,9 @@ function mockElement( style: Partial< Style > = {} ) {
|
|
|
45
45
|
getPropertyValue: ( name: string ) => ( name === '--e-a-border-color-bold' ? resolved[ name ] : '' ),
|
|
46
46
|
} ) );
|
|
47
47
|
|
|
48
|
-
const element =
|
|
49
|
-
|
|
48
|
+
const element = document.createElement( 'div' );
|
|
49
|
+
Object.defineProperty( element, 'ownerDocument', {
|
|
50
|
+
value: {
|
|
50
51
|
defaultView: {
|
|
51
52
|
getComputedStyle,
|
|
52
53
|
requestAnimationFrame: ( cb: FrameRequestCallback ) => {
|
|
@@ -56,7 +57,8 @@ function mockElement( style: Partial< Style > = {} ) {
|
|
|
56
57
|
cancelAnimationFrame: jest.fn(),
|
|
57
58
|
} as unknown as Window,
|
|
58
59
|
},
|
|
59
|
-
|
|
60
|
+
configurable: true,
|
|
61
|
+
} );
|
|
60
62
|
|
|
61
63
|
return { element, resolved, getComputedStyle };
|
|
62
64
|
}
|
|
@@ -78,7 +80,11 @@ describe( 'useGridTracks', () => {
|
|
|
78
80
|
} );
|
|
79
81
|
|
|
80
82
|
it( 'returns the empty snapshot when the element has no owner window', () => {
|
|
81
|
-
const element =
|
|
83
|
+
const element = document.createElement( 'div' );
|
|
84
|
+
Object.defineProperty( element, 'ownerDocument', {
|
|
85
|
+
value: { defaultView: null },
|
|
86
|
+
configurable: true,
|
|
87
|
+
} );
|
|
82
88
|
|
|
83
89
|
const { result } = renderHook( () => useGridTracks( element, RECT ) );
|
|
84
90
|
|
|
@@ -154,6 +160,54 @@ describe( 'useGridTracks', () => {
|
|
|
154
160
|
expect( result.current.columns ).toEqual( [ 100, 100, 100, 100 ] );
|
|
155
161
|
} );
|
|
156
162
|
|
|
163
|
+
it( 'recomputes when children are added to the grid', async () => {
|
|
164
|
+
const grid = document.createElement( 'div' );
|
|
165
|
+
grid.appendChild( document.createElement( 'div' ) );
|
|
166
|
+
document.body.appendChild( grid );
|
|
167
|
+
|
|
168
|
+
const resolved: Style = { ...DEFAULT_STYLE, gridTemplateRows: '80px' };
|
|
169
|
+
const getComputedStyle = jest.fn().mockImplementation( () => ( {
|
|
170
|
+
gridTemplateColumns: resolved.gridTemplateColumns,
|
|
171
|
+
gridTemplateRows: resolved.gridTemplateRows,
|
|
172
|
+
columnGap: resolved.columnGap,
|
|
173
|
+
rowGap: resolved.rowGap,
|
|
174
|
+
paddingTop: resolved.paddingTop,
|
|
175
|
+
paddingRight: resolved.paddingRight,
|
|
176
|
+
paddingBottom: resolved.paddingBottom,
|
|
177
|
+
paddingLeft: resolved.paddingLeft,
|
|
178
|
+
getPropertyValue: ( name: string ) => ( name === '--e-a-border-color-bold' ? resolved[ name ] : '' ),
|
|
179
|
+
} ) );
|
|
180
|
+
|
|
181
|
+
Object.defineProperty( grid, 'ownerDocument', {
|
|
182
|
+
value: {
|
|
183
|
+
defaultView: {
|
|
184
|
+
getComputedStyle,
|
|
185
|
+
requestAnimationFrame: ( cb: FrameRequestCallback ) => {
|
|
186
|
+
cb( 0 );
|
|
187
|
+
return 1;
|
|
188
|
+
},
|
|
189
|
+
cancelAnimationFrame: jest.fn(),
|
|
190
|
+
},
|
|
191
|
+
},
|
|
192
|
+
configurable: true,
|
|
193
|
+
} );
|
|
194
|
+
|
|
195
|
+
const { result } = renderHook( () => useGridTracks( grid, RECT ) );
|
|
196
|
+
|
|
197
|
+
expect( result.current.rows ).toEqual( [ 80 ] );
|
|
198
|
+
|
|
199
|
+
resolved.gridTemplateRows = '80px 80px';
|
|
200
|
+
|
|
201
|
+
await act( async () => {
|
|
202
|
+
grid.appendChild( document.createElement( 'div' ) );
|
|
203
|
+
await Promise.resolve();
|
|
204
|
+
} );
|
|
205
|
+
|
|
206
|
+
expect( result.current.rows ).toEqual( [ 80, 80 ] );
|
|
207
|
+
|
|
208
|
+
document.body.removeChild( grid );
|
|
209
|
+
} );
|
|
210
|
+
|
|
157
211
|
it( 'recomputes when the device mode changes', () => {
|
|
158
212
|
const mock = mockElement( { gridTemplateRows: '80px', rowGap: '8px' } );
|
|
159
213
|
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { useEffect, useState } from 'react';
|
|
2
|
+
|
|
3
|
+
export function useGridChildren( element: HTMLElement | null ): number {
|
|
4
|
+
const [ signal, setSignal ] = useState( 0 );
|
|
5
|
+
|
|
6
|
+
useEffect( () => {
|
|
7
|
+
if ( ! element ) {
|
|
8
|
+
return;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const bump = () => setSignal( ( previous ) => previous + 1 );
|
|
12
|
+
|
|
13
|
+
const resizeObserver = new ResizeObserver( bump );
|
|
14
|
+
const observed = new Set< Element >();
|
|
15
|
+
|
|
16
|
+
const syncChildren = () => {
|
|
17
|
+
for ( const child of Array.from( element.children ) ) {
|
|
18
|
+
if ( ! observed.has( child ) ) {
|
|
19
|
+
resizeObserver.observe( child );
|
|
20
|
+
observed.add( child );
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
for ( const child of observed ) {
|
|
25
|
+
if ( child.parentElement !== element ) {
|
|
26
|
+
resizeObserver.unobserve( child );
|
|
27
|
+
observed.delete( child );
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
syncChildren();
|
|
33
|
+
|
|
34
|
+
const mutationObserver = new MutationObserver( () => {
|
|
35
|
+
syncChildren();
|
|
36
|
+
bump();
|
|
37
|
+
} );
|
|
38
|
+
mutationObserver.observe( element, { childList: true } );
|
|
39
|
+
|
|
40
|
+
return () => {
|
|
41
|
+
mutationObserver.disconnect();
|
|
42
|
+
resizeObserver.disconnect();
|
|
43
|
+
observed.clear();
|
|
44
|
+
};
|
|
45
|
+
}, [ element ] );
|
|
46
|
+
|
|
47
|
+
return signal;
|
|
48
|
+
}
|
|
@@ -3,6 +3,7 @@ import { ELEMENT_STYLE_CHANGE_EVENT } from '@elementor/editor-elements';
|
|
|
3
3
|
import { __privateUseListenTo as useListenTo, windowEvent } from '@elementor/editor-v1-adapters';
|
|
4
4
|
|
|
5
5
|
import { toGridTracks } from '../utils/grid-outline-utils';
|
|
6
|
+
import { useGridChildren } from './use-grid-children';
|
|
6
7
|
|
|
7
8
|
export type GridTracks = {
|
|
8
9
|
columns: number[];
|
|
@@ -31,6 +32,7 @@ export function useGridTracks( element: HTMLElement | null, rect: DOMRect ): Gri
|
|
|
31
32
|
[ windowEvent( ELEMENT_STYLE_CHANGE_EVENT ), windowEvent( DEVICE_MODE_CHANGE_EVENT ) ],
|
|
32
33
|
() => ( {} )
|
|
33
34
|
);
|
|
35
|
+
const childrenTrigger = useGridChildren( element );
|
|
34
36
|
|
|
35
37
|
useEffect( () => {
|
|
36
38
|
const previewWindow = element?.ownerDocument?.defaultView;
|
|
@@ -47,7 +49,7 @@ export function useGridTracks( element: HTMLElement | null, rect: DOMRect ): Gri
|
|
|
47
49
|
return () => {
|
|
48
50
|
previewWindow.cancelAnimationFrame( frame );
|
|
49
51
|
};
|
|
50
|
-
}, [ element, rect.width, rect.height, trigger ] );
|
|
52
|
+
}, [ element, rect.width, rect.height, trigger, childrenTrigger ] );
|
|
51
53
|
|
|
52
54
|
return tracks;
|
|
53
55
|
}
|