@instructure/ui-select 11.7.4-snapshot-113 → 11.7.4-snapshot-116
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/CHANGELOG.md +7 -1
- package/package.json +19 -19
- package/src/Select/v2/README.md +17 -1
- package/src/Select/v2/index.tsx +125 -125
- package/tsconfig.build.tsbuildinfo +1 -1
- package/types/Select/v2/index.d.ts.map +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -3,7 +3,13 @@
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
|
5
5
|
|
|
6
|
-
## [11.7.4-snapshot-
|
|
6
|
+
## [11.7.4-snapshot-116](https://github.com/instructure/instructure-ui/compare/v11.7.3...v11.7.4-snapshot-116) (2026-07-20)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
### Bug Fixes
|
|
10
|
+
|
|
11
|
+
* **ui-select:** don't strip renderBeforeInput from the combobox accessible name ([28d5e24](https://github.com/instructure/instructure-ui/commit/28d5e24e65a24b1e706fb67407523537d5db78b8))
|
|
12
|
+
* **ui-select:** fix screen reader labels on multiple select ([6cc240f](https://github.com/instructure/instructure-ui/commit/6cc240f38af252171954078a9adc603e2e776eb1))
|
|
7
13
|
|
|
8
14
|
|
|
9
15
|
### Features
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@instructure/ui-select",
|
|
3
|
-
"version": "11.7.4-snapshot-
|
|
3
|
+
"version": "11.7.4-snapshot-116",
|
|
4
4
|
"description": "A component for select and autocomplete behavior.",
|
|
5
5
|
"author": "Instructure, Inc. Engineering and Product Design",
|
|
6
6
|
"module": "./es/index.js",
|
|
@@ -15,30 +15,30 @@
|
|
|
15
15
|
"license": "MIT",
|
|
16
16
|
"dependencies": {
|
|
17
17
|
"@babel/runtime": "^7.29.7",
|
|
18
|
-
"@instructure/
|
|
19
|
-
"@instructure/shared-types": "11.7.4-snapshot-
|
|
20
|
-
"@instructure/ui-
|
|
21
|
-
"@instructure/
|
|
22
|
-
"@instructure/ui-
|
|
23
|
-
"@instructure/ui-options": "11.7.4-snapshot-
|
|
24
|
-
"@instructure/ui-popover": "11.7.4-snapshot-
|
|
25
|
-
"@instructure/ui-
|
|
26
|
-
"@instructure/ui-react-utils": "11.7.4-snapshot-
|
|
27
|
-
"@instructure/ui-text-input": "11.7.4-snapshot-
|
|
28
|
-
"@instructure/ui-
|
|
29
|
-
"@instructure/ui-
|
|
30
|
-
"@instructure/ui-view": "11.7.4-snapshot-
|
|
31
|
-
"@instructure/ui-
|
|
32
|
-
"@instructure/uid": "11.7.4-snapshot-
|
|
18
|
+
"@instructure/ui-form-field": "11.7.4-snapshot-116",
|
|
19
|
+
"@instructure/shared-types": "11.7.4-snapshot-116",
|
|
20
|
+
"@instructure/ui-icons": "11.7.4-snapshot-116",
|
|
21
|
+
"@instructure/emotion": "11.7.4-snapshot-116",
|
|
22
|
+
"@instructure/ui-dom-utils": "11.7.4-snapshot-116",
|
|
23
|
+
"@instructure/ui-options": "11.7.4-snapshot-116",
|
|
24
|
+
"@instructure/ui-popover": "11.7.4-snapshot-116",
|
|
25
|
+
"@instructure/ui-selectable": "11.7.4-snapshot-116",
|
|
26
|
+
"@instructure/ui-react-utils": "11.7.4-snapshot-116",
|
|
27
|
+
"@instructure/ui-text-input": "11.7.4-snapshot-116",
|
|
28
|
+
"@instructure/ui-themes": "11.7.4-snapshot-116",
|
|
29
|
+
"@instructure/ui-utils": "11.7.4-snapshot-116",
|
|
30
|
+
"@instructure/ui-view": "11.7.4-snapshot-116",
|
|
31
|
+
"@instructure/ui-position": "11.7.4-snapshot-116",
|
|
32
|
+
"@instructure/uid": "11.7.4-snapshot-116"
|
|
33
33
|
},
|
|
34
34
|
"devDependencies": {
|
|
35
35
|
"@testing-library/jest-dom": "^6.9.1",
|
|
36
36
|
"@testing-library/react": "16.3.2",
|
|
37
37
|
"@testing-library/user-event": "^14.6.1",
|
|
38
38
|
"vitest": "^4.1.9",
|
|
39
|
-
"@instructure/ui-babel-preset": "11.7.4-snapshot-
|
|
40
|
-
"@instructure/ui-color-utils": "11.7.4-snapshot-
|
|
41
|
-
"@instructure/ui-scripts": "11.7.4-snapshot-
|
|
39
|
+
"@instructure/ui-babel-preset": "11.7.4-snapshot-116",
|
|
40
|
+
"@instructure/ui-color-utils": "11.7.4-snapshot-116",
|
|
41
|
+
"@instructure/ui-scripts": "11.7.4-snapshot-116"
|
|
42
42
|
},
|
|
43
43
|
"peerDependencies": {
|
|
44
44
|
"react": ">=18 <=19"
|
package/src/Select/v2/README.md
CHANGED
|
@@ -342,6 +342,8 @@ type: example
|
|
|
342
342
|
|
|
343
343
|
To mark an option as "highlighted", use the option's `isHighlighted` prop. Note that only one highlighted option is permitted. Similarly, use `isSelected` to mark an option or multiple options as "selected". When allowing multiple selections, it's best to render a [Tag](Tag) with [AccessibleContent](AccessibleContent) for each selected option via the `renderBeforeInput` prop.
|
|
344
344
|
|
|
345
|
+
> **Accessibility:** set an explicit `aria-label` on the `Select` (matching `renderLabel`) and list the selected options in the `assistiveText` (e.g. `"Alaska Selected, …"`), as the example below does. This keeps the combobox's accessible name limited to the field label, while screen reader users still hear which options are selected. Each pill's `"Remove …"` label is only announced when that pill receives focus.
|
|
346
|
+
|
|
345
347
|
```js
|
|
346
348
|
---
|
|
347
349
|
type: example
|
|
@@ -497,11 +499,25 @@ type: example
|
|
|
497
499
|
))
|
|
498
500
|
}
|
|
499
501
|
|
|
502
|
+
// Announce the current selection as part of the combobox's description, so
|
|
503
|
+
// navigating to the Select reads "Multiple Select, Alaska Selected, ..."
|
|
504
|
+
// rather than the "Remove ..." labels of the dismissible pills.
|
|
505
|
+
const selectedText = selectedOptionId
|
|
506
|
+
.map((id) => `${getOptionById(id).label} Selected`)
|
|
507
|
+
.join(', ')
|
|
508
|
+
const assistiveText = [
|
|
509
|
+
selectedText,
|
|
510
|
+
'Type or use arrow keys to navigate options. Multiple selections allowed.'
|
|
511
|
+
]
|
|
512
|
+
.filter(Boolean)
|
|
513
|
+
.join('. ')
|
|
514
|
+
|
|
500
515
|
return (
|
|
501
516
|
<div>
|
|
502
517
|
<Select
|
|
503
518
|
renderLabel="Multiple Select"
|
|
504
|
-
|
|
519
|
+
aria-label="Multiple Select"
|
|
520
|
+
assistiveText={assistiveText}
|
|
505
521
|
inputValue={inputValue}
|
|
506
522
|
isShowingOptions={isShowingOptions}
|
|
507
523
|
inputRef={(el) => {
|
package/src/Select/v2/index.tsx
CHANGED
|
@@ -111,18 +111,18 @@ const MemoedOption = memo(
|
|
|
111
111
|
(prevProps, nextProps) => {
|
|
112
112
|
return (
|
|
113
113
|
prevProps.selectOption.props.isHighlighted ===
|
|
114
|
-
|
|
114
|
+
nextProps.selectOption.props.isHighlighted &&
|
|
115
115
|
prevProps.selectOption.props.isSelected ===
|
|
116
|
-
|
|
116
|
+
nextProps.selectOption.props.isSelected &&
|
|
117
117
|
prevProps.selectOption.props.isDisabled ===
|
|
118
|
-
|
|
118
|
+
nextProps.selectOption.props.isDisabled &&
|
|
119
119
|
prevProps.selectOption.props.children ===
|
|
120
|
-
|
|
120
|
+
nextProps.selectOption.props.children &&
|
|
121
121
|
prevProps.selectOption.props.id === nextProps.selectOption.props.id &&
|
|
122
122
|
prevProps.selectOption.props.renderBeforeLabel ===
|
|
123
|
-
|
|
123
|
+
nextProps.selectOption.props.renderBeforeLabel &&
|
|
124
124
|
prevProps.selectOption.props.renderAfterLabel ===
|
|
125
|
-
|
|
125
|
+
nextProps.selectOption.props.renderAfterLabel &&
|
|
126
126
|
prevProps.children === nextProps.children
|
|
127
127
|
)
|
|
128
128
|
}
|
|
@@ -334,59 +334,59 @@ class Select extends Component<SelectProps> {
|
|
|
334
334
|
|
|
335
335
|
return this.interaction === 'enabled'
|
|
336
336
|
? {
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
337
|
+
onRequestShowOptions: (event) => {
|
|
338
|
+
onRequestShowOptions?.(event)
|
|
339
|
+
const selectedOptionId = this.selectedOptionId
|
|
340
340
|
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
}
|
|
345
|
-
},
|
|
346
|
-
onRequestHideOptions: (event) => {
|
|
347
|
-
onRequestHideOptions?.(event)
|
|
348
|
-
},
|
|
349
|
-
onRequestHighlightOption: (
|
|
350
|
-
event,
|
|
351
|
-
{ id, direction }: { id?: string; direction?: number }
|
|
352
|
-
) => {
|
|
353
|
-
if (!isShowingOptions) return
|
|
354
|
-
|
|
355
|
-
const highlightedOptionId = this.highlightedOptionId
|
|
356
|
-
// if id exists, use that
|
|
357
|
-
let highlightId = this._optionIds.indexOf(id!) > -1 ? id : undefined
|
|
358
|
-
if (!highlightId) {
|
|
359
|
-
if (!highlightedOptionId) {
|
|
360
|
-
// nothing highlighted yet, highlight first option
|
|
361
|
-
highlightId = this._optionIds[0]
|
|
362
|
-
} else {
|
|
363
|
-
// find next id based on direction
|
|
364
|
-
const index = this._optionIds.indexOf(highlightedOptionId)
|
|
365
|
-
highlightId =
|
|
366
|
-
index > -1 ? this._optionIds[index + direction!] : undefined
|
|
341
|
+
if (selectedOptionId && !Array.isArray(selectedOptionId)) {
|
|
342
|
+
// highlight selected option on show
|
|
343
|
+
this.highlightOption(event, selectedOptionId)
|
|
367
344
|
}
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
},
|
|
374
|
-
onRequestHighlightFirstOption: (event) => {
|
|
375
|
-
this.highlightOption(event, this._optionIds[0])
|
|
376
|
-
},
|
|
377
|
-
onRequestHighlightLastOption: (event) => {
|
|
378
|
-
this.highlightOption(
|
|
345
|
+
},
|
|
346
|
+
onRequestHideOptions: (event) => {
|
|
347
|
+
onRequestHideOptions?.(event)
|
|
348
|
+
},
|
|
349
|
+
onRequestHighlightOption: (
|
|
379
350
|
event,
|
|
380
|
-
|
|
381
|
-
)
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
//
|
|
386
|
-
|
|
351
|
+
{ id, direction }: { id?: string; direction?: number }
|
|
352
|
+
) => {
|
|
353
|
+
if (!isShowingOptions) return
|
|
354
|
+
|
|
355
|
+
const highlightedOptionId = this.highlightedOptionId
|
|
356
|
+
// if id exists, use that
|
|
357
|
+
let highlightId = this._optionIds.indexOf(id!) > -1 ? id : undefined
|
|
358
|
+
if (!highlightId) {
|
|
359
|
+
if (!highlightedOptionId) {
|
|
360
|
+
// nothing highlighted yet, highlight first option
|
|
361
|
+
highlightId = this._optionIds[0]
|
|
362
|
+
} else {
|
|
363
|
+
// find next id based on direction
|
|
364
|
+
const index = this._optionIds.indexOf(highlightedOptionId)
|
|
365
|
+
highlightId =
|
|
366
|
+
index > -1 ? this._optionIds[index + direction!] : undefined
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
if (highlightId) {
|
|
370
|
+
// only highlight if id exists as a valid option
|
|
371
|
+
this.highlightOption(event, highlightId)
|
|
372
|
+
}
|
|
373
|
+
},
|
|
374
|
+
onRequestHighlightFirstOption: (event) => {
|
|
375
|
+
this.highlightOption(event, this._optionIds[0])
|
|
376
|
+
},
|
|
377
|
+
onRequestHighlightLastOption: (event) => {
|
|
378
|
+
this.highlightOption(
|
|
379
|
+
event,
|
|
380
|
+
this._optionIds[this._optionIds.length - 1]
|
|
381
|
+
)
|
|
382
|
+
},
|
|
383
|
+
onRequestSelectOption: (event, { id }) => {
|
|
384
|
+
if (id && this._optionIds.indexOf(id) !== -1) {
|
|
385
|
+
// only select if id exists as a valid option
|
|
386
|
+
onRequestSelectOption?.(event, { id })
|
|
387
|
+
}
|
|
387
388
|
}
|
|
388
389
|
}
|
|
389
|
-
}
|
|
390
390
|
: {}
|
|
391
391
|
}
|
|
392
392
|
|
|
@@ -413,12 +413,12 @@ class Select extends Component<SelectProps> {
|
|
|
413
413
|
return typeof renderOptionLabel === 'function' &&
|
|
414
414
|
!renderOptionLabel?.prototype?.isReactComponent
|
|
415
415
|
? (renderOptionLabel as any).bind(null, {
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
416
|
+
id,
|
|
417
|
+
isDisabled,
|
|
418
|
+
isSelected,
|
|
419
|
+
isHighlighted,
|
|
420
|
+
children
|
|
421
|
+
})
|
|
422
422
|
: (renderOptionLabel as React.ReactNode)
|
|
423
423
|
}
|
|
424
424
|
|
|
@@ -532,18 +532,18 @@ class Select extends Component<SelectProps> {
|
|
|
532
532
|
|
|
533
533
|
const viewProps: Partial<ViewProps> = isShowingOptions
|
|
534
534
|
? {
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
535
|
+
display: 'block',
|
|
536
|
+
overflowY: 'auto',
|
|
537
|
+
maxHeight:
|
|
538
|
+
optionsMaxHeight ||
|
|
539
|
+
this._optionHeight * visibleOptionsCount! -
|
|
540
|
+
// in Chrome, we need to prevent scrolling when the bottom area of last item is hovered
|
|
541
|
+
(utils.isChromium() ? this.SCROLL_TOLERANCE : 0),
|
|
542
|
+
maxWidth: optionsMaxWidth || this.width,
|
|
543
|
+
background: 'primary',
|
|
544
|
+
elementRef: (node: Element | null) => (this._listView = node),
|
|
545
|
+
borderRadius: 'inherit'
|
|
546
|
+
}
|
|
547
547
|
: { maxHeight: 0 }
|
|
548
548
|
|
|
549
549
|
return (
|
|
@@ -553,30 +553,30 @@ class Select extends Component<SelectProps> {
|
|
|
553
553
|
>
|
|
554
554
|
{isShowingOptions
|
|
555
555
|
? Children.map(children as SelectChildren, (child, index) => {
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
556
|
+
if (!child || !matchComponentTypes(child, [Group, Option])) {
|
|
557
|
+
return // ignore invalid children
|
|
558
|
+
}
|
|
559
|
+
if (matchComponentTypes<OptionChild>(child, [Option])) {
|
|
560
|
+
lastWasGroup = false
|
|
561
|
+
return this.renderOption(child, {
|
|
562
|
+
getOptionProps,
|
|
563
|
+
getDisabledOptionProps
|
|
564
|
+
})
|
|
565
|
+
}
|
|
566
|
+
if (matchComponentTypes<GroupChild>(child, [Group])) {
|
|
567
|
+
const afterGroup = lastWasGroup
|
|
568
|
+
lastWasGroup = true
|
|
569
|
+
return this.renderGroup(child, {
|
|
570
|
+
getOptionProps,
|
|
571
|
+
getDisabledOptionProps,
|
|
572
|
+
// for rendering separators appropriately
|
|
573
|
+
isFirstChild: index === 0,
|
|
574
|
+
isLastChild: index === Children.count(children) - 1,
|
|
575
|
+
afterGroup
|
|
576
|
+
})
|
|
577
|
+
}
|
|
578
|
+
return
|
|
579
|
+
})
|
|
580
580
|
: null}
|
|
581
581
|
</Options>
|
|
582
582
|
</View>
|
|
@@ -615,8 +615,8 @@ class Select extends Component<SelectProps> {
|
|
|
615
615
|
return position === 'before'
|
|
616
616
|
? option.props.renderBeforeLabel
|
|
617
617
|
: option.props.renderAfterLabel
|
|
618
|
-
|
|
619
|
-
|
|
618
|
+
? option.props.renderAfterLabel
|
|
619
|
+
: this.renderIcon()
|
|
620
620
|
}
|
|
621
621
|
}
|
|
622
622
|
} else {
|
|
@@ -625,8 +625,8 @@ class Select extends Component<SelectProps> {
|
|
|
625
625
|
return position === 'before'
|
|
626
626
|
? child.props.renderBeforeLabel
|
|
627
627
|
: child.props.renderAfterLabel
|
|
628
|
-
|
|
629
|
-
|
|
628
|
+
? child.props.renderAfterLabel
|
|
629
|
+
: this.renderIcon()
|
|
630
630
|
}
|
|
631
631
|
}
|
|
632
632
|
}
|
|
@@ -723,26 +723,26 @@ class Select extends Component<SelectProps> {
|
|
|
723
723
|
// popup buttons rather than comboboxes.
|
|
724
724
|
const overrideProps: Partial<TextInputProps> = !isEditable
|
|
725
725
|
? {
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
726
|
+
// We need role="combobox" for the 'open list' button shortcut to work
|
|
727
|
+
// with desktop screenreaders.
|
|
728
|
+
// But desktop Safari with Voiceover does not support proper combobox
|
|
729
|
+
// handling, a 'button' role is set as a workaround.
|
|
730
|
+
// See https://bugs.webkit.org/show_bug.cgi?id=236881
|
|
731
|
+
// Also on iOS Chrome with role='combobox' it announces unnecessarily
|
|
732
|
+
// that its 'read-only' and that this is a 'textfield', see INSTUI-4500
|
|
733
|
+
role:
|
|
734
|
+
utils.isSafari() ||
|
|
735
735
|
utils.isAndroidOrIOS() ||
|
|
736
736
|
(interaction === 'disabled' && utils.isChromium())
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
737
|
+
? 'button'
|
|
738
|
+
: 'combobox',
|
|
739
|
+
title: inputValue,
|
|
740
|
+
'aria-autocomplete': undefined,
|
|
741
|
+
'aria-readonly': true
|
|
742
|
+
}
|
|
743
743
|
: interaction === 'disabled' && utils.isChromium()
|
|
744
|
-
|
|
745
|
-
|
|
744
|
+
? { role: 'button' }
|
|
745
|
+
: {}
|
|
746
746
|
|
|
747
747
|
// backdoor to autocomplete attr to work around chrome autofill issues
|
|
748
748
|
if (passthroughProps['autoComplete']) {
|
|
@@ -789,8 +789,8 @@ class Select extends Component<SelectProps> {
|
|
|
789
789
|
typeof onInputChange === 'function'
|
|
790
790
|
? onInputChange
|
|
791
791
|
: inputValue
|
|
792
|
-
|
|
793
|
-
|
|
792
|
+
? () => {}
|
|
793
|
+
: undefined,
|
|
794
794
|
|
|
795
795
|
onFocus,
|
|
796
796
|
onBlur: utils.createChainedFunction(onBlur, onRequestHideOptions),
|
|
@@ -804,10 +804,10 @@ class Select extends Component<SelectProps> {
|
|
|
804
804
|
suppressHydrationWarning
|
|
805
805
|
{...(interaction === 'enabled' &&
|
|
806
806
|
!isEditable && {
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
807
|
+
themeOverride: (componentTheme) => ({
|
|
808
|
+
backgroundReadonlyColor: componentTheme.backgroundColor
|
|
809
|
+
})
|
|
810
|
+
})}
|
|
811
811
|
/>
|
|
812
812
|
)
|
|
813
813
|
}
|
|
@@ -864,8 +864,8 @@ class Select extends Component<SelectProps> {
|
|
|
864
864
|
mountNode !== undefined
|
|
865
865
|
? mountNode
|
|
866
866
|
: utils.isAndroidOrIOS()
|
|
867
|
-
|
|
868
|
-
|
|
867
|
+
? this.ref
|
|
868
|
+
: undefined
|
|
869
869
|
}
|
|
870
870
|
positionTarget={this._inputContainer}
|
|
871
871
|
isShowingContent={isShowingOptions}
|