@utilitywarehouse/hearth-react-native 0.26.0 → 0.27.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/.turbo/turbo-build.log +1 -1
- package/.turbo/turbo-lint.log +13 -13
- package/CHANGELOG.md +40 -0
- package/build/components/Banner/Banner.js +12 -1
- package/build/components/PillGroup/Pill.js +0 -1
- package/build/components/PillGroup/PillGroup.js +4 -1
- package/build/components/SegmentedControl/SegmentedControl.context.d.ts +14 -0
- package/build/components/SegmentedControl/SegmentedControl.context.js +9 -0
- package/build/components/SegmentedControl/SegmentedControl.d.ts +6 -0
- package/build/components/SegmentedControl/SegmentedControl.js +196 -0
- package/build/components/SegmentedControl/SegmentedControl.props.d.ts +18 -0
- package/build/components/SegmentedControl/SegmentedControl.props.js +1 -0
- package/build/components/SegmentedControl/SegmentedControlOption.d.ts +18 -0
- package/build/components/SegmentedControl/SegmentedControlOption.js +122 -0
- package/build/components/SegmentedControl/SegmentedControlOption.props.d.ts +12 -0
- package/build/components/SegmentedControl/SegmentedControlOption.props.js +1 -0
- package/build/components/SegmentedControl/index.d.ts +4 -0
- package/build/components/SegmentedControl/index.js +2 -0
- package/build/components/index.d.ts +1 -0
- package/build/components/index.js +1 -0
- package/docs/changelog.mdx +136 -0
- package/docs/components/AllComponents.web.tsx +14 -0
- package/package.json +4 -4
- package/src/components/Banner/Banner.tsx +12 -1
- package/src/components/PillGroup/Pill.tsx +0 -1
- package/src/components/PillGroup/PillGroup.tsx +4 -0
- package/src/components/SegmentedControl/SegmentedControl.context.ts +22 -0
- package/src/components/SegmentedControl/SegmentedControl.docs.mdx +90 -0
- package/src/components/SegmentedControl/SegmentedControl.figma.tsx +40 -0
- package/src/components/SegmentedControl/SegmentedControl.props.ts +20 -0
- package/src/components/SegmentedControl/SegmentedControl.stories.tsx +77 -0
- package/src/components/SegmentedControl/SegmentedControl.tsx +257 -0
- package/src/components/SegmentedControl/SegmentedControlOption.props.ts +14 -0
- package/src/components/SegmentedControl/SegmentedControlOption.tsx +213 -0
- package/src/components/SegmentedControl/index.ts +4 -0
- package/src/components/index.ts +1 -0
package/.turbo/turbo-build.log
CHANGED
package/.turbo/turbo-lint.log
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
|
|
2
|
-
> @utilitywarehouse/hearth-react-native@0.
|
|
2
|
+
> @utilitywarehouse/hearth-react-native@0.27.0 lint /home/runner/work/hearth/hearth/packages/react-native
|
|
3
3
|
> TIMING=1 eslint .
|
|
4
4
|
|
|
5
5
|
|
|
@@ -58,15 +58,15 @@
|
|
|
58
58
|
|
|
59
59
|
✖ 25 problems (0 errors, 25 warnings)
|
|
60
60
|
|
|
61
|
-
Rule
|
|
62
|
-
|
|
63
|
-
@typescript-eslint/no-unused-vars
|
|
64
|
-
react-hooks/exhaustive-deps
|
|
65
|
-
no-global-assign
|
|
66
|
-
react-hooks/rules-of-hooks
|
|
67
|
-
|
|
68
|
-
no-misleading-character-class
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
no-regex-spaces
|
|
72
|
-
no-
|
|
61
|
+
Rule | Time (ms) | Relative
|
|
62
|
+
:---------------------------------|----------:|--------:
|
|
63
|
+
@typescript-eslint/no-unused-vars | 1531.354 | 62.0%
|
|
64
|
+
react-hooks/exhaustive-deps | 115.862 | 4.7%
|
|
65
|
+
no-global-assign | 82.745 | 3.3%
|
|
66
|
+
react-hooks/rules-of-hooks | 81.422 | 3.3%
|
|
67
|
+
@typescript-eslint/ban-ts-comment | 54.976 | 2.2%
|
|
68
|
+
no-misleading-character-class | 43.812 | 1.8%
|
|
69
|
+
no-unexpected-multiline | 39.804 | 1.6%
|
|
70
|
+
no-fallthrough | 33.437 | 1.4%
|
|
71
|
+
no-regex-spaces | 28.539 | 1.2%
|
|
72
|
+
no-shadow-restricted-names | 25.451 | 1.0%
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,45 @@
|
|
|
1
1
|
# @utilitywarehouse/hearth-react-native
|
|
2
2
|
|
|
3
|
+
## 0.27.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- [#987](https://github.com/utilitywarehouse/hearth/pull/987) [`eb962d2`](https://github.com/utilitywarehouse/hearth/commit/eb962d2f33b63fa3aeda0b291fd41ace90d04c41) Thanks [@jordmccord](https://github.com/jordmccord)! - 🌟 [FEATURE]: Add `SegmentedControl` and `SegmentedControlOption` components.
|
|
8
|
+
|
|
9
|
+
This introduces a new segmented control component for switching between a small set of related options.
|
|
10
|
+
The component includes controlled and uncontrolled usage, size variants (`sm`, `md`), animated selected indicator movement, and improved accessibility semantics for screen readers.
|
|
11
|
+
|
|
12
|
+
**Components affected**:
|
|
13
|
+
- `SegmentedControl`
|
|
14
|
+
- `SegmentedControlOption`
|
|
15
|
+
|
|
16
|
+
**Developer changes**:
|
|
17
|
+
|
|
18
|
+
Import and compose the new components as follows:
|
|
19
|
+
|
|
20
|
+
```tsx
|
|
21
|
+
import { SegmentedControl, SegmentedControlOption } from '@utilitywarehouse/hearth-react-native';
|
|
22
|
+
|
|
23
|
+
<SegmentedControl defaultValue="day" size="sm">
|
|
24
|
+
<SegmentedControlOption value="day">Day</SegmentedControlOption>
|
|
25
|
+
<SegmentedControlOption value="week">Week</SegmentedControlOption>
|
|
26
|
+
<SegmentedControlOption value="month">Month</SegmentedControlOption>
|
|
27
|
+
</SegmentedControl>;
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
### Patch Changes
|
|
31
|
+
|
|
32
|
+
- [#989](https://github.com/utilitywarehouse/hearth/pull/989) [`c97122e`](https://github.com/utilitywarehouse/hearth/commit/c97122eb429ec4adef656fb245a9256a5619df61) Thanks [@jordmccord](https://github.com/jordmccord)! - 🐛 [FIX]: Ensure horizontal `Banner` fills available width when `onPress` is not provided.
|
|
33
|
+
|
|
34
|
+
Fixed a layout issue where a horizontal `Banner` without `onPress` could fail to stretch correctly within its parent container.
|
|
35
|
+
|
|
36
|
+
**Components affected**:
|
|
37
|
+
- `Banner`
|
|
38
|
+
|
|
39
|
+
**Developer changes**:
|
|
40
|
+
|
|
41
|
+
No changes required.
|
|
42
|
+
|
|
3
43
|
## 0.26.0
|
|
4
44
|
|
|
5
45
|
### Minor Changes
|
|
@@ -11,7 +11,7 @@ import { UnstyledIconButton } from '../UnstyledIconButton';
|
|
|
11
11
|
import BannerContext from './Banner.context';
|
|
12
12
|
const Banner = ({ icon, iconContainerVariant = 'subtle', iconContainerSize = 'md', iconContainerColor = 'pig', illustration, image, heading, description, direction = 'horizontal', link, button, onPress, onClose, variant = 'subtle', style, ...props }) => {
|
|
13
13
|
const hasIllustration = Boolean(illustration);
|
|
14
|
-
styles.useVariants({ direction, hasIllustration });
|
|
14
|
+
styles.useVariants({ direction, hasIllustration, isPressable: Boolean(onPress) });
|
|
15
15
|
const context = useMemo(() => ({
|
|
16
16
|
direction,
|
|
17
17
|
}), [direction]);
|
|
@@ -66,6 +66,10 @@ const styles = StyleSheet.create(theme => ({
|
|
|
66
66
|
true: {},
|
|
67
67
|
false: {},
|
|
68
68
|
},
|
|
69
|
+
isPressable: {
|
|
70
|
+
true: {},
|
|
71
|
+
false: {},
|
|
72
|
+
},
|
|
69
73
|
},
|
|
70
74
|
compoundVariants: [
|
|
71
75
|
{
|
|
@@ -82,6 +86,13 @@ const styles = StyleSheet.create(theme => ({
|
|
|
82
86
|
alignItems: 'center',
|
|
83
87
|
},
|
|
84
88
|
},
|
|
89
|
+
{
|
|
90
|
+
direction: 'horizontal',
|
|
91
|
+
isPressable: false,
|
|
92
|
+
styles: {
|
|
93
|
+
flex: 1,
|
|
94
|
+
},
|
|
95
|
+
},
|
|
85
96
|
],
|
|
86
97
|
},
|
|
87
98
|
media: {
|
|
@@ -20,10 +20,13 @@ export const PillGroup = ({ children, value, multiple = false, wrap = true, onCh
|
|
|
20
20
|
}
|
|
21
21
|
},
|
|
22
22
|
}), [normalizedValue, multiple, onChange]);
|
|
23
|
-
return (_jsx(PillGroupContext.Provider, { value: contextValue, children: wrap ? (_jsx(Box, { style: [styles.group, styles.wrap, style], ...props, children: children })) : (_jsx(ScrollView, { horizontal: true, contentContainerStyle: [styles.group, style], showsHorizontalScrollIndicator: false, ...props, children: children })) }));
|
|
23
|
+
return (_jsx(PillGroupContext.Provider, { value: contextValue, children: wrap ? (_jsx(Box, { style: [styles.group, styles.wrap, style], ...props, children: children })) : (_jsx(ScrollView, { horizontal: true, style: styles.scrollView, contentContainerStyle: [styles.group, style], showsHorizontalScrollIndicator: false, ...props, children: children })) }));
|
|
24
24
|
};
|
|
25
25
|
PillGroup.displayName = 'PillGroup';
|
|
26
26
|
const styles = StyleSheet.create(theme => ({
|
|
27
|
+
scrollView: {
|
|
28
|
+
flexGrow: 0,
|
|
29
|
+
},
|
|
27
30
|
group: {
|
|
28
31
|
flexDirection: 'row',
|
|
29
32
|
gap: theme.components.pill.group.gap,
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export type SegmentedControlContextValue = {
|
|
2
|
+
value?: string;
|
|
3
|
+
select: (value: string) => void;
|
|
4
|
+
disabled?: boolean;
|
|
5
|
+
size: 'sm' | 'md';
|
|
6
|
+
registerOptionLayout: (value: string, layout: {
|
|
7
|
+
x: number;
|
|
8
|
+
y: number;
|
|
9
|
+
width: number;
|
|
10
|
+
height: number;
|
|
11
|
+
}) => void;
|
|
12
|
+
};
|
|
13
|
+
export declare const SegmentedControlContext: import("react").Context<SegmentedControlContextValue | null>;
|
|
14
|
+
export declare const useSegmentedControlContext: () => SegmentedControlContextValue;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { createContext, useContext } from 'react';
|
|
2
|
+
export const SegmentedControlContext = createContext(null);
|
|
3
|
+
export const useSegmentedControlContext = () => {
|
|
4
|
+
const context = useContext(SegmentedControlContext);
|
|
5
|
+
if (!context) {
|
|
6
|
+
throw new Error('SegmentedControlOption must be used within SegmentedControl');
|
|
7
|
+
}
|
|
8
|
+
return context;
|
|
9
|
+
};
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type SegmentedControlProps from './SegmentedControl.props';
|
|
2
|
+
declare const SegmentedControl: {
|
|
3
|
+
({ value: controlledValue, defaultValue, onValueChange, size, disabled, children, style, ...props }: SegmentedControlProps): import("react/jsx-runtime").JSX.Element;
|
|
4
|
+
displayName: string;
|
|
5
|
+
};
|
|
6
|
+
export default SegmentedControl;
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Children, isValidElement, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
3
|
+
import { View } from 'react-native';
|
|
4
|
+
import Animated, { Easing, useAnimatedStyle, useReducedMotion, useSharedValue, withTiming, } from 'react-native-reanimated';
|
|
5
|
+
import { StyleSheet } from 'react-native-unistyles';
|
|
6
|
+
import { useStyleProps } from '../../hooks';
|
|
7
|
+
import { SegmentedControlContext } from './SegmentedControl.context';
|
|
8
|
+
const Indicator = Animated.createAnimatedComponent(View);
|
|
9
|
+
const GROUP_BORDER_WIDTH = 1;
|
|
10
|
+
const SegmentedControl = ({ value: controlledValue, defaultValue, onValueChange, size = 'sm', disabled = false, children, style, ...props }) => {
|
|
11
|
+
const { computedStyles, remainingProps } = useStyleProps(props);
|
|
12
|
+
const isReducedMotion = useReducedMotion();
|
|
13
|
+
const indicatorPositionOffset = GROUP_BORDER_WIDTH;
|
|
14
|
+
const optionValues = useMemo(() => {
|
|
15
|
+
const values = [];
|
|
16
|
+
const walk = (node) => {
|
|
17
|
+
Children.forEach(node, child => {
|
|
18
|
+
if (!isValidElement(child))
|
|
19
|
+
return;
|
|
20
|
+
const childType = child.type;
|
|
21
|
+
const childProps = child.props;
|
|
22
|
+
if (childType?.displayName === 'SegmentedControlOption' &&
|
|
23
|
+
typeof childProps?.value === 'string') {
|
|
24
|
+
values.push(childProps.value);
|
|
25
|
+
}
|
|
26
|
+
if (childProps?.children) {
|
|
27
|
+
walk(childProps.children);
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
};
|
|
31
|
+
walk(children);
|
|
32
|
+
return values;
|
|
33
|
+
}, [children]);
|
|
34
|
+
const optionValuesKey = useMemo(() => optionValues.join('|'), [optionValues]);
|
|
35
|
+
const optionValuesRef = useRef(optionValues);
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
optionValuesRef.current = optionValues;
|
|
38
|
+
}, [optionValues]);
|
|
39
|
+
const getInitialValue = () => {
|
|
40
|
+
if (controlledValue !== undefined)
|
|
41
|
+
return controlledValue;
|
|
42
|
+
if (defaultValue !== undefined)
|
|
43
|
+
return defaultValue;
|
|
44
|
+
return optionValues[0];
|
|
45
|
+
};
|
|
46
|
+
const [uncontrolledValue, setUncontrolledValue] = useState(getInitialValue);
|
|
47
|
+
useEffect(() => {
|
|
48
|
+
if (controlledValue !== undefined) {
|
|
49
|
+
setUncontrolledValue(controlledValue);
|
|
50
|
+
}
|
|
51
|
+
}, [controlledValue]);
|
|
52
|
+
useEffect(() => {
|
|
53
|
+
const currentOptionValues = optionValuesRef.current;
|
|
54
|
+
setUncontrolledValue(prev => {
|
|
55
|
+
if (!prev)
|
|
56
|
+
return currentOptionValues[0];
|
|
57
|
+
if (!currentOptionValues.includes(prev))
|
|
58
|
+
return currentOptionValues[0];
|
|
59
|
+
return prev;
|
|
60
|
+
});
|
|
61
|
+
}, [optionValuesKey]);
|
|
62
|
+
const currentValue = controlledValue !== undefined ? controlledValue : uncontrolledValue;
|
|
63
|
+
const indicatorX = useSharedValue(0);
|
|
64
|
+
const indicatorWidth = useSharedValue(0);
|
|
65
|
+
const indicatorY = useSharedValue(0);
|
|
66
|
+
const indicatorHeight = useSharedValue(0);
|
|
67
|
+
const [hasIndicator, setHasIndicator] = useState(false);
|
|
68
|
+
const layoutsRef = useRef(new Map());
|
|
69
|
+
const prevValueRef = useRef(undefined);
|
|
70
|
+
const initialisedRef = useRef(false);
|
|
71
|
+
const select = useCallback((nextValue) => {
|
|
72
|
+
if (disabled)
|
|
73
|
+
return;
|
|
74
|
+
if (controlledValue === undefined) {
|
|
75
|
+
setUncontrolledValue(nextValue);
|
|
76
|
+
}
|
|
77
|
+
onValueChange?.(nextValue);
|
|
78
|
+
}, [controlledValue, disabled, onValueChange]);
|
|
79
|
+
const registerOptionLayout = useCallback((value, layout) => {
|
|
80
|
+
layoutsRef.current.set(value, layout);
|
|
81
|
+
const activeValue = controlledValue !== undefined ? controlledValue : uncontrolledValue;
|
|
82
|
+
if (!activeValue || activeValue !== value)
|
|
83
|
+
return;
|
|
84
|
+
if (!initialisedRef.current) {
|
|
85
|
+
indicatorX.value = Math.max(0, layout.x - indicatorPositionOffset);
|
|
86
|
+
indicatorWidth.value = layout.width;
|
|
87
|
+
indicatorY.value = Math.max(0, layout.y - indicatorPositionOffset);
|
|
88
|
+
indicatorHeight.value = layout.height;
|
|
89
|
+
prevValueRef.current = activeValue;
|
|
90
|
+
initialisedRef.current = true;
|
|
91
|
+
setHasIndicator(true);
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
if (prevValueRef.current === activeValue)
|
|
95
|
+
return;
|
|
96
|
+
const config = {
|
|
97
|
+
delay: 200,
|
|
98
|
+
duration: isReducedMotion ? 0 : 220,
|
|
99
|
+
easing: Easing.out(Easing.cubic),
|
|
100
|
+
};
|
|
101
|
+
indicatorX.value = withTiming(Math.max(0, layout.x - indicatorPositionOffset), config);
|
|
102
|
+
indicatorWidth.value = withTiming(layout.width, config);
|
|
103
|
+
indicatorY.value = withTiming(Math.max(0, layout.y - indicatorPositionOffset), config);
|
|
104
|
+
indicatorHeight.value = withTiming(layout.height, config);
|
|
105
|
+
prevValueRef.current = activeValue;
|
|
106
|
+
}, [
|
|
107
|
+
controlledValue,
|
|
108
|
+
indicatorHeight,
|
|
109
|
+
indicatorWidth,
|
|
110
|
+
indicatorX,
|
|
111
|
+
indicatorY,
|
|
112
|
+
indicatorPositionOffset,
|
|
113
|
+
isReducedMotion,
|
|
114
|
+
uncontrolledValue,
|
|
115
|
+
]);
|
|
116
|
+
useEffect(() => {
|
|
117
|
+
if (!currentValue || !initialisedRef.current)
|
|
118
|
+
return;
|
|
119
|
+
if (prevValueRef.current === undefined || prevValueRef.current === currentValue)
|
|
120
|
+
return;
|
|
121
|
+
const layout = layoutsRef.current.get(currentValue);
|
|
122
|
+
if (!layout)
|
|
123
|
+
return;
|
|
124
|
+
const config = {
|
|
125
|
+
duration: isReducedMotion ? 0 : 220,
|
|
126
|
+
easing: Easing.out(Easing.cubic),
|
|
127
|
+
};
|
|
128
|
+
indicatorX.value = withTiming(Math.max(0, layout.x - indicatorPositionOffset), config);
|
|
129
|
+
indicatorWidth.value = withTiming(layout.width, config);
|
|
130
|
+
indicatorY.value = withTiming(Math.max(0, layout.y - indicatorPositionOffset), config);
|
|
131
|
+
indicatorHeight.value = withTiming(layout.height, config);
|
|
132
|
+
prevValueRef.current = currentValue;
|
|
133
|
+
}, [
|
|
134
|
+
currentValue,
|
|
135
|
+
indicatorHeight,
|
|
136
|
+
indicatorWidth,
|
|
137
|
+
indicatorX,
|
|
138
|
+
indicatorY,
|
|
139
|
+
indicatorPositionOffset,
|
|
140
|
+
isReducedMotion,
|
|
141
|
+
optionValuesKey,
|
|
142
|
+
]);
|
|
143
|
+
const indicatorStyle = useAnimatedStyle(() => ({
|
|
144
|
+
transform: [{ translateX: indicatorX.value }, { translateY: indicatorY.value }],
|
|
145
|
+
width: indicatorWidth.value,
|
|
146
|
+
height: indicatorHeight.value,
|
|
147
|
+
}));
|
|
148
|
+
styles.useVariants({ disabled, size });
|
|
149
|
+
const contextValue = useMemo(() => ({
|
|
150
|
+
value: currentValue,
|
|
151
|
+
select,
|
|
152
|
+
disabled,
|
|
153
|
+
size,
|
|
154
|
+
registerOptionLayout,
|
|
155
|
+
}), [currentValue, select, disabled, size, registerOptionLayout]);
|
|
156
|
+
return (_jsx(SegmentedControlContext.Provider, { value: contextValue, children: _jsxs(View, { accessibilityRole: "radiogroup", accessibilityState: { disabled }, style: [styles.container, computedStyles, style], ...remainingProps, children: [hasIndicator ? (_jsx(Indicator, { pointerEvents: "none", style: [styles.indicator, indicatorStyle] })) : null, children] }) }));
|
|
157
|
+
};
|
|
158
|
+
SegmentedControl.displayName = 'SegmentedControl';
|
|
159
|
+
const styles = StyleSheet.create(theme => ({
|
|
160
|
+
container: {
|
|
161
|
+
flexDirection: 'row',
|
|
162
|
+
alignItems: 'center',
|
|
163
|
+
alignSelf: 'flex-start',
|
|
164
|
+
gap: theme.components.segmentedControl.group.gap,
|
|
165
|
+
height: theme.components.segmentedControl.group.height,
|
|
166
|
+
borderRadius: theme.components.segmentedControl.group.borderRadius,
|
|
167
|
+
borderWidth: theme.components.segmentedControl.group.borderWidth,
|
|
168
|
+
backgroundColor: theme.color.surface.neutral.subtle,
|
|
169
|
+
borderColor: theme.color.border.strong,
|
|
170
|
+
variants: {
|
|
171
|
+
size: {
|
|
172
|
+
sm: {
|
|
173
|
+
height: 32,
|
|
174
|
+
padding: 2,
|
|
175
|
+
},
|
|
176
|
+
md: {
|
|
177
|
+
height: theme.components.segmentedControl.group.height,
|
|
178
|
+
padding: 2,
|
|
179
|
+
},
|
|
180
|
+
},
|
|
181
|
+
disabled: {
|
|
182
|
+
true: {
|
|
183
|
+
opacity: theme.opacity.disabled,
|
|
184
|
+
},
|
|
185
|
+
},
|
|
186
|
+
},
|
|
187
|
+
},
|
|
188
|
+
indicator: {
|
|
189
|
+
position: 'absolute',
|
|
190
|
+
left: 0,
|
|
191
|
+
top: 0,
|
|
192
|
+
borderRadius: theme.components.segmentedControl.borderRadius,
|
|
193
|
+
backgroundColor: theme.color.interactive.brand.surface.strong.default,
|
|
194
|
+
},
|
|
195
|
+
}));
|
|
196
|
+
export default SegmentedControl;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
import type { ViewProps } from 'react-native';
|
|
3
|
+
import type { FlexLayoutProps } from '../../types';
|
|
4
|
+
export interface SegmentedControlProps extends ViewProps, FlexLayoutProps {
|
|
5
|
+
/** Controlled selected option value. */
|
|
6
|
+
value?: string;
|
|
7
|
+
/** Initial selected option value for uncontrolled mode. */
|
|
8
|
+
defaultValue?: string;
|
|
9
|
+
/** Called when selected option changes. */
|
|
10
|
+
onValueChange?: (value: string) => void;
|
|
11
|
+
/** Size variant. */
|
|
12
|
+
size?: 'sm' | 'md';
|
|
13
|
+
/** Disables all options in the control. */
|
|
14
|
+
disabled?: boolean;
|
|
15
|
+
/** SegmentedControlOption children. */
|
|
16
|
+
children: ReactNode;
|
|
17
|
+
}
|
|
18
|
+
export default SegmentedControlProps;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type SegmentedControlOptionProps from './SegmentedControlOption.props';
|
|
2
|
+
declare const SegmentedControlOption: import("react").ForwardRefExoticComponent<SegmentedControlOptionProps & {
|
|
3
|
+
states?: {
|
|
4
|
+
active?: boolean;
|
|
5
|
+
disabled?: boolean;
|
|
6
|
+
};
|
|
7
|
+
} & Omit<import("react-native").PressableProps, "children"> & {
|
|
8
|
+
tabIndex?: 0 | -1 | undefined;
|
|
9
|
+
} & {
|
|
10
|
+
children?: import("react").ReactNode | (({ hovered, pressed, focused, focusVisible, disabled, }: {
|
|
11
|
+
hovered?: boolean | undefined;
|
|
12
|
+
pressed?: boolean | undefined;
|
|
13
|
+
focused?: boolean | undefined;
|
|
14
|
+
focusVisible?: boolean | undefined;
|
|
15
|
+
disabled?: boolean | undefined;
|
|
16
|
+
}) => import("react").ReactNode);
|
|
17
|
+
} & import("react").RefAttributes<unknown>>;
|
|
18
|
+
export default SegmentedControlOption;
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { createPressable } from '@gluestack-ui/pressable';
|
|
3
|
+
import { useEffect } from 'react';
|
|
4
|
+
import { Platform, Pressable, View } from 'react-native';
|
|
5
|
+
import Animated, { Easing, useAnimatedStyle, useReducedMotion, useSharedValue, withTiming, } from 'react-native-reanimated';
|
|
6
|
+
import { StyleSheet } from 'react-native-unistyles';
|
|
7
|
+
import { BodyText } from '../BodyText';
|
|
8
|
+
import { useSegmentedControlContext } from './SegmentedControl.context';
|
|
9
|
+
const AnimatedView = Animated.createAnimatedComponent(View);
|
|
10
|
+
const SegmentedControlOptionRoot = ({ value, children, accessibilityLabel, disabled = false, style, states = {}, ...props }) => {
|
|
11
|
+
const { value: selectedValue, select, disabled: allDisabled, size, registerOptionLayout, } = useSegmentedControlContext();
|
|
12
|
+
const { active = false } = states;
|
|
13
|
+
const reducedMotion = useReducedMotion();
|
|
14
|
+
const selected = selectedValue === value;
|
|
15
|
+
const isDisabled = disabled || !!allDisabled;
|
|
16
|
+
const selectedProgress = useSharedValue(selected ? 1 : 0);
|
|
17
|
+
useEffect(() => {
|
|
18
|
+
selectedProgress.value = withTiming(selected ? 1 : 0, {
|
|
19
|
+
duration: reducedMotion ? 0 : 220,
|
|
20
|
+
easing: Easing.out(Easing.cubic),
|
|
21
|
+
});
|
|
22
|
+
}, [reducedMotion, selected, selectedProgress]);
|
|
23
|
+
const regularLabelStyle = useAnimatedStyle(() => ({
|
|
24
|
+
opacity: 1 - selectedProgress.value,
|
|
25
|
+
}));
|
|
26
|
+
const selectedLabelStyle = useAnimatedStyle(() => ({
|
|
27
|
+
opacity: selectedProgress.value,
|
|
28
|
+
}));
|
|
29
|
+
styles.useVariants({ selected, disabled: isDisabled, size, active });
|
|
30
|
+
const onPress = () => {
|
|
31
|
+
if (isDisabled)
|
|
32
|
+
return;
|
|
33
|
+
select(value);
|
|
34
|
+
};
|
|
35
|
+
const accessibleLabel = typeof children === 'string' || typeof children === 'number' ? String(children) : value;
|
|
36
|
+
return (_jsx(Pressable, { ...props, accessibilityRole: "radio", accessibilityState: { checked: selected, disabled: isDisabled }, accessibilityLabel: accessibilityLabel ?? accessibleLabel, onPress: onPress, onLayout: e => registerOptionLayout(value, e.nativeEvent.layout), disabled: isDisabled, style: [styles.option, style], ...(Platform.OS === 'web'
|
|
37
|
+
? { 'aria-label': accessibilityLabel ?? accessibleLabel }
|
|
38
|
+
: null), children: _jsxs(View, { style: styles.labelWrap, accessible: false, accessibilityElementsHidden: true, importantForAccessibility: "no-hide-descendants", ...(Platform.OS === 'web' ? { 'aria-hidden': true } : null), children: [_jsx(BodyText, { size: "md", weight: "semibold", style: styles.labelSizer, accessible: false, accessibilityElementsHidden: true, importantForAccessibility: "no-hide-descendants", ...(Platform.OS === 'web' ? { 'aria-hidden': true } : null), children: children }), _jsx(AnimatedView, { pointerEvents: "none", style: [styles.textLayer, regularLabelStyle], accessible: false, accessibilityElementsHidden: true, importantForAccessibility: "no-hide-descendants", ...(Platform.OS === 'web' ? { 'aria-hidden': true } : null), children: _jsx(BodyText, { size: "md", weight: "regular", style: styles.textRegular, children: children }) }), _jsx(AnimatedView, { pointerEvents: "none", style: [styles.textLayer, selectedLabelStyle], accessible: false, accessibilityElementsHidden: true, importantForAccessibility: "no-hide-descendants", ...(Platform.OS === 'web' ? { 'aria-hidden': true } : null), children: _jsx(BodyText, { size: "md", weight: "semibold", style: styles.textSelected, children: children }) })] }) }));
|
|
39
|
+
};
|
|
40
|
+
const SegmentedControlOption = createPressable({ Root: SegmentedControlOptionRoot });
|
|
41
|
+
SegmentedControlOption.displayName = 'SegmentedControlOption';
|
|
42
|
+
const styles = StyleSheet.create(theme => ({
|
|
43
|
+
option: {
|
|
44
|
+
minWidth: theme.components.segmentedControl.minWidth,
|
|
45
|
+
height: theme.components.segmentedControl.height,
|
|
46
|
+
borderRadius: theme.components.segmentedControl.borderRadius,
|
|
47
|
+
paddingHorizontal: theme.components.segmentedControl.paddingHorizontal,
|
|
48
|
+
paddingVertical: theme.components.segmentedControl.paddingVertical,
|
|
49
|
+
justifyContent: 'center',
|
|
50
|
+
alignItems: 'center',
|
|
51
|
+
backgroundColor: 'transparent',
|
|
52
|
+
zIndex: 1,
|
|
53
|
+
variants: {
|
|
54
|
+
size: {
|
|
55
|
+
sm: {
|
|
56
|
+
height: 28,
|
|
57
|
+
paddingHorizontal: theme.space[150],
|
|
58
|
+
paddingVertical: 0,
|
|
59
|
+
},
|
|
60
|
+
md: {
|
|
61
|
+
height: 44,
|
|
62
|
+
paddingHorizontal: theme.components.segmentedControl.paddingHorizontal,
|
|
63
|
+
paddingVertical: theme.components.segmentedControl.paddingVertical,
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
selected: {
|
|
67
|
+
true: {
|
|
68
|
+
backgroundColor: 'transparent',
|
|
69
|
+
_web: {
|
|
70
|
+
_active: {
|
|
71
|
+
backgroundColor: theme.color.interactive.brand.surface.strong.active,
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
false: {
|
|
76
|
+
_web: {
|
|
77
|
+
_hover: {
|
|
78
|
+
backgroundColor: theme.color.interactive.neutral.surface.subtle.hover,
|
|
79
|
+
},
|
|
80
|
+
_active: {
|
|
81
|
+
backgroundColor: theme.color.interactive.neutral.surface.subtle.active,
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
active: {
|
|
87
|
+
true: {
|
|
88
|
+
backgroundColor: theme.color.interactive.neutral.surface.subtle.active,
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
},
|
|
92
|
+
},
|
|
93
|
+
labelWrap: {
|
|
94
|
+
position: 'relative',
|
|
95
|
+
alignItems: 'center',
|
|
96
|
+
justifyContent: 'center',
|
|
97
|
+
},
|
|
98
|
+
labelSizer: {
|
|
99
|
+
opacity: 0,
|
|
100
|
+
},
|
|
101
|
+
textLayer: {
|
|
102
|
+
position: 'absolute',
|
|
103
|
+
left: 0,
|
|
104
|
+
right: 0,
|
|
105
|
+
alignItems: 'center',
|
|
106
|
+
justifyContent: 'center',
|
|
107
|
+
},
|
|
108
|
+
textRegular: {
|
|
109
|
+
color: theme.color.text.primary,
|
|
110
|
+
},
|
|
111
|
+
textSelected: {
|
|
112
|
+
color: theme.color.text.inverted,
|
|
113
|
+
variants: {
|
|
114
|
+
disabled: {
|
|
115
|
+
true: {
|
|
116
|
+
opacity: 1,
|
|
117
|
+
},
|
|
118
|
+
},
|
|
119
|
+
},
|
|
120
|
+
},
|
|
121
|
+
}));
|
|
122
|
+
export default SegmentedControlOption;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
import type { PressableProps, ViewProps } from 'react-native';
|
|
3
|
+
export interface SegmentedControlOptionProps extends Omit<PressableProps, 'children'> {
|
|
4
|
+
/** Unique option value. */
|
|
5
|
+
value: string;
|
|
6
|
+
/** Option label/content. */
|
|
7
|
+
children: ReactNode;
|
|
8
|
+
/** Disables only this option. */
|
|
9
|
+
disabled?: boolean;
|
|
10
|
+
style?: ViewProps['style'];
|
|
11
|
+
}
|
|
12
|
+
export default SegmentedControlOptionProps;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { default as SegmentedControl } from './SegmentedControl';
|
|
2
|
+
export type { SegmentedControlProps } from './SegmentedControl.props';
|
|
3
|
+
export { default as SegmentedControlOption } from './SegmentedControlOption';
|
|
4
|
+
export type { SegmentedControlOptionProps } from './SegmentedControlOption.props';
|