@riebel/react-native-multiple-select 0.6.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/.github/issue_template.md +46 -0
- package/.prettierrc +9 -0
- package/CONTRIBUTORS.md +9 -0
- package/LICENSE +21 -0
- package/README.md +273 -0
- package/babel.config.js +3 -0
- package/dist/MultiSelect.d.ts +9 -0
- package/dist/MultiSelect.js +278 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +6 -0
- package/dist/styles.d.ts +162 -0
- package/dist/styles.js +163 -0
- package/dist/types.d.ts +82 -0
- package/dist/types.js +1 -0
- package/eslint.config.mjs +65 -0
- package/jest.config.ts +22 -0
- package/jest.setup.ts +1 -0
- package/package.json +56 -0
- package/src/MultiSelect.tsx +591 -0
- package/src/__tests__/MultiSelect.test.tsx +370 -0
- package/src/index.ts +14 -0
- package/src/styles.ts +168 -0
- package/src/types.ts +94 -0
- package/tsconfig.json +25 -0
- package/tsconfig.test.json +11 -0
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
import React, { createRef } from 'react'
|
|
2
|
+
import { Text } from 'react-native'
|
|
3
|
+
import { render, fireEvent, screen } from '@testing-library/react-native'
|
|
4
|
+
import MultiSelect from '../MultiSelect'
|
|
5
|
+
import type {
|
|
6
|
+
MultiSelectProps,
|
|
7
|
+
MultiSelectRef,
|
|
8
|
+
MultiSelectItem,
|
|
9
|
+
IconProps
|
|
10
|
+
} from '../types'
|
|
11
|
+
|
|
12
|
+
const MockIcon = (props: IconProps) => (
|
|
13
|
+
<Text testID={`icon-${props.name}`} style={props.style}>
|
|
14
|
+
{props.name}
|
|
15
|
+
</Text>
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
const sampleItems: MultiSelectItem[] = [
|
|
19
|
+
{ _id: '1', name: 'Apple' },
|
|
20
|
+
{ _id: '2', name: 'Banana' },
|
|
21
|
+
{ _id: '3', name: 'Cherry' },
|
|
22
|
+
{ _id: '4', name: 'Date' },
|
|
23
|
+
{ _id: '5', name: 'Elderberry', disabled: true }
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
const defaultProps: MultiSelectProps = {
|
|
27
|
+
items: sampleItems,
|
|
28
|
+
iconComponent: MockIcon,
|
|
29
|
+
onSelectedItemsChange: jest.fn()
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const renderMultiSelect = (overrides: Partial<MultiSelectProps> = {}) =>
|
|
33
|
+
render(<MultiSelect {...defaultProps} {...overrides} />)
|
|
34
|
+
|
|
35
|
+
beforeEach(() => {
|
|
36
|
+
jest.clearAllMocks()
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
describe('MultiSelect', () => {
|
|
40
|
+
describe('rendering', () => {
|
|
41
|
+
it('renders with default select text', () => {
|
|
42
|
+
renderMultiSelect()
|
|
43
|
+
expect(screen.getByText('Select')).toBeTruthy()
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
it('renders with custom select text', () => {
|
|
47
|
+
renderMultiSelect({ selectText: 'Pick items' })
|
|
48
|
+
expect(screen.getByText('Pick items')).toBeTruthy()
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
it('shows selected count in label when items are selected', () => {
|
|
52
|
+
renderMultiSelect({ selectedItems: ['1', '2'] })
|
|
53
|
+
expect(screen.getByText('Select (2 selected)')).toBeTruthy()
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
it('shows item name in label for single mode', () => {
|
|
57
|
+
renderMultiSelect({
|
|
58
|
+
single: true,
|
|
59
|
+
selectedItems: ['1']
|
|
60
|
+
})
|
|
61
|
+
expect(screen.getByText('Apple')).toBeTruthy()
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
it('displays tags for selected items', () => {
|
|
65
|
+
renderMultiSelect({ selectedItems: ['1', '2'] })
|
|
66
|
+
expect(screen.getByText('Apple')).toBeTruthy()
|
|
67
|
+
expect(screen.getByText('Banana')).toBeTruthy()
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
it('hides tags when hideTags is true', () => {
|
|
71
|
+
renderMultiSelect({
|
|
72
|
+
selectedItems: ['1'],
|
|
73
|
+
hideTags: true
|
|
74
|
+
})
|
|
75
|
+
expect(screen.queryByText('Apple')).toBeNull()
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
it('does not show tags in single mode', () => {
|
|
79
|
+
renderMultiSelect({
|
|
80
|
+
single: true,
|
|
81
|
+
selectedItems: ['1']
|
|
82
|
+
})
|
|
83
|
+
// In single mode the label shows the name, but no tag row
|
|
84
|
+
const apples = screen.getAllByText('Apple')
|
|
85
|
+
expect(apples.length).toBe(1)
|
|
86
|
+
})
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
describe('dropdown toggle', () => {
|
|
90
|
+
it('opens dropdown on press', () => {
|
|
91
|
+
renderMultiSelect()
|
|
92
|
+
fireEvent.press(screen.getByText('Select'))
|
|
93
|
+
expect(screen.getByPlaceholderText('Search')).toBeTruthy()
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
it('calls onToggleList when toggled', () => {
|
|
97
|
+
const onToggleList = jest.fn()
|
|
98
|
+
renderMultiSelect({ onToggleList })
|
|
99
|
+
fireEvent.press(screen.getByText('Select'))
|
|
100
|
+
expect(onToggleList).toHaveBeenCalledTimes(1)
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
it('shows all items in the dropdown list', () => {
|
|
104
|
+
renderMultiSelect()
|
|
105
|
+
fireEvent.press(screen.getByText('Select'))
|
|
106
|
+
for (const item of sampleItems) {
|
|
107
|
+
const label = String(item.name)
|
|
108
|
+
expect(screen.getByText(label)).toBeTruthy()
|
|
109
|
+
}
|
|
110
|
+
})
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
describe('item selection', () => {
|
|
114
|
+
it('calls onSelectedItemsChange when an item is pressed', () => {
|
|
115
|
+
const onSelectedItemsChange = jest.fn()
|
|
116
|
+
renderMultiSelect({ onSelectedItemsChange })
|
|
117
|
+
fireEvent.press(screen.getByText('Select'))
|
|
118
|
+
fireEvent.press(screen.getByText('Apple'))
|
|
119
|
+
expect(onSelectedItemsChange).toHaveBeenCalledWith(['1'])
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
it('adds to existing selection in multi mode', () => {
|
|
123
|
+
const onSelectedItemsChange = jest.fn()
|
|
124
|
+
renderMultiSelect({
|
|
125
|
+
onSelectedItemsChange,
|
|
126
|
+
selectedItems: ['1']
|
|
127
|
+
})
|
|
128
|
+
fireEvent.press(screen.getByText('Select (1 selected)'))
|
|
129
|
+
fireEvent.press(screen.getByText('Banana'))
|
|
130
|
+
expect(onSelectedItemsChange).toHaveBeenCalledWith(['1', '2'])
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
it('deselects an already selected item in multi mode', () => {
|
|
134
|
+
const onSelectedItemsChange = jest.fn()
|
|
135
|
+
renderMultiSelect({
|
|
136
|
+
onSelectedItemsChange,
|
|
137
|
+
selectedItems: ['1', '2']
|
|
138
|
+
})
|
|
139
|
+
fireEvent.press(screen.getByText('Select (2 selected)'))
|
|
140
|
+
fireEvent.press(screen.getByText('Apple'))
|
|
141
|
+
expect(onSelectedItemsChange).toHaveBeenCalledWith(['2'])
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
it('replaces selection in single mode', () => {
|
|
145
|
+
const onSelectedItemsChange = jest.fn()
|
|
146
|
+
renderMultiSelect({
|
|
147
|
+
onSelectedItemsChange,
|
|
148
|
+
single: true,
|
|
149
|
+
selectedItems: ['1']
|
|
150
|
+
})
|
|
151
|
+
fireEvent.press(screen.getByText('Apple'))
|
|
152
|
+
fireEvent.press(screen.getByText('Banana'))
|
|
153
|
+
expect(onSelectedItemsChange).toHaveBeenCalledWith(['2'])
|
|
154
|
+
})
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
describe('tag removal', () => {
|
|
158
|
+
it('removes a selected item when tag close icon is pressed', () => {
|
|
159
|
+
const onSelectedItemsChange = jest.fn()
|
|
160
|
+
renderMultiSelect({
|
|
161
|
+
onSelectedItemsChange,
|
|
162
|
+
selectedItems: ['1', '2']
|
|
163
|
+
})
|
|
164
|
+
const closeIcons = screen.getAllByTestId('icon-close-circle')
|
|
165
|
+
fireEvent.press(closeIcons[0])
|
|
166
|
+
expect(onSelectedItemsChange).toHaveBeenCalledWith(['2'])
|
|
167
|
+
})
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
describe('search / filtering', () => {
|
|
171
|
+
it('filters items with partial match by default', () => {
|
|
172
|
+
renderMultiSelect()
|
|
173
|
+
fireEvent.press(screen.getByText('Select'))
|
|
174
|
+
fireEvent.changeText(screen.getByPlaceholderText('Search'), 'app')
|
|
175
|
+
expect(screen.getByText('Apple')).toBeTruthy()
|
|
176
|
+
expect(screen.queryByText('Banana')).toBeNull()
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
it('filters items with full match when filterMethod is full', () => {
|
|
180
|
+
renderMultiSelect({ filterMethod: 'full' })
|
|
181
|
+
fireEvent.press(screen.getByText('Select'))
|
|
182
|
+
fireEvent.changeText(screen.getByPlaceholderText('Search'), 'an')
|
|
183
|
+
expect(screen.getByText('Banana')).toBeTruthy()
|
|
184
|
+
expect(screen.queryByText('Apple')).toBeNull()
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
it('calls onChangeInput when search text changes', () => {
|
|
188
|
+
const onChangeInput = jest.fn()
|
|
189
|
+
renderMultiSelect({ onChangeInput })
|
|
190
|
+
fireEvent.press(screen.getByText('Select'))
|
|
191
|
+
fireEvent.changeText(screen.getByPlaceholderText('Search'), 'test')
|
|
192
|
+
expect(onChangeInput).toHaveBeenCalledWith('test')
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
it('shows no items text when filter yields no results', () => {
|
|
196
|
+
renderMultiSelect({ noItemsText: 'Nothing found.' })
|
|
197
|
+
fireEvent.press(screen.getByText('Select'))
|
|
198
|
+
fireEvent.changeText(screen.getByPlaceholderText('Search'), 'zzzzz')
|
|
199
|
+
expect(screen.getByText('Nothing found.')).toBeTruthy()
|
|
200
|
+
})
|
|
201
|
+
})
|
|
202
|
+
|
|
203
|
+
describe('removeSelected', () => {
|
|
204
|
+
it('hides already selected items from the dropdown', () => {
|
|
205
|
+
renderMultiSelect({
|
|
206
|
+
selectedItems: ['1'],
|
|
207
|
+
removeSelected: true
|
|
208
|
+
})
|
|
209
|
+
fireEvent.press(screen.getByText('Select (1 selected)'))
|
|
210
|
+
expect(screen.queryByText('Apple')).toBeNull()
|
|
211
|
+
expect(screen.getByText('Banana')).toBeTruthy()
|
|
212
|
+
})
|
|
213
|
+
})
|
|
214
|
+
|
|
215
|
+
describe('canAddItems', () => {
|
|
216
|
+
it('shows add item row when search has no exact match', () => {
|
|
217
|
+
renderMultiSelect({ canAddItems: true })
|
|
218
|
+
fireEvent.press(screen.getByText('Select'))
|
|
219
|
+
fireEvent.changeText(screen.getByPlaceholderText('Search'), 'Mango')
|
|
220
|
+
expect(screen.getByText(/Add Mango/)).toBeTruthy()
|
|
221
|
+
})
|
|
222
|
+
|
|
223
|
+
it('calls onAddItem and onSelectedItemsChange when adding', () => {
|
|
224
|
+
const onAddItem = jest.fn()
|
|
225
|
+
const onSelectedItemsChange = jest.fn()
|
|
226
|
+
renderMultiSelect({
|
|
227
|
+
canAddItems: true,
|
|
228
|
+
onAddItem,
|
|
229
|
+
onSelectedItemsChange
|
|
230
|
+
})
|
|
231
|
+
fireEvent.press(screen.getByText('Select'))
|
|
232
|
+
fireEvent.changeText(screen.getByPlaceholderText('Search'), 'Mango')
|
|
233
|
+
fireEvent.press(screen.getByText(/Add Mango/))
|
|
234
|
+
expect(onAddItem).toHaveBeenCalledTimes(1)
|
|
235
|
+
expect(onSelectedItemsChange).toHaveBeenCalled()
|
|
236
|
+
})
|
|
237
|
+
|
|
238
|
+
it('does not show add row when search matches an existing item', () => {
|
|
239
|
+
renderMultiSelect({ canAddItems: true })
|
|
240
|
+
fireEvent.press(screen.getByText('Select'))
|
|
241
|
+
fireEvent.changeText(screen.getByPlaceholderText('Search'), 'Apple')
|
|
242
|
+
expect(screen.queryByText(/Add Apple/)).toBeNull()
|
|
243
|
+
})
|
|
244
|
+
})
|
|
245
|
+
|
|
246
|
+
describe('submit button', () => {
|
|
247
|
+
it('shows submit button by default', () => {
|
|
248
|
+
renderMultiSelect()
|
|
249
|
+
fireEvent.press(screen.getByText('Select'))
|
|
250
|
+
expect(screen.getByText('Submit')).toBeTruthy()
|
|
251
|
+
})
|
|
252
|
+
|
|
253
|
+
it('renders custom submit button text', () => {
|
|
254
|
+
renderMultiSelect({ submitButtonText: 'Done' })
|
|
255
|
+
fireEvent.press(screen.getByText('Select'))
|
|
256
|
+
expect(screen.getByText('Done')).toBeTruthy()
|
|
257
|
+
})
|
|
258
|
+
|
|
259
|
+
it('hides submit button when hideSubmitButton is true', () => {
|
|
260
|
+
renderMultiSelect({ hideSubmitButton: true })
|
|
261
|
+
fireEvent.press(screen.getByText('Select'))
|
|
262
|
+
expect(screen.queryByText('Submit')).toBeNull()
|
|
263
|
+
})
|
|
264
|
+
|
|
265
|
+
it('closes selector on submit press', () => {
|
|
266
|
+
renderMultiSelect()
|
|
267
|
+
fireEvent.press(screen.getByText('Select'))
|
|
268
|
+
expect(screen.getByPlaceholderText('Search')).toBeTruthy()
|
|
269
|
+
fireEvent.press(screen.getByText('Submit'))
|
|
270
|
+
expect(screen.queryByPlaceholderText('Search')).toBeNull()
|
|
271
|
+
})
|
|
272
|
+
})
|
|
273
|
+
|
|
274
|
+
describe('custom keys', () => {
|
|
275
|
+
it('supports custom uniqueKey and displayKey', () => {
|
|
276
|
+
const customItems: MultiSelectItem[] = [
|
|
277
|
+
{ id: 'a', label: 'First' },
|
|
278
|
+
{ id: 'b', label: 'Second' }
|
|
279
|
+
]
|
|
280
|
+
renderMultiSelect({
|
|
281
|
+
items: customItems,
|
|
282
|
+
uniqueKey: 'id',
|
|
283
|
+
displayKey: 'label',
|
|
284
|
+
selectedItems: ['a']
|
|
285
|
+
})
|
|
286
|
+
expect(screen.getByText('First')).toBeTruthy()
|
|
287
|
+
})
|
|
288
|
+
})
|
|
289
|
+
|
|
290
|
+
describe('disabled items', () => {
|
|
291
|
+
it('renders disabled items with disabled styling', () => {
|
|
292
|
+
renderMultiSelect()
|
|
293
|
+
fireEvent.press(screen.getByText('Select'))
|
|
294
|
+
// Elderberry is disabled — it still renders but should be non-interactive
|
|
295
|
+
expect(screen.getByText('Elderberry')).toBeTruthy()
|
|
296
|
+
})
|
|
297
|
+
})
|
|
298
|
+
|
|
299
|
+
describe('imperative handle (ref)', () => {
|
|
300
|
+
it('exposes getSelectedItemsExt via ref', () => {
|
|
301
|
+
const ref = createRef<MultiSelectRef>()
|
|
302
|
+
render(
|
|
303
|
+
<MultiSelect
|
|
304
|
+
{...defaultProps}
|
|
305
|
+
selectedItems={['1', '2']}
|
|
306
|
+
ref={ref}
|
|
307
|
+
/>
|
|
308
|
+
)
|
|
309
|
+
expect(ref.current).not.toBeNull()
|
|
310
|
+
const result = ref.current?.getSelectedItemsExt()
|
|
311
|
+
expect(result).toBeTruthy()
|
|
312
|
+
})
|
|
313
|
+
|
|
314
|
+
it('getSelectedItemsExt accepts optional items override', () => {
|
|
315
|
+
const ref = createRef<MultiSelectRef>()
|
|
316
|
+
render(
|
|
317
|
+
<MultiSelect
|
|
318
|
+
{...defaultProps}
|
|
319
|
+
selectedItems={['1']}
|
|
320
|
+
ref={ref}
|
|
321
|
+
/>
|
|
322
|
+
)
|
|
323
|
+
const result = ref.current?.getSelectedItemsExt(['2'])
|
|
324
|
+
expect(result).toBeTruthy()
|
|
325
|
+
})
|
|
326
|
+
})
|
|
327
|
+
|
|
328
|
+
describe('custom search placeholder', () => {
|
|
329
|
+
it('uses custom searchInputPlaceholderText', () => {
|
|
330
|
+
renderMultiSelect({ searchInputPlaceholderText: 'Find...' })
|
|
331
|
+
fireEvent.press(screen.getByText('Select'))
|
|
332
|
+
expect(screen.getByPlaceholderText('Find...')).toBeTruthy()
|
|
333
|
+
})
|
|
334
|
+
})
|
|
335
|
+
|
|
336
|
+
describe('iconNames', () => {
|
|
337
|
+
it('uses custom icon names when provided', () => {
|
|
338
|
+
renderMultiSelect({
|
|
339
|
+
iconNames: { arrowLeft: 'arrow-back' },
|
|
340
|
+
selectedItems: ['1', '2']
|
|
341
|
+
})
|
|
342
|
+
fireEvent.press(screen.getByText('Select (2 selected)'))
|
|
343
|
+
expect(screen.getByTestId('icon-arrow-back')).toBeTruthy()
|
|
344
|
+
expect(screen.queryByTestId('icon-arrow-left')).toBeNull()
|
|
345
|
+
})
|
|
346
|
+
|
|
347
|
+
it('uses custom close icon name on tags', () => {
|
|
348
|
+
renderMultiSelect({
|
|
349
|
+
iconNames: { close: 'x-circle' },
|
|
350
|
+
selectedItems: ['1']
|
|
351
|
+
})
|
|
352
|
+
expect(screen.getByTestId('icon-x-circle')).toBeTruthy()
|
|
353
|
+
expect(screen.queryByTestId('icon-close-circle')).toBeNull()
|
|
354
|
+
})
|
|
355
|
+
})
|
|
356
|
+
|
|
357
|
+
describe('hideDropdown', () => {
|
|
358
|
+
it('hides the back arrow when hideDropdown is true', () => {
|
|
359
|
+
renderMultiSelect({ hideDropdown: true })
|
|
360
|
+
fireEvent.press(screen.getByText('Select'))
|
|
361
|
+
expect(screen.queryByTestId('icon-arrow-left')).toBeNull()
|
|
362
|
+
})
|
|
363
|
+
|
|
364
|
+
it('shows the back arrow by default', () => {
|
|
365
|
+
renderMultiSelect()
|
|
366
|
+
fireEvent.press(screen.getByText('Select'))
|
|
367
|
+
expect(screen.getByTestId('icon-arrow-left')).toBeTruthy()
|
|
368
|
+
})
|
|
369
|
+
})
|
|
370
|
+
})
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* react-native-multi-select
|
|
3
|
+
* Copyright(c) 2017 Mustapha Babatunde Oluwaleke
|
|
4
|
+
* MIT Licensed
|
|
5
|
+
*/
|
|
6
|
+
export { default } from './MultiSelect'
|
|
7
|
+
export type {
|
|
8
|
+
MultiSelectProps,
|
|
9
|
+
MultiSelectItem,
|
|
10
|
+
MultiSelectRef,
|
|
11
|
+
IconProps,
|
|
12
|
+
IconComponentType,
|
|
13
|
+
IconNames
|
|
14
|
+
} from './types'
|
package/src/styles.ts
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* react-native-multi-select
|
|
3
|
+
* Copyright(c) 2017 Mustapha Babatunde Oluwaleke
|
|
4
|
+
* MIT Licensed
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { StyleSheet } from 'react-native'
|
|
8
|
+
|
|
9
|
+
export const colorPack = {
|
|
10
|
+
primary: '#00A5FF',
|
|
11
|
+
primaryDark: '#215191',
|
|
12
|
+
light: '#FFF',
|
|
13
|
+
textPrimary: '#525966',
|
|
14
|
+
placeholderTextColor: '#A9A9A9',
|
|
15
|
+
danger: '#C62828',
|
|
16
|
+
borderColor: '#e9e9e9',
|
|
17
|
+
backgroundColor: '#b1b1b1'
|
|
18
|
+
} as const
|
|
19
|
+
|
|
20
|
+
const styles = StyleSheet.create({
|
|
21
|
+
footerWrapper: {
|
|
22
|
+
flexWrap: 'wrap',
|
|
23
|
+
alignItems: 'flex-start',
|
|
24
|
+
flexDirection: 'row'
|
|
25
|
+
},
|
|
26
|
+
footerWrapperNC: {
|
|
27
|
+
width: 320,
|
|
28
|
+
flexDirection: 'column'
|
|
29
|
+
},
|
|
30
|
+
subSection: {
|
|
31
|
+
backgroundColor: colorPack.light,
|
|
32
|
+
borderBottomWidth: 1,
|
|
33
|
+
borderColor: colorPack.borderColor,
|
|
34
|
+
paddingLeft: 0,
|
|
35
|
+
paddingRight: 20,
|
|
36
|
+
flex: 1,
|
|
37
|
+
flexDirection: 'row',
|
|
38
|
+
alignItems: 'center'
|
|
39
|
+
},
|
|
40
|
+
greyButton: {
|
|
41
|
+
height: 40,
|
|
42
|
+
borderRadius: 5,
|
|
43
|
+
elevation: 0,
|
|
44
|
+
backgroundColor: colorPack.backgroundColor
|
|
45
|
+
},
|
|
46
|
+
indicator: {
|
|
47
|
+
fontSize: 30,
|
|
48
|
+
color: colorPack.placeholderTextColor
|
|
49
|
+
},
|
|
50
|
+
indicatorPadded: {
|
|
51
|
+
paddingLeft: 15,
|
|
52
|
+
paddingRight: 15
|
|
53
|
+
},
|
|
54
|
+
selectedItem: {
|
|
55
|
+
flexDirection: 'row',
|
|
56
|
+
alignItems: 'center',
|
|
57
|
+
paddingLeft: 15,
|
|
58
|
+
paddingTop: 3,
|
|
59
|
+
paddingRight: 3,
|
|
60
|
+
paddingBottom: 3,
|
|
61
|
+
margin: 3,
|
|
62
|
+
borderRadius: 20,
|
|
63
|
+
borderWidth: 2
|
|
64
|
+
},
|
|
65
|
+
button: {
|
|
66
|
+
height: 40,
|
|
67
|
+
flexDirection: 'row',
|
|
68
|
+
justifyContent: 'center',
|
|
69
|
+
alignItems: 'center'
|
|
70
|
+
},
|
|
71
|
+
buttonText: {
|
|
72
|
+
color: colorPack.light,
|
|
73
|
+
fontSize: 14
|
|
74
|
+
},
|
|
75
|
+
inputGroup: {
|
|
76
|
+
flexDirection: 'row',
|
|
77
|
+
alignItems: 'center',
|
|
78
|
+
paddingLeft: 16,
|
|
79
|
+
backgroundColor: colorPack.light
|
|
80
|
+
},
|
|
81
|
+
dropdownView: {
|
|
82
|
+
flexDirection: 'row',
|
|
83
|
+
alignItems: 'center',
|
|
84
|
+
height: 40,
|
|
85
|
+
marginBottom: 10
|
|
86
|
+
},
|
|
87
|
+
searchIconMargin: {
|
|
88
|
+
marginRight: 10
|
|
89
|
+
},
|
|
90
|
+
tagLabel: {
|
|
91
|
+
flex: 1,
|
|
92
|
+
fontSize: 15
|
|
93
|
+
},
|
|
94
|
+
tagRemoveIcon: {
|
|
95
|
+
fontSize: 22,
|
|
96
|
+
marginLeft: 10
|
|
97
|
+
},
|
|
98
|
+
rowWrap: {
|
|
99
|
+
flexDirection: 'row',
|
|
100
|
+
flexWrap: 'wrap'
|
|
101
|
+
},
|
|
102
|
+
rowItemText: {
|
|
103
|
+
flex: 1,
|
|
104
|
+
fontSize: 16,
|
|
105
|
+
paddingTop: 5,
|
|
106
|
+
paddingBottom: 5
|
|
107
|
+
},
|
|
108
|
+
rowPadding: {
|
|
109
|
+
paddingLeft: 20,
|
|
110
|
+
paddingRight: 20
|
|
111
|
+
},
|
|
112
|
+
rowAlignCenter: {
|
|
113
|
+
flexDirection: 'row',
|
|
114
|
+
alignItems: 'center'
|
|
115
|
+
},
|
|
116
|
+
disabledText: {
|
|
117
|
+
color: 'grey'
|
|
118
|
+
},
|
|
119
|
+
checkIcon: {
|
|
120
|
+
fontSize: 20
|
|
121
|
+
},
|
|
122
|
+
noItemsText: {
|
|
123
|
+
flex: 1,
|
|
124
|
+
marginTop: 20,
|
|
125
|
+
textAlign: 'center',
|
|
126
|
+
color: colorPack.danger
|
|
127
|
+
},
|
|
128
|
+
noItemsRow: {
|
|
129
|
+
flexDirection: 'row',
|
|
130
|
+
alignItems: 'center'
|
|
131
|
+
},
|
|
132
|
+
searchInputFlex: {
|
|
133
|
+
flex: 1
|
|
134
|
+
},
|
|
135
|
+
backArrowMargin: {
|
|
136
|
+
marginLeft: 5
|
|
137
|
+
},
|
|
138
|
+
selectorContent: {
|
|
139
|
+
flexDirection: 'column',
|
|
140
|
+
backgroundColor: '#fafafa'
|
|
141
|
+
},
|
|
142
|
+
subSectionPadded: {
|
|
143
|
+
paddingTop: 10,
|
|
144
|
+
paddingBottom: 10
|
|
145
|
+
},
|
|
146
|
+
dropdownTouchable: {
|
|
147
|
+
flex: 1,
|
|
148
|
+
flexDirection: 'row',
|
|
149
|
+
alignItems: 'center'
|
|
150
|
+
},
|
|
151
|
+
dropdownLabelBase: {
|
|
152
|
+
flex: 1,
|
|
153
|
+
fontSize: 16
|
|
154
|
+
},
|
|
155
|
+
selectedItemLayout: {
|
|
156
|
+
justifyContent: 'center' as const,
|
|
157
|
+
height: 40
|
|
158
|
+
}
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
export const selectorViewStyle = (fixedHeight: boolean) => ({
|
|
162
|
+
flexDirection: 'column' as const,
|
|
163
|
+
marginBottom: 10,
|
|
164
|
+
elevation: 2,
|
|
165
|
+
...(fixedHeight ? { height: 250 } : {})
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
export default styles
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import type { ComponentType, ReactNode } from 'react'
|
|
2
|
+
import type {
|
|
3
|
+
ViewStyle,
|
|
4
|
+
TextStyle,
|
|
5
|
+
TextInputProps,
|
|
6
|
+
StyleProp,
|
|
7
|
+
FlatListProps
|
|
8
|
+
} from 'react-native'
|
|
9
|
+
|
|
10
|
+
export interface IconProps {
|
|
11
|
+
name: string
|
|
12
|
+
size?: number
|
|
13
|
+
color?: string
|
|
14
|
+
style?: StyleProp<TextStyle | ViewStyle>
|
|
15
|
+
onPress?: () => void
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export type IconComponentType = ComponentType<IconProps>
|
|
19
|
+
|
|
20
|
+
export interface IconNames {
|
|
21
|
+
search?: string
|
|
22
|
+
close?: string
|
|
23
|
+
check?: string
|
|
24
|
+
arrowDown?: string
|
|
25
|
+
arrowRight?: string
|
|
26
|
+
arrowLeft?: string
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface MultiSelectItem {
|
|
30
|
+
[key: string]: unknown
|
|
31
|
+
disabled?: boolean
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface MultiSelectProps {
|
|
35
|
+
items: MultiSelectItem[]
|
|
36
|
+
iconComponent: IconComponentType
|
|
37
|
+
iconNames?: Partial<IconNames>
|
|
38
|
+
onSelectedItemsChange: (items: string[]) => void
|
|
39
|
+
single?: boolean
|
|
40
|
+
selectedItems?: string[]
|
|
41
|
+
uniqueKey?: string
|
|
42
|
+
displayKey?: string
|
|
43
|
+
tagBorderColor?: string
|
|
44
|
+
tagTextColor?: string
|
|
45
|
+
tagRemoveIconColor?: string
|
|
46
|
+
tagContainerStyle?: StyleProp<ViewStyle>
|
|
47
|
+
fontFamily?: string
|
|
48
|
+
selectedItemFontFamily?: string
|
|
49
|
+
selectedItemTextColor?: string
|
|
50
|
+
selectedItemIconColor?: string
|
|
51
|
+
itemFontFamily?: string
|
|
52
|
+
itemTextColor?: string
|
|
53
|
+
itemFontSize?: number
|
|
54
|
+
searchIcon?: ReactNode
|
|
55
|
+
searchInputPlaceholderText?: string
|
|
56
|
+
searchInputStyle?: StyleProp<TextStyle>
|
|
57
|
+
selectText?: string
|
|
58
|
+
selectedText?: string
|
|
59
|
+
altFontFamily?: string
|
|
60
|
+
fontSize?: number
|
|
61
|
+
textColor?: string
|
|
62
|
+
fixedHeight?: boolean
|
|
63
|
+
hideTags?: boolean
|
|
64
|
+
hideSubmitButton?: boolean
|
|
65
|
+
hideDropdown?: boolean
|
|
66
|
+
submitButtonColor?: string
|
|
67
|
+
submitButtonText?: string
|
|
68
|
+
canAddItems?: boolean
|
|
69
|
+
removeSelected?: boolean
|
|
70
|
+
noItemsText?: string
|
|
71
|
+
filterMethod?: 'partial' | 'full'
|
|
72
|
+
onAddItem?: (newItems: MultiSelectItem[]) => void
|
|
73
|
+
onChangeInput?: (text: string) => void
|
|
74
|
+
onClearSelector?: () => void
|
|
75
|
+
onToggleList?: () => void
|
|
76
|
+
textInputProps?: TextInputProps
|
|
77
|
+
flatListProps?: Partial<FlatListProps<MultiSelectItem>>
|
|
78
|
+
styleDropdownMenu?: StyleProp<ViewStyle>
|
|
79
|
+
styleDropdownMenuSubsection?: StyleProp<ViewStyle>
|
|
80
|
+
styleInputGroup?: StyleProp<ViewStyle>
|
|
81
|
+
styleItemsContainer?: StyleProp<ViewStyle>
|
|
82
|
+
styleListContainer?: StyleProp<ViewStyle>
|
|
83
|
+
styleMainWrapper?: StyleProp<ViewStyle>
|
|
84
|
+
styleRowList?: StyleProp<ViewStyle>
|
|
85
|
+
styleSelectorContainer?: StyleProp<ViewStyle>
|
|
86
|
+
styleTextDropdown?: StyleProp<TextStyle>
|
|
87
|
+
styleTextDropdownSelected?: StyleProp<TextStyle>
|
|
88
|
+
styleTextTag?: StyleProp<TextStyle>
|
|
89
|
+
styleIndicator?: StyleProp<ViewStyle>
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export interface MultiSelectRef {
|
|
93
|
+
getSelectedItemsExt: (items?: string[]) => ReactNode
|
|
94
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2020",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "bundler",
|
|
6
|
+
"lib": ["ES2020"],
|
|
7
|
+
"jsx": "react-jsx",
|
|
8
|
+
"declaration": true,
|
|
9
|
+
"declarationDir": "dist",
|
|
10
|
+
"outDir": "dist",
|
|
11
|
+
"rootDir": "src",
|
|
12
|
+
"strict": true,
|
|
13
|
+
"esModuleInterop": true,
|
|
14
|
+
"skipLibCheck": true,
|
|
15
|
+
"forceConsistentCasingInFileNames": true,
|
|
16
|
+
"resolveJsonModule": true,
|
|
17
|
+
"isolatedModules": true,
|
|
18
|
+
"noUnusedLocals": true,
|
|
19
|
+
"noUnusedParameters": true,
|
|
20
|
+
"noImplicitReturns": true,
|
|
21
|
+
"types": ["react"]
|
|
22
|
+
},
|
|
23
|
+
"include": ["src"],
|
|
24
|
+
"exclude": ["node_modules", "dist", "src/__tests__"]
|
|
25
|
+
}
|