Skip to content

Commit 8082622

Browse files
committed
chore: Extracted useResolvedElement and extractSize to separate modules
1 parent f707da6 commit 8082622

File tree

7 files changed

+196
-141
lines changed

7 files changed

+196
-141
lines changed

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
"resize"
2828
],
2929
"scripts": {
30-
"build": "rollup -c && tsc && cp dist/index.d.ts polyfilled.d.ts",
30+
"build": "rollup -c && tsc && rm -rf dist/utils && cp dist/index.d.ts polyfilled.d.ts",
3131
"watch": "KARMA_BROWSERS=Chrome run-p 'src:watch' 'karma:watch'",
3232
"src:watch": "rollup -c -w",
3333
"check:size": "size-limit",
@@ -81,6 +81,7 @@
8181
"@babel/preset-typescript": "^7.9.0",
8282
"@rollup/plugin-babel": "^5.2.1",
8383
"@rollup/plugin-inject": "^4.0.1",
84+
"@rollup/plugin-node-resolve": "^13.3.0",
8485
"@semantic-release/changelog": "^5.0.1",
8586
"@semantic-release/commit-analyzer": "^8.0.1",
8687
"@semantic-release/git": "^9.0.0",

rollup.config.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
11
import babel from "@rollup/plugin-babel";
22
import inject from "@rollup/plugin-inject";
3+
import { nodeResolve } from "@rollup/plugin-node-resolve";
34

45
const getConfig = ({ polyfill = false } = {}) => {
56
const config = {
67
input: "src/index.ts",
78
output: [],
89
plugins: [
10+
nodeResolve({
11+
extensions: [".ts"],
12+
}),
913
babel({
1014
extensions: ["ts"],
1115
// Seems like there's not really a difference in case of this lib, but

src/index.ts

Lines changed: 2 additions & 128 deletions
Original file line numberDiff line numberDiff line change
@@ -7,82 +7,8 @@ import {
77
RefCallback,
88
useCallback,
99
} from "react";
10-
11-
type SubscriberCleanup = () => void;
12-
type SubscriberResponse = SubscriberCleanup | void;
13-
14-
// This of course could've been more streamlined with internal state instead of
15-
// refs, but then host hooks / components could not opt out of renders.
16-
// This could've been exported to its own module, but the current build doesn't
17-
// seem to work with module imports and I had no more time to spend on this...
18-
function useResolvedElement<T extends Element>(
19-
subscriber: (element: T) => SubscriberResponse,
20-
refOrElement?: T | RefObject<T> | null
21-
): RefCallback<T> {
22-
const callbackRefElement = useRef<T | null>(null);
23-
const lastReportRef = useRef<{
24-
reporter: () => void;
25-
element: T | null;
26-
} | null>(null);
27-
const cleanupRef = useRef<SubscriberResponse | null>();
28-
29-
// Resolving ".current" purely so that a new callSubscriber instance is created when needed.
30-
const refElement =
31-
refOrElement && "current" in refOrElement ? refOrElement.current : null;
32-
const callSubscriber = useCallback(() => {
33-
let element = null;
34-
if (callbackRefElement.current) {
35-
element = callbackRefElement.current;
36-
} else if (refOrElement) {
37-
if (refOrElement instanceof Element) {
38-
element = refOrElement;
39-
} else {
40-
element = refOrElement.current;
41-
}
42-
}
43-
44-
if (
45-
lastReportRef.current &&
46-
lastReportRef.current.element === element &&
47-
lastReportRef.current.reporter === callSubscriber
48-
) {
49-
return;
50-
}
51-
52-
if (cleanupRef.current) {
53-
cleanupRef.current();
54-
// Making sure the cleanup is not called accidentally multiple times.
55-
cleanupRef.current = null;
56-
}
57-
lastReportRef.current = {
58-
reporter: callSubscriber,
59-
element,
60-
};
61-
62-
// Only calling the subscriber, if there's an actual element to report.
63-
if (element) {
64-
cleanupRef.current = subscriber(element);
65-
}
66-
}, [refOrElement, refElement, subscriber]);
67-
68-
// On each render, we check whether a ref changed, or if we got a new raw
69-
// element.
70-
useEffect(() => {
71-
// With this we're *technically* supporting cases where ref objects' current value changes, but only if there's a
72-
// render accompanying that change as well.
73-
// To guarantee we always have the right element, one must use the ref callback provided instead, but we support
74-
// RefObjects to make the hook API more convenient in certain cases.
75-
callSubscriber();
76-
}, [callSubscriber]);
77-
78-
return useCallback<RefCallback<T>>(
79-
(element) => {
80-
callbackRefElement.current = element;
81-
callSubscriber();
82-
},
83-
[callSubscriber]
84-
);
85-
}
10+
import useResolvedElement from "./utils/useResolvedElement";
11+
import extractSize from "./utils/extractSize";
8612

8713
type ObservedSize = {
8814
width: number | undefined;
@@ -108,58 +34,6 @@ declare global {
10834
}
10935
}
11036

111-
// We're only using the first element of the size sequences, until future versions of the spec solidify on how
112-
// exactly it'll be used for fragments in multi-column scenarios:
113-
// From the spec:
114-
// > The box size properties are exposed as FrozenArray in order to support elements that have multiple fragments,
115-
// > which occur in multi-column scenarios. However the current definitions of content rect and border box do not
116-
// > mention how those boxes are affected by multi-column layout. In this spec, there will only be a single
117-
// > ResizeObserverSize returned in the FrozenArray, which will correspond to the dimensions of the first column.
118-
// > A future version of this spec will extend the returned FrozenArray to contain the per-fragment size information.
119-
// (https://drafts.csswg.org/resize-observer/#resize-observer-entry-interface)
120-
//
121-
// Also, testing these new box options revealed that in both Chrome and FF everything is returned in the callback,
122-
// regardless of the "box" option.
123-
// The spec states the following on this:
124-
// > This does not have any impact on which box dimensions are returned to the defined callback when the event
125-
// > is fired, it solely defines which box the author wishes to observe layout changes on.
126-
// (https://drafts.csswg.org/resize-observer/#resize-observer-interface)
127-
// I'm not exactly clear on what this means, especially when you consider a later section stating the following:
128-
// > This section is non-normative. An author may desire to observe more than one CSS box.
129-
// > In this case, author will need to use multiple ResizeObservers.
130-
// (https://drafts.csswg.org/resize-observer/#resize-observer-interface)
131-
// Which is clearly not how current browser implementations behave, and seems to contradict the previous quote.
132-
// For this reason I decided to only return the requested size,
133-
// even though it seems we have access to results for all box types.
134-
// This also means that we get to keep the current api, being able to return a simple { width, height } pair,
135-
// regardless of box option.
136-
const extractSize = (
137-
entry: ResizeObserverEntry,
138-
boxProp: "borderBoxSize" | "contentBoxSize" | "devicePixelContentBoxSize",
139-
sizeType: keyof ResizeObserverSize
140-
): number | undefined => {
141-
if (!entry[boxProp]) {
142-
if (boxProp === "contentBoxSize") {
143-
// The dimensions in `contentBoxSize` and `contentRect` are equivalent according to the spec.
144-
// See the 6th step in the description for the RO algorithm:
145-
// https://drafts.csswg.org/resize-observer/#create-and-populate-resizeobserverentry-h
146-
// > Set this.contentRect to logical this.contentBoxSize given target and observedBox of "content-box".
147-
// In real browser implementations of course these objects differ, but the width/height values should be equivalent.
148-
return entry.contentRect[sizeType === "inlineSize" ? "width" : "height"];
149-
}
150-
151-
return undefined;
152-
}
153-
154-
// A couple bytes smaller than calling Array.isArray() and just as effective here.
155-
return entry[boxProp][0]
156-
? entry[boxProp][0][sizeType]
157-
: // TS complains about this, because the RO entry type follows the spec and does not reflect Firefox's current
158-
// behaviour of returning objects instead of arrays for `borderBoxSize` and `contentBoxSize`.
159-
// @ts-ignore
160-
entry[boxProp][sizeType];
161-
};
162-
16337
type RoundingFunction = (n: number) => number;
16438

16539
function useResizeObserver<T extends Element>(

src/utils/extractSize.ts

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
// We're only using the first element of the size sequences, until future versions of the spec solidify on how
2+
// exactly it'll be used for fragments in multi-column scenarios:
3+
// From the spec:
4+
// > The box size properties are exposed as FrozenArray in order to support elements that have multiple fragments,
5+
// > which occur in multi-column scenarios. However the current definitions of content rect and border box do not
6+
// > mention how those boxes are affected by multi-column layout. In this spec, there will only be a single
7+
// > ResizeObserverSize returned in the FrozenArray, which will correspond to the dimensions of the first column.
8+
// > A future version of this spec will extend the returned FrozenArray to contain the per-fragment size information.
9+
// (https://drafts.csswg.org/resize-observer/#resize-observer-entry-interface)
10+
//
11+
// Also, testing these new box options revealed that in both Chrome and FF everything is returned in the callback,
12+
// regardless of the "box" option.
13+
// The spec states the following on this:
14+
// > This does not have any impact on which box dimensions are returned to the defined callback when the event
15+
// > is fired, it solely defines which box the author wishes to observe layout changes on.
16+
// (https://drafts.csswg.org/resize-observer/#resize-observer-interface)
17+
// I'm not exactly clear on what this means, especially when you consider a later section stating the following:
18+
// > This section is non-normative. An author may desire to observe more than one CSS box.
19+
// > In this case, author will need to use multiple ResizeObservers.
20+
// (https://drafts.csswg.org/resize-observer/#resize-observer-interface)
21+
// Which is clearly not how current browser implementations behave, and seems to contradict the previous quote.
22+
// For this reason I decided to only return the requested size,
23+
// even though it seems we have access to results for all box types.
24+
// This also means that we get to keep the current api, being able to return a simple { width, height } pair,
25+
// regardless of box option.
26+
export default function extractSize(
27+
entry: ResizeObserverEntry,
28+
boxProp: "borderBoxSize" | "contentBoxSize" | "devicePixelContentBoxSize",
29+
sizeType: keyof ResizeObserverSize
30+
): number | undefined {
31+
if (!entry[boxProp]) {
32+
if (boxProp === "contentBoxSize") {
33+
// The dimensions in `contentBoxSize` and `contentRect` are equivalent according to the spec.
34+
// See the 6th step in the description for the RO algorithm:
35+
// https://drafts.csswg.org/resize-observer/#create-and-populate-resizeobserverentry-h
36+
// > Set this.contentRect to logical this.contentBoxSize given target and observedBox of "content-box".
37+
// In real browser implementations of course these objects differ, but the width/height values should be equivalent.
38+
return entry.contentRect[sizeType === "inlineSize" ? "width" : "height"];
39+
}
40+
41+
return undefined;
42+
}
43+
44+
// A couple bytes smaller than calling Array.isArray() and just as effective here.
45+
return entry[boxProp][0]
46+
? entry[boxProp][0][sizeType]
47+
: // TS complains about this, because the RO entry type follows the spec and does not reflect Firefox's current
48+
// behaviour of returning objects instead of arrays for `borderBoxSize` and `contentBoxSize`.
49+
// @ts-ignore
50+
entry[boxProp][sizeType];
51+
}

src/utils/useResolvedElement.ts

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
// This of course could've been more streamlined with internal state instead of
2+
// refs, but then host hooks / components could not opt out of renders.
3+
// This could've been exported to its own module, but the current build doesn't
4+
// seem to work with module imports and I had no more time to spend on this...
5+
import { RefCallback, RefObject, useCallback, useEffect, useRef } from "react";
6+
7+
type SubscriberCleanup = () => void;
8+
type SubscriberResponse = SubscriberCleanup | void;
9+
10+
export default function useResolvedElement<T extends Element>(
11+
subscriber: (element: T) => SubscriberResponse,
12+
refOrElement?: T | RefObject<T> | null
13+
): RefCallback<T> {
14+
const callbackRefElement = useRef<T | null>(null);
15+
const lastReportRef = useRef<{
16+
reporter: () => void;
17+
element: T | null;
18+
} | null>(null);
19+
const cleanupRef = useRef<SubscriberResponse | null>();
20+
21+
// Resolving ".current" purely so that a new callSubscriber instance is created when needed.
22+
const refElement =
23+
refOrElement && "current" in refOrElement ? refOrElement.current : null;
24+
const callSubscriber = useCallback(() => {
25+
let element = null;
26+
if (callbackRefElement.current) {
27+
element = callbackRefElement.current;
28+
} else if (refOrElement) {
29+
if (refOrElement instanceof Element) {
30+
element = refOrElement;
31+
} else {
32+
element = refOrElement.current;
33+
}
34+
}
35+
36+
if (
37+
lastReportRef.current &&
38+
lastReportRef.current.element === element &&
39+
lastReportRef.current.reporter === callSubscriber
40+
) {
41+
return;
42+
}
43+
44+
if (cleanupRef.current) {
45+
cleanupRef.current();
46+
// Making sure the cleanup is not called accidentally multiple times.
47+
cleanupRef.current = null;
48+
}
49+
lastReportRef.current = {
50+
reporter: callSubscriber,
51+
element,
52+
};
53+
54+
// Only calling the subscriber, if there's an actual element to report.
55+
if (element) {
56+
cleanupRef.current = subscriber(element);
57+
}
58+
}, [refOrElement, refElement, subscriber]);
59+
60+
// On each render, we check whether a ref changed, or if we got a new raw
61+
// element.
62+
useEffect(() => {
63+
// With this we're *technically* supporting cases where ref objects' current value changes, but only if there's a
64+
// render accompanying that change as well.
65+
// To guarantee we always have the right element, one must use the ref callback provided instead, but we support
66+
// RefObjects to make the hook API more convenient in certain cases.
67+
callSubscriber();
68+
}, [callSubscriber]);
69+
70+
return useCallback<RefCallback<T>>(
71+
(element) => {
72+
callbackRefElement.current = element;
73+
callSubscriber();
74+
},
75+
[callSubscriber]
76+
);
77+
}

tsconfig.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@
77
"esModuleInterop": true,
88
"outDir": "dist",
99
"declaration": true,
10-
"emitDeclarationOnly": true
10+
"emitDeclarationOnly": true,
11+
"noImplicitAny": true
1112
},
1213
"include": ["src"]
1314
}

0 commit comments

Comments
 (0)