@utilitywarehouse/hearth-react-native 0.25.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 +15 -15
- package/CHANGELOG.md +71 -0
- package/build/components/Banner/Banner.js +12 -1
- package/build/components/Modal/Modal.d.ts +1 -1
- package/build/components/Modal/Modal.js +30 -7
- package/build/components/Modal/Modal.props.d.ts +4 -2
- 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 +3 -3
- package/src/components/Banner/Banner.tsx +12 -1
- package/src/components/Modal/Modal.docs.mdx +9 -3
- package/src/components/Modal/Modal.props.ts +4 -2
- package/src/components/Modal/Modal.tsx +44 -7
- 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
|
@@ -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';
|
package/docs/changelog.mdx
CHANGED
|
@@ -9,6 +9,142 @@ import { BackToTopButton } from './components';
|
|
|
9
9
|
The changelog for the Hearth React Native library. Here you can find all the changes, improvements, and bug fixes for each version.
|
|
10
10
|
|
|
11
11
|
|
|
12
|
+
## 0.25.0
|
|
13
|
+
|
|
14
|
+
### Minor Changes
|
|
15
|
+
|
|
16
|
+
- [#982](https://github.com/utilitywarehouse/hearth/pull/982) [`506b388`](https://github.com/utilitywarehouse/hearth/commit/506b388ae1ef1065f013024a14bd9e2599a6442d) Thanks [@jordmccord](https://github.com/jordmccord)! - 🌟 [FEATURE]: Add `TimePicker` and `TimePickerInput` components with 12/24-hour support and minute intervals.
|
|
17
|
+
|
|
18
|
+
Includes a shared time picker view, updated wheel behavior for native platforms, and polished visuals like gradient fades.
|
|
19
|
+
|
|
20
|
+
**Components affected**:
|
|
21
|
+
- `TimePicker`
|
|
22
|
+
- `TimePickerInput`
|
|
23
|
+
|
|
24
|
+
```tsx
|
|
25
|
+
import { TimePicker, TimePickerInput } from '@utilitywarehouse/hearth-react-native';
|
|
26
|
+
import { useRef, useState } from 'react';
|
|
27
|
+
import type { DateType } from '@utilitywarehouse/hearth-react-native';
|
|
28
|
+
|
|
29
|
+
const Example = () => {
|
|
30
|
+
const [value, setValue] = useState<DateType>();
|
|
31
|
+
const pickerRef = useRef(null);
|
|
32
|
+
|
|
33
|
+
return (
|
|
34
|
+
<>
|
|
35
|
+
<TimePickerInput
|
|
36
|
+
value={value}
|
|
37
|
+
onChange={({ date }) => setValue(date ?? undefined)}
|
|
38
|
+
onClear={() => setValue(undefined)}
|
|
39
|
+
timePickerProps={{ use12Hours: true, minuteInterval: 5 }}
|
|
40
|
+
/>
|
|
41
|
+
<TimePicker
|
|
42
|
+
ref={pickerRef}
|
|
43
|
+
date={value}
|
|
44
|
+
onChange={({ date }) => setValue(date)}
|
|
45
|
+
use12Hours
|
|
46
|
+
minuteInterval={5}
|
|
47
|
+
/>
|
|
48
|
+
</>
|
|
49
|
+
);
|
|
50
|
+
};
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## 0.24.0
|
|
54
|
+
|
|
55
|
+
### Minor Changes
|
|
56
|
+
|
|
57
|
+
- [#977](https://github.com/utilitywarehouse/hearth/pull/977) [`9d2b534`](https://github.com/utilitywarehouse/hearth/commit/9d2b5348a5748cb613f537808069de2e86bd21d7) Thanks [@jordmccord](https://github.com/jordmccord)! - 🌟 [FEATURE]: Add `ProgressBar` component with linear and circular variants.
|
|
58
|
+
|
|
59
|
+
**Developer changes**:
|
|
60
|
+
|
|
61
|
+
Use `ProgressBar` with a default percentage label, or override the label to show a custom value:
|
|
62
|
+
|
|
63
|
+
```tsx
|
|
64
|
+
import { ProgressBar } from '@utilitywarehouse/hearth-react-native';
|
|
65
|
+
|
|
66
|
+
<ProgressBar value={42} label="Uploading documents" />
|
|
67
|
+
|
|
68
|
+
<ProgressBar
|
|
69
|
+
value={68}
|
|
70
|
+
max={100}
|
|
71
|
+
label="Data allowance"
|
|
72
|
+
variant="circular"
|
|
73
|
+
formatValueText={(value, { max }) => `${max - value}GB remaining`}
|
|
74
|
+
/>
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### Patch Changes
|
|
78
|
+
|
|
79
|
+
- [#978](https://github.com/utilitywarehouse/hearth/pull/978) [`26a1173`](https://github.com/utilitywarehouse/hearth/commit/26a11731a493a8b92ac2a3a183516376ab54663b) Thanks [@jordmccord](https://github.com/jordmccord)! - 💅 [ENHANCEMENT]: Tighten `Modal` prop types and fix brand background text styling
|
|
80
|
+
|
|
81
|
+
Improves TypeScript safety so `stickyFooter` is not allowed when `inNavModal` is true, and `background` can only be set when `inNavModal` is true. Also ensures headings, body text, and button content are correctly inverted when using the brand background.
|
|
82
|
+
|
|
83
|
+
**Components affected**:
|
|
84
|
+
- `Modal`
|
|
85
|
+
|
|
86
|
+
**Developer changes**:
|
|
87
|
+
No changes required unless you were relying on invalid prop combinations.
|
|
88
|
+
|
|
89
|
+
## 0.23.0
|
|
90
|
+
|
|
91
|
+
### Minor Changes
|
|
92
|
+
|
|
93
|
+
- [#975](https://github.com/utilitywarehouse/hearth/pull/975) [`102f04e`](https://github.com/utilitywarehouse/hearth/commit/102f04e0d560cf0faa21da5020c230e88e857251) Thanks [@jordmccord](https://github.com/jordmccord)! - 🌟 [FEATURE]: Add a `background` option for Modal when used inside navigation modals
|
|
94
|
+
|
|
95
|
+
Modal now supports a `background` prop with `default` and `brand` values. When `background="brand"` is used in a navigation modal, the buttons and close icon invert for contrast, and the content area is scrollable.
|
|
96
|
+
|
|
97
|
+
**Components affected**:
|
|
98
|
+
- `Modal`
|
|
99
|
+
|
|
100
|
+
**Developer changes**:
|
|
101
|
+
|
|
102
|
+
No changes required. To opt in to the brand background:
|
|
103
|
+
|
|
104
|
+
```tsx
|
|
105
|
+
<Modal background="brand" inNavModal>
|
|
106
|
+
...
|
|
107
|
+
</Modal>
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## 0.22.1
|
|
111
|
+
|
|
112
|
+
### Patch Changes
|
|
113
|
+
|
|
114
|
+
- [#971](https://github.com/utilitywarehouse/hearth/pull/971) [`be1dfeb`](https://github.com/utilitywarehouse/hearth/commit/be1dfebd4b43f2df8ef6c5eaa42a88364e796479) Thanks [@jordmccord](https://github.com/jordmccord)! - 💅 [ENHANCEMENT]: Improve VerificationInput OTP handling and accessibility
|
|
115
|
+
|
|
116
|
+
VerificationInput now uses a single hidden input to manage focus, selection, and paste behaviour across platforms, improving caret handling and bulk entry. Accessibility labels and hints are now derived from the form field to provide clearer screen reader output.
|
|
117
|
+
|
|
118
|
+
**Components affected**:
|
|
119
|
+
- `VerificationInput`
|
|
120
|
+
|
|
121
|
+
**Developer changes**:
|
|
122
|
+
|
|
123
|
+
No changes required.
|
|
124
|
+
|
|
125
|
+
## 0.22.0
|
|
126
|
+
|
|
127
|
+
### Minor Changes
|
|
128
|
+
|
|
129
|
+
- [#968](https://github.com/utilitywarehouse/hearth/pull/968) [`cee5811`](https://github.com/utilitywarehouse/hearth/commit/cee5811020af02fe754d8311ec8313c1793f108a) Thanks [@jordmccord](https://github.com/jordmccord)! - 🌟 [FEATURE]: Add `badge` support to Radio and Checkbox (including tiles).
|
|
130
|
+
|
|
131
|
+
**Components affected**:
|
|
132
|
+
- `Radio`
|
|
133
|
+
- `RadioTile`
|
|
134
|
+
- `Checkbox`
|
|
135
|
+
- `CheckboxTile`
|
|
136
|
+
|
|
137
|
+
**Developer changes**:
|
|
138
|
+
You can now pass a `badge` React node to render beneath helper text. No changes required unless you want to use the new prop.
|
|
139
|
+
|
|
140
|
+
### Patch Changes
|
|
141
|
+
|
|
142
|
+
- [#966](https://github.com/utilitywarehouse/hearth/pull/966) [`4e9f3f0`](https://github.com/utilitywarehouse/hearth/commit/4e9f3f0284e50da5ba4e49e132dac745a1a8a68d) Thanks [@jordmccord](https://github.com/jordmccord)! - 🐛 [FIX]: Allow Card layout props and remove forced alignment
|
|
143
|
+
|
|
144
|
+
Card now accepts flex layout and display props, and it no longer forces `alignItems: flex-start` on the root, so custom alignment works as expected.
|
|
145
|
+
|
|
146
|
+
- [#969](https://github.com/utilitywarehouse/hearth/pull/969) [`c5c988b`](https://github.com/utilitywarehouse/hearth/commit/c5c988b65f1133b85b822037b086a524bc1255e3) Thanks [@jordmccord](https://github.com/jordmccord)! - 🐛 [FIX]: Render the Modal footer in navigation modals
|
|
147
|
+
|
|
12
148
|
## 0.21.0
|
|
13
149
|
|
|
14
150
|
### Minor Changes
|
|
@@ -82,6 +82,8 @@ import {
|
|
|
82
82
|
RadioCardGroup,
|
|
83
83
|
RadioGroup,
|
|
84
84
|
SectionHeader,
|
|
85
|
+
SegmentedControl,
|
|
86
|
+
SegmentedControlOption,
|
|
85
87
|
Select,
|
|
86
88
|
Skeleton,
|
|
87
89
|
Spinner,
|
|
@@ -743,6 +745,18 @@ const AllComponents: React.FC = () => {
|
|
|
743
745
|
/>
|
|
744
746
|
</Center>
|
|
745
747
|
</ComponentWrapper>
|
|
748
|
+
<ComponentWrapper
|
|
749
|
+
name="Segmented Control"
|
|
750
|
+
link="/?path=/docs/components-segmented-control--docs"
|
|
751
|
+
>
|
|
752
|
+
<Center flex={1}>
|
|
753
|
+
<SegmentedControl defaultValue="day" alignSelf="center">
|
|
754
|
+
<SegmentedControlOption value="day">Day</SegmentedControlOption>
|
|
755
|
+
<SegmentedControlOption value="week">Week</SegmentedControlOption>
|
|
756
|
+
<SegmentedControlOption value="month">Month</SegmentedControlOption>
|
|
757
|
+
</SegmentedControl>
|
|
758
|
+
</Center>
|
|
759
|
+
</ComponentWrapper>
|
|
746
760
|
<ComponentWrapper name="Select" link="/?path=/docs/forms-select--docs">
|
|
747
761
|
<Center flex={1}>
|
|
748
762
|
<BottomSheetModalProvider>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@utilitywarehouse/hearth-react-native",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.27.0",
|
|
4
4
|
"description": "Utility Warehouse React Native UI library",
|
|
5
5
|
"main": "build/index.js",
|
|
6
6
|
"types": "build/index.d.ts",
|
|
@@ -57,10 +57,10 @@
|
|
|
57
57
|
"vite-plugin-svgr": "^4.5.0",
|
|
58
58
|
"vitest": "^3.2.4",
|
|
59
59
|
"@utilitywarehouse/hearth-fonts": "^0.0.4",
|
|
60
|
-
"@utilitywarehouse/hearth-react-icons": "^0.8.0",
|
|
61
60
|
"@utilitywarehouse/hearth-react-native-icons": "^0.8.0",
|
|
62
61
|
"@utilitywarehouse/hearth-svg-assets": "^0.5.0",
|
|
63
|
-
"@utilitywarehouse/hearth-tokens": "^0.2.3"
|
|
62
|
+
"@utilitywarehouse/hearth-tokens": "^0.2.3",
|
|
63
|
+
"@utilitywarehouse/hearth-react-icons": "^0.8.0"
|
|
64
64
|
},
|
|
65
65
|
"peerDependencies": {
|
|
66
66
|
"@gorhom/bottom-sheet": "^5.0.0",
|
|
@@ -29,7 +29,7 @@ const Banner = ({
|
|
|
29
29
|
...props
|
|
30
30
|
}: BannerProps) => {
|
|
31
31
|
const hasIllustration = Boolean(illustration);
|
|
32
|
-
styles.useVariants({ direction, hasIllustration });
|
|
32
|
+
styles.useVariants({ direction, hasIllustration, isPressable: Boolean(onPress) });
|
|
33
33
|
|
|
34
34
|
const context = useMemo(
|
|
35
35
|
() => ({
|
|
@@ -174,6 +174,10 @@ const styles = StyleSheet.create(theme => ({
|
|
|
174
174
|
true: {},
|
|
175
175
|
false: {},
|
|
176
176
|
},
|
|
177
|
+
isPressable: {
|
|
178
|
+
true: {},
|
|
179
|
+
false: {},
|
|
180
|
+
},
|
|
177
181
|
},
|
|
178
182
|
compoundVariants: [
|
|
179
183
|
{
|
|
@@ -190,6 +194,13 @@ const styles = StyleSheet.create(theme => ({
|
|
|
190
194
|
alignItems: 'center',
|
|
191
195
|
},
|
|
192
196
|
},
|
|
197
|
+
{
|
|
198
|
+
direction: 'horizontal',
|
|
199
|
+
isPressable: false,
|
|
200
|
+
styles: {
|
|
201
|
+
flex: 1,
|
|
202
|
+
},
|
|
203
|
+
},
|
|
193
204
|
],
|
|
194
205
|
},
|
|
195
206
|
media: {
|
|
@@ -107,9 +107,10 @@ The Modal component extends the `BottomSheetModal` component and accepts all of
|
|
|
107
107
|
| `primaryButtonProps` | `Omit<ButtonWithoutChildrenProps, 'children'>` | Additional props to pass to the primary button (colorScheme defaults to 'highlight', variant to 'solid') | - |
|
|
108
108
|
| `secondaryButtonProps` | `Omit<ButtonWithoutChildrenProps, 'children'>` | Additional props to pass to the secondary button (colorScheme defaults to 'functional', variant to 'outline') | - |
|
|
109
109
|
| `closeButtonProps` | `Omit<UnstyledIconButtonProps, 'children'>` | Additional props to pass to the close button | - |
|
|
110
|
-
| `fullscreen` | `boolean` | Whether the modal should take up the full screen height
|
|
110
|
+
| `fullscreen` | `boolean` | Whether the modal should take up the full screen height. Only applies when `inNavModal` is `false` | `false` |
|
|
111
111
|
| `inNavModal` | `boolean` | Renders the modal correctly when used inside a navigation modal | `false` |
|
|
112
112
|
| `background` | `'default' \| 'brand'` | Sets the modal background. Only applies when `inNavModal` is `true` | `'default'` |
|
|
113
|
+
| `scrollable` | `boolean` | Whether the modal's content should be placed in a `ScrollView`. Only applies when `inNavModal` is `true` | `true` |
|
|
113
114
|
|
|
114
115
|
\* use this to detect if the modal has been opened or closed, index 0 indicates open state and -1 indicates closed state
|
|
115
116
|
|
|
@@ -462,11 +463,16 @@ const AlertModal = () => {
|
|
|
462
463
|
|
|
463
464
|
### Modal In Navigation Modal
|
|
464
465
|
|
|
465
|
-
When
|
|
466
|
+
When wanting to use the Modal component in a navigation context using [React Navigation](https://reactnavigation.org/docs/modal), you can set `inNavModal` to `true` to make it behave like a standard modal screen.
|
|
467
|
+
|
|
468
|
+
Within React Navigation, you can set `presentation: 'modal'` in the screen's settings to have the Modal look and behave like a standard modal/bottom sheet, or you can set `presentation: 'fullScreenModal'` to have the Modal fill the entire screen.
|
|
469
|
+
|
|
470
|
+
When using `inNavModal`, by default the content will be rendered inside a `ScrollView` to ensure it is scrollable, especially on smaller devices or smaller modals. You can disable this by setting `scrollable={false}` if, for example, you need to center your content or add some custom content.
|
|
471
|
+
|
|
466
472
|
Here's an example of how to implement this with custom close animations for Android:
|
|
467
473
|
|
|
468
474
|
```tsx
|
|
469
|
-
import { useNavigation } from 'react-navigation/native';
|
|
475
|
+
import { useNavigation } from '@react-navigation/native';
|
|
470
476
|
import { useCallback, useEffect, useRef } from 'react';
|
|
471
477
|
import { Platform, StyleSheet, View } from 'react-native';
|
|
472
478
|
|
|
@@ -12,6 +12,7 @@ interface ModalPropsBase extends Omit<BottomSheetProps, 'children'> {
|
|
|
12
12
|
loadingHeading?: string;
|
|
13
13
|
description?: string;
|
|
14
14
|
fullscreen?: boolean;
|
|
15
|
+
stickyFooter?: boolean;
|
|
15
16
|
children?: ViewProps['children'];
|
|
16
17
|
onPressPrimaryButton?: () => void;
|
|
17
18
|
primaryButtonText?: string;
|
|
@@ -28,12 +29,13 @@ interface ModalPropsBase extends Omit<BottomSheetProps, 'children'> {
|
|
|
28
29
|
type ModalProps =
|
|
29
30
|
| (ModalPropsBase & {
|
|
30
31
|
inNavModal?: false | undefined;
|
|
31
|
-
|
|
32
|
+
scrollable?: never;
|
|
32
33
|
background?: never;
|
|
33
34
|
})
|
|
34
35
|
| (ModalPropsBase & {
|
|
35
36
|
inNavModal: true;
|
|
36
|
-
|
|
37
|
+
fullscreen?: never;
|
|
38
|
+
scrollable?: boolean;
|
|
37
39
|
background?: 'default' | 'brand';
|
|
38
40
|
});
|
|
39
41
|
|
|
@@ -6,8 +6,8 @@ import {
|
|
|
6
6
|
} from '@gorhom/bottom-sheet';
|
|
7
7
|
import { BottomSheetModalMethods } from '@gorhom/bottom-sheet/lib/typescript/types';
|
|
8
8
|
import { CloseMediumIcon } from '@utilitywarehouse/hearth-react-native-icons';
|
|
9
|
-
import { useCallback, useEffect, useImperativeHandle, useRef } from 'react';
|
|
10
|
-
import { AccessibilityInfo, Platform, ScrollView, View, findNodeHandle } from 'react-native';
|
|
9
|
+
import { useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react';
|
|
10
|
+
import { AccessibilityInfo, Dimensions, Platform, ScrollView, View, findNodeHandle } from 'react-native';
|
|
11
11
|
import Animated, {
|
|
12
12
|
Easing,
|
|
13
13
|
useAnimatedStyle,
|
|
@@ -51,6 +51,7 @@ const Modal = ({
|
|
|
51
51
|
inNavModal = false,
|
|
52
52
|
stickyFooter = true,
|
|
53
53
|
background = 'default',
|
|
54
|
+
scrollable = true,
|
|
54
55
|
...props
|
|
55
56
|
}: ModalProps) => {
|
|
56
57
|
const bottomSheetModalRef = useRef<BottomSheetModal>(null);
|
|
@@ -61,6 +62,16 @@ const Modal = ({
|
|
|
61
62
|
const pretendContentTranslateY = useSharedValue(20);
|
|
62
63
|
const isBrandBackground = background === 'brand';
|
|
63
64
|
|
|
65
|
+
const [inNavModalHeight, setInNavModalHeight] = useState<number>();
|
|
66
|
+
|
|
67
|
+
const isNavModalFullScreen = useMemo(() => {
|
|
68
|
+
if (!inNavModalHeight || !inNavModal) return false;
|
|
69
|
+
|
|
70
|
+
const screenHeight = Dimensions.get('window').height;
|
|
71
|
+
|
|
72
|
+
return inNavModalHeight >= screenHeight;
|
|
73
|
+
}, [inNavModalHeight, inNavModal]);
|
|
74
|
+
|
|
64
75
|
const triggerCloseAnimation = useCallback(() => {
|
|
65
76
|
if (Platform.OS === 'android' && inNavModal) {
|
|
66
77
|
pretendContentTranslateY.value = withTiming(20, {
|
|
@@ -173,6 +184,9 @@ const Modal = ({
|
|
|
173
184
|
stickyFooter,
|
|
174
185
|
showHandle: props.showHandle,
|
|
175
186
|
background: isBrandBackground ? 'brand' : 'primary',
|
|
187
|
+
...(inNavModal && {
|
|
188
|
+
fullscreen: isNavModalFullScreen,
|
|
189
|
+
}),
|
|
176
190
|
});
|
|
177
191
|
|
|
178
192
|
const footer = (
|
|
@@ -200,6 +214,8 @@ const Modal = ({
|
|
|
200
214
|
</View>
|
|
201
215
|
);
|
|
202
216
|
|
|
217
|
+
const InNavModalContainer = scrollable ? ScrollView : View;
|
|
218
|
+
|
|
203
219
|
const content = (
|
|
204
220
|
<>
|
|
205
221
|
{loading ? (
|
|
@@ -275,8 +291,14 @@ const Modal = ({
|
|
|
275
291
|
</View>
|
|
276
292
|
</View>
|
|
277
293
|
) : null}
|
|
278
|
-
{inNavModal
|
|
279
|
-
|
|
294
|
+
{inNavModal && (
|
|
295
|
+
<InNavModalContainer style={{ flexGrow: stickyFooter ? 1 : 0 }}>
|
|
296
|
+
{children}
|
|
297
|
+
{!stickyFooter ? <View style={styles.inNavModalFooterContainer}>{footer}</View> : null}
|
|
298
|
+
</InNavModalContainer>
|
|
299
|
+
)}
|
|
300
|
+
{!inNavModal && children}
|
|
301
|
+
{((!stickyFooter && !inNavModal) || (inNavModal && stickyFooter)) && !noButtons ? footer : null}
|
|
280
302
|
</View>
|
|
281
303
|
)}
|
|
282
304
|
</>
|
|
@@ -300,6 +322,9 @@ const Modal = ({
|
|
|
300
322
|
|
|
301
323
|
return inNavModal ? (
|
|
302
324
|
<View
|
|
325
|
+
onLayout={(e) => {
|
|
326
|
+
setInNavModalHeight(e.nativeEvent.layout.height);
|
|
327
|
+
}}
|
|
303
328
|
style={{
|
|
304
329
|
flex: 1,
|
|
305
330
|
backgroundColor: theme.color.background[isBrandBackground ? 'brand' : 'primary'],
|
|
@@ -313,7 +338,9 @@ const Modal = ({
|
|
|
313
338
|
<Animated.View
|
|
314
339
|
style={[styles.inNavModalContainer, Platform.OS === 'android' && animatedInNavModalStyle]}
|
|
315
340
|
>
|
|
316
|
-
<View style={styles.inNavModalContent}>
|
|
341
|
+
<View style={styles.inNavModalContent}>
|
|
342
|
+
{content}
|
|
343
|
+
</View>
|
|
317
344
|
</Animated.View>
|
|
318
345
|
</View>
|
|
319
346
|
) : (
|
|
@@ -444,8 +471,6 @@ const styles = StyleSheet.create((theme, rt) => ({
|
|
|
444
471
|
borderTopLeftRadius: theme.components.modal.borderRadius,
|
|
445
472
|
borderTopRightRadius: theme.components.modal.borderRadius,
|
|
446
473
|
backgroundColor: theme.color.surface.neutral.strong,
|
|
447
|
-
gap: theme.components.modal.gap,
|
|
448
|
-
padding: theme.components.modal.padding,
|
|
449
474
|
paddingBottom: theme.components.modal.padding + rt.insets.bottom,
|
|
450
475
|
variants: {
|
|
451
476
|
background: {
|
|
@@ -454,8 +479,20 @@ const styles = StyleSheet.create((theme, rt) => ({
|
|
|
454
479
|
backgroundColor: theme.color.background.brand,
|
|
455
480
|
},
|
|
456
481
|
},
|
|
482
|
+
fullscreen: {
|
|
483
|
+
true: {
|
|
484
|
+
padding: theme.components.modal.padding,
|
|
485
|
+
paddingTop: rt.insets.top,
|
|
486
|
+
},
|
|
487
|
+
false: {
|
|
488
|
+
padding: theme.components.modal.padding,
|
|
489
|
+
}
|
|
490
|
+
}
|
|
457
491
|
},
|
|
458
492
|
},
|
|
493
|
+
inNavModalFooterContainer: {
|
|
494
|
+
paddingTop: theme.components.modal.padding,
|
|
495
|
+
},
|
|
459
496
|
androidContainer: {
|
|
460
497
|
height: rt.insets.top + 18,
|
|
461
498
|
paddingLeft: theme.components.modal.padding,
|
|
@@ -42,6 +42,7 @@ export const PillGroup = ({
|
|
|
42
42
|
) : (
|
|
43
43
|
<ScrollView
|
|
44
44
|
horizontal
|
|
45
|
+
style={styles.scrollView}
|
|
45
46
|
contentContainerStyle={[styles.group, style]}
|
|
46
47
|
showsHorizontalScrollIndicator={false}
|
|
47
48
|
{...props}
|
|
@@ -56,6 +57,9 @@ export const PillGroup = ({
|
|
|
56
57
|
PillGroup.displayName = 'PillGroup';
|
|
57
58
|
|
|
58
59
|
const styles = StyleSheet.create(theme => ({
|
|
60
|
+
scrollView: {
|
|
61
|
+
flexGrow: 0,
|
|
62
|
+
},
|
|
59
63
|
group: {
|
|
60
64
|
flexDirection: 'row',
|
|
61
65
|
gap: theme.components.pill.group.gap,
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { createContext, useContext } from 'react';
|
|
2
|
+
|
|
3
|
+
export type SegmentedControlContextValue = {
|
|
4
|
+
value?: string;
|
|
5
|
+
select: (value: string) => void;
|
|
6
|
+
disabled?: boolean;
|
|
7
|
+
size: 'sm' | 'md';
|
|
8
|
+
registerOptionLayout: (
|
|
9
|
+
value: string,
|
|
10
|
+
layout: { x: number; y: number; width: number; height: number }
|
|
11
|
+
) => void;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export const SegmentedControlContext = createContext<SegmentedControlContextValue | null>(null);
|
|
15
|
+
|
|
16
|
+
export const useSegmentedControlContext = () => {
|
|
17
|
+
const context = useContext(SegmentedControlContext);
|
|
18
|
+
if (!context) {
|
|
19
|
+
throw new Error('SegmentedControlOption must be used within SegmentedControl');
|
|
20
|
+
}
|
|
21
|
+
return context;
|
|
22
|
+
};
|