@loadsmart/loadsmart-ui 5.7.0 → 5.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/README.md +13 -7
- package/dist/components/Dropdown/Dropdown.types.d.ts +6 -0
- package/dist/components/Dropdown/useDropdown.d.ts +1 -1
- package/dist/components/Select/Select.stories.d.ts +1 -0
- package/dist/components/Select/Select.types.d.ts +4 -0
- package/dist/index.js +177 -177
- package/dist/index.js.map +1 -1
- package/dist/testing/index.js +1 -1
- package/dist/testing/index.js.map +1 -1
- package/package.json +3 -5
- package/src/components/Dropdown/Dropdown.context.ts +1 -0
- package/src/components/Dropdown/Dropdown.tsx +12 -4
- package/src/components/Dropdown/Dropdown.types.ts +6 -0
- package/src/components/Dropdown/DropdownTrigger.tsx +5 -4
- package/src/components/Dropdown/useDropdown.ts +9 -8
- package/src/components/Select/Select.fixtures.ts +1 -1
- package/src/components/Select/Select.stories.tsx +98 -15
- package/src/components/Select/Select.test.tsx +127 -9
- package/src/components/Select/Select.tsx +14 -8
- package/src/components/Select/Select.types.ts +5 -0
- package/src/components/Select/useSelect.ts +59 -15
- package/src/stories/startPage.stories.mdx +20 -8
- package/src/testing/SelectEvent/SelectEvent.ts +4 -0
|
@@ -105,6 +105,10 @@ const DividerText = styled(Text)`
|
|
|
105
105
|
background-color: ${token('color-neutral-white')};
|
|
106
106
|
`
|
|
107
107
|
|
|
108
|
+
function isQuerying(status: SelectStatus) {
|
|
109
|
+
return ['pending-query', 'querying'].includes(status)
|
|
110
|
+
}
|
|
111
|
+
|
|
108
112
|
type SeparatorProps = {
|
|
109
113
|
status: SelectStatus
|
|
110
114
|
after?: unknown[]
|
|
@@ -112,7 +116,7 @@ type SeparatorProps = {
|
|
|
112
116
|
}
|
|
113
117
|
|
|
114
118
|
function Separator({ status, after = [], before = [] }: SeparatorProps): JSX.Element {
|
|
115
|
-
if (status
|
|
119
|
+
if (!isQuerying(status)) {
|
|
116
120
|
if (isEmpty(after) && !isEmpty(before)) return <Divider />
|
|
117
121
|
if (isEmpty(after)) return <Fragment />
|
|
118
122
|
}
|
|
@@ -121,7 +125,7 @@ function Separator({ status, after = [], before = [] }: SeparatorProps): JSX.Ele
|
|
|
121
125
|
<div style={{ position: 'relative' }}>
|
|
122
126
|
<Divider />
|
|
123
127
|
<DividerText variant="caption-bold" color="color-neutral-light">
|
|
124
|
-
{status
|
|
128
|
+
{isQuerying(status) ? 'Loading...' : `${after.length} option${pluralize(after.length)}`}
|
|
125
129
|
</DividerText>
|
|
126
130
|
</div>
|
|
127
131
|
)
|
|
@@ -131,7 +135,7 @@ function renderOptionsSingle(select: useSelectReturn, components?: Components):
|
|
|
131
135
|
const { Option, Empty, CreatableOption } = getComponents(components)
|
|
132
136
|
const isCreatable = select.isCreatable()
|
|
133
137
|
|
|
134
|
-
if (select.status
|
|
138
|
+
if (isQuerying(select.status) && isEmpty(select.options)) {
|
|
135
139
|
return <SelectEmpty>Loading...</SelectEmpty>
|
|
136
140
|
}
|
|
137
141
|
|
|
@@ -145,11 +149,12 @@ function renderOptionsSingle(select: useSelectReturn, components?: Components):
|
|
|
145
149
|
|
|
146
150
|
return (
|
|
147
151
|
<>
|
|
152
|
+
{select.createOptionPosition === 'first' && isCreatable ? <CreatableOption /> : null}
|
|
148
153
|
{select.options.map((option) => {
|
|
149
154
|
const { value } = select.getSelectableOption(option)
|
|
150
155
|
return <Option key={String(value)} value={value} />
|
|
151
156
|
})}
|
|
152
|
-
{
|
|
157
|
+
{select.createOptionPosition === 'last' && isCreatable ? <CreatableOption /> : null}
|
|
153
158
|
</>
|
|
154
159
|
)
|
|
155
160
|
}
|
|
@@ -174,18 +179,19 @@ function renderOptionsMultiple(select: useSelectReturn, components?: Components)
|
|
|
174
179
|
|
|
175
180
|
let remaining = (
|
|
176
181
|
<Fragment>
|
|
182
|
+
{select.createOptionPosition === 'first' && isCreatable ? <CreatableOption /> : null}
|
|
177
183
|
{remainingOptions.map((option) => (
|
|
178
184
|
<Option key={String(option.value)} value={option.value} />
|
|
179
185
|
))}
|
|
180
|
-
{
|
|
186
|
+
{select.createOptionPosition === 'last' && isCreatable ? <CreatableOption /> : null}
|
|
181
187
|
</Fragment>
|
|
182
188
|
)
|
|
183
189
|
|
|
184
|
-
if (select.status
|
|
190
|
+
if (!isQuerying(select.status) && isEmpty(remainingOptions)) {
|
|
185
191
|
remaining = isCreatable ? <CreatableOption /> : <Empty>No more options.</Empty>
|
|
186
192
|
}
|
|
187
193
|
|
|
188
|
-
if (select.status
|
|
194
|
+
if (!isQuerying(select.status) && isEmpty(select.options)) {
|
|
189
195
|
remaining = isCreatable ? <CreatableOption /> : <Empty>No results found.</Empty>
|
|
190
196
|
}
|
|
191
197
|
|
|
@@ -214,7 +220,7 @@ function Select(props: SelectProps): JSX.Element {
|
|
|
214
220
|
}
|
|
215
221
|
|
|
216
222
|
function getTrailing() {
|
|
217
|
-
if (select.status
|
|
223
|
+
if (isQuerying(select.status)) {
|
|
218
224
|
return <Loading data-testid="select-trigger-loading">···</Loading>
|
|
219
225
|
}
|
|
220
226
|
|
|
@@ -67,6 +67,8 @@ export type Components = {
|
|
|
67
67
|
CreatableOption?: CreatableOptionType
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
+
export type CreateOptionPosition = 'first' | 'last'
|
|
71
|
+
|
|
70
72
|
export interface SelectProps extends DropdownProps {
|
|
71
73
|
name: string
|
|
72
74
|
placeholder?: string
|
|
@@ -79,6 +81,8 @@ export interface SelectProps extends DropdownProps {
|
|
|
79
81
|
onChange?: (event: EventLike<Option | Option[] | null>) => void
|
|
80
82
|
onCreate?: (query: string) => Promise<void | Option> | void | Option
|
|
81
83
|
onQueryChange?: (e: ChangeEvent<HTMLInputElement>) => void
|
|
84
|
+
isValidNewOption?: ((query: string) => boolean) | boolean
|
|
85
|
+
createOptionPosition?: CreateOptionPosition
|
|
82
86
|
}
|
|
83
87
|
|
|
84
88
|
export type SelectOptionProps = {
|
|
@@ -123,6 +127,7 @@ export type useSelectReturn = {
|
|
|
123
127
|
}
|
|
124
128
|
getCreatebleProps: () => CreatableProps
|
|
125
129
|
isCreatable: () => boolean
|
|
130
|
+
createOptionPosition?: CreateOptionPosition
|
|
126
131
|
}
|
|
127
132
|
|
|
128
133
|
export type useSelectExternalReturn = {
|
|
@@ -3,6 +3,7 @@ import { isFunction } from '@loadsmart/utils-function'
|
|
|
3
3
|
import { isNil } from '@loadsmart/utils-object'
|
|
4
4
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
|
5
5
|
|
|
6
|
+
import isEmpty from 'utils/toolset/isEmpty'
|
|
6
7
|
import { useDropdown } from 'components/Dropdown'
|
|
7
8
|
import { useDidMount } from 'hooks/useDidMount'
|
|
8
9
|
import { useFocusTrap } from 'hooks/useFocusTrap'
|
|
@@ -191,8 +192,18 @@ function useOptions<T = any>(props: { datasources: SelectDatasource<T>[]; adapte
|
|
|
191
192
|
*/
|
|
192
193
|
function useSelect(props: SelectProps): useSelectReturn {
|
|
193
194
|
const didMount = useDidMount()
|
|
194
|
-
const {
|
|
195
|
-
|
|
195
|
+
const {
|
|
196
|
+
multiple,
|
|
197
|
+
onQueryChange,
|
|
198
|
+
onChange,
|
|
199
|
+
onCreate,
|
|
200
|
+
id,
|
|
201
|
+
name,
|
|
202
|
+
disabled = false,
|
|
203
|
+
onBlur,
|
|
204
|
+
isValidNewOption = (query: string) => Boolean(query),
|
|
205
|
+
createOptionPosition = 'last',
|
|
206
|
+
} = props
|
|
196
207
|
|
|
197
208
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
198
209
|
const datasources = useMemo<SelectDatasource<any>[]>(() => getDatasources(props), [
|
|
@@ -221,11 +232,32 @@ function useSelect(props: SelectProps): useSelectReturn {
|
|
|
221
232
|
},
|
|
222
233
|
})
|
|
223
234
|
|
|
235
|
+
const [queryTyped, setQueryTyped] = useState(false)
|
|
224
236
|
const [query, setQuery] = useState<string>(
|
|
225
237
|
getDisplayValue(adapters, selectable.selected, multiple)
|
|
226
238
|
)
|
|
227
239
|
const options = useOptions({ datasources, adapters })
|
|
228
240
|
|
|
241
|
+
const expandDisabled = useMemo(
|
|
242
|
+
() =>
|
|
243
|
+
!query.length &&
|
|
244
|
+
isEmpty(options.get()) &&
|
|
245
|
+
isEmpty(selectable.selected) &&
|
|
246
|
+
isFunction(isValidNewOption)
|
|
247
|
+
? !isValidNewOption(query)
|
|
248
|
+
: !isValidNewOption,
|
|
249
|
+
[query, options, selectable.selected, isValidNewOption]
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
const dropdown = useDropdown({ ...props, expandDisabled })
|
|
253
|
+
|
|
254
|
+
useEffect(() => {
|
|
255
|
+
if (queryTyped) {
|
|
256
|
+
options.fetch(query)
|
|
257
|
+
dropdown.expand()
|
|
258
|
+
}
|
|
259
|
+
}, [query, queryTyped])
|
|
260
|
+
|
|
229
261
|
const getSelectableOption = useCallback(
|
|
230
262
|
function getSelectableOption(option: Option) {
|
|
231
263
|
const adapter = getAdapter(adapters, option._type)
|
|
@@ -274,9 +306,19 @@ function useSelect(props: SelectProps): useSelectReturn {
|
|
|
274
306
|
|
|
275
307
|
onBlur?.(event)
|
|
276
308
|
},
|
|
309
|
+
expandDisabled,
|
|
277
310
|
}
|
|
278
311
|
},
|
|
279
|
-
[
|
|
312
|
+
[
|
|
313
|
+
adapters,
|
|
314
|
+
dropdown.expanded,
|
|
315
|
+
dropdown.toggle,
|
|
316
|
+
multiple,
|
|
317
|
+
options,
|
|
318
|
+
selectable.selected,
|
|
319
|
+
onBlur,
|
|
320
|
+
expandDisabled,
|
|
321
|
+
]
|
|
280
322
|
)
|
|
281
323
|
|
|
282
324
|
const getTriggerProps = useCallback(
|
|
@@ -292,15 +334,13 @@ function useSelect(props: SelectProps): useSelectReturn {
|
|
|
292
334
|
onChange(e: ChangeEvent<HTMLInputElement>) {
|
|
293
335
|
onQueryChange?.(e)
|
|
294
336
|
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
dropdown.expand()
|
|
298
|
-
options.fetch(query)
|
|
337
|
+
setQuery(e.target.value)
|
|
338
|
+
setQueryTyped(true)
|
|
299
339
|
},
|
|
300
340
|
onFocus: TriggerOnFocusHandler,
|
|
301
341
|
}
|
|
302
342
|
},
|
|
303
|
-
[id, query, onQueryChange, dropdown, options]
|
|
343
|
+
[id, query, onQueryChange, dropdown, options, selectable.selected]
|
|
304
344
|
)
|
|
305
345
|
|
|
306
346
|
const getClearProps = useCallback(
|
|
@@ -396,14 +436,17 @@ function useSelect(props: SelectProps): useSelectReturn {
|
|
|
396
436
|
)
|
|
397
437
|
}
|
|
398
438
|
|
|
399
|
-
|
|
400
|
-
isFunction(
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
439
|
+
function getIsValidNewOption() {
|
|
440
|
+
if (isFunction(isValidNewOption)) {
|
|
441
|
+
return isValidNewOption(query) && !isQueryEqualAnOption()
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
return isValidNewOption
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
return isFunction(onCreate) && options.status === 'queried' && getIsValidNewOption()
|
|
405
448
|
},
|
|
406
|
-
[getSelectableOption, onCreate, options, query, selectable.selected]
|
|
449
|
+
[getSelectableOption, isValidNewOption, onCreate, options, query, selectable.selected]
|
|
407
450
|
)
|
|
408
451
|
|
|
409
452
|
useEffect(
|
|
@@ -461,6 +504,7 @@ function useSelect(props: SelectProps): useSelectReturn {
|
|
|
461
504
|
getDropdownProps,
|
|
462
505
|
getCreatebleProps,
|
|
463
506
|
isCreatable,
|
|
507
|
+
createOptionPosition,
|
|
464
508
|
}
|
|
465
509
|
}
|
|
466
510
|
|
|
@@ -2,20 +2,26 @@ import { Meta } from '@storybook/addon-docs/blocks'
|
|
|
2
2
|
|
|
3
3
|
<Meta title="Getting started page" />
|
|
4
4
|
|
|
5
|
-
|
|
6
5
|
Miranda UI is a [React](https://reactjs.org/) components library. It works with [Styled Components](https://www.styled-components.com).
|
|
7
6
|
|
|
8
7
|
## Steps to install
|
|
9
8
|
|
|
10
|
-
1.
|
|
11
|
-
|
|
9
|
+
1. You need to have node 16 or the latest LTS version installed;
|
|
10
|
+
|
|
11
|
+
One option is to use [NVM](https://github.com/nvm-sh/nvm). You can run `nvm use` or set it to run automatically in a directory with a .nvmrc file.
|
|
12
|
+
|
|
13
|
+
1. Install dependencies: `yarn`
|
|
14
|
+
|
|
15
|
+
If you don't have yarn, follow yarn [installation docs](https://classic.yarnpkg.com/lang/en/docs/install/#windows-stable)
|
|
12
16
|
|
|
13
17
|
## Usage
|
|
14
18
|
|
|
15
19
|
```sh
|
|
16
20
|
yarn add @loadsmart/loadsmart-ui
|
|
17
21
|
```
|
|
22
|
+
|
|
18
23
|
or
|
|
24
|
+
|
|
19
25
|
```sh
|
|
20
26
|
npm install @loadsmart/loadsmart-ui
|
|
21
27
|
```
|
|
@@ -43,7 +49,9 @@ Install all dependencies:
|
|
|
43
49
|
```bash
|
|
44
50
|
yarn
|
|
45
51
|
```
|
|
46
|
-
|
|
52
|
+
|
|
53
|
+
or
|
|
54
|
+
|
|
47
55
|
```bash
|
|
48
56
|
npm i
|
|
49
57
|
```
|
|
@@ -53,7 +61,9 @@ Run the application - you'll be able to see all components documentation in Stor
|
|
|
53
61
|
```bash
|
|
54
62
|
yarn dev
|
|
55
63
|
```
|
|
56
|
-
|
|
64
|
+
|
|
65
|
+
or
|
|
66
|
+
|
|
57
67
|
```bash
|
|
58
68
|
npm run dev
|
|
59
69
|
```
|
|
@@ -65,7 +75,9 @@ To run tests:
|
|
|
65
75
|
```bash
|
|
66
76
|
yarn test
|
|
67
77
|
```
|
|
68
|
-
|
|
78
|
+
|
|
79
|
+
or
|
|
80
|
+
|
|
69
81
|
```bash
|
|
70
82
|
npm run test
|
|
71
83
|
```
|
|
@@ -77,7 +89,7 @@ When creating a new one, you must add its new folder following this pattern.
|
|
|
77
89
|
|
|
78
90
|
It's also essential to include the `.stories` and `.test` files to cover both documentation and quality standards.
|
|
79
91
|
|
|
80
|
-
|
|
81
|
-
<br
|
|
92
|
+
<br />
|
|
93
|
+
<br />
|
|
82
94
|
|
|
83
95
|
We use [`semantic-release`](https://github.com/semantic-release/) to evaluate our commits and trigger automatic release to NPM. For that, please follow [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/), so your changes will properly be evaluated and published if that's the case.
|
|
@@ -42,6 +42,10 @@ async function expand(input: HTMLElement): Promise<void> {
|
|
|
42
42
|
return
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
+
await waitFor(() => {
|
|
46
|
+
expect(within(selectContainer).getByTestId('select-trigger-handle')).toBeEnabled()
|
|
47
|
+
})
|
|
48
|
+
|
|
45
49
|
await act(async () => {
|
|
46
50
|
userEvent.click(within(selectContainer).getByTestId('select-trigger-handle'))
|
|
47
51
|
|