@helpwave/hightide-native 0.7.0 → 0.9.1
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 +6 -8
- package/src/components/chat/ChatAttachmentMessageBubble.tsx +2 -2
- package/src/components/chat/ChatMessageBubble.tsx +10 -9
- package/src/components/user-interaction/Button.tsx +5 -2
- package/src/components/user-interaction/MultiSelect/MultiSelect.tsx +14 -37
- package/src/components/user-interaction/MultiSelect/MultiSelectComponent.tsx +40 -0
- package/src/components/user-interaction/MultiSelect/MultiSelectContext.tsx +7 -3
- package/src/components/user-interaction/MultiSelect/MultiSelectOption.tsx +35 -16
- package/src/components/user-interaction/MultiSelect/MultiSelectRoot.tsx +10 -10
- package/src/components/user-interaction/MultiSelect/MultiSelectTrigger.tsx +3 -3
- package/src/components/user-interaction/MultiSelect/index.ts +2 -6
- package/src/components/user-interaction/Select/Select.tsx +14 -37
- package/src/components/user-interaction/Select/SelectComponent.tsx +40 -0
- package/src/components/user-interaction/Select/SelectContext.tsx +7 -3
- package/src/components/user-interaction/Select/SelectOption.tsx +35 -16
- package/src/components/user-interaction/Select/SelectRoot.tsx +9 -9
- package/src/components/user-interaction/Select/index.ts +2 -6
package/package.json
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"access": "public"
|
|
11
11
|
},
|
|
12
12
|
"license": "AGPL-3.0-only",
|
|
13
|
-
"version": "0.
|
|
13
|
+
"version": "0.9.1",
|
|
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-
|
|
51
|
-
"@helpwave/hightide-
|
|
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": "
|
|
80
|
-
"init": "
|
|
81
|
-
"
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 = (
|
|
53
|
-
|
|
52
|
+
const resolveMessageStatusIcon = (status: ChatMessageStatus): IconComponent => (
|
|
53
|
+
status === 'sending'
|
|
54
54
|
? HightideIconRegistry.Clock
|
|
55
|
-
: HightideIconRegistry.
|
|
55
|
+
: status === 'sent' ? HightideIconRegistry.Check
|
|
56
|
+
: HightideIconRegistry.CheckCheck
|
|
56
57
|
)
|
|
57
58
|
|
|
58
59
|
export const ChatMessageBubble = ({
|
|
59
60
|
direction,
|
|
60
61
|
timestamp,
|
|
61
|
-
|
|
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
|
-
() => (
|
|
90
|
-
[
|
|
90
|
+
() => (status === undefined ? undefined : resolveMessageStatusIcon(status)),
|
|
91
|
+
[status]
|
|
91
92
|
)
|
|
92
93
|
const messageStatusIconColor = useMemo(() => (
|
|
93
|
-
|
|
94
|
+
status === 'read'
|
|
94
95
|
? theme.colors.primary.color
|
|
95
96
|
: resolvedMetaDataIcon.color
|
|
96
|
-
), [
|
|
97
|
+
), [status, resolvedMetaDataIcon.color, theme.colors.primary.color])
|
|
97
98
|
|
|
98
99
|
return (
|
|
99
100
|
<View {...props} style={resolvedContainerStyle}>
|
|
@@ -2,7 +2,8 @@ import { forwardRef, useMemo, useState } from 'react'
|
|
|
2
2
|
import {
|
|
3
3
|
Pressable,
|
|
4
4
|
View,
|
|
5
|
-
type PressableProps
|
|
5
|
+
type PressableProps,
|
|
6
|
+
type TextProps
|
|
6
7
|
} from 'react-native'
|
|
7
8
|
|
|
8
9
|
import type { ColorPairToken } from '@helpwave/hightide-design/theme-tokens'
|
|
@@ -43,6 +44,7 @@ export type ButtonProps = Omit<PressableProps, 'children' | 'style'> & {
|
|
|
43
44
|
style?: StyleOverwrite<ButtonState, ButtonStyle>,
|
|
44
45
|
stateLayerStyle?: StyleOverwrite<ButtonState, ButtonStyle>,
|
|
45
46
|
textStyle?: StyleOverwrite<ButtonState, ButtonTextStyle>,
|
|
47
|
+
textProps?: Omit<TextProps, 'style'>,
|
|
46
48
|
iconStyle?: StyleOverwrite<ButtonState, IconStyle>,
|
|
47
49
|
}
|
|
48
50
|
|
|
@@ -57,6 +59,7 @@ export const Button = forwardRef<React.ComponentRef<typeof Pressable>, ButtonPro
|
|
|
57
59
|
style,
|
|
58
60
|
stateLayerStyle,
|
|
59
61
|
textStyle,
|
|
62
|
+
textProps,
|
|
60
63
|
iconStyle,
|
|
61
64
|
hitSlop: providedHitSlop,
|
|
62
65
|
onLayout: providedOnLayout,
|
|
@@ -120,7 +123,7 @@ export const Button = forwardRef<React.ComponentRef<typeof Pressable>, ButtonPro
|
|
|
120
123
|
{leadingIcon !== undefined && (
|
|
121
124
|
<ThemedIcon icon={leadingIcon} />
|
|
122
125
|
)}
|
|
123
|
-
<ThemedText>{children}</ThemedText>
|
|
126
|
+
<ThemedText numberOfLines={1} ellipsizeMode="tail" {...textProps}>{children}</ThemedText>
|
|
124
127
|
{trailingIcon !== undefined && (
|
|
125
128
|
<ThemedIcon icon={trailingIcon} />
|
|
126
129
|
)}
|
|
@@ -1,40 +1,17 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
import
|
|
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
|
|
7
|
-
import { MultiSelectMenu, type MultiSelectMenuProps } from './MultiSelectMenu'
|
|
8
|
-
import { MultiSelectTrigger, type MultiSelectTriggerProps } from './MultiSelectTrigger'
|
|
6
|
+
import { MultiSelectTrigger } from './MultiSelectTrigger'
|
|
9
7
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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
|
|
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
|
|
10
|
-
id: string,
|
|
9
|
+
export type MultiSelectOptionIdentity<T> = {
|
|
11
10
|
value: T,
|
|
12
|
-
|
|
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
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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
|
|
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
|
-
|
|
37
|
-
|
|
38
|
-
label,
|
|
56
|
+
value: identity,
|
|
57
|
+
label: resolvedLabel,
|
|
39
58
|
display,
|
|
40
59
|
disabled,
|
|
41
60
|
})
|
|
42
|
-
}, [disabled, display,
|
|
61
|
+
}, [disabled, display, identity, registerOption, resolvedLabel])
|
|
43
62
|
|
|
44
|
-
const isSelected = context.selectedIds.includes(
|
|
45
|
-
const isVisible = context.visibleOptionIds.includes(
|
|
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 ===
|
|
72
|
+
isHighlighted: context.highlightedId === optionId,
|
|
54
73
|
isDisabled: disabled,
|
|
55
|
-
}), [context.config.color, context.highlightedId, disabled,
|
|
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 :
|
|
89
|
+
title={children ? undefined : resolvedLabel}
|
|
71
90
|
content={children}
|
|
72
91
|
color={optionColor}
|
|
73
92
|
disabled={disabled}
|
|
74
|
-
onPress={() => context.toggleSelection(
|
|
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
|
|
2
|
-
export
|
|
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
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
import
|
|
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
|
|
7
|
-
import { SelectMenu, type SelectMenuProps } from './SelectMenu'
|
|
8
|
-
import { SelectTrigger, type SelectTriggerProps } from './SelectTrigger'
|
|
6
|
+
import { SelectTrigger } from './SelectTrigger'
|
|
9
7
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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
|
|
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
|
|
10
|
-
id: string,
|
|
9
|
+
export type SelectOptionIdentity<T> = {
|
|
11
10
|
value: T,
|
|
12
|
-
|
|
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
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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
|
|
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
|
-
|
|
36
|
-
|
|
37
|
-
label,
|
|
55
|
+
value: identity,
|
|
56
|
+
label: resolvedLabel,
|
|
38
57
|
display,
|
|
39
58
|
disabled,
|
|
40
59
|
})
|
|
41
|
-
}, [disabled, display,
|
|
60
|
+
}, [disabled, display, identity, registerOption, resolvedLabel])
|
|
42
61
|
|
|
43
|
-
const isSelected = context.selectedId ===
|
|
44
|
-
const isVisible = context.visibleOptionIds.includes(
|
|
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 ===
|
|
67
|
+
isHighlighted: context.highlightedId === optionId,
|
|
49
68
|
isDisabled: disabled,
|
|
50
|
-
}), [context.highlightedId, disabled,
|
|
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 :
|
|
82
|
+
title={children ? undefined : resolvedLabel}
|
|
64
83
|
content={children}
|
|
65
84
|
disabled={disabled}
|
|
66
|
-
onPress={() => context.toggleSelection(
|
|
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
|
|
2
|
-
export
|
|
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'
|