@design-edito/tools 0.5.2 → 0.5.4
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/agnostic/html/deep-select/index.js +5 -3
- package/agnostic/html/hyper-json/smart-tags/coalesced/index.d.ts +9 -9
- package/agnostic/html/hyper-json/smart-tags/coalesced/index.js +9 -9
- package/agnostic/html/hyper-json/smart-tags/isolated/index.d.ts +3 -3
- package/agnostic/html/hyper-json/smart-tags/isolated/index.js +3 -3
- package/agnostic/html/index.d.ts +1 -1
- package/agnostic/html/index.js +1 -1
- package/agnostic/index.d.ts +2 -2
- package/agnostic/index.js +2 -2
- package/agnostic/misc/index.d.ts +3 -3
- package/agnostic/misc/index.js +3 -3
- package/agnostic/misc/logs/index.d.ts +1 -1
- package/agnostic/misc/logs/index.js +1 -1
- package/agnostic/numbers/index.d.ts +1 -1
- package/agnostic/numbers/index.js +1 -1
- package/agnostic/optim/index.d.ts +1 -1
- package/agnostic/optim/index.js +1 -1
- package/agnostic/strings/index.d.ts +2 -2
- package/agnostic/strings/index.js +2 -2
- package/agnostic/time/dates/format-date/index.d.ts +4 -2
- package/agnostic/time/dates/format-date/index.js +43 -94
- package/agnostic/time/dates/format-date/index.test.js +154 -0
- package/agnostic/time/dates/format-date/parts.d.ts +28 -0
- package/agnostic/time/dates/format-date/parts.js +136 -0
- package/agnostic/time/dates/format-date/types.d.ts +84 -0
- package/agnostic/time/dates/format-date/types.js +1 -0
- package/agnostic/time/duration/format-duration/index.d.ts +42 -0
- package/agnostic/time/duration/format-duration/index.js +94 -0
- package/agnostic/time/duration/format-duration/index.test.d.ts +1 -0
- package/agnostic/time/duration/format-duration/index.test.js +148 -0
- package/agnostic/time/duration/format-duration/parts.d.ts +32 -0
- package/agnostic/time/duration/format-duration/parts.js +107 -0
- package/agnostic/time/duration/format-duration/types.d.ts +93 -0
- package/agnostic/time/duration/format-duration/types.js +1 -0
- package/agnostic/time/duration/index.d.ts +6 -1
- package/agnostic/time/duration/index.js +2 -2
- package/agnostic/time/duration/index.test.d.ts +1 -0
- package/agnostic/time/duration/index.test.js +168 -0
- package/agnostic/time/index.d.ts +1 -1
- package/agnostic/time/index.js +1 -1
- package/components/BeforeAfter/index.controlled.d.ts +25 -25
- package/components/BeforeAfter/index.controlled.js +47 -53
- package/components/BeforeAfter/index.d.ts +20 -23
- package/components/BeforeAfter/index.js +48 -40
- package/components/Button/index.js +1 -1
- package/components/Clippable/index.d.ts +17 -26
- package/components/Clippable/index.js +21 -23
- package/components/Disclaimer/index.d.ts +32 -24
- package/components/Disclaimer/index.js +29 -24
- package/components/Drawer/index.d.ts +45 -25
- package/components/Drawer/index.js +61 -52
- package/components/EventListener/index.d.ts +4 -5
- package/components/Gallery/index.d.ts +14 -16
- package/components/Gallery/index.js +25 -30
- package/components/Iframe/index.js +2 -2
- package/components/Image/index.d.ts +5 -15
- package/components/Image/index.js +9 -34
- package/components/Input/index.d.ts +23 -19
- package/components/Input/index.js +30 -20
- package/components/JsonEditor/index.d.ts +177 -9
- package/components/JsonEditor/index.js +133 -77
- package/components/JsonEditor/types.d.ts +11 -0
- package/components/JsonEditor/types.js +1 -0
- package/components/JsonEditor/utils.d.ts +8 -0
- package/components/JsonEditor/utils.js +23 -0
- package/components/ListLoader/index.controlled.d.ts +5 -5
- package/components/ListLoader/index.controlled.js +4 -4
- package/components/ListLoader/index.d.ts +11 -11
- package/components/ListLoader/index.js +12 -12
- package/components/Overlayer/index.d.ts +4 -4
- package/components/Overlayer/index.js +6 -6
- package/components/Paginator/index.d.ts +7 -12
- package/components/Paginator/index.js +7 -11
- package/components/ResizeObserver/index.d.ts +14 -3
- package/components/ResizeObserver/index.js +49 -24
- package/components/Scrllgngn/index.d.ts +17 -13
- package/components/Scrllgngn/index.js +25 -84
- package/components/Scrllgngn/utils.d.ts +52 -0
- package/components/Scrllgngn/utils.js +102 -0
- package/components/ScrollListener/index.d.ts +43 -28
- package/components/ScrollListener/index.js +59 -79
- package/components/ScrollListener/utils.d.ts +37 -16
- package/components/ScrollListener/utils.js +141 -28
- package/components/Select/index.d.ts +24 -20
- package/components/Select/index.js +30 -20
- package/components/Sequencer/index.controlled.d.ts +16 -26
- package/components/Sequencer/index.controlled.js +15 -12
- package/components/Sequencer/index.d.ts +25 -30
- package/components/Sequencer/index.js +20 -34
- package/components/Subtitles/index.d.ts +29 -5
- package/components/Subtitles/index.js +90 -12
- package/components/Textarea/index.d.ts +26 -20
- package/components/Textarea/index.js +40 -21
- package/components/Theatre/index.d.ts +35 -39
- package/components/Theatre/index.js +46 -58
- package/components/UIModule/index.d.ts +25 -29
- package/components/UIModule/index.js +81 -110
- package/components/Video/index.controlled.d.ts +51 -51
- package/components/Video/index.controlled.js +75 -72
- package/components/Video/index.d.ts +12 -14
- package/components/Video/index.js +41 -86
- package/components/Video/utils.d.ts +35 -3
- package/components/Video/utils.js +37 -14
- package/components/index.d.ts +6 -6
- package/components/index.js +6 -6
- package/components/utils/index.d.ts +22 -0
- package/components/utils/index.js +31 -0
- package/index.d.ts +1 -1
- package/index.js +1 -1
- package/node/@aws-s3/storage/directory/index.d.ts +2 -2
- package/node/@aws-s3/storage/directory/index.js +2 -2
- package/node/@aws-s3/storage/file/index.d.ts +1 -1
- package/node/@aws-s3/storage/file/index.js +1 -1
- package/node/@aws-s3/storage/index.d.ts +1 -1
- package/node/@aws-s3/storage/index.js +1 -1
- package/node/@google-cloud/storage/directory/index.d.ts +2 -2
- package/node/@google-cloud/storage/directory/index.js +2 -2
- package/node/@google-cloud/storage/file/index.d.ts +2 -2
- package/node/@google-cloud/storage/file/index.js +2 -2
- package/node/@google-cloud/storage/index.d.ts +1 -1
- package/node/@google-cloud/storage/index.js +1 -1
- package/node/cloud-storage/operations/index.d.ts +2 -2
- package/node/cloud-storage/operations/index.js +2 -2
- package/node/ftps/file/index.d.ts +2 -2
- package/node/ftps/file/index.js +2 -2
- package/node/images/index.d.ts +1 -1
- package/node/images/index.js +1 -1
- package/node/images/transform/operations/index.d.ts +2 -2
- package/node/images/transform/operations/index.js +2 -2
- package/node/index.d.ts +1 -1
- package/node/index.js +1 -1
- package/node/process/index.d.ts +1 -1
- package/node/process/index.js +1 -1
- package/node/sftp/file/index.d.ts +2 -2
- package/node/sftp/file/index.js +2 -2
- package/node/sftp/index.d.ts +1 -1
- package/node/sftp/index.js +1 -1
- package/package.json +20 -5
- package/TODO.md +0 -269
- package/components/BeforeAfter/utils.d.ts +0 -4
- package/components/BeforeAfter/utils.js +0 -9
- package/components/Input/index.controlled.d.ts +0 -42
- package/components/Input/index.controlled.js +0 -54
- package/components/Select/index.controlled.d.ts +0 -43
- package/components/Select/index.controlled.js +0 -54
- package/components/Subtitles/types.d.ts +0 -24
- package/components/Subtitles/utils.d.ts +0 -32
- package/components/Subtitles/utils.js +0 -107
- package/components/Textarea/index.controlled.d.ts +0 -50
- package/components/Textarea/index.controlled.js +0 -75
- /package/{components/Subtitles/types.js → agnostic/time/dates/format-date/index.test.d.ts} +0 -0
- /package/components/ResizeObserver/{style.module.css → styles.module.css} +0 -0
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { PropsPage, PropsStickyBlock } from './index.js';
|
|
2
|
+
/** The subset of a bounding rect a {@link Scrllgngn} tracks and exposes. */
|
|
3
|
+
export type ScreenRect = {
|
|
4
|
+
left: number;
|
|
5
|
+
right: number;
|
|
6
|
+
width: number;
|
|
7
|
+
height: number;
|
|
8
|
+
};
|
|
9
|
+
/** Zero-based positions of the pages a consolidated block is displayed on. */
|
|
10
|
+
export type BlockConsolidatedData = {
|
|
11
|
+
displayOnPages: number[];
|
|
12
|
+
};
|
|
13
|
+
/** A sticky block, merged across every page it appears on. */
|
|
14
|
+
export type ConsolidatedStickyBlock = PropsStickyBlock & BlockConsolidatedData;
|
|
15
|
+
/**
|
|
16
|
+
* Merges the sticky blocks of every page into a single entry per block id.
|
|
17
|
+
*
|
|
18
|
+
* A block repeated across pages under the same `id` becomes one sticky block
|
|
19
|
+
* listing all the pages it shows on, so it stays mounted across them instead of
|
|
20
|
+
* being torn down and rebuilt at every page change. Blocks without an `id` get a
|
|
21
|
+
* random one and are therefore never merged.
|
|
22
|
+
*
|
|
23
|
+
* @param pages - The pages to walk, in order.
|
|
24
|
+
* @returns The sticky blocks, keyed by id. Scroll blocks are left out.
|
|
25
|
+
*/
|
|
26
|
+
export declare function consolidateStickyBlocks(pages?: PropsPage[]): Map<string, ConsolidatedStickyBlock>;
|
|
27
|
+
/**
|
|
28
|
+
* Picks the sticky blocks of one layer close enough to the current page to be
|
|
29
|
+
* worth mounting.
|
|
30
|
+
*
|
|
31
|
+
* @param blocks - Every consolidated sticky block.
|
|
32
|
+
* @param depth - The layer to pick from.
|
|
33
|
+
* @param currentPagePos - Zero-based position of the page in view.
|
|
34
|
+
* @param lazyLoadDistance - How many pages around the current one still mount
|
|
35
|
+
* their blocks.
|
|
36
|
+
* @returns The blocks to render, stacked by ascending `zIndex`.
|
|
37
|
+
*/
|
|
38
|
+
export declare function lazyLoadedBlocks(blocks: Map<string, ConsolidatedStickyBlock>, depth: 'back' | 'front', currentPagePos: number, lazyLoadDistance: number): ConsolidatedStickyBlock[];
|
|
39
|
+
/**
|
|
40
|
+
* Builds the CSS custom properties exposed on a {@link Scrllgngn} root.
|
|
41
|
+
*
|
|
42
|
+
* The public `screen-*` set follows the usual convention and stays absent until
|
|
43
|
+
* the first measurement lands. The `--PRIVATE-*` set is what the component's own
|
|
44
|
+
* stylesheet positions its fixed layers with, and it is emitted from the very
|
|
45
|
+
* first render — as `initial`, the guaranteed-invalid value — so those layers can
|
|
46
|
+
* never inherit the same names from an ancestor component. `initial` leaves them
|
|
47
|
+
* behaving exactly as an unset variable would, without opening the hole.
|
|
48
|
+
*
|
|
49
|
+
* @param rect - The measured bounding rect, absent until the first resize lands.
|
|
50
|
+
* @returns The custom properties, keyed by their full name.
|
|
51
|
+
*/
|
|
52
|
+
export declare function toScreenCssProps(rect?: ScreenRect): Record<string, string>;
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { randomHash } from '../../agnostic/random/uuid/index.js';
|
|
2
|
+
/**
|
|
3
|
+
* Merges the sticky blocks of every page into a single entry per block id.
|
|
4
|
+
*
|
|
5
|
+
* A block repeated across pages under the same `id` becomes one sticky block
|
|
6
|
+
* listing all the pages it shows on, so it stays mounted across them instead of
|
|
7
|
+
* being torn down and rebuilt at every page change. Blocks without an `id` get a
|
|
8
|
+
* random one and are therefore never merged.
|
|
9
|
+
*
|
|
10
|
+
* @param pages - The pages to walk, in order.
|
|
11
|
+
* @returns The sticky blocks, keyed by id. Scroll blocks are left out.
|
|
12
|
+
*/
|
|
13
|
+
export function consolidateStickyBlocks(pages) {
|
|
14
|
+
const consolidatedBlocks = new Map();
|
|
15
|
+
for (const page of pages ?? []) {
|
|
16
|
+
const pageIndex = pages?.indexOf(page) ?? -1;
|
|
17
|
+
for (const block of page.blocks ?? []) {
|
|
18
|
+
const blockId = block.id ?? randomHash(12);
|
|
19
|
+
const found = consolidatedBlocks.get(blockId);
|
|
20
|
+
if (found !== undefined)
|
|
21
|
+
consolidatedBlocks.set(blockId, {
|
|
22
|
+
...found,
|
|
23
|
+
...block,
|
|
24
|
+
displayOnPages: [
|
|
25
|
+
...found.displayOnPages,
|
|
26
|
+
pageIndex
|
|
27
|
+
]
|
|
28
|
+
});
|
|
29
|
+
else
|
|
30
|
+
consolidatedBlocks.set(blockId, {
|
|
31
|
+
...block,
|
|
32
|
+
displayOnPages: [pageIndex]
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return new Map(Array
|
|
37
|
+
.from(consolidatedBlocks)
|
|
38
|
+
.filter((e) => {
|
|
39
|
+
const block = e[1];
|
|
40
|
+
return block.depth === 'back'
|
|
41
|
+
|| block.depth === 'front';
|
|
42
|
+
}));
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Picks the sticky blocks of one layer close enough to the current page to be
|
|
46
|
+
* worth mounting.
|
|
47
|
+
*
|
|
48
|
+
* @param blocks - Every consolidated sticky block.
|
|
49
|
+
* @param depth - The layer to pick from.
|
|
50
|
+
* @param currentPagePos - Zero-based position of the page in view.
|
|
51
|
+
* @param lazyLoadDistance - How many pages around the current one still mount
|
|
52
|
+
* their blocks.
|
|
53
|
+
* @returns The blocks to render, stacked by ascending `zIndex`.
|
|
54
|
+
*/
|
|
55
|
+
export function lazyLoadedBlocks(blocks, depth, currentPagePos, lazyLoadDistance) {
|
|
56
|
+
return Array
|
|
57
|
+
.from(blocks)
|
|
58
|
+
.map(([, block]) => block)
|
|
59
|
+
.filter(block => block.depth === depth
|
|
60
|
+
&& block.displayOnPages.some(dispPage => {
|
|
61
|
+
const absDiff = Math.abs(dispPage - currentPagePos);
|
|
62
|
+
return absDiff <= lazyLoadDistance;
|
|
63
|
+
}))
|
|
64
|
+
.sort((a, b) => (a.zIndex ?? -Infinity) - (b.zIndex ?? -Infinity));
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Builds the CSS custom properties exposed on a {@link Scrllgngn} root.
|
|
68
|
+
*
|
|
69
|
+
* The public `screen-*` set follows the usual convention and stays absent until
|
|
70
|
+
* the first measurement lands. The `--PRIVATE-*` set is what the component's own
|
|
71
|
+
* stylesheet positions its fixed layers with, and it is emitted from the very
|
|
72
|
+
* first render — as `initial`, the guaranteed-invalid value — so those layers can
|
|
73
|
+
* never inherit the same names from an ancestor component. `initial` leaves them
|
|
74
|
+
* behaving exactly as an unset variable would, without opening the hole.
|
|
75
|
+
*
|
|
76
|
+
* @param rect - The measured bounding rect, absent until the first resize lands.
|
|
77
|
+
* @returns The custom properties, keyed by their full name.
|
|
78
|
+
*/
|
|
79
|
+
export function toScreenCssProps(rect) {
|
|
80
|
+
const publicProps = rect === undefined
|
|
81
|
+
? {}
|
|
82
|
+
: {
|
|
83
|
+
'--lm-scrllgngn-screen-left': `${rect.left}px`,
|
|
84
|
+
'--lm-scrllgngn-screen-left-raw': `${rect.left}`,
|
|
85
|
+
'--lm-scrllgngn-screen-right': `${rect.right}px`,
|
|
86
|
+
'--lm-scrllgngn-screen-right-raw': `${rect.right}`,
|
|
87
|
+
'--lm-scrllgngn-screen-width': `${rect.width}px`,
|
|
88
|
+
'--lm-scrllgngn-screen-width-raw': `${rect.width}`,
|
|
89
|
+
'--lm-scrllgngn-screen-height': `${rect.height}px`,
|
|
90
|
+
'--lm-scrllgngn-screen-height-raw': `${rect.height}`
|
|
91
|
+
};
|
|
92
|
+
return {
|
|
93
|
+
...publicProps,
|
|
94
|
+
'--PRIVATE-left': toPrivateLength(rect?.left),
|
|
95
|
+
'--PRIVATE-right': toPrivateLength(rect?.right),
|
|
96
|
+
'--PRIVATE-width': toPrivateLength(rect?.width),
|
|
97
|
+
'--PRIVATE-height': toPrivateLength(rect?.height)
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
function toPrivateLength(value) {
|
|
101
|
+
return value === undefined ? 'initial' : `${value}px`;
|
|
102
|
+
}
|
|
@@ -2,46 +2,61 @@ import { type FunctionComponent, type PropsWithChildren } from 'react';
|
|
|
2
2
|
import type { WithClassName } from '../utils/types.js';
|
|
3
3
|
import { type ScrollState } from './utils.js';
|
|
4
4
|
/**
|
|
5
|
-
* Props for the ScrollListener component.
|
|
5
|
+
* Props for the {@link ScrollListener} component.
|
|
6
6
|
*
|
|
7
|
-
* @property startOnVisible - When `true`, scroll tracking starts
|
|
8
|
-
*
|
|
9
|
-
* @property stopOnHidden - When `true`, scroll tracking stops when the component
|
|
10
|
-
* the viewport.
|
|
11
|
-
* @property
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* component changes. Receives `true` when the component is intersecting the viewport,
|
|
16
|
-
* otherwise `false`.
|
|
7
|
+
* @property startOnVisible - When `true`, scroll tracking only starts once the
|
|
8
|
+
* component enters the viewport, instead of on mount.
|
|
9
|
+
* @property stopOnHidden - When `true`, scroll tracking stops when the component
|
|
10
|
+
* leaves the viewport.
|
|
11
|
+
* @property onScrollStateChanged - Called after the measured {@link ScrollState}
|
|
12
|
+
* changed. Receives `undefined` until the first measurement lands.
|
|
13
|
+
* @property onVisibilityChanged - Called on every intersection change, with
|
|
14
|
+
* `true` when the component intersects the viewport.
|
|
17
15
|
* @property className - Optional additional class name(s) applied to the root element.
|
|
18
16
|
* @property children - React nodes rendered inside the scroll listener container.
|
|
19
17
|
*/
|
|
20
18
|
export type Props = PropsWithChildren<WithClassName<{
|
|
21
|
-
stopOnHidden?: boolean;
|
|
22
19
|
startOnVisible?: boolean;
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
};
|
|
20
|
+
stopOnHidden?: boolean;
|
|
21
|
+
onScrollStateChanged?: (scrollState?: ScrollState) => void;
|
|
22
|
+
onVisibilityChanged?: (isVisible: boolean) => void;
|
|
27
23
|
}>>;
|
|
28
24
|
/**
|
|
29
|
-
*
|
|
30
|
-
*
|
|
25
|
+
* Exposes scroll metrics — both the document's and its own — as CSS custom
|
|
26
|
+
* properties on its root element, so scroll-driven styling needs no JavaScript
|
|
27
|
+
* of its own.
|
|
31
28
|
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
29
|
+
* ### CSS custom properties on the root element
|
|
30
|
+
* Each measurement comes as a `px` length under its bare name, and as a `-raw`
|
|
31
|
+
* twin holding the plain number:
|
|
32
|
+
* - `--lm-scroll-listener-window-width`, `--lm-scroll-listener-window-height`
|
|
33
|
+
* - `--lm-scroll-listener-html-width`, `--lm-scroll-listener-html-height`
|
|
34
|
+
* - `--lm-scroll-listener-scroll-x`, `--lm-scroll-listener-scroll-y`
|
|
35
|
+
* - `--lm-scroll-listener-width`, `--lm-scroll-listener-height`
|
|
36
|
+
* - `--lm-scroll-listener-offset-x`, `--lm-scroll-listener-offset-y`
|
|
35
37
|
*
|
|
36
|
-
*
|
|
37
|
-
* scroll-
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
38
|
+
* Ratios are unitless and have no twin:
|
|
39
|
+
* - `--lm-scroll-listener-window-scrolled-x-ratio`,
|
|
40
|
+
* `--lm-scroll-listener-window-scrolled-y-ratio` — how far the document is
|
|
41
|
+
* scrolled.
|
|
42
|
+
* - `--lm-scroll-listener-self-inner-scrolled-x-ratio`,
|
|
43
|
+
* `--lm-scroll-listener-self-inner-scrolled-y-ratio` — `0` when the element's
|
|
44
|
+
* start edge meets the viewport's start edge, `1` when its end edge meets the
|
|
45
|
+
* viewport's end edge. The span where it is fully visible.
|
|
46
|
+
* - `--lm-scroll-listener-self-outer-scrolled-x-ratio`,
|
|
47
|
+
* `--lm-scroll-listener-self-outer-scrolled-y-ratio` — `0` when the element is
|
|
48
|
+
* about to enter the viewport, `1` once it has fully left it. The span where it
|
|
49
|
+
* overlaps the viewport at all.
|
|
41
50
|
*
|
|
42
51
|
* @param props - Component properties.
|
|
43
52
|
* @see {@link Props}
|
|
44
|
-
* @returns A container
|
|
45
|
-
*
|
|
53
|
+
* @returns A container exposing the metrics, wrapping its children inside an
|
|
54
|
+
* {@link IntersectionObserverComponent}.
|
|
55
|
+
*
|
|
56
|
+
* @remarks
|
|
57
|
+
* All mounted instances share a single pair of `scroll` / `resize` listeners and
|
|
58
|
+
* a single measurement pass per animation frame — the document is measured once
|
|
59
|
+
* for everyone, each element only for itself. The listeners exist only while at
|
|
60
|
+
* least one instance is tracking.
|
|
46
61
|
*/
|
|
47
62
|
export declare const ScrollListener: FunctionComponent<Props>;
|
|
@@ -1,100 +1,80 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
import { useEffect, useRef, useState
|
|
2
|
+
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
3
3
|
import { clss } from '../../agnostic/css/clss/index.js';
|
|
4
4
|
import { randomHash } from '../../agnostic/random/uuid/index.js';
|
|
5
|
-
import { mergeClassNames } from '../utils/index.js';
|
|
6
5
|
import { IntersectionObserverComponent } from '../IntersectionObserver/index.js';
|
|
6
|
+
import { mergeClassNames, useChangeDispatch } from '../utils/index.js';
|
|
7
7
|
import { scrollListener as publicClassName } from '../public-classnames.js';
|
|
8
|
-
import {
|
|
8
|
+
import { subscribe, toScrollCssProps, unsubscribe } from './utils.js';
|
|
9
9
|
import cssModule from './styles.module.css';
|
|
10
10
|
/**
|
|
11
|
-
*
|
|
12
|
-
*
|
|
11
|
+
* Exposes scroll metrics — both the document's and its own — as CSS custom
|
|
12
|
+
* properties on its root element, so scroll-driven styling needs no JavaScript
|
|
13
|
+
* of its own.
|
|
13
14
|
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
15
|
+
* ### CSS custom properties on the root element
|
|
16
|
+
* Each measurement comes as a `px` length under its bare name, and as a `-raw`
|
|
17
|
+
* twin holding the plain number:
|
|
18
|
+
* - `--lm-scroll-listener-window-width`, `--lm-scroll-listener-window-height`
|
|
19
|
+
* - `--lm-scroll-listener-html-width`, `--lm-scroll-listener-html-height`
|
|
20
|
+
* - `--lm-scroll-listener-scroll-x`, `--lm-scroll-listener-scroll-y`
|
|
21
|
+
* - `--lm-scroll-listener-width`, `--lm-scroll-listener-height`
|
|
22
|
+
* - `--lm-scroll-listener-offset-x`, `--lm-scroll-listener-offset-y`
|
|
17
23
|
*
|
|
18
|
-
*
|
|
19
|
-
* scroll-
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
24
|
+
* Ratios are unitless and have no twin:
|
|
25
|
+
* - `--lm-scroll-listener-window-scrolled-x-ratio`,
|
|
26
|
+
* `--lm-scroll-listener-window-scrolled-y-ratio` — how far the document is
|
|
27
|
+
* scrolled.
|
|
28
|
+
* - `--lm-scroll-listener-self-inner-scrolled-x-ratio`,
|
|
29
|
+
* `--lm-scroll-listener-self-inner-scrolled-y-ratio` — `0` when the element's
|
|
30
|
+
* start edge meets the viewport's start edge, `1` when its end edge meets the
|
|
31
|
+
* viewport's end edge. The span where it is fully visible.
|
|
32
|
+
* - `--lm-scroll-listener-self-outer-scrolled-x-ratio`,
|
|
33
|
+
* `--lm-scroll-listener-self-outer-scrolled-y-ratio` — `0` when the element is
|
|
34
|
+
* about to enter the viewport, `1` once it has fully left it. The span where it
|
|
35
|
+
* overlaps the viewport at all.
|
|
23
36
|
*
|
|
24
37
|
* @param props - Component properties.
|
|
25
38
|
* @see {@link Props}
|
|
26
|
-
* @returns A container
|
|
27
|
-
*
|
|
39
|
+
* @returns A container exposing the metrics, wrapping its children inside an
|
|
40
|
+
* {@link IntersectionObserverComponent}.
|
|
41
|
+
*
|
|
42
|
+
* @remarks
|
|
43
|
+
* All mounted instances share a single pair of `scroll` / `resize` listeners and
|
|
44
|
+
* a single measurement pass per animation frame — the document is measured once
|
|
45
|
+
* for everyone, each element only for itself. The listeners exist only while at
|
|
46
|
+
* least one instance is tracking.
|
|
28
47
|
*/
|
|
29
|
-
export const ScrollListener = ({ startOnVisible, stopOnHidden,
|
|
30
|
-
|
|
31
|
-
const [
|
|
48
|
+
export const ScrollListener = ({ startOnVisible, stopOnHidden, onScrollStateChanged, onVisibilityChanged, className, children }) => {
|
|
49
|
+
// State & refs
|
|
50
|
+
const [subscriberId] = useState(() => randomHash(6));
|
|
51
|
+
const [scrollState, setScrollState] = useState();
|
|
32
52
|
const rootRef = useRef(null);
|
|
33
|
-
//
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
// Fx. no dep. - register / unregister to scroll listeners
|
|
53
|
+
// State dispatch
|
|
54
|
+
useChangeDispatch(scrollState, onScrollStateChanged);
|
|
55
|
+
// Fx. no dep. - track from mount, unless waiting for the component to show up.
|
|
56
|
+
// The cleanup runs whichever way the subscription was opened.
|
|
38
57
|
useEffect(() => {
|
|
39
|
-
|
|
40
|
-
|
|
58
|
+
if (startOnVisible !== true) {
|
|
59
|
+
subscribe(subscriberId, { rootRef, onScrollStateChange: setScrollState });
|
|
60
|
+
}
|
|
61
|
+
return () => unsubscribe(subscriberId);
|
|
41
62
|
}, []);
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
if (
|
|
47
|
-
|
|
48
|
-
if (ioEntry?.isIntersecting !== true && stopOnHidden === true)
|
|
49
|
-
unregister(privateId);
|
|
50
|
-
}, [startOnVisible, stopOnHidden]);
|
|
51
|
-
// Rendering
|
|
52
|
-
const customProps = Object
|
|
53
|
-
.entries(scrollData !== undefined
|
|
54
|
-
? {
|
|
55
|
-
// Global
|
|
56
|
-
[`--${publicClassName}-window-width`]: scrollData.global.win.width,
|
|
57
|
-
[`--${publicClassName}-window-height`]: scrollData.global.win.height,
|
|
58
|
-
[`--${publicClassName}-html-width`]: scrollData.global.html.width,
|
|
59
|
-
[`--${publicClassName}-html-height`]: scrollData.global.html.height,
|
|
60
|
-
[`--${publicClassName}-scroll-x`]: scrollData.global.scroll.x,
|
|
61
|
-
[`--${publicClassName}-scroll-y`]: scrollData.global.scroll.y,
|
|
62
|
-
// Local
|
|
63
|
-
[`--${publicClassName}-width`]: scrollData.local.width,
|
|
64
|
-
[`--${publicClassName}-height`]: scrollData.local.height,
|
|
65
|
-
[`--${publicClassName}-offset-x`]: scrollData.local.offsetX,
|
|
66
|
-
[`--${publicClassName}-offset-y`]: scrollData.local.offsetY
|
|
63
|
+
// User action handlers
|
|
64
|
+
const handleIntersection = useCallback(({ ioEntry }) => {
|
|
65
|
+
const isVisible = ioEntry?.isIntersecting ?? false;
|
|
66
|
+
onVisibilityChanged?.(isVisible);
|
|
67
|
+
if (isVisible && startOnVisible === true) {
|
|
68
|
+
subscribe(subscriberId, { rootRef, onScrollStateChange: setScrollState });
|
|
67
69
|
}
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
}), {});
|
|
73
|
-
if (scrollData !== undefined) {
|
|
74
|
-
customProps[`--${publicClassName}-window-scrolled-x-ratio`] = (scrollData.global.scroll.x) / Math.max((scrollData.global.html.width - scrollData.global.win.width), 1);
|
|
75
|
-
customProps[`--${publicClassName}-window-scrolled-y-ratio`] = (scrollData.global.scroll.y) / Math.max((scrollData.global.html.height - scrollData.global.win.height), 1);
|
|
76
|
-
const topTouchesTop = scrollData.local.offsetY;
|
|
77
|
-
const topTouchesBottom = scrollData.local.offsetY - scrollData.global.win.height;
|
|
78
|
-
const bottomTouchesTop = scrollData.local.offsetY + scrollData.local.height;
|
|
79
|
-
const bottomTouchesBottom = (scrollData.local.offsetY + scrollData.local.height) - scrollData.global.win.height;
|
|
80
|
-
const leftTouchesLeft = scrollData.local.offsetX;
|
|
81
|
-
const leftTouchesRight = scrollData.local.offsetX - scrollData.global.win.width;
|
|
82
|
-
const rightTouchesLeft = scrollData.local.offsetX + scrollData.local.width;
|
|
83
|
-
const rightTouchesRight = (scrollData.local.offsetX + scrollData.local.width) - scrollData.global.win.width;
|
|
84
|
-
const innerYRange = [bottomTouchesBottom, topTouchesTop];
|
|
85
|
-
const outerYRange = [topTouchesBottom, bottomTouchesTop];
|
|
86
|
-
const innerXRange = [rightTouchesRight, leftTouchesLeft];
|
|
87
|
-
const outerXRange = [leftTouchesRight, rightTouchesLeft];
|
|
88
|
-
const innerScrollYRatio = (scrollData.global.scroll.y - innerYRange[0]) / Math.max((innerYRange[1] - innerYRange[0]), 1);
|
|
89
|
-
const outerScrollYRatio = (scrollData.global.scroll.y - outerYRange[0]) / Math.max((outerYRange[1] - outerYRange[0]), 1);
|
|
90
|
-
const innerScrollXRatio = (scrollData.global.scroll.x - innerXRange[0]) / Math.max((innerXRange[1] - innerXRange[0]), 1);
|
|
91
|
-
const outerScrollXRatio = (scrollData.global.scroll.x - outerXRange[0]) / Math.max((outerXRange[1] - outerXRange[0]), 1);
|
|
92
|
-
customProps[`--${publicClassName}-self-inner-scrolled-y-ratio`] = innerScrollYRatio;
|
|
93
|
-
customProps[`--${publicClassName}-self-outer-scrolled-y-ratio`] = outerScrollYRatio;
|
|
94
|
-
customProps[`--${publicClassName}-self-inner-scrolled-x-ratio`] = innerScrollXRatio;
|
|
95
|
-
customProps[`--${publicClassName}-self-outer-scrolled-x-ratio`] = outerScrollXRatio;
|
|
96
|
-
}
|
|
70
|
+
if (!isVisible && stopOnHidden === true)
|
|
71
|
+
unsubscribe(subscriberId);
|
|
72
|
+
}, [startOnVisible, stopOnHidden, onVisibilityChanged]);
|
|
73
|
+
// Rendering
|
|
97
74
|
const c = clss(publicClassName, { cssModule });
|
|
98
75
|
const rootClss = mergeClassNames(c(), className);
|
|
76
|
+
const customProps = scrollState === undefined
|
|
77
|
+
? {}
|
|
78
|
+
: toScrollCssProps(scrollState);
|
|
99
79
|
return _jsx("div", { className: rootClss, ref: rootRef, style: { ...customProps }, children: _jsx(IntersectionObserverComponent, { onIntersected: handleIntersection, children: children }) });
|
|
100
80
|
};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { RefObject
|
|
1
|
+
import type { RefObject } from 'react';
|
|
2
2
|
/**
|
|
3
3
|
* Scroll-related measurements for the document and viewport.
|
|
4
4
|
*
|
|
@@ -42,29 +42,50 @@ export type LocalScrollData = {
|
|
|
42
42
|
height: number;
|
|
43
43
|
};
|
|
44
44
|
/**
|
|
45
|
-
* Combined scroll state
|
|
45
|
+
* Combined scroll state handed to each subscribed {@link ScrollListener}.
|
|
46
46
|
*
|
|
47
|
-
* @property global - Document-level scroll data
|
|
48
|
-
*
|
|
47
|
+
* @property global - Document-level scroll data, measured once per frame and
|
|
48
|
+
* shared by every subscriber.
|
|
49
|
+
* @property local - Position and size of this subscriber's own element.
|
|
49
50
|
*/
|
|
50
51
|
export type ScrollState = {
|
|
51
52
|
global: GlobalScrollData;
|
|
52
53
|
local: LocalScrollData;
|
|
53
54
|
};
|
|
54
|
-
export declare const globalDocumentScrollListener: () => GlobalScrollData;
|
|
55
|
-
export declare const localElementScrollListener: (div: HTMLDivElement) => LocalScrollData;
|
|
56
55
|
/**
|
|
57
|
-
*
|
|
56
|
+
* A component subscribed to the shared scroll listener.
|
|
58
57
|
*
|
|
59
|
-
* @property
|
|
60
|
-
*
|
|
61
|
-
* @property
|
|
58
|
+
* @property rootRef - Ref to the tracked element. A subscriber whose ref is not
|
|
59
|
+
* attached yet is skipped for that frame.
|
|
60
|
+
* @property onScrollStateChange - Receives the freshly measured state.
|
|
62
61
|
*/
|
|
63
|
-
export type
|
|
64
|
-
id: string;
|
|
62
|
+
export type Subscriber = {
|
|
65
63
|
rootRef: RefObject<HTMLDivElement | null>;
|
|
66
|
-
|
|
64
|
+
onScrollStateChange: (scrollState: ScrollState) => void;
|
|
67
65
|
};
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
66
|
+
/**
|
|
67
|
+
* Starts tracking one element, attaching the shared listeners if this is the
|
|
68
|
+
* first subscriber. A measurement is scheduled right away, so a subscriber gets
|
|
69
|
+
* its initial state without waiting for a first scroll.
|
|
70
|
+
*
|
|
71
|
+
* @param id - Identifies the subscriber for {@link unsubscribe}.
|
|
72
|
+
* @param subscriber - The element to track and where to report.
|
|
73
|
+
*/
|
|
74
|
+
export declare function subscribe(id: string, subscriber: Subscriber): void;
|
|
75
|
+
/**
|
|
76
|
+
* Stops tracking one element, detaching the shared listeners once no subscriber
|
|
77
|
+
* is left. Unsubscribing an unknown id is a no-op.
|
|
78
|
+
*
|
|
79
|
+
* @param id - The id passed to {@link subscribe}.
|
|
80
|
+
*/
|
|
81
|
+
export declare function unsubscribe(id: string): void;
|
|
82
|
+
/**
|
|
83
|
+
* Builds the CSS custom properties exposed on a {@link ScrollListener} root.
|
|
84
|
+
*
|
|
85
|
+
* Measurements are lengths and come with a `-raw` twin carrying the plain number
|
|
86
|
+
* for `calc()`; ratios are unitless and have no twin.
|
|
87
|
+
*
|
|
88
|
+
* @param scrollState - The state to expose.
|
|
89
|
+
* @returns The custom properties, keyed by their full name.
|
|
90
|
+
*/
|
|
91
|
+
export declare function toScrollCssProps(scrollState: ScrollState): Record<string, string>;
|