@scalewing/react 1.33.0 → 1.33.1
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/CHANGELOG.md +6 -0
- package/dist/components/Split.js +33 -46
- package/dist/components/use-split-range.d.ts +17 -0
- package/dist/components/use-split-range.js +49 -0
- package/dist/css/css-split.js +1 -0
- package/dist/split-measure.d.ts +7 -0
- package/dist/split-measure.js +21 -0
- package/dist/split-size.d.ts +15 -1
- package/dist/split-size.js +21 -9
- package/dist/styles.css +1 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# @scalewing/react
|
|
2
2
|
|
|
3
|
+
## 1.33.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 2a52f01: `Split` no longer widens the page in a narrow container. The split is now at most its container's width less the split minimum (`max-width: calc(100% - var(--sw-split-min))`), so the next column keeps 14rem and the start pane shrinks below its 22rem default instead of running past the container. Before, at 390 px the pane kept 22rem, the next column got no width and its words pushed the page out sideways (the gallery measured 481 px; found by Teisoro F-011). The drag and keyboard range follows the same room: the pane's maximum is the container less the handle and the next column's minimum (as before), and when that room is under the minimum the range narrows to it, so a gesture starts from the width on screen, a move to the end never collapses or jumps the pane, and one step to the start collapses it. The pane keeps the width the reader chose (a collapse never overwrites it) and shows it held to the container's range, so a pane expanded after the container widens shows at least its minimum. The separator's `aria-valuenow` reports the width that shows (100 when the container leaves no range) and follows container resizes. No API, class or token changes.
|
|
8
|
+
|
|
3
9
|
## 1.33.0
|
|
4
10
|
|
|
5
11
|
### Minor Changes
|
package/dist/components/Split.js
CHANGED
|
@@ -1,21 +1,16 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { spacingScale } from '@scalewing/tokens';
|
|
4
|
-
import { useCallback, useId,
|
|
4
|
+
import { useCallback, useId, useRef, useState, } from 'react';
|
|
5
5
|
import { cx } from '../class-names.js';
|
|
6
|
-
import { applySplitDrag, applySplitKey,
|
|
7
|
-
|
|
8
|
-
const styles = getComputedStyle(node);
|
|
9
|
-
const remPx = cssLengthToPx(getComputedStyle(document.documentElement).fontSize) || 16;
|
|
10
|
-
return readSplitMetrics((name) => styles.getPropertyValue(name), remPx);
|
|
11
|
-
}
|
|
6
|
+
import { applySplitDrag, applySplitKey, clampSplitSize, splitPercent, } from '../split-size.js';
|
|
7
|
+
import { useSplitRange } from './use-split-range.js';
|
|
12
8
|
export function Split({ children, className, collapsed: collapsedProp, label, onCollapsedChange, ...rest }) {
|
|
13
9
|
const paneId = useId();
|
|
14
10
|
const splitRef = useRef(null);
|
|
15
11
|
const handleRef = useRef(null);
|
|
16
12
|
const dragRef = useRef(null);
|
|
17
13
|
const [width, setWidth] = useState(null);
|
|
18
|
-
const [range, setRange] = useState(null);
|
|
19
14
|
const [collapsedUncontrolled, setCollapsedUncontrolled] = useState(false);
|
|
20
15
|
const collapsed = collapsedProp ?? collapsedUncontrolled;
|
|
21
16
|
const setCollapsed = useCallback((next) => {
|
|
@@ -24,38 +19,25 @@ export function Split({ children, className, collapsed: collapsedProp, label, on
|
|
|
24
19
|
setCollapsedUncontrolled(next);
|
|
25
20
|
}
|
|
26
21
|
}, [collapsedProp, onCollapsedChange]);
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
setWidth((current) => current ?? measured.defaultWidth);
|
|
35
|
-
}, []);
|
|
36
|
-
function dragMetrics() {
|
|
37
|
-
const split = splitRef.current;
|
|
38
|
-
const handle = handleRef.current;
|
|
39
|
-
if (!split) {
|
|
40
|
-
return { min: 0, max: 0, step: 16, defaultWidth: 0 };
|
|
41
|
-
}
|
|
42
|
-
const measured = metricsFrom(split);
|
|
43
|
-
const parentWidth = split.parentElement?.getBoundingClientRect().width ?? 0;
|
|
44
|
-
const handleWidth = handle?.getBoundingClientRect().width ?? 0;
|
|
45
|
-
return {
|
|
46
|
-
...measured,
|
|
47
|
-
max: effectiveSplitMax(measured.max, parentWidth, handleWidth, measured.min),
|
|
48
|
-
};
|
|
49
|
-
}
|
|
50
|
-
function currentSnapshot() {
|
|
51
|
-
const measured = dragMetrics();
|
|
22
|
+
const { range, measure } = useSplitRange(splitRef, handleRef);
|
|
23
|
+
/**
|
|
24
|
+
* `width` is the reader's chosen width; what shows is it held to the range
|
|
25
|
+
* the container leaves, so an expansion never shows a pane under its
|
|
26
|
+
* minimum or past the room, and a gesture starts from what shows.
|
|
27
|
+
*/
|
|
28
|
+
function currentSnapshot(metrics) {
|
|
52
29
|
return {
|
|
53
30
|
collapsed,
|
|
54
|
-
width: width ??
|
|
31
|
+
width: clampSplitSize(width ?? metrics.defaultWidth, metrics.min, metrics.max),
|
|
55
32
|
};
|
|
56
33
|
}
|
|
57
|
-
function applySnapshot(next) {
|
|
58
|
-
|
|
34
|
+
function applySnapshot(next, start) {
|
|
35
|
+
// Only a gesture that moves the pane chooses a width: a collapse, or a
|
|
36
|
+
// move held at the end of a narrow container's range, keeps the chosen
|
|
37
|
+
// one, so it shows again when the container widens.
|
|
38
|
+
if (!next.collapsed && next.width !== start.width) {
|
|
39
|
+
setWidth(next.width);
|
|
40
|
+
}
|
|
59
41
|
if (next.collapsed !== collapsed) {
|
|
60
42
|
setCollapsed(next.collapsed);
|
|
61
43
|
}
|
|
@@ -67,11 +49,12 @@ export function Split({ children, className, collapsed: collapsedProp, label, on
|
|
|
67
49
|
event.preventDefault();
|
|
68
50
|
event.currentTarget.focus();
|
|
69
51
|
event.currentTarget.setPointerCapture?.(event.pointerId);
|
|
52
|
+
const metrics = measure();
|
|
70
53
|
dragRef.current = {
|
|
71
54
|
pointerId: event.pointerId,
|
|
72
55
|
startX: event.clientX,
|
|
73
|
-
snapshot: currentSnapshot(),
|
|
74
|
-
metrics
|
|
56
|
+
snapshot: currentSnapshot(metrics),
|
|
57
|
+
metrics,
|
|
75
58
|
moved: false,
|
|
76
59
|
};
|
|
77
60
|
}
|
|
@@ -84,7 +67,7 @@ export function Split({ children, className, collapsed: collapsedProp, label, on
|
|
|
84
67
|
if (Math.abs(delta) >= spacingScale[1]) {
|
|
85
68
|
drag.moved = true;
|
|
86
69
|
}
|
|
87
|
-
applySnapshot(applySplitDrag(drag.snapshot, delta, drag.metrics));
|
|
70
|
+
applySnapshot(applySplitDrag(drag.snapshot, delta, drag.metrics), drag.snapshot);
|
|
88
71
|
}
|
|
89
72
|
function endDrag(event) {
|
|
90
73
|
const drag = dragRef.current;
|
|
@@ -101,17 +84,21 @@ export function Split({ children, className, collapsed: collapsedProp, label, on
|
|
|
101
84
|
if (!splitRef.current) {
|
|
102
85
|
return;
|
|
103
86
|
}
|
|
104
|
-
const
|
|
87
|
+
const metrics = measure();
|
|
88
|
+
const start = currentSnapshot(metrics);
|
|
89
|
+
const next = applySplitKey(event.key, start, metrics);
|
|
105
90
|
if (!next) {
|
|
106
91
|
return;
|
|
107
92
|
}
|
|
108
93
|
event.preventDefault();
|
|
109
|
-
applySnapshot(next);
|
|
94
|
+
applySnapshot(next, start);
|
|
110
95
|
}
|
|
111
|
-
|
|
96
|
+
// What shows: the chosen width held to the container's range.
|
|
97
|
+
const shownWidth = range
|
|
98
|
+
? clampSplitSize(width ?? range.defaultWidth, range.min, range.max)
|
|
99
|
+
: null;
|
|
100
|
+
const valueNow = collapsed || !range || shownWidth === null
|
|
112
101
|
? 0
|
|
113
|
-
: range
|
|
114
|
-
|
|
115
|
-
: 0;
|
|
116
|
-
return (_jsxs("div", { ...rest, ref: splitRef, className: cx('sw-split', className), "data-collapsed": collapsed ? '' : undefined, children: [_jsx("div", { className: "sw-split-pane", id: paneId, "aria-hidden": collapsed || undefined, style: width === null ? undefined : { width: `${width}px` }, children: children }), _jsx("div", { ref: handleRef, "aria-controls": paneId, "aria-label": collapsed ? `Show ${label}` : `Resize ${label}`, "aria-orientation": "vertical", "aria-valuemax": 100, "aria-valuemin": 0, "aria-valuenow": valueNow, className: "sw-split-handle", onKeyDown: onKeyDown, onPointerCancel: endDrag, onPointerDown: onPointerDown, onPointerMove: onPointerMove, onPointerUp: endDrag, role: "separator", tabIndex: 0, children: _jsx("span", { "aria-hidden": "true", className: "sw-split-grip" }) })] }));
|
|
102
|
+
: splitPercent(shownWidth, range.min, range.max);
|
|
103
|
+
return (_jsxs("div", { ...rest, ref: splitRef, className: cx('sw-split', className), "data-collapsed": collapsed ? '' : undefined, children: [_jsx("div", { className: "sw-split-pane", id: paneId, "aria-hidden": collapsed || undefined, style: shownWidth === null ? undefined : { width: `${shownWidth}px` }, children: children }), _jsx("div", { ref: handleRef, "aria-controls": paneId, "aria-label": collapsed ? `Show ${label}` : `Resize ${label}`, "aria-orientation": "vertical", "aria-valuemax": 100, "aria-valuemin": 0, "aria-valuenow": valueNow, className: "sw-split-handle", onKeyDown: onKeyDown, onPointerCancel: endDrag, onPointerDown: onPointerDown, onPointerMove: onPointerMove, onPointerUp: endDrag, role: "separator", tabIndex: 0, children: _jsx("span", { "aria-hidden": "true", className: "sw-split-grip" }) })] }));
|
|
117
104
|
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { type RefObject } from 'react';
|
|
2
|
+
import { type SplitMetrics } from '../split-size.js';
|
|
3
|
+
export type SplitRange = Pick<SplitMetrics, 'min' | 'max'> & {
|
|
4
|
+
defaultWidth: number;
|
|
5
|
+
};
|
|
6
|
+
/**
|
|
7
|
+
* The range a Split's pane can take in its container, measured on layout and
|
|
8
|
+
* whenever the container resizes, and `measure` for a gesture, which also
|
|
9
|
+
* brings the shown range up to date. An unchanged range keeps its state, so
|
|
10
|
+
* a resize that changes nothing does not render again.
|
|
11
|
+
*/
|
|
12
|
+
export declare function useSplitRange(splitRef: RefObject<HTMLDivElement | null>, handleRef: RefObject<HTMLDivElement | null>): {
|
|
13
|
+
range: SplitRange | null;
|
|
14
|
+
measure: () => SplitMetrics & {
|
|
15
|
+
defaultWidth: number;
|
|
16
|
+
};
|
|
17
|
+
};
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { useCallback, useLayoutEffect, useState } from 'react';
|
|
3
|
+
import { containerContentWidth, splitTokenMetrics } from '../split-measure.js';
|
|
4
|
+
import { effectiveSplitRange } from '../split-size.js';
|
|
5
|
+
function sameRange(a, b) {
|
|
6
|
+
return (a !== null &&
|
|
7
|
+
a.min === b.min &&
|
|
8
|
+
a.max === b.max &&
|
|
9
|
+
a.defaultWidth === b.defaultWidth);
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* The range a Split's pane can take in its container, measured on layout and
|
|
13
|
+
* whenever the container resizes, and `measure` for a gesture, which also
|
|
14
|
+
* brings the shown range up to date. An unchanged range keeps its state, so
|
|
15
|
+
* a resize that changes nothing does not render again.
|
|
16
|
+
*/
|
|
17
|
+
export function useSplitRange(splitRef, handleRef) {
|
|
18
|
+
const [range, setRange] = useState(null);
|
|
19
|
+
const measure = useCallback(() => {
|
|
20
|
+
const split = splitRef.current;
|
|
21
|
+
if (!split) {
|
|
22
|
+
return { min: 0, max: 0, step: 16, defaultWidth: 0 };
|
|
23
|
+
}
|
|
24
|
+
const tokens = splitTokenMetrics(split);
|
|
25
|
+
const handleWidth = handleRef.current?.getBoundingClientRect().width ?? 0;
|
|
26
|
+
const measured = {
|
|
27
|
+
...tokens,
|
|
28
|
+
...effectiveSplitRange(tokens, containerContentWidth(split), handleWidth),
|
|
29
|
+
};
|
|
30
|
+
const next = {
|
|
31
|
+
min: measured.min,
|
|
32
|
+
max: measured.max,
|
|
33
|
+
defaultWidth: measured.defaultWidth,
|
|
34
|
+
};
|
|
35
|
+
setRange((current) => (sameRange(current, next) ? current : next));
|
|
36
|
+
return measured;
|
|
37
|
+
}, [splitRef, handleRef]);
|
|
38
|
+
useLayoutEffect(() => {
|
|
39
|
+
const parent = splitRef.current?.parentElement;
|
|
40
|
+
measure();
|
|
41
|
+
if (!parent || typeof ResizeObserver === 'undefined') {
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
const observer = new ResizeObserver(() => measure());
|
|
45
|
+
observer.observe(parent);
|
|
46
|
+
return () => observer.disconnect();
|
|
47
|
+
}, [measure, splitRef]);
|
|
48
|
+
return { range, measure };
|
|
49
|
+
}
|
package/dist/css/css-split.js
CHANGED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { type SplitMetrics } from './split-size.js';
|
|
2
|
+
/** The container's content width, which the split's percentage cap uses. */
|
|
3
|
+
export declare function containerContentWidth(node: HTMLElement): number;
|
|
4
|
+
/** The split's token range, step and default width, in px. */
|
|
5
|
+
export declare function splitTokenMetrics(node: HTMLElement): SplitMetrics & {
|
|
6
|
+
defaultWidth: number;
|
|
7
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// DOM reads for Split: the split's token metrics from its computed custom
|
|
2
|
+
// properties, and its container's content width (what the stylesheet's
|
|
3
|
+
// percentage cap resolves against). The range math stays pure in
|
|
4
|
+
// `split-size.ts`.
|
|
5
|
+
import { cssLengthToPx, readSplitMetrics, } from './split-size.js';
|
|
6
|
+
/** The container's content width, which the split's percentage cap uses. */
|
|
7
|
+
export function containerContentWidth(node) {
|
|
8
|
+
const parent = node.parentElement;
|
|
9
|
+
if (!parent) {
|
|
10
|
+
return 0;
|
|
11
|
+
}
|
|
12
|
+
const styles = getComputedStyle(parent);
|
|
13
|
+
const padding = cssLengthToPx(styles.paddingLeft) + cssLengthToPx(styles.paddingRight);
|
|
14
|
+
return Math.max(0, parent.clientWidth - padding);
|
|
15
|
+
}
|
|
16
|
+
/** The split's token range, step and default width, in px. */
|
|
17
|
+
export function splitTokenMetrics(node) {
|
|
18
|
+
const styles = getComputedStyle(node);
|
|
19
|
+
const remPx = cssLengthToPx(getComputedStyle(document.documentElement).fontSize) || 16;
|
|
20
|
+
return readSplitMetrics((name) => styles.getPropertyValue(name), remPx);
|
|
21
|
+
}
|
package/dist/split-size.d.ts
CHANGED
|
@@ -12,7 +12,21 @@ export declare function readSplitMetrics(read: (name: string) => string, remPx?:
|
|
|
12
12
|
defaultWidth: number;
|
|
13
13
|
};
|
|
14
14
|
export declare function clampSplitSize(size: number, min: number, max: number): number;
|
|
15
|
+
/**
|
|
16
|
+
* The pane's place between its minimum and maximum as 0–100. When the
|
|
17
|
+
* container leaves no range (min equals max), the pane is as wide as it can
|
|
18
|
+
* be: 100.
|
|
19
|
+
*/
|
|
15
20
|
export declare function splitPercent(width: number, min: number, max: number): number;
|
|
16
|
-
|
|
21
|
+
/**
|
|
22
|
+
* The range a pane can take in its container. The stylesheet keeps the split
|
|
23
|
+
* at most its container less the split minimum, so the next column keeps that
|
|
24
|
+
* minimum and the pane gets the room left after the handle. In a narrow
|
|
25
|
+
* container that room is under the token minimum, so the range narrows to it:
|
|
26
|
+
* a pane never grows past what shows, and only a move narrower than the room
|
|
27
|
+
* collapses it. A container that is not laid out (zero width) keeps the
|
|
28
|
+
* token range.
|
|
29
|
+
*/
|
|
30
|
+
export declare function effectiveSplitRange(tokens: Pick<SplitMetrics, 'min' | 'max'>, containerWidth: number, handleWidth: number): Pick<SplitMetrics, 'min' | 'max'>;
|
|
17
31
|
export declare function applySplitDrag(start: SplitSnapshot, deltaX: number, metrics: SplitMetrics): SplitSnapshot;
|
|
18
32
|
export declare function applySplitKey(key: string, start: SplitSnapshot, metrics: SplitMetrics): SplitSnapshot | null;
|
package/dist/split-size.js
CHANGED
|
@@ -23,21 +23,33 @@ export function readSplitMetrics(read, remPx = 16) {
|
|
|
23
23
|
export function clampSplitSize(size, min, max) {
|
|
24
24
|
return Math.min(max, Math.max(min, size));
|
|
25
25
|
}
|
|
26
|
+
/**
|
|
27
|
+
* The pane's place between its minimum and maximum as 0–100. When the
|
|
28
|
+
* container leaves no range (min equals max), the pane is as wide as it can
|
|
29
|
+
* be: 100.
|
|
30
|
+
*/
|
|
26
31
|
export function splitPercent(width, min, max) {
|
|
27
32
|
if (max <= min) {
|
|
28
|
-
return
|
|
33
|
+
return 100;
|
|
29
34
|
}
|
|
30
35
|
return Math.round(((clampSplitSize(width, min, max) - min) / (max - min)) * 100);
|
|
31
36
|
}
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
37
|
+
/**
|
|
38
|
+
* The range a pane can take in its container. The stylesheet keeps the split
|
|
39
|
+
* at most its container less the split minimum, so the next column keeps that
|
|
40
|
+
* minimum and the pane gets the room left after the handle. In a narrow
|
|
41
|
+
* container that room is under the token minimum, so the range narrows to it:
|
|
42
|
+
* a pane never grows past what shows, and only a move narrower than the room
|
|
43
|
+
* collapses it. A container that is not laid out (zero width) keeps the
|
|
44
|
+
* token range.
|
|
45
|
+
*/
|
|
46
|
+
export function effectiveSplitRange(tokens, containerWidth, handleWidth) {
|
|
47
|
+
if (containerWidth <= 0) {
|
|
48
|
+
return { min: tokens.min, max: tokens.max };
|
|
35
49
|
}
|
|
36
|
-
const room =
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
}
|
|
40
|
-
return Math.min(tokenMax, room);
|
|
50
|
+
const room = Math.max(0, containerWidth - handleWidth - tokens.min);
|
|
51
|
+
const max = Math.min(tokens.max, room);
|
|
52
|
+
return { min: Math.min(tokens.min, max), max };
|
|
41
53
|
}
|
|
42
54
|
export function applySplitDrag(start, deltaX, metrics) {
|
|
43
55
|
if (start.collapsed) {
|
package/dist/styles.css
CHANGED