@medway-ui/native 1.8.1 → 1.9.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 +18 -9
- package/lib/commonjs/components/Button.js +1 -1
- package/lib/commonjs/components/Button.js.map +1 -1
- package/lib/commonjs/components/Input.js +128 -29
- package/lib/commonjs/components/Input.js.map +1 -1
- package/lib/commonjs/components/Switch.js +5 -2
- package/lib/commonjs/components/Switch.js.map +1 -1
- package/lib/commonjs/editor/editorHtml.generated.js +3 -3
- package/lib/commonjs/editor/index.js.map +1 -1
- package/lib/module/components/Button.js +1 -1
- package/lib/module/components/Button.js.map +1 -1
- package/lib/module/components/Input.js +130 -31
- package/lib/module/components/Input.js.map +1 -1
- package/lib/module/components/Switch.js +5 -2
- package/lib/module/components/Switch.js.map +1 -1
- package/lib/module/editor/editorHtml.generated.js +3 -3
- package/lib/module/editor/index.js.map +1 -1
- package/lib/typescript/components/Input.d.ts +5 -1
- package/lib/typescript/components/Input.d.ts.map +1 -1
- package/lib/typescript/components/Switch.d.ts +3 -1
- package/lib/typescript/components/Switch.d.ts.map +1 -1
- package/lib/typescript/editor/editorHtml.generated.d.ts +3 -3
- package/lib/typescript/editor/index.d.ts +2 -1
- package/lib/typescript/editor/index.d.ts.map +1 -1
- package/package.json +11 -23
- package/setup/MedwayUiProvider.tsx +30 -8
- package/setup/babel-brownfield.js +14 -34
- package/setup/metro.js +6 -67
- package/setup/monorepo.js +102 -0
- package/setup/tailwind.js +5 -6
- package/src/components/Button.tsx +1 -1
- package/src/components/Input.tsx +147 -26
- package/src/components/Switch.tsx +6 -6
- package/src/editor/editorHtml.generated.ts +3 -3
- package/src/editor/index.ts +2 -4
package/src/components/Input.tsx
CHANGED
|
@@ -2,6 +2,7 @@ import {
|
|
|
2
2
|
useState,
|
|
3
3
|
useCallback,
|
|
4
4
|
useEffect,
|
|
5
|
+
useRef,
|
|
5
6
|
cloneElement,
|
|
6
7
|
type ReactElement,
|
|
7
8
|
type ReactNode,
|
|
@@ -11,6 +12,8 @@ import {
|
|
|
11
12
|
View,
|
|
12
13
|
TextInput,
|
|
13
14
|
Pressable,
|
|
15
|
+
StyleSheet,
|
|
16
|
+
type LayoutChangeEvent,
|
|
14
17
|
type TextInputProps,
|
|
15
18
|
type ViewProps,
|
|
16
19
|
} from "react-native";
|
|
@@ -20,6 +23,7 @@ import Animated, {
|
|
|
20
23
|
withTiming,
|
|
21
24
|
interpolate,
|
|
22
25
|
Easing,
|
|
26
|
+
type SharedValue,
|
|
23
27
|
} from "react-native-reanimated";
|
|
24
28
|
import { cva, type VariantProps } from "class-variance-authority";
|
|
25
29
|
import {
|
|
@@ -80,6 +84,10 @@ export interface InputLabelProps {
|
|
|
80
84
|
children?: ReactNode;
|
|
81
85
|
floated?: boolean;
|
|
82
86
|
disabled?: boolean;
|
|
87
|
+
invalid?: boolean;
|
|
88
|
+
size?: VariantProps<typeof inputVariants>["size"];
|
|
89
|
+
hasStartIcon?: boolean;
|
|
90
|
+
hasEndIcon?: boolean;
|
|
83
91
|
}
|
|
84
92
|
|
|
85
93
|
export interface InputTextProps extends TextComponentProps {
|
|
@@ -92,6 +100,10 @@ const INPUT_ICON_STROKE_WIDTH = 1.6;
|
|
|
92
100
|
const CHEVRON_SIZE_LG = 10;
|
|
93
101
|
const CHEVRON_SIZE_MD = 8;
|
|
94
102
|
|
|
103
|
+
const LABELED_INPUT_HEIGHT_CLASS = { lg: "h-14", md: "h-11" } as const;
|
|
104
|
+
const LABEL_FONT_SIZE = { lg: 16, md: 14 } as const;
|
|
105
|
+
const FLOATED_LABEL_FONT_SIZE = 12;
|
|
106
|
+
|
|
95
107
|
const inputVariants = cva(
|
|
96
108
|
"w-full rounded-md border-2 border-gray-stroke-muted bg-surface-card px-4 text-gray-foreground",
|
|
97
109
|
{
|
|
@@ -130,42 +142,112 @@ function InputField({ className, ...props }: ViewProps) {
|
|
|
130
142
|
return <View className={cn("relative", className)} {...props} />;
|
|
131
143
|
}
|
|
132
144
|
|
|
133
|
-
|
|
145
|
+
// `left-4`/`right-4` and `left-12`/`right-12` in FloatingLabel, in rem.
|
|
146
|
+
const LABEL_INSET_REM = 1;
|
|
147
|
+
const LABEL_ICON_INSET_REM = 3;
|
|
148
|
+
|
|
149
|
+
// Reanimated views drop `className` (see Skeleton), so the classes stay on plain views.
|
|
150
|
+
function FloatingLabelBackground({ progress }: { progress: SharedValue<number> }) {
|
|
151
|
+
const style = useAnimatedStyle(() => ({ opacity: progress.value }));
|
|
152
|
+
|
|
153
|
+
return (
|
|
154
|
+
<Animated.View style={[StyleSheet.absoluteFill, style]}>
|
|
155
|
+
<View className="absolute -inset-x-1 inset-y-0 rounded-sm bg-surface-card" />
|
|
156
|
+
</Animated.View>
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function FloatingLabel({
|
|
161
|
+
children,
|
|
162
|
+
floated = false,
|
|
163
|
+
disabled = false,
|
|
164
|
+
invalid = false,
|
|
165
|
+
size,
|
|
166
|
+
hasStartIcon = false,
|
|
167
|
+
hasEndIcon = false,
|
|
168
|
+
}: InputLabelProps) {
|
|
169
|
+
const resolvedSize = size ?? "lg";
|
|
134
170
|
const progress = useSharedValue(floated ? 1 : 0);
|
|
171
|
+
const metrics = useSharedValue({ restWidth: 0, floatedWidth: 0, floatedX: 0, floatedY: 0 });
|
|
172
|
+
const lastFloated = useRef(floated);
|
|
173
|
+
const floatedScale = FLOATED_LABEL_FONT_SIZE / LABEL_FONT_SIZE[resolvedSize];
|
|
135
174
|
|
|
136
175
|
useEffect(() => {
|
|
176
|
+
if (lastFloated.current === floated) return;
|
|
177
|
+
lastFloated.current = floated;
|
|
137
178
|
progress.value = withTiming(floated ? 1 : 0, { duration: 200, easing: Easing.out(Easing.ease) });
|
|
138
|
-
}, [floated]);
|
|
179
|
+
}, [floated, progress]);
|
|
180
|
+
|
|
181
|
+
const handleLayout = (event: LayoutChangeEvent) => {
|
|
182
|
+
const { x, width, height } = event.nativeEvent.layout;
|
|
183
|
+
const rem = x / (hasStartIcon ? LABEL_ICON_INSET_REM : LABEL_INSET_REM);
|
|
184
|
+
const inset = rem * LABEL_INSET_REM;
|
|
185
|
+
const fieldWidth = x + width + rem * (hasEndIcon ? LABEL_ICON_INSET_REM : LABEL_INSET_REM);
|
|
186
|
+
metrics.value = {
|
|
187
|
+
restWidth: width,
|
|
188
|
+
floatedWidth: fieldWidth - inset * 2,
|
|
189
|
+
floatedX: inset - x,
|
|
190
|
+
floatedY: -height / 2,
|
|
191
|
+
};
|
|
192
|
+
};
|
|
139
193
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
194
|
+
// Older React Native ignores `transformOrigin`, so it scales around the center
|
|
195
|
+
// and `translateX` makes up for the width lost on the left.
|
|
196
|
+
const labelStyle = useAnimatedStyle(() => {
|
|
197
|
+
const { restWidth, floatedWidth, floatedX, floatedY } = metrics.value;
|
|
198
|
+
const scale = interpolate(progress.value, [0, 1], [1, floatedScale]);
|
|
199
|
+
const width = interpolate(progress.value, [0, 1], [restWidth, floatedWidth / floatedScale]);
|
|
200
|
+
|
|
201
|
+
return {
|
|
202
|
+
opacity: restWidth > 0 ? 1 : 0,
|
|
203
|
+
width,
|
|
204
|
+
transform: [
|
|
205
|
+
{ translateX: interpolate(progress.value, [0, 1], [0, floatedX]) - (width * (1 - scale)) / 2 },
|
|
206
|
+
{ translateY: interpolate(progress.value, [0, 1], [0, floatedY]) },
|
|
207
|
+
{ scale },
|
|
208
|
+
],
|
|
209
|
+
};
|
|
210
|
+
});
|
|
144
211
|
|
|
145
212
|
return (
|
|
146
|
-
<
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
left: 16,
|
|
152
|
-
top: "50%",
|
|
153
|
-
zIndex: 10,
|
|
154
|
-
fontFamily: fontFamilyMedium,
|
|
155
|
-
},
|
|
156
|
-
]}
|
|
213
|
+
<View
|
|
214
|
+
pointerEvents="none"
|
|
215
|
+
importantForAccessibility="no-hide-descendants"
|
|
216
|
+
accessibilityElementsHidden
|
|
217
|
+
onLayout={handleLayout}
|
|
157
218
|
className={cn(
|
|
158
|
-
"
|
|
159
|
-
|
|
219
|
+
"absolute inset-y-0 z-10",
|
|
220
|
+
hasStartIcon ? "left-12" : "left-4",
|
|
221
|
+
hasEndIcon ? "right-12" : "right-4"
|
|
160
222
|
)}
|
|
161
223
|
>
|
|
162
|
-
|
|
163
|
-
|
|
224
|
+
<Animated.View
|
|
225
|
+
style={[{ flex: 1, justifyContent: "center", alignItems: "flex-start" }, labelStyle]}
|
|
226
|
+
>
|
|
227
|
+
<View className="max-w-full">
|
|
228
|
+
{!disabled && <FloatingLabelBackground progress={progress} />}
|
|
229
|
+
<Text
|
|
230
|
+
numberOfLines={1}
|
|
231
|
+
className={cn(
|
|
232
|
+
floated ? "font-medium" : "font-normal",
|
|
233
|
+
resolvedSize === "md" ? "text-sm" : "text-md",
|
|
234
|
+
disabled
|
|
235
|
+
? "text-gray-foreground-muted"
|
|
236
|
+
: invalid
|
|
237
|
+
? "text-feedback-error-foreground"
|
|
238
|
+
: "text-gray-foreground"
|
|
239
|
+
)}
|
|
240
|
+
>
|
|
241
|
+
{children}
|
|
242
|
+
</Text>
|
|
243
|
+
</View>
|
|
244
|
+
</Animated.View>
|
|
245
|
+
</View>
|
|
164
246
|
);
|
|
165
247
|
}
|
|
166
248
|
|
|
167
|
-
function InputLabel(
|
|
168
|
-
return <FloatingLabel
|
|
249
|
+
function InputLabel(props: InputLabelProps) {
|
|
250
|
+
return <FloatingLabel {...props} />;
|
|
169
251
|
}
|
|
170
252
|
|
|
171
253
|
function InputText({ invalid = false, className, ...props }: InputTextProps) {
|
|
@@ -265,11 +347,23 @@ function InputPassword({
|
|
|
265
347
|
)}
|
|
266
348
|
</InputStartIcon>
|
|
267
349
|
<View className="relative flex-1">
|
|
268
|
-
{label &&
|
|
350
|
+
{label && (
|
|
351
|
+
<InputLabel
|
|
352
|
+
floated={isFloated}
|
|
353
|
+
disabled={props.editable === false}
|
|
354
|
+
invalid={invalid}
|
|
355
|
+
size={size}
|
|
356
|
+
hasStartIcon
|
|
357
|
+
hasEndIcon
|
|
358
|
+
>
|
|
359
|
+
{label}
|
|
360
|
+
</InputLabel>
|
|
361
|
+
)}
|
|
269
362
|
<TextInput
|
|
270
363
|
secureTextEntry={!visible}
|
|
271
364
|
className={cn(
|
|
272
365
|
inputVariants({ size, invalid, focused, disabled: props.editable === false }),
|
|
366
|
+
label ? LABELED_INPUT_HEIGHT_CLASS[size ?? "lg"] : "",
|
|
273
367
|
"pl-12 pr-12 font-medium",
|
|
274
368
|
className
|
|
275
369
|
)}
|
|
@@ -277,6 +371,7 @@ function InputPassword({
|
|
|
277
371
|
defaultValue={defaultValue}
|
|
278
372
|
accessibilityLabel={label}
|
|
279
373
|
{...props}
|
|
374
|
+
placeholderTextColor={label ? "transparent" : props.placeholderTextColor}
|
|
280
375
|
onFocus={handleFocus}
|
|
281
376
|
onBlur={handleBlur}
|
|
282
377
|
onChangeText={handleChangeText}
|
|
@@ -348,7 +443,18 @@ function InputNumeric({
|
|
|
348
443
|
</InputStartIcon>
|
|
349
444
|
)}
|
|
350
445
|
<View className="relative flex-1">
|
|
351
|
-
{label &&
|
|
446
|
+
{label && (
|
|
447
|
+
<InputLabel
|
|
448
|
+
floated={isFloated}
|
|
449
|
+
disabled={!editable}
|
|
450
|
+
invalid={invalid}
|
|
451
|
+
size={size}
|
|
452
|
+
hasStartIcon={!!iconLeft}
|
|
453
|
+
hasEndIcon
|
|
454
|
+
>
|
|
455
|
+
{label}
|
|
456
|
+
</InputLabel>
|
|
457
|
+
)}
|
|
352
458
|
<TextInput
|
|
353
459
|
keyboardType={allowNegative ? "numbers-and-punctuation" : "numeric"}
|
|
354
460
|
value={displayValue}
|
|
@@ -362,12 +468,14 @@ function InputNumeric({
|
|
|
362
468
|
editable={editable}
|
|
363
469
|
className={cn(
|
|
364
470
|
inputVariants({ size, invalid, focused, disabled: !editable }),
|
|
471
|
+
label ? LABELED_INPUT_HEIGHT_CLASS[size ?? "lg"] : "",
|
|
365
472
|
iconLeft ? "pl-12" : "",
|
|
366
473
|
"pr-12 font-medium",
|
|
367
474
|
className
|
|
368
475
|
)}
|
|
369
476
|
accessibilityLabel={label}
|
|
370
477
|
{...props}
|
|
478
|
+
placeholderTextColor={label ? "transparent" : props.placeholderTextColor}
|
|
371
479
|
onFocus={handleFocus}
|
|
372
480
|
onBlur={handleBlur}
|
|
373
481
|
style={[{ fontFamily: fontFamilyMedium }, props.style]}
|
|
@@ -459,7 +567,18 @@ function InputTextual({
|
|
|
459
567
|
</InputStartIcon>
|
|
460
568
|
)}
|
|
461
569
|
<View className="relative flex-1">
|
|
462
|
-
{label &&
|
|
570
|
+
{label && (
|
|
571
|
+
<InputLabel
|
|
572
|
+
floated={isFloated}
|
|
573
|
+
disabled={!editable}
|
|
574
|
+
invalid={invalid}
|
|
575
|
+
size={size}
|
|
576
|
+
hasStartIcon={!!resolvedIconLeft}
|
|
577
|
+
hasEndIcon={!!resolvedIconRight}
|
|
578
|
+
>
|
|
579
|
+
{label}
|
|
580
|
+
</InputLabel>
|
|
581
|
+
)}
|
|
463
582
|
<TextInput
|
|
464
583
|
keyboardType={type === "email" ? "email-address" : "default"}
|
|
465
584
|
value={value}
|
|
@@ -467,6 +586,7 @@ function InputTextual({
|
|
|
467
586
|
editable={editable}
|
|
468
587
|
className={cn(
|
|
469
588
|
inputVariants({ size, invalid, focused, disabled: !editable }),
|
|
589
|
+
label ? LABELED_INPUT_HEIGHT_CLASS[size ?? "lg"] : "",
|
|
470
590
|
resolvedIconLeft ? "pl-12" : "",
|
|
471
591
|
resolvedIconRight ? "pr-12" : "",
|
|
472
592
|
"font-medium",
|
|
@@ -474,6 +594,7 @@ function InputTextual({
|
|
|
474
594
|
)}
|
|
475
595
|
accessibilityLabel={label}
|
|
476
596
|
{...props}
|
|
597
|
+
placeholderTextColor={label ? "transparent" : props.placeholderTextColor}
|
|
477
598
|
onFocus={handleFocus}
|
|
478
599
|
onBlur={handleBlur}
|
|
479
600
|
onChangeText={handleChangeText}
|
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
type ReactElement,
|
|
10
10
|
type ReactNode,
|
|
11
11
|
} from "react";
|
|
12
|
-
import { StyleSheet, type ViewStyle } from "react-native";
|
|
12
|
+
import { StyleSheet, type StyleProp, type ViewStyle } from "react-native";
|
|
13
13
|
import { cssInterop } from "nativewind";
|
|
14
14
|
import * as SwitchPrimitive from "@rn-primitives/switch";
|
|
15
15
|
import Animated, {
|
|
@@ -115,9 +115,10 @@ const switchVariants = cva("shrink-0", {
|
|
|
115
115
|
interface SwitchProps
|
|
116
116
|
extends Omit<
|
|
117
117
|
SwitchPrimitive.RootProps,
|
|
118
|
-
"checked" | "onCheckedChange" | "children"
|
|
118
|
+
"checked" | "onCheckedChange" | "children" | "style"
|
|
119
119
|
>,
|
|
120
120
|
VariantProps<typeof switchVariants> {
|
|
121
|
+
style?: StyleProp<ViewStyle>;
|
|
121
122
|
checked?: boolean;
|
|
122
123
|
defaultChecked?: boolean;
|
|
123
124
|
onCheckedChange?: (checked: boolean) => void;
|
|
@@ -333,10 +334,9 @@ const Switch = memo(function Switch({
|
|
|
333
334
|
switchVariants({ size: resolvedSize, variant }),
|
|
334
335
|
className
|
|
335
336
|
)}
|
|
336
|
-
style
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
]}
|
|
337
|
+
// Not a function: with a `className`, NativeWind spreads `style` into an
|
|
338
|
+
// object, and a function spreads to nothing.
|
|
339
|
+
style={[baseRootStyle, style]}
|
|
340
340
|
>
|
|
341
341
|
<Animated.View
|
|
342
342
|
pointerEvents="none"
|