Skip to content

Commit bcf9b10

Browse files
committed
Fix accessibility issues in dropdown
1 parent 47ce558 commit bcf9b10

File tree

3 files changed

+250
-118
lines changed

3 files changed

+250
-118
lines changed

components/dash-core-components/src/components/css/dropdown.css

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,14 @@
11
.dash-dropdown {
2+
display: block;
23
box-sizing: border-box;
34
margin: calc(var(--Dash-Spacing) * 2) 0;
5+
padding: 0;
6+
background: inherit;
7+
border: none;
8+
width: 100%;
9+
cursor: pointer;
10+
font-size: inherit;
11+
overflow: hidden;
412
}
513

614
.dash-dropdown-grid-container {
@@ -19,7 +27,7 @@
1927
grid-template-columns: auto 1fr auto auto;
2028
}
2129

22-
.dash-dropdown-trigger,
30+
.dash-dropdown,
2331
.dash-dropdown-content {
2432
border-radius: var(--Dash-Spacing);
2533
border: 1px solid var(--Dash-Stroke-Strong);
@@ -28,17 +36,12 @@
2836
}
2937

3038
.dash-dropdown-trigger {
31-
background: inherit;
32-
padding: 6px 12px;
33-
width: 100%;
39+
padding: 0 12px;
3440
min-height: 32px;
3541
height: 100%;
36-
cursor: pointer;
37-
font-size: inherit;
38-
overflow: hidden;
3942
}
4043

41-
.dash-dropdown-trigger:disabled {
44+
.dash-dropdown:disabled {
4245
opacity: 0.6;
4346
cursor: not-allowed;
4447
}

components/dash-core-components/src/fragments/Dropdown.tsx

Lines changed: 111 additions & 110 deletions
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ const Dropdown = (props: DropdownProps) => {
105105
DetailedDropdownOption[]
106106
>([]);
107107
const persistentOptions = useRef<DropdownProps['options']>([]);
108-
const dropdownContainerRef = useRef<HTMLDivElement>(null);
108+
const dropdownContainerRef = useRef<HTMLButtonElement>(null);
109109

110110
const ctx = window.dash_component_api.useDashContext();
111111
const loading = ctx.useLoading();
@@ -428,26 +428,26 @@ const Dropdown = (props: DropdownProps) => {
428428
);
429429

430430
return (
431-
<div
432-
id={id}
433-
ref={dropdownContainerRef}
434-
className={`dash-dropdown ${className ?? ''}`}
435-
style={style}
436-
data-dash-is-loading={loading || undefined}
437-
>
438-
<Popover.Root open={isOpen} onOpenChange={handleOpenChange}>
439-
<Popover.Trigger asChild>
440-
<button
441-
className="dash-dropdown-grid-container dash-dropdown-trigger"
442-
aria-label={props.placeholder}
443-
disabled={disabled}
444-
type="button"
445-
onKeyDown={e => {
446-
if (e.key === 'ArrowDown') {
447-
setIsOpen(true);
448-
}
449-
}}
450-
>
431+
<Popover.Root open={isOpen} onOpenChange={handleOpenChange}>
432+
<Popover.Trigger asChild>
433+
<button
434+
id={id}
435+
ref={dropdownContainerRef}
436+
disabled={disabled}
437+
type="button"
438+
onKeyDown={e => {
439+
if (e.key === 'ArrowDown') {
440+
setIsOpen(true);
441+
}
442+
}}
443+
className={`dash-dropdown ${className ?? ''}`}
444+
style={style}
445+
aria-label={props.placeholder}
446+
aria-haspopup="listbox"
447+
aria-expanded={isOpen}
448+
data-dash-is-loading={loading || undefined}
449+
>
450+
<span className="dash-dropdown-grid-container dash-dropdown-trigger">
451451
{displayValue.length > 0 && (
452452
<span className="dash-dropdown-value">
453453
{displayValue}
@@ -481,100 +481,101 @@ const Dropdown = (props: DropdownProps) => {
481481
)}
482482

483483
<CaretDownIcon className="dash-dropdown-trigger-icon" />
484-
</button>
485-
</Popover.Trigger>
486-
487-
<Popover.Portal container={dropdownContainerRef.current}>
488-
<Popover.Content
489-
className="dash-dropdown-content"
490-
align="start"
491-
sideOffset={5}
492-
onOpenAutoFocus={e => e.preventDefault()}
493-
onKeyDown={handleKeyDown}
494-
style={{
495-
maxHeight: maxHeight ? `${maxHeight}px` : 'auto',
496-
}}
497-
>
498-
{searchable && (
499-
<div className="dash-dropdown-grid-container dash-dropdown-search-container">
500-
<MagnifyingGlassIcon className="dash-dropdown-search-icon" />
501-
<input
502-
type="search"
503-
className="dash-dropdown-search"
504-
placeholder={localizations?.search}
505-
value={search_value || ''}
506-
autoComplete="off"
507-
onChange={e =>
508-
onInputChange(e.target.value)
509-
}
510-
autoFocus
511-
/>
512-
{search_value && (
513-
<button
514-
type="button"
515-
className="dash-dropdown-clear"
516-
onClick={handleClearSearch}
517-
aria-label={localizations?.clear_search}
518-
>
519-
<Cross1Icon />
520-
</button>
521-
)}
522-
</div>
523-
)}
524-
{multi && (
525-
<div className="dash-dropdown-actions">
484+
</span>
485+
</button>
486+
</Popover.Trigger>
487+
488+
<Popover.Portal container={dropdownContainerRef.current}>
489+
<Popover.Content
490+
className="dash-dropdown-content"
491+
align="start"
492+
sideOffset={5}
493+
onOpenAutoFocus={e => e.preventDefault()}
494+
onKeyDown={handleKeyDown}
495+
style={{
496+
maxHeight: maxHeight ? `${maxHeight}px` : 'auto',
497+
}}
498+
>
499+
{searchable && (
500+
<div className="dash-dropdown-grid-container dash-dropdown-search-container">
501+
<MagnifyingGlassIcon className="dash-dropdown-search-icon" />
502+
<input
503+
type="search"
504+
className="dash-dropdown-search"
505+
placeholder={localizations?.search}
506+
value={search_value || ''}
507+
autoComplete="off"
508+
onChange={e => onInputChange(e.target.value)}
509+
autoFocus
510+
/>
511+
{search_value && (
512+
<button
513+
type="button"
514+
className="dash-dropdown-clear"
515+
onClick={handleClearSearch}
516+
aria-label={localizations?.clear_search}
517+
>
518+
<Cross1Icon />
519+
</button>
520+
)}
521+
</div>
522+
)}
523+
{multi && (
524+
<div className="dash-dropdown-actions">
525+
<button
526+
type="button"
527+
className="dash-dropdown-action-button"
528+
onClick={handleSelectAll}
529+
>
530+
{localizations?.select_all}
531+
</button>
532+
{canDeselectAll && (
526533
<button
527534
type="button"
528535
className="dash-dropdown-action-button"
529-
onClick={handleSelectAll}
536+
onClick={handleDeselectAll}
530537
>
531-
{localizations?.select_all}
538+
{localizations?.deselect_all}
532539
</button>
533-
{canDeselectAll && (
534-
<button
535-
type="button"
536-
className="dash-dropdown-action-button"
537-
onClick={handleDeselectAll}
538-
>
539-
{localizations?.deselect_all}
540-
</button>
541-
)}
542-
</div>
543-
)}
544-
{isOpen && (
545-
<div className="dash-dropdown-options">
546-
{displayOptions.map((option, i) => {
547-
const isSelected = multi
548-
? sanitizedValues.includes(option.value)
549-
: value === option.value;
550-
551-
return (
552-
<DropdownOption
553-
key={`${option.value}-${i}`}
554-
index={i}
555-
option={option}
556-
isSelected={isSelected}
557-
onClick={handleOptionClick}
558-
style={{
559-
height: optionHeight
560-
? `${optionHeight}px`
561-
: undefined,
562-
}}
563-
/>
564-
);
565-
})}
566-
{search_value &&
567-
displayOptions.length === 0 && (
568-
<span className="dash-dropdown-option">
569-
{localizations?.no_options_found}
570-
</span>
571-
)}
572-
</div>
573-
)}
574-
</Popover.Content>
575-
</Popover.Portal>
576-
</Popover.Root>
577-
</div>
540+
)}
541+
</div>
542+
)}
543+
{isOpen && (
544+
<div
545+
className="dash-dropdown-options"
546+
role="listbox"
547+
aria-multiselectable={multi}
548+
>
549+
{displayOptions.map((option, i) => {
550+
const isSelected = multi
551+
? sanitizedValues.includes(option.value)
552+
: value === option.value;
553+
554+
return (
555+
<DropdownOption
556+
key={`${option.value}-${i}`}
557+
index={i}
558+
option={option}
559+
isSelected={isSelected}
560+
onClick={handleOptionClick}
561+
style={{
562+
height: optionHeight
563+
? `${optionHeight}px`
564+
: undefined,
565+
}}
566+
/>
567+
);
568+
})}
569+
{search_value && displayOptions.length === 0 && (
570+
<span className="dash-dropdown-option">
571+
{localizations?.no_options_found}
572+
</span>
573+
)}
574+
</div>
575+
)}
576+
</Popover.Content>
577+
</Popover.Portal>
578+
</Popover.Root>
578579
);
579580
};
580581

0 commit comments

Comments
 (0)