@helpwave/hightide-native 0.7.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.
package/package.json CHANGED
@@ -10,7 +10,7 @@
10
10
  "access": "public"
11
11
  },
12
12
  "license": "AGPL-3.0-only",
13
- "version": "0.7.0",
13
+ "version": "0.9.0",
14
14
  "type": "module",
15
15
  "files": [
16
16
  "src"
@@ -47,8 +47,8 @@
47
47
  },
48
48
  "dependencies": {
49
49
  "lucide-react-native": "0.561.0",
50
- "@helpwave/hightide-utils": "0.1.1",
51
- "@helpwave/hightide-design": "0.7.0"
50
+ "@helpwave/hightide-design": "0.7.2",
51
+ "@helpwave/hightide-utils": "0.1.3"
52
52
  },
53
53
  "peerDependencies": {
54
54
  "@react-native-async-storage/async-storage": ">=2.0.0",
@@ -76,11 +76,9 @@
76
76
  "url": "https://github.com/helpwave/hightide/issues"
77
77
  },
78
78
  "scripts": {
79
- "clean": "pnpm run barrel -- --clean && rm -rf node_modules",
80
- "init": "pnpm run barrel",
81
- "barrel": "node node_modules/@helpwave/hightide-utils/scripts/barrel.js",
82
- "barrel:check": "node node_modules/@helpwave/hightide-utils/scripts/barrel.js --check",
83
- "build": "pnpm run barrel",
79
+ "clean": "rm -rf node_modules",
80
+ "init": "node -e \"process.exit(0)\"",
81
+ "build": "node -e \"process.exit(0)\"",
84
82
  "lint": "eslint .",
85
83
  "test": "node -e \"process.exit(0)\""
86
84
  }
@@ -191,7 +191,7 @@ export const ChatAttachmentMessageBubble = ({
191
191
  downloadLabel = 'Download',
192
192
  onDownload,
193
193
  timestamp,
194
- messageStatus,
194
+ status,
195
195
  style,
196
196
  bodyStyle,
197
197
  bodyTextStyle,
@@ -273,7 +273,7 @@ export const ChatAttachmentMessageBubble = ({
273
273
  {...props}
274
274
  direction={direction}
275
275
  timestamp={timestamp}
276
- messageStatus={messageStatus}
276
+ status={status}
277
277
  style={resolvedContainerStyle}
278
278
  bodyStyle={resolvedBodyStyle}
279
279
  bodyTextStyle={resolvedBodyTextStyle}
@@ -39,7 +39,7 @@ export type { ChatMessageDirection, ChatMessageStatus }
39
39
  export type ChatMessageBubbleProps = Omit<ViewProps, 'children' | 'style'> & {
40
40
  direction: ChatMessageDirection,
41
41
  timestamp?: Date,
42
- messageStatus?: ChatMessageStatus,
42
+ status?: ChatMessageStatus,
43
43
  children?: ReactNode,
44
44
  style?: StyleOverwrite<ChatMessageBubbleState, ChatMessageBubbleContainerStyle>,
45
45
  bodyStyle?: StyleOverwrite<ChatMessageBubbleState, ChatMessageBubbleBodyStyle>,
@@ -49,16 +49,17 @@ export type ChatMessageBubbleProps = Omit<ViewProps, 'children' | 'style'> & {
49
49
  metaDataTextStyle?: StyleOverwrite<ChatMessageBubbleState, ChatMessageBubbleMetaDataTextStyle>,
50
50
  }
51
51
 
52
- const resolveMessageStatusIcon = (messageStatus: ChatMessageStatus): IconComponent => (
53
- messageStatus === 'sent'
52
+ const resolveMessageStatusIcon = (status: ChatMessageStatus): IconComponent => (
53
+ status === 'sending'
54
54
  ? HightideIconRegistry.Clock
55
- : HightideIconRegistry.CheckCheck
55
+ : status === 'sent' ? HightideIconRegistry.Check
56
+ : HightideIconRegistry.CheckCheck
56
57
  )
57
58
 
58
59
  export const ChatMessageBubble = ({
59
60
  direction,
60
61
  timestamp,
61
- messageStatus,
62
+ status,
62
63
  children,
63
64
  style,
64
65
  bodyStyle,
@@ -86,14 +87,14 @@ export const ChatMessageBubble = ({
86
87
  : DateUtils.formatAbsolute(timestamp, locale, 'time', { timeZone, is24HourFormat })
87
88
  ), [timestamp, locale, timeZone, is24HourFormat])
88
89
  const messageStatusIcon = useMemo(
89
- () => (messageStatus === undefined ? undefined : resolveMessageStatusIcon(messageStatus)),
90
- [messageStatus]
90
+ () => (status === undefined ? undefined : resolveMessageStatusIcon(status)),
91
+ [status]
91
92
  )
92
93
  const messageStatusIconColor = useMemo(() => (
93
- messageStatus === 'read'
94
+ status === 'read'
94
95
  ? theme.colors.primary.color
95
96
  : resolvedMetaDataIcon.color
96
- ), [messageStatus, resolvedMetaDataIcon.color, theme.colors.primary.color])
97
+ ), [status, resolvedMetaDataIcon.color, theme.colors.primary.color])
97
98
 
98
99
  return (
99
100
  <View {...props} style={resolvedContainerStyle}>
@@ -1,40 +1,17 @@
1
- import type { ReactNode } from 'react'
2
- import { View, type StyleProp, type ViewStyle } from 'react-native'
3
-
4
- import type { MultiSelectRootProps } from './MultiSelectRoot'
1
+ import { MultiSelectComponent } from './MultiSelectComponent'
2
+ import { MultiSelectContext } from './MultiSelectContext'
3
+ import { MultiSelectMenu } from './MultiSelectMenu'
4
+ import { MultiSelectOption } from './MultiSelectOption'
5
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'
6
+ import { MultiSelectTrigger } from './MultiSelectTrigger'
9
7
 
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
- }
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
+ })
18
16
 
19
- export const MultiSelect = <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
- }
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
+ }
@@ -6,10 +6,14 @@ import type { ColorPairToken } from '@helpwave/hightide-design/theme-tokens'
6
6
  import type { UseMultiSelectFirstHighlightBehavior } from '../../../hooks/useMultiSelect'
7
7
  import type { FormFieldInteractionStates } from '../../../types/formField'
8
8
 
9
- export type MultiSelectOptionType<T = string> = {
10
- id: string,
9
+ export type MultiSelectOptionIdentity<T> = {
11
10
  value: T,
12
- label?: string,
11
+ id: string,
12
+ }
13
+
14
+ export type MultiSelectOptionType<T = string> = {
15
+ value: MultiSelectOptionIdentity<T>,
16
+ label: string,
13
17
  display?: ReactNode,
14
18
  disabled?: boolean,
15
19
  }
@@ -9,19 +9,36 @@ import type { IconStyle } from '../../../icons'
9
9
  import type { MultiSelectOptionState } from '../../../theme/types/components/multiSelect'
10
10
  import { ListActionItem } from '../../list/ListActionItem'
11
11
  import { ThemedIcon } from '../../visualization-and-display/ThemedIcon'
12
+ import type { MultiSelectOptionIdentity } from './MultiSelectContext'
12
13
  import { useMultiSelectContext } from './MultiSelectContext'
13
14
 
14
- export type MultiSelectOptionProps<T = string> = {
15
- id: string,
16
- value: T,
17
- label: string,
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
+ ) & {
18
28
  disabled?: boolean,
19
29
  children?: ReactNode,
20
30
  }
21
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
+
22
39
  export const MultiSelectOption = <T,>({
23
- id,
24
40
  value,
41
+ valueId,
25
42
  label,
26
43
  disabled = false,
27
44
  children,
@@ -29,20 +46,22 @@ export const MultiSelectOption = <T,>({
29
46
  const { theme } = useTheme()
30
47
  const context = useMultiSelectContext<T>()
31
48
  const { registerOption } = context
32
- const display = children ?? label
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
33
53
 
34
54
  useEffect(() => {
35
55
  return registerOption({
36
- id,
37
- value,
38
- label,
56
+ value: identity,
57
+ label: resolvedLabel,
39
58
  display,
40
59
  disabled,
41
60
  })
42
- }, [disabled, display, id, label, registerOption, value])
61
+ }, [disabled, display, identity, registerOption, resolvedLabel])
43
62
 
44
- const isSelected = context.selectedIds.includes(id)
45
- const isVisible = context.visibleOptionIds.includes(id)
63
+ const isSelected = context.selectedIds.includes(optionId)
64
+ const isVisible = context.visibleOptionIds.includes(optionId)
46
65
  const optionColor = isSelected
47
66
  ? (context.config.color ?? theme.colors.primary)
48
67
  : undefined
@@ -50,9 +69,9 @@ export const MultiSelectOption = <T,>({
50
69
  const optionState = useMemo((): MultiSelectOptionState => ({
51
70
  color: context.config.color,
52
71
  isSelected,
53
- isHighlighted: context.highlightedId === id,
72
+ isHighlighted: context.highlightedId === optionId,
54
73
  isDisabled: disabled,
55
- }), [context.config.color, context.highlightedId, disabled, id, isSelected])
74
+ }), [context.config.color, context.highlightedId, disabled, optionId, isSelected])
56
75
 
57
76
  const multiSelectTheme = theme.components.multiSelect
58
77
  const resolvedCheckboxStyle = useMemoizedTheme(multiSelectTheme.checkbox, optionState)
@@ -67,11 +86,11 @@ export const MultiSelectOption = <T,>({
67
86
 
68
87
  return (
69
88
  <ListActionItem
70
- title={children ? undefined : label}
89
+ title={children ? undefined : resolvedLabel}
71
90
  content={children}
72
91
  color={optionColor}
73
92
  disabled={disabled}
74
- onPress={() => context.toggleSelection(id)}
93
+ onPress={() => context.toggleSelection(optionId)}
75
94
  leading={(
76
95
  <View style={resolvedCheckboxStyle}>
77
96
  <ThemedIcon
@@ -34,7 +34,7 @@ const mergeOptionMaps = <T,>(
34
34
  ): Record<string, MultiSelectOptionType<T>> => {
35
35
  const merged = { ...snapshots }
36
36
  for (const option of live) {
37
- merged[option.id] = option
37
+ merged[option.value.id] = option
38
38
  }
39
39
  return merged
40
40
  }
@@ -59,14 +59,14 @@ export function MultiSelectRoot<T>({
59
59
  const [optionSnapshots, setOptionSnapshots] = useState<Record<string, MultiSelectOptionType<T>>>({})
60
60
 
61
61
  const registerOption = useCallback((item: MultiSelectOptionType<T>) => {
62
- setOptionSnapshots((previous) => ({ ...previous, [item.id]: item }))
62
+ setOptionSnapshots((previous) => ({ ...previous, [item.value.id]: item }))
63
63
  setOptions((previous) => {
64
- const next = previous.filter((option) => option.id !== item.id)
64
+ const next = previous.filter((option) => option.value.id !== item.value.id)
65
65
  next.push(item)
66
66
  return next
67
67
  })
68
68
  return () => {
69
- setOptions((previous) => previous.filter((option) => option.id !== item.id))
69
+ setOptions((previous) => previous.filter((option) => option.value.id !== item.value.id))
70
70
  }
71
71
  }, [])
72
72
 
@@ -82,7 +82,7 @@ export function MultiSelectRoot<T>({
82
82
  }
83
83
  const known = Object.values(idToOptionMap)
84
84
  return value
85
- .map((item) => known.find((option) => compare(option.value, item))?.id)
85
+ .map((item) => known.find((option) => compare(option.value.value, item))?.value.id)
86
86
  .filter((id): id is string => id !== undefined)
87
87
  }, [compare, idToOptionMap, value])
88
88
 
@@ -92,7 +92,7 @@ export function MultiSelectRoot<T>({
92
92
  }
93
93
  const known = Object.values(idToOptionMap)
94
94
  return initialValue
95
- .map((item) => known.find((option) => compare(option.value, item))?.id)
95
+ .map((item) => known.find((option) => compare(option.value.value, item))?.value.id)
96
96
  .filter((id): id is string => id !== undefined)
97
97
  }, [compare, idToOptionMap, initialValue])
98
98
 
@@ -101,21 +101,21 @@ export function MultiSelectRoot<T>({
101
101
 
102
102
  const onValueChangeWrapper = useCallback((ids: string[]) => {
103
103
  const values = ids
104
- .map((id) => idToOptionMap[id]?.value)
104
+ .map((id) => idToOptionMap[id]?.value.value)
105
105
  .filter((item): item is T => item != null)
106
106
  onValueChangeStable(values)
107
107
  }, [idToOptionMap, onValueChangeStable])
108
108
 
109
109
  const onEditCompleteWrapper = useCallback((ids: string[]) => {
110
110
  const values = ids
111
- .map((id) => idToOptionMap[id]?.value)
111
+ .map((id) => idToOptionMap[id]?.value.value)
112
112
  .filter((item): item is T => item != null)
113
113
  onEditCompleteStable(values)
114
114
  }, [idToOptionMap, onEditCompleteStable])
115
115
 
116
116
  const hookOptions = useMemo(
117
117
  () => options.map((option) => ({
118
- id: option.id,
118
+ id: option.value.id,
119
119
  label: option.label,
120
120
  disabled: option.disabled,
121
121
  })),
@@ -135,7 +135,7 @@ export function MultiSelectRoot<T>({
135
135
  const knownOptionCount = Math.max(options.length, Object.keys(optionSnapshots).length)
136
136
  const selectedValues = useMemo(
137
137
  () => state.value
138
- .map((id) => idToOptionMap[id]?.value)
138
+ .map((id) => idToOptionMap[id]?.value.value)
139
139
  .filter((item): item is T => item != null),
140
140
  [idToOptionMap, state.value]
141
141
  )
@@ -78,8 +78,8 @@ export const MultiSelectTrigger = <T,>({
78
78
  ) : selectedOptions.length > 0 ? (
79
79
  <View style={{ flexDirection: 'row', flexWrap: 'wrap', gap: 6 }}>
80
80
  {selectedOptions.map((option) => (
81
- <Chip key={option.id} size="md" color={context.config.color} variant="tonal">
82
- <ThemedText>{option.label ?? String(option.value)}</ThemedText>
81
+ <Chip key={option.value.id} size="md" color={context.config.color} variant="tonal">
82
+ <ThemedText>{option.label ?? String(option.value.value)}</ThemedText>
83
83
  {!context.readOnly && (
84
84
  <View
85
85
  style={{
@@ -94,7 +94,7 @@ export const MultiSelectTrigger = <T,>({
94
94
  color={theme.colors.negative}
95
95
  variant="foreground"
96
96
  disabled={!interactive}
97
- onPress={() => context.toggleSelection(option.id, false)}
97
+ onPress={() => context.toggleSelection(option.value.id, false)}
98
98
  style={{
99
99
  position: 'absolute',
100
100
  left: '50%',
@@ -1,6 +1,2 @@
1
- export * from './MultiSelect'
2
- export * from './MultiSelectContext'
3
- export * from './MultiSelectMenu'
4
- export * from './MultiSelectOption'
5
- export * from './MultiSelectRoot'
6
- export * from './MultiSelectTrigger'
1
+ export { MultiSelect } from './MultiSelect'
2
+ export type { MultiSelectProps } from './MultiSelectComponent'
@@ -1,40 +1,17 @@
1
- import type { ReactNode } from 'react'
2
- import { View, type StyleProp, type ViewStyle } from 'react-native'
3
-
4
- import type { SelectRootProps } from './SelectRoot'
1
+ import { SelectComponent } from './SelectComponent'
2
+ import { SelectContext } from './SelectContext'
3
+ import { SelectMenu } from './SelectMenu'
4
+ import { SelectOption } from './SelectOption'
5
5
  import { SelectRoot } from './SelectRoot'
6
- import type { SelectOptionType } from './SelectContext'
7
- import { SelectMenu, type SelectMenuProps } from './SelectMenu'
8
- import { SelectTrigger, type SelectTriggerProps } from './SelectTrigger'
6
+ import { SelectTrigger } from './SelectTrigger'
9
7
 
10
- export type SelectProps<T = string> = Omit<SelectRootProps<T>, 'children'> & {
11
- children?: ReactNode,
12
- placeholder?: SelectTriggerProps<T>['placeholder'],
13
- selectedDisplay?: (option: SelectOptionType<T> | null) => ReactNode,
14
- triggerProps?: SelectTriggerProps<T>,
15
- menuProps?: Omit<SelectMenuProps, 'children'>,
16
- style?: StyleProp<ViewStyle>,
17
- }
8
+ const Select = Object.assign(SelectComponent, {
9
+ Root: SelectRoot,
10
+ Trigger: SelectTrigger,
11
+ Option: SelectOption,
12
+ Menu: SelectMenu,
13
+ Context: SelectContext,
14
+ Provider: SelectContext.Provider,
15
+ })
18
16
 
19
- export const Select = <T,>({
20
- children,
21
- placeholder,
22
- selectedDisplay,
23
- triggerProps,
24
- menuProps,
25
- style,
26
- ...props
27
- }: SelectProps<T>) => {
28
- return (
29
- <SelectRoot<T> {...props}>
30
- <View style={style}>
31
- <SelectTrigger<T>
32
- placeholder={placeholder}
33
- selectedDisplay={selectedDisplay}
34
- {...triggerProps}
35
- />
36
- <SelectMenu {...menuProps}>{children}</SelectMenu>
37
- </View>
38
- </SelectRoot>
39
- )
40
- }
17
+ export { Select }
@@ -0,0 +1,40 @@
1
+ import type { ReactNode } from 'react'
2
+ import { View, type StyleProp, type ViewStyle } from 'react-native'
3
+
4
+ import type { SelectRootProps } from './SelectRoot'
5
+ import { SelectRoot } from './SelectRoot'
6
+ import type { SelectOptionType } from './SelectContext'
7
+ import { SelectMenu, type SelectMenuProps } from './SelectMenu'
8
+ import { SelectTrigger, type SelectTriggerProps } from './SelectTrigger'
9
+
10
+ export type SelectProps<T = string> = Omit<SelectRootProps<T>, 'children'> & {
11
+ children?: ReactNode,
12
+ placeholder?: SelectTriggerProps<T>['placeholder'],
13
+ selectedDisplay?: (option: SelectOptionType<T> | null) => ReactNode,
14
+ triggerProps?: SelectTriggerProps<T>,
15
+ menuProps?: Omit<SelectMenuProps, 'children'>,
16
+ style?: StyleProp<ViewStyle>,
17
+ }
18
+
19
+ export const SelectComponent = <T,>({
20
+ children,
21
+ placeholder,
22
+ selectedDisplay,
23
+ triggerProps,
24
+ menuProps,
25
+ style,
26
+ ...props
27
+ }: SelectProps<T>) => {
28
+ return (
29
+ <SelectRoot<T> {...props}>
30
+ <View style={style}>
31
+ <SelectTrigger<T>
32
+ placeholder={placeholder}
33
+ selectedDisplay={selectedDisplay}
34
+ {...triggerProps}
35
+ />
36
+ <SelectMenu {...menuProps}>{children}</SelectMenu>
37
+ </View>
38
+ </SelectRoot>
39
+ )
40
+ }
@@ -6,10 +6,14 @@ import type { ColorPairToken } from '@helpwave/hightide-design/theme-tokens'
6
6
  import type { UseSelectFirstHighlightBehavior } from '../../../hooks/useSelect'
7
7
  import type { FormFieldInteractionStates } from '../../../types/formField'
8
8
 
9
- export type SelectOptionType<T = string> = {
10
- id: string,
9
+ export type SelectOptionIdentity<T> = {
11
10
  value: T,
12
- label?: string,
11
+ id: string,
12
+ }
13
+
14
+ export type SelectOptionType<T = string> = {
15
+ value: SelectOptionIdentity<T>,
16
+ label: string,
13
17
  display?: ReactNode,
14
18
  disabled?: boolean,
15
19
  }
@@ -8,19 +8,36 @@ import type { IconStyle } from '../../../icons'
8
8
  import type { MultiSelectOptionState } from '../../../theme/types/components/multiSelect'
9
9
  import { ListActionItem } from '../../list/ListActionItem'
10
10
  import { ThemedIcon } from '../../visualization-and-display/ThemedIcon'
11
+ import type { SelectOptionIdentity } from './SelectContext'
11
12
  import { useSelectContext } from './SelectContext'
12
13
 
13
- export type SelectOptionProps<T = string> = {
14
- id: string,
15
- value: T,
16
- label: string,
14
+ export type SelectOptionProps<T = string> = (
15
+ T extends string
16
+ ? {
17
+ valueId?: undefined,
18
+ value: T,
19
+ label?: string,
20
+ }
21
+ : {
22
+ valueId: string,
23
+ value: T,
24
+ label: string,
25
+ }
26
+ ) & {
17
27
  disabled?: boolean,
18
28
  children?: ReactNode,
19
29
  }
20
30
 
31
+ const toIdentity = <T,>(value: T, valueId: string | undefined): SelectOptionIdentity<T> => {
32
+ if (valueId === undefined) {
33
+ return { value, id: value as string }
34
+ }
35
+ return { value, id: valueId }
36
+ }
37
+
21
38
  export const SelectOption = <T,>({
22
- id,
23
39
  value,
40
+ valueId,
24
41
  label,
25
42
  disabled = false,
26
43
  children,
@@ -28,26 +45,28 @@ export const SelectOption = <T,>({
28
45
  const { theme } = useTheme()
29
46
  const context = useSelectContext<T>()
30
47
  const { registerOption } = context
31
- const display = children ?? label
48
+ const identity = useMemo(() => toIdentity<T>(value, valueId), [value, valueId])
49
+ const resolvedLabel: string = valueId === undefined ? (label ?? (value as string)) : label
50
+ const display = children ?? resolvedLabel
51
+ const optionId = identity.id
32
52
 
33
53
  useEffect(() => {
34
54
  return registerOption({
35
- id,
36
- value,
37
- label,
55
+ value: identity,
56
+ label: resolvedLabel,
38
57
  display,
39
58
  disabled,
40
59
  })
41
- }, [disabled, display, id, label, registerOption, value])
60
+ }, [disabled, display, identity, registerOption, resolvedLabel])
42
61
 
43
- const isSelected = context.selectedId === id
44
- const isVisible = context.visibleOptionIds.includes(id)
62
+ const isSelected = context.selectedId === optionId
63
+ const isVisible = context.visibleOptionIds.includes(optionId)
45
64
 
46
65
  const optionState = useMemo((): MultiSelectOptionState => ({
47
66
  isSelected,
48
- isHighlighted: context.highlightedId === id,
67
+ isHighlighted: context.highlightedId === optionId,
49
68
  isDisabled: disabled,
50
- }), [context.highlightedId, disabled, id, isSelected])
69
+ }), [context.highlightedId, disabled, optionId, isSelected])
51
70
 
52
71
  const checkIcon = useMemoizedTheme<MultiSelectOptionState, IconStyle>(
53
72
  theme.components.listItem.action.icon,
@@ -60,10 +79,10 @@ export const SelectOption = <T,>({
60
79
 
61
80
  return (
62
81
  <ListActionItem
63
- title={children ? undefined : label}
82
+ title={children ? undefined : resolvedLabel}
64
83
  content={children}
65
84
  disabled={disabled}
66
- onPress={() => context.toggleSelection(id)}
85
+ onPress={() => context.toggleSelection(optionId)}
67
86
  leading={(
68
87
  <ThemedIcon
69
88
  icon={HightideIconRegistry.Check}
@@ -35,7 +35,7 @@ const mergeOptionMaps = <T,>(
35
35
  ): Record<string, SelectOptionType<T>> => {
36
36
  const merged = { ...snapshots }
37
37
  for (const option of live) {
38
- merged[option.id] = option
38
+ merged[option.value.id] = option
39
39
  }
40
40
  return merged
41
41
  }
@@ -61,14 +61,14 @@ export function SelectRoot<T>({
61
61
  const [optionSnapshots, setOptionSnapshots] = useState<Record<string, SelectOptionType<T>>>({})
62
62
 
63
63
  const registerOption = useCallback((item: SelectOptionType<T>) => {
64
- setOptionSnapshots((previous) => ({ ...previous, [item.id]: item }))
64
+ setOptionSnapshots((previous) => ({ ...previous, [item.value.id]: item }))
65
65
  setOptions((previous) => {
66
- const next = previous.filter((option) => option.value !== item.value)
66
+ const next = previous.filter((option) => option.value.id !== item.value.id)
67
67
  next.push(item)
68
68
  return next
69
69
  })
70
70
  return () => {
71
- setOptions((previous) => previous.filter((option) => option.id !== item.id))
71
+ setOptions((previous) => previous.filter((option) => option.value.id !== item.value.id))
72
72
  }
73
73
  }, [])
74
74
 
@@ -82,14 +82,14 @@ export function SelectRoot<T>({
82
82
  if (value === undefined) {
83
83
  return undefined
84
84
  }
85
- return Object.values(idToOptionMap).find((option) => compare(option.value, value))?.id ?? null
85
+ return Object.values(idToOptionMap).find((option) => compare(option.value.value, value))?.value.id ?? null
86
86
  }, [compare, idToOptionMap, value])
87
87
 
88
88
  const mappedInitialValueId = useMemo(() => {
89
89
  if (initialValue === undefined) {
90
90
  return undefined
91
91
  }
92
- return Object.values(idToOptionMap).find((option) => compare(option.value, initialValue))?.id ?? null
92
+ return Object.values(idToOptionMap).find((option) => compare(option.value.value, initialValue))?.value.id ?? null
93
93
  }, [compare, idToOptionMap, initialValue])
94
94
 
95
95
  const onValueChangeStable = useEventCallbackStabilizer(onValueChange)
@@ -101,7 +101,7 @@ export function SelectRoot<T>({
101
101
  if (option === undefined) {
102
102
  return
103
103
  }
104
- onValueChangeStable(option.value)
104
+ onValueChangeStable(option.value.value)
105
105
  }, [idToOptionMap, onValueChangeStable])
106
106
 
107
107
  const onEditCompleteWrapper = useCallback((id: string) => {
@@ -109,12 +109,12 @@ export function SelectRoot<T>({
109
109
  if (option === undefined) {
110
110
  return
111
111
  }
112
- onEditCompleteStable(option.value)
112
+ onEditCompleteStable(option.value.value)
113
113
  }, [idToOptionMap, onEditCompleteStable])
114
114
 
115
115
  const hookOptions = useMemo(
116
116
  () => options.map((option) => ({
117
- id: option.id,
117
+ id: option.value.id,
118
118
  label: option.label,
119
119
  disabled: option.disabled,
120
120
  })),
@@ -1,6 +1,2 @@
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
+ export { Select } from './Select'
2
+ export type { SelectProps } from './SelectComponent'