@utilitywarehouse/hearth-react-native 0.9.0 → 0.10.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 +1 -1
- package/CHANGELOG.md +8 -0
- package/build/components/PillGroup/Pill.d.ts +16 -0
- package/build/components/PillGroup/Pill.js +94 -0
- package/build/components/PillGroup/Pill.props.d.ts +10 -0
- package/build/components/PillGroup/Pill.props.js +1 -0
- package/build/components/PillGroup/PillGroup.context.d.ts +6 -0
- package/build/components/PillGroup/PillGroup.context.js +5 -0
- package/build/components/PillGroup/PillGroup.d.ts +5 -0
- package/build/components/PillGroup/PillGroup.js +34 -0
- package/build/components/PillGroup/PillGroup.props.d.ts +15 -0
- package/build/components/PillGroup/PillGroup.props.js +1 -0
- package/build/components/PillGroup/index.d.ts +4 -0
- package/build/components/PillGroup/index.js +2 -0
- package/build/components/Select/Select.js +2 -1
- package/build/components/Toast/Toast.context.d.ts +9 -0
- package/build/components/Toast/Toast.context.js +90 -0
- package/build/components/Toast/Toast.props.d.ts +29 -0
- package/build/components/Toast/Toast.props.js +1 -0
- package/build/components/Toast/ToastItem.d.ts +10 -0
- package/build/components/Toast/ToastItem.js +129 -0
- package/build/components/Toast/index.d.ts +3 -0
- package/build/components/Toast/index.js +2 -0
- package/build/components/index.d.ts +2 -0
- package/build/components/index.js +2 -0
- package/build/tokens/components/dark/checkbox.d.ts +3 -0
- package/build/tokens/components/dark/checkbox.js +3 -0
- package/build/tokens/components/dark/input.d.ts +6 -0
- package/build/tokens/components/dark/input.js +6 -0
- package/build/tokens/components/dark/radio.d.ts +3 -0
- package/build/tokens/components/dark/radio.js +3 -0
- package/build/tokens/components/dark/table.d.ts +2 -0
- package/build/tokens/components/dark/table.js +2 -0
- package/build/tokens/components/dark/toast.d.ts +6 -2
- package/build/tokens/components/dark/toast.js +6 -2
- package/build/tokens/components/light/checkbox.d.ts +3 -0
- package/build/tokens/components/light/checkbox.js +3 -0
- package/build/tokens/components/light/input.d.ts +6 -0
- package/build/tokens/components/light/input.js +6 -0
- package/build/tokens/components/light/radio.d.ts +3 -0
- package/build/tokens/components/light/radio.js +3 -0
- package/build/tokens/components/light/table.d.ts +2 -0
- package/build/tokens/components/light/table.js +2 -0
- package/build/tokens/components/light/toast.d.ts +6 -2
- package/build/tokens/components/light/toast.js +6 -2
- package/docs/assets/toast-ios.MP4 +0 -0
- package/docs/components/AllComponents.web.tsx +26 -0
- package/package.json +3 -3
- package/src/components/PillGroup/Pill.props.ts +13 -0
- package/src/components/PillGroup/Pill.tsx +120 -0
- package/src/components/PillGroup/PillGroup.context.tsx +12 -0
- package/src/components/PillGroup/PillGroup.docs.mdx +96 -0
- package/src/components/PillGroup/PillGroup.props.ts +22 -0
- package/src/components/PillGroup/PillGroup.stories.tsx +159 -0
- package/src/components/PillGroup/PillGroup.tsx +66 -0
- package/src/components/PillGroup/index.ts +4 -0
- package/src/components/Select/Select.tsx +2 -0
- package/src/components/Toast/Toast.context.tsx +118 -0
- package/src/components/Toast/Toast.docs.mdx +164 -0
- package/src/components/Toast/Toast.props.ts +33 -0
- package/src/components/Toast/Toast.stories.tsx +356 -0
- package/src/components/Toast/ToastItem.tsx +200 -0
- package/src/components/Toast/index.ts +3 -0
- package/src/components/index.ts +2 -0
- package/src/tokens/components/dark/checkbox.ts +3 -0
- package/src/tokens/components/dark/input.ts +6 -0
- package/src/tokens/components/dark/radio.ts +3 -0
- package/src/tokens/components/dark/table.ts +2 -0
- package/src/tokens/components/dark/toast.ts +6 -2
- package/src/tokens/components/light/checkbox.ts +3 -0
- package/src/tokens/components/light/input.ts +6 -0
- package/src/tokens/components/light/radio.ts +3 -0
- package/src/tokens/components/light/table.ts +2 -0
- package/src/tokens/components/light/toast.ts +6 -2
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.10.0 lint /home/runner/work/hearth/hearth/packages/react-native
|
|
3
3
|
> TIMING=1 eslint --max-warnings 0
|
|
4
4
|
|
|
5
5
|
Rule | Time (ms) | Relative
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# @utilitywarehouse/hearth-react-native
|
|
2
2
|
|
|
3
|
+
## 0.10.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- [#659](https://github.com/utilitywarehouse/hearth/pull/659) [`99afbed`](https://github.com/utilitywarehouse/hearth/commit/99afbed95df550d5d5a8bb6e04f7640077bf4883) Thanks [@MichalCiesliczka](https://github.com/MichalCiesliczka)! - Adds `Pill` and `PillGroup` components
|
|
8
|
+
|
|
9
|
+
- [#671](https://github.com/utilitywarehouse/hearth/pull/671) [`706448d`](https://github.com/utilitywarehouse/hearth/commit/706448d91f354cb96a71a5f3acc9d17aaa767078) Thanks [@jordmccord](https://github.com/jordmccord)! - Adds `Toast` component
|
|
10
|
+
|
|
3
11
|
## 0.9.0
|
|
4
12
|
|
|
5
13
|
### Minor Changes
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { PillProps } from './Pill.props';
|
|
2
|
+
export declare const Pill: import("react").ForwardRefExoticComponent<PillProps & {
|
|
3
|
+
states?: {
|
|
4
|
+
active?: boolean;
|
|
5
|
+
};
|
|
6
|
+
} & Omit<import("react-native").PressableProps, "children"> & {
|
|
7
|
+
tabIndex?: 0 | -1 | undefined;
|
|
8
|
+
} & {
|
|
9
|
+
children?: import("react").ReactNode | (({ hovered, pressed, focused, focusVisible, disabled, }: {
|
|
10
|
+
hovered?: boolean | undefined;
|
|
11
|
+
pressed?: boolean | undefined;
|
|
12
|
+
focused?: boolean | undefined;
|
|
13
|
+
focusVisible?: boolean | undefined;
|
|
14
|
+
disabled?: boolean | undefined;
|
|
15
|
+
}) => import("react").ReactNode);
|
|
16
|
+
} & import("react").RefAttributes<unknown>>;
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { createPressable } from '@gluestack-ui/pressable';
|
|
3
|
+
import { Pressable } from 'react-native';
|
|
4
|
+
import { StyleSheet } from 'react-native-unistyles';
|
|
5
|
+
import { Icon } from '../Icon';
|
|
6
|
+
import { BodyText } from '../BodyText';
|
|
7
|
+
import { usePillGroupContext } from './PillGroup.context';
|
|
8
|
+
const PillRoot = ({ value, label, icon, states = {}, ...props }) => {
|
|
9
|
+
const { active } = states;
|
|
10
|
+
const context = usePillGroupContext();
|
|
11
|
+
const isSelected = context?.value.includes(value) ?? false;
|
|
12
|
+
styles.useVariants({ selected: isSelected, active });
|
|
13
|
+
const handlePress = () => {
|
|
14
|
+
context?.onChange(value);
|
|
15
|
+
};
|
|
16
|
+
return (_jsxs(Pressable, { ...props, style: styles.pill, accessibilityRole: "button", accessibilityState: { selected: isSelected }, onPress: handlePress, children: [icon && _jsx(Icon, { as: icon, size: "sm", style: styles.icon }), _jsx(BodyText, { weight: "semibold", style: styles.text, children: label })] }));
|
|
17
|
+
};
|
|
18
|
+
export const Pill = createPressable({ Root: PillRoot });
|
|
19
|
+
Pill.displayName = 'Pill';
|
|
20
|
+
const styles = StyleSheet.create(theme => ({
|
|
21
|
+
pill: {
|
|
22
|
+
flexDirection: 'row',
|
|
23
|
+
alignItems: 'center',
|
|
24
|
+
justifyContent: 'center',
|
|
25
|
+
height: theme.components.pill.height,
|
|
26
|
+
minWidth: theme.components.pill.minWidth,
|
|
27
|
+
gap: theme.components.pill.gap,
|
|
28
|
+
paddingHorizontal: theme.components.pill.paddingHorizontal,
|
|
29
|
+
paddingVertical: theme.components.pill.paddingVertical,
|
|
30
|
+
borderRadius: theme.components.pill.borderRadius,
|
|
31
|
+
borderWidth: theme.components.pill.borderWidth,
|
|
32
|
+
borderColor: theme.color.interactive.neutral.border.subtle,
|
|
33
|
+
backgroundColor: 'transparent',
|
|
34
|
+
_web: {
|
|
35
|
+
_hover: {
|
|
36
|
+
backgroundColor: theme.color.interactive.neutral.surface.subtle.hover,
|
|
37
|
+
},
|
|
38
|
+
'_focus-visible': theme.helpers.focusVisible,
|
|
39
|
+
},
|
|
40
|
+
variants: {
|
|
41
|
+
active: {
|
|
42
|
+
true: {
|
|
43
|
+
backgroundColor: theme.color.interactive.neutral.surface.subtle.active,
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
selected: {
|
|
47
|
+
true: {
|
|
48
|
+
backgroundColor: theme.color.interactive.brand.surface.strong.default,
|
|
49
|
+
borderColor: theme.color.interactive.brand.surface.strong.default,
|
|
50
|
+
_web: {
|
|
51
|
+
_hover: {
|
|
52
|
+
backgroundColor: theme.color.interactive.brand.surface.strong.hover,
|
|
53
|
+
borderColor: theme.color.interactive.brand.surface.strong.hover,
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
compoundVariants: [
|
|
60
|
+
{
|
|
61
|
+
selected: true,
|
|
62
|
+
active: true,
|
|
63
|
+
styles: {
|
|
64
|
+
backgroundColor: theme.color.interactive.brand.surface.strong.active,
|
|
65
|
+
borderColor: theme.color.interactive.brand.surface.strong.active,
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
],
|
|
69
|
+
},
|
|
70
|
+
text: {
|
|
71
|
+
variants: {
|
|
72
|
+
selected: {
|
|
73
|
+
true: {
|
|
74
|
+
color: theme.color.text.inverted,
|
|
75
|
+
},
|
|
76
|
+
false: {
|
|
77
|
+
color: theme.color.text.primary,
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
icon: {
|
|
83
|
+
variants: {
|
|
84
|
+
selected: {
|
|
85
|
+
true: {
|
|
86
|
+
color: theme.color.icon.inverted,
|
|
87
|
+
},
|
|
88
|
+
false: {
|
|
89
|
+
color: theme.color.icon.primary,
|
|
90
|
+
},
|
|
91
|
+
},
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
}));
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { PressableProps } from 'react-native';
|
|
3
|
+
export interface PillProps extends Omit<PressableProps, 'children'> {
|
|
4
|
+
/** Value returned when selected */
|
|
5
|
+
value: string;
|
|
6
|
+
/** Text label shown inside the pill */
|
|
7
|
+
label: string;
|
|
8
|
+
/** Left icon */
|
|
9
|
+
icon?: React.ComponentType<any>;
|
|
10
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export interface PillGroupContextValue {
|
|
2
|
+
value: string[];
|
|
3
|
+
onChange: (value: string) => void;
|
|
4
|
+
}
|
|
5
|
+
export declare const PillGroupContext: import("react").Context<PillGroupContextValue | null>;
|
|
6
|
+
export declare const usePillGroupContext: () => PillGroupContextValue | null;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useMemo } from 'react';
|
|
3
|
+
import { ScrollView } from 'react-native';
|
|
4
|
+
import { StyleSheet } from 'react-native-unistyles';
|
|
5
|
+
import { Box } from '../Box';
|
|
6
|
+
import { PillGroupContext } from './PillGroup.context';
|
|
7
|
+
export const PillGroup = ({ children, value, multiple = false, wrap = true, onChange, style, ...props }) => {
|
|
8
|
+
const normalizedValue = Array.isArray(value) ? value : [value];
|
|
9
|
+
const contextValue = useMemo(() => ({
|
|
10
|
+
value: normalizedValue,
|
|
11
|
+
onChange: (pillValue) => {
|
|
12
|
+
if (multiple) {
|
|
13
|
+
const newValue = normalizedValue.includes(pillValue)
|
|
14
|
+
? normalizedValue.filter(v => v !== pillValue)
|
|
15
|
+
: [...normalizedValue, pillValue];
|
|
16
|
+
onChange?.(newValue);
|
|
17
|
+
}
|
|
18
|
+
else {
|
|
19
|
+
onChange?.(pillValue);
|
|
20
|
+
}
|
|
21
|
+
},
|
|
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 })) }));
|
|
24
|
+
};
|
|
25
|
+
PillGroup.displayName = 'PillGroup';
|
|
26
|
+
const styles = StyleSheet.create(theme => ({
|
|
27
|
+
group: {
|
|
28
|
+
flexDirection: 'row',
|
|
29
|
+
gap: theme.components.pill.group.gap,
|
|
30
|
+
},
|
|
31
|
+
wrap: {
|
|
32
|
+
flexWrap: 'wrap',
|
|
33
|
+
},
|
|
34
|
+
}));
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { ScrollViewProps, ViewStyle } from 'react-native';
|
|
3
|
+
export interface PillGroupProps extends Omit<ScrollViewProps, 'horizontal' | 'contentContainerStyle' | 'showsHorizontalScrollIndicator'> {
|
|
4
|
+
/** Controlled selected value(s) */
|
|
5
|
+
value: string | string[];
|
|
6
|
+
/** Multi-select mode. Default = false */
|
|
7
|
+
multiple?: boolean;
|
|
8
|
+
/** Allow pills to wrap lines. Default = true */
|
|
9
|
+
wrap?: boolean;
|
|
10
|
+
/** Handle selection changes */
|
|
11
|
+
onChange?: (value: string | string[]) => void;
|
|
12
|
+
/** Children must be <Pill> elements */
|
|
13
|
+
children: React.ReactNode;
|
|
14
|
+
style?: ViewStyle | ViewStyle[];
|
|
15
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -77,7 +77,7 @@ const Select = ({ options = [], value, onValueChange, label, placeholder = 'Sele
|
|
|
77
77
|
selectedValue: value,
|
|
78
78
|
onValueChange,
|
|
79
79
|
close: closeBottomSheet,
|
|
80
|
-
}, children: [menuHeading && (_jsx(View, { style: styles.headingContainer, children: _jsx(DetailText, { size: "lg", children: menuHeading }) })), searchable && (_jsx(View, { style: styles.searchContainer, children: _jsx(Input, { placeholder: searchPlaceholder, value: search, onChangeText: setSearch, type: "search" }) })), children ? (_jsx(BottomSheetScrollView, { children: children })) : (_jsx(BottomSheetFlatList, { data: filteredOptions, keyExtractor: (option) => option.value, renderItem: renderSelectOption, ListEmptyComponent: renderEmptyComponent, ...listProps }))] }) })] }));
|
|
80
|
+
}, children: [menuHeading && (_jsx(View, { style: styles.headingContainer, children: _jsx(DetailText, { size: "lg", children: menuHeading }) })), searchable && (_jsx(View, { style: styles.searchContainer, children: _jsx(Input, { placeholder: searchPlaceholder, value: search, inBottomSheet: true, onChangeText: setSearch, type: "search" }) })), children ? (_jsx(BottomSheetScrollView, { children: children })) : (_jsx(BottomSheetFlatList, { data: filteredOptions, keyExtractor: (option) => option.value, renderItem: renderSelectOption, ListEmptyComponent: renderEmptyComponent, ...listProps }))] }) })] }));
|
|
81
81
|
};
|
|
82
82
|
const styles = StyleSheet.create(theme => ({
|
|
83
83
|
container: {
|
|
@@ -161,6 +161,7 @@ const styles = StyleSheet.create(theme => ({
|
|
|
161
161
|
emptyContainer: {
|
|
162
162
|
alignItems: 'center',
|
|
163
163
|
justifyContent: 'center',
|
|
164
|
+
marginTop: theme.space.md,
|
|
164
165
|
},
|
|
165
166
|
}));
|
|
166
167
|
Select.displayName = 'Select';
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import type { ToastContextValue } from './Toast.props';
|
|
3
|
+
declare const ToastContext: React.Context<ToastContextValue | undefined>;
|
|
4
|
+
export declare const useToastContext: () => ToastContextValue;
|
|
5
|
+
export declare const ToastProvider: React.FC<{
|
|
6
|
+
children: React.ReactNode;
|
|
7
|
+
}>;
|
|
8
|
+
export declare const useToast: () => ToastContextValue;
|
|
9
|
+
export default ToastContext;
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { createContext, useCallback, useContext, useEffect, useRef, useState } from 'react';
|
|
3
|
+
import { View } from 'react-native';
|
|
4
|
+
import { StyleSheet } from 'react-native-unistyles';
|
|
5
|
+
import ToastItem from './ToastItem';
|
|
6
|
+
const ToastContext = createContext(undefined);
|
|
7
|
+
export const useToastContext = () => {
|
|
8
|
+
const ctx = useContext(ToastContext);
|
|
9
|
+
if (!ctx)
|
|
10
|
+
throw new Error('useToastContext must be used within ToastProvider');
|
|
11
|
+
return ctx;
|
|
12
|
+
};
|
|
13
|
+
export const ToastProvider = ({ children }) => {
|
|
14
|
+
const [toasts, setToasts] = useState([]);
|
|
15
|
+
const timers = useRef({});
|
|
16
|
+
const toastRefs = useRef({});
|
|
17
|
+
const removeToast = useCallback((id) => {
|
|
18
|
+
setToasts(s => s.filter(t => t.id !== id));
|
|
19
|
+
const timer = timers.current[id];
|
|
20
|
+
if (timer) {
|
|
21
|
+
clearTimeout(timer);
|
|
22
|
+
delete timers.current[id];
|
|
23
|
+
}
|
|
24
|
+
delete toastRefs.current[id];
|
|
25
|
+
}, []);
|
|
26
|
+
const addToast = useCallback((opts) => {
|
|
27
|
+
const id = opts.id ?? `${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
|
|
28
|
+
const toast = {
|
|
29
|
+
id,
|
|
30
|
+
text: opts.text,
|
|
31
|
+
actionText: opts.actionText,
|
|
32
|
+
onPress: opts.onPress,
|
|
33
|
+
onDismiss: opts.onDismiss,
|
|
34
|
+
icon: opts.icon,
|
|
35
|
+
duration: opts.duration ?? 6000,
|
|
36
|
+
showDismissIcon: opts.showDismissIcon,
|
|
37
|
+
dismissOnPress: opts.dismissOnPress ?? true,
|
|
38
|
+
};
|
|
39
|
+
setToasts(s => [toast, ...s]);
|
|
40
|
+
// set auto-dismiss timer
|
|
41
|
+
if (toast.duration && toast.duration > 0) {
|
|
42
|
+
const t = setTimeout(() => {
|
|
43
|
+
// call dismiss animation if ref exists, otherwise remove immediately
|
|
44
|
+
const ref = toastRefs.current[id];
|
|
45
|
+
if (ref) {
|
|
46
|
+
ref.dismiss();
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
removeToast(id);
|
|
50
|
+
}
|
|
51
|
+
}, toast.duration);
|
|
52
|
+
timers.current[id] = t;
|
|
53
|
+
}
|
|
54
|
+
return id;
|
|
55
|
+
}, [removeToast]);
|
|
56
|
+
useEffect(() => {
|
|
57
|
+
return () => {
|
|
58
|
+
// cleanup timers on unmount
|
|
59
|
+
Object.values(timers.current).forEach(t => clearTimeout(t));
|
|
60
|
+
timers.current = {};
|
|
61
|
+
};
|
|
62
|
+
}, []);
|
|
63
|
+
return (_jsxs(ToastContext.Provider, { value: { addToast, removeToast }, children: [children, _jsx(View, { pointerEvents: "box-none", style: styles.container, children: _jsx(View, { style: styles.stack, children: toasts.map(t => (_jsx(ToastItem, { ref: el => {
|
|
64
|
+
toastRefs.current[t.id] = el;
|
|
65
|
+
}, toast: t, onClose: removeToast }, t.id))) }) })] }));
|
|
66
|
+
};
|
|
67
|
+
export const useToast = () => {
|
|
68
|
+
const ctx = useContext(ToastContext);
|
|
69
|
+
if (!ctx)
|
|
70
|
+
throw new Error('useToast must be used within ToastProvider');
|
|
71
|
+
return ctx;
|
|
72
|
+
};
|
|
73
|
+
export default ToastContext;
|
|
74
|
+
const styles = StyleSheet.create(theme => ({
|
|
75
|
+
container: {
|
|
76
|
+
position: 'absolute',
|
|
77
|
+
left: 0,
|
|
78
|
+
right: 0,
|
|
79
|
+
bottom: 0,
|
|
80
|
+
alignItems: 'stretch',
|
|
81
|
+
paddingBottom: theme.space['200'],
|
|
82
|
+
pointerEvents: 'box-none',
|
|
83
|
+
},
|
|
84
|
+
stack: {
|
|
85
|
+
width: '100%',
|
|
86
|
+
alignItems: 'center',
|
|
87
|
+
justifyContent: 'flex-end',
|
|
88
|
+
gap: theme.components.toast.stack.gap,
|
|
89
|
+
},
|
|
90
|
+
}));
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
export interface ToastOptions {
|
|
3
|
+
id?: string;
|
|
4
|
+
text: string | ReactNode;
|
|
5
|
+
/** Optional action text to display as a link */
|
|
6
|
+
actionText?: string;
|
|
7
|
+
/** Optional callback when action link or toast is pressed */
|
|
8
|
+
onPress?: () => void;
|
|
9
|
+
/** Optional callback when toast is dismissed */
|
|
10
|
+
onDismiss?: () => void;
|
|
11
|
+
/** Optional icon component */
|
|
12
|
+
icon?: React.ComponentType;
|
|
13
|
+
/** Duration in milliseconds; default 6000 */
|
|
14
|
+
duration?: number;
|
|
15
|
+
/** Whether to show the dismiss icon button; default true */
|
|
16
|
+
showDismissIcon?: boolean;
|
|
17
|
+
/** Whether to dismiss the toast when pressed; default true */
|
|
18
|
+
dismissOnPress?: boolean;
|
|
19
|
+
}
|
|
20
|
+
export interface ToastInstance extends ToastOptions {
|
|
21
|
+
id: string;
|
|
22
|
+
/** resolved duration */
|
|
23
|
+
duration: number;
|
|
24
|
+
}
|
|
25
|
+
export interface ToastContextValue {
|
|
26
|
+
addToast: (opts: ToastOptions) => string;
|
|
27
|
+
removeToast: (id: string) => void;
|
|
28
|
+
}
|
|
29
|
+
export default ToastOptions;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { ToastInstance } from './Toast.props';
|
|
2
|
+
export interface ToastItemHandle {
|
|
3
|
+
dismiss: () => void;
|
|
4
|
+
}
|
|
5
|
+
interface Props {
|
|
6
|
+
toast: ToastInstance;
|
|
7
|
+
onClose: (id: string) => void;
|
|
8
|
+
}
|
|
9
|
+
declare const ToastItem: import("react").ForwardRefExoticComponent<Props & import("react").RefAttributes<ToastItemHandle>>;
|
|
10
|
+
export default ToastItem;
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { CloseSmallIcon } from '@utilitywarehouse/hearth-react-native-icons';
|
|
3
|
+
import { forwardRef, useEffect, useImperativeHandle } from 'react';
|
|
4
|
+
import { AccessibilityInfo, Platform, Pressable, View } from 'react-native';
|
|
5
|
+
import { Gesture, GestureDetector } from 'react-native-gesture-handler';
|
|
6
|
+
import Animated, { useAnimatedStyle, useSharedValue, withSpring, withTiming, } from 'react-native-reanimated';
|
|
7
|
+
import { StyleSheet, withUnistyles } from 'react-native-unistyles';
|
|
8
|
+
import { scheduleOnRN } from 'react-native-worklets';
|
|
9
|
+
import { BodyText } from '../BodyText';
|
|
10
|
+
import { Icon } from '../Icon';
|
|
11
|
+
import { Link } from '../Link';
|
|
12
|
+
import { UnstyledIconButton } from '../UnstyledIconButton';
|
|
13
|
+
const AnimatedView = Platform.OS === 'web' ? withUnistyles(Animated.View) : Animated.View;
|
|
14
|
+
const ToastItem = forwardRef(({ toast, onClose }, ref) => {
|
|
15
|
+
const translateY = useSharedValue(30);
|
|
16
|
+
const opacity = useSharedValue(0);
|
|
17
|
+
const gestureTranslateY = useSharedValue(0);
|
|
18
|
+
useEffect(() => {
|
|
19
|
+
translateY.value = withTiming(0, { duration: 300 });
|
|
20
|
+
opacity.value = withTiming(1, { duration: 300 });
|
|
21
|
+
// Announce toast content to screen readers
|
|
22
|
+
// Use a slight delay to ensure iOS VoiceOver picks it up
|
|
23
|
+
const timer = setTimeout(() => {
|
|
24
|
+
const message = typeof toast.text === 'string' ? toast.text : 'Toast notification';
|
|
25
|
+
const announcement = toast.actionText ? `${message}, ${toast.actionText}` : message;
|
|
26
|
+
AccessibilityInfo.announceForAccessibility(announcement);
|
|
27
|
+
}, 100);
|
|
28
|
+
return () => clearTimeout(timer);
|
|
29
|
+
}, [toast.text, toast.actionText]);
|
|
30
|
+
const animatedStyle = useAnimatedStyle(() => ({
|
|
31
|
+
transform: [{ translateY: translateY.value + gestureTranslateY.value }],
|
|
32
|
+
opacity: opacity.value,
|
|
33
|
+
}));
|
|
34
|
+
const handleDismiss = (fromGesture = false) => {
|
|
35
|
+
'worklet';
|
|
36
|
+
// Call onDismiss callback if provided
|
|
37
|
+
if (toast.onDismiss) {
|
|
38
|
+
scheduleOnRN(toast.onDismiss);
|
|
39
|
+
}
|
|
40
|
+
// animate out then call onClose
|
|
41
|
+
if (!fromGesture) {
|
|
42
|
+
gestureTranslateY.value = 0;
|
|
43
|
+
}
|
|
44
|
+
// Continue from current position and animate further down
|
|
45
|
+
translateY.value = withTiming(100, { duration: 250 });
|
|
46
|
+
opacity.value = withTiming(0, { duration: 250 }, finished => {
|
|
47
|
+
if (finished)
|
|
48
|
+
scheduleOnRN(onClose, toast.id);
|
|
49
|
+
});
|
|
50
|
+
};
|
|
51
|
+
useImperativeHandle(ref, () => ({
|
|
52
|
+
dismiss: handleDismiss,
|
|
53
|
+
}));
|
|
54
|
+
const panGesture = Gesture.Pan()
|
|
55
|
+
.onUpdate(event => {
|
|
56
|
+
// only allow downward drag
|
|
57
|
+
if (event.translationY > 0) {
|
|
58
|
+
gestureTranslateY.value = event.translationY;
|
|
59
|
+
}
|
|
60
|
+
})
|
|
61
|
+
.onEnd(event => {
|
|
62
|
+
if (event.translationY > 30 || event.velocityY > 800) {
|
|
63
|
+
handleDismiss(true);
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
// spring back to original position
|
|
67
|
+
gestureTranslateY.value = withSpring(0, {
|
|
68
|
+
damping: 20,
|
|
69
|
+
stiffness: 300,
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
const IconComp = toast.icon;
|
|
74
|
+
const showDismissIcon = toast.showDismissIcon !== false; // default true
|
|
75
|
+
const dismissOnPress = toast.dismissOnPress === true; // default false
|
|
76
|
+
const handlePress = () => {
|
|
77
|
+
if (toast.onPress) {
|
|
78
|
+
toast.onPress();
|
|
79
|
+
if (dismissOnPress) {
|
|
80
|
+
handleDismiss(false);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
const toastContent = (_jsxs(View, { style: styles.inner, children: [_jsxs(View, { style: styles.content, children: [IconComp ? (_jsx(View, { style: styles.iconWrap, children: _jsx(Icon, { as: IconComp, style: styles.icon }) })) : null, _jsx(BodyText, { inverted: true, children: toast.text })] }), toast.actionText ? (_jsx(Link, { onPress: handlePress, showIcon: false, inverted: true, children: toast.actionText })) : null, showDismissIcon ? (_jsx(View, { style: styles.actions, children: _jsx(UnstyledIconButton, { icon: CloseSmallIcon, accessibilityLabel: "Dismiss", inverted: true, onPress: () => handleDismiss(false) }) })) : null] }));
|
|
85
|
+
return (_jsx(GestureDetector, { gesture: panGesture, children: _jsx(AnimatedView, { style: [styles.toast, animatedStyle], ...(Platform.OS === 'ios' && {
|
|
86
|
+
accessible: true,
|
|
87
|
+
accessibilityRole: 'alert',
|
|
88
|
+
accessibilityLiveRegion: 'polite',
|
|
89
|
+
}), importantForAccessibility: Platform.OS === 'android' ? 'no-hide-descendants' : undefined, children: toast.onPress ? (_jsx(Pressable, { onPress: handlePress, style: styles.pressable, children: toastContent })) : (toastContent) }) }));
|
|
90
|
+
});
|
|
91
|
+
ToastItem.displayName = 'ToastItem';
|
|
92
|
+
const styles = StyleSheet.create(theme => ({
|
|
93
|
+
toast: {
|
|
94
|
+
backgroundColor: theme.components.toast.backgroundColor,
|
|
95
|
+
borderRadius: theme.components.toast.borderRadius,
|
|
96
|
+
padding: theme.components.toast.padding,
|
|
97
|
+
width: '95%',
|
|
98
|
+
},
|
|
99
|
+
pressable: {
|
|
100
|
+
width: '100%',
|
|
101
|
+
},
|
|
102
|
+
inner: {
|
|
103
|
+
flexDirection: 'row',
|
|
104
|
+
alignItems: 'center',
|
|
105
|
+
width: '100%',
|
|
106
|
+
gap: theme.components.toast.gap,
|
|
107
|
+
},
|
|
108
|
+
iconWrap: {
|
|
109
|
+
width: 24,
|
|
110
|
+
height: 24,
|
|
111
|
+
justifyContent: 'center',
|
|
112
|
+
alignItems: 'center',
|
|
113
|
+
flexShrink: 0,
|
|
114
|
+
},
|
|
115
|
+
icon: {
|
|
116
|
+
color: theme.color.icon.inverted,
|
|
117
|
+
},
|
|
118
|
+
content: {
|
|
119
|
+
flex: 1,
|
|
120
|
+
gap: theme.components.toast.text.gap,
|
|
121
|
+
flexDirection: 'row',
|
|
122
|
+
alignItems: 'center',
|
|
123
|
+
minWidth: 0,
|
|
124
|
+
},
|
|
125
|
+
actions: {
|
|
126
|
+
flexShrink: 0,
|
|
127
|
+
},
|
|
128
|
+
}));
|
|
129
|
+
export default ToastItem;
|
|
@@ -37,6 +37,7 @@ export * from './Link';
|
|
|
37
37
|
export * from './List';
|
|
38
38
|
export * from './Menu';
|
|
39
39
|
export * from './Modal';
|
|
40
|
+
export * from './PillGroup';
|
|
40
41
|
export * from './ProgressStepper';
|
|
41
42
|
export * from './Radio';
|
|
42
43
|
export * from './RadioCard';
|
|
@@ -48,6 +49,7 @@ export * from './Switch';
|
|
|
48
49
|
export * from './Tabs';
|
|
49
50
|
export * from './Textarea';
|
|
50
51
|
export * from './ThemedImage';
|
|
52
|
+
export * from './Toast';
|
|
51
53
|
export * from './ToggleButtonCard';
|
|
52
54
|
export { FlatList, Image, KeyboardAvoidingView, ScrollView, SectionList, View } from 'react-native';
|
|
53
55
|
export { Pressable } from 'react-native';
|
|
@@ -38,6 +38,7 @@ export * from './Link';
|
|
|
38
38
|
export * from './List';
|
|
39
39
|
export * from './Menu';
|
|
40
40
|
export * from './Modal';
|
|
41
|
+
export * from './PillGroup';
|
|
41
42
|
export * from './ProgressStepper';
|
|
42
43
|
export * from './Radio';
|
|
43
44
|
export * from './RadioCard';
|
|
@@ -49,6 +50,7 @@ export * from './Switch';
|
|
|
49
50
|
export * from './Tabs';
|
|
50
51
|
export * from './Textarea';
|
|
51
52
|
export * from './ThemedImage';
|
|
53
|
+
export * from './Toast';
|
|
52
54
|
export * from './ToggleButtonCard';
|
|
53
55
|
export { FlatList, Image, KeyboardAvoidingView, ScrollView, SectionList, View } from 'react-native';
|
|
54
56
|
export { Pressable } from 'react-native';
|