react-native-molecules 0.5.0-beta.3 → 0.5.0-beta.31
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/components/Accordion/Accordion.tsx +2 -6
- package/components/Accordion/AccordionItem.tsx +16 -12
- package/components/Accordion/AccordionItemContent.tsx +6 -1
- package/components/Accordion/AccordionItemHeader.tsx +1 -1
- package/components/Accordion/utils.ts +6 -0
- package/components/ActivityIndicator/ActivityIndicator.tsx +6 -15
- package/components/Appbar/AppbarBase.tsx +18 -13
- package/components/Button/Button.tsx +211 -264
- package/components/Button/index.tsx +9 -3
- package/components/Button/types.ts +16 -2
- package/components/Button/utils.ts +230 -208
- package/components/Card/Card.tsx +1 -1
- package/components/Checkbox/Checkbox.tsx +125 -88
- package/components/Checkbox/CheckboxBase.ios.tsx +14 -23
- package/components/Checkbox/CheckboxBase.tsx +21 -137
- package/components/Checkbox/context.tsx +14 -0
- package/components/Checkbox/index.tsx +11 -4
- package/components/Checkbox/types.ts +63 -29
- package/components/Checkbox/utils.ts +25 -108
- package/components/Chip/Chip.tsx +41 -52
- package/components/Chip/utils.ts +3 -7
- package/components/DateField/DateField.tsx +111 -0
- package/components/DateField/index.tsx +6 -0
- package/components/{DatePickerInput/inputUtils.ts → DateField/useDateFieldState.ts} +19 -51
- package/components/DatePicker/DateCalendar.tsx +83 -0
- package/components/DatePicker/DatePickerActions.tsx +73 -0
- package/components/DatePicker/DatePickerModal.tsx +246 -0
- package/components/DatePicker/DatePickerPopover.tsx +79 -0
- package/components/DatePicker/DatePickerProvider.tsx +158 -0
- package/components/DatePicker/DatePickerTrigger.tsx +23 -0
- package/components/DatePicker/context.tsx +83 -0
- package/components/DatePicker/index.tsx +45 -0
- package/components/DatePicker/utils.ts +295 -0
- package/components/DatePickerInline/DatePickerDockedHeader.tsx +117 -0
- package/components/DatePickerInline/DatePickerInline.tsx +17 -16
- package/components/DatePickerInline/DatePickerInlineBase.tsx +11 -5
- package/components/DatePickerInline/DatePickerInlineHeader.tsx +50 -20
- package/components/DatePickerInline/Day.tsx +25 -1
- package/components/DatePickerInline/DayNames.tsx +13 -10
- package/components/DatePickerInline/DayRange.tsx +2 -4
- package/components/DatePickerInline/HeaderItem.tsx +44 -29
- package/components/DatePickerInline/Month.tsx +48 -67
- package/components/DatePickerInline/MonthPicker.tsx +80 -92
- package/components/DatePickerInline/Swiper.native.tsx +21 -4
- package/components/DatePickerInline/Swiper.tsx +169 -14
- package/components/DatePickerInline/SwiperUtils.ts +1 -1
- package/components/DatePickerInline/Week.tsx +6 -1
- package/components/DatePickerInline/YearPicker.tsx +220 -78
- package/components/DatePickerInline/dateUtils.tsx +18 -13
- package/components/DatePickerInline/store.tsx +27 -0
- package/components/DatePickerInline/types.ts +6 -2
- package/components/DatePickerInline/utils.ts +66 -29
- package/components/Divider/Divider.tsx +192 -0
- package/components/Divider/index.tsx +10 -0
- package/components/Drawer/Drawer.tsx +17 -6
- package/components/Drawer/DrawerItemGroup.tsx +3 -7
- package/components/ElementGroup/ElementGroup.tsx +1 -1
- package/components/FilePicker/FilePicker.tsx +48 -78
- package/components/FilePicker/index.tsx +2 -1
- package/components/FilePicker/utils.ts +9 -0
- package/components/HelperText/HelperText.tsx +0 -35
- package/components/Icon/iconFactory.tsx +5 -4
- package/components/Icon/index.tsx +1 -1
- package/components/Icon/types.ts +17 -6
- package/components/IconButton/IconButton.tsx +84 -84
- package/components/IconButton/index.tsx +1 -0
- package/components/IconButton/types.ts +10 -0
- package/components/IconButton/utils.ts +167 -33
- package/components/List/List.tsx +276 -0
- package/components/List/context.tsx +27 -0
- package/components/List/index.ts +8 -0
- package/components/List/types.ts +117 -0
- package/components/List/utils.ts +79 -0
- package/components/LoadingIndicator/LoadingIndicator.tsx +253 -0
- package/components/LoadingIndicator/LoadingIndicator.web.tsx +136 -0
- package/components/LoadingIndicator/index.tsx +13 -0
- package/components/LoadingIndicator/utils.ts +117 -0
- package/components/Menu/Menu.tsx +162 -39
- package/components/Menu/index.tsx +10 -7
- package/components/Menu/utils.ts +21 -70
- package/components/NavigationRail/NavigationRail.tsx +15 -9
- package/components/Popover/Popover.tsx +119 -145
- package/components/Popover/PopoverRoot.tsx +60 -0
- package/components/Popover/common.ts +54 -34
- package/components/Popover/index.ts +12 -1
- package/components/Popover/usePlatformMeasure.native.ts +90 -0
- package/components/Popover/usePlatformMeasure.ts +120 -0
- package/components/Popover/utils.ts +34 -0
- package/components/Portal/Portal.tsx +1 -2
- package/components/Radio/Radio.tsx +188 -0
- package/components/Radio/RadioBase.ios.tsx +69 -0
- package/components/Radio/RadioBase.tsx +136 -0
- package/components/Radio/context.tsx +23 -0
- package/components/Radio/index.tsx +20 -0
- package/components/Radio/types.ts +101 -0
- package/components/Radio/utils.ts +115 -0
- package/components/Rating/Rating.tsx +1 -1
- package/components/Select/Select.tsx +521 -785
- package/components/Select/context.tsx +81 -0
- package/components/Select/index.ts +26 -14
- package/components/Select/types.ts +65 -58
- package/components/Select/utils.ts +126 -0
- package/components/Slot/Slot.tsx +224 -0
- package/components/Slot/compose-refs.tsx +62 -0
- package/components/Slot/index.tsx +8 -0
- package/components/Surface/Surface.android.tsx +32 -7
- package/components/Surface/Surface.ios.tsx +34 -29
- package/components/Surface/Surface.tsx +31 -4
- package/components/Surface/utils.ts +44 -6
- package/components/Switch/Switch.ios.tsx +1 -1
- package/components/Switch/Switch.tsx +10 -3
- package/components/Tabs/TabItem.tsx +35 -58
- package/components/Tabs/TabLabel.tsx +5 -9
- package/components/Tabs/Tabs.tsx +156 -150
- package/components/Tabs/utils.ts +15 -2
- package/components/Text/textFactory.tsx +17 -5
- package/components/TextInput/TextInput.tsx +663 -579
- package/components/TextInput/index.tsx +19 -3
- package/components/TextInput/types.ts +77 -28
- package/components/TextInput/utils.ts +235 -145
- package/components/TimeField/TimeField.tsx +75 -0
- package/components/TimeField/index.tsx +6 -0
- package/components/TimeField/useTimeFieldState.ts +70 -0
- package/components/{TimePickerField/sanitizeTime.ts → TimeField/utils.ts} +77 -10
- package/components/TimePicker/AnalogClock.tsx +1 -1
- package/components/TimePicker/TimeInput.tsx +87 -42
- package/components/TimePicker/TimeInputs.tsx +138 -50
- package/components/TimePicker/TimePicker.tsx +74 -11
- package/components/TimePicker/TimePickerModal.tsx +186 -0
- package/components/TimePicker/context.tsx +17 -0
- package/components/TimePicker/index.tsx +15 -3
- package/components/TimePicker/utils.ts +93 -4
- package/components/Tooltip/Tooltip.tsx +42 -67
- package/components/Tooltip/TooltipContent.tsx +32 -5
- package/components/Tooltip/TooltipTrigger.tsx +21 -24
- package/components/Tooltip/index.tsx +1 -1
- package/components/TouchableRipple/TouchableRipple.native.tsx +83 -16
- package/components/TouchableRipple/TouchableRipple.tsx +150 -102
- package/components/TouchableRipple/rippleFromForegroundColor.ts +21 -0
- package/hocs/index.tsx +1 -1
- package/hocs/withKeyboardAccessibility.tsx +2 -3
- package/hocs/withPortal.tsx +1 -1
- package/hooks/index.tsx +2 -12
- package/hooks/useActionState.tsx +19 -8
- package/hooks/useContrastColor.ts +1 -2
- package/hooks/useFilePicker.tsx +7 -17
- package/hooks/useHandleNumberFormat.tsx +2 -2
- package/hooks/useMediaQuery.tsx +1 -2
- package/package.json +95 -111
- package/shortcuts-manager/ShortcutsManager/ShortcutsManager.tsx +6 -3
- package/shortcuts-manager/ShortcutsManager/utils.tsx +1 -1
- package/shortcuts-manager/useSetScopes/useSetScopes.tsx +1 -1
- package/shortcuts-manager/useShortcut/useShortcut.tsx +1 -1
- package/styles/shadow.ts +2 -1
- package/styles/themes/LightTheme.tsx +1 -1
- package/utils/DocumentPicker/documentPicker.ts +78 -27
- package/utils/DocumentPicker/types.ts +0 -1
- package/utils/extractSubcomponents.ts +89 -0
- package/utils/extractTextStyles.ts +1 -2
- package/utils/formatNumberWithMask/formatNumberWithMask.ts +2 -1
- package/utils/index.ts +0 -3
- package/utils/normalizeToNumberString/normalizeToNumberString.ts +1 -1
- package/components/DatePickerDocked/DatePickerDocked.tsx +0 -30
- package/components/DatePickerDocked/DatePickerDockedHeader.tsx +0 -129
- package/components/DatePickerDocked/index.tsx +0 -17
- package/components/DatePickerDocked/types.ts +0 -11
- package/components/DatePickerDocked/utils.ts +0 -157
- package/components/DatePickerInline/DatePickerContext.tsx +0 -21
- package/components/DatePickerInput/DatePickerInput.tsx +0 -139
- package/components/DatePickerInput/DatePickerInputModal.tsx +0 -48
- package/components/DatePickerInput/DatePickerInputWithoutModal.tsx +0 -77
- package/components/DatePickerInput/DateRangeInput.tsx +0 -88
- package/components/DatePickerInput/index.tsx +0 -10
- package/components/DatePickerInput/types.ts +0 -28
- package/components/DatePickerInput/utils.ts +0 -15
- package/components/DatePickerModal/AnimatedCrossView.tsx +0 -94
- package/components/DatePickerModal/CalendarEdit.tsx +0 -139
- package/components/DatePickerModal/DatePickerModal.tsx +0 -85
- package/components/DatePickerModal/DatePickerModalContent.tsx +0 -155
- package/components/DatePickerModal/DatePickerModalContentHeader.tsx +0 -213
- package/components/DatePickerModal/DatePickerModalHeader.tsx +0 -74
- package/components/DatePickerModal/DatePickerModalHeaderBackground.tsx +0 -13
- package/components/DatePickerModal/index.tsx +0 -16
- package/components/DatePickerModal/types.ts +0 -92
- package/components/DatePickerModal/utils.ts +0 -122
- package/components/DateTimePicker/DateTimePicker.tsx +0 -172
- package/components/DateTimePicker/index.tsx +0 -10
- package/components/DateTimePicker/utils.ts +0 -12
- package/components/HorizontalDivider/HorizontalDivider.tsx +0 -103
- package/components/HorizontalDivider/index.tsx +0 -9
- package/components/ListItem/ListItem.tsx +0 -136
- package/components/ListItem/ListItemDescription.tsx +0 -25
- package/components/ListItem/ListItemTitle.tsx +0 -25
- package/components/ListItem/index.tsx +0 -14
- package/components/ListItem/utils.ts +0 -115
- package/components/Menu/MenuDivider.tsx +0 -13
- package/components/Menu/MenuItem.tsx +0 -128
- package/components/Popover/Popover.native.tsx +0 -185
- package/components/RadioButton/RadioButton.tsx +0 -138
- package/components/RadioButton/RadioButtonAndroid.tsx +0 -188
- package/components/RadioButton/RadioButtonGroup.tsx +0 -98
- package/components/RadioButton/RadioButtonIOS.tsx +0 -106
- package/components/RadioButton/RadioButtonItem.tsx +0 -232
- package/components/RadioButton/index.ts +0 -22
- package/components/RadioButton/utils.ts +0 -165
- package/components/TimePickerField/TimePickerField.tsx +0 -152
- package/components/TimePickerField/index.tsx +0 -10
- package/components/TimePickerField/utils.ts +0 -94
- package/components/TimePickerModal/TimePickerModal.tsx +0 -115
- package/components/TimePickerModal/index.tsx +0 -10
- package/components/TimePickerModal/utils.ts +0 -47
- package/components/VerticalDivider/VerticalDivider.tsx +0 -100
- package/components/VerticalDivider/index.tsx +0 -9
- package/context-bridge/index.tsx +0 -87
- package/fast-context/index.tsx +0 -190
- package/hocs/typedMemo.tsx +0 -5
- package/hooks/useControlledValue.tsx +0 -68
- package/hooks/useLatest.tsx +0 -9
- package/hooks/useMergedRefs.ts +0 -14
- package/hooks/usePrevious.ts +0 -13
- package/hooks/useSearchable.tsx +0 -74
- package/hooks/useSubcomponents.tsx +0 -59
- package/hooks/useToggle.tsx +0 -24
- package/utils/color.ts +0 -22
- package/utils/compare/index.ts +0 -54
- package/utils/lodash.ts +0 -49
- package/utils/repository.ts +0 -53
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import type { ReactNode, RefObject } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
type AccessibilityRole,
|
|
4
|
+
type GestureResponderEvent,
|
|
5
|
+
type ScrollViewProps,
|
|
6
|
+
type StyleProp,
|
|
7
|
+
type ViewStyle,
|
|
8
|
+
} from 'react-native';
|
|
9
|
+
|
|
10
|
+
import type { TouchableRippleProps } from '../TouchableRipple';
|
|
11
|
+
|
|
12
|
+
export type ListItemId = string | number;
|
|
13
|
+
|
|
14
|
+
export type DefaultListItemT = {
|
|
15
|
+
id?: ListItemId;
|
|
16
|
+
label?: string;
|
|
17
|
+
selectable?: boolean;
|
|
18
|
+
[key: string]: unknown;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export type ListValue<Multiple extends boolean = false> = Multiple extends true
|
|
22
|
+
? ListItemId[]
|
|
23
|
+
: ListItemId | null;
|
|
24
|
+
|
|
25
|
+
export type ListEmptyStateRender = (ctx: {
|
|
26
|
+
/** True when `items` (the raw input) has at least one entry. */
|
|
27
|
+
hasItems: boolean;
|
|
28
|
+
}) => ReactNode;
|
|
29
|
+
|
|
30
|
+
export type ListContextValue<Option extends object = DefaultListItemT> = {
|
|
31
|
+
value: ListItemId | ListItemId[] | null;
|
|
32
|
+
multiple: boolean;
|
|
33
|
+
onAdd: (item: Option) => void;
|
|
34
|
+
onRemove: (item: Option) => void;
|
|
35
|
+
isSelectedId: (id: ListItemId) => boolean;
|
|
36
|
+
disabled?: boolean;
|
|
37
|
+
error: boolean;
|
|
38
|
+
allowDeselect: boolean;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
type ListPropsBase = {
|
|
42
|
+
children: ReactNode;
|
|
43
|
+
disabled?: boolean;
|
|
44
|
+
error?: boolean;
|
|
45
|
+
/**
|
|
46
|
+
* Whether re-clicking the currently-selected row should remove it. Defaults
|
|
47
|
+
* to `true` for multiple, `false` for single (re-clicking the picked row
|
|
48
|
+
* in a "pick one and close" flow shouldn't clear the value).
|
|
49
|
+
*/
|
|
50
|
+
allowDeselect?: boolean;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
type SingleListProps<Option extends object = DefaultListItemT> = {
|
|
54
|
+
multiple?: false | undefined;
|
|
55
|
+
value?: ListValue<false>;
|
|
56
|
+
defaultValue?: ListValue<false>;
|
|
57
|
+
onChange?: (value: ListValue<false>, item: Option, event?: GestureResponderEvent) => void;
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
type MultipleListProps<Option extends object = DefaultListItemT> = {
|
|
61
|
+
multiple: true;
|
|
62
|
+
value?: ListValue<true>;
|
|
63
|
+
defaultValue?: ListValue<true>;
|
|
64
|
+
onChange?: (value: ListValue<true>, item: Option, event?: GestureResponderEvent) => void;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
export type ListProps<Option extends object = DefaultListItemT> = ListPropsBase &
|
|
68
|
+
(SingleListProps<Option> | MultipleListProps<Option>);
|
|
69
|
+
|
|
70
|
+
export type ListContentProps = Omit<ScrollViewProps, 'children'> & {
|
|
71
|
+
children?: ReactNode;
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Props for `<List.Item>`. When `value` is provided, the item participates in the
|
|
76
|
+
* surrounding `<List>` context — it derives its `selected` state from the context's
|
|
77
|
+
* value and toggles selection on press (unless `shouldToggleOnPress` is false).
|
|
78
|
+
*
|
|
79
|
+
* Without `value`, the item is a plain styled row (use it for menu-style entries
|
|
80
|
+
* that don't represent a selectable option).
|
|
81
|
+
*
|
|
82
|
+
* Note: when `value` is set, both `onPress` and the selection toggle fire on press,
|
|
83
|
+
* in that order. For most cases that's fine — pass `onPress` for side effects
|
|
84
|
+
* (e.g. closing a menu) and let the toggle drive `onChange`. Pass
|
|
85
|
+
* `onBeforeToggle` for side effects that should only run when the built-in
|
|
86
|
+
* toggle will happen. Set `shouldToggleOnPress={false}` to suppress the toggle entirely.
|
|
87
|
+
*
|
|
88
|
+
* Deselection: by default, single-select rows do **not** deselect on re-click
|
|
89
|
+
* (use `<List allowDeselect>` to opt in or out at the list level).
|
|
90
|
+
*/
|
|
91
|
+
export type ListItemProps<Option extends object = DefaultListItemT> = Omit<
|
|
92
|
+
TouchableRippleProps,
|
|
93
|
+
'children' | 'onPress'
|
|
94
|
+
> & {
|
|
95
|
+
ref?: RefObject<unknown>;
|
|
96
|
+
children?: ReactNode;
|
|
97
|
+
value?: ListItemId;
|
|
98
|
+
style?: StyleProp<ViewStyle>;
|
|
99
|
+
variant?: 'default' | 'menuItem';
|
|
100
|
+
selected?: boolean;
|
|
101
|
+
disabled?: boolean;
|
|
102
|
+
hovered?: boolean;
|
|
103
|
+
hoverable?: boolean;
|
|
104
|
+
shouldToggleOnPress?: boolean;
|
|
105
|
+
/** Runs after `onPress`, before the built-in selection toggle. */
|
|
106
|
+
onBeforeToggle?: (event: GestureResponderEvent) => void;
|
|
107
|
+
onPress?: (event: GestureResponderEvent) => void;
|
|
108
|
+
accessibilityRole?: AccessibilityRole;
|
|
109
|
+
accessibilityState?: Record<string, unknown>;
|
|
110
|
+
/** Reserved for generic item shape; not consumed directly. */
|
|
111
|
+
__optionType?: Option;
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
export type ListItemElementProps = {
|
|
115
|
+
children?: ReactNode;
|
|
116
|
+
style?: StyleProp<ViewStyle>;
|
|
117
|
+
};
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { StyleSheet } from 'react-native-unistyles';
|
|
2
|
+
|
|
3
|
+
import { getRegisteredComponentStylesWithFallback } from '../../core';
|
|
4
|
+
|
|
5
|
+
const defaultStyles = StyleSheet.create(theme => ({
|
|
6
|
+
emptyState: {
|
|
7
|
+
paddingHorizontal: theme.spacings['4'],
|
|
8
|
+
paddingVertical: theme.spacings['6'],
|
|
9
|
+
alignItems: 'center',
|
|
10
|
+
justifyContent: 'center',
|
|
11
|
+
},
|
|
12
|
+
emptyStateText: {
|
|
13
|
+
color: theme.colors.onSurfaceVariant,
|
|
14
|
+
fontSize: 14,
|
|
15
|
+
},
|
|
16
|
+
}));
|
|
17
|
+
|
|
18
|
+
export const listStyles = getRegisteredComponentStylesWithFallback('List', defaultStyles);
|
|
19
|
+
|
|
20
|
+
const listItemStylesDefault = StyleSheet.create(theme => ({
|
|
21
|
+
root: {
|
|
22
|
+
backgroundColor: theme.colors.surface,
|
|
23
|
+
flexDirection: 'row',
|
|
24
|
+
alignItems: 'center',
|
|
25
|
+
gap: theme.spacings['4'],
|
|
26
|
+
|
|
27
|
+
_web: {
|
|
28
|
+
outlineStyle: 'none',
|
|
29
|
+
},
|
|
30
|
+
|
|
31
|
+
variants: {
|
|
32
|
+
state: {
|
|
33
|
+
disabled: {
|
|
34
|
+
opacity: 0.38,
|
|
35
|
+
},
|
|
36
|
+
hovered: {},
|
|
37
|
+
focused: {},
|
|
38
|
+
|
|
39
|
+
selected: {
|
|
40
|
+
backgroundColor: theme.colors.surfaceVariant,
|
|
41
|
+
},
|
|
42
|
+
selectedAndFocused: {
|
|
43
|
+
backgroundColor: theme.colors.surfaceVariant,
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
variant: {
|
|
47
|
+
default: {
|
|
48
|
+
paddingLeft: theme.spacings['4'],
|
|
49
|
+
paddingRight: theme.spacings['6'],
|
|
50
|
+
minHeight: 56,
|
|
51
|
+
},
|
|
52
|
+
menuItem: {
|
|
53
|
+
paddingHorizontal: theme.spacings['3'],
|
|
54
|
+
minHeight: 40,
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
stateLayer: {
|
|
60
|
+
variants: {
|
|
61
|
+
state: {
|
|
62
|
+
hovered: {
|
|
63
|
+
backgroundColor: theme.colors.stateLayer.hover.onSurface,
|
|
64
|
+
},
|
|
65
|
+
focused: {
|
|
66
|
+
backgroundColor: theme.colors.stateLayer.hover.onSurface,
|
|
67
|
+
},
|
|
68
|
+
selectedAndFocused: {
|
|
69
|
+
backgroundColor: theme.colors.stateLayer.focussed.onSurface,
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
}));
|
|
75
|
+
|
|
76
|
+
export const listItemStyles = getRegisteredComponentStylesWithFallback(
|
|
77
|
+
'List_Item',
|
|
78
|
+
listItemStylesDefault,
|
|
79
|
+
);
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
import { interpolate as flubberInterpolate } from 'flubber';
|
|
2
|
+
import { memo, useCallback, useEffect, useState } from 'react';
|
|
3
|
+
import { View } from 'react-native';
|
|
4
|
+
import Animated, {
|
|
5
|
+
cancelAnimation,
|
|
6
|
+
Easing,
|
|
7
|
+
useAnimatedStyle,
|
|
8
|
+
useDerivedValue,
|
|
9
|
+
useFrameCallback,
|
|
10
|
+
useSharedValue,
|
|
11
|
+
withRepeat,
|
|
12
|
+
withTiming,
|
|
13
|
+
} from 'react-native-reanimated';
|
|
14
|
+
import Svg, { Path } from 'react-native-svg';
|
|
15
|
+
|
|
16
|
+
import {
|
|
17
|
+
cookie4Path,
|
|
18
|
+
cookie9Path,
|
|
19
|
+
loadingIndicatorStyles as componentStyles,
|
|
20
|
+
ovalPath,
|
|
21
|
+
pentagonPath,
|
|
22
|
+
pillPath,
|
|
23
|
+
type Props,
|
|
24
|
+
softBurstPath,
|
|
25
|
+
sunnyPath,
|
|
26
|
+
useProcessProps,
|
|
27
|
+
} from './utils';
|
|
28
|
+
|
|
29
|
+
// Animation constants matching the web version
|
|
30
|
+
const ANIMATION_DURATION = 4550; // 4.55 seconds total cycle
|
|
31
|
+
const PULSE_DURATION = 2275; // 2.275 seconds for pulse
|
|
32
|
+
|
|
33
|
+
const SHAPE_PATHS = [
|
|
34
|
+
softBurstPath,
|
|
35
|
+
cookie9Path,
|
|
36
|
+
pentagonPath,
|
|
37
|
+
pillPath,
|
|
38
|
+
sunnyPath,
|
|
39
|
+
cookie4Path,
|
|
40
|
+
ovalPath,
|
|
41
|
+
];
|
|
42
|
+
|
|
43
|
+
// Number of pre-computed frames per transition for smooth animation
|
|
44
|
+
const FRAMES_PER_TRANSITION = 30;
|
|
45
|
+
const TOTAL_FRAMES = SHAPE_PATHS.length * FRAMES_PER_TRANSITION;
|
|
46
|
+
|
|
47
|
+
// Material 3 Express Fast Spatial easing function
|
|
48
|
+
// Original: cubic-bezier(0.42, 1.67, 0.21, 0.90)
|
|
49
|
+
const expressFastSpatialEase = (t: number): number => {
|
|
50
|
+
'worklet';
|
|
51
|
+
// Approximate cubic-bezier(0.42, 1.67, 0.21, 0.90) using a simplified function
|
|
52
|
+
// This creates the characteristic overshoot and fast settle
|
|
53
|
+
const p1x = 0.42,
|
|
54
|
+
p1y = 1.67,
|
|
55
|
+
p2x = 0.21,
|
|
56
|
+
p2y = 0.9;
|
|
57
|
+
|
|
58
|
+
// Simple cubic bezier approximation
|
|
59
|
+
const cx = 3.0 * p1x;
|
|
60
|
+
const bx = 3.0 * (p2x - p1x) - cx;
|
|
61
|
+
const ax = 1.0 - cx - bx;
|
|
62
|
+
|
|
63
|
+
const cy = 3.0 * p1y;
|
|
64
|
+
const by = 3.0 * (p2y - p1y) - cy;
|
|
65
|
+
const ay = 1.0 - cy - by;
|
|
66
|
+
|
|
67
|
+
// Sample the bezier curve
|
|
68
|
+
const sampleX = (x: number) => ((ax * x + bx) * x + cx) * x;
|
|
69
|
+
const sampleY = (x: number) => ((ay * x + by) * x + cy) * x;
|
|
70
|
+
|
|
71
|
+
// Newton-Raphson to find t for given x
|
|
72
|
+
let guess = t;
|
|
73
|
+
for (let i = 0; i < 4; i++) {
|
|
74
|
+
const currentX = sampleX(guess) - t;
|
|
75
|
+
const currentSlope = (3.0 * ax * guess + 2.0 * bx) * guess + cx;
|
|
76
|
+
if (Math.abs(currentSlope) < 1e-6) break;
|
|
77
|
+
guess -= currentX / currentSlope;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return sampleY(guess);
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Pre-compute all animation frames at initialization for smooth playback.
|
|
85
|
+
* This avoids runtime flubber calls which can cause jank.
|
|
86
|
+
*/
|
|
87
|
+
const precomputeFrames = (): string[] => {
|
|
88
|
+
const frames: string[] = [];
|
|
89
|
+
|
|
90
|
+
for (let i = 0; i < SHAPE_PATHS.length; i++) {
|
|
91
|
+
const fromPath = SHAPE_PATHS[i];
|
|
92
|
+
const toPath = SHAPE_PATHS[(i + 1) % SHAPE_PATHS.length];
|
|
93
|
+
|
|
94
|
+
// Create interpolator with higher precision
|
|
95
|
+
const interpolator = flubberInterpolate(fromPath, toPath, {
|
|
96
|
+
maxSegmentLength: 1, // Higher precision for smoother morphing
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
for (let j = 0; j < FRAMES_PER_TRANSITION; j++) {
|
|
100
|
+
const t = j / FRAMES_PER_TRANSITION;
|
|
101
|
+
// Apply easing to match CSS animation
|
|
102
|
+
const easedT = expressFastSpatialEase(t);
|
|
103
|
+
frames.push(interpolator(Math.max(0, Math.min(1, easedT))));
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return frames;
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
const frames = precomputeFrames();
|
|
111
|
+
|
|
112
|
+
const LoadingIndicator = ({
|
|
113
|
+
animating = true,
|
|
114
|
+
color: colorProp,
|
|
115
|
+
size: sizeProp = 'md',
|
|
116
|
+
style,
|
|
117
|
+
variant = 'default',
|
|
118
|
+
innerContainerProps,
|
|
119
|
+
...rest
|
|
120
|
+
}: Props) => {
|
|
121
|
+
const [currentPath, setCurrentPath] = useState(frames[0]);
|
|
122
|
+
|
|
123
|
+
const progress = useSharedValue(0);
|
|
124
|
+
const pulseScale = useSharedValue(1);
|
|
125
|
+
// Track last frame to avoid redundant updates
|
|
126
|
+
const lastFrameRef = useSharedValue(-1);
|
|
127
|
+
|
|
128
|
+
componentStyles.useVariants({
|
|
129
|
+
variant: variant as 'contained',
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
const { size, strokeColor } = useProcessProps({
|
|
133
|
+
variant,
|
|
134
|
+
size: sizeProp,
|
|
135
|
+
color: colorProp,
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
const updatePathFromFrame = useCallback((frameIndex: number) => {
|
|
139
|
+
const safeIndex = Math.max(0, Math.min(frameIndex, frames.length - 1));
|
|
140
|
+
setCurrentPath(prevPath => {
|
|
141
|
+
const newPath = frames[safeIndex];
|
|
142
|
+
return prevPath === newPath ? prevPath : newPath;
|
|
143
|
+
});
|
|
144
|
+
}, []);
|
|
145
|
+
|
|
146
|
+
useFrameCallback(() => {
|
|
147
|
+
'worklet';
|
|
148
|
+
const frameIndex = Math.floor(progress.value * TOTAL_FRAMES) % TOTAL_FRAMES;
|
|
149
|
+
if (frameIndex !== lastFrameRef.value) {
|
|
150
|
+
lastFrameRef.value = frameIndex;
|
|
151
|
+
updatePathFromFrame(frameIndex);
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
useEffect(() => {
|
|
156
|
+
if (animating) {
|
|
157
|
+
// Main morphing and rotation animation
|
|
158
|
+
progress.value = 0;
|
|
159
|
+
progress.value = withRepeat(
|
|
160
|
+
withTiming(1, {
|
|
161
|
+
duration: ANIMATION_DURATION,
|
|
162
|
+
easing: Easing.linear,
|
|
163
|
+
}),
|
|
164
|
+
-1, // Infinite repeat
|
|
165
|
+
false, // Don't reverse
|
|
166
|
+
);
|
|
167
|
+
|
|
168
|
+
// Pulse animation
|
|
169
|
+
pulseScale.value = 1;
|
|
170
|
+
pulseScale.value = withRepeat(
|
|
171
|
+
withTiming(1.1, {
|
|
172
|
+
duration: PULSE_DURATION,
|
|
173
|
+
easing: Easing.inOut(Easing.ease),
|
|
174
|
+
}),
|
|
175
|
+
-1, // Infinite repeat
|
|
176
|
+
true, // Reverse (ping-pong effect)
|
|
177
|
+
);
|
|
178
|
+
} else {
|
|
179
|
+
cancelAnimation(progress);
|
|
180
|
+
cancelAnimation(pulseScale);
|
|
181
|
+
progress.value = 0;
|
|
182
|
+
pulseScale.value = 1;
|
|
183
|
+
updatePathFromFrame(0);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return () => {
|
|
187
|
+
cancelAnimation(progress);
|
|
188
|
+
cancelAnimation(pulseScale);
|
|
189
|
+
};
|
|
190
|
+
}, [animating, progress, pulseScale, updatePathFromFrame]);
|
|
191
|
+
|
|
192
|
+
// Derived value for rotation with per-segment easing (matches CSS animation-timing-function per keyframe)
|
|
193
|
+
const rotation = useDerivedValue(() => {
|
|
194
|
+
'worklet';
|
|
195
|
+
const p = progress.value;
|
|
196
|
+
const segmentCount = SHAPE_PATHS.length;
|
|
197
|
+
|
|
198
|
+
// Determine which segment we're in
|
|
199
|
+
const scaledProgress = p * segmentCount;
|
|
200
|
+
const segmentIndex = Math.min(Math.floor(scaledProgress), segmentCount - 1);
|
|
201
|
+
const segmentProgress = scaledProgress - segmentIndex;
|
|
202
|
+
|
|
203
|
+
// Apply easing to this segment's progress
|
|
204
|
+
const easedSegmentProgress = expressFastSpatialEase(segmentProgress);
|
|
205
|
+
|
|
206
|
+
// Calculate rotation: base rotation for completed segments + eased progress within current segment
|
|
207
|
+
const rotationPerSegment = 1080 / segmentCount; // ~154.29 degrees per segment
|
|
208
|
+
const baseRotation = segmentIndex * rotationPerSegment;
|
|
209
|
+
const segmentRotation = easedSegmentProgress * rotationPerSegment;
|
|
210
|
+
|
|
211
|
+
return baseRotation + segmentRotation;
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
const animatedStyle = useAnimatedStyle(() => {
|
|
215
|
+
return {
|
|
216
|
+
transform: [{ scale: pulseScale.value }, { rotate: `${rotation.value}deg` }],
|
|
217
|
+
};
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
if (!animating) return null;
|
|
221
|
+
|
|
222
|
+
return (
|
|
223
|
+
<View
|
|
224
|
+
style={[
|
|
225
|
+
componentStyles.container,
|
|
226
|
+
{
|
|
227
|
+
width: Math.floor((10 / 48) * size * 2) + size,
|
|
228
|
+
height: Math.floor((10 / 48) * size * 2) + size,
|
|
229
|
+
},
|
|
230
|
+
style,
|
|
231
|
+
]}
|
|
232
|
+
accessible
|
|
233
|
+
accessibilityLabel="Loading"
|
|
234
|
+
accessibilityRole="progressbar"
|
|
235
|
+
accessibilityState={{ busy: animating }}
|
|
236
|
+
{...rest}>
|
|
237
|
+
<Animated.View
|
|
238
|
+
{...innerContainerProps}
|
|
239
|
+
style={[
|
|
240
|
+
{ width: size, height: size },
|
|
241
|
+
componentStyles.innerContainer,
|
|
242
|
+
innerContainerProps?.style,
|
|
243
|
+
animatedStyle,
|
|
244
|
+
]}>
|
|
245
|
+
<Svg width={size} height={size} viewBox="0 0 38 38">
|
|
246
|
+
<Path d={currentPath} fill={strokeColor} />
|
|
247
|
+
</Svg>
|
|
248
|
+
</Animated.View>
|
|
249
|
+
</View>
|
|
250
|
+
);
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
export default memo(LoadingIndicator);
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { memo, useId } from 'react';
|
|
2
|
+
import { View } from 'react-native';
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
cookie4Path,
|
|
6
|
+
cookie9Path,
|
|
7
|
+
expressFastSpatial,
|
|
8
|
+
loadingIndicatorStyles as componentStyles,
|
|
9
|
+
ovalPath,
|
|
10
|
+
pentagonPath,
|
|
11
|
+
pillPath,
|
|
12
|
+
type Props,
|
|
13
|
+
softBurstPath,
|
|
14
|
+
sunnyPath,
|
|
15
|
+
useProcessProps,
|
|
16
|
+
} from './utils';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Material 3 Expressive Loading Indicator for Web.
|
|
20
|
+
* Uses CSS 'd' attribute transitions for true shape morphing.
|
|
21
|
+
*/
|
|
22
|
+
const LoadingIndicator = ({
|
|
23
|
+
animating = true,
|
|
24
|
+
color: colorProp,
|
|
25
|
+
size: sizeProp = 'md',
|
|
26
|
+
style,
|
|
27
|
+
variant = 'default',
|
|
28
|
+
innerContainerProps,
|
|
29
|
+
}: Props) => {
|
|
30
|
+
const id = useId();
|
|
31
|
+
componentStyles.useVariants({
|
|
32
|
+
variant: variant as 'contained',
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
const { size, strokeColor } = useProcessProps({
|
|
36
|
+
variant,
|
|
37
|
+
size: sizeProp,
|
|
38
|
+
color: colorProp,
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
if (!animating) return null;
|
|
42
|
+
|
|
43
|
+
return (
|
|
44
|
+
<View
|
|
45
|
+
style={[
|
|
46
|
+
componentStyles.container,
|
|
47
|
+
{
|
|
48
|
+
width: Math.floor((10 / 48) * size * 2) + size,
|
|
49
|
+
height: Math.floor((10 / 48) * size * 2) + size,
|
|
50
|
+
},
|
|
51
|
+
style,
|
|
52
|
+
]}
|
|
53
|
+
accessible
|
|
54
|
+
accessibilityLabel="Loading"
|
|
55
|
+
accessibilityRole="progressbar"
|
|
56
|
+
accessibilityState={{ busy: animating }}>
|
|
57
|
+
<View
|
|
58
|
+
{...innerContainerProps}
|
|
59
|
+
style={[
|
|
60
|
+
{ width: size, height: size },
|
|
61
|
+
componentStyles.innerContainer,
|
|
62
|
+
innerContainerProps?.style,
|
|
63
|
+
]}>
|
|
64
|
+
<svg
|
|
65
|
+
className={`m3-expressive-svg-${id}`}
|
|
66
|
+
width={size}
|
|
67
|
+
height={size}
|
|
68
|
+
viewBox="0 0 38 38">
|
|
69
|
+
<path className={`m3-expressive-path-${id}`} fill={strokeColor} />
|
|
70
|
+
</svg>
|
|
71
|
+
</View>
|
|
72
|
+
<style>
|
|
73
|
+
{`
|
|
74
|
+
.m3-expressive-svg-${id} {
|
|
75
|
+
transform-origin: center;
|
|
76
|
+
display: block;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
.m3-expressive-path-${id} {
|
|
80
|
+
animation: m3-expressive-combined 4.55s linear infinite;
|
|
81
|
+
transform-origin: center;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
@keyframes m3-expressive-pulse-${id} {
|
|
85
|
+
0%, 100% { transform: scale(1); }
|
|
86
|
+
50% { transform: scale(1.1); }
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
@keyframes m3-expressive-combined {
|
|
90
|
+
0% {
|
|
91
|
+
animation-timing-function: ${expressFastSpatial};
|
|
92
|
+
transform: rotate(0deg);
|
|
93
|
+
d: path('${softBurstPath}');
|
|
94
|
+
}
|
|
95
|
+
14.28% {
|
|
96
|
+
animation-timing-function: ${expressFastSpatial};
|
|
97
|
+
transform: rotate(154.29deg);
|
|
98
|
+
d: path('${cookie9Path}');
|
|
99
|
+
}
|
|
100
|
+
28.57% {
|
|
101
|
+
animation-timing-function: ${expressFastSpatial};
|
|
102
|
+
transform: rotate(308.57deg);
|
|
103
|
+
d: path('${pentagonPath}');
|
|
104
|
+
}
|
|
105
|
+
42.85% {
|
|
106
|
+
animation-timing-function: ${expressFastSpatial};
|
|
107
|
+
transform: rotate(462.86deg);
|
|
108
|
+
d: path('${pillPath}');
|
|
109
|
+
}
|
|
110
|
+
57.14% {
|
|
111
|
+
animation-timing-function: ${expressFastSpatial};
|
|
112
|
+
transform: rotate(617.14deg);
|
|
113
|
+
d: path('${sunnyPath}');
|
|
114
|
+
}
|
|
115
|
+
71.42% {
|
|
116
|
+
animation-timing-function: ${expressFastSpatial};
|
|
117
|
+
transform: rotate(771.43deg);
|
|
118
|
+
d: path('${cookie4Path}');
|
|
119
|
+
}
|
|
120
|
+
85.71% {
|
|
121
|
+
animation-timing-function: ${expressFastSpatial};
|
|
122
|
+
transform: rotate(925.71deg);
|
|
123
|
+
d: path('${ovalPath}');
|
|
124
|
+
}
|
|
125
|
+
100% {
|
|
126
|
+
transform: rotate(1080deg);
|
|
127
|
+
d: path('${softBurstPath}');
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
`}
|
|
131
|
+
</style>
|
|
132
|
+
</View>
|
|
133
|
+
);
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
export default memo(LoadingIndicator);
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { getRegisteredComponentWithFallback } from '../../core';
|
|
2
|
+
import LoadingIndicatorDefault from './LoadingIndicator';
|
|
3
|
+
|
|
4
|
+
export const LoadingIndicator = getRegisteredComponentWithFallback(
|
|
5
|
+
'LoadingIndicator',
|
|
6
|
+
LoadingIndicatorDefault,
|
|
7
|
+
);
|
|
8
|
+
|
|
9
|
+
export {
|
|
10
|
+
type Props as LoadingIndicatorProps,
|
|
11
|
+
loadingIndicatorStyles,
|
|
12
|
+
loadingIndicatorStylesDefault,
|
|
13
|
+
} from './utils';
|