@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 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-113](https://github.com/instructure/instructure-ui/compare/v11.7.3...v11.7.4-snapshot-113) (2026-07-20)
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-113",
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/emotion": "11.7.4-snapshot-113",
19
- "@instructure/shared-types": "11.7.4-snapshot-113",
20
- "@instructure/ui-dom-utils": "11.7.4-snapshot-113",
21
- "@instructure/ui-icons": "11.7.4-snapshot-113",
22
- "@instructure/ui-form-field": "11.7.4-snapshot-113",
23
- "@instructure/ui-options": "11.7.4-snapshot-113",
24
- "@instructure/ui-popover": "11.7.4-snapshot-113",
25
- "@instructure/ui-position": "11.7.4-snapshot-113",
26
- "@instructure/ui-react-utils": "11.7.4-snapshot-113",
27
- "@instructure/ui-text-input": "11.7.4-snapshot-113",
28
- "@instructure/ui-selectable": "11.7.4-snapshot-113",
29
- "@instructure/ui-themes": "11.7.4-snapshot-113",
30
- "@instructure/ui-view": "11.7.4-snapshot-113",
31
- "@instructure/ui-utils": "11.7.4-snapshot-113",
32
- "@instructure/uid": "11.7.4-snapshot-113"
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-113",
40
- "@instructure/ui-color-utils": "11.7.4-snapshot-113",
41
- "@instructure/ui-scripts": "11.7.4-snapshot-113"
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"
@@ -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
- assistiveText="Type or use arrow keys to navigate options. Multiple selections allowed."
519
+ aria-label="Multiple Select"
520
+ assistiveText={assistiveText}
505
521
  inputValue={inputValue}
506
522
  isShowingOptions={isShowingOptions}
507
523
  inputRef={(el) => {
@@ -111,18 +111,18 @@ const MemoedOption = memo(
111
111
  (prevProps, nextProps) => {
112
112
  return (
113
113
  prevProps.selectOption.props.isHighlighted ===
114
- nextProps.selectOption.props.isHighlighted &&
114
+ nextProps.selectOption.props.isHighlighted &&
115
115
  prevProps.selectOption.props.isSelected ===
116
- nextProps.selectOption.props.isSelected &&
116
+ nextProps.selectOption.props.isSelected &&
117
117
  prevProps.selectOption.props.isDisabled ===
118
- nextProps.selectOption.props.isDisabled &&
118
+ nextProps.selectOption.props.isDisabled &&
119
119
  prevProps.selectOption.props.children ===
120
- nextProps.selectOption.props.children &&
120
+ nextProps.selectOption.props.children &&
121
121
  prevProps.selectOption.props.id === nextProps.selectOption.props.id &&
122
122
  prevProps.selectOption.props.renderBeforeLabel ===
123
- nextProps.selectOption.props.renderBeforeLabel &&
123
+ nextProps.selectOption.props.renderBeforeLabel &&
124
124
  prevProps.selectOption.props.renderAfterLabel ===
125
- nextProps.selectOption.props.renderAfterLabel &&
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
- onRequestShowOptions: (event) => {
338
- onRequestShowOptions?.(event)
339
- const selectedOptionId = this.selectedOptionId
337
+ onRequestShowOptions: (event) => {
338
+ onRequestShowOptions?.(event)
339
+ const selectedOptionId = this.selectedOptionId
340
340
 
341
- if (selectedOptionId && !Array.isArray(selectedOptionId)) {
342
- // highlight selected option on show
343
- this.highlightOption(event, selectedOptionId)
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
- 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(
345
+ },
346
+ onRequestHideOptions: (event) => {
347
+ onRequestHideOptions?.(event)
348
+ },
349
+ onRequestHighlightOption: (
379
350
  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 })
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
- id,
417
- isDisabled,
418
- isSelected,
419
- isHighlighted,
420
- children
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
- 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
- }
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
- 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
- })
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
- ? option.props.renderAfterLabel
619
- : this.renderIcon()
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
- ? child.props.renderAfterLabel
629
- : this.renderIcon()
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
- // 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() ||
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
- ? 'button'
738
- : 'combobox',
739
- title: inputValue,
740
- 'aria-autocomplete': undefined,
741
- 'aria-readonly': true
742
- }
737
+ ? 'button'
738
+ : 'combobox',
739
+ title: inputValue,
740
+ 'aria-autocomplete': undefined,
741
+ 'aria-readonly': true
742
+ }
743
743
  : interaction === 'disabled' && utils.isChromium()
744
- ? { role: 'button' }
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
- : undefined,
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
- themeOverride: (componentTheme) => ({
808
- backgroundReadonlyColor: componentTheme.backgroundColor
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
- ? this.ref
868
- : undefined
867
+ ? this.ref
868
+ : undefined
869
869
  }
870
870
  positionTarget={this._inputContainer}
871
871
  isShowingContent={isShowingOptions}