@instructure/ui-simple-select 11.7.2-snapshot-48 → 11.7.2-snapshot-49

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.
Files changed (39) hide show
  1. package/CHANGELOG.md +12 -2
  2. package/es/SimpleSelect/v2/Group/index.js +46 -0
  3. package/es/SimpleSelect/v2/Group/props.js +26 -0
  4. package/es/SimpleSelect/v2/Option/index.js +48 -0
  5. package/es/SimpleSelect/v2/Option/props.js +26 -0
  6. package/es/SimpleSelect/v2/index.js +444 -0
  7. package/es/SimpleSelect/v2/props.js +26 -0
  8. package/es/exports/b.js +26 -0
  9. package/lib/SimpleSelect/v2/Group/index.js +52 -0
  10. package/lib/SimpleSelect/v2/Group/props.js +31 -0
  11. package/lib/SimpleSelect/v2/Option/index.js +54 -0
  12. package/lib/SimpleSelect/v2/Option/props.js +31 -0
  13. package/lib/SimpleSelect/v2/index.js +454 -0
  14. package/lib/SimpleSelect/v2/props.js +31 -0
  15. package/lib/exports/b.js +26 -0
  16. package/package.json +21 -21
  17. package/src/SimpleSelect/v2/Group/index.tsx +51 -0
  18. package/src/SimpleSelect/v2/Group/props.ts +49 -0
  19. package/src/SimpleSelect/v2/Option/index.tsx +52 -0
  20. package/src/SimpleSelect/v2/Option/props.ts +83 -0
  21. package/src/SimpleSelect/v2/README.md +157 -0
  22. package/src/SimpleSelect/v2/index.tsx +559 -0
  23. package/src/SimpleSelect/v2/props.ts +300 -0
  24. package/src/exports/b.ts +31 -0
  25. package/tsconfig.build.tsbuildinfo +1 -1
  26. package/types/SimpleSelect/v2/Group/index.d.ts +20 -0
  27. package/types/SimpleSelect/v2/Group/index.d.ts.map +1 -0
  28. package/types/SimpleSelect/v2/Group/props.d.ts +19 -0
  29. package/types/SimpleSelect/v2/Group/props.d.ts.map +1 -0
  30. package/types/SimpleSelect/v2/Option/index.d.ts +26 -0
  31. package/types/SimpleSelect/v2/Option/index.d.ts.map +1 -0
  32. package/types/SimpleSelect/v2/Option/props.d.ts +45 -0
  33. package/types/SimpleSelect/v2/Option/props.d.ts.map +1 -0
  34. package/types/SimpleSelect/v2/index.d.ts +90 -0
  35. package/types/SimpleSelect/v2/index.d.ts.map +1 -0
  36. package/types/SimpleSelect/v2/props.d.ts +180 -0
  37. package/types/SimpleSelect/v2/props.d.ts.map +1 -0
  38. package/types/exports/b.d.ts +7 -0
  39. package/types/exports/b.d.ts.map +1 -0
@@ -0,0 +1,559 @@
1
+ /*
2
+ * The MIT License (MIT)
3
+ *
4
+ * Copyright (c) 2015 - present Instructure, Inc.
5
+ *
6
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
7
+ * of this software and associated documentation files (the "Software"), to deal
8
+ * in the Software without restriction, including without limitation the rights
9
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
+ * copies of the Software, and to permit persons to whom the Software is
11
+ * furnished to do so, subject to the following conditions:
12
+ *
13
+ * The above copyright notice and this permission notice shall be included in all
14
+ * copies or substantial portions of the Software.
15
+ *
16
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22
+ * SOFTWARE.
23
+ */
24
+
25
+ import {
26
+ isValidElement,
27
+ ComponentElement,
28
+ Component,
29
+ Children,
30
+ type ReactElement
31
+ } from 'react'
32
+
33
+ import * as utils from '@instructure/ui-utils'
34
+ import {
35
+ matchComponentTypes,
36
+ passthroughProps,
37
+ callRenderProp,
38
+ getInteraction,
39
+ withDeterministicId
40
+ } from '@instructure/ui-react-utils'
41
+
42
+ import { Select } from '@instructure/ui-select/latest'
43
+ import type { SelectProps } from '@instructure/ui-select/latest'
44
+
45
+ import { Option } from './Option'
46
+ import type {
47
+ SimpleSelectOptionProps,
48
+ RenderSimpleSelectOptionLabel
49
+ } from './Option/props'
50
+
51
+ import { Group } from './Group'
52
+ import type { SimpleSelectGroupProps } from './Group/props'
53
+
54
+ import type { SimpleSelectProps } from './props'
55
+ import { allowedProps, SimpleSelectState } from './props'
56
+
57
+ type OptionChild = ComponentElement<SimpleSelectOptionProps, Option>
58
+ type GroupChild = ComponentElement<SimpleSelectGroupProps, Group>
59
+
60
+ type GetOption = <F extends keyof SimpleSelectOptionProps>(
61
+ field: F,
62
+ value?: SimpleSelectOptionProps[F]
63
+ ) => OptionChild | undefined
64
+
65
+ /**
66
+ ---
67
+ category: components
68
+ tags: form, field, dropdown
69
+ ---
70
+ **/
71
+ @withDeterministicId()
72
+ class SimpleSelect extends Component<SimpleSelectProps, SimpleSelectState> {
73
+ static readonly componentId = 'SimpleSelect'
74
+
75
+ static Option = Option
76
+ static Group = Group
77
+
78
+ static allowedProps = allowedProps
79
+
80
+ static defaultProps = {
81
+ size: 'medium',
82
+ isRequired: false,
83
+ isInline: false,
84
+ visibleOptionsCount: 8,
85
+ placement: 'bottom stretch',
86
+ constrain: 'window',
87
+ renderEmptyOption: '---',
88
+ isOptionContentAppliedToInput: false
89
+ }
90
+
91
+ ref: Select | null = null
92
+
93
+ private readonly _emptyOptionId
94
+
95
+ constructor(props: SimpleSelectProps) {
96
+ super(props)
97
+
98
+ const option = this.getInitialOption(props)
99
+
100
+ this.state = {
101
+ inputValue: option ? option.props.children : '',
102
+ isShowingOptions: false,
103
+ highlightedOptionId: undefined,
104
+ selectedOptionId: option ? option.props.id : undefined
105
+ }
106
+
107
+ this._emptyOptionId = props.deterministicId!('Select-EmptyOption')
108
+ }
109
+
110
+ get _select() {
111
+ console.warn(
112
+ '_select property is deprecated and will be removed in v9, please use ref instead'
113
+ )
114
+
115
+ return this.ref
116
+ }
117
+
118
+ focus() {
119
+ this.ref && this.ref.focus()
120
+ }
121
+
122
+ blur() {
123
+ this.ref && this.ref.blur()
124
+ }
125
+
126
+ get focused() {
127
+ return this.ref ? this.ref.focused : false
128
+ }
129
+
130
+ get id() {
131
+ return this.ref ? this.ref.id : undefined
132
+ }
133
+
134
+ get isControlled() {
135
+ return typeof this.props.value !== 'undefined'
136
+ }
137
+
138
+ get interaction() {
139
+ return getInteraction({ props: this.props })
140
+ }
141
+
142
+ hasOptionsChanged(
143
+ prevChildren: SimpleSelectProps['children'],
144
+ currentChildren: SimpleSelectProps['children']
145
+ ) {
146
+ const getValues = (children: SimpleSelectProps['children']) =>
147
+ Children.map(children, (child) => {
148
+ if (isValidElement(child)) {
149
+ return (child as ReactElement<any>).props.value
150
+ }
151
+ return null
152
+ })
153
+
154
+ const prevValues = getValues(prevChildren)
155
+ const currentValues = getValues(currentChildren)
156
+
157
+ return JSON.stringify(prevValues) !== JSON.stringify(currentValues)
158
+ }
159
+
160
+ componentDidUpdate(prevProps: SimpleSelectProps) {
161
+ if (this.hasOptionsChanged(prevProps.children, this.props.children)) {
162
+ // Compare current input value to children's child prop, this is put into
163
+ // state.inputValue
164
+ const option = this.getOption('children', this.state.inputValue)
165
+ this.setState({
166
+ inputValue: option ? option.props.children : undefined,
167
+ selectedOptionId: option ? option.props.id : ''
168
+ })
169
+ }
170
+ if (this.props.value !== prevProps.value) {
171
+ // if value has changed externally try to find an option with the same value
172
+ // and select it
173
+ let option = this.getOption('value', this.props.value)
174
+ if (typeof this.props.value === 'undefined') {
175
+ // preserve current value when changing from controlled to uncontrolled
176
+ option = this.getOption('value', prevProps.value)
177
+ }
178
+ this.setState({
179
+ inputValue: option ? option.props.children : '',
180
+ selectedOptionId: option ? option.props.id : ''
181
+ })
182
+ }
183
+ }
184
+
185
+ getInitialOption(props: SimpleSelectProps) {
186
+ const { value, defaultValue } = props
187
+ const initialValue = value || defaultValue
188
+
189
+ if (typeof initialValue === 'string' || typeof initialValue === 'number') {
190
+ // get option based on value or defaultValue, if provided
191
+ return this.getOption('value', initialValue)
192
+ }
193
+ // otherwise get the first option
194
+ return this.getFirstOption()
195
+ }
196
+
197
+ getOptionLabelById(id: string) {
198
+ const option = this.getOption('id', id)
199
+ return option ? option.props.children : ''
200
+ }
201
+
202
+ getFirstOption() {
203
+ const children = Children.toArray(this.props.children) as (
204
+ | OptionChild
205
+ | GroupChild
206
+ )[]
207
+ let match: OptionChild | undefined
208
+
209
+ for (let i = 0; i < children.length; i++) {
210
+ const child = children[i]
211
+ if (matchComponentTypes<OptionChild>(child, [Option])) {
212
+ match = child
213
+ } else if (matchComponentTypes<GroupChild>(child, [Group])) {
214
+ // first child is a group, not an option, find first child in group
215
+ match = (Children.toArray(child.props.children) as OptionChild[])[0]
216
+ }
217
+ if (match) {
218
+ break
219
+ }
220
+ }
221
+ return match
222
+ }
223
+
224
+ getOption: GetOption = (field, value) => {
225
+ const children = Children.toArray(this.props.children) as (
226
+ | OptionChild
227
+ | GroupChild
228
+ )[]
229
+ let match: OptionChild | undefined
230
+
231
+ for (let i = 0; i < children.length; ++i) {
232
+ const child = children[i]
233
+ if (matchComponentTypes<OptionChild>(child, [Option])) {
234
+ if (child.props[field] === value) {
235
+ match = child
236
+ }
237
+ } else if (matchComponentTypes<GroupChild>(child, [Group])) {
238
+ const groupChildren = Children.toArray(
239
+ child.props.children
240
+ ) as OptionChild[]
241
+ for (let j = 0; j < groupChildren.length; ++j) {
242
+ const groupChild = groupChildren[j]
243
+ if (groupChild.props[field] === value) {
244
+ match = groupChild
245
+ break
246
+ }
247
+ }
248
+ }
249
+ if (match) break
250
+ }
251
+ return match
252
+ }
253
+
254
+ getOptionByPosition(position: 'first' | 'last'): OptionChild | undefined {
255
+ const children = Children.toArray(this.props.children)
256
+
257
+ // Determine where to start looking based on position
258
+ const index = position === 'first' ? 0 : children.length - 1
259
+
260
+ // Check if child is an option or group
261
+ const child = children[index]
262
+ if (!child) return undefined
263
+
264
+ // If it's a regular option, return it
265
+ if (matchComponentTypes<OptionChild>(child, [Option])) {
266
+ return child
267
+ }
268
+
269
+ // If it's a group, get its options
270
+ if (matchComponentTypes<GroupChild>(child, [Group])) {
271
+ const groupOptions = Children.toArray(child.props.children)
272
+ const groupIndex = position === 'first' ? 0 : groupOptions.length - 1
273
+ return groupOptions[groupIndex] as OptionChild
274
+ }
275
+
276
+ return undefined
277
+ }
278
+
279
+ handleRef = (node: Select) => {
280
+ this.ref = node
281
+ }
282
+
283
+ handleBlur: SelectProps['onBlur'] = (event) => {
284
+ this.setState({ highlightedOptionId: undefined })
285
+ if (typeof this.props.onBlur === 'function') {
286
+ this.props.onBlur(event)
287
+ }
288
+ }
289
+
290
+ handleShowOptions: SelectProps['onRequestShowOptions'] = (event) => {
291
+ this.setState({ isShowingOptions: true })
292
+ if (typeof this.props.onShowOptions === 'function') {
293
+ this.props.onShowOptions(event)
294
+ }
295
+
296
+ if (event.type.startsWith('key')) {
297
+ const keyboardEvent = event as React.KeyboardEvent
298
+ const children = Children.toArray(this.props.children) as (
299
+ | OptionChild
300
+ | GroupChild
301
+ )[]
302
+
303
+ if (!this.state.inputValue && children.length > 0) {
304
+ const position =
305
+ keyboardEvent.key === 'ArrowDown'
306
+ ? 'first'
307
+ : keyboardEvent.key === 'ArrowUp'
308
+ ? 'last'
309
+ : undefined
310
+ if (position) {
311
+ const optionId = this.getOptionByPosition(position)?.props.id
312
+ optionId &&
313
+ this.setState({
314
+ highlightedOptionId: optionId
315
+ })
316
+ }
317
+ }
318
+ }
319
+ }
320
+
321
+ handleHideOptions: SelectProps['onRequestHideOptions'] = (event) => {
322
+ this.setState((state) => {
323
+ const option = this.getOption('id', state.selectedOptionId)
324
+ return {
325
+ isShowingOptions: false,
326
+ highlightedOptionId: undefined,
327
+ inputValue: option ? option.props.children : ''
328
+ }
329
+ })
330
+ if (typeof this.props.onHideOptions === 'function') {
331
+ this.props.onHideOptions(event)
332
+ }
333
+ }
334
+
335
+ handleHighlightOption: SelectProps['onRequestHighlightOption'] = (
336
+ _event,
337
+ { id }
338
+ ) => {
339
+ if (id === this._emptyOptionId) return
340
+
341
+ this.setState({
342
+ highlightedOptionId: id,
343
+ inputValue: this.state.inputValue
344
+ })
345
+ }
346
+
347
+ handleSelectOption: SelectProps['onRequestSelectOption'] = (
348
+ event,
349
+ { id }
350
+ ) => {
351
+ if (id === this._emptyOptionId) {
352
+ // selected option is the empty option
353
+ this.setState({ isShowingOptions: false })
354
+ return
355
+ }
356
+
357
+ const option = this.getOption('id', id)
358
+ const value = option && option.props.value
359
+
360
+ // Focus needs to be reapplied to input
361
+ // after selecting an item to make sure VoiceOver behaves correctly on iOS
362
+ if (utils.isAndroidOrIOS()) {
363
+ this.blur()
364
+ this.focus()
365
+ }
366
+
367
+ if (this.isControlled) {
368
+ this.setState({ isShowingOptions: false })
369
+ } else {
370
+ this.setState((state) => ({
371
+ isShowingOptions: false,
372
+ selectedOptionId: id,
373
+ inputValue: option ? option.props.children : state.inputValue
374
+ }))
375
+ }
376
+ // fire onChange if selected option changed
377
+ if (option && typeof this.props.onChange === 'function') {
378
+ this.props.onChange(event, { value, id })
379
+ }
380
+ // hide options list whenever selection is made
381
+ if (typeof this.props.onHideOptions === 'function') {
382
+ this.props.onHideOptions(event)
383
+ }
384
+ }
385
+
386
+ renderChildren() {
387
+ let children = Children.toArray(this.props.children) as (
388
+ | OptionChild
389
+ | GroupChild
390
+ )[]
391
+ children = Children.map(children, (child) => {
392
+ if (matchComponentTypes<OptionChild>(child, [Option])) {
393
+ return this.renderOption(child)
394
+ } else if (matchComponentTypes<GroupChild>(child, [Group])) {
395
+ return this.renderGroup(child)
396
+ }
397
+ return null
398
+ }).filter((child) => !!child)
399
+
400
+ if (children.length === 0) {
401
+ // no valid children, render empty option
402
+ return this.renderEmptyOption()
403
+ }
404
+
405
+ return children
406
+ }
407
+
408
+ renderEmptyOption() {
409
+ return (
410
+ <Select.Option
411
+ id={this._emptyOptionId}
412
+ isHighlighted={false}
413
+ isSelected={false}
414
+ >
415
+ {callRenderProp(this.props.renderEmptyOption)}
416
+ </Select.Option>
417
+ ) as OptionChild
418
+ }
419
+
420
+ renderOption(option: OptionChild) {
421
+ const {
422
+ id,
423
+ value,
424
+ children,
425
+ renderBeforeLabel,
426
+ renderAfterLabel,
427
+ ...rest
428
+ } = option.props
429
+
430
+ const isDisabled = option.props.isDisabled ?? false // after the react 19 upgrade `isDisabled` is undefined instead of defaulting to false if not specified (but only in vitest env for some reason)
431
+ const isSelected = id === this.state.selectedOptionId
432
+ const isHighlighted = id === this.state.highlightedOptionId
433
+
434
+ const getRenderLabel = (renderLabel: RenderSimpleSelectOptionLabel) => {
435
+ if (
436
+ typeof renderLabel === 'function' &&
437
+ !renderLabel?.prototype?.isReactComponent
438
+ ) {
439
+ return (renderLabel as any).bind(null, {
440
+ id,
441
+ isDisabled,
442
+ isSelected,
443
+ isHighlighted,
444
+ children
445
+ })
446
+ }
447
+ return renderLabel
448
+ }
449
+
450
+ return (
451
+ <Select.Option
452
+ id={id}
453
+ value={value}
454
+ key={option.key || id}
455
+ isHighlighted={isHighlighted}
456
+ isSelected={isSelected}
457
+ isDisabled={isDisabled}
458
+ renderBeforeLabel={getRenderLabel(renderBeforeLabel)}
459
+ renderAfterLabel={getRenderLabel(renderAfterLabel)}
460
+ {...passthroughProps(rest)}
461
+ >
462
+ {children}
463
+ </Select.Option>
464
+ ) as OptionChild
465
+ }
466
+
467
+ renderGroup(group: GroupChild) {
468
+ const { id, renderLabel, children, ...rest } = group.props
469
+ return (
470
+ <Select.Group
471
+ renderLabel={renderLabel}
472
+ key={group.key || id}
473
+ {...passthroughProps(rest)}
474
+ >
475
+ {Children.map(children as OptionChild[], (child) =>
476
+ this.renderOption(child)
477
+ )}
478
+ </Select.Group>
479
+ ) as GroupChild
480
+ }
481
+
482
+ render() {
483
+ const {
484
+ renderLabel,
485
+ value,
486
+ defaultValue,
487
+ id,
488
+ size,
489
+ assistiveText,
490
+ placeholder,
491
+ interaction,
492
+ isRequired,
493
+ isInline,
494
+ width,
495
+ optionsMaxWidth,
496
+ optionsMaxHeight,
497
+ visibleOptionsCount,
498
+ messages,
499
+ placement,
500
+ constrain,
501
+ mountNode,
502
+ inputRef,
503
+ listRef,
504
+ renderEmptyOption,
505
+ renderBeforeInput,
506
+ renderAfterInput,
507
+ onFocus,
508
+ onBlur,
509
+ onShowOptions,
510
+ onHideOptions,
511
+ children,
512
+ layout,
513
+ ...rest
514
+ } = this.props
515
+
516
+ return (
517
+ <Select
518
+ renderLabel={renderLabel}
519
+ inputValue={this.state.inputValue}
520
+ isShowingOptions={this.state.isShowingOptions}
521
+ id={id}
522
+ size={size}
523
+ assistiveText={assistiveText}
524
+ placeholder={placeholder}
525
+ interaction={this.interaction}
526
+ isRequired={isRequired}
527
+ isInline={isInline}
528
+ width={width}
529
+ optionsMaxWidth={optionsMaxWidth}
530
+ optionsMaxHeight={optionsMaxHeight}
531
+ visibleOptionsCount={visibleOptionsCount}
532
+ messages={messages}
533
+ placement={placement}
534
+ constrain={constrain}
535
+ mountNode={mountNode}
536
+ ref={this.handleRef}
537
+ inputRef={inputRef}
538
+ listRef={listRef}
539
+ renderBeforeInput={renderBeforeInput}
540
+ renderAfterInput={renderAfterInput}
541
+ onFocus={onFocus}
542
+ onBlur={this.handleBlur}
543
+ onRequestShowOptions={this.handleShowOptions}
544
+ onRequestHideOptions={this.handleHideOptions}
545
+ onRequestHighlightOption={this.handleHighlightOption}
546
+ onRequestSelectOption={this.handleSelectOption}
547
+ isOptionContentAppliedToInput={this.props.isOptionContentAppliedToInput}
548
+ layout={layout}
549
+ {...passthroughProps(rest)}
550
+ data-cid="SimpleSelect"
551
+ >
552
+ {this.renderChildren()}
553
+ </Select>
554
+ )
555
+ }
556
+ }
557
+
558
+ export { SimpleSelect }
559
+ export default SimpleSelect