@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
@@ -3,11 +3,9 @@ import {
3
3
  useMemo,
4
4
  useState
5
5
  } from 'react'
6
- import type {
7
- TextStyle } from 'react-native'
8
6
  import {
9
- StyleSheet,
10
7
  TextInput,
8
+ View,
11
9
  type TextInputProps
12
10
  } from 'react-native'
13
11
 
@@ -19,9 +17,11 @@ import {
19
17
 
20
18
  import type { ColorPairToken } from '@helpwave/hightide-design/theme-tokens'
21
19
  import { useTheme } from '../../global-contexts/theme/ThemeContext'
20
+ import { useMemoizedTheme } from '../../hooks/useMemoizedTheme'
22
21
  import type {
23
22
  InputContainerStyle,
24
23
  InputState,
24
+ InputStateLayerStyle,
25
25
  InputTextStyle
26
26
  } from '../../theme/types/components/input'
27
27
  import type { StyleOverwrite } from '../../theme/types/resolver'
@@ -52,7 +52,8 @@ export type InputProps = Omit<TextInputProps, 'value' | 'style'>
52
52
  color?: ColorPairToken,
53
53
  editCompleteOptions?: EditCompleteOptions,
54
54
  initialValue?: string,
55
- style?: StyleOverwrite<InputState, InputContainerStyle>,
55
+ style?: StyleOverwrite<InputState, InputContainerStyle>,
56
+ stateLayerStyle?: StyleOverwrite<InputState, InputStateLayerStyle>,
56
57
  textStyle?: StyleOverwrite<InputState, InputTextStyle>,
57
58
  }
58
59
 
@@ -68,7 +69,10 @@ export const Input = forwardRef<TextInput, InputProps>(function Input({
68
69
  onEditComplete,
69
70
  editCompleteOptions,
70
71
  style,
72
+ stateLayerStyle,
71
73
  textStyle,
74
+ onPointerEnter,
75
+ onPointerLeave,
72
76
  ...props
73
77
  }, ref) {
74
78
  const { theme } = useTheme()
@@ -106,91 +110,76 @@ export const Input = forwardRef<TextInput, InputProps>(function Input({
106
110
  isFocused: interactive && isFocused,
107
111
  }), [color, disabled, invalid, readOnly, interactive, isHovered, isPressed, isFocused])
108
112
 
109
- const resolvedContainerStyle = useMemo(
110
- () => theme.components.input.container(state, style),
111
- [theme, state, style]
112
- )
113
- const resolvedTextStyle = useMemo(
114
- () => theme.components.input.text(state, textStyle),
115
- [theme, state, textStyle]
116
- )
117
- const resolvedPlaceholderStyle = useMemo(
118
- () => theme.components.input.placeholder(state),
119
- [theme, state]
120
- )
121
-
122
-
123
- const resolvedStyle = useMemo(
124
- () => {
125
- const resolvedSheet: TextStyle = StyleSheet.flatten<TextStyle>([
126
- resolvedContainerStyle,
127
- resolvedTextStyle,
128
- ])
129
- // this is required for android to work
130
- // TODO find a better solution like separating container and text input
131
- delete resolvedSheet['boxShadow']
132
- return resolvedSheet
133
- },
134
- [resolvedContainerStyle, resolvedTextStyle]
135
- )
113
+ const resolvedContainerStyle = useMemoizedTheme(theme.components.input.container, state, style)
114
+ const resolvedStateLayerStyle = useMemoizedTheme(theme.components.input.stateLayer, state, stateLayerStyle)
115
+ const resolvedTextStyle = useMemoizedTheme(theme.components.input.text, state, textStyle)
116
+ const resolvedPlaceholderStyle = useMemoizedTheme(theme.components.input.placeholder, state)
136
117
 
137
118
  return (
138
- <TextInput
139
- {...props}
140
- ref={ref}
141
- value={value}
142
- editable={interactive}
143
- onChangeText={(nextValue) => {
144
- props.onChangeText?.(nextValue)
145
- restartTimer(() => {
146
- onEditComplete?.(nextValue)
147
- })
148
- setValue(nextValue)
149
- }}
150
- onFocus={(event) => {
151
- if (interactive) {
152
- setIsFocused(true)
153
- }
154
- props.onFocus?.(event)
155
- }}
156
- onBlur={(event) => {
157
- setIsFocused(false)
158
- props.onBlur?.(event)
159
- if (allowEditCompleteOnBlur) {
160
- onEditComplete?.(value ?? '')
161
- clearTimer()
162
- }
163
- }}
164
- onPressIn={(event) => {
165
- if (interactive) {
166
- setIsPressed(true)
167
- }
168
- props.onPressIn?.(event)
169
- }}
170
- onPressOut={(event) => {
171
- setIsPressed(false)
172
- props.onPressOut?.(event)
173
- }}
119
+ <View
120
+ style={resolvedContainerStyle}
174
121
  onPointerEnter={(event) => {
175
122
  if (interactive) {
176
123
  setIsHovered(true)
177
124
  }
178
- props.onPointerEnter?.(event)
125
+ onPointerEnter?.(event)
179
126
  }}
180
127
  onPointerLeave={(event) => {
181
128
  setIsHovered(false)
182
- props.onPointerLeave?.(event)
183
- }}
184
- onSubmitEditing={(event) => {
185
- props.onSubmitEditing?.(event)
186
- if (allowEnterComplete) {
187
- onEditComplete?.(value ?? '')
188
- clearTimer()
189
- }
129
+ onPointerLeave?.(event)
190
130
  }}
191
- placeholderTextColor={resolvedPlaceholderStyle.color}
192
- style={resolvedStyle}
193
- accessibilityState={{ disabled, selected: required }}
194
- />
131
+ >
132
+ <View
133
+ pointerEvents="none"
134
+ style={resolvedStateLayerStyle}
135
+ />
136
+ <TextInput
137
+ {...props}
138
+ ref={ref}
139
+ value={value}
140
+ editable={interactive}
141
+ onChangeText={(nextValue) => {
142
+ props.onChangeText?.(nextValue)
143
+ restartTimer(() => {
144
+ onEditComplete?.(nextValue)
145
+ })
146
+ setValue(nextValue)
147
+ }}
148
+ onFocus={(event) => {
149
+ if (interactive) {
150
+ setIsFocused(true)
151
+ }
152
+ props.onFocus?.(event)
153
+ }}
154
+ onBlur={(event) => {
155
+ setIsFocused(false)
156
+ props.onBlur?.(event)
157
+ if (allowEditCompleteOnBlur) {
158
+ onEditComplete?.(value ?? '')
159
+ clearTimer()
160
+ }
161
+ }}
162
+ onPressIn={(event) => {
163
+ if (interactive) {
164
+ setIsPressed(true)
165
+ }
166
+ props.onPressIn?.(event)
167
+ }}
168
+ onPressOut={(event) => {
169
+ setIsPressed(false)
170
+ props.onPressOut?.(event)
171
+ }}
172
+ onSubmitEditing={(event) => {
173
+ props.onSubmitEditing?.(event)
174
+ if (allowEnterComplete) {
175
+ onEditComplete?.(value ?? '')
176
+ clearTimer()
177
+ }
178
+ }}
179
+ placeholderTextColor={resolvedPlaceholderStyle.color}
180
+ style={resolvedTextStyle}
181
+ accessibilityState={{ disabled, selected: required }}
182
+ />
183
+ </View>
195
184
  )
196
185
  })
@@ -0,0 +1,17 @@
1
+ import { MultiSelectComponent } from './MultiSelectComponent'
2
+ import { MultiSelectContext } from './MultiSelectContext'
3
+ import { MultiSelectMenu } from './MultiSelectMenu'
4
+ import { MultiSelectOption } from './MultiSelectOption'
5
+ import { MultiSelectRoot } from './MultiSelectRoot'
6
+ import { MultiSelectTrigger } from './MultiSelectTrigger'
7
+
8
+ const MultiSelect = Object.assign(MultiSelectComponent, {
9
+ Root: MultiSelectRoot,
10
+ Trigger: MultiSelectTrigger,
11
+ Option: MultiSelectOption,
12
+ Menu: MultiSelectMenu,
13
+ Context: MultiSelectContext,
14
+ Provider: MultiSelectContext.Provider,
15
+ })
16
+
17
+ export { MultiSelect }
@@ -0,0 +1,40 @@
1
+ import type { ReactNode } from 'react'
2
+ import { View, type StyleProp, type ViewStyle } from 'react-native'
3
+
4
+ import type { MultiSelectRootProps } from './MultiSelectRoot'
5
+ import { MultiSelectRoot } from './MultiSelectRoot'
6
+ import type { MultiSelectOptionType } from './MultiSelectContext'
7
+ import { MultiSelectMenu, type MultiSelectMenuProps } from './MultiSelectMenu'
8
+ import { MultiSelectTrigger, type MultiSelectTriggerProps } from './MultiSelectTrigger'
9
+
10
+ export type MultiSelectProps<T = string> = Omit<MultiSelectRootProps<T>, 'children'> & {
11
+ children?: ReactNode,
12
+ placeholder?: MultiSelectTriggerProps<T>['placeholder'],
13
+ selectedDisplay?: (options: ReadonlyArray<MultiSelectOptionType<T>>) => ReactNode,
14
+ triggerProps?: MultiSelectTriggerProps<T>,
15
+ menuProps?: Omit<MultiSelectMenuProps, 'children'>,
16
+ style?: StyleProp<ViewStyle>,
17
+ }
18
+
19
+ export const MultiSelectComponent = <T,>({
20
+ children,
21
+ placeholder,
22
+ selectedDisplay,
23
+ triggerProps,
24
+ menuProps,
25
+ style,
26
+ ...props
27
+ }: MultiSelectProps<T>) => {
28
+ return (
29
+ <MultiSelectRoot<T> {...props}>
30
+ <View style={[{ width: '100%' }, style]}>
31
+ <MultiSelectTrigger<T>
32
+ placeholder={placeholder}
33
+ selectedDisplay={selectedDisplay}
34
+ {...triggerProps}
35
+ />
36
+ <MultiSelectMenu {...menuProps}>{children}</MultiSelectMenu>
37
+ </View>
38
+ </MultiSelectRoot>
39
+ )
40
+ }
@@ -0,0 +1,74 @@
1
+ import type { ReactNode } from 'react'
2
+ import { createContext, useContext } from 'react'
3
+
4
+ import type { ColorPairToken } from '@helpwave/hightide-design/theme-tokens'
5
+
6
+ import type { UseMultiSelectFirstHighlightBehavior } from '../../../hooks/useMultiSelect'
7
+ import type { FormFieldInteractionStates } from '../../../types/formField'
8
+
9
+ export type MultiSelectOptionIdentity<T> = {
10
+ value: T,
11
+ id: string,
12
+ }
13
+
14
+ export type MultiSelectOptionType<T = string> = {
15
+ value: MultiSelectOptionIdentity<T>,
16
+ label: string,
17
+ display?: ReactNode,
18
+ disabled?: boolean,
19
+ }
20
+
21
+ export type MultiSelectContextState<T> = Partial<FormFieldInteractionStates> & {
22
+ value: T[],
23
+ options: ReadonlyArray<MultiSelectOptionType<T>>,
24
+ selectedIds: string[],
25
+ highlightedId: string | null,
26
+ isOpen: boolean,
27
+ }
28
+
29
+ export type MultiSelectContextComputedState<T> = {
30
+ visibleOptionIds: ReadonlyArray<string>,
31
+ idToOptionMap: Record<string, MultiSelectOptionType<T>>,
32
+ }
33
+
34
+ export type MultiSelectContextActions<T> = {
35
+ registerOption: (option: MultiSelectOptionType<T>) => () => void,
36
+ toggleSelection: (id: string, isSelected?: boolean) => void,
37
+ highlightFirst: () => void,
38
+ highlightLast: () => void,
39
+ highlightNext: () => void,
40
+ highlightPrevious: () => void,
41
+ highlightItem: (id: string) => void,
42
+ handleTypeaheadKey: (key: string) => void,
43
+ setIsOpen: (open: boolean, behavior?: UseMultiSelectFirstHighlightBehavior) => void,
44
+ toggleIsOpen: (behavior?: UseMultiSelectFirstHighlightBehavior) => void,
45
+ }
46
+
47
+ export type MultiSelectContextSearch = {
48
+ hasSearch: boolean,
49
+ searchQuery: string,
50
+ setSearchQuery: (query: string) => void,
51
+ }
52
+
53
+ export type MultiSelectContextConfig = {
54
+ searchableThreshold: number,
55
+ color?: ColorPairToken,
56
+ }
57
+
58
+ export type MultiSelectContextType<T> = MultiSelectContextActions<T>
59
+ & MultiSelectContextState<T>
60
+ & MultiSelectContextComputedState<T>
61
+ & {
62
+ config: MultiSelectContextConfig,
63
+ search: MultiSelectContextSearch,
64
+ }
65
+
66
+ export const MultiSelectContext = createContext<MultiSelectContextType<unknown> | null>(null)
67
+
68
+ export function useMultiSelectContext<T>(): MultiSelectContextType<T> {
69
+ const ctx = useContext(MultiSelectContext)
70
+ if (!ctx) {
71
+ throw new Error('useMultiSelectContext must be used within MultiSelectRoot')
72
+ }
73
+ return ctx as MultiSelectContextType<T>
74
+ }
@@ -0,0 +1,87 @@
1
+ import type { ReactNode } from 'react'
2
+ import { Modal, Pressable, ScrollView, View } from 'react-native'
3
+
4
+ import { useTranslation } from '@helpwave/hightide-utils/context'
5
+
6
+ import { useTheme } from '../../../global-contexts/theme/ThemeContext'
7
+ import { useMemoizedTheme } from '../../../hooks/useMemoizedTheme'
8
+ import { ThemedText } from '../../visualization-and-display/ThemedText'
9
+ import { SearchBar } from '../SearchBar'
10
+ import { useMultiSelectContext } from './MultiSelectContext'
11
+
12
+ export type MultiSelectMenuProps = {
13
+ children?: ReactNode,
14
+ }
15
+
16
+ export const MultiSelectMenu = ({ children }: MultiSelectMenuProps) => {
17
+ const { theme } = useTheme()
18
+ const translation = useTranslation()
19
+ const context = useMultiSelectContext()
20
+ const multiSelectTheme = theme.components.multiSelect
21
+ const isSearchVisible = context.search.hasSearch
22
+ const visibleCount = context.visibleOptionIds.length
23
+ const showEmptySearchResults = isSearchVisible
24
+ && context.search.searchQuery.trim().length > 0
25
+ && visibleCount === 0
26
+
27
+ const resolvedOverlayStyle = useMemoizedTheme(multiSelectTheme.overlay, {})
28
+ const resolvedMenuStyle = useMemoizedTheme(multiSelectTheme.menu, { hasSearch: isSearchVisible })
29
+ const resolvedHeaderStyle = useMemoizedTheme(multiSelectTheme.header, {})
30
+ const resolvedEmptyTextStyle = useMemoizedTheme(multiSelectTheme.emptyText, {})
31
+
32
+ return (
33
+ <>
34
+ {!context.isOpen && (
35
+ <View
36
+ collapsable={false}
37
+ pointerEvents="none"
38
+ style={{ width: 0, height: 0, overflow: 'hidden' }}
39
+ >
40
+ {children}
41
+ </View>
42
+ )}
43
+ <Modal
44
+ visible={context.isOpen}
45
+ transparent
46
+ animationType="fade"
47
+ onRequestClose={() => context.setIsOpen(false)}
48
+ >
49
+ <Pressable
50
+ style={resolvedOverlayStyle}
51
+ onPress={() => context.setIsOpen(false)}
52
+ >
53
+ <Pressable
54
+ style={resolvedMenuStyle}
55
+ onPress={(event) => event.stopPropagation()}
56
+ >
57
+ {isSearchVisible && (
58
+ <View style={resolvedHeaderStyle}>
59
+ <SearchBar
60
+ value={context.search.searchQuery}
61
+ onValueChange={context.search.setSearchQuery}
62
+ onSearch={context.search.setSearchQuery}
63
+ />
64
+ </View>
65
+ )}
66
+ {showEmptySearchResults && (
67
+ <View
68
+ style={{
69
+ flex: 1,
70
+ justifyContent: 'center',
71
+ alignItems: 'center',
72
+ }}
73
+ >
74
+ <ThemedText style={resolvedEmptyTextStyle}>
75
+ {translation('nothingFound')}
76
+ </ThemedText>
77
+ </View>
78
+ )}
79
+ <ScrollView>
80
+ {children}
81
+ </ScrollView>
82
+ </Pressable>
83
+ </Pressable>
84
+ </Modal>
85
+ </>
86
+ )
87
+ }
@@ -0,0 +1,106 @@
1
+ import type { ReactNode } from 'react'
2
+ import { useEffect, useMemo } from 'react'
3
+ import { View } from 'react-native'
4
+
5
+ import { useTheme } from '../../../global-contexts/theme/ThemeContext'
6
+ import { useMemoizedTheme } from '../../../hooks/useMemoizedTheme'
7
+ import { HightideIconRegistry } from '../../../icons/HightideIconRegistry'
8
+ import type { IconStyle } from '../../../icons'
9
+ import type { MultiSelectOptionState } from '../../../theme/types/components/multiSelect'
10
+ import { ListActionItem } from '../../list/ListActionItem'
11
+ import { ThemedIcon } from '../../visualization-and-display/ThemedIcon'
12
+ import type { MultiSelectOptionIdentity } from './MultiSelectContext'
13
+ import { useMultiSelectContext } from './MultiSelectContext'
14
+
15
+ export type MultiSelectOptionProps<T = string> = (
16
+ T extends string
17
+ ? {
18
+ valueId?: undefined,
19
+ value: T,
20
+ label?: string,
21
+ }
22
+ : {
23
+ valueId: string,
24
+ value: T,
25
+ label: string,
26
+ }
27
+ ) & {
28
+ disabled?: boolean,
29
+ children?: ReactNode,
30
+ }
31
+
32
+ const toIdentity = <T,>(value: T, valueId: string | undefined): MultiSelectOptionIdentity<T> => {
33
+ if (valueId === undefined) {
34
+ return { value, id: value as string }
35
+ }
36
+ return { value, id: valueId }
37
+ }
38
+
39
+ export const MultiSelectOption = <T,>({
40
+ value,
41
+ valueId,
42
+ label,
43
+ disabled = false,
44
+ children,
45
+ }: MultiSelectOptionProps<T>) => {
46
+ const { theme } = useTheme()
47
+ const context = useMultiSelectContext<T>()
48
+ const { registerOption } = context
49
+ const identity = useMemo(() => toIdentity<T>(value, valueId), [value, valueId])
50
+ const resolvedLabel: string = valueId === undefined ? (label ?? (value as string)) : label
51
+ const display = children ?? resolvedLabel
52
+ const optionId = identity.id
53
+
54
+ useEffect(() => {
55
+ return registerOption({
56
+ value: identity,
57
+ label: resolvedLabel,
58
+ display,
59
+ disabled,
60
+ })
61
+ }, [disabled, display, identity, registerOption, resolvedLabel])
62
+
63
+ const isSelected = context.selectedIds.includes(optionId)
64
+ const isVisible = context.visibleOptionIds.includes(optionId)
65
+ const optionColor = isSelected
66
+ ? (context.config.color ?? theme.colors.primary)
67
+ : undefined
68
+
69
+ const optionState = useMemo((): MultiSelectOptionState => ({
70
+ color: context.config.color,
71
+ isSelected,
72
+ isHighlighted: context.highlightedId === optionId,
73
+ isDisabled: disabled,
74
+ }), [context.config.color, context.highlightedId, disabled, optionId, isSelected])
75
+
76
+ const multiSelectTheme = theme.components.multiSelect
77
+ const resolvedCheckboxStyle = useMemoizedTheme(multiSelectTheme.checkbox, optionState)
78
+ const checkboxIcon = useMemoizedTheme<MultiSelectOptionState, IconStyle>(
79
+ multiSelectTheme.checkboxIcon,
80
+ optionState
81
+ )
82
+
83
+ if (!context.isOpen || !isVisible) {
84
+ return null
85
+ }
86
+
87
+ return (
88
+ <ListActionItem
89
+ title={children ? undefined : resolvedLabel}
90
+ content={children}
91
+ color={optionColor}
92
+ disabled={disabled}
93
+ onPress={() => context.toggleSelection(optionId)}
94
+ leading={(
95
+ <View style={resolvedCheckboxStyle}>
96
+ <ThemedIcon
97
+ icon={HightideIconRegistry.Check}
98
+ size={checkboxIcon.size}
99
+ strokeWidth={checkboxIcon.strokeWidth}
100
+ color={checkboxIcon.color}
101
+ />
102
+ </View>
103
+ )}
104
+ />
105
+ )
106
+ }