@khanacademy/wonder-blocks-dropdown 5.3.8 → 5.4.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.
@@ -1,7 +1,11 @@
1
1
  import * as React from "react";
2
2
  import * as ReactDOM from "react-dom";
3
3
 
4
- import type {AriaProps, StyleType} from "@khanacademy/wonder-blocks-core";
4
+ import {
5
+ IDProvider,
6
+ type AriaProps,
7
+ type StyleType,
8
+ } from "@khanacademy/wonder-blocks-core";
5
9
 
6
10
  import DropdownCore from "./dropdown-core";
7
11
  import DropdownOpener from "./dropdown-opener";
@@ -150,6 +154,13 @@ type Props = AriaProps &
150
154
  * top. The items will be filtered by the input.
151
155
  */
152
156
  isFilterable?: boolean;
157
+ /**
158
+ * Unique identifier attached to the listbox dropdown. If used, we need to
159
+ * guarantee that the ID is unique within everything rendered on a page.
160
+ * If one is not provided, one is auto-generated. It is used for the
161
+ * opener's `aria-controls` attribute for screenreaders.
162
+ */
163
+ dropdownId?: string;
153
164
  }>;
154
165
 
155
166
  type State = Readonly<{
@@ -366,6 +377,7 @@ export default class SingleSelect extends React.Component<Props, State> {
366
377
 
367
378
  renderOpener(
368
379
  isDisabled: boolean,
380
+ dropdownId: string,
369
381
  ):
370
382
  | React.ReactElement<React.ComponentProps<typeof DropdownOpener>>
371
383
  | React.ReactElement<React.ComponentProps<typeof SelectOpener>> {
@@ -409,32 +421,43 @@ export default class SingleSelect extends React.Component<Props, State> {
409
421
  ? getLabel(selectedItem.props)
410
422
  : placeholder;
411
423
 
412
- const dropdownOpener = opener ? (
413
- <DropdownOpener
414
- onClick={this.handleClick}
415
- disabled={isDisabled}
416
- ref={this.handleOpenerRef}
417
- text={menuText}
418
- opened={this.state.open}
419
- >
420
- {opener}
421
- </DropdownOpener>
422
- ) : (
423
- <SelectOpener
424
- {...sharedProps}
425
- disabled={isDisabled}
426
- id={id}
427
- error={error}
428
- isPlaceholder={!selectedItem}
429
- light={light}
430
- onOpenChanged={this.handleOpenChanged}
431
- open={this.state.open}
432
- ref={this.handleOpenerRef}
433
- testId={testId}
434
- >
435
- {menuText}
436
- </SelectOpener>
424
+ const dropdownOpener = (
425
+ <IDProvider id={id} scope="single-select-opener">
426
+ {(uniqueOpenerId) => {
427
+ return opener ? (
428
+ <DropdownOpener
429
+ id={uniqueOpenerId}
430
+ aria-controls={dropdownId}
431
+ aria-haspopup="listbox"
432
+ onClick={this.handleClick}
433
+ disabled={isDisabled}
434
+ ref={this.handleOpenerRef}
435
+ text={menuText}
436
+ opened={this.state.open}
437
+ >
438
+ {opener}
439
+ </DropdownOpener>
440
+ ) : (
441
+ <SelectOpener
442
+ {...sharedProps}
443
+ aria-controls={dropdownId}
444
+ disabled={isDisabled}
445
+ id={uniqueOpenerId}
446
+ error={error}
447
+ isPlaceholder={!selectedItem}
448
+ light={light}
449
+ onOpenChanged={this.handleOpenChanged}
450
+ open={this.state.open}
451
+ ref={this.handleOpenerRef}
452
+ testId={testId}
453
+ >
454
+ {menuText}
455
+ </SelectOpener>
456
+ );
457
+ }}
458
+ </IDProvider>
437
459
  );
460
+
438
461
  return dropdownOpener;
439
462
  }
440
463
 
@@ -453,6 +476,7 @@ export default class SingleSelect extends React.Component<Props, State> {
453
476
  "aria-invalid": ariaInvalid,
454
477
  "aria-required": ariaRequired,
455
478
  disabled,
479
+ dropdownId,
456
480
  } = this.props;
457
481
  const {searchText} = this.state;
458
482
  const allChildren = (
@@ -465,39 +489,45 @@ export default class SingleSelect extends React.Component<Props, State> {
465
489
  ).length;
466
490
  const items = this.getMenuItems(allChildren);
467
491
  const isDisabled = numEnabledOptions === 0 || disabled;
468
- const opener = this.renderOpener(isDisabled);
469
492
 
470
493
  return (
471
- <DropdownCore
472
- role="listbox"
473
- selectionType="single"
474
- alignment={alignment}
475
- autoFocus={autoFocus}
476
- enableTypeAhead={enableTypeAhead}
477
- dropdownStyle={[
478
- isFilterable && filterableDropdownStyle,
479
- selectDropdownStyle,
480
- dropdownStyle,
481
- ]}
482
- initialFocusedIndex={this.selectedIndex}
483
- items={items}
484
- light={light}
485
- onOpenChanged={this.handleOpenChanged}
486
- open={this.state.open}
487
- opener={opener}
488
- openerElement={this.state.openerElement}
489
- style={style}
490
- className={className}
491
- isFilterable={isFilterable}
492
- onSearchTextChanged={
493
- isFilterable ? this.handleSearchTextChanged : undefined
494
- }
495
- searchText={isFilterable ? searchText : ""}
496
- labels={labels}
497
- aria-invalid={ariaInvalid}
498
- aria-required={ariaRequired}
499
- disabled={isDisabled}
500
- />
494
+ <IDProvider id={dropdownId} scope="single-select-dropdown">
495
+ {(uniqueDropdownId) => (
496
+ <DropdownCore
497
+ id={uniqueDropdownId}
498
+ role="listbox"
499
+ selectionType="single"
500
+ alignment={alignment}
501
+ autoFocus={autoFocus}
502
+ enableTypeAhead={enableTypeAhead}
503
+ dropdownStyle={[
504
+ isFilterable && filterableDropdownStyle,
505
+ selectDropdownStyle,
506
+ dropdownStyle,
507
+ ]}
508
+ initialFocusedIndex={this.selectedIndex}
509
+ items={items}
510
+ light={light}
511
+ onOpenChanged={this.handleOpenChanged}
512
+ open={this.state.open}
513
+ opener={this.renderOpener(isDisabled, uniqueDropdownId)}
514
+ openerElement={this.state.openerElement}
515
+ style={style}
516
+ className={className}
517
+ isFilterable={isFilterable}
518
+ onSearchTextChanged={
519
+ isFilterable
520
+ ? this.handleSearchTextChanged
521
+ : undefined
522
+ }
523
+ searchText={isFilterable ? searchText : ""}
524
+ labels={labels}
525
+ aria-invalid={ariaInvalid}
526
+ aria-required={ariaRequired}
527
+ disabled={isDisabled}
528
+ />
529
+ )}
530
+ </IDProvider>
501
531
  );
502
532
  }
503
533
  }