@helpwave/hightide-native 0.5.0 → 0.7.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.
Files changed (92) hide show
  1. package/package.json +3 -3
  2. package/src/components/card/Card.tsx +2 -4
  3. package/src/components/chat/ChatAttachmentMessageBubble.tsx +155 -94
  4. package/src/components/chat/ChatConversationList.tsx +4 -12
  5. package/src/components/chat/ChatConversationRow.tsx +159 -164
  6. package/src/components/chat/ChatDateDivider.tsx +3 -8
  7. package/src/components/chat/ChatMessageBubble.tsx +54 -52
  8. package/src/components/chat/ChatMessageComposer.tsx +4 -12
  9. package/src/components/chat/ChatMessageList.tsx +3 -9
  10. package/src/components/chat/ChatQuickReplyChip.tsx +41 -27
  11. package/src/components/chat/ChatSystemLine.tsx +4 -12
  12. package/src/components/chat/ChatThreadHeader.tsx +136 -101
  13. package/src/components/layout/Divider.tsx +2 -4
  14. package/src/components/list/ListActionItem.tsx +57 -65
  15. package/src/components/list/ListItem.tsx +8 -28
  16. package/src/components/list/ListItemTextContent.tsx +4 -2
  17. package/src/components/list/ListNavigationItem.tsx +55 -63
  18. package/src/components/user-interaction/Button.tsx +43 -49
  19. package/src/components/user-interaction/Checkbox.tsx +47 -48
  20. package/src/components/user-interaction/IconButton.tsx +40 -42
  21. package/src/components/user-interaction/Input.tsx +70 -67
  22. package/src/components/user-interaction/MultiSelect/MultiSelect.tsx +40 -0
  23. package/src/components/user-interaction/MultiSelect/MultiSelectContext.tsx +70 -0
  24. package/src/components/user-interaction/MultiSelect/MultiSelectMenu.tsx +87 -0
  25. package/src/components/user-interaction/MultiSelect/MultiSelectOption.tsx +87 -0
  26. package/src/components/user-interaction/MultiSelect/MultiSelectRoot.tsx +194 -0
  27. package/src/components/user-interaction/MultiSelect/MultiSelectTrigger.tsx +123 -0
  28. package/src/components/user-interaction/MultiSelect/index.ts +6 -0
  29. package/src/components/user-interaction/SearchBar.tsx +11 -17
  30. package/src/components/user-interaction/Select/Select.tsx +40 -0
  31. package/src/components/user-interaction/Select/SelectContext.tsx +69 -0
  32. package/src/components/user-interaction/Select/SelectMenu.tsx +87 -0
  33. package/src/components/user-interaction/Select/SelectOption.tsx +77 -0
  34. package/src/components/user-interaction/Select/SelectRoot.tsx +186 -0
  35. package/src/components/user-interaction/Select/SelectTrigger.tsx +92 -0
  36. package/src/components/user-interaction/Select/index.ts +6 -0
  37. package/src/components/user-interaction/Switch.tsx +46 -98
  38. package/src/components/user-interaction/Textarea.tsx +179 -0
  39. package/src/components/user-interaction/ThemedPressable.tsx +48 -49
  40. package/src/components/user-interaction/index.ts +1 -0
  41. package/src/components/visualization-and-display/Avatar.tsx +19 -17
  42. package/src/components/visualization-and-display/Chip.tsx +4 -12
  43. package/src/components/visualization-and-display/ThemedText.tsx +12 -5
  44. package/src/enums/chatMessageDirection.ts +18 -0
  45. package/src/enums/chatMessageStatus.ts +18 -0
  46. package/src/enums/hightideThemeModes.ts +18 -0
  47. package/src/enums/index.ts +7 -0
  48. package/src/enums/listItemContentOrder.ts +18 -0
  49. package/src/enums/themedTextAppearance.ts +18 -0
  50. package/src/enums/useMultiSelectFirstHighlightBehavior.ts +18 -0
  51. package/src/enums/useSelectFirstHighlightBehavior.ts +18 -0
  52. package/src/global-contexts/content-theme/ContentThemeProvider.tsx +9 -5
  53. package/src/global-contexts/theme/ThemeProvider.tsx +4 -1
  54. package/src/hooks/index.ts +2 -0
  55. package/src/hooks/useAnimatedStyleTransition.ts +460 -0
  56. package/src/hooks/useMemoizedTheme.ts +23 -0
  57. package/src/hooks/useMultiSelect.ts +3 -1
  58. package/src/hooks/useSelect.ts +3 -1
  59. package/src/icons/HightideIconRegistry.ts +2 -0
  60. package/src/theme/adapters/style-adapter-utils.ts +121 -27
  61. package/src/theme/resolvers/avatar.ts +48 -39
  62. package/src/theme/resolvers/button.ts +7 -17
  63. package/src/theme/resolvers/chat/attachment-message-bubble.ts +5 -6
  64. package/src/theme/resolvers/chat/conversation-row.ts +4 -6
  65. package/src/theme/resolvers/chat/message-bubble.ts +1 -2
  66. package/src/theme/resolvers/chat/message-composer.ts +1 -2
  67. package/src/theme/resolvers/chat/quick-reply-chip.ts +2 -3
  68. package/src/theme/resolvers/chat/system-line.ts +1 -2
  69. package/src/theme/resolvers/chat/thread-header.ts +3 -4
  70. package/src/theme/resolvers/checkbox.ts +2 -2
  71. package/src/theme/resolvers/chip.ts +1 -2
  72. package/src/theme/resolvers/iconButton.ts +4 -13
  73. package/src/theme/resolvers/index.ts +1 -0
  74. package/src/theme/resolvers/input.ts +30 -14
  75. package/src/theme/resolvers/listItem.ts +2 -3
  76. package/src/theme/resolvers/multiSelect.ts +1 -3
  77. package/src/theme/resolvers/searchBar.ts +1 -2
  78. package/src/theme/resolvers/select.ts +1 -2
  79. package/src/theme/resolvers/textarea.ts +14 -0
  80. package/src/theme/resolvers/themedPressable.ts +4 -15
  81. package/src/theme/themes/createHightideTheme.ts +6 -0
  82. package/src/theme/themes/hightideThemes.ts +3 -1
  83. package/src/theme/types/components/chat.ts +6 -21
  84. package/src/theme/types/components/hightide.ts +2 -0
  85. package/src/theme/types/components/index.ts +1 -0
  86. package/src/theme/types/components/input.ts +3 -0
  87. package/src/theme/types/components/textarea.ts +9 -0
  88. package/src/theme/types/resolver.ts +4 -27
  89. package/src/utils/index.ts +1 -0
  90. package/src/utils/pressableInteraction.ts +7 -0
  91. package/src/components/user-interaction/MultiSelect.tsx +0 -281
  92. package/src/components/user-interaction/Select.tsx +0 -242
@@ -0,0 +1,92 @@
1
+ import type { ReactNode } from 'react'
2
+ import { useMemo, useState } from 'react'
3
+ import {
4
+ View,
5
+ type StyleProp,
6
+ type ViewStyle
7
+ } from 'react-native'
8
+
9
+ import { useTheme } from '../../../global-contexts/theme/ThemeContext'
10
+ import { useMemoizedTheme } from '../../../hooks/useMemoizedTheme'
11
+ import { HightideIconRegistry } from '../../../icons/HightideIconRegistry'
12
+ import type { IconStyle } from '../../../icons'
13
+ import type { SelectState } from '../../../theme/types/components/select'
14
+ import { ThemedIcon } from '../../visualization-and-display/ThemedIcon'
15
+ import { ThemedText } from '../../visualization-and-display/ThemedText'
16
+ import { useSelectContext, type SelectOptionType } from './SelectContext'
17
+ import { ThemedPressable } from '../ThemedPressable'
18
+
19
+ export type SelectTriggerProps<T = string> = {
20
+ placeholder?: ReactNode,
21
+ selectedDisplay?: (option: SelectOptionType<T> | null) => ReactNode,
22
+ style?: StyleProp<ViewStyle>,
23
+ }
24
+
25
+ export const SelectTrigger = <T,>({
26
+ placeholder = 'Select…',
27
+ selectedDisplay,
28
+ style,
29
+ }: SelectTriggerProps<T>) => {
30
+ const { theme } = useTheme()
31
+ const context = useSelectContext<T>()
32
+ const [isPressed, setIsPressed] = useState(false)
33
+ const interactive = !context.disabled && !context.readOnly
34
+ const selectedOption = context.selectedId
35
+ ? (context.idToOptionMap[context.selectedId] ?? null)
36
+ : null
37
+
38
+ const resolvedState = useMemo((): SelectState => ({
39
+ color: context.config.color,
40
+ isDisabled: !!context.disabled,
41
+ isReadonly: !!context.readOnly,
42
+ isInvalid: !!context.invalid,
43
+ isOpen: context.isOpen,
44
+ hasValue: !!context.selectedId,
45
+ isPressed,
46
+ }), [
47
+ context.config.color,
48
+ context.disabled,
49
+ context.invalid,
50
+ context.isOpen,
51
+ context.readOnly,
52
+ context.selectedId,
53
+ isPressed,
54
+ ])
55
+
56
+ const selectTheme = theme.components.select
57
+ const resolvedTriggerStyle = useMemoizedTheme(selectTheme.trigger, resolvedState)
58
+ const resolvedStateLayerStyle = useMemoizedTheme(selectTheme.stateLayer, resolvedState)
59
+ const resolvedTriggerTextStyle = useMemoizedTheme(selectTheme.triggerText, resolvedState)
60
+ const resolvedIcon = useMemoizedTheme<SelectState, IconStyle>(selectTheme.icon, resolvedState)
61
+
62
+ const customDisplay = selectedDisplay?.(selectedOption)
63
+ const fallbackDisplay = selectedOption?.display
64
+ ?? selectedOption?.label
65
+ ?? placeholder
66
+ const resolvedDisplay = selectedDisplay ? customDisplay : fallbackDisplay
67
+
68
+ return (
69
+ <ThemedPressable
70
+ disabled={!interactive}
71
+ style={[resolvedTriggerStyle, style]}
72
+ onPressIn={() => setIsPressed(true)}
73
+ onPressOut={() => setIsPressed(false)}
74
+ onPress={() => context.toggleIsOpen()}
75
+ >
76
+ <View pointerEvents="none" style={resolvedStateLayerStyle} />
77
+ {typeof resolvedDisplay === 'string' || typeof resolvedDisplay === 'number' ? (
78
+ <ThemedText style={[resolvedTriggerTextStyle]}>
79
+ {resolvedDisplay}
80
+ </ThemedText>
81
+ ) : (
82
+ resolvedDisplay
83
+ )}
84
+ <ThemedIcon
85
+ icon={HightideIconRegistry.ChevronDown}
86
+ size={resolvedIcon.size}
87
+ strokeWidth={resolvedIcon.strokeWidth}
88
+ color={resolvedIcon.color}
89
+ />
90
+ </ThemedPressable>
91
+ )
92
+ }
@@ -0,0 +1,6 @@
1
+ export * from './Select'
2
+ export * from './SelectContext'
3
+ export * from './SelectMenu'
4
+ export * from './SelectOption'
5
+ export * from './SelectRoot'
6
+ export * from './SelectTrigger'
@@ -1,8 +1,7 @@
1
1
  import {
2
- Fragment,
3
2
  useCallback,
4
- useEffect,
5
- useRef
3
+ useMemo,
4
+ useState
6
5
  } from 'react'
7
6
  import {
8
7
  Animated,
@@ -20,6 +19,8 @@ import {
20
19
 
21
20
  import { useDebugContext } from '../../global-contexts/debug'
22
21
  import { useTheme } from '../../global-contexts/theme/ThemeContext'
22
+ import { useAnimatedStyleTransition } from '../../hooks/useAnimatedStyleTransition'
23
+ import { useMemoizedTheme } from '../../hooks/useMemoizedTheme'
23
24
  import type {
24
25
  SwitchContainerStyle,
25
26
  SwitchState,
@@ -34,7 +35,12 @@ import type {
34
35
  import { createHitBoxOverlayStyle } from '../../utils/hitBoxOverlay'
35
36
  import { useMinimumTouchTargetHitSlop } from '../../utils/minimumTouchTargetHitSlop'
36
37
 
37
- const ANIMATION_DURATION_MS = 250
38
+ const thumbAnimatedAttributes: (keyof ViewStyle)[] = [
39
+ 'transform',
40
+ 'position',
41
+ 'width',
42
+ 'height',
43
+ ]
38
44
 
39
45
  export type SwitchProps = Omit<PressableProps, 'children' | 'style' | 'disabled'>
40
46
  & Partial<FormFieldInteractionStates>
@@ -47,17 +53,6 @@ export type SwitchProps = Omit<PressableProps, 'children' | 'style' | 'disabled'
47
53
  thumbStyle?: StyleOverwrite<SwitchState, SwitchThumbStyle>,
48
54
  }
49
55
 
50
- type PressableInteraction = {
51
- pressed: boolean,
52
- hovered?: boolean,
53
- focused?: boolean,
54
- focusVisible?: boolean,
55
- }
56
-
57
- const toNumber = (value: string | number | undefined, fallback: number): number => (
58
- typeof value === 'number' ? value : fallback
59
- )
60
-
61
56
  export const Switch = ({
62
57
  value: controlledValue,
63
58
  initialValue = false,
@@ -83,6 +78,7 @@ export const Switch = ({
83
78
  onLayout: providedOnLayout,
84
79
  })
85
80
  const interactive = !disabled && !readOnly
81
+ const [isPressed, setIsPressed] = useState(false)
86
82
 
87
83
  const onEditCompleteStable = useEventCallbackStabilizer(onEditComplete)
88
84
  const onValueChangeStable = useEventCallbackStabilizer(onValueChange)
@@ -98,25 +94,27 @@ export const Switch = ({
98
94
  defaultValue: initialValue,
99
95
  })
100
96
 
101
- const progress = useRef(new Animated.Value(value ? 1 : 0)).current
102
-
103
- useEffect(() => {
104
- Animated.timing(progress, {
105
- toValue: value ? 1 : 0,
106
- duration: ANIMATION_DURATION_MS,
107
- useNativeDriver: false,
108
- }).start()
109
- }, [progress, value])
110
-
111
- const resolveState = (interaction: PressableInteraction): SwitchState => ({
97
+ const resolvedState = useMemo((): SwitchState => ({
112
98
  isActive: value,
113
99
  isInvalid: invalid,
114
100
  isDisabled: disabled,
115
101
  isReadonly: readOnly,
116
- isPressed: interaction.pressed,
117
- isHovered: !!interaction.hovered,
118
- isFocused: !!interaction.focused,
119
- isFocusVisible: !!interaction.focusVisible,
102
+ isPressed,
103
+ }), [
104
+ value,
105
+ invalid,
106
+ disabled,
107
+ readOnly,
108
+ isPressed,
109
+ ])
110
+
111
+ const resolvedContainerStyle = useMemoizedTheme(theme.components.switch.container, resolvedState, containerStyle)
112
+ const resolvedTrackStyle = useMemoizedTheme(theme.components.switch.track, resolvedState, trackStyle)
113
+ const resolvedThumbStyle = useMemoizedTheme(theme.components.switch.thumb, resolvedState, thumbStyle)
114
+ const animatedThumbStyle = useAnimatedStyleTransition({
115
+ style: resolvedThumbStyle,
116
+ duration: theme.motion.durations.normal,
117
+ animatedAttributes: thumbAnimatedAttributes,
120
118
  })
121
119
 
122
120
  return (
@@ -131,81 +129,31 @@ export const Switch = ({
131
129
  }}
132
130
  hitSlop={hitSlop}
133
131
  onLayout={onLayout}
132
+ style={[resolvedContainerStyle, style]}
133
+ onPressIn={(event) => {
134
+ setIsPressed(true)
135
+ props.onPressIn?.(event)
136
+ }}
137
+ onPressOut={(event) => {
138
+ setIsPressed(false)
139
+ props.onPressOut?.(event)
140
+ }}
134
141
  onPress={(event) => {
135
142
  if (interactive) {
136
143
  setValue((previous) => !previous)
137
144
  }
138
145
  props.onPress?.(event)
139
146
  }}
140
- style={(pressableState) => {
141
- const state = resolveState(pressableState as PressableInteraction)
142
- return [theme.components.switch.container(state, containerStyle), style]
143
- }}
144
147
  >
145
- {(pressableState) => {
146
- const state = resolveState(pressableState as PressableInteraction)
147
- const inactiveState: SwitchState = { ...state, isActive: false }
148
- const activeState: SwitchState = { ...state, isActive: true }
149
- const resolvedTrackStyle = theme.components.switch.track(state, trackStyle)
150
- const resolvedThumbInactive = theme.components.switch.thumb(inactiveState, thumbStyle)
151
- const resolvedThumbActive = theme.components.switch.thumb(activeState, thumbStyle)
152
- const trackWidth = toNumber(resolvedTrackStyle.width as string | number | undefined, 40)
153
- const trackHeight = toNumber(resolvedTrackStyle.height as string | number | undefined, 28)
154
- const fallbackBorderWidth = toNumber(resolvedTrackStyle.borderWidth, 0)
155
- const trackBorderTop = toNumber(resolvedTrackStyle.borderTopWidth, fallbackBorderWidth)
156
- const trackBorderRight = toNumber(resolvedTrackStyle.borderRightWidth, fallbackBorderWidth)
157
- const trackBorderBottom = toNumber(resolvedTrackStyle.borderBottomWidth, fallbackBorderWidth)
158
- const trackBorderLeft = toNumber(resolvedTrackStyle.borderLeftWidth, fallbackBorderWidth)
159
- const thumbInactiveSize = toNumber(resolvedThumbInactive.width as string | number | undefined, 16)
160
- const thumbActiveSize = toNumber(resolvedThumbActive.width as string | number | undefined, 20)
161
- const trackInnerWidth = trackWidth - trackBorderLeft - trackBorderRight
162
- const trackInnerHeight = trackHeight - trackBorderTop - trackBorderBottom
163
- const thumbOffsetInactive = (trackInnerHeight - thumbInactiveSize) / 2
164
- const thumbOffsetActive = trackInnerWidth - thumbActiveSize - (
165
- (trackInnerHeight - thumbActiveSize) / 2
166
- )
167
- const thumbTopInactive = (trackInnerHeight - thumbInactiveSize) / 2
168
- const thumbTopActive = (trackInnerHeight - thumbActiveSize) / 2
169
- const thumbSize = progress.interpolate({
170
- inputRange: [0, 1],
171
- outputRange: [thumbInactiveSize, thumbActiveSize],
172
- })
173
- const thumbTranslateX = progress.interpolate({
174
- inputRange: [0, 1],
175
- outputRange: [thumbOffsetInactive, thumbOffsetActive],
176
- })
177
- const thumbTop = progress.interpolate({
178
- inputRange: [0, 1],
179
- outputRange: [thumbTopInactive, thumbTopActive],
180
- })
181
- const thumbColor = value
182
- ? resolvedThumbActive.backgroundColor
183
- : resolvedThumbInactive.backgroundColor
184
-
185
- return (
186
- <Fragment>
187
- {hitBox.isVisualizing && (
188
- <View
189
- pointerEvents="none"
190
- style={createHitBoxOverlayStyle(hitSlop, hitBox.color)}
191
- />
192
- )}
193
- <View style={resolvedTrackStyle}>
194
- <Animated.View
195
- style={{
196
- position: 'absolute',
197
- top: thumbTop,
198
- width: thumbSize,
199
- height: thumbSize,
200
- borderRadius: thumbActiveSize / 2,
201
- backgroundColor: thumbColor,
202
- transform: [{ translateX: thumbTranslateX }],
203
- }}
204
- />
205
- </View>
206
- </Fragment>
207
- )
208
- }}
148
+ {hitBox.isVisualizing && (
149
+ <View
150
+ pointerEvents="none"
151
+ style={createHitBoxOverlayStyle(hitSlop, hitBox.color)}
152
+ />
153
+ )}
154
+ <View style={resolvedTrackStyle}>
155
+ <Animated.View style={animatedThumbStyle} />
156
+ </View>
209
157
  </Pressable>
210
158
  )
211
159
  }
@@ -0,0 +1,179 @@
1
+ import {
2
+ forwardRef,
3
+ useMemo,
4
+ useState
5
+ } from 'react'
6
+ import {
7
+ TextInput,
8
+ View,
9
+ type TextInputProps
10
+ } from 'react-native'
11
+
12
+ import {
13
+ useControlledState,
14
+ useDelay
15
+ } from '@helpwave/hightide-utils/hooks'
16
+
17
+ import type { ColorPairToken } from '@helpwave/hightide-design/theme-tokens'
18
+ import { useTheme } from '../../global-contexts/theme/ThemeContext'
19
+ import { useMemoizedTheme } from '../../hooks/useMemoizedTheme'
20
+ import type {
21
+ TextareaContainerStyle,
22
+ TextareaState,
23
+ TextareaStateLayerStyle,
24
+ TextareaTextStyle
25
+ } from '../../theme/types/components/textarea'
26
+ import type { StyleOverwrite } from '../../theme/types/resolver'
27
+ import type {
28
+ FormFieldDataHandling,
29
+ FormFieldInteractionStates
30
+ } from '../../types/formField'
31
+ import type { EditCompleteOptions, EditCompleteOptionsResolved } from './Input'
32
+
33
+ const defaultEditCompleteOptions: EditCompleteOptionsResolved = {
34
+ allowEnterComplete: false,
35
+ onBlur: true,
36
+ afterDelay: false,
37
+ delay: 2500,
38
+ }
39
+
40
+ export type TextareaProps = Omit<TextInputProps, 'value' | 'style'>
41
+ & Partial<FormFieldDataHandling<string>>
42
+ & Partial<FormFieldInteractionStates>
43
+ & {
44
+ color?: ColorPairToken,
45
+ editCompleteOptions?: EditCompleteOptions,
46
+ initialValue?: string,
47
+ style?: StyleOverwrite<TextareaState, TextareaContainerStyle>,
48
+ stateLayerStyle?: StyleOverwrite<TextareaState, TextareaStateLayerStyle>,
49
+ textStyle?: StyleOverwrite<TextareaState, TextareaTextStyle>,
50
+ }
51
+
52
+ export const Textarea = forwardRef<TextInput, TextareaProps>(function Textarea({
53
+ value: controlledValue,
54
+ initialValue,
55
+ color,
56
+ invalid = false,
57
+ disabled = false,
58
+ readOnly = false,
59
+ required = false,
60
+ onValueChange,
61
+ onEditComplete,
62
+ editCompleteOptions,
63
+ style,
64
+ stateLayerStyle,
65
+ textStyle,
66
+ onPointerEnter,
67
+ onPointerLeave,
68
+ ...props
69
+ }, ref) {
70
+ const { theme } = useTheme()
71
+ const [isHovered, setIsHovered] = useState(false)
72
+ const [isPressed, setIsPressed] = useState(false)
73
+ const [isFocused, setIsFocused] = useState(false)
74
+
75
+ const [value, setValue] = useControlledState({
76
+ value: controlledValue,
77
+ onValueChange,
78
+ defaultValue: initialValue,
79
+ })
80
+
81
+ const {
82
+ onBlur: allowEditCompleteOnBlur,
83
+ afterDelay,
84
+ delay,
85
+ allowEnterComplete,
86
+ } = { ...defaultEditCompleteOptions, ...editCompleteOptions }
87
+
88
+ const { restartTimer, clearTimer } = useDelay({
89
+ delay,
90
+ disabled: !afterDelay || disabled || readOnly,
91
+ })
92
+
93
+ const interactive = !disabled && !readOnly
94
+
95
+ const state = useMemo((): TextareaState => ({
96
+ color,
97
+ isDisabled: disabled,
98
+ isInvalid: invalid,
99
+ isReadonly: readOnly,
100
+ isHovered: interactive && isHovered,
101
+ isPressed: interactive && isPressed,
102
+ isFocused: interactive && isFocused,
103
+ }), [color, disabled, invalid, readOnly, interactive, isHovered, isPressed, isFocused])
104
+
105
+ const resolvedContainerStyle = useMemoizedTheme(theme.components.textarea.container, state, style)
106
+ const resolvedStateLayerStyle = useMemoizedTheme(theme.components.textarea.stateLayer, state, stateLayerStyle)
107
+ const resolvedTextStyle = useMemoizedTheme(theme.components.textarea.text, state, textStyle)
108
+ const resolvedPlaceholderStyle = useMemoizedTheme(theme.components.textarea.placeholder, state)
109
+
110
+ return (
111
+ <View
112
+ style={resolvedContainerStyle}
113
+ onPointerEnter={(event) => {
114
+ if (interactive) {
115
+ setIsHovered(true)
116
+ }
117
+ onPointerEnter?.(event)
118
+ }}
119
+ onPointerLeave={(event) => {
120
+ setIsHovered(false)
121
+ onPointerLeave?.(event)
122
+ }}
123
+ >
124
+ <View
125
+ pointerEvents="none"
126
+ style={resolvedStateLayerStyle}
127
+ />
128
+ <TextInput
129
+ {...props}
130
+ ref={ref}
131
+ value={value}
132
+ editable={interactive}
133
+ multiline
134
+ textAlignVertical="top"
135
+ placeholderTextColor={resolvedPlaceholderStyle.color}
136
+ onChangeText={(nextValue) => {
137
+ props.onChangeText?.(nextValue)
138
+ restartTimer(() => {
139
+ onEditComplete?.(nextValue)
140
+ })
141
+ setValue(nextValue)
142
+ }}
143
+ onFocus={(event) => {
144
+ if (interactive) {
145
+ setIsFocused(true)
146
+ }
147
+ props.onFocus?.(event)
148
+ }}
149
+ onBlur={(event) => {
150
+ setIsFocused(false)
151
+ props.onBlur?.(event)
152
+ if (allowEditCompleteOnBlur) {
153
+ onEditComplete?.(value ?? '')
154
+ clearTimer()
155
+ }
156
+ }}
157
+ onPressIn={(event) => {
158
+ if (interactive) {
159
+ setIsPressed(true)
160
+ }
161
+ props.onPressIn?.(event)
162
+ }}
163
+ onPressOut={(event) => {
164
+ setIsPressed(false)
165
+ props.onPressOut?.(event)
166
+ }}
167
+ onSubmitEditing={(event) => {
168
+ props.onSubmitEditing?.(event)
169
+ if (allowEnterComplete) {
170
+ onEditComplete?.(value ?? '')
171
+ clearTimer()
172
+ }
173
+ }}
174
+ style={resolvedTextStyle}
175
+ accessibilityState={{ disabled, selected: required }}
176
+ />
177
+ </View>
178
+ )
179
+ })
@@ -1,4 +1,4 @@
1
- import { Fragment, forwardRef } from 'react'
1
+ import { forwardRef, useMemo, useState } from 'react'
2
2
  import {
3
3
  Pressable,
4
4
  View,
@@ -15,6 +15,7 @@ import type { ColorPairToken } from '@helpwave/hightide-design/theme-tokens'
15
15
  import { ContentThemeOverrideProvider } from '../../global-contexts/content-theme/ContentThemeProvider'
16
16
  import { useDebugContext } from '../../global-contexts/debug'
17
17
  import { useTheme } from '../../global-contexts/theme/ThemeContext'
18
+ import { useMemoizedTheme } from '../../hooks/useMemoizedTheme'
18
19
  import type {
19
20
  ThemedPressableIconStyle,
20
21
  ThemedPressableState,
@@ -45,13 +46,6 @@ export type ThemedPressableProps = Omit<PressableProps, 'style'> & {
45
46
  textStyle?: StyleOverwrite<ThemedPressableState, ThemedPressableTextStyle>,
46
47
  }
47
48
 
48
- type PressableInteraction = {
49
- pressed: boolean,
50
- hovered?: boolean,
51
- focused?: boolean,
52
- focusVisible?: boolean,
53
- }
54
-
55
49
  export const ThemedPressable = forwardRef<React.ComponentRef<typeof Pressable>, ThemedPressableProps>(function ThemedPressable({
56
50
  children,
57
51
  size = 'md',
@@ -75,19 +69,33 @@ export const ThemedPressable = forwardRef<React.ComponentRef<typeof Pressable>,
75
69
  hitSlop: providedHitSlop,
76
70
  onLayout: providedOnLayout,
77
71
  })
72
+ const [isPressed, setIsPressed] = useState(false)
78
73
 
79
- const resolveState = (interaction: PressableInteraction): ThemedPressableState => ({
74
+ const resolvedState = useMemo((): ThemedPressableState => ({
80
75
  size,
81
76
  color,
82
77
  coloringStyle,
83
78
  coloringColorVariant,
84
79
  hasAdditionalHorizontalPadding,
85
80
  isDisabled: !!disabled,
86
- isPressed: interaction.pressed,
87
- isHovered: !!interaction.hovered,
88
- isFocused: !!interaction.focused,
89
- isFocusVisible: !!interaction.focusVisible,
90
- })
81
+ isPressed,
82
+ }), [
83
+ size,
84
+ color,
85
+ coloringStyle,
86
+ coloringColorVariant,
87
+ hasAdditionalHorizontalPadding,
88
+ disabled,
89
+ isPressed,
90
+ ])
91
+
92
+ const resolvedContainerStyle = useMemoizedTheme(theme.components.themedPressable.container, resolvedState, style)
93
+ const resolvedStateLayerStyle = useMemoizedTheme(theme.components.themedPressable.stateLayer, resolvedState, stateLayerStyle)
94
+ const resolvedTextStyle = useMemoizedTheme(theme.components.themedPressable.text, resolvedState, textStyle)
95
+ const resolvedIconStyle = useMemoizedTheme(theme.components.themedPressable.icon, resolvedState, iconStyle)
96
+ const resolvedChildren = typeof children === 'function'
97
+ ? children({ pressed: isPressed })
98
+ : children
91
99
 
92
100
  return (
93
101
  <Pressable
@@ -96,43 +104,34 @@ export const ThemedPressable = forwardRef<React.ComponentRef<typeof Pressable>,
96
104
  disabled={disabled}
97
105
  hitSlop={hitSlop}
98
106
  onLayout={onLayout}
99
- style={(pressableState) => {
100
- const state = resolveState(pressableState)
101
- return theme.components.themedPressable.container(state, style)
107
+ style={resolvedContainerStyle}
108
+ onPressIn={(event) => {
109
+ setIsPressed(true)
110
+ props.onPressIn?.(event)
102
111
  }}
103
- >
104
- {(pressableState) => {
105
- const state = resolveState(pressableState)
106
- const resolvedText = theme.components.themedPressable.text(state, textStyle)
107
- const resolvedIcon = theme.components.themedPressable.icon(state, iconStyle)
108
- const resolvedChildren = typeof children === 'function'
109
- ? children(pressableState)
110
- : children
111
- const backroundColor = theme.components.themedPressable.container(state, style).backgroundColor
112
-
113
- return (
114
- <Fragment>
115
- {hitBox.isVisualizing && (
116
- <View
117
- pointerEvents="none"
118
- style={createHitBoxOverlayStyle(hitSlop, hitBox.color)}
119
- />
120
- )}
121
- <View
122
- pointerEvents="none"
123
- style={theme.components.themedPressable.stateLayer(state, stateLayerStyle)}
124
- />
125
- <ContentThemeOverrideProvider
126
- foreground={resolvedText.color}
127
- background={backroundColor}
128
- textStyle={resolvedText}
129
- iconStyle={resolvedIcon}
130
- >
131
- {resolvedChildren}
132
- </ContentThemeOverrideProvider>
133
- </Fragment>
134
- )
112
+ onPressOut={(event) => {
113
+ setIsPressed(false)
114
+ props.onPressOut?.(event)
135
115
  }}
116
+ >
117
+ {hitBox.isVisualizing && (
118
+ <View
119
+ pointerEvents="none"
120
+ style={createHitBoxOverlayStyle(hitSlop, hitBox.color)}
121
+ />
122
+ )}
123
+ <View
124
+ pointerEvents="none"
125
+ style={resolvedStateLayerStyle}
126
+ />
127
+ <ContentThemeOverrideProvider
128
+ foreground={resolvedTextStyle.color}
129
+ background={resolvedContainerStyle.backgroundColor}
130
+ textStyle={resolvedTextStyle}
131
+ iconStyle={resolvedIconStyle}
132
+ >
133
+ {resolvedChildren}
134
+ </ContentThemeOverrideProvider>
136
135
  </Pressable>
137
136
  )
138
137
  })
@@ -6,4 +6,5 @@ export * from './MultiSelect'
6
6
  export * from './SearchBar'
7
7
  export * from './Select'
8
8
  export * from './Switch'
9
+ export * from './Textarea'
9
10
  export * from './ThemedPressable'