@crisp-ui-kit/crisp 0.19.0 → 0.20.0
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/cjs/components/menu/menu.js +8 -9
- package/dist/cjs/components/multiselect/multiselect.js +11 -8
- package/dist/cjs/components/popover/popover.js +8 -10
- package/dist/cjs/components/select/select.js +11 -8
- package/dist/cjs/components/wheelpicker/wheelpicker.js +8 -2
- package/dist/cjs/core/index.js +5 -1
- package/dist/cjs/core/place-floating.js +55 -0
- package/dist/cjs/core/use-prefers-reduced-motion.js +67 -0
- package/dist/esm/components/menu/menu.js +8 -9
- package/dist/esm/components/multiselect/multiselect.js +11 -8
- package/dist/esm/components/popover/popover.js +8 -10
- package/dist/esm/components/select/select.js +11 -8
- package/dist/esm/components/wheelpicker/wheelpicker.js +8 -2
- package/dist/esm/core/index.js +2 -0
- package/dist/esm/core/place-floating.js +52 -0
- package/dist/esm/core/use-prefers-reduced-motion.js +31 -0
- package/dist/types/components/menu/menu.d.ts +2 -1
- package/dist/types/components/popover/popover.d.ts +2 -1
- package/dist/types/core/index.d.ts +2 -0
- package/dist/types/core/place-floating.d.ts +20 -0
- package/dist/types/core/use-prefers-reduced-motion.d.ts +17 -0
- package/package.json +1 -1
|
@@ -41,6 +41,7 @@ exports.MenuLabel = MenuLabel;
|
|
|
41
41
|
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
42
42
|
const lucide_react_1 = require("lucide-react");
|
|
43
43
|
const React = __importStar(require("react"));
|
|
44
|
+
const core_1 = require("../../core");
|
|
44
45
|
const menu_recipe_1 = require("./menu.recipe");
|
|
45
46
|
const menu_keyboard_1 = require("./menu-keyboard");
|
|
46
47
|
const MenuContext = React.createContext(null);
|
|
@@ -71,15 +72,13 @@ function Menu({ trigger, children, align = "start", open: openProp, defaultOpen,
|
|
|
71
72
|
const rect = triggerRef.current?.getBoundingClientRect();
|
|
72
73
|
if (!rect)
|
|
73
74
|
return;
|
|
74
|
-
const
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
:
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
else
|
|
82
|
-
style.left = rect.left;
|
|
75
|
+
const { style, up: flipUp } = (0, core_1.placeFloating)({
|
|
76
|
+
rect,
|
|
77
|
+
align,
|
|
78
|
+
gap: 4,
|
|
79
|
+
flipBelow: 280,
|
|
80
|
+
anchor: triggerRef.current,
|
|
81
|
+
});
|
|
83
82
|
setUp(flipUp);
|
|
84
83
|
setPos(style);
|
|
85
84
|
};
|
|
@@ -37,6 +37,7 @@ exports.MultiSelect = MultiSelect;
|
|
|
37
37
|
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
38
38
|
const lucide_react_1 = require("lucide-react");
|
|
39
39
|
const React = __importStar(require("react"));
|
|
40
|
+
const core_1 = require("../../core");
|
|
40
41
|
const select_1 = require("../select");
|
|
41
42
|
const multiselect_recipe_1 = require("./multiselect.recipe");
|
|
42
43
|
const TONES = [
|
|
@@ -92,15 +93,17 @@ function MultiSelect({ options, value: valueProp, defaultValue, onValueChange, p
|
|
|
92
93
|
return;
|
|
93
94
|
const rect = triggerRef.current?.getBoundingClientRect();
|
|
94
95
|
if (rect) {
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
96
|
+
// the popup can grow past the trigger (minWidth, not width), so which
|
|
97
|
+
// edge it is pinned to decides which way it grows — start, in whichever
|
|
98
|
+
// direction that is (#210)
|
|
99
|
+
const { style, up: flipUp } = (0, core_1.placeFloating)({
|
|
100
|
+
rect,
|
|
101
|
+
gap: 4,
|
|
102
|
+
flipBelow: 300,
|
|
103
|
+
anchor: triggerRef.current,
|
|
103
104
|
});
|
|
105
|
+
setUp(flipUp);
|
|
106
|
+
setPos({ ...style, minWidth: rect.width });
|
|
104
107
|
}
|
|
105
108
|
setQuery("");
|
|
106
109
|
setHighlight(0);
|
|
@@ -36,6 +36,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
36
36
|
exports.Popover = Popover;
|
|
37
37
|
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
38
38
|
const React = __importStar(require("react"));
|
|
39
|
+
const core_1 = require("../../core");
|
|
39
40
|
const popover_recipe_1 = require("./popover.recipe");
|
|
40
41
|
/**
|
|
41
42
|
* A generic anchored popover — the Menu/Select surface holding arbitrary
|
|
@@ -58,16 +59,13 @@ function Popover({ trigger, children, align = "start", open: openProp, defaultOp
|
|
|
58
59
|
const el = triggerRef.current;
|
|
59
60
|
if (!el)
|
|
60
61
|
return;
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
:
|
|
67
|
-
|
|
68
|
-
style.right = window.innerWidth - rect.right;
|
|
69
|
-
else
|
|
70
|
-
style.left = rect.left;
|
|
62
|
+
const { style, up: flipUp } = (0, core_1.placeFloating)({
|
|
63
|
+
rect: el.getBoundingClientRect(),
|
|
64
|
+
align,
|
|
65
|
+
gap: 6,
|
|
66
|
+
flipBelow: 280,
|
|
67
|
+
anchor: el,
|
|
68
|
+
});
|
|
71
69
|
setUp(flipUp);
|
|
72
70
|
setPos(style);
|
|
73
71
|
};
|
|
@@ -37,6 +37,7 @@ exports.Select = Select;
|
|
|
37
37
|
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
38
38
|
const lucide_react_1 = require("lucide-react");
|
|
39
39
|
const React = __importStar(require("react"));
|
|
40
|
+
const core_1 = require("../../core");
|
|
40
41
|
const listbox_keyboard_1 = require("./listbox-keyboard");
|
|
41
42
|
const select_recipe_1 = require("./select.recipe");
|
|
42
43
|
function optionText(o) {
|
|
@@ -87,15 +88,17 @@ function Select({ options, value, defaultValue, onValueChange, placeholder = "Se
|
|
|
87
88
|
return;
|
|
88
89
|
const rect = triggerRef.current?.getBoundingClientRect();
|
|
89
90
|
if (rect) {
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
91
|
+
// the popup can grow past the trigger (minWidth, not width), so which
|
|
92
|
+
// edge it is pinned to decides which way it grows — start, in whichever
|
|
93
|
+
// direction that is (#210)
|
|
94
|
+
const { style, up: flipUp } = (0, core_1.placeFloating)({
|
|
95
|
+
rect,
|
|
96
|
+
gap: 4,
|
|
97
|
+
flipBelow: 260,
|
|
98
|
+
anchor: triggerRef.current,
|
|
98
99
|
});
|
|
100
|
+
setUp(flipUp);
|
|
101
|
+
setPos({ ...style, minWidth: rect.width });
|
|
99
102
|
}
|
|
100
103
|
setQuery("");
|
|
101
104
|
const sel = options.findIndex((o) => o.value === selected);
|
|
@@ -36,6 +36,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
36
36
|
exports.WheelPicker = WheelPicker;
|
|
37
37
|
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
38
38
|
const React = __importStar(require("react"));
|
|
39
|
+
const core_1 = require("../../core");
|
|
39
40
|
const wheelpicker_recipe_1 = require("./wheelpicker.recipe");
|
|
40
41
|
const ITEM_HEIGHT = { small: 36, medium: 44 };
|
|
41
42
|
/**
|
|
@@ -70,6 +71,11 @@ function WheelPicker({ columns, value: valueProp, defaultValue, onValueChange, s
|
|
|
70
71
|
setInternal(merged);
|
|
71
72
|
onValueChange?.(merged);
|
|
72
73
|
};
|
|
74
|
+
// CSS cannot reach this: `scroll-behavior` in a stylesheet does not override
|
|
75
|
+
// an explicit `behavior` passed to scrollTo, so the preference is read here
|
|
76
|
+
// and the two "smooth" call sites go through `glide` (#208).
|
|
77
|
+
const reduceMotion = (0, core_1.usePrefersReducedMotion)();
|
|
78
|
+
const glide = reduceMotion ? "auto" : "smooth";
|
|
73
79
|
const scrollTo = React.useCallback((key, index, behavior) => {
|
|
74
80
|
const el = refs.current.get(key);
|
|
75
81
|
if (!el || index < 0)
|
|
@@ -107,7 +113,7 @@ function WheelPicker({ columns, value: valueProp, defaultValue, onValueChange, s
|
|
|
107
113
|
if (!opt)
|
|
108
114
|
return;
|
|
109
115
|
commit(col.key, opt.value);
|
|
110
|
-
scrollTo(col.key, clamped,
|
|
116
|
+
scrollTo(col.key, clamped, glide);
|
|
111
117
|
};
|
|
112
118
|
if (e.key === "ArrowDown")
|
|
113
119
|
go(i + 1);
|
|
@@ -127,7 +133,7 @@ function WheelPicker({ columns, value: valueProp, defaultValue, onValueChange, s
|
|
|
127
133
|
const selected = opt.value === value[col.key];
|
|
128
134
|
return ((0, jsx_runtime_1.jsx)("div", { id: optionId(col.key, opt.value), role: "option", "aria-selected": selected, "data-selected": selected ? "" : undefined, className: "wheelpicker-item", onClick: () => {
|
|
129
135
|
commit(col.key, opt.value);
|
|
130
|
-
scrollTo(col.key, col.options.indexOf(opt),
|
|
136
|
+
scrollTo(col.key, col.options.indexOf(opt), glide);
|
|
131
137
|
}, children: opt.label }, opt.value));
|
|
132
138
|
}) }, col.key))) }) })] }));
|
|
133
139
|
}
|
package/dist/cjs/core/index.js
CHANGED
|
@@ -14,13 +14,17 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
|
14
14
|
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
15
|
};
|
|
16
16
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
-
exports.resolveRecipe = exports.Primitive = exports.defineRecipe = exports.cx = void 0;
|
|
17
|
+
exports.usePrefersReducedMotion = exports.resolveRecipe = exports.Primitive = exports.placeFloating = exports.defineRecipe = exports.cx = void 0;
|
|
18
18
|
var cx_1 = require("./cx");
|
|
19
19
|
Object.defineProperty(exports, "cx", { enumerable: true, get: function () { return cx_1.cx; } });
|
|
20
20
|
var define_1 = require("./define");
|
|
21
21
|
Object.defineProperty(exports, "defineRecipe", { enumerable: true, get: function () { return define_1.defineRecipe; } });
|
|
22
|
+
var place_floating_1 = require("./place-floating");
|
|
23
|
+
Object.defineProperty(exports, "placeFloating", { enumerable: true, get: function () { return place_floating_1.placeFloating; } });
|
|
22
24
|
var primitive_1 = require("./primitive");
|
|
23
25
|
Object.defineProperty(exports, "Primitive", { enumerable: true, get: function () { return primitive_1.Primitive; } });
|
|
24
26
|
var resolve_1 = require("./resolve");
|
|
25
27
|
Object.defineProperty(exports, "resolveRecipe", { enumerable: true, get: function () { return resolve_1.resolveRecipe; } });
|
|
26
28
|
__exportStar(require("./rtl"), exports);
|
|
29
|
+
var use_prefers_reduced_motion_1 = require("./use-prefers-reduced-motion");
|
|
30
|
+
Object.defineProperty(exports, "usePrefersReducedMotion", { enumerable: true, get: function () { return use_prefers_reduced_motion_1.usePrefersReducedMotion; } });
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.placeFloating = placeFloating;
|
|
4
|
+
/**
|
|
5
|
+
* Where a floating layer goes, in logical terms.
|
|
6
|
+
*
|
|
7
|
+
* `start` and `end` are inline edges, not left and right. `Menu` and `Popover`
|
|
8
|
+
* both took an `align` prop named that way and then wrote
|
|
9
|
+
* `align === "end" ? right : left` with no direction anywhere, so in an RTL
|
|
10
|
+
* document both were backwards and a layer near the viewport edge overflowed on
|
|
11
|
+
* the wrong side (#210).
|
|
12
|
+
*
|
|
13
|
+
* The direction is read from the anchor's computed style rather than from a
|
|
14
|
+
* prop, so an inherited `dir` on any ancestor is honoured without every call
|
|
15
|
+
* site having to thread it down.
|
|
16
|
+
*
|
|
17
|
+
* Physical `left`/`right` are deliberate in the OUTPUT: this positions a
|
|
18
|
+
* `position: fixed` layer against viewport coordinates, which are physical in
|
|
19
|
+
* both directions. What must be direction-aware is the CHOICE of edge, and
|
|
20
|
+
* that happens here, once. Converting the output to `inset-inline-start` would
|
|
21
|
+
* be the half-conversion #146 exists to prevent — the rect it is measured
|
|
22
|
+
* against would still be physical.
|
|
23
|
+
*/
|
|
24
|
+
/**
|
|
25
|
+
* The nearest declared direction. The `dir` attribute is checked first because
|
|
26
|
+
* it is how a document says this in practice and it is readable without a
|
|
27
|
+
* layout, then computed style for a direction set purely in CSS.
|
|
28
|
+
*/
|
|
29
|
+
function isRtl(anchor) {
|
|
30
|
+
if (!anchor || typeof window === "undefined")
|
|
31
|
+
return false;
|
|
32
|
+
const declared = anchor.closest("[dir]")?.getAttribute("dir");
|
|
33
|
+
if (declared)
|
|
34
|
+
return declared.toLowerCase() === "rtl";
|
|
35
|
+
try {
|
|
36
|
+
return window.getComputedStyle(anchor).direction === "rtl";
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
function placeFloating({ rect, align = "start", gap, flipBelow, anchor, }) {
|
|
43
|
+
const rtl = isRtl(anchor);
|
|
44
|
+
const up = window.innerHeight - rect.bottom < flipBelow;
|
|
45
|
+
const style = up
|
|
46
|
+
? { bottom: window.innerHeight - rect.top + gap }
|
|
47
|
+
: { top: rect.bottom + gap };
|
|
48
|
+
// start is the left edge in LTR and the right edge in RTL; end is the other
|
|
49
|
+
const toRight = align === "end" ? !rtl : rtl;
|
|
50
|
+
if (toRight)
|
|
51
|
+
style.right = window.innerWidth - rect.right;
|
|
52
|
+
else
|
|
53
|
+
style.left = rect.left;
|
|
54
|
+
return { style, up };
|
|
55
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.usePrefersReducedMotion = usePrefersReducedMotion;
|
|
37
|
+
const React = __importStar(require("react"));
|
|
38
|
+
const QUERY = "(prefers-reduced-motion: reduce)";
|
|
39
|
+
const subscribe = (onChange) => {
|
|
40
|
+
if (typeof window === "undefined" || !window.matchMedia)
|
|
41
|
+
return () => { };
|
|
42
|
+
const mq = window.matchMedia(QUERY);
|
|
43
|
+
mq.addEventListener("change", onChange);
|
|
44
|
+
return () => mq.removeEventListener("change", onChange);
|
|
45
|
+
};
|
|
46
|
+
const getSnapshot = () => typeof window !== "undefined" && window.matchMedia
|
|
47
|
+
? window.matchMedia(QUERY).matches
|
|
48
|
+
: false;
|
|
49
|
+
/**
|
|
50
|
+
* Whether the user has asked for reduced motion, live.
|
|
51
|
+
*
|
|
52
|
+
* crisp's CSS carries a `@media (prefers-reduced-motion: reduce)` override in
|
|
53
|
+
* every animating recipe, and `bun run spec` checks it. That covers CSS.
|
|
54
|
+
* Motion driven from JavaScript — `scrollTo({ behavior: "smooth" })`,
|
|
55
|
+
* `element.animate()` — is invisible to a stylesheet: `scroll-behavior` in CSS
|
|
56
|
+
* does NOT override an explicit `behavior` argument. Those call sites have to
|
|
57
|
+
* read the preference here (#208).
|
|
58
|
+
*
|
|
59
|
+
* The server snapshot is `false` rather than "unknown" on purpose. It is the
|
|
60
|
+
* only value that keeps the markup identical on both sides, and a component
|
|
61
|
+
* using this must not put the result in its output for the same reason — the
|
|
62
|
+
* preference belongs in a handler or an effect, where it changes behaviour
|
|
63
|
+
* rather than HTML.
|
|
64
|
+
*/
|
|
65
|
+
function usePrefersReducedMotion() {
|
|
66
|
+
return React.useSyncExternalStore(subscribe, getSnapshot, () => false);
|
|
67
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { Check } from "lucide-react";
|
|
3
3
|
import * as React from "react";
|
|
4
|
+
import { placeFloating } from "../../core/index.js";
|
|
4
5
|
import { menuRecipe } from "./menu.recipe.js";
|
|
5
6
|
import { useMenuKeyboard } from "./menu-keyboard.js";
|
|
6
7
|
const MenuContext = React.createContext(null);
|
|
@@ -31,15 +32,13 @@ export function Menu({ trigger, children, align = "start", open: openProp, defau
|
|
|
31
32
|
const rect = triggerRef.current?.getBoundingClientRect();
|
|
32
33
|
if (!rect)
|
|
33
34
|
return;
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
:
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
else
|
|
42
|
-
style.left = rect.left;
|
|
35
|
+
const { style, up: flipUp } = placeFloating({
|
|
36
|
+
rect,
|
|
37
|
+
align,
|
|
38
|
+
gap: 4,
|
|
39
|
+
flipBelow: 280,
|
|
40
|
+
anchor: triggerRef.current,
|
|
41
|
+
});
|
|
43
42
|
setUp(flipUp);
|
|
44
43
|
setPos(style);
|
|
45
44
|
};
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { Check, ChevronDown, Search } from "lucide-react";
|
|
3
3
|
import * as React from "react";
|
|
4
|
+
import { placeFloating } from "../../core/index.js";
|
|
4
5
|
import { useListboxKeyboard } from "../select/index.js";
|
|
5
6
|
import { multiselectRecipe } from "./multiselect.recipe.js";
|
|
6
7
|
const TONES = [
|
|
@@ -56,15 +57,17 @@ export function MultiSelect({ options, value: valueProp, defaultValue, onValueCh
|
|
|
56
57
|
return;
|
|
57
58
|
const rect = triggerRef.current?.getBoundingClientRect();
|
|
58
59
|
if (rect) {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
60
|
+
// the popup can grow past the trigger (minWidth, not width), so which
|
|
61
|
+
// edge it is pinned to decides which way it grows — start, in whichever
|
|
62
|
+
// direction that is (#210)
|
|
63
|
+
const { style, up: flipUp } = placeFloating({
|
|
64
|
+
rect,
|
|
65
|
+
gap: 4,
|
|
66
|
+
flipBelow: 300,
|
|
67
|
+
anchor: triggerRef.current,
|
|
67
68
|
});
|
|
69
|
+
setUp(flipUp);
|
|
70
|
+
setPos({ ...style, minWidth: rect.width });
|
|
68
71
|
}
|
|
69
72
|
setQuery("");
|
|
70
73
|
setHighlight(0);
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import * as React from "react";
|
|
3
|
+
import { placeFloating } from "../../core/index.js";
|
|
3
4
|
import { popoverRecipe } from "./popover.recipe.js";
|
|
4
5
|
/**
|
|
5
6
|
* A generic anchored popover — the Menu/Select surface holding arbitrary
|
|
@@ -22,16 +23,13 @@ export function Popover({ trigger, children, align = "start", open: openProp, de
|
|
|
22
23
|
const el = triggerRef.current;
|
|
23
24
|
if (!el)
|
|
24
25
|
return;
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
:
|
|
31
|
-
|
|
32
|
-
style.right = window.innerWidth - rect.right;
|
|
33
|
-
else
|
|
34
|
-
style.left = rect.left;
|
|
26
|
+
const { style, up: flipUp } = placeFloating({
|
|
27
|
+
rect: el.getBoundingClientRect(),
|
|
28
|
+
align,
|
|
29
|
+
gap: 6,
|
|
30
|
+
flipBelow: 280,
|
|
31
|
+
anchor: el,
|
|
32
|
+
});
|
|
35
33
|
setUp(flipUp);
|
|
36
34
|
setPos(style);
|
|
37
35
|
};
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { Check, ChevronDown, Search } from "lucide-react";
|
|
3
3
|
import * as React from "react";
|
|
4
|
+
import { placeFloating } from "../../core/index.js";
|
|
4
5
|
import { useListboxKeyboard } from "./listbox-keyboard.js";
|
|
5
6
|
import { selectRecipe } from "./select.recipe.js";
|
|
6
7
|
function optionText(o) {
|
|
@@ -51,15 +52,17 @@ export function Select({ options, value, defaultValue, onValueChange, placeholde
|
|
|
51
52
|
return;
|
|
52
53
|
const rect = triggerRef.current?.getBoundingClientRect();
|
|
53
54
|
if (rect) {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
55
|
+
// the popup can grow past the trigger (minWidth, not width), so which
|
|
56
|
+
// edge it is pinned to decides which way it grows — start, in whichever
|
|
57
|
+
// direction that is (#210)
|
|
58
|
+
const { style, up: flipUp } = placeFloating({
|
|
59
|
+
rect,
|
|
60
|
+
gap: 4,
|
|
61
|
+
flipBelow: 260,
|
|
62
|
+
anchor: triggerRef.current,
|
|
62
63
|
});
|
|
64
|
+
setUp(flipUp);
|
|
65
|
+
setPos({ ...style, minWidth: rect.width });
|
|
63
66
|
}
|
|
64
67
|
setQuery("");
|
|
65
68
|
const sel = options.findIndex((o) => o.value === selected);
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import * as React from "react";
|
|
3
|
+
import { usePrefersReducedMotion } from "../../core/index.js";
|
|
3
4
|
import { wheelpickerRecipe } from "./wheelpicker.recipe.js";
|
|
4
5
|
const ITEM_HEIGHT = { small: 36, medium: 44 };
|
|
5
6
|
/**
|
|
@@ -34,6 +35,11 @@ export function WheelPicker({ columns, value: valueProp, defaultValue, onValueCh
|
|
|
34
35
|
setInternal(merged);
|
|
35
36
|
onValueChange?.(merged);
|
|
36
37
|
};
|
|
38
|
+
// CSS cannot reach this: `scroll-behavior` in a stylesheet does not override
|
|
39
|
+
// an explicit `behavior` passed to scrollTo, so the preference is read here
|
|
40
|
+
// and the two "smooth" call sites go through `glide` (#208).
|
|
41
|
+
const reduceMotion = usePrefersReducedMotion();
|
|
42
|
+
const glide = reduceMotion ? "auto" : "smooth";
|
|
37
43
|
const scrollTo = React.useCallback((key, index, behavior) => {
|
|
38
44
|
const el = refs.current.get(key);
|
|
39
45
|
if (!el || index < 0)
|
|
@@ -71,7 +77,7 @@ export function WheelPicker({ columns, value: valueProp, defaultValue, onValueCh
|
|
|
71
77
|
if (!opt)
|
|
72
78
|
return;
|
|
73
79
|
commit(col.key, opt.value);
|
|
74
|
-
scrollTo(col.key, clamped,
|
|
80
|
+
scrollTo(col.key, clamped, glide);
|
|
75
81
|
};
|
|
76
82
|
if (e.key === "ArrowDown")
|
|
77
83
|
go(i + 1);
|
|
@@ -91,7 +97,7 @@ export function WheelPicker({ columns, value: valueProp, defaultValue, onValueCh
|
|
|
91
97
|
const selected = opt.value === value[col.key];
|
|
92
98
|
return (_jsx("div", { id: optionId(col.key, opt.value), role: "option", "aria-selected": selected, "data-selected": selected ? "" : undefined, className: "wheelpicker-item", onClick: () => {
|
|
93
99
|
commit(col.key, opt.value);
|
|
94
|
-
scrollTo(col.key, col.options.indexOf(opt),
|
|
100
|
+
scrollTo(col.key, col.options.indexOf(opt), glide);
|
|
95
101
|
}, children: opt.label }, opt.value));
|
|
96
102
|
}) }, col.key))) }) })] }));
|
|
97
103
|
}
|
package/dist/esm/core/index.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
export { cx } from "./cx.js";
|
|
2
2
|
export { defineRecipe } from "./define.js";
|
|
3
|
+
export { placeFloating, } from "./place-floating.js";
|
|
3
4
|
export { Primitive } from "./primitive.js";
|
|
4
5
|
export { resolveRecipe } from "./resolve.js";
|
|
5
6
|
export * from "./rtl.js";
|
|
7
|
+
export { usePrefersReducedMotion } from "./use-prefers-reduced-motion.js";
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where a floating layer goes, in logical terms.
|
|
3
|
+
*
|
|
4
|
+
* `start` and `end` are inline edges, not left and right. `Menu` and `Popover`
|
|
5
|
+
* both took an `align` prop named that way and then wrote
|
|
6
|
+
* `align === "end" ? right : left` with no direction anywhere, so in an RTL
|
|
7
|
+
* document both were backwards and a layer near the viewport edge overflowed on
|
|
8
|
+
* the wrong side (#210).
|
|
9
|
+
*
|
|
10
|
+
* The direction is read from the anchor's computed style rather than from a
|
|
11
|
+
* prop, so an inherited `dir` on any ancestor is honoured without every call
|
|
12
|
+
* site having to thread it down.
|
|
13
|
+
*
|
|
14
|
+
* Physical `left`/`right` are deliberate in the OUTPUT: this positions a
|
|
15
|
+
* `position: fixed` layer against viewport coordinates, which are physical in
|
|
16
|
+
* both directions. What must be direction-aware is the CHOICE of edge, and
|
|
17
|
+
* that happens here, once. Converting the output to `inset-inline-start` would
|
|
18
|
+
* be the half-conversion #146 exists to prevent — the rect it is measured
|
|
19
|
+
* against would still be physical.
|
|
20
|
+
*/
|
|
21
|
+
/**
|
|
22
|
+
* The nearest declared direction. The `dir` attribute is checked first because
|
|
23
|
+
* it is how a document says this in practice and it is readable without a
|
|
24
|
+
* layout, then computed style for a direction set purely in CSS.
|
|
25
|
+
*/
|
|
26
|
+
function isRtl(anchor) {
|
|
27
|
+
if (!anchor || typeof window === "undefined")
|
|
28
|
+
return false;
|
|
29
|
+
const declared = anchor.closest("[dir]")?.getAttribute("dir");
|
|
30
|
+
if (declared)
|
|
31
|
+
return declared.toLowerCase() === "rtl";
|
|
32
|
+
try {
|
|
33
|
+
return window.getComputedStyle(anchor).direction === "rtl";
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
export function placeFloating({ rect, align = "start", gap, flipBelow, anchor, }) {
|
|
40
|
+
const rtl = isRtl(anchor);
|
|
41
|
+
const up = window.innerHeight - rect.bottom < flipBelow;
|
|
42
|
+
const style = up
|
|
43
|
+
? { bottom: window.innerHeight - rect.top + gap }
|
|
44
|
+
: { top: rect.bottom + gap };
|
|
45
|
+
// start is the left edge in LTR and the right edge in RTL; end is the other
|
|
46
|
+
const toRight = align === "end" ? !rtl : rtl;
|
|
47
|
+
if (toRight)
|
|
48
|
+
style.right = window.innerWidth - rect.right;
|
|
49
|
+
else
|
|
50
|
+
style.left = rect.left;
|
|
51
|
+
return { style, up };
|
|
52
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
const QUERY = "(prefers-reduced-motion: reduce)";
|
|
3
|
+
const subscribe = (onChange) => {
|
|
4
|
+
if (typeof window === "undefined" || !window.matchMedia)
|
|
5
|
+
return () => { };
|
|
6
|
+
const mq = window.matchMedia(QUERY);
|
|
7
|
+
mq.addEventListener("change", onChange);
|
|
8
|
+
return () => mq.removeEventListener("change", onChange);
|
|
9
|
+
};
|
|
10
|
+
const getSnapshot = () => typeof window !== "undefined" && window.matchMedia
|
|
11
|
+
? window.matchMedia(QUERY).matches
|
|
12
|
+
: false;
|
|
13
|
+
/**
|
|
14
|
+
* Whether the user has asked for reduced motion, live.
|
|
15
|
+
*
|
|
16
|
+
* crisp's CSS carries a `@media (prefers-reduced-motion: reduce)` override in
|
|
17
|
+
* every animating recipe, and `bun run spec` checks it. That covers CSS.
|
|
18
|
+
* Motion driven from JavaScript — `scrollTo({ behavior: "smooth" })`,
|
|
19
|
+
* `element.animate()` — is invisible to a stylesheet: `scroll-behavior` in CSS
|
|
20
|
+
* does NOT override an explicit `behavior` argument. Those call sites have to
|
|
21
|
+
* read the preference here (#208).
|
|
22
|
+
*
|
|
23
|
+
* The server snapshot is `false` rather than "unknown" on purpose. It is the
|
|
24
|
+
* only value that keeps the markup identical on both sides, and a component
|
|
25
|
+
* using this must not put the result in its output for the same reason — the
|
|
26
|
+
* preference belongs in a handler or an effect, where it changes behaviour
|
|
27
|
+
* rather than HTML.
|
|
28
|
+
*/
|
|
29
|
+
export function usePrefersReducedMotion() {
|
|
30
|
+
return React.useSyncExternalStore(subscribe, getSnapshot, () => false);
|
|
31
|
+
}
|
|
@@ -3,7 +3,8 @@ export type MenuProps = {
|
|
|
3
3
|
/** The element that opens the menu (an icon button, etc.). */
|
|
4
4
|
trigger: React.ReactElement;
|
|
5
5
|
children: React.ReactNode;
|
|
6
|
-
/** Align the menu to the start
|
|
6
|
+
/** Align the menu to the trigger's inline `start` or `end` edge — the left
|
|
7
|
+
* edge in an LTR document, the right one in RTL. */
|
|
7
8
|
align?: "start" | "end";
|
|
8
9
|
/** Controlled open state. Omit to let Menu manage it (see defaultOpen). */
|
|
9
10
|
open?: boolean;
|
|
@@ -4,7 +4,8 @@ export type PopoverProps = {
|
|
|
4
4
|
trigger: React.ReactElement;
|
|
5
5
|
/** Popover body. */
|
|
6
6
|
children: React.ReactNode;
|
|
7
|
-
/** Align the popover to the trigger's
|
|
7
|
+
/** Align the popover to the trigger's inline `start` or `end` edge — the
|
|
8
|
+
* left edge in an LTR document, the right one in RTL. */
|
|
8
9
|
align?: "start" | "end";
|
|
9
10
|
/** Controlled open state. Omit to let the popover manage it. */
|
|
10
11
|
/** Controlled open state. Omit to let Popover manage it (see defaultOpen). */
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
export { cx } from "./cx";
|
|
2
2
|
export { defineRecipe } from "./define";
|
|
3
|
+
export { type FloatingAlign, type PlaceFloatingOptions, placeFloating, } from "./place-floating";
|
|
3
4
|
export { Primitive } from "./primitive";
|
|
4
5
|
export { resolveRecipe } from "./resolve";
|
|
5
6
|
export * from "./rtl";
|
|
6
7
|
export type * from "./types";
|
|
8
|
+
export { usePrefersReducedMotion } from "./use-prefers-reduced-motion";
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type * as React from "react";
|
|
2
|
+
export type FloatingAlign = "start" | "end";
|
|
3
|
+
export type PlaceFloatingOptions = {
|
|
4
|
+
/** The trigger's box, from `getBoundingClientRect()`. */
|
|
5
|
+
rect: DOMRect;
|
|
6
|
+
/** Which of the trigger's INLINE edges the layer lines up with. */
|
|
7
|
+
align?: FloatingAlign;
|
|
8
|
+
/** Distance between the trigger and the layer. */
|
|
9
|
+
gap: number;
|
|
10
|
+
/** Flip above the trigger when there is less room below than this. */
|
|
11
|
+
flipBelow: number;
|
|
12
|
+
/** The element the layer belongs to; its computed direction decides sides. */
|
|
13
|
+
anchor?: Element | null;
|
|
14
|
+
};
|
|
15
|
+
export type PlacedFloating = {
|
|
16
|
+
style: React.CSSProperties;
|
|
17
|
+
/** True when the layer was flipped above the trigger. */
|
|
18
|
+
up: boolean;
|
|
19
|
+
};
|
|
20
|
+
export declare function placeFloating({ rect, align, gap, flipBelow, anchor, }: PlaceFloatingOptions): PlacedFloating;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Whether the user has asked for reduced motion, live.
|
|
3
|
+
*
|
|
4
|
+
* crisp's CSS carries a `@media (prefers-reduced-motion: reduce)` override in
|
|
5
|
+
* every animating recipe, and `bun run spec` checks it. That covers CSS.
|
|
6
|
+
* Motion driven from JavaScript — `scrollTo({ behavior: "smooth" })`,
|
|
7
|
+
* `element.animate()` — is invisible to a stylesheet: `scroll-behavior` in CSS
|
|
8
|
+
* does NOT override an explicit `behavior` argument. Those call sites have to
|
|
9
|
+
* read the preference here (#208).
|
|
10
|
+
*
|
|
11
|
+
* The server snapshot is `false` rather than "unknown" on purpose. It is the
|
|
12
|
+
* only value that keeps the markup identical on both sides, and a component
|
|
13
|
+
* using this must not put the result in its output for the same reason — the
|
|
14
|
+
* preference belongs in a handler or an effect, where it changes behaviour
|
|
15
|
+
* rather than HTML.
|
|
16
|
+
*/
|
|
17
|
+
export declare function usePrefersReducedMotion(): boolean;
|