react-animated-select 0.2.8 → 0.3.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.
@@ -1,17 +1,14 @@
1
- import {useState, useMemo, useCallback, useId} from 'react'
1
+ import {useState, useMemo, useCallback, useId, useEffect} from 'react'
2
2
 
3
- export function useSelectLogic({
3
+ function useSelectLogic({
4
4
  options = [],
5
5
  jsxOptions = [],
6
-
7
6
  value,
8
7
  defaultValue = undefined,
9
8
  onChange,
10
-
11
9
  disabled = false,
12
10
  loading = false,
13
11
  error = false,
14
-
15
12
  placeholder = 'Choose option',
16
13
  emptyText = 'No options',
17
14
  disabledText = 'Disabled',
@@ -20,71 +17,60 @@ export function useSelectLogic({
20
17
  disabledOption = 'Disabled option',
21
18
  emptyOption = 'Empty option',
22
19
  invalidOption = 'Invalid option',
23
- setVisibility
20
+ setVisibility,
21
+ hasMore,
22
+ loadButton,
23
+ setLoadingTitle,
24
+ loadingTitle,
25
+ loadMoreText,
26
+ loadMore,
27
+ childrenFirst
24
28
  }) {
25
-
26
29
  const stableId = useId()
27
-
28
- // controlled select check
29
30
  const isControlled = value !== undefined
30
-
31
- // filled in value of the select by the user via prop
32
- const [internalValue, setInternalValue] = useState(defaultValue)
33
-
34
- // value chosenness status
35
- const selectedValue = isControlled ? value : internalValue
31
+
32
+ const [selectedId, setSelectedId] = useState(null)
36
33
 
37
34
  const isOptionObject = (obj) => {
38
35
  if (!obj || typeof obj !== 'object' || Array.isArray(obj)) return false
39
- return (
40
- 'id' in obj ||
41
- 'value' in obj ||
42
- 'name' in obj ||
43
- 'label' in obj ||
44
- 'disabled' in obj
45
- )
36
+ return ('id' in obj || 'value' in obj || 'name' in obj || 'label' in obj || 'disabled' in obj)
46
37
  }
47
38
 
48
- // normalization list of options
49
- const normalizedPropOptions = useMemo(() => {
50
- if (!options) return []
51
-
39
+ const normalizedOptions = useMemo(() => {
52
40
  const flat = []
53
-
54
- const push = (key, value, originalItem) => {
55
- let computedUserId = originalItem?.id ?? originalItem?.value
56
- if (computedUserId === undefined || computedUserId === null) {
57
- computedUserId = value ?? key
58
- }
59
-
60
- if (typeof value === 'number' && value === 0) {
61
- flat.push({
62
- key: '0',
63
- value: 0,
64
- userId: 0,
65
- label: '0',
66
- original: originalItem
41
+ const push = (key, val, originalItem) => {
42
+ let computedUserId = originalItem?.id ?? originalItem?.value ?? val ?? key
43
+
44
+ if (typeof val === 'function') {
45
+ flat.push({
46
+ key: `invalid-${flat.length}`,
47
+ value: val,
48
+ userId: null,
49
+ disabled: true,
50
+ invalid: true,
51
+ label: invalidOption,
52
+ original: originalItem
67
53
  })
68
54
  return
69
55
  }
70
56
 
71
- if (typeof value == 'boolean') {
57
+ if (val === '') {
72
58
  flat.push({
73
- key: `bool-${flat.length}`,
74
- value,
75
- userId: computedUserId,
76
- label: String(value),
77
- type: 'boolean',
59
+ key: `empty-str-${flat.length}`,
60
+ value: '',
61
+ userId: null,
62
+ disabled: true,
63
+ label: emptyOption,
78
64
  original: originalItem
79
65
  })
80
66
  return
81
67
  }
82
68
 
83
- if (value === '' || value === null || value === undefined) {
69
+ if (val === null || val === undefined) {
84
70
  flat.push({
85
71
  key: `empty-${flat.length}`,
86
72
  value: null,
87
- userId: computedUserId ?? `empty-${flat.length}`,
73
+ userId: null,
88
74
  disabled: true,
89
75
  label: emptyOption,
90
76
  original: originalItem
@@ -92,177 +78,180 @@ export function useSelectLogic({
92
78
  return
93
79
  }
94
80
 
95
- if (typeof value == 'function') {
96
- flat.push({
97
- key: `invalid-${flat.length}`,
98
- value: null,
99
- userId: computedUserId ?? `invalid-${flat.length}`,
100
- disabled: true,
101
- label: invalidOption,
102
- original: originalItem
81
+ if (typeof val === 'number' || typeof val === 'boolean') {
82
+ flat.push({
83
+ key: `${typeof val}-${val}-${flat.length}`,
84
+ value: val,
85
+ userId: computedUserId,
86
+ label: String(val),
87
+ original: originalItem
103
88
  })
104
89
  return
105
- }
106
-
107
- if (value && typeof value === 'object' && !Array.isArray(value)) {
108
- flat.push({
109
- key: value.id ?? value.value ?? value.name ?? key,
110
- value,
111
- userId: computedUserId,
112
- disabled: !!value.disabled,
113
- label: value.name ?? value.label ?? key,
114
- original: originalItem
115
- })
116
90
  } else {
117
- flat.push({
118
- key,
119
- value,
120
- userId: computedUserId,
121
- label: String(value ?? key),
122
- original: originalItem
91
+ flat.push({
92
+ key: key ?? `opt-${flat.length}`,
93
+ value: val,
94
+ userId: computedUserId,
95
+ label: String(val ?? key),
96
+ original: originalItem
123
97
  })
124
98
  }
125
99
  }
126
100
 
127
101
  if (Array.isArray(options)) {
128
- for (const item of options) {
129
- if (
130
- item &&
131
- typeof item == 'object' &&
132
- !Array.isArray(item) &&
133
- Object.keys(item).length == 1 &&
134
- item.disabled == true
135
- ) {
136
- flat.push({
137
- key: `disabled-${flat.length}`,
138
- value: null,
139
- userId: null,
140
- disabled: true,
141
- label: disabledOption,
142
- original: item
143
- })
144
- continue
145
- }
102
+ options.forEach((item, index) => {
103
+ if (item && typeof item === 'object' && Object.keys(item).length === 1 && item.disabled === true) {
104
+ flat.push({key: `dis-${index}`, value: null, userId: null, disabled: true, label: disabledOption, original: item})
105
+ } else if (isOptionObject(item)) {
106
+ const stableUserId = item.id ?? (typeof item.value !== 'object' ? item.value : (item.label ?? item.name ?? item.value))
146
107
 
147
- if (isOptionObject(item)) {
108
+ let rawLabel = item.name || item.label || item.id || item.value
148
109
 
149
- const stableUserId = item.id ??
150
- (typeof item.value !== 'object' ? item.value : (item.label ?? item.name ?? item.value));
110
+ if (rawLabel === null || rawLabel === undefined || rawLabel === '') {
111
+ const fallbackEntry = Object.entries(item).find(([k, v]) =>
112
+ k !== 'disabled' && v !== null && v !== undefined && v !== ''
113
+ )
114
+ if (fallbackEntry) {
115
+ rawLabel = fallbackEntry[1]
116
+ }
117
+ }
118
+
119
+ const hasNoContent = rawLabel === null || rawLabel === undefined || rawLabel === ''
120
+ const finalLabel = hasNoContent ? emptyOption : String(rawLabel)
151
121
 
152
122
  flat.push({
153
- key: item.id ?? item.value ?? item.name ?? `opt-${flat.length}`,
154
- value: item.value ?? item.id ?? item,
123
+ key: item.id ?? item.value ?? item.name ?? `opt-${index}`,
124
+ value: item.value !== undefined ? item.value : (item.id !== undefined ? item.id : item),
155
125
  userId: stableUserId,
156
- disabled: !!item.disabled,
157
- label: item.name ?? item.label ?? String(item.id ?? item.value),
126
+ disabled: hasNoContent || !!item.disabled,
127
+ label: finalLabel,
158
128
  original: item
159
129
  })
160
- }
161
- else if (item && typeof item == 'object') {
162
- for (const [k, v] of Object.entries(item)) {
163
- push(k, v, v)
164
- }
165
- }
166
- else {
130
+ } else if (item && typeof item === 'object' && !Array.isArray(item)) {
131
+ Object.entries(item).forEach(([k, v]) => push(k, v, v))
132
+ } else {
167
133
  push(item, item, item)
168
134
  }
169
- }
170
- }
171
- else if (typeof options == 'object') {
172
- for (const [k, v] of Object.entries(options)) {
173
- push(k, v, v)
174
- }
135
+ })
136
+ } else if (typeof options === 'object' && options !== null) {
137
+ Object.entries(options).forEach(([k, v]) => push(k, v, v))
175
138
  }
176
139
 
177
- return flat.map((item, i) => {
178
- const internalId = `${stableId}-opt-${i}`
140
+ const propOpts = flat.map((item, i) => ({
141
+ id: `${stableId}-opt-${i}`,
142
+ userId: item.userId,
143
+ name: String(item.label),
144
+ raw: item.value,
145
+ original: item.original,
146
+ disabled: item.disabled,
147
+ invalid: item.invalid,
148
+ type: typeof item.value === 'boolean' ? 'boolean' : 'normal'
149
+ }))
150
+
151
+ const jsxOpts = jsxOptions.map((opt, index) => {
152
+ const hasNoValue = opt.value === null || opt.value === undefined
153
+ const hasNoLabel = opt.label === null || opt.label === undefined || opt.label === ''
154
+
155
+ const isActuallyEmpty = hasNoValue && hasNoLabel
179
156
 
180
157
  return {
181
- id: internalId,
182
- userId: item.userId,
183
- name: String(item.label),
184
- raw: item.value,
185
- original: item.original,
186
- disabled: item.disabled,
187
- type: typeof item.value === 'boolean' ? 'boolean' : 'normal'
188
- }
189
- })
190
-
191
- }, [options, stableId])
192
-
193
- const normalizedJsxOptions = useMemo(() => {
194
- return jsxOptions.map((opt, index) => {
195
- const uniqueId = `jsx-${stableId.replace(/:/g, '')}-${opt.id}-${index}`
196
-
197
- return {
198
- id: uniqueId,
199
- userId: opt.id,
200
- value: opt.value,
158
+ ...opt,
159
+ id: `jsx-${stableId.replace(/:/g, '')}-${opt.id}-${index}`,
160
+ userId: opt.id,
201
161
  raw: opt.value,
202
162
  original: opt.value,
203
- name: opt.label,
204
- jsx: opt.jsx,
205
- disabled: opt.disabled,
206
- className: opt.className,
163
+ name: isActuallyEmpty ? emptyOption : opt.label,
164
+ disabled: opt.disabled || isActuallyEmpty,
207
165
  type: typeof opt.value === 'boolean' ? 'boolean' : 'normal'
208
166
  }
209
167
  })
210
- }, [jsxOptions, stableId])
211
168
 
212
- const normalizedOptions = useMemo(() => {
213
- return [...normalizedPropOptions, ...normalizedJsxOptions]
214
- }, [normalizedPropOptions, normalizedJsxOptions])
169
+ const combined = childrenFirst ? [...jsxOpts, ...propOpts] : [...propOpts, ...jsxOpts]
215
170
 
216
- const hasOptions = normalizedOptions.length > 0
171
+ if (hasMore && loadButton) {
172
+ const isLoading = loadingTitle === loadMoreText
217
173
 
218
- const active = useMemo(() => (
219
- !error && !loading && !disabled && hasOptions
220
- ), [error, loading, disabled, hasOptions])
174
+ combined.push({
175
+ id: 'special-load-more-id',
176
+ name: loadingTitle,
177
+ loadMore: true,
178
+ loading: isLoading,
179
+ type: 'special'
180
+ })
181
+ }
221
182
 
222
- const controlledId = useMemo(() => {
223
- if (!isControlled) return null
183
+ return combined
184
+ }, [options, jsxOptions, stableId, emptyOption, disabledOption, hasMore, loadButton, loadingTitle, loadMoreText])
185
+
186
+ const findIdByValue = useCallback((val) => {
187
+ if (val === undefined || val === null) return null
224
188
 
225
- if (internalValue) {
226
- const currentCachedOption = normalizedOptions.find(o => o.id === internalValue)
227
- if (currentCachedOption && currentCachedOption.userId === value) {
228
- return internalValue
229
- }
189
+ const refMatch = normalizedOptions.find(o => o.original === val || o.raw === val)
190
+ if (refMatch) return refMatch.id
191
+
192
+ if (typeof val === 'object') {
193
+ try {
194
+ const str = JSON.stringify(val)
195
+ const structMatch = normalizedOptions.find(o =>
196
+ o.original && typeof o.original === 'object' && JSON.stringify(o.original) === str
197
+ )
198
+ if (structMatch) return structMatch.id
199
+ } catch {}
230
200
  }
231
201
 
232
- return normalizedOptions.find(o => o.userId === value)?.id ?? null
233
- }, [isControlled, value, normalizedOptions, internalValue])
202
+ return normalizedOptions.find(o => o.userId === val)?.id ?? null
203
+ }, [normalizedOptions])
234
204
 
235
- const selected = useMemo(() => {
236
- const currentId = isControlled ? controlledId : internalValue
205
+ useEffect(() => {
206
+ const effectiveValue = isControlled ? value : defaultValue
237
207
 
238
- if (!currentId) return null
208
+ const currentSelected = normalizedOptions.find(o => o.id === selectedId)
209
+ const isStillValid = currentSelected && (
210
+ currentSelected.original === effectiveValue ||
211
+ currentSelected.raw === effectiveValue ||
212
+ currentSelected.userId === effectiveValue
213
+ )
239
214
 
240
- return normalizedOptions.find(o => o.id === currentId) ?? null
215
+ if (!isStillValid) {
216
+ setSelectedId(findIdByValue(effectiveValue))
217
+ }
218
+ }, [value, defaultValue, isControlled, normalizedOptions, findIdByValue])
241
219
 
242
- }, [isControlled, controlledId, internalValue, normalizedOptions])
220
+ const selected = useMemo(() => {
221
+ return normalizedOptions.find(o => o.id === selectedId) ?? null
222
+ }, [selectedId, normalizedOptions])
243
223
 
244
224
  const selectOption = useCallback((option, e) => {
245
- if (option.disabled) {
246
- e.stopPropagation()
247
- e.preventDefault()
225
+ if (option.disabled || option.loadMore) {
226
+ e?.stopPropagation()
227
+ e?.preventDefault()
228
+
229
+ if (loadingTitle !== loadMoreText) {
230
+ setLoadingTitle(loadMoreText)
231
+ loadMore()
232
+ }
233
+
248
234
  return
249
235
  }
250
236
 
251
- setInternalValue(option.id)
252
-
253
- onChange?.(option?.original, option?.userId)
254
-
237
+ setSelectedId(option.id)
238
+ onChange?.(option.original, option.userId)
255
239
  setVisibility(false)
256
240
  }, [onChange, setVisibility])
257
241
 
258
- const clear = useCallback((e) => {
259
- e.preventDefault()
260
- e.stopPropagation()
261
-
262
- setInternalValue(null)
263
-
242
+ const clear = useCallback(() => {
243
+ setSelectedId(null)
264
244
  onChange?.(null, null)
265
245
  }, [onChange])
266
246
 
267
- return {normalizedOptions, selected, selectOption, clear, hasOptions, active, selectedValue, placeholder, emptyText, disabledText, loadingText, errorText, disabledOption, emptyOption, invalidOption, disabled, loading, error}
268
- }
247
+ return {
248
+ normalizedOptions, selected, selectOption, clear,
249
+ hasOptions: normalizedOptions.length > 0,
250
+ active: !error && !loading && !disabled && normalizedOptions.length > 0,
251
+ selectedValue: value ?? defaultValue,
252
+ placeholder, emptyText, disabledText, loadingText, errorText,
253
+ disabledOption, emptyOption, invalidOption, disabled, loading, error
254
+ }
255
+ }
256
+
257
+ export default useSelectLogic