@a-type/ui 2.1.6 → 2.1.8

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.
@@ -1,6 +1,13 @@
1
1
  import { debounce } from '@a-type/utils';
2
2
  import clsx from 'clsx';
3
- import { CSSProperties, ReactNode, useEffect, useRef, useState } from 'react';
3
+ import {
4
+ CSSProperties,
5
+ ReactNode,
6
+ useEffect,
7
+ useLayoutEffect,
8
+ useRef,
9
+ useState,
10
+ } from 'react';
4
11
 
5
12
  interface Layout {
6
13
  attach(container: HTMLElement): () => void;
@@ -12,70 +19,19 @@ interface MasonryLayoutConfig {
12
19
  gap: number;
13
20
  }
14
21
 
15
- class MasonryLayout implements Layout {
16
- private containerResizeObserver: ResizeObserver | null = null;
17
- private containerMutationObserver: MutationObserver | null = null;
18
- private childSizeObserver: ResizeObserver;
19
- private childMutationObserver: MutationObserver;
20
-
21
- private container: HTMLElement | null = null;
22
-
23
- private columns: number = 0;
22
+ abstract class MasonryLayout implements Layout {
23
+ protected container: HTMLElement | null = null;
24
+ protected columns: number = 0;
24
25
 
25
26
  constructor(private config: MasonryLayoutConfig) {
26
27
  this.columns =
27
28
  typeof config.columns === 'function' ? config.columns(0) : config.columns;
28
- this.childSizeObserver = new ResizeObserver(this.handleChildResize);
29
- this.childMutationObserver = new MutationObserver(this.relayout);
30
29
  this.relayout();
31
30
  }
32
31
 
33
- attach = (container: HTMLElement) => {
34
- this.containerResizeObserver?.disconnect();
35
- this.containerMutationObserver?.disconnect();
36
-
37
- this.container = container;
38
-
39
- this.containerResizeObserver = new ResizeObserver(
40
- this.handleContainerResize,
41
- );
42
- this.containerMutationObserver = new MutationObserver(
43
- this.handleContainerMutation,
44
- );
45
- this.containerResizeObserver.observe(container);
46
- this.containerMutationObserver.observe(container, { childList: true });
47
-
48
- container.style.setProperty('position', 'relative');
49
- container.style.setProperty('overflow', 'hidden');
50
- container.style.setProperty('visibility', 'visible');
51
- container.childNodes.forEach((node) => {
52
- if (node instanceof HTMLElement) {
53
- this.setupChild(node);
54
- }
55
- });
56
-
57
- this.updateFromContainerSize(container.offsetWidth);
58
-
59
- this.relayout();
32
+ abstract attach(container: HTMLElement): () => void;
60
33
 
61
- return () => {
62
- this.containerResizeObserver?.disconnect();
63
- this.containerMutationObserver?.disconnect();
64
- container.style.removeProperty('position');
65
- container.style.removeProperty('overflow');
66
- this.container = null;
67
- };
68
- };
69
-
70
- private setupChild = (child: HTMLElement) => {
71
- child.style.setProperty('position', 'absolute');
72
- // hide until laid out
73
- child.style.setProperty('visibility', 'hidden');
74
- this.childSizeObserver.observe(child);
75
- this.childMutationObserver.observe(child, {
76
- attributeFilter: ['data-span'],
77
- });
78
- };
34
+ abstract setupChild(child: HTMLElement): void;
79
35
 
80
36
  updateConfig = (config: MasonryLayoutConfig) => {
81
37
  const gapChanged = config.gap !== this.config.gap;
@@ -89,12 +45,7 @@ class MasonryLayout implements Layout {
89
45
  }
90
46
  };
91
47
 
92
- private handleContainerResize = (entries: ResizeObserverEntry[]) => {
93
- const containerWidth = entries[0].contentRect.width;
94
- this.updateFromContainerSize(containerWidth);
95
- };
96
-
97
- private updateFromContainerSize = (containerWidth: number) => {
48
+ protected updateFromContainerSize = (containerWidth: number) => {
98
49
  if (typeof this.config.columns === 'function') {
99
50
  const newValue = this.config.columns(containerWidth);
100
51
  if (newValue !== this.columns) {
@@ -106,35 +57,7 @@ class MasonryLayout implements Layout {
106
57
  return false;
107
58
  };
108
59
 
109
- private handleContainerMutation = (entries: MutationRecord[]) => {
110
- for (const entry of entries) {
111
- entry.addedNodes.forEach((node) => {
112
- if (node instanceof HTMLElement) {
113
- this.setupChild(node);
114
- }
115
- });
116
- entry.removedNodes.forEach((node) => {
117
- if (node instanceof HTMLElement) {
118
- this.childSizeObserver?.unobserve(node);
119
- }
120
- });
121
- }
122
- this.relayout();
123
- };
124
-
125
- private handleChildResize = (entries: ResizeObserverEntry[]) => {
126
- // only worry about height changes
127
- for (const entry of entries) {
128
- const lastSeenHeight = entry.target.getAttribute('data-last-height');
129
- const currentHeight = entry.contentRect.height;
130
- entry.target.setAttribute('data-last-height', currentHeight.toString());
131
- if (lastSeenHeight && lastSeenHeight !== currentHeight.toString()) {
132
- this.relayout();
133
- }
134
- }
135
- };
136
-
137
- private relayout = debounce(() => {
60
+ protected relayout = debounce(() => {
138
61
  if (!this.container) {
139
62
  return;
140
63
  }
@@ -187,11 +110,115 @@ class MasonryLayout implements Layout {
187
110
  }, 100);
188
111
  }
189
112
 
190
- class ServerLayout implements Layout {
191
- attach(container: HTMLElement): () => void {
113
+ class ClientLayout extends MasonryLayout {
114
+ private containerResizeObserver: ResizeObserver | null = null;
115
+ private containerMutationObserver: MutationObserver | null = null;
116
+ private childSizeObserver: ResizeObserver;
117
+ private childMutationObserver: MutationObserver;
118
+
119
+ constructor(config: MasonryLayoutConfig) {
120
+ super(config);
121
+ this.childSizeObserver = new ResizeObserver(this.handleChildResize);
122
+ this.childMutationObserver = new MutationObserver(this.relayout);
123
+ }
124
+
125
+ private handleContainerMutation = (entries: MutationRecord[]) => {
126
+ for (const entry of entries) {
127
+ entry.addedNodes.forEach((node) => {
128
+ if (node instanceof HTMLElement) {
129
+ this.setupChild(node);
130
+ }
131
+ });
132
+ entry.removedNodes.forEach((node) => {
133
+ if (node instanceof HTMLElement) {
134
+ this.childSizeObserver?.unobserve(node);
135
+ }
136
+ });
137
+ }
138
+ this.relayout();
139
+ };
140
+
141
+ private handleChildResize = (entries: ResizeObserverEntry[]) => {
142
+ // only worry about height changes
143
+ for (const entry of entries) {
144
+ const lastSeenHeight = entry.target.getAttribute('data-last-height');
145
+ const currentHeight = entry.contentRect.height;
146
+ entry.target.setAttribute('data-last-height', currentHeight.toString());
147
+ if (lastSeenHeight && lastSeenHeight !== currentHeight.toString()) {
148
+ this.relayout();
149
+ }
150
+ }
151
+ };
152
+
153
+ attach = (container: HTMLElement) => {
154
+ this.containerResizeObserver?.disconnect();
155
+ this.containerMutationObserver?.disconnect();
156
+
157
+ this.container = container;
158
+
159
+ this.containerResizeObserver = new ResizeObserver(
160
+ this.handleContainerResize,
161
+ );
162
+ this.containerMutationObserver = new MutationObserver(
163
+ this.handleContainerMutation,
164
+ );
165
+ this.containerResizeObserver.observe(container);
166
+ this.containerMutationObserver.observe(container, { childList: true });
167
+
168
+ container.style.setProperty('position', 'relative');
169
+ container.style.setProperty('overflow', 'hidden');
170
+ container.style.setProperty('visibility', 'visible');
171
+ container.childNodes.forEach((node) => {
172
+ if (node instanceof HTMLElement) {
173
+ this.setupChild(node);
174
+ }
175
+ });
176
+
177
+ this.updateFromContainerSize(container.offsetWidth);
178
+
179
+ return () => {
180
+ this.containerResizeObserver?.disconnect();
181
+ this.containerMutationObserver?.disconnect();
182
+ container.style.removeProperty('position');
183
+ container.style.removeProperty('overflow');
184
+ this.container = null;
185
+ };
186
+ };
187
+
188
+ setupChild = (child: HTMLElement) => {
189
+ child.style.setProperty('position', 'absolute');
190
+ // hide until laid out
191
+ child.style.setProperty('visibility', 'hidden');
192
+ this.childSizeObserver.observe(child);
193
+ this.childMutationObserver.observe(child, {
194
+ attributeFilter: ['data-span'],
195
+ });
196
+ };
197
+
198
+ private handleContainerResize = (entries: ResizeObserverEntry[]) => {
199
+ const containerWidth = entries[0].contentRect.width;
200
+ this.updateFromContainerSize(containerWidth);
201
+ };
202
+ }
203
+
204
+ class ServerLayout extends MasonryLayout {
205
+ attach = (container: HTMLElement): (() => void) => {
206
+ this.container = container;
207
+ this.container.style.setProperty('position', 'relative');
208
+ this.container.style.setProperty('overflow', 'hidden');
209
+ this.container.style.setProperty('visibility', 'visible');
210
+ container.childNodes.forEach((node) => {
211
+ if (node instanceof HTMLElement) {
212
+ this.setupChild(node);
213
+ }
214
+ });
215
+ this.updateFromContainerSize(container.offsetWidth);
192
216
  return () => {};
217
+ };
218
+ setupChild(child: HTMLElement): void {
219
+ child.style.setProperty('position', 'absolute');
220
+ child.style.setProperty('visibility', 'visible');
193
221
  }
194
- updateConfig(config: MasonryLayoutConfig): void {}
195
222
  }
196
223
 
197
224
  function pickTrack(tracks: number[], trackSpan: number) {
@@ -226,15 +253,15 @@ export function Masonry({
226
253
  }: MasonryProps) {
227
254
  const [layout] = useState<Layout>(() => {
228
255
  if (typeof window === 'undefined') {
229
- return new ServerLayout();
256
+ return new ServerLayout({ columns, gap });
230
257
  }
231
- return new MasonryLayout({ columns, gap });
258
+ return new ClientLayout({ columns, gap });
232
259
  });
233
- useEffect(() => {
260
+ useIsomorphicLayoutEffect(() => {
234
261
  layout.updateConfig({ columns, gap });
235
262
  }, [layout, columns, gap]);
236
263
  const ref = useRef<HTMLDivElement>(null);
237
- useEffect(() => {
264
+ useIsomorphicLayoutEffect(() => {
238
265
  if (ref.current) {
239
266
  return layout.attach(ref.current);
240
267
  }
@@ -254,3 +281,12 @@ export function Masonry({
254
281
  export function masonrySpan(span: number) {
255
282
  return { 'data-span': span };
256
283
  }
284
+
285
+ function useIsomorphicLayoutEffect(
286
+ effect: () => void | (() => void),
287
+ deps: any[] = [],
288
+ ) {
289
+ const isBrowser = typeof window !== 'undefined';
290
+ const useIsoEffect = isBrowser ? useLayoutEffect : useEffect;
291
+ return useIsoEffect(effect, deps);
292
+ }