Skip to content

Commit 043cb5a

Browse files
committed
fix further accessibility issues with inputs & dropdown
1 parent e9423f6 commit 043cb5a

File tree

15 files changed

+150
-69
lines changed

15 files changed

+150
-69
lines changed

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

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
cursor: pointer;
1010
font-size: inherit;
1111
overflow: hidden;
12+
accent-color: var(--Dash-Fill-Interactive-Strong);
13+
outline-color: var(--Dash-Fill-Interactive-Strong);
1214
}
1315

1416
.dash-dropdown-grid-container {
@@ -66,9 +68,9 @@
6668
background: var(--Dash-Fill-Inverse-Strong);
6769
width: fit-content;
6870
min-width: var(--radix-popover-trigger-width);
69-
max-width: 100vw;
71+
max-width: 98vw;
7072
overflow-y: auto;
71-
z-index: 50;
73+
z-index: 500;
7274
box-shadow: 0px 10px 38px -10px rgba(22, 23, 24, 0.35),
7375
0px 10px 20px -15px rgba(22, 23, 24, 0.2);
7476
}
@@ -179,6 +181,8 @@
179181
text-decoration: none;
180182
color: var(--Dash-Text-Disabled);
181183
white-space: nowrap;
184+
accent-color: var(--Dash-Fill-Interactive-Strong);
185+
outline-color: var(--Dash-Fill-Interactive-Strong);
182186
}
183187

184188
.dash-dropdown-action-button:hover {

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

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,12 @@
1616
}
1717

1818
.dash-input-container:focus-within {
19-
outline: 2px solid var(--Dash-Fill-Interactive-Strong);
19+
outline: 1px solid var(--Dash-Fill-Interactive-Strong);
20+
}
21+
22+
.dash-input-container:has(.dash-input-element:disabled) {
23+
opacity: 0.6;
24+
cursor: not-allowed;
2025
}
2126

2227
.dash-input-container input:focus {
@@ -40,6 +45,11 @@
4045
box-sizing: border-box;
4146
z-index: 1;
4247
order: 2;
48+
accent-color: var(--Dash-Fill-Interactive-Strong);
49+
}
50+
51+
.dash-input-element:disabled {
52+
cursor: not-allowed;
4353
}
4454

4555
/* Hide native steppers for number inputs */

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

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,6 @@
2929
margin: 0 calc(var(--Dash-Spacing) * 2) 0 0;
3030
box-sizing: border-box;
3131
border: 1px solid var(--Dash-Stroke-Strong);
32-
}
33-
34-
.dash-options-list-option-checkbox:hover,
35-
.dash-options-list-option-checkbox:focus,
36-
.dash-options-list-option-checkbox:checked {
3732
accent-color: var(--Dash-Fill-Interactive-Strong);
33+
outline-color: var(--Dash-Fill-Interactive-Strong);
3834
}

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

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import '../components/css/dropdown.css';
1919
import isEqual from 'react-fast-compare';
2020
import {DetailedOption, DropdownProps, OptionValue} from '../types';
2121
import {OptionsList, OptionLabel} from '../utils/optionRendering';
22+
import uuid from 'uniqid';
2223

2324
const Dropdown = (props: DropdownProps) => {
2425
const {
@@ -344,6 +345,8 @@ const Dropdown = (props: DropdownProps) => {
344345
[filteredOptions, sanitizedValues]
345346
);
346347

348+
const accessibleId = id ?? uuid();
349+
347350
return (
348351
<Popover.Root open={isOpen} onOpenChange={handleOpenChange}>
349352
<Popover.Trigger asChild>
@@ -359,24 +362,33 @@ const Dropdown = (props: DropdownProps) => {
359362
}}
360363
className={`dash-dropdown ${className ?? ''}`}
361364
style={style}
362-
aria-label={props.placeholder}
365+
aria-labelledby={`${accessibleId}-value-count ${accessibleId}-value`}
363366
aria-haspopup="listbox"
364367
aria-expanded={isOpen}
365368
data-dash-is-loading={loading || undefined}
366369
>
367370
<span className="dash-dropdown-grid-container dash-dropdown-trigger">
368371
{displayValue.length > 0 && (
369-
<span className="dash-dropdown-value">
372+
<span
373+
id={accessibleId + '-value'}
374+
className="dash-dropdown-value"
375+
>
370376
{displayValue}
371377
</span>
372378
)}
373379
{displayValue.length === 0 && (
374-
<span className="dash-dropdown-value dash-dropdown-placeholder">
380+
<span
381+
id={accessibleId + '-value'}
382+
className="dash-dropdown-value dash-dropdown-placeholder"
383+
>
375384
{props.placeholder}
376385
</span>
377386
)}
378387
{sanitizedValues.length > 1 && (
379-
<span className="dash-dropdown-value-count">
388+
<span
389+
id={accessibleId + '-value-count'}
390+
className="dash-dropdown-value-count"
391+
>
380392
{localizations?.selected_count?.replace(
381393
'{num_selected}',
382394
`${sanitizedValues.length}`
@@ -402,7 +414,7 @@ const Dropdown = (props: DropdownProps) => {
402414
</button>
403415
</Popover.Trigger>
404416

405-
<Popover.Portal container={dropdownContainerRef.current}>
417+
<Popover.Portal>
406418
<Popover.Content
407419
className="dash-dropdown-content"
408420
align="start"

components/dash-core-components/tests/integration/dropdown/test_a11y.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,10 @@ def test_a11y001_label_focuses_dropdown(dash_duo):
2727
dash_duo.wait_for_element("#dropdown")
2828

2929
with pytest.raises(TimeoutException):
30-
dash_duo.wait_for_element("#dropdown .dash-dropdown-options", timeout=0.25)
30+
dash_duo.wait_for_element(".dash-dropdown-options", timeout=0.25)
3131

3232
dash_duo.find_element("#label").click()
33-
dash_duo.wait_for_element("#dropdown .dash-dropdown-options")
33+
dash_duo.wait_for_element(".dash-dropdown-options")
3434

3535
assert dash_duo.get_logs() == []
3636

@@ -54,10 +54,10 @@ def test_a11y002_label_with_htmlFor_can_focus_dropdown(dash_duo):
5454
dash_duo.wait_for_element("#dropdown")
5555

5656
with pytest.raises(TimeoutException):
57-
dash_duo.wait_for_element("#dropdown .dash-dropdown-options", timeout=0.25)
57+
dash_duo.wait_for_element(".dash-dropdown-options", timeout=0.25)
5858

5959
dash_duo.find_element("#label").click()
60-
dash_duo.wait_for_element("#dropdown .dash-dropdown-options")
60+
dash_duo.wait_for_element(".dash-dropdown-options")
6161

6262
assert dash_duo.get_logs() == []
6363

@@ -84,16 +84,16 @@ def send_keys(key):
8484

8585
dropdown = dash_duo.find_element("#dropdown")
8686
dropdown.click()
87-
dash_duo.wait_for_element("#dropdown .dash-dropdown-options")
87+
dash_duo.wait_for_element(".dash-dropdown-options")
8888

8989
send_keys(
9090
Keys.ESCAPE
9191
) # Expecting focus to remain on the dropdown after escaping out
9292
with pytest.raises(TimeoutException):
93-
dash_duo.wait_for_element("#dropdown .dash-dropdown-options", timeout=0.25)
93+
dash_duo.wait_for_element(".dash-dropdown-options", timeout=0.25)
9494

9595
send_keys(Keys.ARROW_DOWN) # Expecting the dropdown to open up
96-
dash_duo.wait_for_element("#dropdown .dash-dropdown-search")
96+
dash_duo.wait_for_element(".dash-dropdown-search")
9797

9898
num_elements = len(dash_duo.find_elements(".dash-dropdown-option"))
9999
assert num_elements == 100

components/dash-core-components/tests/integration/dropdown/test_clearable_false.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ def update_value(val):
4444

4545
# Clicking the selected item should not de-select it.
4646
selected_item = dash_duo.find_element(
47-
f'#my-unclearable-dropdown input[value="{output_text}"]'
47+
f'.dash-dropdown-options input[value="{output_text}"]'
4848
)
4949
selected_item.click()
5050
assert dash_duo.find_element("#dropdown-value").text == output_text
@@ -93,7 +93,7 @@ def update_value(val):
9393

9494
# Attempt to deselect all items. Everything should deselect until we get to
9595
# the last item which cannot be cleared.
96-
selected = dash_duo.find_elements("#my-unclearable-dropdown input[checked]")
96+
selected = dash_duo.find_elements(".dash-dropdown-options input[checked]")
9797
[el.click() for el in selected]
9898

9999
assert dash_duo.find_element("#dropdown-value").text == "SF"

components/dash-core-components/tests/integration/dropdown/test_dynamic_options.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ def update_options(search_value):
3131
dropdown.click()
3232

3333
# Get the inner input used for search value.
34-
input_ = dash_dcc.find_element("#my-dynamic-dropdown input")
34+
input_ = dash_dcc.find_element(".dash-dropdown-content input")
3535

3636
# Focus on the input to open the options menu
3737
input_.send_keys("x")
@@ -43,7 +43,7 @@ def update_options(search_value):
4343
input_.send_keys("o")
4444

4545
time.sleep(0.25)
46-
options = dash_dcc.find_elements("#my-dynamic-dropdown .dash-dropdown-option")
46+
options = dash_dcc.find_elements(".dash-dropdown-options .dash-dropdown-option")
4747

4848
# Should show all options.
4949
assert len(options) == 3
@@ -52,7 +52,7 @@ def update_options(search_value):
5252
input_.send_keys("n")
5353

5454
time.sleep(0.25)
55-
options = dash_dcc.find_elements("#my-dynamic-dropdown .dash-dropdown-option")
55+
options = dash_dcc.find_elements(".dash-dropdown-options .dash-dropdown-option")
5656

5757
assert len(options) == 1
5858
assert options[0].text == "Montreal"

components/dash-core-components/tests/integration/dropdown/test_localization.py

Lines changed: 14 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -28,32 +28,26 @@ def test_ddlo001_translations(dash_duo):
2828

2929
dash_duo.find_element("#dropdown").click()
3030
dash_duo.wait_for_contains_text(
31-
"#dropdown .dash-dropdown-action-button:first-child", "Sélectionner tout"
31+
".dash-dropdown-action-button:first-child", "Sélectionner tout"
3232
)
3333
dash_duo.wait_for_contains_text(
34-
"#dropdown .dash-dropdown-action-button:last-child", "Désélectionner tout"
34+
".dash-dropdown-action-button:last-child", "Désélectionner tout"
3535
)
3636

37-
search_input = dash_duo.find_element("#dropdown .dash-dropdown-search")
37+
search_input = dash_duo.find_element(".dash-dropdown-search")
3838
assert search_input.accessible_name == "Rechercher"
3939

4040
search_input.send_keys(1)
41-
assert (
42-
dash_duo.find_element("#dropdown .dash-dropdown-clear").accessible_name
43-
== "Annuler"
44-
)
41+
assert dash_duo.find_element(".dash-dropdown-clear").accessible_name == "Annuler"
4542

46-
dash_duo.find_element("#dropdown .dash-dropdown-action-button:first-child").click()
43+
dash_duo.find_element(".dash-dropdown-action-button:first-child").click()
4744

4845
search_input.send_keys(9)
49-
assert (
50-
dash_duo.find_element("#dropdown .dash-dropdown-option").text
51-
== "Aucun d'options"
52-
)
46+
assert dash_duo.find_element(".dash-dropdown-option").text == "Aucun d'options"
5347

5448
assert (
5549
dash_duo.find_element(
56-
"#dropdown .dash-dropdown-trigger .dash-dropdown-clear"
50+
".dash-dropdown-trigger .dash-dropdown-clear"
5751
).accessible_name
5852
== "Effacer les sélections"
5953
)
@@ -80,32 +74,28 @@ def test_ddlo002_partial_translations(dash_duo):
8074

8175
dash_duo.find_element("#dropdown").click()
8276
dash_duo.wait_for_contains_text(
83-
"#dropdown .dash-dropdown-action-button:first-child", "Select All"
77+
".dash-dropdown-action-button:first-child", "Select All"
8478
)
8579
dash_duo.wait_for_contains_text(
86-
"#dropdown .dash-dropdown-action-button:last-child", "Deselect All"
80+
".dash-dropdown-action-button:last-child", "Deselect All"
8781
)
8882

89-
search_input = dash_duo.find_element("#dropdown .dash-dropdown-search")
83+
search_input = dash_duo.find_element(".dash-dropdown-search")
9084
assert search_input.accessible_name == "Lookup"
9185

9286
search_input.send_keys(1)
9387
assert (
94-
dash_duo.find_element("#dropdown .dash-dropdown-clear").accessible_name
95-
== "Clear search"
88+
dash_duo.find_element(".dash-dropdown-clear").accessible_name == "Clear search"
9689
)
9790

98-
dash_duo.find_element("#dropdown .dash-dropdown-action-button:first-child").click()
91+
dash_duo.find_element(".dash-dropdown-action-button:first-child").click()
9992

10093
search_input.send_keys(9)
101-
assert (
102-
dash_duo.find_element("#dropdown .dash-dropdown-option").text
103-
== "No options found"
104-
)
94+
assert dash_duo.find_element(".dash-dropdown-option").text == "No options found"
10595

10696
assert (
10797
dash_duo.find_element(
108-
"#dropdown .dash-dropdown-trigger .dash-dropdown-clear"
98+
".dash-dropdown-trigger .dash-dropdown-clear"
10999
).accessible_name
110100
== "Clear selection"
111101
)

components/dash-core-components/tests/integration/dropdown/test_remove_option.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -183,9 +183,9 @@ def on_value(value):
183183

184184
dropdown = dash_dcc.find_element("#drop")
185185
dropdown.click()
186-
select_input = dash_dcc.find_element("#drop .dash-dropdown-search")
186+
select_input = dash_dcc.find_element(".dash-dropdown-search")
187187
select_input.send_keys("a")
188-
dash_dcc.find_element("#drop .dash-dropdown-option").send_keys(Keys.SPACE)
188+
dash_dcc.find_element(".dash-dropdown-option").send_keys(Keys.SPACE)
189189

190190
dash_dcc.wait_for_text_to_equal("#output", "Value=a")
191191
dash_dcc.wait_for_text_to_equal("#count-output", "2")

components/dash-core-components/tests/integration/dropdown/test_styles.py

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@
22
from dash.dcc import Dropdown
33
from dash.html import Div
44
from dash.dash_table import DataTable
5-
65
from flaky import flaky
6+
from selenium.webdriver.common.action_chains import ActionChains
77

88

99
@flaky(max_runs=3)
@@ -40,10 +40,16 @@ def test_ddst001_cursor_should_be_pointer(dash_duo):
4040
dash_duo.start_server(app)
4141

4242
dash_duo.find_element("#dropdown").click()
43-
dash_duo.wait_for_element("#dropdown .dash-dropdown-options")
43+
dash_duo.wait_for_element(".dash-dropdown-options")
4444

45-
items = dash_duo.find_elements(
46-
"#dropdown .dash-dropdown-options .dash-dropdown-option"
47-
)
45+
items = dash_duo.find_elements(".dash-dropdown-options .dash-dropdown-option")
4846

4947
assert items[0].value_of_css_property("cursor") == "pointer"
48+
49+
# If the search element is visible, then we should be able to click on it.
50+
search_element = dash_duo.find_element(".dash-dropdown-search")
51+
actions = ActionChains(dash_duo.driver)
52+
actions.move_to_element_with_offset(search_element, 8, 8).click().perform()
53+
54+
# The dropdown should remain open after clicking into the search bar
55+
dash_duo.wait_for_element(".dash-dropdown-options")

0 commit comments

Comments
 (0)