@swan-io/lake 1.7.0 → 1.8.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/package.json +7 -8
- package/src/components/AutoWidthImage.d.ts +95 -3
- package/src/components/BottomPanel.js +68 -6
- package/src/components/Breadcrumbs.js +3 -3
- package/src/components/ChoicePicker.js +99 -8
- package/src/components/Form.d.ts +122 -3
- package/src/components/Heading.d.ts +96 -3
- package/src/components/LakeButton.js +1 -1
- package/src/components/LakeHeading.d.ts +96 -3
- package/src/components/LakeTextInput.d.ts +189 -2
- package/src/components/Link.d.ts +44 -48
- package/src/components/Pressable.d.ts +118 -122
- package/src/utils/math.d.ts +13 -0
- package/src/utils/math.js +9 -1
- package/src/utils/viewport.d.ts +2 -0
- package/src/utils/viewport.js +33 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@swan-io/lake",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.8.0",
|
|
4
4
|
"engines": {
|
|
5
5
|
"node": ">=14.0.0",
|
|
6
6
|
"yarn": "^1.20.0"
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
],
|
|
27
27
|
"license": "MIT",
|
|
28
28
|
"dependencies": {
|
|
29
|
-
"@popperjs/core": "^2.11.
|
|
29
|
+
"@popperjs/core": "^2.11.7",
|
|
30
30
|
"@swan-io/boxed": "^0.13.0",
|
|
31
31
|
"@swan-io/chicane": "^1.3.4",
|
|
32
32
|
"dayjs": "^1.11.7",
|
|
@@ -35,22 +35,21 @@
|
|
|
35
35
|
"react": "^18.2.0",
|
|
36
36
|
"react-atomic-state": "^1.2.7",
|
|
37
37
|
"react-dom": "^18.2.0",
|
|
38
|
-
"react-native-web": "^0.19.
|
|
38
|
+
"react-native-web": "^0.19.4",
|
|
39
39
|
"react-popper": "^2.3.0",
|
|
40
40
|
"react-ux-form": "^1.3.0",
|
|
41
41
|
"rifm": "^0.12.1",
|
|
42
42
|
"ts-dedent": "^2.2.0",
|
|
43
|
-
"ts-pattern": "^4.2.
|
|
44
|
-
"urql": "^
|
|
43
|
+
"ts-pattern": "^4.2.2",
|
|
44
|
+
"urql": "^4.0.0",
|
|
45
45
|
"uuid": "^9.0.0"
|
|
46
46
|
},
|
|
47
47
|
"devDependencies": {
|
|
48
|
-
"@
|
|
49
|
-
"@types/react": "^18.0.28",
|
|
48
|
+
"@types/react": "^18.0.35",
|
|
50
49
|
"@types/react-dom": "^18.0.11",
|
|
51
50
|
"@types/react-native": "^0.71.5",
|
|
52
51
|
"@types/uuid": "^9.0.1",
|
|
53
52
|
"jsdom": "^21.1.1",
|
|
54
|
-
"type-fest": "^3.
|
|
53
|
+
"type-fest": "^3.8.0"
|
|
55
54
|
}
|
|
56
55
|
}
|
|
@@ -1,7 +1,99 @@
|
|
|
1
1
|
/// <reference types="react" />
|
|
2
|
-
import { Image
|
|
3
|
-
|
|
4
|
-
|
|
2
|
+
import { Image } from "react-native";
|
|
3
|
+
export declare const AutoWidthImage: import("react").MemoExoticComponent<import("react").ForwardRefExoticComponent<{
|
|
4
|
+
style?: import("react-native").StyleProp<import("react-native").ImageStyle>;
|
|
5
|
+
role?: import("react-native").WebRole | undefined;
|
|
6
|
+
defaultSource?: any;
|
|
7
|
+
draggable?: boolean | undefined;
|
|
8
|
+
id?: string | undefined;
|
|
9
|
+
onLayout?: ((event: import("react-native").LayoutChangeEvent) => void) | undefined;
|
|
10
|
+
onError?: ((error: import("react-native").NativeSyntheticEvent<import("react-native").ImageErrorEventData>) => void) | undefined;
|
|
11
|
+
onLoad?: ((event: import("react-native").NativeSyntheticEvent<import("react-native").ImageLoadEventData>) => void) | undefined;
|
|
12
|
+
onLoadEnd?: (() => void) | undefined;
|
|
13
|
+
onLoadStart?: (() => void) | undefined;
|
|
14
|
+
progressiveRenderingEnabled?: boolean | undefined;
|
|
15
|
+
borderRadius?: number | undefined;
|
|
16
|
+
borderTopLeftRadius?: number | undefined;
|
|
17
|
+
borderTopRightRadius?: number | undefined;
|
|
18
|
+
borderBottomLeftRadius?: number | undefined;
|
|
19
|
+
borderBottomRightRadius?: number | undefined;
|
|
20
|
+
resizeMode?: import("react-native").ImageResizeMode | undefined;
|
|
21
|
+
resizeMethod?: "auto" | "resize" | "scale" | undefined;
|
|
22
|
+
loadingIndicatorSource?: import("react-native").ImageURISource | undefined;
|
|
23
|
+
testID?: string | undefined;
|
|
24
|
+
nativeID?: string | undefined;
|
|
25
|
+
alt?: string | undefined;
|
|
26
|
+
blurRadius?: number | undefined;
|
|
27
|
+
capInsets?: import("react-native").Insets | undefined;
|
|
28
|
+
onProgress?: ((event: import("react-native").NativeSyntheticEvent<import("react-native").ImageProgressEventDataIOS>) => void) | undefined;
|
|
29
|
+
onPartialLoad?: (() => void) | undefined;
|
|
30
|
+
fadeDuration?: number | undefined;
|
|
31
|
+
accessible?: boolean | undefined;
|
|
32
|
+
accessibilityActions?: readonly Readonly<{
|
|
33
|
+
name: string;
|
|
34
|
+
label?: string | undefined;
|
|
35
|
+
}>[] | undefined;
|
|
36
|
+
accessibilityLabel?: string | undefined;
|
|
37
|
+
'aria-label'?: string | undefined;
|
|
38
|
+
accessibilityRole?: import("react-native").AccessibilityRole | undefined;
|
|
39
|
+
accessibilityState?: import("react-native").AccessibilityState | undefined;
|
|
40
|
+
'aria-busy'?: boolean | undefined;
|
|
41
|
+
'aria-checked'?: boolean | "mixed" | undefined;
|
|
42
|
+
'aria-disabled'?: boolean | undefined;
|
|
43
|
+
'aria-expanded'?: boolean | undefined;
|
|
44
|
+
'aria-selected'?: boolean | undefined;
|
|
45
|
+
'aria-labelledby'?: string | undefined;
|
|
46
|
+
accessibilityHint?: string | undefined;
|
|
47
|
+
accessibilityValue?: import("react-native").AccessibilityValue | undefined;
|
|
48
|
+
'aria-valuemax'?: number | undefined;
|
|
49
|
+
'aria-valuemin'?: number | undefined;
|
|
50
|
+
'aria-valuenow'?: number | undefined;
|
|
51
|
+
'aria-valuetext'?: string | undefined;
|
|
52
|
+
onAccessibilityAction?: ((event: import("react-native").AccessibilityActionEvent) => void) | undefined;
|
|
53
|
+
importantForAccessibility?: "auto" | "yes" | "no" | "no-hide-descendants" | undefined;
|
|
54
|
+
'aria-hidden'?: boolean | undefined;
|
|
55
|
+
'aria-live'?: "polite" | "assertive" | "off" | undefined;
|
|
56
|
+
'aria-modal'?: boolean | undefined;
|
|
57
|
+
accessibilityLiveRegion?: "none" | "polite" | "assertive" | undefined;
|
|
58
|
+
accessibilityElementsHidden?: boolean | undefined;
|
|
59
|
+
accessibilityViewIsModal?: boolean | undefined;
|
|
60
|
+
onAccessibilityEscape?: (() => void) | undefined;
|
|
61
|
+
onAccessibilityTap?: (() => void) | undefined;
|
|
62
|
+
onMagicTap?: (() => void) | undefined;
|
|
63
|
+
accessibilityIgnoresInvertColors?: boolean | undefined;
|
|
64
|
+
tabIndex?: 0 | -1 | undefined;
|
|
65
|
+
"aria-activedescendant"?: string | undefined;
|
|
66
|
+
"aria-atomic"?: boolean | undefined;
|
|
67
|
+
"aria-autocomplete"?: string | undefined;
|
|
68
|
+
"aria-colcount"?: number | undefined;
|
|
69
|
+
"aria-colindex"?: number | undefined;
|
|
70
|
+
"aria-colspan"?: number | undefined;
|
|
71
|
+
"aria-controls"?: string | undefined;
|
|
72
|
+
"aria-current"?: boolean | "time" | "page" | "step" | "location" | "date" | undefined;
|
|
73
|
+
"aria-describedby"?: string | undefined;
|
|
74
|
+
"aria-details"?: string | undefined;
|
|
75
|
+
"aria-errormessage"?: string | undefined;
|
|
76
|
+
"aria-flowto"?: string | undefined;
|
|
77
|
+
"aria-haspopup"?: string | undefined;
|
|
78
|
+
"aria-invalid"?: boolean | undefined;
|
|
79
|
+
"aria-keyshortcuts"?: string | undefined;
|
|
80
|
+
"aria-level"?: number | undefined;
|
|
81
|
+
"aria-multiline"?: boolean | undefined;
|
|
82
|
+
"aria-multiselectable"?: boolean | undefined;
|
|
83
|
+
"aria-orientation"?: "horizontal" | "vertical" | undefined;
|
|
84
|
+
"aria-owns"?: string | undefined;
|
|
85
|
+
"aria-placeholder"?: string | undefined;
|
|
86
|
+
"aria-posinset"?: number | undefined;
|
|
87
|
+
"aria-pressed"?: boolean | undefined;
|
|
88
|
+
"aria-readonly"?: boolean | undefined;
|
|
89
|
+
"aria-required"?: boolean | undefined;
|
|
90
|
+
"aria-roledescription"?: string | undefined;
|
|
91
|
+
"aria-rowcount"?: number | undefined;
|
|
92
|
+
"aria-rowindex"?: number | undefined;
|
|
93
|
+
"aria-rowspan"?: number | undefined;
|
|
94
|
+
"aria-setsize"?: number | undefined;
|
|
95
|
+
"aria-sort"?: "none" | "ascending" | "descending" | "other" | undefined;
|
|
96
|
+
} & {
|
|
5
97
|
ariaLabel?: string | undefined;
|
|
6
98
|
maxWidth?: number | undefined;
|
|
7
99
|
height: number;
|
|
@@ -1,13 +1,22 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { Suspense, useEffect, useState } from "react";
|
|
3
|
-
import { Pressable, ScrollView, StyleSheet, View } from "react-native";
|
|
2
|
+
import { Suspense, useEffect, useMemo, useRef, useState } from "react";
|
|
3
|
+
import { PanResponder, Pressable, ScrollView, StyleSheet, View } from "react-native";
|
|
4
4
|
import { commonStyles } from "../constants/commonStyles";
|
|
5
|
-
import { backgroundColor, radii, shadows, spacings } from "../constants/design";
|
|
5
|
+
import { backgroundColor, colors, radii, shadows, spacings } from "../constants/design";
|
|
6
6
|
import { useBodyClassName } from "../hooks/useBodyClassName";
|
|
7
|
+
import { limitElastic } from "../utils/math";
|
|
7
8
|
import { FocusTrap } from "./FocusTrap";
|
|
8
9
|
import { LoadingView } from "./LoadingView";
|
|
9
10
|
import { Portal } from "./Portal";
|
|
10
11
|
import { TransitionView } from "./TransitionView";
|
|
12
|
+
const ELASTIC_LENGTH = 100; // the maximum value you can reach
|
|
13
|
+
const ELASTIC_STRENGTH = 0.008; // higher value, maximum value reached faster
|
|
14
|
+
const limitGrab = limitElastic({
|
|
15
|
+
elasticLength: ELASTIC_LENGTH,
|
|
16
|
+
elasticStrength: ELASTIC_STRENGTH,
|
|
17
|
+
});
|
|
18
|
+
const DELTA_Y_CLOSE_THRESHOLD = 100;
|
|
19
|
+
const SWIPE_CLOSE_VELOCITY = 0.5;
|
|
11
20
|
const BACKGROUND_COLOR = "rgba(0, 0, 0, 0.6)";
|
|
12
21
|
const styles = StyleSheet.create({
|
|
13
22
|
fill: {
|
|
@@ -53,6 +62,18 @@ const styles = StyleSheet.create({
|
|
|
53
62
|
animationDuration: "300ms",
|
|
54
63
|
animationTimingFunction: "ease-in-out",
|
|
55
64
|
},
|
|
65
|
+
container: {
|
|
66
|
+
...StyleSheet.absoluteFillObject,
|
|
67
|
+
transitionDuration: "300ms",
|
|
68
|
+
transitionProperty: "transform",
|
|
69
|
+
},
|
|
70
|
+
bottomCache: {
|
|
71
|
+
position: "absolute",
|
|
72
|
+
bottom: -ELASTIC_LENGTH + 1,
|
|
73
|
+
width: "100%",
|
|
74
|
+
height: ELASTIC_LENGTH,
|
|
75
|
+
backgroundColor: backgroundColor.accented,
|
|
76
|
+
},
|
|
56
77
|
modalContainer: {
|
|
57
78
|
...StyleSheet.absoluteFillObject,
|
|
58
79
|
},
|
|
@@ -69,14 +90,26 @@ const styles = StyleSheet.create({
|
|
|
69
90
|
borderTopRightRadius: radii[8],
|
|
70
91
|
boxShadow: shadows.modal,
|
|
71
92
|
alignSelf: "stretch",
|
|
72
|
-
marginTop: spacings[32],
|
|
73
93
|
},
|
|
74
94
|
pressableOverlay: {
|
|
75
|
-
...
|
|
95
|
+
...commonStyles.fill,
|
|
96
|
+
outlineWidth: 0,
|
|
97
|
+
// make focus indicator invisible on iOS (outline: none doesn't work)
|
|
98
|
+
opacity: 0,
|
|
99
|
+
},
|
|
100
|
+
grabContainer: {
|
|
101
|
+
paddingHorizontal: 128,
|
|
102
|
+
paddingVertical: spacings[12],
|
|
103
|
+
},
|
|
104
|
+
grabLine: {
|
|
105
|
+
backgroundColor: colors.gray[100],
|
|
106
|
+
height: 5,
|
|
107
|
+
borderRadius: radii[4],
|
|
76
108
|
},
|
|
77
109
|
});
|
|
78
110
|
export const BottomPanel = ({ visible, onPressClose, children, returnFocus = true }) => {
|
|
79
111
|
const [rootElement, setRootElement] = useState(() => undefined);
|
|
112
|
+
const container = useRef(null);
|
|
80
113
|
useEffect(() => {
|
|
81
114
|
const rootElement = document.createElement("div");
|
|
82
115
|
document.body.append(rootElement);
|
|
@@ -86,9 +119,38 @@ export const BottomPanel = ({ visible, onPressClose, children, returnFocus = tru
|
|
|
86
119
|
setRootElement(undefined);
|
|
87
120
|
};
|
|
88
121
|
}, []);
|
|
122
|
+
const panResponder = useMemo(() => PanResponder.create({
|
|
123
|
+
onMoveShouldSetPanResponder: () => true,
|
|
124
|
+
onPanResponderGrant: () => {
|
|
125
|
+
if (container.current instanceof HTMLElement) {
|
|
126
|
+
container.current.style.transitionDuration = "0ms";
|
|
127
|
+
}
|
|
128
|
+
},
|
|
129
|
+
onPanResponderMove: (_event, { dy }) => {
|
|
130
|
+
const translateY = dy > 0 ? dy : -limitGrab(-dy);
|
|
131
|
+
if (container.current instanceof HTMLElement) {
|
|
132
|
+
container.current.style.transform = `translateY(${translateY}px)`;
|
|
133
|
+
}
|
|
134
|
+
},
|
|
135
|
+
onPanResponderRelease: (_event, gestureState) => {
|
|
136
|
+
if (container.current instanceof HTMLElement) {
|
|
137
|
+
// @ts-expect-error
|
|
138
|
+
container.current.style.transitionDuration = null;
|
|
139
|
+
}
|
|
140
|
+
const shouldClose = gestureState.dy > DELTA_Y_CLOSE_THRESHOLD || gestureState.vy > SWIPE_CLOSE_VELOCITY;
|
|
141
|
+
if (shouldClose) {
|
|
142
|
+
onPressClose();
|
|
143
|
+
}
|
|
144
|
+
else {
|
|
145
|
+
if (container.current instanceof HTMLElement) {
|
|
146
|
+
container.current.style.transform = `translateY(0px)`;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
},
|
|
150
|
+
}), [onPressClose]);
|
|
89
151
|
useBodyClassName("BottomPanelOpen", { enabled: visible });
|
|
90
152
|
if (rootElement == null) {
|
|
91
153
|
return null;
|
|
92
154
|
}
|
|
93
|
-
return (_jsxs(Portal, { container: rootElement, children: [_jsx(TransitionView, { style: styles.fill, enter: styles.overlayEnter, leave: styles.overlayLeave, children: visible ? _jsx(View, { style: styles.overlay }) : null }), _jsx(Suspense, { fallback: _jsx(LoadingView, { color: backgroundColor.accented, delay: 0 }), children: _jsx(TransitionView, { style: styles.fill, enter: styles.modalEnter, leave: styles.modalLeave, children: visible ? (_jsxs(ScrollView, { style: styles.modalContainer, contentContainerStyle: styles.modalContentContainer, children:
|
|
155
|
+
return (_jsxs(Portal, { container: rootElement, children: [_jsx(TransitionView, { style: styles.fill, enter: styles.overlayEnter, leave: styles.overlayLeave, children: visible ? _jsx(View, { style: styles.overlay }) : null }), _jsx(Suspense, { fallback: _jsx(LoadingView, { color: backgroundColor.accented, delay: 0 }), children: _jsx(TransitionView, { style: styles.fill, enter: styles.modalEnter, leave: styles.modalLeave, children: visible ? (_jsxs(View, { ref: container, style: styles.container, children: [_jsx(ScrollView, { style: styles.modalContainer, contentContainerStyle: styles.modalContentContainer, children: _jsxs(FocusTrap, { autoFocus: true, focusLock: true, returnFocus: returnFocus, style: styles.trap, children: [onPressClose != null ? (_jsx(Pressable, { onPress: onPressClose, style: styles.pressableOverlay })) : null, _jsxs(View, { style: styles.modal, children: [_jsx(View, { style: styles.grabContainer, ...panResponder.panHandlers, children: _jsx(View, { style: styles.grabLine }) }), children] })] }) }), _jsx(View, { style: styles.bottomCache })] })) : null }) })] }));
|
|
94
156
|
};
|
|
@@ -165,10 +165,10 @@ export const useCrumb = (crumb) => {
|
|
|
165
165
|
}, [id, crumb, setValue, index]);
|
|
166
166
|
};
|
|
167
167
|
const CHEVRON = (_jsx(View, { style: styles.chevron, children: _jsx(Icon, { name: "chevron-right-filled", color: colors.gray[500], size: 16 }) }));
|
|
168
|
-
const BreadcrumbsSiblingsDropdown = ({ siblings, onPress, }) => {
|
|
168
|
+
const BreadcrumbsSiblingsDropdown = ({ siblings, isLast, onPress, }) => {
|
|
169
169
|
return (_jsx(View, { style: styles.siblingsDropdown, children: siblings.map(({ url, label, isMatching }) => {
|
|
170
170
|
return (_jsx(Link, { to: url, ariaCurrentValue: "location", onPress: (event) => {
|
|
171
|
-
if (isMatching) {
|
|
171
|
+
if (isMatching && isLast) {
|
|
172
172
|
event.preventDefault();
|
|
173
173
|
}
|
|
174
174
|
onPress();
|
|
@@ -192,7 +192,7 @@ const BreadcrumbsItem = ({ crumb, isFirstItem = false, isLastItem = false, shoul
|
|
|
192
192
|
return (_jsxs(View, { style: [styles.item, shouldAnimate ? animations.fadeAndSlideInFromRight.enter : null], children: [!isFirstItem ? CHEVRON : null, _jsxs(View, { children: [_jsx(Link, { to: crumb.link, ariaCurrentValue: "location", onPress: handlePress, children: _jsx(View, { ref: hoverRef, style: [
|
|
193
193
|
styles.horizontalLink,
|
|
194
194
|
shouldAnimate && animations.fadeAndSlideInFromRight.enter,
|
|
195
|
-
], children: _jsxs(LakeText, { color: colors.gray[800], style: [styles.horizontalLinkText, isLastItem && styles.activeHorizontalLinkText], children: [_jsx(Text, { style: isHovered && !isLastItem ? styles.horizontalLinkTextHovered : undefined, children: crumb.label }), crumb.siblings != null ? (_jsxs(_Fragment, { children: [_jsx(Space, { width: 4 }), _jsx(Icon, { name: "chevron-down-filled", color: colors.gray[500], size: 16 })] })) : null] }) }) }), _jsx(View, { style: styles.dropdownContainer, children: _jsx(TransitionView, { ...animations.fadeAndSlideInFromBottom, children: siblings ? (_jsx(FocusTrap, { autoFocus: true, focusLock: true, returnFocus: true, onClickOutside: () => setSiblings(null), onEscapeKey: () => setSiblings(null), children: _jsx(BreadcrumbsSiblingsDropdown, { siblings: siblings, onPress: () => setSiblings(null) }) })) : null }) })] })] }));
|
|
195
|
+
], children: _jsxs(LakeText, { color: colors.gray[800], style: [styles.horizontalLinkText, isLastItem && styles.activeHorizontalLinkText], children: [_jsx(Text, { style: isHovered && !isLastItem ? styles.horizontalLinkTextHovered : undefined, children: crumb.label }), crumb.siblings != null ? (_jsxs(_Fragment, { children: [_jsx(Space, { width: 4 }), _jsx(Icon, { name: "chevron-down-filled", color: colors.gray[500], size: 16 })] })) : null] }) }) }), _jsx(View, { style: styles.dropdownContainer, children: _jsx(TransitionView, { ...animations.fadeAndSlideInFromBottom, children: siblings ? (_jsx(FocusTrap, { autoFocus: true, focusLock: true, returnFocus: true, onClickOutside: () => setSiblings(null), onEscapeKey: () => setSiblings(null), children: _jsx(BreadcrumbsSiblingsDropdown, { siblings: siblings, isLast: isLastItem, onPress: () => setSiblings(null) }) })) : null }) })] })] }));
|
|
196
196
|
};
|
|
197
197
|
const BreadcrumbsDropdown = ({ crumbs, onHoverStart, onHoverEnd, onLinkFocus, onLinkBlur, onLinkPress, }) => {
|
|
198
198
|
const containerRef = useRef(null);
|
|
@@ -1,8 +1,11 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { useState } from "react";
|
|
3
|
-
import { StyleSheet, View } from "react-native";
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useRef, useState } from "react";
|
|
3
|
+
import { ScrollView, StyleSheet, View } from "react-native";
|
|
4
|
+
import { match } from "ts-pattern";
|
|
4
5
|
import { breakpoints, negativeSpacings, spacings } from "../constants/design";
|
|
5
6
|
import { useResponsive } from "../hooks/useResponsive";
|
|
7
|
+
import { clampValue } from "../utils/math";
|
|
8
|
+
import { detectScrollAnimationEnd } from "../utils/viewport";
|
|
6
9
|
import { LakeButton } from "./LakeButton";
|
|
7
10
|
import { LakeRadio } from "./LakeRadio";
|
|
8
11
|
import { Pressable } from "./Pressable";
|
|
@@ -16,6 +19,9 @@ const styles = StyleSheet.create({
|
|
|
16
19
|
overflow: "hidden",
|
|
17
20
|
marginHorizontal: negativeSpacings[12],
|
|
18
21
|
},
|
|
22
|
+
scrollSnap: {
|
|
23
|
+
scrollSnapType: "x mandatory",
|
|
24
|
+
},
|
|
19
25
|
container: {
|
|
20
26
|
alignSelf: "stretch",
|
|
21
27
|
flexDirection: "row",
|
|
@@ -35,6 +41,8 @@ const styles = StyleSheet.create({
|
|
|
35
41
|
flexBasis: "33.333%",
|
|
36
42
|
maxWidth: 300,
|
|
37
43
|
padding: spacings[12],
|
|
44
|
+
},
|
|
45
|
+
itemAnimation: {
|
|
38
46
|
transform: "translateZ(0px)",
|
|
39
47
|
animationKeyframes: {
|
|
40
48
|
from: {
|
|
@@ -58,6 +66,7 @@ const styles = StyleSheet.create({
|
|
|
58
66
|
width: "100%",
|
|
59
67
|
flexBasis: "auto",
|
|
60
68
|
maxWidth: "none",
|
|
69
|
+
scrollSnapAlign: "center",
|
|
61
70
|
},
|
|
62
71
|
tileContents: {
|
|
63
72
|
alignItems: "center",
|
|
@@ -74,26 +83,108 @@ const styles = StyleSheet.create({
|
|
|
74
83
|
top: "50%",
|
|
75
84
|
left: negativeSpacings[24],
|
|
76
85
|
transform: "translateY(-50%)",
|
|
86
|
+
borderTopLeftRadius: 0,
|
|
87
|
+
borderBottomLeftRadius: 0,
|
|
88
|
+
borderWidth: 1,
|
|
89
|
+
borderLeftWidth: 0,
|
|
77
90
|
},
|
|
78
91
|
rightButton: {
|
|
79
92
|
position: "absolute",
|
|
80
93
|
top: "50%",
|
|
81
94
|
right: negativeSpacings[24],
|
|
82
95
|
transform: "translateY(-50%)",
|
|
96
|
+
borderTopRightRadius: 0,
|
|
97
|
+
borderBottomRightRadius: 0,
|
|
98
|
+
borderWidth: 1,
|
|
99
|
+
borderRightWidth: 0,
|
|
83
100
|
},
|
|
84
101
|
});
|
|
85
102
|
const identity = (x) => x;
|
|
86
103
|
export const ChoicePicker = ({ items, getId = identity, large = false, renderItem, value, onChange, }) => {
|
|
104
|
+
const containerRef = useRef(null);
|
|
87
105
|
const { desktop } = useResponsive(breakpoints.medium);
|
|
88
|
-
const [
|
|
89
|
-
|
|
106
|
+
const [mobilePosition, setMobilePosition] = useState("start");
|
|
107
|
+
useEffect(() => {
|
|
108
|
+
if (desktop) {
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
// auto scroll to selected value on mobile
|
|
112
|
+
const scrollContainer = containerRef.current;
|
|
113
|
+
const index = items.findIndex(item => value === item);
|
|
114
|
+
if (index !== -1 && scrollContainer instanceof HTMLDivElement) {
|
|
115
|
+
const width = scrollContainer.offsetWidth;
|
|
116
|
+
scrollContainer.scrollTo({ x: index * width, animated: false });
|
|
117
|
+
}
|
|
118
|
+
// if no value is selected, select first item
|
|
119
|
+
if (value == null && items[0] != null) {
|
|
120
|
+
onChange(items[0]);
|
|
121
|
+
}
|
|
122
|
+
// disable exhaustive-deps because we only want to run this effect only when screen size go from desktop to mobile
|
|
123
|
+
}, [desktop]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
124
|
+
const onScroll = () => {
|
|
125
|
+
// prevent scroll event when we change screen size from mobile to desktop
|
|
126
|
+
if (desktop) {
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
const scrollContainer = containerRef.current;
|
|
130
|
+
if (scrollContainer instanceof HTMLDivElement) {
|
|
131
|
+
const scrollLeft = scrollContainer.scrollLeft;
|
|
132
|
+
const width = scrollContainer.offsetWidth;
|
|
133
|
+
const index = clampValue(0, items.length - 1)(Math.round(scrollLeft / width));
|
|
134
|
+
const item = items[index];
|
|
135
|
+
if (item != null) {
|
|
136
|
+
onChange(item);
|
|
137
|
+
}
|
|
138
|
+
match(index)
|
|
139
|
+
.with(0, () => setMobilePosition("start"))
|
|
140
|
+
.with(items.length - 1, () => setMobilePosition("end"))
|
|
141
|
+
.otherwise(() => setMobilePosition("middle"));
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
const onPressPrevious = () => {
|
|
145
|
+
const scrollContainer = containerRef.current;
|
|
146
|
+
if (scrollContainer instanceof HTMLDivElement) {
|
|
147
|
+
const scrollLeft = scrollContainer.scrollLeft;
|
|
148
|
+
const width = scrollContainer.offsetWidth;
|
|
149
|
+
const index = Math.round(scrollLeft / width);
|
|
150
|
+
const previousIndex = Math.max(0, index - 1);
|
|
151
|
+
// remove scroll snap during scroll animation to avoid weird behavior on older browsers
|
|
152
|
+
scrollContainer.style.scrollSnapType = "none";
|
|
153
|
+
containerRef.current?.scrollTo({ x: previousIndex * width, animated: true });
|
|
154
|
+
detectScrollAnimationEnd(scrollContainer).onResolve(() => {
|
|
155
|
+
// set back scroll snap
|
|
156
|
+
// @ts-expect-error
|
|
157
|
+
scrollContainer.style.scrollSnapType = null;
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
const onPressNext = () => {
|
|
162
|
+
const scrollContainer = containerRef.current;
|
|
163
|
+
if (scrollContainer instanceof HTMLDivElement) {
|
|
164
|
+
const scrollLeft = scrollContainer.scrollLeft;
|
|
165
|
+
const width = scrollContainer.offsetWidth;
|
|
166
|
+
const index = Math.round(scrollLeft / width);
|
|
167
|
+
const nextIndex = Math.min(items.length - 1, index + 1);
|
|
168
|
+
// remove scroll snap during scroll animation to avoid weird behavior on older browsers
|
|
169
|
+
scrollContainer.style.scrollSnapType = "none";
|
|
170
|
+
containerRef.current?.scrollTo({ x: nextIndex * width, animated: true });
|
|
171
|
+
detectScrollAnimationEnd(scrollContainer).onResolve(() => {
|
|
172
|
+
// set back scroll snap
|
|
173
|
+
// @ts-expect-error
|
|
174
|
+
scrollContainer.style.scrollSnapType = null;
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
};
|
|
178
|
+
return (_jsxs(View, { children: [_jsx(View, { style: styles.root, children: _jsx(ScrollView, { ref: containerRef, horizontal: !desktop, onScroll: onScroll, scrollEventThrottle: 200, style: styles.scrollSnap, contentContainerStyle: [
|
|
90
179
|
styles.container,
|
|
91
180
|
!desktop && styles.mobileContainer,
|
|
92
|
-
!desktop && {
|
|
181
|
+
!desktop && { width: `${items.length * 100}%` },
|
|
93
182
|
], children: items.map((item, index) => (_jsx(Pressable, { style: [
|
|
94
183
|
styles.item,
|
|
184
|
+
desktop && styles.itemAnimation,
|
|
185
|
+
desktop && { animationDelay: `${200 + 100 * index}ms` },
|
|
95
186
|
large && styles.itemLarge,
|
|
96
187
|
!desktop && styles.itemSmallViewport,
|
|
97
|
-
{
|
|
98
|
-
], onPress: () => onChange(item), children: ({ hovered }) => (_jsx(Tile, { hovered: hovered, selected: value != null && getId(item) === getId(value), flexGrow: 1, children: _jsxs(View, { style: styles.tileContents, children: [_jsx(View, { style: styles.tileRenderedContents, children: renderItem(item) }), _jsx(Space, { height: 24 }), _jsx(LakeRadio, { value: value != null && getId(item) === getId(value) })] }) })) }, String(index)))) }) }), !desktop && (_jsx(
|
|
188
|
+
!desktop && { width: `${100 / items.length}%` },
|
|
189
|
+
], onPress: () => onChange(item), children: ({ hovered }) => (_jsx(Tile, { hovered: hovered, selected: value != null && getId(item) === getId(value), flexGrow: 1, children: _jsxs(View, { style: styles.tileContents, children: [_jsx(View, { style: styles.tileRenderedContents, children: renderItem(item) }), desktop && (_jsxs(_Fragment, { children: [_jsx(Space, { height: 24 }), _jsx(LakeRadio, { value: value != null && getId(item) === getId(value) })] }))] }) })) }, String(index)))) }) }), !desktop && (_jsx(LakeButton, { icon: "chevron-left-filled", mode: "secondary", forceBackground: true, onPress: onPressPrevious, disabled: mobilePosition === "start", style: styles.leftButton })), !desktop && (_jsx(LakeButton, { icon: "chevron-right-filled", mode: "secondary", forceBackground: true, onPress: onPressNext, disabled: mobilePosition === "end", style: styles.rightButton }))] }));
|
|
99
190
|
};
|
package/src/components/Form.d.ts
CHANGED
|
@@ -1,7 +1,126 @@
|
|
|
1
1
|
import { ReactNode } from "react";
|
|
2
|
-
import { View
|
|
3
|
-
|
|
4
|
-
|
|
2
|
+
import { View } from "react-native";
|
|
3
|
+
export declare const Form: import("react").MemoExoticComponent<import("react").ForwardRefExoticComponent<{
|
|
4
|
+
children?: ReactNode;
|
|
5
|
+
hitSlop?: import("react-native").Insets | undefined;
|
|
6
|
+
id?: string | undefined;
|
|
7
|
+
onLayout?: ((event: import("react-native").LayoutChangeEvent) => void) | undefined;
|
|
8
|
+
pointerEvents?: "auto" | "none" | "box-none" | "box-only" | undefined;
|
|
9
|
+
removeClippedSubviews?: boolean | undefined;
|
|
10
|
+
style?: import("react-native").StyleProp<import("react-native").ViewStyle>;
|
|
11
|
+
testID?: string | undefined;
|
|
12
|
+
nativeID?: string | undefined;
|
|
13
|
+
onKeyDown?: ((event: NativeSyntheticEvent<import("react").KeyboardEvent<Element>>) => void) | undefined;
|
|
14
|
+
onKeyDownCapture?: ((event: NativeSyntheticEvent<import("react").KeyboardEvent<Element>>) => void) | undefined;
|
|
15
|
+
onKeyUp?: ((event: NativeSyntheticEvent<import("react").KeyboardEvent<Element>>) => void) | undefined;
|
|
16
|
+
onKeyUpCapture?: ((event: NativeSyntheticEvent<import("react").KeyboardEvent<Element>>) => void) | undefined;
|
|
17
|
+
collapsable?: boolean | undefined;
|
|
18
|
+
needsOffscreenAlphaCompositing?: boolean | undefined;
|
|
19
|
+
renderToHardwareTextureAndroid?: boolean | undefined;
|
|
20
|
+
focusable?: boolean | undefined;
|
|
21
|
+
shouldRasterizeIOS?: boolean | undefined;
|
|
22
|
+
isTVSelectable?: boolean | undefined;
|
|
23
|
+
hasTVPreferredFocus?: boolean | undefined;
|
|
24
|
+
tvParallaxProperties?: import("react-native").TVParallaxProperties | undefined;
|
|
25
|
+
tvParallaxShiftDistanceX?: number | undefined;
|
|
26
|
+
tvParallaxShiftDistanceY?: number | undefined;
|
|
27
|
+
tvParallaxTiltAngle?: number | undefined;
|
|
28
|
+
tvParallaxMagnification?: number | undefined;
|
|
29
|
+
onStartShouldSetResponder?: ((event: import("react-native").GestureResponderEvent) => boolean) | undefined;
|
|
30
|
+
onMoveShouldSetResponder?: ((event: import("react-native").GestureResponderEvent) => boolean) | undefined;
|
|
31
|
+
onResponderEnd?: ((event: import("react-native").GestureResponderEvent) => void) | undefined;
|
|
32
|
+
onResponderGrant?: ((event: import("react-native").GestureResponderEvent) => void) | undefined;
|
|
33
|
+
onResponderReject?: ((event: import("react-native").GestureResponderEvent) => void) | undefined;
|
|
34
|
+
onResponderMove?: ((event: import("react-native").GestureResponderEvent) => void) | undefined;
|
|
35
|
+
onResponderRelease?: ((event: import("react-native").GestureResponderEvent) => void) | undefined;
|
|
36
|
+
onResponderStart?: ((event: import("react-native").GestureResponderEvent) => void) | undefined;
|
|
37
|
+
onResponderTerminationRequest?: ((event: import("react-native").GestureResponderEvent) => boolean) | undefined;
|
|
38
|
+
onResponderTerminate?: ((event: import("react-native").GestureResponderEvent) => void) | undefined;
|
|
39
|
+
onStartShouldSetResponderCapture?: ((event: import("react-native").GestureResponderEvent) => boolean) | undefined;
|
|
40
|
+
onMoveShouldSetResponderCapture?: ((event: import("react-native").GestureResponderEvent) => boolean) | undefined;
|
|
41
|
+
onTouchStart?: ((event: import("react-native").GestureResponderEvent) => void) | undefined;
|
|
42
|
+
onTouchMove?: ((event: import("react-native").GestureResponderEvent) => void) | undefined;
|
|
43
|
+
onTouchEnd?: ((event: import("react-native").GestureResponderEvent) => void) | undefined;
|
|
44
|
+
onTouchCancel?: ((event: import("react-native").GestureResponderEvent) => void) | undefined;
|
|
45
|
+
onTouchEndCapture?: ((event: import("react-native").GestureResponderEvent) => void) | undefined;
|
|
46
|
+
onPointerEnter?: ((event: import("react-native").PointerEvent) => void) | undefined;
|
|
47
|
+
onPointerEnterCapture?: ((event: import("react-native").PointerEvent) => void) | undefined;
|
|
48
|
+
onPointerLeave?: ((event: import("react-native").PointerEvent) => void) | undefined;
|
|
49
|
+
onPointerLeaveCapture?: ((event: import("react-native").PointerEvent) => void) | undefined;
|
|
50
|
+
onPointerMove?: ((event: import("react-native").PointerEvent) => void) | undefined;
|
|
51
|
+
onPointerMoveCapture?: ((event: import("react-native").PointerEvent) => void) | undefined;
|
|
52
|
+
onPointerCancel?: ((event: import("react-native").PointerEvent) => void) | undefined;
|
|
53
|
+
onPointerCancelCapture?: ((event: import("react-native").PointerEvent) => void) | undefined;
|
|
54
|
+
onPointerDown?: ((event: import("react-native").PointerEvent) => void) | undefined;
|
|
55
|
+
onPointerDownCapture?: ((event: import("react-native").PointerEvent) => void) | undefined;
|
|
56
|
+
onPointerUp?: ((event: import("react-native").PointerEvent) => void) | undefined;
|
|
57
|
+
onPointerUpCapture?: ((event: import("react-native").PointerEvent) => void) | undefined;
|
|
58
|
+
accessible?: boolean | undefined;
|
|
59
|
+
accessibilityActions?: readonly Readonly<{
|
|
60
|
+
name: string;
|
|
61
|
+
label?: string | undefined;
|
|
62
|
+
}>[] | undefined;
|
|
63
|
+
accessibilityLabel?: string | undefined;
|
|
64
|
+
'aria-label'?: string | undefined;
|
|
65
|
+
accessibilityRole?: import("react-native").AccessibilityRole | undefined;
|
|
66
|
+
accessibilityState?: import("react-native").AccessibilityState | undefined;
|
|
67
|
+
'aria-busy'?: boolean | undefined;
|
|
68
|
+
'aria-checked'?: boolean | "mixed" | undefined;
|
|
69
|
+
'aria-disabled'?: boolean | undefined;
|
|
70
|
+
'aria-expanded'?: boolean | undefined;
|
|
71
|
+
'aria-selected'?: boolean | undefined;
|
|
72
|
+
'aria-labelledby'?: string | undefined;
|
|
73
|
+
accessibilityHint?: string | undefined;
|
|
74
|
+
accessibilityValue?: import("react-native").AccessibilityValue | undefined;
|
|
75
|
+
'aria-valuemax'?: number | undefined;
|
|
76
|
+
'aria-valuemin'?: number | undefined;
|
|
77
|
+
'aria-valuenow'?: number | undefined;
|
|
78
|
+
'aria-valuetext'?: string | undefined;
|
|
79
|
+
onAccessibilityAction?: ((event: import("react-native").AccessibilityActionEvent) => void) | undefined;
|
|
80
|
+
importantForAccessibility?: "auto" | "yes" | "no" | "no-hide-descendants" | undefined;
|
|
81
|
+
'aria-hidden'?: boolean | undefined;
|
|
82
|
+
'aria-live'?: "polite" | "assertive" | "off" | undefined;
|
|
83
|
+
'aria-modal'?: boolean | undefined;
|
|
84
|
+
accessibilityLiveRegion?: "none" | "polite" | "assertive" | undefined;
|
|
85
|
+
accessibilityElementsHidden?: boolean | undefined;
|
|
86
|
+
accessibilityViewIsModal?: boolean | undefined;
|
|
87
|
+
onAccessibilityEscape?: (() => void) | undefined;
|
|
88
|
+
onAccessibilityTap?: (() => void) | undefined;
|
|
89
|
+
onMagicTap?: (() => void) | undefined;
|
|
90
|
+
accessibilityIgnoresInvertColors?: boolean | undefined;
|
|
91
|
+
tabIndex?: 0 | -1 | undefined;
|
|
92
|
+
"aria-activedescendant"?: string | undefined;
|
|
93
|
+
"aria-atomic"?: boolean | undefined;
|
|
94
|
+
"aria-autocomplete"?: string | undefined;
|
|
95
|
+
"aria-colcount"?: number | undefined;
|
|
96
|
+
"aria-colindex"?: number | undefined;
|
|
97
|
+
"aria-colspan"?: number | undefined;
|
|
98
|
+
"aria-controls"?: string | undefined;
|
|
99
|
+
"aria-current"?: boolean | "time" | "page" | "step" | "location" | "date" | undefined;
|
|
100
|
+
"aria-describedby"?: string | undefined;
|
|
101
|
+
"aria-details"?: string | undefined;
|
|
102
|
+
"aria-errormessage"?: string | undefined;
|
|
103
|
+
"aria-flowto"?: string | undefined;
|
|
104
|
+
"aria-haspopup"?: string | undefined;
|
|
105
|
+
"aria-invalid"?: boolean | undefined;
|
|
106
|
+
"aria-keyshortcuts"?: string | undefined;
|
|
107
|
+
"aria-level"?: number | undefined;
|
|
108
|
+
"aria-multiline"?: boolean | undefined;
|
|
109
|
+
"aria-multiselectable"?: boolean | undefined;
|
|
110
|
+
"aria-orientation"?: "horizontal" | "vertical" | undefined;
|
|
111
|
+
"aria-owns"?: string | undefined;
|
|
112
|
+
"aria-placeholder"?: string | undefined;
|
|
113
|
+
"aria-posinset"?: number | undefined;
|
|
114
|
+
"aria-pressed"?: boolean | undefined;
|
|
115
|
+
"aria-readonly"?: boolean | undefined;
|
|
116
|
+
"aria-required"?: boolean | undefined;
|
|
117
|
+
"aria-roledescription"?: string | undefined;
|
|
118
|
+
"aria-rowcount"?: number | undefined;
|
|
119
|
+
"aria-rowindex"?: number | undefined;
|
|
120
|
+
"aria-rowspan"?: number | undefined;
|
|
121
|
+
"aria-setsize"?: number | undefined;
|
|
122
|
+
"aria-sort"?: "none" | "ascending" | "descending" | "other" | undefined;
|
|
123
|
+
} & {
|
|
5
124
|
children?: ReactNode;
|
|
6
125
|
onReset?: ((event: React.FormEvent<HTMLElement>) => void) | undefined;
|
|
7
126
|
onSubmit?: ((event: React.FormEvent<HTMLElement>) => void) | undefined;
|