@helpwave/hightide-native 0.6.0 → 0.9.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 (88) hide show
  1. package/package.json +6 -8
  2. package/src/components/card/Card.tsx +2 -4
  3. package/src/components/chat/ChatAttachmentMessageBubble.tsx +152 -90
  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 +52 -45
  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 +69 -80
  22. package/src/components/user-interaction/MultiSelect/MultiSelect.tsx +17 -0
  23. package/src/components/user-interaction/MultiSelect/MultiSelectComponent.tsx +40 -0
  24. package/src/components/user-interaction/MultiSelect/MultiSelectContext.tsx +74 -0
  25. package/src/components/user-interaction/MultiSelect/MultiSelectMenu.tsx +87 -0
  26. package/src/components/user-interaction/MultiSelect/MultiSelectOption.tsx +106 -0
  27. package/src/components/user-interaction/MultiSelect/MultiSelectRoot.tsx +194 -0
  28. package/src/components/user-interaction/MultiSelect/MultiSelectTrigger.tsx +123 -0
  29. package/src/components/user-interaction/MultiSelect/index.ts +2 -0
  30. package/src/components/user-interaction/SearchBar.tsx +11 -17
  31. package/src/components/user-interaction/Select/Select.tsx +17 -0
  32. package/src/components/user-interaction/Select/SelectComponent.tsx +40 -0
  33. package/src/components/user-interaction/Select/SelectContext.tsx +73 -0
  34. package/src/components/user-interaction/Select/SelectMenu.tsx +87 -0
  35. package/src/components/user-interaction/Select/SelectOption.tsx +96 -0
  36. package/src/components/user-interaction/Select/SelectRoot.tsx +186 -0
  37. package/src/components/user-interaction/Select/SelectTrigger.tsx +92 -0
  38. package/src/components/user-interaction/Select/index.ts +2 -0
  39. package/src/components/user-interaction/Switch.tsx +46 -98
  40. package/src/components/user-interaction/Textarea.tsx +165 -12
  41. package/src/components/user-interaction/ThemedPressable.tsx +48 -49
  42. package/src/components/visualization-and-display/Avatar.tsx +19 -17
  43. package/src/components/visualization-and-display/Chip.tsx +4 -12
  44. package/src/components/visualization-and-display/ThemedText.tsx +12 -5
  45. package/src/enums/chatMessageDirection.ts +18 -0
  46. package/src/enums/chatMessageStatus.ts +18 -0
  47. package/src/enums/hightideThemeModes.ts +18 -0
  48. package/src/enums/index.ts +7 -0
  49. package/src/enums/listItemContentOrder.ts +18 -0
  50. package/src/enums/themedTextAppearance.ts +18 -0
  51. package/src/enums/useMultiSelectFirstHighlightBehavior.ts +18 -0
  52. package/src/enums/useSelectFirstHighlightBehavior.ts +18 -0
  53. package/src/global-contexts/content-theme/ContentThemeProvider.tsx +9 -5
  54. package/src/global-contexts/theme/ThemeProvider.tsx +4 -1
  55. package/src/hooks/index.ts +2 -0
  56. package/src/hooks/useAnimatedStyleTransition.ts +460 -0
  57. package/src/hooks/useMemoizedTheme.ts +23 -0
  58. package/src/hooks/useMultiSelect.ts +3 -1
  59. package/src/hooks/useSelect.ts +3 -1
  60. package/src/icons/HightideIconRegistry.ts +2 -0
  61. package/src/theme/adapters/style-adapter-utils.ts +121 -27
  62. package/src/theme/resolvers/avatar.ts +48 -39
  63. package/src/theme/resolvers/button.ts +7 -17
  64. package/src/theme/resolvers/chat/attachment-message-bubble.ts +5 -6
  65. package/src/theme/resolvers/chat/conversation-row.ts +4 -6
  66. package/src/theme/resolvers/chat/message-bubble.ts +1 -2
  67. package/src/theme/resolvers/chat/message-composer.ts +1 -2
  68. package/src/theme/resolvers/chat/quick-reply-chip.ts +2 -3
  69. package/src/theme/resolvers/chat/system-line.ts +1 -2
  70. package/src/theme/resolvers/chat/thread-header.ts +3 -4
  71. package/src/theme/resolvers/checkbox.ts +2 -2
  72. package/src/theme/resolvers/chip.ts +1 -2
  73. package/src/theme/resolvers/iconButton.ts +4 -13
  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 -2
  77. package/src/theme/resolvers/searchBar.ts +1 -2
  78. package/src/theme/resolvers/select.ts +1 -2
  79. package/src/theme/resolvers/themedPressable.ts +4 -15
  80. package/src/theme/themes/hightideThemes.ts +3 -1
  81. package/src/theme/types/components/chat.ts +6 -21
  82. package/src/theme/types/components/input.ts +3 -0
  83. package/src/theme/types/components/textarea.ts +1 -0
  84. package/src/theme/types/resolver.ts +4 -27
  85. package/src/utils/index.ts +1 -0
  86. package/src/utils/pressableInteraction.ts +7 -0
  87. package/src/components/user-interaction/MultiSelect.tsx +0 -282
  88. package/src/components/user-interaction/Select.tsx +0 -242
@@ -4,7 +4,9 @@ import type { StyleProp, TextStyle } from 'react-native'
4
4
 
5
5
  import { ThemedText } from '../visualization-and-display/ThemedText'
6
6
 
7
- export type ListItemContentOrder = 'titleFirst' | 'subtitleFirst'
7
+ import type { ListItemContentOrder } from '../../enums/listItemContentOrder'
8
+
9
+ export type { ListItemContentOrder }
8
10
 
9
11
  export type ListItemTextContentProps = {
10
12
  title?: string,
@@ -25,7 +27,7 @@ export const ListItemTextContent = ({
25
27
  subtitleStyle?: StyleProp<TextStyle>,
26
28
  }) => {
27
29
  if (content != null) {
28
- return <>{content}</>
30
+ return <Fragment>{content}</Fragment>
29
31
  }
30
32
 
31
33
  const titleNode = title != null
@@ -1,5 +1,6 @@
1
1
  import {
2
- Fragment,
2
+ useMemo,
3
+ useState,
3
4
  type ReactNode
4
5
  } from 'react'
5
6
  import {
@@ -16,6 +17,7 @@ import { HightideIconRegistry } from '../../icons/HightideIconRegistry'
16
17
  import { ThemedIcon } from '../visualization-and-display/ThemedIcon'
17
18
  import { useDebugContext } from '../../global-contexts/debug'
18
19
  import { useTheme } from '../../global-contexts/theme/ThemeContext'
20
+ import { useMemoizedTheme } from '../../hooks/useMemoizedTheme'
19
21
  import type {
20
22
  ListNavigationItemDescriptionStyle,
21
23
  ListNavigationItemState,
@@ -44,13 +46,6 @@ export type ListNavigationItemProps = Omit<PressableProps, 'children' | 'style'>
44
46
  subtitleStyle?: StyleOverwrite<ListNavigationItemState, ListNavigationItemDescriptionStyle>,
45
47
  }
46
48
 
47
- type PressableInteraction = {
48
- pressed: boolean,
49
- hovered?: boolean,
50
- focused?: boolean,
51
- focusVisible?: boolean,
52
- }
53
-
54
49
  export const ListNavigationItem = ({
55
50
  title,
56
51
  subtitle,
@@ -74,15 +69,21 @@ export const ListNavigationItem = ({
74
69
  hitSlop: providedHitSlop,
75
70
  onLayout: providedOnLayout,
76
71
  })
72
+ const [isPressed, setIsPressed] = useState(false)
77
73
 
78
- const resolveState = (interaction: PressableInteraction): ListNavigationItemState => ({
74
+ const resolvedState = useMemo((): ListNavigationItemState => ({
79
75
  color,
80
76
  isDisabled: !!disabled,
81
- isPressed: interaction.pressed,
82
- isHovered: !!interaction.hovered,
83
- isFocused: !!interaction.focused,
84
- isFocusVisible: !!interaction.focusVisible,
85
- })
77
+ isPressed,
78
+ }), [color, disabled, isPressed])
79
+
80
+ const resolvedItemStyle = useMemoizedTheme(theme.components.listItem.navigation.container, resolvedState, itemStyle)
81
+ const resolvedLeadingItemContainerStyle = useMemoizedTheme(theme.components.listItem.navigation.leadingItemContainer, resolvedState)
82
+ const resolvedContentStyle = useMemoizedTheme(theme.components.listItem.navigation.content, resolvedState)
83
+ const resolvedTrailingItemContainerStyle = useMemoizedTheme(theme.components.listItem.navigation.trailingItemContainer, resolvedState)
84
+ const resolvedTitleStyle = useMemoizedTheme(theme.components.listItem.navigation.titleText, resolvedState, titleStyle)
85
+ const resolvedSubtitleStyle = useMemoizedTheme(theme.components.listItem.navigation.descriptionText, resolvedState, subtitleStyle)
86
+ const resolvedIconStyle = useMemoizedTheme(theme.components.listItem.navigation.icon, resolvedState)
86
87
 
87
88
  return (
88
89
  <Pressable
@@ -90,57 +91,48 @@ export const ListNavigationItem = ({
90
91
  disabled={disabled}
91
92
  hitSlop={hitSlop}
92
93
  onLayout={onLayout}
93
- style={(pressableState) => {
94
- const state = resolveState(pressableState as PressableInteraction)
95
- return [theme.components.listItem.navigation.container(state, itemStyle), style]
94
+ style={[resolvedItemStyle, style]}
95
+ onPressIn={(event) => {
96
+ setIsPressed(true)
97
+ props.onPressIn?.(event)
96
98
  }}
97
- >
98
- {(pressableState) => {
99
- const state = resolveState(pressableState as PressableInteraction)
100
- const resolvedLeadingItemContainerStyle = theme.components.listItem.navigation.leadingItemContainer(state)
101
- const resolvedContentStyle = theme.components.listItem.navigation.content(state)
102
- const resolvedTrailingItemContainerStyle = theme.components.listItem.navigation.trailingItemContainer(state)
103
- const resolvedTitleStyle = theme.components.listItem.navigation.titleText(state, titleStyle)
104
- const resolvedSubtitleStyle = theme.components.listItem.navigation.descriptionText(state, subtitleStyle)
105
- const resolvedIconStyle = theme.components.listItem.navigation.icon(state)
106
-
107
- return (
108
- <Fragment>
109
- {hitBox.isVisualizing && (
110
- <View
111
- pointerEvents="none"
112
- style={createHitBoxOverlayStyle(hitSlop, hitBox.color)}
113
- />
114
- )}
115
- {leading != null && (
116
- <ListItemAccessory
117
- style={resolvedLeadingItemContainerStyle}
118
- foreground={color?.onColor}
119
- iconStyle={resolvedIconStyle}
120
- >
121
- {leading}
122
- </ListItemAccessory>
123
- )}
124
- <View style={resolvedContentStyle}>
125
- <ListItemTextContent
126
- title={title}
127
- subtitle={subtitle}
128
- content={content}
129
- contentOrder={contentOrder}
130
- titleStyle={resolvedTitleStyle}
131
- subtitleStyle={resolvedSubtitleStyle}
132
- />
133
- </View>
134
- <ListItemAccessory
135
- style={resolvedTrailingItemContainerStyle}
136
- foreground={color?.onColor}
137
- iconStyle={resolvedIconStyle}
138
- >
139
- <ThemedIcon icon={HightideIconRegistry.ChevronRight} />
140
- </ListItemAccessory>
141
- </Fragment>
142
- )
99
+ onPressOut={(event) => {
100
+ setIsPressed(false)
101
+ props.onPressOut?.(event)
143
102
  }}
103
+ >
104
+ {hitBox.isVisualizing && (
105
+ <View
106
+ pointerEvents="none"
107
+ style={createHitBoxOverlayStyle(hitSlop, hitBox.color)}
108
+ />
109
+ )}
110
+ {leading != null && (
111
+ <ListItemAccessory
112
+ style={resolvedLeadingItemContainerStyle}
113
+ foreground={color?.onColor}
114
+ iconStyle={resolvedIconStyle}
115
+ >
116
+ {leading}
117
+ </ListItemAccessory>
118
+ )}
119
+ <View style={resolvedContentStyle}>
120
+ <ListItemTextContent
121
+ title={title}
122
+ subtitle={subtitle}
123
+ content={content}
124
+ contentOrder={contentOrder}
125
+ titleStyle={resolvedTitleStyle}
126
+ subtitleStyle={resolvedSubtitleStyle}
127
+ />
128
+ </View>
129
+ <ListItemAccessory
130
+ style={resolvedTrailingItemContainerStyle}
131
+ foreground={color?.onColor}
132
+ iconStyle={resolvedIconStyle}
133
+ >
134
+ <ThemedIcon icon={HightideIconRegistry.ChevronRight} />
135
+ </ListItemAccessory>
144
136
  </Pressable>
145
137
  )
146
138
  }
@@ -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,
@@ -22,6 +22,7 @@ import { createHitBoxOverlayStyle } from '../../utils/hitBoxOverlay'
22
22
  import { useMinimumTouchTargetHitSlop } from '../../utils/minimumTouchTargetHitSlop'
23
23
  import { ThemedIcon } from '../visualization-and-display/ThemedIcon'
24
24
  import { ThemedText } from '../visualization-and-display/ThemedText'
25
+ import { useMemoizedTheme } from '../../hooks/useMemoizedTheme'
25
26
 
26
27
  export type ButtonSize = ComponentSize
27
28
 
@@ -45,13 +46,6 @@ export type ButtonProps = Omit<PressableProps, 'children' | 'style'> & {
45
46
  iconStyle?: StyleOverwrite<ButtonState, IconStyle>,
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 Button = forwardRef<React.ComponentRef<typeof Pressable>, ButtonProps>(function Button({
56
50
  children,
57
51
  size = 'md',
@@ -76,16 +70,19 @@ export const Button = forwardRef<React.ComponentRef<typeof Pressable>, ButtonPro
76
70
  onLayout: providedOnLayout,
77
71
  })
78
72
 
79
- const resolveState = (interaction: PressableInteraction): ButtonState => ({
73
+ const [isPressed, setIsPressed] = useState(false)
74
+ const resolvedState = useMemo(() => ({
80
75
  size,
81
76
  color,
82
77
  variant,
83
78
  isDisabled: !!disabled,
84
- isPressed: interaction.pressed,
85
- isHovered: !!interaction.hovered,
86
- isFocused: !!interaction.focused,
87
- isFocusVisible: !!interaction.focusVisible,
88
- })
79
+ isPressed: isPressed,
80
+ }), [color, disabled, isPressed, size, variant])
81
+
82
+ const resolvedContainerStyle = useMemoizedTheme(theme.components.button.container, resolvedState, style)
83
+ const resolvedTextStyle = useMemoizedTheme(theme.components.button.text, resolvedState, textStyle)
84
+ const resolvedIcon = useMemoizedTheme(theme.components.button.icon, resolvedState, iconStyle)
85
+ const resolvedStateLayerStyle = useMemoizedTheme(theme.components.button.stateLayer, resolvedState, stateLayerStyle)
89
86
 
90
87
  return (
91
88
  <Pressable
@@ -94,43 +91,40 @@ export const Button = forwardRef<React.ComponentRef<typeof Pressable>, ButtonPro
94
91
  disabled={disabled}
95
92
  hitSlop={hitSlop}
96
93
  onLayout={onLayout}
97
- style={(pressableState) => {
98
- const state = resolveState(pressableState as PressableInteraction)
99
- return theme.components.button.container(state, style)
94
+ style={resolvedContainerStyle}
95
+ onPressIn={(event) => {
96
+ setIsPressed(true)
97
+ props.onPressIn?.(event)
100
98
  }}
101
- >
102
- {(pressableState) => {
103
- const state = resolveState(pressableState as PressableInteraction)
104
- const resolvedText = theme.components.button.text(state, textStyle)
105
- const resolvedIcon = theme.components.button.icon(state, iconStyle)
106
-
107
- return (
108
- <Fragment>
109
- {hitBox.isVisualizing && (
110
- <View
111
- pointerEvents="none"
112
- style={createHitBoxOverlayStyle(hitSlop, hitBox.color)}
113
- />
114
- )}
115
- <View
116
- pointerEvents="none"
117
- style={theme.components.button.stateLayer(state, stateLayerStyle)}
118
- />
119
- <ContentThemeOverrideProvider
120
- textStyle={resolvedText}
121
- iconStyle={resolvedIcon}
122
- >
123
- {leadingIcon !== undefined && (
124
- <ThemedIcon icon={leadingIcon} />
125
- )}
126
- <ThemedText>{children}</ThemedText>
127
- {trailingIcon !== undefined && (
128
- <ThemedIcon icon={trailingIcon} />
129
- )}
130
- </ContentThemeOverrideProvider>
131
- </Fragment>
132
- )
99
+ onPressOut={(event) => {
100
+ setIsPressed(false)
101
+ props.onPressOut?.(event)
133
102
  }}
103
+ >
104
+ {hitBox.isVisualizing && (
105
+ <View
106
+ pointerEvents="none"
107
+ style={createHitBoxOverlayStyle(hitSlop, hitBox.color)}
108
+ />
109
+ )}
110
+ <View
111
+ pointerEvents="none"
112
+ style={resolvedStateLayerStyle}
113
+ />
114
+ <ContentThemeOverrideProvider
115
+ foreground={resolvedTextStyle?.color}
116
+ background={resolvedContainerStyle?.backgroundColor}
117
+ textStyle={resolvedTextStyle}
118
+ iconStyle={resolvedIcon}
119
+ >
120
+ {leadingIcon !== undefined && (
121
+ <ThemedIcon icon={leadingIcon} />
122
+ )}
123
+ <ThemedText>{children}</ThemedText>
124
+ {trailingIcon !== undefined && (
125
+ <ThemedIcon icon={trailingIcon} />
126
+ )}
127
+ </ContentThemeOverrideProvider>
134
128
  </Pressable>
135
129
  )
136
130
  })
@@ -1,7 +1,4 @@
1
- import {
2
- Fragment,
3
- useCallback
4
- } from 'react'
1
+ import { useCallback, useMemo, useState } from 'react'
5
2
  import {
6
3
  Pressable,
7
4
  View,
@@ -19,6 +16,7 @@ import { HightideIconRegistry } from '../../icons/HightideIconRegistry'
19
16
  import { ThemedIcon } from '../visualization-and-display/ThemedIcon'
20
17
  import { useDebugContext } from '../../global-contexts/debug'
21
18
  import { useTheme } from '../../global-contexts/theme/ThemeContext'
19
+ import { useMemoizedTheme } from '../../hooks/useMemoizedTheme'
22
20
  import type {
23
21
  CheckboxSize,
24
22
  CheckboxState,
@@ -47,13 +45,6 @@ export type CheckboxProps = Omit<PressableProps, 'children' | 'style'>
47
45
  stateLayerStyle?: StyleOverwrite<CheckboxState, CheckboxStateLayerStyle>,
48
46
  }
49
47
 
50
- type PressableInteraction = {
51
- pressed: boolean,
52
- hovered?: boolean,
53
- focused?: boolean,
54
- focusVisible?: boolean,
55
- }
56
-
57
48
  export const Checkbox = ({
58
49
  value: controlledValue,
59
50
  initialValue = false,
@@ -81,6 +72,7 @@ export const Checkbox = ({
81
72
  onLayout: providedOnLayout,
82
73
  })
83
74
  const interactive = !disabled && !readOnly
75
+ const [isPressed, setIsPressed] = useState(false)
84
76
 
85
77
  const onEditCompleteStable = useEventCallbackStabilizer(onEditComplete)
86
78
  const onValueChangeStable = useEventCallbackStabilizer(onValueChange)
@@ -96,7 +88,7 @@ export const Checkbox = ({
96
88
  defaultValue: initialValue,
97
89
  })
98
90
 
99
- const resolveState = (interaction: PressableInteraction): CheckboxState => ({
91
+ const resolvedState = useMemo((): CheckboxState => ({
100
92
  size,
101
93
  color,
102
94
  isChecked: value,
@@ -105,11 +97,23 @@ export const Checkbox = ({
105
97
  isDisabled: disabled,
106
98
  isReadonly: readOnly,
107
99
  isRounded,
108
- isPressed: interaction.pressed,
109
- isHovered: !!interaction.hovered,
110
- isFocused: !!interaction.focused,
111
- isFocusVisible: !!interaction.focusVisible,
112
- })
100
+ isPressed,
101
+ }), [
102
+ size,
103
+ color,
104
+ value,
105
+ indeterminate,
106
+ invalid,
107
+ disabled,
108
+ readOnly,
109
+ isRounded,
110
+ isPressed,
111
+ ])
112
+
113
+ const resolvedContainerStyle = useMemoizedTheme(theme.components.checkbox.container, resolvedState, containerStyle)
114
+ const resolvedStateLayerStyle = useMemoizedTheme(theme.components.checkbox.stateLayer, resolvedState, stateLayerStyle)
115
+ const resolvedIcon = useMemoizedTheme(theme.components.checkbox.icon, resolvedState)
116
+ const showIcon = !!(indeterminate || value)
113
117
 
114
118
  return (
115
119
  <Pressable
@@ -122,44 +126,39 @@ export const Checkbox = ({
122
126
  }}
123
127
  hitSlop={hitSlop}
124
128
  onLayout={onLayout}
129
+ style={[resolvedContainerStyle, style]}
130
+ onPressIn={(event) => {
131
+ setIsPressed(true)
132
+ props.onPressIn?.(event)
133
+ }}
134
+ onPressOut={(event) => {
135
+ setIsPressed(false)
136
+ props.onPressOut?.(event)
137
+ }}
125
138
  onPress={(event) => {
126
139
  if (interactive) {
127
140
  setValue((previous) => !previous)
128
141
  }
129
142
  props.onPress?.(event)
130
143
  }}
131
- style={(pressableState) => {
132
- const state = resolveState(pressableState as PressableInteraction)
133
- return [theme.components.checkbox.container(state, containerStyle), style]
134
- }}
135
144
  >
136
- {(pressableState) => {
137
- const state = resolveState(pressableState as PressableInteraction)
138
- const resolvedIcon = theme.components.checkbox.icon(state)
139
- const showIcon = !!(indeterminate || value)
140
-
141
- return (
142
- <Fragment>
143
- {hitBox.isVisualizing && (
144
- <View
145
- pointerEvents="none"
146
- style={createHitBoxOverlayStyle(hitSlop, hitBox.color)}
147
- />
148
- )}
149
- <View
150
- pointerEvents="none"
151
- style={theme.components.checkbox.stateLayer(state, stateLayerStyle)}
152
- />
153
- {showIcon && (
154
- <ThemedIcon
155
- icon={indeterminate ? HightideIconRegistry.Minus : HightideIconRegistry.Check}
156
- size={resolvedIcon.size}
157
- color={resolvedIcon.color}
158
- />
159
- )}
160
- </Fragment>
161
- )
162
- }}
145
+ {hitBox.isVisualizing && (
146
+ <View
147
+ pointerEvents="none"
148
+ style={createHitBoxOverlayStyle(hitSlop, hitBox.color)}
149
+ />
150
+ )}
151
+ <View
152
+ pointerEvents="none"
153
+ style={resolvedStateLayerStyle}
154
+ />
155
+ {showIcon && (
156
+ <ThemedIcon
157
+ icon={indeterminate ? HightideIconRegistry.Minus : HightideIconRegistry.Check}
158
+ size={resolvedIcon.size}
159
+ color={resolvedIcon.color}
160
+ />
161
+ )}
163
162
  </Pressable>
164
163
  )
165
164
  }
@@ -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,
@@ -13,6 +13,7 @@ import type {
13
13
  import { ContentThemeOverrideProvider } from '../../global-contexts/content-theme/ContentThemeProvider'
14
14
  import { useDebugContext } from '../../global-contexts/debug'
15
15
  import { useTheme } from '../../global-contexts/theme/ThemeContext'
16
+ import { useMemoizedTheme } from '../../hooks/useMemoizedTheme'
16
17
  import type {
17
18
  IconButtonState,
18
19
  IconButtonStyle
@@ -43,15 +44,8 @@ export type IconButtonProps = Omit<PressableProps, 'children' | 'style'> & {
43
44
  iconStyle?: StyleOverwrite<IconButtonState, IconStyle>,
44
45
  }
45
46
 
46
- type PressableInteraction = {
47
- pressed: boolean,
48
- hovered?: boolean,
49
- focused?: boolean,
50
- focusVisible?: boolean,
51
- }
52
-
53
47
  export const IconButton = forwardRef<React.ComponentRef<typeof Pressable>, IconButtonProps>(function IconButton({
54
- icon: IconComponent,
48
+ icon,
55
49
  size = 'md',
56
50
  color,
57
51
  variant = 'filled',
@@ -71,17 +65,25 @@ export const IconButton = forwardRef<React.ComponentRef<typeof Pressable>, IconB
71
65
  hitSlop: providedHitSlop,
72
66
  onLayout: providedOnLayout,
73
67
  })
68
+ const [isPressed, setIsPressed] = useState(false)
74
69
 
75
- const resolveState = (interaction: PressableInteraction): IconButtonState => ({
70
+ const resolvedState = useMemo((): IconButtonState => ({
76
71
  size,
77
72
  color,
78
73
  variant,
79
74
  isDisabled: !!disabled,
80
- isPressed: interaction.pressed,
81
- isHovered: !!interaction.hovered,
82
- isFocused: !!interaction.focused,
83
- isFocusVisible: !!interaction.focusVisible,
84
- })
75
+ isPressed,
76
+ }), [
77
+ size,
78
+ color,
79
+ variant,
80
+ disabled,
81
+ isPressed,
82
+ ])
83
+
84
+ const resolvedContainerStyle = useMemoizedTheme(theme.components.iconButton.container, resolvedState, style)
85
+ const resolvedStateLayerStyle = useMemoizedTheme(theme.components.iconButton.stateLayer, resolvedState, stateLayerStyle)
86
+ const resolvedIconStyle = useMemoizedTheme(theme.components.iconButton.icon, resolvedState, iconStyle)
85
87
 
86
88
  return (
87
89
  <Pressable
@@ -92,35 +94,31 @@ export const IconButton = forwardRef<React.ComponentRef<typeof Pressable>, IconB
92
94
  accessibilityLabel={accessibilityLabel}
93
95
  hitSlop={hitSlop}
94
96
  onLayout={onLayout}
95
- style={(pressableState) => {
96
- const state = resolveState(pressableState as PressableInteraction)
97
- return theme.components.iconButton.container(state, style)
97
+ style={resolvedContainerStyle}
98
+ onPressIn={(event) => {
99
+ setIsPressed(true)
100
+ props.onPressIn?.(event)
98
101
  }}
99
- >
100
- {(pressableState) => {
101
- const state = resolveState(pressableState as PressableInteraction)
102
- const resolvedIcon = theme.components.iconButton.icon(state, iconStyle)
103
-
104
- return (
105
- <Fragment>
106
- {hitBox.isVisualizing && (
107
- <View
108
- pointerEvents="none"
109
- style={createHitBoxOverlayStyle(hitSlop, hitBox.color)}
110
- />
111
- )}
112
- <View
113
- pointerEvents="none"
114
- style={theme.components.iconButton.stateLayer(state, stateLayerStyle)}
115
- />
116
- <ContentThemeOverrideProvider
117
- iconStyle={resolvedIcon}
118
- >
119
- <ThemedIcon icon={IconComponent} />
120
- </ContentThemeOverrideProvider>
121
- </Fragment>
122
- )
102
+ onPressOut={(event) => {
103
+ setIsPressed(false)
104
+ props.onPressOut?.(event)
123
105
  }}
106
+ >
107
+ {hitBox.isVisualizing && (
108
+ <View
109
+ pointerEvents="none"
110
+ style={createHitBoxOverlayStyle(hitSlop, hitBox.color)}
111
+ />
112
+ )}
113
+ <View
114
+ pointerEvents="none"
115
+ style={resolvedStateLayerStyle}
116
+ />
117
+ <ContentThemeOverrideProvider
118
+ iconStyle={resolvedIconStyle}
119
+ >
120
+ <ThemedIcon icon={icon} />
121
+ </ContentThemeOverrideProvider>
124
122
  </Pressable>
125
123
  )
126
124
  })