@loadsmart/loadsmart-ui 5.6.3 → 5.8.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.
@@ -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 !== 'querying') {
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 === 'querying' ? 'Loading...' : `${after.length} option${pluralize(after.length)}`}
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 === 'querying' && isEmpty(select.options)) {
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
- {isCreatable && <CreatableOption />}
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
- {isCreatable && <CreatableOption />}
186
+ {select.createOptionPosition === 'last' && isCreatable ? <CreatableOption /> : null}
181
187
  </Fragment>
182
188
  )
183
189
 
184
- if (select.status !== 'querying' && isEmpty(remainingOptions)) {
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 !== 'querying' && isEmpty(select.options)) {
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 === 'querying') {
223
+ if (isQuerying(select.status)) {
218
224
  return <Loading data-testid="select-trigger-loading">&middot;&middot;&middot;</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 { multiple, onQueryChange, onChange, onCreate, id, name, disabled = false, onBlur } = props
195
- const dropdown = useDropdown(props)
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,26 @@ 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
+ () => !query.length && isEmpty(options.get()) && isEmpty(selectable.selected),
243
+ [query, options, selectable.selected]
244
+ )
245
+
246
+ const dropdown = useDropdown({ ...props, expandDisabled })
247
+
248
+ useEffect(() => {
249
+ if (queryTyped) {
250
+ options.fetch(query)
251
+ dropdown.expand()
252
+ }
253
+ }, [query, queryTyped])
254
+
229
255
  const getSelectableOption = useCallback(
230
256
  function getSelectableOption(option: Option) {
231
257
  const adapter = getAdapter(adapters, option._type)
@@ -274,9 +300,19 @@ function useSelect(props: SelectProps): useSelectReturn {
274
300
 
275
301
  onBlur?.(event)
276
302
  },
303
+ expandDisabled,
277
304
  }
278
305
  },
279
- [adapters, dropdown.expanded, dropdown.toggle, multiple, options, selectable.selected, onBlur]
306
+ [
307
+ adapters,
308
+ dropdown.expanded,
309
+ dropdown.toggle,
310
+ multiple,
311
+ options,
312
+ selectable.selected,
313
+ onBlur,
314
+ expandDisabled,
315
+ ]
280
316
  )
281
317
 
282
318
  const getTriggerProps = useCallback(
@@ -292,15 +328,13 @@ function useSelect(props: SelectProps): useSelectReturn {
292
328
  onChange(e: ChangeEvent<HTMLInputElement>) {
293
329
  onQueryChange?.(e)
294
330
 
295
- const query = e.target.value
296
- setQuery(query)
297
- dropdown.expand()
298
- options.fetch(query)
331
+ setQuery(e.target.value)
332
+ setQueryTyped(true)
299
333
  },
300
334
  onFocus: TriggerOnFocusHandler,
301
335
  }
302
336
  },
303
- [id, query, onQueryChange, dropdown, options]
337
+ [id, query, onQueryChange, dropdown, options, selectable.selected]
304
338
  )
305
339
 
306
340
  const getClearProps = useCallback(
@@ -396,14 +430,17 @@ function useSelect(props: SelectProps): useSelectReturn {
396
430
  )
397
431
  }
398
432
 
399
- return (
400
- isFunction(onCreate) &&
401
- Boolean(query) &&
402
- options.status === 'queried' &&
403
- !isQueryEqualAnOption()
404
- )
433
+ function getIsValidNewOption() {
434
+ if (isFunction(isValidNewOption)) {
435
+ return isValidNewOption(query) && !isQueryEqualAnOption()
436
+ }
437
+
438
+ return isValidNewOption
439
+ }
440
+
441
+ return isFunction(onCreate) && options.status === 'queried' && getIsValidNewOption()
405
442
  },
406
- [getSelectableOption, onCreate, options, query, selectable.selected]
443
+ [getSelectableOption, isValidNewOption, onCreate, options, query, selectable.selected]
407
444
  )
408
445
 
409
446
  useEffect(
@@ -461,6 +498,7 @@ function useSelect(props: SelectProps): useSelectReturn {
461
498
  getDropdownProps,
462
499
  getCreatebleProps,
463
500
  isCreatable,
501
+ createOptionPosition,
464
502
  }
465
503
  }
466
504
 
@@ -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. Install node 11
11
- 2. Install dependencies: `yarn`
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
- or
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
- or
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
- or
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 /><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
 
@@ -21,12 +21,18 @@ function getTokenFromTheme<P extends ThemedProps>(token: TokenLike<P>, props: P)
21
21
  * @param {[ThemedStyledProps]} props - Component props.
22
22
  * @returns {ThemeTokenValue} Token value or `undefined` if the token was not found for the current theme.
23
23
  */
24
- export function getToken<P extends ThemedProps>(
25
- token: TokenLike<P>,
26
- props?: P
27
- ): ThemeTokenValue | ((props: P) => ThemeTokenValue) {
24
+ export function getToken<TProps extends ThemedProps>(
25
+ token: TokenLike<TProps>
26
+ ): (props: TProps) => ThemeTokenValue
27
+
28
+ export function getToken<TProps extends ThemedProps>(
29
+ token: TokenLike<TProps>,
30
+ props: TProps
31
+ ): ThemeTokenValue
32
+
33
+ export function getToken<TProps extends ThemedProps>(token: any, props?: any): any {
28
34
  if (props === undefined) {
29
- return (props) => getTokenFromTheme(token, props)
35
+ return (props: TProps) => getTokenFromTheme(token, props)
30
36
  }
31
37
 
32
38
  return getTokenFromTheme(token, props)