@webority/mobile-ui 0.0.37 → 0.0.38
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/lib/commonjs/components/AuthedImage/authedImageCache.js +2 -17
- package/lib/commonjs/components/Carousel/Carousel.js +7 -5
- package/lib/commonjs/components/FieldBase/useFloatingLabel.js +18 -7
- package/lib/commonjs/components/OptionSheet/OptionSheet.js +18 -2
- package/lib/commonjs/form/equal.js +56 -0
- package/lib/commonjs/form/useForm.js +19 -4
- package/lib/module/components/AuthedImage/authedImageCache.js +1 -16
- package/lib/module/components/Carousel/Carousel.js +7 -5
- package/lib/module/components/FieldBase/useFloatingLabel.js +18 -7
- package/lib/module/components/OptionSheet/OptionSheet.js +18 -2
- package/lib/module/form/equal.js +51 -0
- package/lib/module/form/useForm.js +19 -4
- package/lib/typescript/commonjs/components/Carousel/Carousel.d.ts +2 -3
- package/lib/typescript/commonjs/form/equal.d.ts +9 -0
- package/lib/typescript/module/components/Carousel/Carousel.d.ts +2 -3
- package/lib/typescript/module/form/equal.d.ts +9 -0
- package/package.json +2 -2
|
@@ -32,22 +32,6 @@ const getAuthedImageCacheLimit = () => byteCeiling;
|
|
|
32
32
|
*/
|
|
33
33
|
exports.getAuthedImageCacheLimit = getAuthedImageCacheLimit;
|
|
34
34
|
const deriveCacheKey = (uri, override) => (0, _hashString.hashString)(override ?? uri);
|
|
35
|
-
exports.deriveCacheKey = deriveCacheKey;
|
|
36
|
-
const B64_ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
|
|
37
|
-
const encodeBase64 = buffer => {
|
|
38
|
-
const bytes = new Uint8Array(buffer);
|
|
39
|
-
let out = '';
|
|
40
|
-
for (let i = 0; i < bytes.length; i += 3) {
|
|
41
|
-
const b0 = bytes[i];
|
|
42
|
-
const b1 = i + 1 < bytes.length ? bytes[i + 1] : undefined;
|
|
43
|
-
const b2 = i + 2 < bytes.length ? bytes[i + 2] : undefined;
|
|
44
|
-
out += B64_ALPHABET[b0 >> 2];
|
|
45
|
-
out += B64_ALPHABET[(b0 & 0x03) << 4 | (b1 ?? 0) >> 4];
|
|
46
|
-
out += b1 === undefined ? '=' : B64_ALPHABET[((b1 & 0x0f) << 2 | (b2 ?? 0) >> 6) & 0x3f];
|
|
47
|
-
out += b2 === undefined ? '=' : B64_ALPHABET[b2 & 0x3f];
|
|
48
|
-
}
|
|
49
|
-
return out;
|
|
50
|
-
};
|
|
51
35
|
|
|
52
36
|
/**
|
|
53
37
|
* The cache is an OPTIMISATION, so a failing store must never stop an image
|
|
@@ -60,6 +44,7 @@ const encodeBase64 = buffer => {
|
|
|
60
44
|
* handling below already covers: each read returns null and each write returns
|
|
61
45
|
* false, the image still fetches, and nothing is logged per image.
|
|
62
46
|
*/
|
|
47
|
+
exports.deriveCacheKey = deriveCacheKey;
|
|
63
48
|
const cacheUsable = () => (0, _mobileCore.isStorageAvailable)();
|
|
64
49
|
const readEntry = async cacheKey => {
|
|
65
50
|
if (!cacheUsable()) {
|
|
@@ -197,7 +182,7 @@ const fetchAndStore = async (cacheKey, options, signal) => {
|
|
|
197
182
|
const buffer = response.data ?? new ArrayBuffer(0);
|
|
198
183
|
const rawType = responseHeader(response.headers, 'content-type');
|
|
199
184
|
const contentType = (rawType ? rawType.split(';')[0].trim() : '') || 'image/jpeg';
|
|
200
|
-
const dataUri = `data:${contentType};base64,${encodeBase64(buffer)}`;
|
|
185
|
+
const dataUri = `data:${contentType};base64,${(0, _mobileCore.encodeBase64)(buffer)}`;
|
|
201
186
|
const now = Date.now();
|
|
202
187
|
// `bytes` measures the stored data URI, not the decoded image: the ceiling
|
|
203
188
|
// exists to bound what sits in storage, and base64 costs about a third more.
|
|
@@ -15,9 +15,8 @@ function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r
|
|
|
15
15
|
/**
|
|
16
16
|
* Carousel — generic horizontal carousel with pagination, auto-play, and looping.
|
|
17
17
|
*
|
|
18
|
-
* Backed by a virtualized FlatList for memory efficiency.
|
|
19
|
-
*
|
|
20
|
-
* gesture frame-perfectly without round-tripping to JS.
|
|
18
|
+
* Backed by a virtualized FlatList for memory efficiency. Dot and bar
|
|
19
|
+
* pagination track the scroll gesture through `Animated.event`.
|
|
21
20
|
*
|
|
22
21
|
* USAGE:
|
|
23
22
|
*
|
|
@@ -178,6 +177,9 @@ const CarouselInner = /*#__PURE__*/(0, _react.forwardRef)((props, ref) => {
|
|
|
178
177
|
}, [startAutoPlay, clearAutoPlay]);
|
|
179
178
|
|
|
180
179
|
// ─── Scroll handlers ───
|
|
180
|
+
// The dot and bar widths interpolate on the JS driver, so every scroll frame
|
|
181
|
+
// crosses to JS. Only those indicators need it; without them nothing listens.
|
|
182
|
+
const tracksScroll = showPagination && data.length > 1 && paginationStyle !== 'numbers';
|
|
181
183
|
const onScroll = (0, _react.useMemo)(() => _reactNative.Animated.event([{
|
|
182
184
|
nativeEvent: {
|
|
183
185
|
contentOffset: {
|
|
@@ -282,8 +284,8 @@ const CarouselInner = /*#__PURE__*/(0, _react.forwardRef)((props, ref) => {
|
|
|
282
284
|
snapToAlignment: "start",
|
|
283
285
|
decelerationRate: "fast",
|
|
284
286
|
showsHorizontalScrollIndicator: false,
|
|
285
|
-
onScroll: onScroll,
|
|
286
|
-
scrollEventThrottle: 16,
|
|
287
|
+
onScroll: tracksScroll ? onScroll : undefined,
|
|
288
|
+
scrollEventThrottle: tracksScroll ? 16 : undefined,
|
|
287
289
|
onMomentumScrollEnd: onMomentumScrollEnd,
|
|
288
290
|
onScrollBeginDrag: onScrollBeginDrag,
|
|
289
291
|
onScrollEndDrag: onScrollEndDrag,
|
|
@@ -49,19 +49,30 @@ const useFloatingLabel = ({
|
|
|
49
49
|
style
|
|
50
50
|
}) => {
|
|
51
51
|
const theme = (0, _index.useTheme)();
|
|
52
|
+
// Two values on one timeline. The label only moves by transform, so it runs
|
|
53
|
+
// on the native driver and the float never waits on the JS thread, which is
|
|
54
|
+
// busy exactly then with the keyboard opening. The outlined notch animates a
|
|
55
|
+
// width, which only the JS driver can, so it has its own value.
|
|
52
56
|
const anim = (0, _react.useRef)((0, _index.createAnimatedValue)(floated ? 1 : 0)).current;
|
|
57
|
+
const notchAnim = (0, _react.useRef)((0, _index.createAnimatedValue)(floated ? 1 : 0)).current;
|
|
58
|
+
const notched = variant === 'outlined';
|
|
53
59
|
// The label's unscaled layout inside the wrapper; sizes the notch.
|
|
54
60
|
const [layout, setLayout] = (0, _react.useState)(null);
|
|
55
61
|
(0, _react.useEffect)(() => {
|
|
56
|
-
const
|
|
57
|
-
|
|
62
|
+
const to = floated ? 1 : 0;
|
|
63
|
+
const timing = (value, useNativeDriver) => _reactNative.Animated.timing(value, {
|
|
64
|
+
toValue: to,
|
|
58
65
|
duration: theme.motion.duration.fast,
|
|
59
66
|
easing: _reactNative.Easing.bezier(...theme.motion.easing.standard),
|
|
60
|
-
useNativeDriver
|
|
67
|
+
useNativeDriver
|
|
61
68
|
});
|
|
69
|
+
if (!notched) {
|
|
70
|
+
notchAnim.setValue(to);
|
|
71
|
+
}
|
|
72
|
+
const a = notched ? _reactNative.Animated.parallel([timing(anim, true), timing(notchAnim, false)]) : timing(anim, true);
|
|
62
73
|
a.start();
|
|
63
74
|
return () => a.stop();
|
|
64
|
-
}, [floated, anim, theme.motion.duration.fast, theme.motion.easing.standard]);
|
|
75
|
+
}, [floated, notched, anim, notchAnim, theme.motion.duration.fast, theme.motion.easing.standard]);
|
|
65
76
|
return (0, _react.useMemo)(() => {
|
|
66
77
|
if (!enabled || !label) {
|
|
67
78
|
return {
|
|
@@ -117,9 +128,9 @@ const useFloatingLabel = ({
|
|
|
117
128
|
// opening with the float animation.
|
|
118
129
|
const floatedWidth = layout ? layout.width * FLOATED_LABEL_SCALE : 0;
|
|
119
130
|
const originShift = !layout ? 0 : textAlign === 'right' ? layout.width - floatedWidth : textAlign === 'center' ? (layout.width - floatedWidth) / 2 : 0;
|
|
120
|
-
const topNotch =
|
|
131
|
+
const topNotch = notched && layout ? {
|
|
121
132
|
left: layout.x + floatShift + originShift - NOTCH_GAP,
|
|
122
|
-
width:
|
|
133
|
+
width: notchAnim.interpolate({
|
|
123
134
|
inputRange: [0, 1],
|
|
124
135
|
outputRange: [0, floatedWidth + NOTCH_GAP * 2]
|
|
125
136
|
})
|
|
@@ -158,7 +169,7 @@ const useFloatingLabel = ({
|
|
|
158
169
|
node,
|
|
159
170
|
topNotch
|
|
160
171
|
};
|
|
161
|
-
}, [enabled, label, required,
|
|
172
|
+
}, [enabled, label, required, notched, sizeTokens, leadingOffset, trailingOffset, textAlign, paddingLeft, lineHeight, restNudge, floatTrim, color, style, anim, notchAnim, layout, theme]);
|
|
162
173
|
};
|
|
163
174
|
exports.useFloatingLabel = useFloatingLabel;
|
|
164
175
|
const styles = _reactNative.StyleSheet.create({
|
|
@@ -273,19 +273,35 @@ const OptionSheet = props => {
|
|
|
273
273
|
// Rows are not measured until rendered, so a jump far past the rendered
|
|
274
274
|
// window cannot land in one go: scroll near it by the average row height,
|
|
275
275
|
// then retry once the window has caught up.
|
|
276
|
+
const jumpRetryRef = (0, _react.useRef)(null);
|
|
277
|
+
const cancelJumpRetry = (0, _react.useCallback)(() => {
|
|
278
|
+
if (jumpRetryRef.current !== null) {
|
|
279
|
+
clearTimeout(jumpRetryRef.current);
|
|
280
|
+
jumpRetryRef.current = null;
|
|
281
|
+
}
|
|
282
|
+
}, []);
|
|
276
283
|
const handleScrollToIndexFailed = (0, _react.useCallback)(info => {
|
|
277
284
|
listRef.current?.scrollToOffset({
|
|
278
285
|
offset: info.averageItemLength * info.index,
|
|
279
286
|
animated: false
|
|
280
287
|
});
|
|
281
|
-
|
|
288
|
+
cancelJumpRetry();
|
|
289
|
+
jumpRetryRef.current = setTimeout(() => {
|
|
290
|
+
jumpRetryRef.current = null;
|
|
282
291
|
listRef.current?.scrollToIndex({
|
|
283
292
|
index: info.index,
|
|
284
293
|
animated: true,
|
|
285
294
|
viewPosition: 0
|
|
286
295
|
});
|
|
287
296
|
}, 50);
|
|
288
|
-
}, []);
|
|
297
|
+
}, [cancelJumpRetry]);
|
|
298
|
+
// A retry pending when the sheet closes or unmounts would scroll a list that is gone.
|
|
299
|
+
(0, _react.useEffect)(() => {
|
|
300
|
+
if (!rendered) {
|
|
301
|
+
cancelJumpRetry();
|
|
302
|
+
}
|
|
303
|
+
return cancelJumpRetry;
|
|
304
|
+
}, [rendered, cancelJumpRetry]);
|
|
289
305
|
const renderRow = ({
|
|
290
306
|
item: row
|
|
291
307
|
}) => {
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.valuesEqual = void 0;
|
|
7
|
+
const isPlainObject = value => {
|
|
8
|
+
if (value === null || typeof value !== 'object') {
|
|
9
|
+
return false;
|
|
10
|
+
}
|
|
11
|
+
const proto = Object.getPrototypeOf(value);
|
|
12
|
+
return proto === Object.prototype || proto === null;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Whether two form value trees hold the same data. The form engine copies only
|
|
17
|
+
* the branch a write touches, so most subtrees are compared by identity and the
|
|
18
|
+
* walk stops early; serialising both trees cost a full pass on every keystroke.
|
|
19
|
+
* A key holding `undefined` counts as absent, the way the serialised comparison
|
|
20
|
+
* treated it, and dates compare by instant.
|
|
21
|
+
*/
|
|
22
|
+
const valuesEqual = (a, b) => {
|
|
23
|
+
if (Object.is(a, b)) {
|
|
24
|
+
return true;
|
|
25
|
+
}
|
|
26
|
+
if (a instanceof Date && b instanceof Date) {
|
|
27
|
+
return a.getTime() === b.getTime();
|
|
28
|
+
}
|
|
29
|
+
if (Array.isArray(a) || Array.isArray(b)) {
|
|
30
|
+
if (!Array.isArray(a) || !Array.isArray(b) || a.length !== b.length) {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
for (let i = 0; i < a.length; i += 1) {
|
|
34
|
+
if (!valuesEqual(a[i], b[i])) {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return true;
|
|
39
|
+
}
|
|
40
|
+
if (!isPlainObject(a) || !isPlainObject(b)) {
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
const keysA = Object.keys(a).filter(key => a[key] !== undefined);
|
|
44
|
+
const keysB = Object.keys(b).filter(key => b[key] !== undefined);
|
|
45
|
+
if (keysA.length !== keysB.length) {
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
for (const key of keysA) {
|
|
49
|
+
if (!valuesEqual(a[key], b[key])) {
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return true;
|
|
54
|
+
};
|
|
55
|
+
exports.valuesEqual = valuesEqual;
|
|
56
|
+
//# sourceMappingURL=equal.js.map
|
|
@@ -5,6 +5,7 @@ Object.defineProperty(exports, "__esModule", {
|
|
|
5
5
|
});
|
|
6
6
|
exports.useForm = useForm;
|
|
7
7
|
var _react = require("react");
|
|
8
|
+
var _equal = require("./equal.js");
|
|
8
9
|
var _path = require("./path.js");
|
|
9
10
|
// Loosely typed (Values -> any) so the generic schema from any useForm<Values>
|
|
10
11
|
// instance flows in without variance friction; callers re-apply Values at use.
|
|
@@ -55,7 +56,21 @@ function useForm(config) {
|
|
|
55
56
|
const fieldOrder = (0, _react.useRef)([]);
|
|
56
57
|
const focusedFieldRef = (0, _react.useRef)(null);
|
|
57
58
|
|
|
58
|
-
//
|
|
59
|
+
// Reinitialize compares initial values by value, not identity, so a caller
|
|
60
|
+
// passing an inline object literal does not reset the form on every render.
|
|
61
|
+
const initialVersionRef = (0, _react.useRef)({
|
|
62
|
+
values: config.initialValues,
|
|
63
|
+
version: 0
|
|
64
|
+
});
|
|
65
|
+
if (!(0, _equal.valuesEqual)(config.initialValues, initialVersionRef.current.values)) {
|
|
66
|
+
initialVersionRef.current = {
|
|
67
|
+
values: config.initialValues,
|
|
68
|
+
version: initialVersionRef.current.version + 1
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
const initialVersion = initialVersionRef.current.version;
|
|
72
|
+
|
|
73
|
+
// biome-ignore lint/correctness/useExhaustiveDependencies: `initialVersion` stands for `config.initialValues` compared by value
|
|
59
74
|
(0, _react.useEffect)(() => {
|
|
60
75
|
if (!configRef.current.enableReinitialize) {
|
|
61
76
|
return;
|
|
@@ -68,7 +83,7 @@ function useForm(config) {
|
|
|
68
83
|
setValuesState(next);
|
|
69
84
|
setErrorsState({});
|
|
70
85
|
setTouchedState({});
|
|
71
|
-
}, [
|
|
86
|
+
}, [initialVersion]);
|
|
72
87
|
const validateField = (0, _react.useCallback)(async name => {
|
|
73
88
|
const validate = configRef.current.validate;
|
|
74
89
|
const allValues = ref.current.values;
|
|
@@ -312,7 +327,7 @@ function useForm(config) {
|
|
|
312
327
|
}
|
|
313
328
|
return true;
|
|
314
329
|
}, [values, errors]);
|
|
315
|
-
const dirty = (0, _react.useMemo)(() =>
|
|
330
|
+
const dirty = (0, _react.useMemo)(() => !(0, _equal.valuesEqual)(values, initialRef.current), [values]);
|
|
316
331
|
return {
|
|
317
332
|
values,
|
|
318
333
|
errors: uiErrors,
|
|
@@ -345,7 +360,7 @@ function useForm(config) {
|
|
|
345
360
|
error: isTouched || submitCount > 0 ? rawError : undefined,
|
|
346
361
|
rawError,
|
|
347
362
|
touched: isTouched,
|
|
348
|
-
dirty:
|
|
363
|
+
dirty: !(0, _equal.valuesEqual)(value, (0, _path.getPath)(initialRef.current, name))
|
|
349
364
|
};
|
|
350
365
|
},
|
|
351
366
|
getFieldProps: name => ({
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
|
-
import { ACCESS_TOKEN_KEY, getToken, isStorageAvailable, Logger, publicClient, Storage } from '@webority/mobile-core';
|
|
3
|
+
import { ACCESS_TOKEN_KEY, encodeBase64, getToken, isStorageAvailable, Logger, publicClient, Storage } from '@webority/mobile-core';
|
|
4
4
|
import { hashString } from "../../utils/hashString.js";
|
|
5
5
|
const KEY_PREFIX = '@webority/authed-image/v1/';
|
|
6
6
|
const DATA_PREFIX = `${KEY_PREFIX}d/`;
|
|
@@ -26,21 +26,6 @@ export const getAuthedImageCacheLimit = () => byteCeiling;
|
|
|
26
26
|
* Hex only by construction, so it is always a safe storage key segment.
|
|
27
27
|
*/
|
|
28
28
|
export const deriveCacheKey = (uri, override) => hashString(override ?? uri);
|
|
29
|
-
const B64_ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
|
|
30
|
-
const encodeBase64 = buffer => {
|
|
31
|
-
const bytes = new Uint8Array(buffer);
|
|
32
|
-
let out = '';
|
|
33
|
-
for (let i = 0; i < bytes.length; i += 3) {
|
|
34
|
-
const b0 = bytes[i];
|
|
35
|
-
const b1 = i + 1 < bytes.length ? bytes[i + 1] : undefined;
|
|
36
|
-
const b2 = i + 2 < bytes.length ? bytes[i + 2] : undefined;
|
|
37
|
-
out += B64_ALPHABET[b0 >> 2];
|
|
38
|
-
out += B64_ALPHABET[(b0 & 0x03) << 4 | (b1 ?? 0) >> 4];
|
|
39
|
-
out += b1 === undefined ? '=' : B64_ALPHABET[((b1 & 0x0f) << 2 | (b2 ?? 0) >> 6) & 0x3f];
|
|
40
|
-
out += b2 === undefined ? '=' : B64_ALPHABET[b2 & 0x3f];
|
|
41
|
-
}
|
|
42
|
-
return out;
|
|
43
|
-
};
|
|
44
29
|
|
|
45
30
|
/**
|
|
46
31
|
* The cache is an OPTIMISATION, so a failing store must never stop an image
|
|
@@ -3,9 +3,8 @@
|
|
|
3
3
|
/**
|
|
4
4
|
* Carousel — generic horizontal carousel with pagination, auto-play, and looping.
|
|
5
5
|
*
|
|
6
|
-
* Backed by a virtualized FlatList for memory efficiency.
|
|
7
|
-
*
|
|
8
|
-
* gesture frame-perfectly without round-tripping to JS.
|
|
6
|
+
* Backed by a virtualized FlatList for memory efficiency. Dot and bar
|
|
7
|
+
* pagination track the scroll gesture through `Animated.event`.
|
|
9
8
|
*
|
|
10
9
|
* USAGE:
|
|
11
10
|
*
|
|
@@ -173,6 +172,9 @@ const CarouselInner = /*#__PURE__*/forwardRef((props, ref) => {
|
|
|
173
172
|
}, [startAutoPlay, clearAutoPlay]);
|
|
174
173
|
|
|
175
174
|
// ─── Scroll handlers ───
|
|
175
|
+
// The dot and bar widths interpolate on the JS driver, so every scroll frame
|
|
176
|
+
// crosses to JS. Only those indicators need it; without them nothing listens.
|
|
177
|
+
const tracksScroll = showPagination && data.length > 1 && paginationStyle !== 'numbers';
|
|
176
178
|
const onScroll = useMemo(() => Animated.event([{
|
|
177
179
|
nativeEvent: {
|
|
178
180
|
contentOffset: {
|
|
@@ -277,8 +279,8 @@ const CarouselInner = /*#__PURE__*/forwardRef((props, ref) => {
|
|
|
277
279
|
snapToAlignment: "start",
|
|
278
280
|
decelerationRate: "fast",
|
|
279
281
|
showsHorizontalScrollIndicator: false,
|
|
280
|
-
onScroll: onScroll,
|
|
281
|
-
scrollEventThrottle: 16,
|
|
282
|
+
onScroll: tracksScroll ? onScroll : undefined,
|
|
283
|
+
scrollEventThrottle: tracksScroll ? 16 : undefined,
|
|
282
284
|
onMomentumScrollEnd: onMomentumScrollEnd,
|
|
283
285
|
onScrollBeginDrag: onScrollBeginDrag,
|
|
284
286
|
onScrollEndDrag: onScrollEndDrag,
|
|
@@ -46,19 +46,30 @@ export const useFloatingLabel = ({
|
|
|
46
46
|
style
|
|
47
47
|
}) => {
|
|
48
48
|
const theme = useTheme();
|
|
49
|
+
// Two values on one timeline. The label only moves by transform, so it runs
|
|
50
|
+
// on the native driver and the float never waits on the JS thread, which is
|
|
51
|
+
// busy exactly then with the keyboard opening. The outlined notch animates a
|
|
52
|
+
// width, which only the JS driver can, so it has its own value.
|
|
49
53
|
const anim = useRef(createAnimatedValue(floated ? 1 : 0)).current;
|
|
54
|
+
const notchAnim = useRef(createAnimatedValue(floated ? 1 : 0)).current;
|
|
55
|
+
const notched = variant === 'outlined';
|
|
50
56
|
// The label's unscaled layout inside the wrapper; sizes the notch.
|
|
51
57
|
const [layout, setLayout] = useState(null);
|
|
52
58
|
useEffect(() => {
|
|
53
|
-
const
|
|
54
|
-
|
|
59
|
+
const to = floated ? 1 : 0;
|
|
60
|
+
const timing = (value, useNativeDriver) => Animated.timing(value, {
|
|
61
|
+
toValue: to,
|
|
55
62
|
duration: theme.motion.duration.fast,
|
|
56
63
|
easing: Easing.bezier(...theme.motion.easing.standard),
|
|
57
|
-
useNativeDriver
|
|
64
|
+
useNativeDriver
|
|
58
65
|
});
|
|
66
|
+
if (!notched) {
|
|
67
|
+
notchAnim.setValue(to);
|
|
68
|
+
}
|
|
69
|
+
const a = notched ? Animated.parallel([timing(anim, true), timing(notchAnim, false)]) : timing(anim, true);
|
|
59
70
|
a.start();
|
|
60
71
|
return () => a.stop();
|
|
61
|
-
}, [floated, anim, theme.motion.duration.fast, theme.motion.easing.standard]);
|
|
72
|
+
}, [floated, notched, anim, notchAnim, theme.motion.duration.fast, theme.motion.easing.standard]);
|
|
62
73
|
return useMemo(() => {
|
|
63
74
|
if (!enabled || !label) {
|
|
64
75
|
return {
|
|
@@ -114,9 +125,9 @@ export const useFloatingLabel = ({
|
|
|
114
125
|
// opening with the float animation.
|
|
115
126
|
const floatedWidth = layout ? layout.width * FLOATED_LABEL_SCALE : 0;
|
|
116
127
|
const originShift = !layout ? 0 : textAlign === 'right' ? layout.width - floatedWidth : textAlign === 'center' ? (layout.width - floatedWidth) / 2 : 0;
|
|
117
|
-
const topNotch =
|
|
128
|
+
const topNotch = notched && layout ? {
|
|
118
129
|
left: layout.x + floatShift + originShift - NOTCH_GAP,
|
|
119
|
-
width:
|
|
130
|
+
width: notchAnim.interpolate({
|
|
120
131
|
inputRange: [0, 1],
|
|
121
132
|
outputRange: [0, floatedWidth + NOTCH_GAP * 2]
|
|
122
133
|
})
|
|
@@ -155,7 +166,7 @@ export const useFloatingLabel = ({
|
|
|
155
166
|
node,
|
|
156
167
|
topNotch
|
|
157
168
|
};
|
|
158
|
-
}, [enabled, label, required,
|
|
169
|
+
}, [enabled, label, required, notched, sizeTokens, leadingOffset, trailingOffset, textAlign, paddingLeft, lineHeight, restNudge, floatTrim, color, style, anim, notchAnim, layout, theme]);
|
|
159
170
|
};
|
|
160
171
|
const styles = StyleSheet.create({
|
|
161
172
|
label: {
|
|
@@ -270,19 +270,35 @@ export const OptionSheet = props => {
|
|
|
270
270
|
// Rows are not measured until rendered, so a jump far past the rendered
|
|
271
271
|
// window cannot land in one go: scroll near it by the average row height,
|
|
272
272
|
// then retry once the window has caught up.
|
|
273
|
+
const jumpRetryRef = useRef(null);
|
|
274
|
+
const cancelJumpRetry = useCallback(() => {
|
|
275
|
+
if (jumpRetryRef.current !== null) {
|
|
276
|
+
clearTimeout(jumpRetryRef.current);
|
|
277
|
+
jumpRetryRef.current = null;
|
|
278
|
+
}
|
|
279
|
+
}, []);
|
|
273
280
|
const handleScrollToIndexFailed = useCallback(info => {
|
|
274
281
|
listRef.current?.scrollToOffset({
|
|
275
282
|
offset: info.averageItemLength * info.index,
|
|
276
283
|
animated: false
|
|
277
284
|
});
|
|
278
|
-
|
|
285
|
+
cancelJumpRetry();
|
|
286
|
+
jumpRetryRef.current = setTimeout(() => {
|
|
287
|
+
jumpRetryRef.current = null;
|
|
279
288
|
listRef.current?.scrollToIndex({
|
|
280
289
|
index: info.index,
|
|
281
290
|
animated: true,
|
|
282
291
|
viewPosition: 0
|
|
283
292
|
});
|
|
284
293
|
}, 50);
|
|
285
|
-
}, []);
|
|
294
|
+
}, [cancelJumpRetry]);
|
|
295
|
+
// A retry pending when the sheet closes or unmounts would scroll a list that is gone.
|
|
296
|
+
useEffect(() => {
|
|
297
|
+
if (!rendered) {
|
|
298
|
+
cancelJumpRetry();
|
|
299
|
+
}
|
|
300
|
+
return cancelJumpRetry;
|
|
301
|
+
}, [rendered, cancelJumpRetry]);
|
|
286
302
|
const renderRow = ({
|
|
287
303
|
item: row
|
|
288
304
|
}) => {
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const isPlainObject = value => {
|
|
4
|
+
if (value === null || typeof value !== 'object') {
|
|
5
|
+
return false;
|
|
6
|
+
}
|
|
7
|
+
const proto = Object.getPrototypeOf(value);
|
|
8
|
+
return proto === Object.prototype || proto === null;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Whether two form value trees hold the same data. The form engine copies only
|
|
13
|
+
* the branch a write touches, so most subtrees are compared by identity and the
|
|
14
|
+
* walk stops early; serialising both trees cost a full pass on every keystroke.
|
|
15
|
+
* A key holding `undefined` counts as absent, the way the serialised comparison
|
|
16
|
+
* treated it, and dates compare by instant.
|
|
17
|
+
*/
|
|
18
|
+
export const valuesEqual = (a, b) => {
|
|
19
|
+
if (Object.is(a, b)) {
|
|
20
|
+
return true;
|
|
21
|
+
}
|
|
22
|
+
if (a instanceof Date && b instanceof Date) {
|
|
23
|
+
return a.getTime() === b.getTime();
|
|
24
|
+
}
|
|
25
|
+
if (Array.isArray(a) || Array.isArray(b)) {
|
|
26
|
+
if (!Array.isArray(a) || !Array.isArray(b) || a.length !== b.length) {
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
for (let i = 0; i < a.length; i += 1) {
|
|
30
|
+
if (!valuesEqual(a[i], b[i])) {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
36
|
+
if (!isPlainObject(a) || !isPlainObject(b)) {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
const keysA = Object.keys(a).filter(key => a[key] !== undefined);
|
|
40
|
+
const keysB = Object.keys(b).filter(key => b[key] !== undefined);
|
|
41
|
+
if (keysA.length !== keysB.length) {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
for (const key of keysA) {
|
|
45
|
+
if (!valuesEqual(a[key], b[key])) {
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return true;
|
|
50
|
+
};
|
|
51
|
+
//# sourceMappingURL=equal.js.map
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
3
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
4
|
+
import { valuesEqual } from "./equal.js";
|
|
4
5
|
import { getPath, setPath } from "./path.js";
|
|
5
6
|
// Loosely typed (Values -> any) so the generic schema from any useForm<Values>
|
|
6
7
|
// instance flows in without variance friction; callers re-apply Values at use.
|
|
@@ -51,7 +52,21 @@ export function useForm(config) {
|
|
|
51
52
|
const fieldOrder = useRef([]);
|
|
52
53
|
const focusedFieldRef = useRef(null);
|
|
53
54
|
|
|
54
|
-
//
|
|
55
|
+
// Reinitialize compares initial values by value, not identity, so a caller
|
|
56
|
+
// passing an inline object literal does not reset the form on every render.
|
|
57
|
+
const initialVersionRef = useRef({
|
|
58
|
+
values: config.initialValues,
|
|
59
|
+
version: 0
|
|
60
|
+
});
|
|
61
|
+
if (!valuesEqual(config.initialValues, initialVersionRef.current.values)) {
|
|
62
|
+
initialVersionRef.current = {
|
|
63
|
+
values: config.initialValues,
|
|
64
|
+
version: initialVersionRef.current.version + 1
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
const initialVersion = initialVersionRef.current.version;
|
|
68
|
+
|
|
69
|
+
// biome-ignore lint/correctness/useExhaustiveDependencies: `initialVersion` stands for `config.initialValues` compared by value
|
|
55
70
|
useEffect(() => {
|
|
56
71
|
if (!configRef.current.enableReinitialize) {
|
|
57
72
|
return;
|
|
@@ -64,7 +79,7 @@ export function useForm(config) {
|
|
|
64
79
|
setValuesState(next);
|
|
65
80
|
setErrorsState({});
|
|
66
81
|
setTouchedState({});
|
|
67
|
-
}, [
|
|
82
|
+
}, [initialVersion]);
|
|
68
83
|
const validateField = useCallback(async name => {
|
|
69
84
|
const validate = configRef.current.validate;
|
|
70
85
|
const allValues = ref.current.values;
|
|
@@ -308,7 +323,7 @@ export function useForm(config) {
|
|
|
308
323
|
}
|
|
309
324
|
return true;
|
|
310
325
|
}, [values, errors]);
|
|
311
|
-
const dirty = useMemo(() =>
|
|
326
|
+
const dirty = useMemo(() => !valuesEqual(values, initialRef.current), [values]);
|
|
312
327
|
return {
|
|
313
328
|
values,
|
|
314
329
|
errors: uiErrors,
|
|
@@ -341,7 +356,7 @@ export function useForm(config) {
|
|
|
341
356
|
error: isTouched || submitCount > 0 ? rawError : undefined,
|
|
342
357
|
rawError,
|
|
343
358
|
touched: isTouched,
|
|
344
|
-
dirty:
|
|
359
|
+
dirty: !valuesEqual(value, getPath(initialRef.current, name))
|
|
345
360
|
};
|
|
346
361
|
},
|
|
347
362
|
getFieldProps: name => ({
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Carousel — generic horizontal carousel with pagination, auto-play, and looping.
|
|
3
3
|
*
|
|
4
|
-
* Backed by a virtualized FlatList for memory efficiency.
|
|
5
|
-
*
|
|
6
|
-
* gesture frame-perfectly without round-tripping to JS.
|
|
4
|
+
* Backed by a virtualized FlatList for memory efficiency. Dot and bar
|
|
5
|
+
* pagination track the scroll gesture through `Animated.event`.
|
|
7
6
|
*
|
|
8
7
|
* USAGE:
|
|
9
8
|
*
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Whether two form value trees hold the same data. The form engine copies only
|
|
3
|
+
* the branch a write touches, so most subtrees are compared by identity and the
|
|
4
|
+
* walk stops early; serialising both trees cost a full pass on every keystroke.
|
|
5
|
+
* A key holding `undefined` counts as absent, the way the serialised comparison
|
|
6
|
+
* treated it, and dates compare by instant.
|
|
7
|
+
*/
|
|
8
|
+
export declare const valuesEqual: (a: unknown, b: unknown) => boolean;
|
|
9
|
+
//# sourceMappingURL=equal.d.ts.map
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Carousel — generic horizontal carousel with pagination, auto-play, and looping.
|
|
3
3
|
*
|
|
4
|
-
* Backed by a virtualized FlatList for memory efficiency.
|
|
5
|
-
*
|
|
6
|
-
* gesture frame-perfectly without round-tripping to JS.
|
|
4
|
+
* Backed by a virtualized FlatList for memory efficiency. Dot and bar
|
|
5
|
+
* pagination track the scroll gesture through `Animated.event`.
|
|
7
6
|
*
|
|
8
7
|
* USAGE:
|
|
9
8
|
*
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Whether two form value trees hold the same data. The form engine copies only
|
|
3
|
+
* the branch a write touches, so most subtrees are compared by identity and the
|
|
4
|
+
* walk stops early; serialising both trees cost a full pass on every keystroke.
|
|
5
|
+
* A key holding `undefined` counts as absent, the way the serialised comparison
|
|
6
|
+
* treated it, and dates compare by instant.
|
|
7
|
+
*/
|
|
8
|
+
export declare const valuesEqual: (a: unknown, b: unknown) => boolean;
|
|
9
|
+
//# sourceMappingURL=equal.d.ts.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@webority/mobile-ui",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.38",
|
|
4
4
|
"description": "Beautiful, animated, accessible React Native components, theme and form engine for Webority projects.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"react-native",
|
|
@@ -64,7 +64,7 @@
|
|
|
64
64
|
"typecheck": "tsc --noEmit"
|
|
65
65
|
},
|
|
66
66
|
"peerDependencies": {
|
|
67
|
-
"@webority/mobile-core": ">=0.0.
|
|
67
|
+
"@webority/mobile-core": ">=0.0.27 <0.1.0",
|
|
68
68
|
"expo": ">=57.0.0",
|
|
69
69
|
"expo-haptics": ">=57.0.0",
|
|
70
70
|
"react": "^19.1.0",
|