@unif/react-native-design 0.31.3 → 0.32.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/README.md +6 -0
- package/lib/module/components/ui/Avatar/Avatar.js +2 -1
- package/lib/module/components/ui/Avatar/Avatar.js.map +1 -1
- package/lib/module/components/ui/Avatar/geometry.js +43 -0
- package/lib/module/components/ui/Avatar/geometry.js.map +1 -0
- package/lib/module/components/ui/Avatar/styles.js +1 -41
- package/lib/module/components/ui/Avatar/styles.js.map +1 -1
- package/lib/module/components/ui/AvatarGroup/AvatarGroup.js +1 -1
- package/lib/module/components/ui/AvatarGroup/AvatarGroup.js.map +1 -1
- package/lib/module/components/ui/CircularProgress/CircularProgress.js +14 -25
- package/lib/module/components/ui/CircularProgress/CircularProgress.js.map +1 -1
- package/lib/module/components/ui/CircularProgress/normalizeCircularProgress.js.map +1 -1
- package/lib/module/components/ui/CircularProgress/styles.js +24 -0
- package/lib/module/components/ui/CircularProgress/styles.js.map +1 -0
- package/lib/module/components/ui/Pulse/PulseDot.js +5 -5
- package/lib/module/components/ui/Pulse/PulseDot.js.map +1 -1
- package/lib/module/components/ui/Pulse/constants.js +15 -0
- package/lib/module/components/ui/Pulse/constants.js.map +1 -0
- package/lib/module/components/ui/Pulse/usePulse.js +4 -62
- package/lib/module/components/ui/Pulse/usePulse.js.map +1 -1
- package/lib/module/components/ui/Skeleton/Skeleton.js +6 -2
- package/lib/module/components/ui/Skeleton/Skeleton.js.map +1 -1
- package/lib/module/components/ui/Skeleton/constants.js +9 -0
- package/lib/module/components/ui/Skeleton/constants.js.map +1 -0
- package/lib/module/components/ui/TextField/TextFieldBase.js +21 -4
- package/lib/module/components/ui/TextField/TextFieldBase.js.map +1 -1
- package/lib/module/components/ui/TextField/constants.js +7 -0
- package/lib/module/components/ui/TextField/constants.js.map +1 -0
- package/lib/module/components/ui/TextField/styles.js +10 -2
- package/lib/module/components/ui/TextField/styles.js.map +1 -1
- package/lib/module/components/ui/TextField/useMultilineLayout.js +18 -0
- package/lib/module/components/ui/TextField/useMultilineLayout.js.map +1 -0
- package/lib/module/components/ui/TextField/useMultilineLayout.web.js +44 -0
- package/lib/module/components/ui/TextField/useMultilineLayout.web.js.map +1 -0
- package/lib/module/components/ui/Thumbnail/Thumbnail.js +13 -6
- package/lib/module/components/ui/Thumbnail/Thumbnail.js.map +1 -1
- package/lib/module/components/ui/Thumbnail/constants.js +22 -0
- package/lib/module/components/ui/Thumbnail/constants.js.map +1 -0
- package/lib/module/components/ui/Thumbnail/normalize.js +30 -1
- package/lib/module/components/ui/Thumbnail/normalize.js.map +1 -1
- package/lib/module/components/ui/Thumbnail/styles.js +5 -28
- package/lib/module/components/ui/Thumbnail/styles.js.map +1 -1
- package/lib/module/components/ui/index.js.map +1 -1
- package/lib/module/components/ui/shared/pulse/constants.js +5 -0
- package/lib/module/components/ui/shared/pulse/constants.js.map +1 -0
- package/lib/module/components/ui/{Pulse → shared/pulse}/normalizePulseOptions.js +2 -12
- package/lib/module/components/ui/shared/pulse/normalizePulseOptions.js.map +1 -0
- package/lib/module/components/ui/shared/pulse/types.js +2 -0
- package/lib/module/components/ui/shared/pulse/types.js.map +1 -0
- package/lib/module/components/ui/shared/pulse/usePulseAnimation.js +30 -0
- package/lib/module/components/ui/shared/pulse/usePulseAnimation.js.map +1 -0
- package/lib/module/components/ui/{Pulse → shared/pulse}/usePulseDriver.js +1 -1
- package/lib/module/components/ui/shared/pulse/usePulseDriver.js.map +1 -0
- package/lib/module/components/ui/{Pulse → shared/pulse}/usePulseDriver.web.js +2 -17
- package/lib/module/components/ui/shared/pulse/usePulseDriver.web.js.map +1 -0
- package/lib/typescript/src/components/ui/Avatar/Avatar.d.ts.map +1 -1
- package/lib/typescript/src/components/ui/Avatar/geometry.d.ts +7 -0
- package/lib/typescript/src/components/ui/Avatar/geometry.d.ts.map +1 -0
- package/lib/typescript/src/components/ui/Avatar/styles.d.ts +1 -9
- package/lib/typescript/src/components/ui/Avatar/styles.d.ts.map +1 -1
- package/lib/typescript/src/components/ui/Avatar/types.d.ts +7 -2
- package/lib/typescript/src/components/ui/Avatar/types.d.ts.map +1 -1
- package/lib/typescript/src/components/ui/CircularProgress/CircularProgress.d.ts.map +1 -1
- package/lib/typescript/src/components/ui/CircularProgress/normalizeCircularProgress.d.ts +2 -14
- package/lib/typescript/src/components/ui/CircularProgress/normalizeCircularProgress.d.ts.map +1 -1
- package/lib/typescript/src/components/ui/CircularProgress/styles.d.ts +19 -0
- package/lib/typescript/src/components/ui/CircularProgress/styles.d.ts.map +1 -0
- package/lib/typescript/src/components/ui/CircularProgress/types.d.ts +17 -2
- package/lib/typescript/src/components/ui/CircularProgress/types.d.ts.map +1 -1
- package/lib/typescript/src/components/ui/Pulse/PulseDot.d.ts.map +1 -1
- package/lib/typescript/src/components/ui/Pulse/constants.d.ts +4 -0
- package/lib/typescript/src/components/ui/Pulse/constants.d.ts.map +1 -0
- package/lib/typescript/src/components/ui/Pulse/types.d.ts +6 -6
- package/lib/typescript/src/components/ui/Pulse/types.d.ts.map +1 -1
- package/lib/typescript/src/components/ui/Pulse/usePulse.d.ts +0 -9
- package/lib/typescript/src/components/ui/Pulse/usePulse.d.ts.map +1 -1
- package/lib/typescript/src/components/ui/Skeleton/Skeleton.d.ts.map +1 -1
- package/lib/typescript/src/components/ui/Skeleton/constants.d.ts +3 -0
- package/lib/typescript/src/components/ui/Skeleton/constants.d.ts.map +1 -0
- package/lib/typescript/src/components/ui/Skeleton/types.d.ts +2 -2
- package/lib/typescript/src/components/ui/Skeleton/types.d.ts.map +1 -1
- package/lib/typescript/src/components/ui/TextField/TextFieldBase.d.ts.map +1 -1
- package/lib/typescript/src/components/ui/TextField/constants.d.ts +4 -0
- package/lib/typescript/src/components/ui/TextField/constants.d.ts.map +1 -0
- package/lib/typescript/src/components/ui/TextField/styles.d.ts +6 -0
- package/lib/typescript/src/components/ui/TextField/styles.d.ts.map +1 -1
- package/lib/typescript/src/components/ui/TextField/types.d.ts +36 -11
- package/lib/typescript/src/components/ui/TextField/types.d.ts.map +1 -1
- package/lib/typescript/src/components/ui/TextField/useMultilineLayout.d.ts +4 -0
- package/lib/typescript/src/components/ui/TextField/useMultilineLayout.d.ts.map +1 -0
- package/lib/typescript/src/components/ui/TextField/useMultilineLayout.web.d.ts +4 -0
- package/lib/typescript/src/components/ui/TextField/useMultilineLayout.web.d.ts.map +1 -0
- package/lib/typescript/src/components/ui/Textarea/types.d.ts +4 -2
- package/lib/typescript/src/components/ui/Textarea/types.d.ts.map +1 -1
- package/lib/typescript/src/components/ui/Thumbnail/Thumbnail.d.ts.map +1 -1
- package/lib/typescript/src/components/ui/Thumbnail/constants.d.ts +4 -0
- package/lib/typescript/src/components/ui/Thumbnail/constants.d.ts.map +1 -0
- package/lib/typescript/src/components/ui/Thumbnail/index.d.ts +1 -1
- package/lib/typescript/src/components/ui/Thumbnail/index.d.ts.map +1 -1
- package/lib/typescript/src/components/ui/Thumbnail/normalize.d.ts +2 -6
- package/lib/typescript/src/components/ui/Thumbnail/normalize.d.ts.map +1 -1
- package/lib/typescript/src/components/ui/Thumbnail/styles.d.ts +9 -8
- package/lib/typescript/src/components/ui/Thumbnail/styles.d.ts.map +1 -1
- package/lib/typescript/src/components/ui/Thumbnail/types.d.ts +23 -5
- package/lib/typescript/src/components/ui/Thumbnail/types.d.ts.map +1 -1
- package/lib/typescript/src/components/ui/index.d.ts +1 -1
- package/lib/typescript/src/components/ui/index.d.ts.map +1 -1
- package/lib/typescript/src/components/ui/shared/pulse/constants.d.ts +2 -0
- package/lib/typescript/src/components/ui/shared/pulse/constants.d.ts.map +1 -0
- package/lib/typescript/src/components/ui/shared/pulse/normalizePulseOptions.d.ts +7 -0
- package/lib/typescript/src/components/ui/shared/pulse/normalizePulseOptions.d.ts.map +1 -0
- package/lib/typescript/src/components/ui/shared/pulse/types.d.ts +17 -0
- package/lib/typescript/src/components/ui/shared/pulse/types.d.ts.map +1 -0
- package/lib/typescript/src/components/ui/shared/pulse/usePulseAnimation.d.ts +8 -0
- package/lib/typescript/src/components/ui/shared/pulse/usePulseAnimation.d.ts.map +1 -0
- package/lib/typescript/src/components/ui/{Pulse → shared/pulse}/usePulseDriver.d.ts +1 -1
- package/lib/typescript/src/components/ui/shared/pulse/usePulseDriver.d.ts.map +1 -0
- package/lib/typescript/src/components/ui/shared/pulse/usePulseDriver.web.d.ts +7 -0
- package/lib/typescript/src/components/ui/shared/pulse/usePulseDriver.web.d.ts.map +1 -0
- package/package.json +1 -1
- package/src/components/ui/Avatar/Avatar.tsx +2 -6
- package/src/components/ui/Avatar/geometry.ts +30 -0
- package/src/components/ui/Avatar/styles.ts +2 -30
- package/src/components/ui/Avatar/types.ts +8 -2
- package/src/components/ui/AvatarGroup/AvatarGroup.tsx +1 -1
- package/src/components/ui/CircularProgress/CircularProgress.tsx +20 -26
- package/src/components/ui/CircularProgress/normalizeCircularProgress.ts +7 -15
- package/src/components/ui/CircularProgress/styles.ts +13 -0
- package/src/components/ui/CircularProgress/types.ts +19 -2
- package/src/components/ui/Pulse/PulseDot.tsx +6 -5
- package/src/components/ui/Pulse/constants.ts +14 -0
- package/src/components/ui/Pulse/types.ts +6 -6
- package/src/components/ui/Pulse/usePulse.ts +4 -81
- package/src/components/ui/Skeleton/Skeleton.tsx +6 -6
- package/src/components/ui/Skeleton/constants.ts +8 -0
- package/src/components/ui/Skeleton/types.ts +2 -2
- package/src/components/ui/TextField/TextFieldBase.tsx +28 -4
- package/src/components/ui/TextField/constants.ts +6 -0
- package/src/components/ui/TextField/styles.ts +8 -2
- package/src/components/ui/TextField/types.ts +41 -11
- package/src/components/ui/TextField/useMultilineLayout.ts +20 -0
- package/src/components/ui/TextField/useMultilineLayout.web.ts +59 -0
- package/src/components/ui/Textarea/types.ts +9 -6
- package/src/components/ui/Thumbnail/Thumbnail.tsx +19 -5
- package/src/components/ui/Thumbnail/constants.ts +24 -0
- package/src/components/ui/Thumbnail/index.ts +1 -0
- package/src/components/ui/Thumbnail/normalize.ts +38 -19
- package/src/components/ui/Thumbnail/styles.ts +6 -17
- package/src/components/ui/Thumbnail/types.ts +27 -5
- package/src/components/ui/index.ts +1 -0
- package/src/components/ui/shared/pulse/constants.ts +2 -0
- package/src/components/ui/shared/pulse/normalizePulseOptions.ts +48 -0
- package/src/components/ui/shared/pulse/types.ts +18 -0
- package/src/components/ui/shared/pulse/usePulseAnimation.ts +29 -0
- package/src/components/ui/{Pulse → shared/pulse}/usePulseDriver.ts +2 -2
- package/src/components/ui/{Pulse → shared/pulse}/usePulseDriver.web.ts +4 -19
- package/lib/module/components/ui/Pulse/normalizePulseOptions.js.map +0 -1
- package/lib/module/components/ui/Pulse/usePulseDriver.js.map +0 -1
- package/lib/module/components/ui/Pulse/usePulseDriver.web.js.map +0 -1
- package/lib/typescript/src/components/ui/Pulse/normalizePulseOptions.d.ts +0 -32
- package/lib/typescript/src/components/ui/Pulse/normalizePulseOptions.d.ts.map +0 -1
- package/lib/typescript/src/components/ui/Pulse/usePulseDriver.d.ts.map +0 -1
- package/lib/typescript/src/components/ui/Pulse/usePulseDriver.web.d.ts +0 -18
- package/lib/typescript/src/components/ui/Pulse/usePulseDriver.web.d.ts.map +0 -1
- package/src/components/ui/Pulse/normalizePulseOptions.ts +0 -76
|
@@ -7,7 +7,7 @@ import React, {
|
|
|
7
7
|
} from 'react';
|
|
8
8
|
import { Platform, Text, TextInput, View } from 'react-native';
|
|
9
9
|
import type { TextInputProps } from 'react-native';
|
|
10
|
-
import { fixed,
|
|
10
|
+
import { fixed, useColors, useThemedStyles } from '../../../theme';
|
|
11
11
|
import { childTestID } from '../../../utils/testID';
|
|
12
12
|
import { createLogger } from '../../../utils/logger';
|
|
13
13
|
import {
|
|
@@ -20,6 +20,7 @@ import { TextFieldSlot } from './TextFieldSlot';
|
|
|
20
20
|
import type { TextFieldBaseProps, TextFieldHandle } from './types';
|
|
21
21
|
import { useErrorAnnouncement } from './useErrorAnnouncement';
|
|
22
22
|
import { useTextFieldValue } from './useTextFieldValue';
|
|
23
|
+
import { useMultilineLayout } from './useMultilineLayout';
|
|
23
24
|
|
|
24
25
|
const log = createLogger('TextField');
|
|
25
26
|
|
|
@@ -49,6 +50,9 @@ export const TextFieldBase = forwardRef<TextFieldHandle, TextFieldBaseProps>(
|
|
|
49
50
|
placeholderTextColor: callerPlaceholder,
|
|
50
51
|
onFocus,
|
|
51
52
|
onBlur,
|
|
53
|
+
onContentSizeChange,
|
|
54
|
+
onLayout,
|
|
55
|
+
scrollEnabled,
|
|
52
56
|
testID,
|
|
53
57
|
style: _style,
|
|
54
58
|
numberOfLines: _numberOfLines,
|
|
@@ -94,6 +98,15 @@ export const TextFieldBase = forwardRef<TextFieldHandle, TextFieldBaseProps>(
|
|
|
94
98
|
minHeight,
|
|
95
99
|
maxHeight
|
|
96
100
|
);
|
|
101
|
+
const multilineLayout = useMultilineLayout({
|
|
102
|
+
enabled: multiline,
|
|
103
|
+
value: controller.value,
|
|
104
|
+
minHeight: normalizedTextareaHeights.minHeight,
|
|
105
|
+
maxHeight: normalizedTextareaHeights.maxHeight,
|
|
106
|
+
fontSize: styles.input.fontSize,
|
|
107
|
+
placeholder: allowedNativeProps.placeholder as string | undefined,
|
|
108
|
+
inputRef: nativeRef,
|
|
109
|
+
});
|
|
97
110
|
const sanitizedContainer = sanitizeTextFieldContainerStyle(containerStyle);
|
|
98
111
|
const effectiveEditable = disabled !== true && editable !== false;
|
|
99
112
|
const mergedAccessibilityState = {
|
|
@@ -193,6 +206,15 @@ export const TextFieldBase = forwardRef<TextFieldHandle, TextFieldBaseProps>(
|
|
|
193
206
|
onChangeText={controller.onChangeText}
|
|
194
207
|
editable={effectiveEditable}
|
|
195
208
|
multiline={multiline}
|
|
209
|
+
onContentSizeChange={onContentSizeChange}
|
|
210
|
+
onLayout={(event) => {
|
|
211
|
+
if (multiline) multilineLayout.onLayout?.(event);
|
|
212
|
+
onLayout?.(event);
|
|
213
|
+
}}
|
|
214
|
+
scrollEnabled={
|
|
215
|
+
scrollEnabled ??
|
|
216
|
+
(multiline ? multilineLayout.scrollEnabled : undefined)
|
|
217
|
+
}
|
|
196
218
|
onFocus={(event) => {
|
|
197
219
|
setFocused(true);
|
|
198
220
|
onFocus?.(event);
|
|
@@ -204,9 +226,11 @@ export const TextFieldBase = forwardRef<TextFieldHandle, TextFieldBaseProps>(
|
|
|
204
226
|
style={[
|
|
205
227
|
styles.input,
|
|
206
228
|
multiline && styles.inputMultiline,
|
|
207
|
-
multiline &&
|
|
208
|
-
|
|
209
|
-
|
|
229
|
+
multiline &&
|
|
230
|
+
(multilineLayout.scrollEnabled
|
|
231
|
+
? styles.inputScrolling
|
|
232
|
+
: styles.inputClipped),
|
|
233
|
+
multiline && multilineLayout.inputStyle,
|
|
210
234
|
searchLayout !== undefined && {
|
|
211
235
|
minHeight: searchLayout.interactiveHeight,
|
|
212
236
|
},
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { StyleSheet } from 'react-native';
|
|
2
2
|
import { fw, radius, space, type as t } from '../../../theme';
|
|
3
3
|
import type { ColorTokens } from '../../../theme';
|
|
4
|
+
import {
|
|
5
|
+
TEXTAREA_VERTICAL_PADDING,
|
|
6
|
+
TEXT_FIELD_BORDER_WIDTH,
|
|
7
|
+
} from './constants';
|
|
4
8
|
|
|
5
9
|
/** TextField primitive styles —— Input + Textarea 共享。 */
|
|
6
10
|
export const makeStyles = (c: ColorTokens) =>
|
|
@@ -11,10 +15,10 @@ export const makeStyles = (c: ColorTokens) =>
|
|
|
11
15
|
gap: space[3],
|
|
12
16
|
paddingHorizontal: space[5],
|
|
13
17
|
borderRadius: radius.md,
|
|
14
|
-
borderWidth:
|
|
18
|
+
borderWidth: TEXT_FIELD_BORDER_WIDTH,
|
|
15
19
|
},
|
|
16
20
|
wrapMultiline: {
|
|
17
|
-
paddingVertical:
|
|
21
|
+
paddingVertical: TEXTAREA_VERTICAL_PADDING,
|
|
18
22
|
alignItems: 'flex-start',
|
|
19
23
|
},
|
|
20
24
|
/** Search 的可见底色/边框绝不承载命中区域。 */
|
|
@@ -59,6 +63,8 @@ export const makeStyles = (c: ColorTokens) =>
|
|
|
59
63
|
inputMultiline: {
|
|
60
64
|
textAlignVertical: 'top' as const,
|
|
61
65
|
},
|
|
66
|
+
inputScrolling: { overflow: 'scroll' },
|
|
67
|
+
inputClipped: { overflow: 'hidden' },
|
|
62
68
|
slotDisplay: {
|
|
63
69
|
alignItems: 'center',
|
|
64
70
|
justifyContent: 'center',
|
|
@@ -1,4 +1,11 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { ComponentRef, RefObject } from 'react';
|
|
2
|
+
import type { TextInput } from 'react-native';
|
|
3
|
+
import type {
|
|
4
|
+
StyleProp,
|
|
5
|
+
TextInputProps,
|
|
6
|
+
TextStyle,
|
|
7
|
+
ViewStyle,
|
|
8
|
+
} from 'react-native';
|
|
2
9
|
import type { IconName } from '../Icon';
|
|
3
10
|
|
|
4
11
|
/**
|
|
@@ -7,18 +14,18 @@ import type { IconName } from '../Icon';
|
|
|
7
14
|
* `defaultValue?: never` 让「同时传 value 和 defaultValue」在类型层就报错 ——
|
|
8
15
|
* 那是最常见的受控/非受控混用写法,运行时表现为「defaultValue 被静默忽略」。
|
|
9
16
|
*/
|
|
10
|
-
export
|
|
17
|
+
export interface ControlledTextValueProps {
|
|
11
18
|
value: string;
|
|
12
19
|
onChangeText: (value: string) => void;
|
|
13
20
|
defaultValue?: never;
|
|
14
|
-
}
|
|
21
|
+
}
|
|
15
22
|
|
|
16
23
|
/** 非受控:内部持值,`defaultValue` 只在首次 render 读一次。 */
|
|
17
|
-
export
|
|
24
|
+
export interface UncontrolledTextValueProps {
|
|
18
25
|
value?: never;
|
|
19
26
|
defaultValue?: string;
|
|
20
27
|
onChangeText?: (value: string) => void;
|
|
21
|
-
}
|
|
28
|
+
}
|
|
22
29
|
|
|
23
30
|
/** internal —— 由 InputProps / TextareaProps / SearchProps 组合进公共类型。 */
|
|
24
31
|
export type TextFieldValueProps =
|
|
@@ -31,10 +38,10 @@ export type TextFieldValueProps =
|
|
|
31
38
|
* 为什么不暴露原生 TextInput —— `clear()` 会绕过值状态机(受控下把 UI 清空但调用方
|
|
32
39
|
* state 不变),`setNativeProps()` 能直接改任意原生属性,两者都会让公共契约失效。
|
|
33
40
|
*/
|
|
34
|
-
export
|
|
41
|
+
export interface TextFieldHandle {
|
|
35
42
|
focus: () => void;
|
|
36
43
|
blur: () => void;
|
|
37
|
-
}
|
|
44
|
+
}
|
|
38
45
|
|
|
39
46
|
/**
|
|
40
47
|
* 左右 slot 的可验证配置。**不接受任意 ReactNode** ——
|
|
@@ -67,14 +74,14 @@ export type TextFieldContainerStyle = Omit<
|
|
|
67
74
|
>;
|
|
68
75
|
|
|
69
76
|
/** internal:Search 把真实交互行与仅作视觉的 surface 明确分层。 */
|
|
70
|
-
export
|
|
77
|
+
export interface SearchFieldLayout {
|
|
71
78
|
interactiveHeight: number;
|
|
72
79
|
visibleHeight: number;
|
|
73
80
|
verticalInset: number;
|
|
74
|
-
}
|
|
81
|
+
}
|
|
75
82
|
|
|
76
83
|
/** TextField 家族共享的非值 props(内部)。 */
|
|
77
|
-
export
|
|
84
|
+
export interface TextFieldCommonProps {
|
|
78
85
|
/** 左侧 slot —— 图标 / 文本 / 带 handler 与名称的操作 */
|
|
79
86
|
leading?: TextFieldSlot;
|
|
80
87
|
/** 右侧 slot —— 图标 / 文本 / 带 handler 与名称的操作 */
|
|
@@ -85,7 +92,7 @@ export type TextFieldCommonProps = {
|
|
|
85
92
|
disabled?: boolean;
|
|
86
93
|
/** 外层容器样式;不接受会破坏最小 frame 的尺寸字段 */
|
|
87
94
|
containerStyle?: StyleProp<TextFieldContainerStyle>;
|
|
88
|
-
}
|
|
95
|
+
}
|
|
89
96
|
|
|
90
97
|
/**
|
|
91
98
|
* 这些原生 TextInput props 从公共类型删除:
|
|
@@ -127,3 +134,26 @@ export type TextFieldBaseProps = Omit<TextInputProps, RemovedTextInputProps> &
|
|
|
127
134
|
/** internal:Search 的 44pt interactive row + 36pt decorative surface。 */
|
|
128
135
|
searchLayout?: SearchFieldLayout;
|
|
129
136
|
};
|
|
137
|
+
|
|
138
|
+
export interface MultilineLayoutInput {
|
|
139
|
+
enabled: boolean;
|
|
140
|
+
value: string;
|
|
141
|
+
minHeight: number;
|
|
142
|
+
maxHeight: number | undefined;
|
|
143
|
+
fontSize: number | undefined;
|
|
144
|
+
placeholder: string | undefined;
|
|
145
|
+
inputRef: RefObject<ComponentRef<typeof TextInput> | null>;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export interface MultilineLayout {
|
|
149
|
+
inputStyle: Pick<TextStyle, 'height' | 'minHeight' | 'maxHeight'>;
|
|
150
|
+
scrollEnabled: boolean;
|
|
151
|
+
onLayout?: TextInputProps['onLayout'];
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** RN Web 的公开 host ref 仅在平台实现内使用这两个 DOM 测量属性。 */
|
|
155
|
+
export interface WebTextInputMeasurement {
|
|
156
|
+
scrollHeight: number;
|
|
157
|
+
scrollTop: number;
|
|
158
|
+
style: { height: string; minHeight: string };
|
|
159
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { TEXTAREA_VERTICAL_INSET } from './constants';
|
|
2
|
+
import type { MultilineLayout, MultilineLayoutInput } from './types';
|
|
3
|
+
|
|
4
|
+
/** 原生 Yoga 直接测量文本;固定 height 会阻断 iOS Fabric 的后续内容尺寸通知。 */
|
|
5
|
+
export function useMultilineLayout({
|
|
6
|
+
minHeight,
|
|
7
|
+
maxHeight,
|
|
8
|
+
}: MultilineLayoutInput): MultilineLayout {
|
|
9
|
+
return {
|
|
10
|
+
inputStyle: {
|
|
11
|
+
minHeight: Math.max(0, minHeight - TEXTAREA_VERTICAL_INSET),
|
|
12
|
+
maxHeight:
|
|
13
|
+
maxHeight === undefined
|
|
14
|
+
? undefined
|
|
15
|
+
: Math.max(0, maxHeight - TEXTAREA_VERTICAL_INSET),
|
|
16
|
+
},
|
|
17
|
+
// 原生仅在内容溢出时滚动;min=max 时也不等待不会再变化的框高事件。
|
|
18
|
+
scrollEnabled: true,
|
|
19
|
+
};
|
|
20
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { useCallback, useLayoutEffect, useState } from 'react';
|
|
2
|
+
import { TEXTAREA_VERTICAL_INSET } from './constants';
|
|
3
|
+
import type {
|
|
4
|
+
MultilineLayout,
|
|
5
|
+
MultilineLayoutInput,
|
|
6
|
+
WebTextInputMeasurement,
|
|
7
|
+
} from './types';
|
|
8
|
+
|
|
9
|
+
/** RNW 的 contentSize 使用 scrollHeight,固定高度会阻止缩短,须临时解除高度后测量。 */
|
|
10
|
+
export function useMultilineLayout({
|
|
11
|
+
enabled,
|
|
12
|
+
value,
|
|
13
|
+
minHeight,
|
|
14
|
+
maxHeight,
|
|
15
|
+
fontSize,
|
|
16
|
+
placeholder,
|
|
17
|
+
inputRef,
|
|
18
|
+
}: MultilineLayoutInput): MultilineLayout {
|
|
19
|
+
const [contentHeight, setContentHeight] = useState(0);
|
|
20
|
+
const [width, setWidth] = useState(0);
|
|
21
|
+
const minimum = Math.max(0, minHeight - TEXTAREA_VERTICAL_INSET);
|
|
22
|
+
const maximum =
|
|
23
|
+
maxHeight === undefined
|
|
24
|
+
? Infinity
|
|
25
|
+
: Math.max(0, maxHeight - TEXTAREA_VERTICAL_INSET);
|
|
26
|
+
useLayoutEffect(() => {
|
|
27
|
+
if (!enabled || inputRef.current === null) return;
|
|
28
|
+
const node = inputRef.current as unknown as WebTextInputMeasurement;
|
|
29
|
+
const { height, minHeight: previousMinimum } = node.style;
|
|
30
|
+
const scrollTop = node.scrollTop;
|
|
31
|
+
node.style.height = '0px';
|
|
32
|
+
node.style.minHeight = '0px';
|
|
33
|
+
const measured = node.scrollHeight;
|
|
34
|
+
node.style.height = height;
|
|
35
|
+
node.style.minHeight = previousMinimum;
|
|
36
|
+
node.scrollTop = scrollTop;
|
|
37
|
+
if (Number.isFinite(measured)) setContentHeight(measured);
|
|
38
|
+
}, [
|
|
39
|
+
enabled,
|
|
40
|
+
value,
|
|
41
|
+
width,
|
|
42
|
+
fontSize,
|
|
43
|
+
placeholder,
|
|
44
|
+
minHeight,
|
|
45
|
+
maxHeight,
|
|
46
|
+
inputRef,
|
|
47
|
+
]);
|
|
48
|
+
const onLayout = useCallback<NonNullable<MultilineLayout['onLayout']>>(
|
|
49
|
+
(event) => setWidth(event.nativeEvent.layout.width),
|
|
50
|
+
[]
|
|
51
|
+
);
|
|
52
|
+
return {
|
|
53
|
+
inputStyle: {
|
|
54
|
+
height: Math.min(maximum, Math.max(minimum, contentHeight)),
|
|
55
|
+
},
|
|
56
|
+
scrollEnabled: contentHeight > maximum,
|
|
57
|
+
onLayout,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
@@ -13,9 +13,12 @@ import type {
|
|
|
13
13
|
*/
|
|
14
14
|
export type TextareaProps = Omit<TextInputProps, RemovedTextInputProps> &
|
|
15
15
|
TextFieldCommonProps &
|
|
16
|
-
TextFieldValueProps &
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
16
|
+
TextFieldValueProps &
|
|
17
|
+
TextareaHeightProps;
|
|
18
|
+
|
|
19
|
+
interface TextareaHeightProps {
|
|
20
|
+
/** 最小高度,默认 96(约 5 行) */
|
|
21
|
+
minHeight?: number;
|
|
22
|
+
/** 最大高度(超过后内部滚动),默认不限制 */
|
|
23
|
+
maxHeight?: number;
|
|
24
|
+
}
|
|
@@ -5,8 +5,11 @@ import { resolveImageSource } from '../../../utils/imageSource';
|
|
|
5
5
|
import { createLogger } from '../../../utils/logger';
|
|
6
6
|
import { ImageAttempt } from '../shared/ImageAttempt';
|
|
7
7
|
import { A11Y_HIDDEN_PROPS } from '../shared/a11y';
|
|
8
|
-
import {
|
|
9
|
-
|
|
8
|
+
import {
|
|
9
|
+
normalizeThumbnailDimensions,
|
|
10
|
+
sanitizeThumbnailImageStyle,
|
|
11
|
+
} from './normalize';
|
|
12
|
+
import { makeStyles } from './styles';
|
|
10
13
|
import type { ThumbnailProps } from './types';
|
|
11
14
|
|
|
12
15
|
const log = createLogger('Thumbnail');
|
|
@@ -22,6 +25,7 @@ export function Thumbnail(props: ThumbnailProps): React.JSX.Element {
|
|
|
22
25
|
uri,
|
|
23
26
|
source,
|
|
24
27
|
size = 'md',
|
|
28
|
+
fallback,
|
|
25
29
|
selected,
|
|
26
30
|
containerStyle,
|
|
27
31
|
imageStyle,
|
|
@@ -31,7 +35,8 @@ export function Thumbnail(props: ThumbnailProps): React.JSX.Element {
|
|
|
31
35
|
} = props;
|
|
32
36
|
const colors = useColors();
|
|
33
37
|
const styles = useThemedStyles(makeStyles);
|
|
34
|
-
const
|
|
38
|
+
const normalizedDimensions = normalizeThumbnailDimensions(size);
|
|
39
|
+
const dim = normalizedDimensions.dimensions;
|
|
35
40
|
const hasUri = hasOwn(props, 'uri');
|
|
36
41
|
const hasSource = hasOwn(props, 'source');
|
|
37
42
|
const sourceCandidate =
|
|
@@ -43,6 +48,7 @@ export function Thumbnail(props: ThumbnailProps): React.JSX.Element {
|
|
|
43
48
|
const resolvedSource = resolveImageSource(sourceCandidate);
|
|
44
49
|
const sanitizedImageStyle = sanitizeThumbnailImageStyle(imageStyle);
|
|
45
50
|
const diagnostics = [
|
|
51
|
+
...normalizedDimensions.diagnostics,
|
|
46
52
|
...(resolvedSource === undefined ? ['source'] : []),
|
|
47
53
|
...sanitizedImageStyle.diagnostics.map((field) =>
|
|
48
54
|
field === 'style' ? 'imageStyle' : `imageStyle.${field}`
|
|
@@ -72,16 +78,24 @@ export function Thumbnail(props: ThumbnailProps): React.JSX.Element {
|
|
|
72
78
|
borderRadius: dim.borderRadius,
|
|
73
79
|
borderColor: selected ? colors.primary : 'transparent',
|
|
74
80
|
};
|
|
81
|
+
const fallbackContent =
|
|
82
|
+
fallback === undefined ? null : (
|
|
83
|
+
<View {...imageAccessibilityProps} style={styles.fallback}>
|
|
84
|
+
{fallback}
|
|
85
|
+
</View>
|
|
86
|
+
);
|
|
75
87
|
|
|
76
88
|
return (
|
|
77
89
|
<View style={containerStyle} testID={testID}>
|
|
78
90
|
<View style={[styles.visualFrame, dim]}>
|
|
79
|
-
{resolvedSource === undefined ?
|
|
91
|
+
{resolvedSource === undefined ? (
|
|
92
|
+
fallbackContent
|
|
93
|
+
) : (
|
|
80
94
|
<ImageAttempt
|
|
81
95
|
{...imageAccessibilityProps}
|
|
82
96
|
key={resolvedSource.key}
|
|
83
97
|
source={resolvedSource.source}
|
|
84
|
-
fallback={
|
|
98
|
+
fallback={fallbackContent}
|
|
85
99
|
style={[sanitizedImageStyle.style, StyleSheet.absoluteFill]}
|
|
86
100
|
resizeMode={resizeMode}
|
|
87
101
|
/>
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { r } from '../../../theme';
|
|
2
|
+
import type { ThumbnailDimensions, ThumbnailSize } from './types';
|
|
3
|
+
|
|
4
|
+
export const THUMBNAIL_DIMENSIONS: Readonly<
|
|
5
|
+
Record<ThumbnailSize, Required<ThumbnailDimensions>>
|
|
6
|
+
> = {
|
|
7
|
+
sm: { width: r(64), height: r(40), borderRadius: r(6) },
|
|
8
|
+
md: { width: r(113), height: r(67), borderRadius: r(8) },
|
|
9
|
+
lg: { width: r(160), height: r(96), borderRadius: r(10) },
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export const RESERVED_IMAGE_STYLE_KEYS = [
|
|
13
|
+
'position',
|
|
14
|
+
'top',
|
|
15
|
+
'right',
|
|
16
|
+
'bottom',
|
|
17
|
+
'left',
|
|
18
|
+
'width',
|
|
19
|
+
'height',
|
|
20
|
+
'minWidth',
|
|
21
|
+
'minHeight',
|
|
22
|
+
'maxWidth',
|
|
23
|
+
'maxHeight',
|
|
24
|
+
] as const;
|
|
@@ -1,25 +1,44 @@
|
|
|
1
1
|
import { StyleSheet } from 'react-native';
|
|
2
2
|
import type { StyleProp } from 'react-native';
|
|
3
|
-
import type {
|
|
3
|
+
import type {
|
|
4
|
+
NormalizedThumbnailDimensions,
|
|
5
|
+
SanitizedThumbnailImageStyle,
|
|
6
|
+
ThumbnailImageStyle,
|
|
7
|
+
} from './types';
|
|
8
|
+
import { RESERVED_IMAGE_STYLE_KEYS, THUMBNAIL_DIMENSIONS } from './constants';
|
|
4
9
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
'
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
'
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
10
|
+
export function normalizeThumbnailDimensions(
|
|
11
|
+
size: unknown
|
|
12
|
+
): NormalizedThumbnailDimensions {
|
|
13
|
+
if (size === undefined || size === 'sm' || size === 'md' || size === 'lg') {
|
|
14
|
+
return { dimensions: THUMBNAIL_DIMENSIONS[size ?? 'md'], diagnostics: [] };
|
|
15
|
+
}
|
|
16
|
+
if (typeof size === 'object' && size !== null && !Array.isArray(size)) {
|
|
17
|
+
const { width, height, borderRadius } = size as Record<string, unknown>;
|
|
18
|
+
if (
|
|
19
|
+
typeof width === 'number' &&
|
|
20
|
+
Number.isFinite(width) &&
|
|
21
|
+
width > 0 &&
|
|
22
|
+
typeof height === 'number' &&
|
|
23
|
+
Number.isFinite(height) &&
|
|
24
|
+
height > 0 &&
|
|
25
|
+
(borderRadius === undefined ||
|
|
26
|
+
(typeof borderRadius === 'number' &&
|
|
27
|
+
Number.isFinite(borderRadius) &&
|
|
28
|
+
borderRadius >= 0))
|
|
29
|
+
) {
|
|
30
|
+
return {
|
|
31
|
+
dimensions: {
|
|
32
|
+
width,
|
|
33
|
+
height,
|
|
34
|
+
borderRadius: borderRadius ?? THUMBNAIL_DIMENSIONS.md.borderRadius,
|
|
35
|
+
},
|
|
36
|
+
diagnostics: [],
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return { dimensions: THUMBNAIL_DIMENSIONS.md, diagnostics: ['size'] };
|
|
41
|
+
}
|
|
23
42
|
|
|
24
43
|
function invalidStyle(): SanitizedThumbnailImageStyle {
|
|
25
44
|
return { style: {}, diagnostics: ['style'] };
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { StyleSheet } from 'react-native';
|
|
2
|
-
import {
|
|
3
|
-
import type { ThumbnailSize } from './types';
|
|
2
|
+
import { type ColorTokens } from '../../../theme';
|
|
4
3
|
|
|
5
4
|
/** Thumbnail 静态样式 ——
|
|
6
5
|
* - `visualFrame`:固定尺寸、裁切、加载失败 placeholder
|
|
@@ -14,19 +13,9 @@ export const makeStyles = (c: ColorTokens) =>
|
|
|
14
13
|
ring: {
|
|
15
14
|
borderWidth: 2,
|
|
16
15
|
},
|
|
16
|
+
fallback: {
|
|
17
|
+
...StyleSheet.absoluteFill,
|
|
18
|
+
alignItems: 'center',
|
|
19
|
+
justifyContent: 'center',
|
|
20
|
+
},
|
|
17
21
|
});
|
|
18
|
-
|
|
19
|
-
/** Thumbnail size 阶梯 —— width / height / borderRadius 全 r() 缩放。
|
|
20
|
-
* 新增 size 在 types.ts 加 union + 这里加 case,两处同步。 */
|
|
21
|
-
export const sizingFor = (size: ThumbnailSize) => {
|
|
22
|
-
switch (size) {
|
|
23
|
-
case 'sm':
|
|
24
|
-
return { width: r(64), height: r(40), borderRadius: r(6) };
|
|
25
|
-
case 'lg':
|
|
26
|
-
return { width: r(160), height: r(96), borderRadius: r(10) };
|
|
27
|
-
case 'md':
|
|
28
|
-
default:
|
|
29
|
-
// 113×67 与 NewsList / NewsArea 历史一致,16:9.5 接近视频宽高比
|
|
30
|
-
return { width: r(113), height: r(67), borderRadius: r(8) };
|
|
31
|
-
}
|
|
32
|
-
};
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
1
2
|
import type {
|
|
2
3
|
ImageProps,
|
|
3
4
|
ImageSourcePropType,
|
|
@@ -6,12 +7,29 @@ import type {
|
|
|
6
7
|
ViewStyle,
|
|
7
8
|
} from 'react-native';
|
|
8
9
|
|
|
9
|
-
/** 缩略图尺寸阶梯 ——
|
|
10
|
+
/** 缩略图尺寸阶梯 —— 跨场景统一,新增阶梯在这里与 constants.ts 同步。
|
|
10
11
|
* - sm:64×40 chat 行内、列表二级图缩略
|
|
11
12
|
* - md:113×67 NewsList / NewsArea 公告右侧缩略(默认)
|
|
12
13
|
* - lg:160×96 详情顶部 hero 小图、卡片头图缩略 */
|
|
13
14
|
export type ThumbnailSize = 'sm' | 'md' | 'lg';
|
|
14
15
|
|
|
16
|
+
/** 已计算好的 RN 布局尺寸;组件不再调用 r() 缩放。 */
|
|
17
|
+
export interface ThumbnailDimensions {
|
|
18
|
+
width: number;
|
|
19
|
+
height: number;
|
|
20
|
+
borderRadius?: number;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface NormalizedThumbnailDimensions {
|
|
24
|
+
dimensions: Required<ThumbnailDimensions>;
|
|
25
|
+
diagnostics: readonly string[];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface SanitizedThumbnailImageStyle {
|
|
29
|
+
style: ThumbnailImageStyle;
|
|
30
|
+
diagnostics: readonly string[];
|
|
31
|
+
}
|
|
32
|
+
|
|
15
33
|
export type ThumbnailImageStyle = Omit<
|
|
16
34
|
ImageStyle,
|
|
17
35
|
| 'position'
|
|
@@ -39,9 +57,11 @@ type ThumbnailSource =
|
|
|
39
57
|
uri?: never;
|
|
40
58
|
};
|
|
41
59
|
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
size?: ThumbnailSize
|
|
60
|
+
interface ThumbnailCommonProps {
|
|
61
|
+
/** 原三档或实际图像框尺寸,默认 md;非法对象整体回退 md。 */
|
|
62
|
+
size?: ThumbnailSize | Readonly<ThumbnailDimensions>;
|
|
63
|
+
/** 无有效 source 或当前图片失败时居中显示;加载中不显示。 */
|
|
64
|
+
fallback?: ReactNode;
|
|
45
65
|
/** 选中态:在固定 visual frame 内显示 2pt 品牌色 ring。 */
|
|
46
66
|
selected?: boolean;
|
|
47
67
|
/** 完整外部布局样式,只落到稳定根 View。 */
|
|
@@ -54,4 +74,6 @@ export type ThumbnailProps = ThumbnailSource & {
|
|
|
54
74
|
accessibilityLabel?: string;
|
|
55
75
|
/** E2E / 测试定位,只落到外层 layout View。 */
|
|
56
76
|
testID?: string;
|
|
57
|
-
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export type ThumbnailProps = ThumbnailSource & ThumbnailCommonProps;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { PulseOptions } from '../../Pulse/types';
|
|
2
|
+
import type {
|
|
3
|
+
NormalizedPulseOptions,
|
|
4
|
+
PulseDefaults,
|
|
5
|
+
PulseDiagnostic,
|
|
6
|
+
} from './types';
|
|
7
|
+
import { MAX_TIMER } from './constants';
|
|
8
|
+
|
|
9
|
+
/** 共用的参数校验:非法字段采用所属组件默认值并报告诊断,不夹值或取整。 */
|
|
10
|
+
export function normalizePulseOptions(
|
|
11
|
+
input: PulseOptions = {},
|
|
12
|
+
defaults: Readonly<PulseDefaults>
|
|
13
|
+
): NormalizedPulseOptions {
|
|
14
|
+
const diagnostics: PulseDiagnostic[] = [];
|
|
15
|
+
const pick = (
|
|
16
|
+
field: keyof PulseDefaults,
|
|
17
|
+
valid: (value: number) => boolean
|
|
18
|
+
): number => {
|
|
19
|
+
const received = input[field];
|
|
20
|
+
if (received === undefined) return defaults[field];
|
|
21
|
+
if (typeof received === 'number' && valid(received)) return received;
|
|
22
|
+
diagnostics.push({ field, received, fallback: defaults[field] });
|
|
23
|
+
return defaults[field];
|
|
24
|
+
};
|
|
25
|
+
const duration = pick(
|
|
26
|
+
'duration',
|
|
27
|
+
(value) => Number.isFinite(value) && value >= 1 && value < MAX_TIMER
|
|
28
|
+
);
|
|
29
|
+
const delay = pick(
|
|
30
|
+
'delay',
|
|
31
|
+
(value) => Number.isFinite(value) && value >= 0 && value < MAX_TIMER
|
|
32
|
+
);
|
|
33
|
+
const from = pick(
|
|
34
|
+
'from',
|
|
35
|
+
(value) => Number.isFinite(value) && value >= 0 && value <= 1
|
|
36
|
+
);
|
|
37
|
+
const to = pick(
|
|
38
|
+
'to',
|
|
39
|
+
(value) => Number.isFinite(value) && value >= 0 && value <= 1
|
|
40
|
+
);
|
|
41
|
+
return { duration, delay, from, to, isStatic: from === to, diagnostics };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** driver 是否应当启动循环。静态或系统开启减弱动效时都不启动。 */
|
|
45
|
+
export const shouldAnimatePulse = (
|
|
46
|
+
options: NormalizedPulseOptions,
|
|
47
|
+
reducedMotion: boolean
|
|
48
|
+
): boolean => !reducedMotion && !options.isStatic;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export interface PulseDefaults {
|
|
2
|
+
duration: number;
|
|
3
|
+
delay: number;
|
|
4
|
+
from: number;
|
|
5
|
+
to: number;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface PulseDiagnostic {
|
|
9
|
+
field: keyof PulseDefaults;
|
|
10
|
+
received: unknown;
|
|
11
|
+
fallback: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface NormalizedPulseOptions extends PulseDefaults {
|
|
15
|
+
/** 两端相等 —— 没有可播的动画,driver 直接停在 `to`。 */
|
|
16
|
+
isStatic: boolean;
|
|
17
|
+
diagnostics: readonly PulseDiagnostic[];
|
|
18
|
+
}
|