@unif/react-native-design 0.32.2 → 0.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.
Files changed (64) hide show
  1. package/lib/module/components/ui/Avatar/Avatar.js +3 -3
  2. package/lib/module/components/ui/Avatar/Avatar.js.map +1 -1
  3. package/lib/module/components/ui/DrawerHeader/DrawerHeader.js +3 -3
  4. package/lib/module/components/ui/DrawerHeader/DrawerHeader.js.map +1 -1
  5. package/lib/module/components/ui/TextField/TextFieldBase.js +7 -1
  6. package/lib/module/components/ui/TextField/TextFieldBase.js.map +1 -1
  7. package/lib/module/components/ui/TextField/constants.js +2 -1
  8. package/lib/module/components/ui/TextField/constants.js.map +1 -1
  9. package/lib/module/components/ui/TextField/styles.js +5 -0
  10. package/lib/module/components/ui/TextField/styles.js.map +1 -1
  11. package/lib/module/components/ui/TextField/useMultilineLayout.js +4 -4
  12. package/lib/module/components/ui/TextField/useMultilineLayout.js.map +1 -1
  13. package/lib/module/components/ui/TextField/useMultilineLayout.web.js +4 -4
  14. package/lib/module/components/ui/TextField/useMultilineLayout.web.js.map +1 -1
  15. package/lib/module/components/ui/Textarea/Textarea.js +7 -3
  16. package/lib/module/components/ui/Textarea/Textarea.js.map +1 -1
  17. package/lib/module/components/ui/Thumbnail/Thumbnail.js +3 -3
  18. package/lib/module/components/ui/Thumbnail/Thumbnail.js.map +1 -1
  19. package/lib/module/components/ui/shared/ImageAttempt.js.map +1 -1
  20. package/lib/module/components/ui/shared/selectImageAttemptSource.shared.js +6 -5
  21. package/lib/module/components/ui/shared/selectImageAttemptSource.shared.js.map +1 -1
  22. package/lib/module/components/ui/shared/useImageSource.js +11 -0
  23. package/lib/module/components/ui/shared/useImageSource.js.map +1 -0
  24. package/lib/module/utils/imageSource.js +47 -13
  25. package/lib/module/utils/imageSource.js.map +1 -1
  26. package/lib/typescript/src/components/ui/TextField/TextFieldBase.d.ts.map +1 -1
  27. package/lib/typescript/src/components/ui/TextField/constants.d.ts +1 -0
  28. package/lib/typescript/src/components/ui/TextField/constants.d.ts.map +1 -1
  29. package/lib/typescript/src/components/ui/TextField/styles.d.ts +5 -0
  30. package/lib/typescript/src/components/ui/TextField/styles.d.ts.map +1 -1
  31. package/lib/typescript/src/components/ui/TextField/types.d.ts +3 -0
  32. package/lib/typescript/src/components/ui/TextField/types.d.ts.map +1 -1
  33. package/lib/typescript/src/components/ui/TextField/useMultilineLayout.d.ts +1 -1
  34. package/lib/typescript/src/components/ui/TextField/useMultilineLayout.d.ts.map +1 -1
  35. package/lib/typescript/src/components/ui/TextField/useMultilineLayout.web.d.ts +1 -1
  36. package/lib/typescript/src/components/ui/TextField/useMultilineLayout.web.d.ts.map +1 -1
  37. package/lib/typescript/src/components/ui/Textarea/Textarea.d.ts +1 -1
  38. package/lib/typescript/src/components/ui/Textarea/Textarea.d.ts.map +1 -1
  39. package/lib/typescript/src/components/ui/Textarea/types.d.ts +4 -2
  40. package/lib/typescript/src/components/ui/Textarea/types.d.ts.map +1 -1
  41. package/lib/typescript/src/components/ui/shared/ImageAttempt.d.ts +3 -2
  42. package/lib/typescript/src/components/ui/shared/ImageAttempt.d.ts.map +1 -1
  43. package/lib/typescript/src/components/ui/shared/selectImageAttemptSource.shared.d.ts +3 -2
  44. package/lib/typescript/src/components/ui/shared/selectImageAttemptSource.shared.d.ts.map +1 -1
  45. package/lib/typescript/src/components/ui/shared/useImageSource.d.ts +6 -0
  46. package/lib/typescript/src/components/ui/shared/useImageSource.d.ts.map +1 -0
  47. package/lib/typescript/src/utils/imageSource.d.ts +3 -2
  48. package/lib/typescript/src/utils/imageSource.d.ts.map +1 -1
  49. package/package.json +1 -1
  50. package/src/components/ui/Avatar/Avatar.tsx +3 -3
  51. package/src/components/ui/DrawerHeader/DrawerHeader.tsx +3 -3
  52. package/src/components/ui/TextField/TextFieldBase.tsx +13 -1
  53. package/src/components/ui/TextField/constants.ts +2 -1
  54. package/src/components/ui/TextField/styles.ts +5 -0
  55. package/src/components/ui/TextField/types.ts +3 -0
  56. package/src/components/ui/TextField/useMultilineLayout.ts +3 -3
  57. package/src/components/ui/TextField/useMultilineLayout.web.ts +4 -5
  58. package/src/components/ui/Textarea/Textarea.tsx +8 -3
  59. package/src/components/ui/Textarea/types.ts +4 -2
  60. package/src/components/ui/Thumbnail/Thumbnail.tsx +3 -3
  61. package/src/components/ui/shared/ImageAttempt.tsx +3 -2
  62. package/src/components/ui/shared/selectImageAttemptSource.shared.ts +7 -5
  63. package/src/components/ui/shared/useImageSource.ts +8 -0
  64. package/src/utils/imageSource.ts +78 -15
@@ -1,5 +1,4 @@
1
1
  import { useCallback, useLayoutEffect, useState } from 'react';
2
- import { TEXTAREA_VERTICAL_INSET } from './constants';
3
2
  import type {
4
3
  MultilineLayout,
5
4
  MultilineLayoutInput,
@@ -12,17 +11,16 @@ export function useMultilineLayout({
12
11
  value,
13
12
  minHeight,
14
13
  maxHeight,
14
+ verticalInset,
15
15
  fontSize,
16
16
  placeholder,
17
17
  inputRef,
18
18
  }: MultilineLayoutInput): MultilineLayout {
19
19
  const [contentHeight, setContentHeight] = useState(0);
20
20
  const [width, setWidth] = useState(0);
21
- const minimum = Math.max(0, minHeight - TEXTAREA_VERTICAL_INSET);
21
+ const minimum = Math.max(0, minHeight - verticalInset);
22
22
  const maximum =
23
- maxHeight === undefined
24
- ? Infinity
25
- : Math.max(0, maxHeight - TEXTAREA_VERTICAL_INSET);
23
+ maxHeight === undefined ? Infinity : Math.max(0, maxHeight - verticalInset);
26
24
  useLayoutEffect(() => {
27
25
  if (!enabled || inputRef.current === null) return;
28
26
  const node = inputRef.current as unknown as WebTextInputMeasurement;
@@ -43,6 +41,7 @@ export function useMultilineLayout({
43
41
  placeholder,
44
42
  minHeight,
45
43
  maxHeight,
44
+ verticalInset,
46
45
  inputRef,
47
46
  ]);
48
47
  const onLayout = useCallback<NonNullable<MultilineLayout['onLayout']>>(
@@ -5,7 +5,7 @@ import type { TextFieldHandle } from '../TextField/types';
5
5
  import type { TextareaProps } from './types';
6
6
 
7
7
  /**
8
- * 多行文本输入框 —— 视觉与 Input 一致(idle/focus/filled/error/disabled)。
8
+ * 多行文本输入框 —— 默认视觉与 Input 一致;plain 供外层已提供表面的组合使用。
9
9
  *
10
10
  * 跟 Input 区别:
11
11
  * - 内置 `multiline=true`,业务不用传
@@ -17,12 +17,17 @@ import type { TextareaProps } from './types';
17
17
  * Ref:forwardRef<TextFieldHandle>,业务只可调 `focus()` / `blur()`。
18
18
  */
19
19
  export const Textarea = forwardRef<TextFieldHandle, TextareaProps>(
20
- function Textarea(props, ref) {
20
+ function Textarea({ surface = 'default', ...props }, ref) {
21
21
  const { props: safeProps } = sanitizeTextFieldWrapperProps(
22
22
  props as TextareaProps & Record<string, unknown>
23
23
  );
24
24
  return (
25
- <TextFieldBase ref={ref} {...(safeProps as TextareaProps)} multiline />
25
+ <TextFieldBase
26
+ ref={ref}
27
+ {...(safeProps as TextareaProps)}
28
+ multiline
29
+ plainSurface={surface === 'plain'}
30
+ />
26
31
  );
27
32
  }
28
33
  );
@@ -14,9 +14,11 @@ import type {
14
14
  export type TextareaProps = Omit<TextInputProps, RemovedTextInputProps> &
15
15
  TextFieldCommonProps &
16
16
  TextFieldValueProps &
17
- TextareaHeightProps;
17
+ TextareaLayoutProps;
18
18
 
19
- interface TextareaHeightProps {
19
+ interface TextareaLayoutProps {
20
+ /** default 绘制独立输入表面;plain 嵌入复合输入,保留文字内边距但不绘制表面。 */
21
+ surface?: 'default' | 'plain';
20
22
  /** 最小高度,默认 96(约 5 行) */
21
23
  minHeight?: number;
22
24
  /** 最大高度(超过后内部滚动),默认不限制 */
@@ -1,7 +1,7 @@
1
1
  import React, { useEffect } from 'react';
2
2
  import { StyleSheet, View } from 'react-native';
3
3
  import { useColors, useThemedStyles } from '../../../theme';
4
- import { resolveImageSource } from '../../../utils/imageSource';
4
+ import { useImageSource } from '../shared/useImageSource';
5
5
  import { createLogger } from '../../../utils/logger';
6
6
  import { ImageAttempt } from '../shared/ImageAttempt';
7
7
  import { A11Y_HIDDEN_PROPS } from '../shared/a11y';
@@ -45,7 +45,7 @@ export function Thumbnail(props: ThumbnailProps): React.JSX.Element {
45
45
  ? { uri: uri.trim() }
46
46
  : source
47
47
  : undefined;
48
- const resolvedSource = resolveImageSource(sourceCandidate);
48
+ const resolvedSource = useImageSource(sourceCandidate);
49
49
  const sanitizedImageStyle = sanitizeThumbnailImageStyle(imageStyle);
50
50
  const diagnostics = [
51
51
  ...normalizedDimensions.diagnostics,
@@ -94,7 +94,7 @@ export function Thumbnail(props: ThumbnailProps): React.JSX.Element {
94
94
  <ImageAttempt
95
95
  {...imageAccessibilityProps}
96
96
  key={resolvedSource.key}
97
- source={resolvedSource.source}
97
+ source={resolvedSource}
98
98
  fallback={fallbackContent}
99
99
  style={[sanitizedImageStyle.style, StyleSheet.absoluteFill]}
100
100
  resizeMode={resizeMode}
@@ -1,10 +1,11 @@
1
1
  import React, { useCallback, useState } from 'react';
2
2
  import { Image } from 'react-native';
3
- import type { ImageProps, ImageSourcePropType } from 'react-native';
3
+ import type { ImageProps } from 'react-native';
4
+ import type { ResolvedImageSource } from '../../../utils/imageSource';
4
5
  import { selectImageAttemptSource } from './selectImageAttemptSource';
5
6
 
6
7
  type ImageAttemptProps = Omit<ImageProps, 'onError' | 'source'> & {
7
- source: ImageSourcePropType;
8
+ source: ResolvedImageSource;
8
9
  fallback: React.ReactNode;
9
10
  };
10
11
 
@@ -1,16 +1,18 @@
1
1
  import type { ImageSourcePropType } from 'react-native';
2
- import { isValidImageSource } from '../../../utils/imageSource';
2
+ import type { ResolvedImageSource } from '../../../utils/imageSource';
3
3
 
4
+ // Input validation belongs to the media component's resolver. Its frozen result
5
+ // is safe to select here without reading or serializing the original input again.
4
6
  export function selectNativeImageAttemptSource(
5
- source: unknown
7
+ resolved: ResolvedImageSource | undefined
6
8
  ): ImageSourcePropType | undefined {
7
- return isValidImageSource(source) ? source : undefined;
9
+ return resolved?.source;
8
10
  }
9
11
 
10
12
  export function selectWebImageAttemptSource(
11
- source: unknown
13
+ resolved: ResolvedImageSource | undefined
12
14
  ): ImageSourcePropType | undefined {
13
- if (!isValidImageSource(source)) return undefined;
15
+ const source = resolved?.source;
14
16
 
15
17
  // RNW 0.21 不解析 native 的 candidate 数组;Web 明确使用第一候选项。
16
18
  return Array.isArray(source) ? source[0] : source;
@@ -0,0 +1,8 @@
1
+ import { useMemo } from 'react';
2
+ import { createImageSourceResolver } from '../../../utils/imageSource';
3
+
4
+ /** Reuse immutable image inputs without retaining a global source cache. */
5
+ export function useImageSource(source: unknown) {
6
+ const resolve = useMemo(createImageSourceResolver, []);
7
+ return resolve(source);
8
+ }
@@ -20,13 +20,14 @@ type SnapshotResult =
20
20
  valid: true;
21
21
  snapshot: SnapshotValue;
22
22
  canonical: string;
23
+ children?: ReadonlyMap<string, SnapshotResult>;
23
24
  }
24
25
  | {
25
26
  valid: false;
26
27
  canonical: string;
27
28
  };
28
29
 
29
- type ResolvedImageSource = Readonly<{
30
+ export type ResolvedImageSource = Readonly<{
30
31
  source: ImageSourcePropType;
31
32
  key: string;
32
33
  }>;
@@ -34,8 +35,12 @@ type ResolvedImageSource = Readonly<{
34
35
  const hasOwn = (value: object, key: PropertyKey): boolean =>
35
36
  Object.prototype.hasOwnProperty.call(value, key);
36
37
 
37
- function valid(snapshot: SnapshotValue, canonical: string): SnapshotResult {
38
- return { valid: true, snapshot, canonical };
38
+ function valid(
39
+ snapshot: SnapshotValue,
40
+ canonical: string,
41
+ children?: ReadonlyMap<string, SnapshotResult>
42
+ ): SnapshotResult {
43
+ return { valid: true, snapshot, canonical, children };
39
44
  }
40
45
 
41
46
  function invalid(kind: string): SnapshotResult {
@@ -45,7 +50,8 @@ function invalid(kind: string): SnapshotResult {
45
50
  function visitArray(
46
51
  value: object,
47
52
  keys: readonly string[],
48
- seen: WeakSet<object>
53
+ seen: WeakSet<object>,
54
+ previous?: SnapshotResult
49
55
  ): SnapshotResult {
50
56
  if (!keys.includes('length')) return invalid('descriptor');
51
57
 
@@ -67,6 +73,12 @@ function visitArray(
67
73
  const keySet = new Set(keys);
68
74
  const parts: string[] = [];
69
75
  const snapshot: SnapshotValue[] = [];
76
+ const children = new Map<string, SnapshotResult>();
77
+ const previousChildren = previous?.valid ? previous.children : undefined;
78
+ let unchanged =
79
+ previous?.valid === true &&
80
+ Array.isArray(previous.snapshot) &&
81
+ previousChildren?.size === length;
70
82
  let allValid = true;
71
83
 
72
84
  for (let index = 0; index < length; index += 1) {
@@ -82,7 +94,10 @@ function visitArray(
82
94
  return invalid('descriptor');
83
95
  }
84
96
 
85
- const item = visit(descriptor.value, seen);
97
+ const priorItem = previousChildren?.get(key);
98
+ const item = visit(descriptor.value, seen, priorItem);
99
+ children.set(key, item);
100
+ unchanged = unchanged && item === priorItem;
86
101
  parts.push(item.canonical);
87
102
  if (item.valid) {
88
103
  snapshot.push(item.snapshot);
@@ -91,19 +106,29 @@ function visitArray(
91
106
  }
92
107
  }
93
108
 
109
+ if (allValid && unchanged && previous?.valid) return previous;
94
110
  const canonical = `array:[${parts.join(',')}]`;
95
111
  return allValid
96
- ? valid(Object.freeze(snapshot), canonical)
112
+ ? valid(Object.freeze(snapshot), canonical, children)
97
113
  : { valid: false, canonical };
98
114
  }
99
115
 
100
116
  function visitObject(
101
117
  value: object,
102
118
  keys: readonly string[],
103
- seen: WeakSet<object>
119
+ seen: WeakSet<object>,
120
+ previous?: SnapshotResult
104
121
  ): SnapshotResult {
105
122
  const snapshot: Record<string, SnapshotValue> = {};
106
123
  const parts: string[] = [];
124
+ const children = new Map<string, SnapshotResult>();
125
+ const previousChildren = previous?.valid ? previous.children : undefined;
126
+ let unchanged =
127
+ previous?.valid === true &&
128
+ previous.snapshot !== null &&
129
+ typeof previous.snapshot === 'object' &&
130
+ !Array.isArray(previous.snapshot) &&
131
+ previousChildren?.size === keys.length;
107
132
  let allValid = true;
108
133
 
109
134
  for (const key of [...keys].sort()) {
@@ -112,7 +137,10 @@ function visitObject(
112
137
  if (!descriptor.enumerable) return invalid('non-enumerable');
113
138
  if (!hasOwn(descriptor, 'value')) return invalid('accessor');
114
139
 
115
- const item = visit(descriptor.value, seen);
140
+ const priorItem = previousChildren?.get(key);
141
+ const item = visit(descriptor.value, seen, priorItem);
142
+ children.set(key, item);
143
+ unchanged = unchanged && item === priorItem;
116
144
  parts.push(`${JSON.stringify(key)}:${item.canonical}`);
117
145
  if (item.valid) {
118
146
  Object.defineProperty(snapshot, key, {
@@ -126,9 +154,10 @@ function visitObject(
126
154
  }
127
155
  }
128
156
 
157
+ if (allValid && unchanged && previous?.valid) return previous;
129
158
  const canonical = `object:{${parts.join(',')}}`;
130
159
  return allValid
131
- ? valid(Object.freeze(snapshot), canonical)
160
+ ? valid(Object.freeze(snapshot), canonical, children)
132
161
  : { valid: false, canonical };
133
162
  }
134
163
 
@@ -136,7 +165,19 @@ function visitObject(
136
165
  * 只从 own data descriptor 读取一次,绝不执行 getter。snapshot 与 canonical
137
166
  * key 因而来自同一份值;任何 descriptor / Proxy 异常都失败关闭。
138
167
  */
139
- function visit(value: unknown, seen: WeakSet<object>): SnapshotResult {
168
+ function visit(
169
+ value: unknown,
170
+ seen: WeakSet<object>,
171
+ previous?: SnapshotResult
172
+ ): SnapshotResult {
173
+ // Objects are always re-read through their descriptors: callers can mutate
174
+ // an existing source or supply a Proxy. Only observed primitive values are reused.
175
+ if (
176
+ (value === null || typeof value !== 'object') &&
177
+ previous?.valid &&
178
+ Object.is(previous.snapshot, value)
179
+ )
180
+ return previous;
140
181
  if (value === undefined) return valid(undefined, 'undefined:');
141
182
  if (value === null) return valid(null, 'null:');
142
183
 
@@ -173,8 +214,8 @@ function visit(value: unknown, seen: WeakSet<object>): SnapshotResult {
173
214
  seen.add(value);
174
215
  try {
175
216
  return isArray
176
- ? visitArray(value, keys, seen)
177
- : visitObject(value, keys, seen);
217
+ ? visitArray(value, keys, seen, previous)
218
+ : visitObject(value, keys, seen, previous);
178
219
  } finally {
179
220
  seen.delete(value);
180
221
  }
@@ -183,9 +224,12 @@ function visit(value: unknown, seen: WeakSet<object>): SnapshotResult {
183
224
  }
184
225
  }
185
226
 
186
- function snapshotValue(value: unknown): SnapshotResult {
227
+ function snapshotValue(
228
+ value: unknown,
229
+ previous?: SnapshotResult
230
+ ): SnapshotResult {
187
231
  try {
188
- return visit(value, new WeakSet<object>());
232
+ return visit(value, new WeakSet<object>(), previous);
189
233
  } catch {
190
234
  return invalid('exception');
191
235
  }
@@ -273,7 +317,12 @@ function asImageSource(
273
317
  export function resolveImageSource(
274
318
  source: unknown
275
319
  ): ResolvedImageSource | undefined {
276
- const result = snapshotValue(source);
320
+ return resolveSnapshot(snapshotValue(source));
321
+ }
322
+
323
+ function resolveSnapshot(
324
+ result: SnapshotResult
325
+ ): ResolvedImageSource | undefined {
277
326
  if (!result.valid) return undefined;
278
327
 
279
328
  const snapshot = asImageSource(result.snapshot);
@@ -285,6 +334,20 @@ export function resolveImageSource(
285
334
  });
286
335
  }
287
336
 
337
+ /** One resolver belongs to one mounted media component; only its latest tree is kept. */
338
+ export function createImageSourceResolver() {
339
+ let previous: SnapshotResult | undefined;
340
+ let resolved: ResolvedImageSource | undefined;
341
+ return (source: unknown): ResolvedImageSource | undefined => {
342
+ const next = snapshotValue(source, previous);
343
+ if (next !== previous) {
344
+ resolved = resolveSnapshot(next);
345
+ previous = resolved === undefined ? undefined : next;
346
+ }
347
+ return resolved;
348
+ };
349
+ }
350
+
288
351
  export function imageSourceKey(
289
352
  source: ImageSourcePropType | undefined
290
353
  ): string {