@studio-piot/native-ui 0.16.0 → 0.20.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 +13 -7
- package/dist/components/Choice/Choice.d.ts +6 -4
- package/dist/components/Choice/Choice.d.ts.map +1 -1
- package/dist/components/Choice/Choice.js +11 -5
- package/dist/components/Choice/Choice.js.map +1 -1
- package/dist/components/Choice/choiceLayout.d.ts +16 -0
- package/dist/components/Choice/choiceLayout.d.ts.map +1 -1
- package/dist/components/Choice/choiceLayout.js +20 -1
- package/dist/components/Choice/choiceLayout.js.map +1 -1
- package/dist/components/Choice/choiceThemeRoles.d.ts +1 -0
- package/dist/components/Choice/choiceThemeRoles.d.ts.map +1 -1
- package/dist/components/Choice/choiceThemeRoles.js +1 -0
- package/dist/components/Choice/choiceThemeRoles.js.map +1 -1
- package/dist/components/Choice/types.d.ts +7 -0
- package/dist/components/Choice/types.d.ts.map +1 -1
- package/dist/components/DateTime/DateTimePresenter.android.d.ts +4 -0
- package/dist/components/DateTime/DateTimePresenter.android.d.ts.map +1 -0
- package/dist/components/DateTime/DateTimePresenter.android.js +47 -0
- package/dist/components/DateTime/DateTimePresenter.android.js.map +1 -0
- package/dist/components/DateTime/DateTimePresenter.d.ts +4 -0
- package/dist/components/DateTime/DateTimePresenter.d.ts.map +1 -0
- package/dist/components/DateTime/DateTimePresenter.ios.d.ts +9 -0
- package/dist/components/DateTime/DateTimePresenter.ios.d.ts.map +1 -0
- package/dist/components/DateTime/DateTimePresenter.ios.js +69 -0
- package/dist/components/DateTime/DateTimePresenter.ios.js.map +1 -0
- package/dist/components/DateTime/DateTimePresenter.js +8 -0
- package/dist/components/DateTime/DateTimePresenter.js.map +1 -0
- package/dist/components/DateTime/FieldDateTime.d.ts +8 -0
- package/dist/components/DateTime/FieldDateTime.d.ts.map +1 -0
- package/dist/components/DateTime/FieldDateTime.js +90 -0
- package/dist/components/DateTime/FieldDateTime.js.map +1 -0
- package/dist/components/DateTime/dateTimeThemeRoles.d.ts +13 -0
- package/dist/components/DateTime/dateTimeThemeRoles.d.ts.map +1 -0
- package/dist/components/DateTime/dateTimeThemeRoles.js +15 -0
- package/dist/components/DateTime/dateTimeThemeRoles.js.map +1 -0
- package/dist/components/DateTime/dateTimeValue.d.ts +11 -0
- package/dist/components/DateTime/dateTimeValue.d.ts.map +1 -0
- package/dist/components/DateTime/dateTimeValue.js +40 -0
- package/dist/components/DateTime/dateTimeValue.js.map +1 -0
- package/dist/components/DateTime/index.d.ts +5 -0
- package/dist/components/DateTime/index.d.ts.map +1 -0
- package/dist/components/DateTime/index.js +10 -0
- package/dist/components/DateTime/index.js.map +1 -0
- package/dist/components/DateTime/types.d.ts +57 -0
- package/dist/components/DateTime/types.d.ts.map +1 -0
- package/dist/components/DateTime/types.js +3 -0
- package/dist/components/DateTime/types.js.map +1 -0
- package/dist/components/Select/Select.d.ts.map +1 -1
- package/dist/components/Select/Select.js +2 -9
- package/dist/components/Select/Select.js.map +1 -1
- package/dist/components/Tabs/Tabs.d.ts +8 -0
- package/dist/components/Tabs/Tabs.d.ts.map +1 -0
- package/dist/components/Tabs/Tabs.js +124 -0
- package/dist/components/Tabs/Tabs.js.map +1 -0
- package/dist/components/Tabs/index.d.ts +4 -0
- package/dist/components/Tabs/index.d.ts.map +1 -0
- package/dist/components/Tabs/index.js +8 -0
- package/dist/components/Tabs/index.js.map +1 -0
- package/dist/components/Tabs/tabsLayout.d.ts +13 -0
- package/dist/components/Tabs/tabsLayout.d.ts.map +1 -0
- package/dist/components/Tabs/tabsLayout.js +20 -0
- package/dist/components/Tabs/tabsLayout.js.map +1 -0
- package/dist/components/Tabs/tabsThemeRoles.d.ts +8 -0
- package/dist/components/Tabs/tabsThemeRoles.d.ts.map +1 -0
- package/dist/components/Tabs/tabsThemeRoles.js +10 -0
- package/dist/components/Tabs/tabsThemeRoles.js.map +1 -0
- package/dist/components/Tabs/types.d.ts +30 -0
- package/dist/components/Tabs/types.d.ts.map +1 -0
- package/dist/components/Tabs/types.js +3 -0
- package/dist/components/Tabs/types.js.map +1 -0
- package/dist/components/index.d.ts +1 -0
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/index.js +5 -2
- package/dist/components/index.js.map +1 -1
- package/dist/date-time.d.ts +6 -0
- package/dist/date-time.d.ts.map +1 -0
- package/dist/date-time.js +22 -0
- package/dist/date-time.js.map +1 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -2
- package/dist/index.js.map +1 -1
- package/dist/layout/SheetContainer.d.ts +6 -0
- package/dist/layout/SheetContainer.d.ts.map +1 -0
- package/dist/layout/SheetContainer.js +14 -0
- package/dist/layout/SheetContainer.js.map +1 -0
- package/docs/components/choice.md +26 -3
- package/docs/components/field-date-time.md +89 -0
- package/docs/components/tabs.md +85 -0
- package/package.json +16 -2
- package/src/components/Choice/Choice.tsx +17 -6
- package/src/components/Choice/choiceLayout.test.ts +36 -1
- package/src/components/Choice/choiceLayout.ts +25 -0
- package/src/components/Choice/choiceThemeRoles.test.ts +1 -0
- package/src/components/Choice/choiceThemeRoles.ts +1 -0
- package/src/components/Choice/types.ts +7 -0
- package/src/components/DateTime/DateTimePresenter.android.tsx +76 -0
- package/src/components/DateTime/DateTimePresenter.ios.tsx +122 -0
- package/src/components/DateTime/DateTimePresenter.tsx +6 -0
- package/src/components/DateTime/FieldDateTime.tsx +164 -0
- package/src/components/DateTime/dateTimeThemeRoles.test.ts +18 -0
- package/src/components/DateTime/dateTimeThemeRoles.ts +13 -0
- package/src/components/DateTime/dateTimeValue.test.ts +49 -0
- package/src/components/DateTime/dateTimeValue.ts +36 -0
- package/src/components/DateTime/index.ts +4 -0
- package/src/components/DateTime/types.ts +58 -0
- package/src/components/Select/Select.tsx +1 -9
- package/src/components/Tabs/Tabs.tsx +148 -0
- package/src/components/Tabs/index.ts +3 -0
- package/src/components/Tabs/tabsLayout.test.ts +36 -0
- package/src/components/Tabs/tabsLayout.ts +23 -0
- package/src/components/Tabs/tabsThemeRoles.test.ts +14 -0
- package/src/components/Tabs/tabsThemeRoles.ts +8 -0
- package/src/components/Tabs/types.ts +31 -0
- package/src/components/index.ts +7 -0
- package/src/date-time.ts +5 -0
- package/src/index.ts +8 -1
- package/src/layout/SheetContainer.tsx +11 -0
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { useEffect, useRef } from "react";
|
|
2
|
+
import { Pressable, StyleSheet, Text, View } from "react-native";
|
|
3
|
+
import Animated, {
|
|
4
|
+
Easing,
|
|
5
|
+
useAnimatedStyle,
|
|
6
|
+
useSharedValue,
|
|
7
|
+
withTiming,
|
|
8
|
+
} from "react-native-reanimated";
|
|
9
|
+
import { useResolvedFieldColor } from "../Field/useResolvedFieldColor";
|
|
10
|
+
import { resolveTabIndex, tabIndicatorFrame } from "./tabsLayout";
|
|
11
|
+
import { TABS_THEME_ROLES } from "./tabsThemeRoles";
|
|
12
|
+
import type { TabsProps } from "./types";
|
|
13
|
+
|
|
14
|
+
const SLIDE_MS = 300;
|
|
15
|
+
const ICON_SIZE = 17;
|
|
16
|
+
const INDICATOR_HEIGHT = 2;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Underline tabs that switch between views of one screen, e.g. Planned /
|
|
20
|
+
* Completed. Equal-width labels on a hairline, with an underline that slides
|
|
21
|
+
* to the active tab. Controlled: `value` + `onChange`.
|
|
22
|
+
*/
|
|
23
|
+
export function Tabs<T extends string = string>({
|
|
24
|
+
value,
|
|
25
|
+
onChange,
|
|
26
|
+
tabs,
|
|
27
|
+
inset = 0,
|
|
28
|
+
borderColor: borderColorProp,
|
|
29
|
+
labelColor: labelColorProp,
|
|
30
|
+
activeLabelColor: activeLabelColorProp,
|
|
31
|
+
indicatorColor: indicatorColorProp,
|
|
32
|
+
style,
|
|
33
|
+
}: TabsProps<T>) {
|
|
34
|
+
const borderColor = useResolvedFieldColor(borderColorProp, TABS_THEME_ROLES.borderColor);
|
|
35
|
+
const labelColor = useResolvedFieldColor(labelColorProp, TABS_THEME_ROLES.labelColor);
|
|
36
|
+
const activeLabelColor = useResolvedFieldColor(
|
|
37
|
+
activeLabelColorProp,
|
|
38
|
+
TABS_THEME_ROLES.activeLabelColor,
|
|
39
|
+
);
|
|
40
|
+
const indicatorColor = useResolvedFieldColor(
|
|
41
|
+
indicatorColorProp,
|
|
42
|
+
TABS_THEME_ROLES.indicatorColor,
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
const index = resolveTabIndex(value, tabs);
|
|
46
|
+
const width = useRef(0);
|
|
47
|
+
const laidOut = useRef(false);
|
|
48
|
+
const xSV = useSharedValue(0);
|
|
49
|
+
const widthSV = useSharedValue(0);
|
|
50
|
+
const opacitySV = useSharedValue(0);
|
|
51
|
+
|
|
52
|
+
const place = (animated: boolean) => {
|
|
53
|
+
const frame = tabIndicatorFrame(index, tabs.length, width.current, inset);
|
|
54
|
+
if (!frame) {
|
|
55
|
+
opacitySV.value = withTiming(0, { duration: 160 });
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
widthSV.value = frame.width;
|
|
59
|
+
opacitySV.value = 1;
|
|
60
|
+
xSV.value = animated
|
|
61
|
+
? withTiming(frame.x, { duration: SLIDE_MS, easing: Easing.out(Easing.cubic) })
|
|
62
|
+
: frame.x;
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
useEffect(() => {
|
|
66
|
+
// place() reads the latest index and width.
|
|
67
|
+
if (laidOut.current) place(true);
|
|
68
|
+
}, [index, tabs.length, inset]);
|
|
69
|
+
|
|
70
|
+
const indicatorStyle = useAnimatedStyle(() => ({
|
|
71
|
+
width: widthSV.value,
|
|
72
|
+
opacity: opacitySV.value,
|
|
73
|
+
transform: [{ translateX: xSV.value }],
|
|
74
|
+
}));
|
|
75
|
+
|
|
76
|
+
if (tabs.length === 0) return null;
|
|
77
|
+
|
|
78
|
+
return (
|
|
79
|
+
<View
|
|
80
|
+
accessibilityRole="tablist"
|
|
81
|
+
onLayout={(event) => {
|
|
82
|
+
width.current = event.nativeEvent.layout.width;
|
|
83
|
+
place(laidOut.current);
|
|
84
|
+
laidOut.current = true;
|
|
85
|
+
}}
|
|
86
|
+
style={[styles.row, { borderBottomColor: borderColor, paddingHorizontal: inset }, style]}
|
|
87
|
+
>
|
|
88
|
+
{tabs.map((tab, i) => {
|
|
89
|
+
const active = i === index;
|
|
90
|
+
const color = active ? activeLabelColor : labelColor;
|
|
91
|
+
return (
|
|
92
|
+
<Pressable
|
|
93
|
+
key={tab.value}
|
|
94
|
+
accessibilityRole="tab"
|
|
95
|
+
accessibilityState={{ selected: active, disabled: tab.disabled }}
|
|
96
|
+
disabled={tab.disabled}
|
|
97
|
+
onPress={() => onChange(tab.value)}
|
|
98
|
+
style={({ pressed }) => [
|
|
99
|
+
styles.tab,
|
|
100
|
+
{ opacity: tab.disabled ? 0.4 : pressed ? 0.6 : 1 },
|
|
101
|
+
]}
|
|
102
|
+
>
|
|
103
|
+
{tab.icon ? (
|
|
104
|
+
<View
|
|
105
|
+
accessibilityElementsHidden
|
|
106
|
+
importantForAccessibility="no-hide-descendants"
|
|
107
|
+
>
|
|
108
|
+
{tab.icon({ color, size: ICON_SIZE })}
|
|
109
|
+
</View>
|
|
110
|
+
) : null}
|
|
111
|
+
<Text numberOfLines={1} style={[styles.label, { color }]}>
|
|
112
|
+
{tab.label}
|
|
113
|
+
</Text>
|
|
114
|
+
</Pressable>
|
|
115
|
+
);
|
|
116
|
+
})}
|
|
117
|
+
<Animated.View
|
|
118
|
+
pointerEvents="none"
|
|
119
|
+
style={[styles.indicator, { backgroundColor: indicatorColor }, indicatorStyle]}
|
|
120
|
+
/>
|
|
121
|
+
</View>
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const styles = StyleSheet.create({
|
|
126
|
+
row: {
|
|
127
|
+
borderBottomWidth: StyleSheet.hairlineWidth,
|
|
128
|
+
flexDirection: "row",
|
|
129
|
+
},
|
|
130
|
+
tab: {
|
|
131
|
+
alignItems: "center",
|
|
132
|
+
flex: 1,
|
|
133
|
+
flexDirection: "row",
|
|
134
|
+
gap: 6,
|
|
135
|
+
justifyContent: "center",
|
|
136
|
+
paddingVertical: 12,
|
|
137
|
+
},
|
|
138
|
+
label: {
|
|
139
|
+
fontSize: 15,
|
|
140
|
+
fontWeight: "600",
|
|
141
|
+
},
|
|
142
|
+
indicator: {
|
|
143
|
+
bottom: 0,
|
|
144
|
+
height: INDICATOR_HEIGHT,
|
|
145
|
+
left: 0,
|
|
146
|
+
position: "absolute",
|
|
147
|
+
},
|
|
148
|
+
});
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { describe, it } from "node:test";
|
|
3
|
+
import { resolveTabIndex, tabIndicatorFrame } from "./tabsLayout.ts";
|
|
4
|
+
|
|
5
|
+
describe("tabIndicatorFrame", () => {
|
|
6
|
+
it("splits the width evenly and sits under the active tab", () => {
|
|
7
|
+
assert.deepEqual(tabIndicatorFrame(0, 2, 300), { x: 0, width: 150 });
|
|
8
|
+
assert.deepEqual(tabIndicatorFrame(1, 2, 300), { x: 150, width: 150 });
|
|
9
|
+
assert.deepEqual(tabIndicatorFrame(2, 3, 300), { x: 200, width: 100 });
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it("keeps tabs inside the inset while the row spans the full width", () => {
|
|
13
|
+
// 360 wide with 16 either side: two tabs of 164.
|
|
14
|
+
assert.deepEqual(tabIndicatorFrame(0, 2, 360, 16), { x: 16, width: 164 });
|
|
15
|
+
assert.deepEqual(tabIndicatorFrame(1, 2, 360, 16), { x: 180, width: 164 });
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it("is hidden with no tabs, no width or no active tab", () => {
|
|
19
|
+
assert.equal(tabIndicatorFrame(0, 0, 300), null);
|
|
20
|
+
assert.equal(tabIndicatorFrame(0, 2, 0), null);
|
|
21
|
+
assert.equal(tabIndicatorFrame(0, 2, 30, 16), null);
|
|
22
|
+
assert.equal(tabIndicatorFrame(-1, 2, 300), null);
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
describe("resolveTabIndex", () => {
|
|
27
|
+
const tabs = [{ value: "planned" }, { value: "completed" }];
|
|
28
|
+
|
|
29
|
+
it("finds the active tab", () => {
|
|
30
|
+
assert.equal(resolveTabIndex("completed", tabs), 1);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("is -1 for an unknown value", () => {
|
|
34
|
+
assert.equal(resolveTabIndex("archived", tabs), -1);
|
|
35
|
+
});
|
|
36
|
+
});
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where the active tab's indicator sits: an equal share of the width inside
|
|
3
|
+
* `inset` on each side (the row and its hairline still span the full width).
|
|
4
|
+
* Pure so it can be unit-tested.
|
|
5
|
+
*/
|
|
6
|
+
export function tabIndicatorFrame(
|
|
7
|
+
index: number,
|
|
8
|
+
count: number,
|
|
9
|
+
width: number,
|
|
10
|
+
inset = 0,
|
|
11
|
+
): { x: number; width: number } | null {
|
|
12
|
+
const inner = width - inset * 2;
|
|
13
|
+
if (count <= 0 || inner <= 0 || index < 0) return null;
|
|
14
|
+
const tabWidth = inner / count;
|
|
15
|
+
return { x: inset + index * tabWidth, width: tabWidth };
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function resolveTabIndex<T extends string>(
|
|
19
|
+
value: T,
|
|
20
|
+
tabs: readonly { value: T }[],
|
|
21
|
+
): number {
|
|
22
|
+
return tabs.findIndex((tab) => tab.value === value);
|
|
23
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { describe, it } from "node:test";
|
|
3
|
+
import { TABS_THEME_ROLES } from "./tabsThemeRoles.ts";
|
|
4
|
+
|
|
5
|
+
describe("TABS_THEME_ROLES", () => {
|
|
6
|
+
it("maps colour props to tabs-prefixed roles", () => {
|
|
7
|
+
assert.deepEqual(TABS_THEME_ROLES, {
|
|
8
|
+
borderColor: "tabsBorder",
|
|
9
|
+
labelColor: "tabsLabel",
|
|
10
|
+
activeLabelColor: "tabsActiveLabel",
|
|
11
|
+
indicatorColor: "tabsIndicator",
|
|
12
|
+
});
|
|
13
|
+
});
|
|
14
|
+
});
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { StyleProp, ViewStyle } from "react-native";
|
|
2
|
+
import type { ButtonIcon } from "../Button";
|
|
3
|
+
|
|
4
|
+
export type TabItem<T extends string = string> = {
|
|
5
|
+
value: T;
|
|
6
|
+
label: string;
|
|
7
|
+
/** Left of the label, in the label's colour: `({ color, size }) => ReactNode`. */
|
|
8
|
+
icon?: ButtonIcon;
|
|
9
|
+
disabled?: boolean;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export type TabsProps<T extends string = string> = {
|
|
13
|
+
value: T;
|
|
14
|
+
onChange: (value: T) => void;
|
|
15
|
+
tabs: readonly TabItem<T>[];
|
|
16
|
+
/**
|
|
17
|
+
* Space on each side between the row's edges and the tabs. The hairline
|
|
18
|
+
* still spans the full width, so a full-bleed row can line its tabs up
|
|
19
|
+
* with the page content. Default 0.
|
|
20
|
+
*/
|
|
21
|
+
inset?: number;
|
|
22
|
+
/** Line under all tabs. Defaults to theme role `tabsBorder`. */
|
|
23
|
+
borderColor?: string;
|
|
24
|
+
/** Inactive label. Defaults to theme role `tabsLabel`. */
|
|
25
|
+
labelColor?: string;
|
|
26
|
+
/** Active label. Defaults to theme role `tabsActiveLabel`. */
|
|
27
|
+
activeLabelColor?: string;
|
|
28
|
+
/** Sliding underline. Defaults to theme role `tabsIndicator`. */
|
|
29
|
+
indicatorColor?: string;
|
|
30
|
+
style?: StyleProp<ViewStyle>;
|
|
31
|
+
};
|
package/src/components/index.ts
CHANGED
|
@@ -120,6 +120,13 @@ export {
|
|
|
120
120
|
type TabAccessoryThemeColorProp,
|
|
121
121
|
type TabAccessoryVariant,
|
|
122
122
|
} from "./TabAccessoryButton";
|
|
123
|
+
export {
|
|
124
|
+
Tabs,
|
|
125
|
+
TABS_THEME_ROLES,
|
|
126
|
+
type TabItem,
|
|
127
|
+
type TabsProps,
|
|
128
|
+
type TabsThemeColorProp,
|
|
129
|
+
} from "./Tabs";
|
|
123
130
|
export {
|
|
124
131
|
Page,
|
|
125
132
|
PageBleed,
|
package/src/date-time.ts
ADDED
package/src/index.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export const version = "0.
|
|
1
|
+
export const version = "0.20.0";
|
|
2
2
|
|
|
3
3
|
export * from "./theme";
|
|
4
4
|
export { SHEET_TOP_RADIUS, screenPaddingHorizontal } from "./layout";
|
|
@@ -124,6 +124,13 @@ export {
|
|
|
124
124
|
type TabAccessoryThemeColorProp,
|
|
125
125
|
type TabAccessoryVariant,
|
|
126
126
|
} from "./components/TabAccessoryButton";
|
|
127
|
+
export {
|
|
128
|
+
Tabs,
|
|
129
|
+
TABS_THEME_ROLES,
|
|
130
|
+
type TabItem,
|
|
131
|
+
type TabsProps,
|
|
132
|
+
type TabsThemeColorProp,
|
|
133
|
+
} from "./components/Tabs";
|
|
127
134
|
export {
|
|
128
135
|
Page,
|
|
129
136
|
PageBleed,
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import { Platform } from "react-native";
|
|
3
|
+
import { FullWindowOverlay } from "react-native-screens";
|
|
4
|
+
|
|
5
|
+
/** Renders a bottom sheet above React Navigation native modals (iOS). */
|
|
6
|
+
export function SheetContainer({ children }: { children?: ReactNode }) {
|
|
7
|
+
if (Platform.OS === "ios") {
|
|
8
|
+
return <FullWindowOverlay>{children}</FullWindowOverlay>;
|
|
9
|
+
}
|
|
10
|
+
return <>{children}</>;
|
|
11
|
+
}
|