Skip to content

Commit b463af8

Browse files
authored
Refactor dropdowns to follow WAI recommendations (#3287)
1 parent 8fd9844 commit b463af8

File tree

10 files changed

+235
-137
lines changed

10 files changed

+235
-137
lines changed

warehouse/static/js/warehouse/index.js

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,46 @@ docReady(() => {
166166
}
167167
});
168168

169+
var bindDropdowns = function () {
170+
// Bind click handlers to dropdowns for keyboard users
171+
let dropdowns = document.querySelectorAll(".dropdown");
172+
for (let dropdown of dropdowns) {
173+
let trigger = dropdown.querySelector(".dropdown__trigger");
174+
let content = dropdown.querySelector(".dropdown__content");
175+
176+
if (!trigger.dataset.dropdownBound) {
177+
// If the user has clicked the trigger (either with a mouse or by pressing
178+
// space/enter on the keyboard) show the content
179+
trigger.addEventListener("click", function () {
180+
// Toggle the visibility of the content
181+
if (content.classList.contains("display-block")) {
182+
content.classList.remove("display-block");
183+
} else {
184+
content.classList.add("display-block");
185+
}
186+
});
187+
188+
// If the user has moused onto the trigger and has happened to click it,
189+
// remove the `display-block` class so that it doesn't stay visable when
190+
// they mouse out
191+
trigger.addEventListener("mouseout", function() {
192+
content.classList.remove("display-block");
193+
});
194+
195+
// Set the 'data-dropdownBound' attribute so we don't bind multiple
196+
// handlers to the same trigger after the client-side-includes load
197+
trigger.dataset.dropdownBound = true;
198+
}
199+
}
200+
};
201+
202+
// Bind the dropdowns when the page is ready
203+
docReady(bindDropdowns);
204+
205+
// Bind again when client-side includes have been loaded (for the logged-in
206+
// user dropdown)
207+
document.addEventListener("CSILoaded", bindDropdowns);
208+
169209
const application = Application.start();
170210
const context = require.context("./controllers", true, /\.js$/);
171211
application.load(definitionsFromContext(context));

warehouse/static/js/warehouse/utils/html-include.js

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,15 @@ export default () => {
4040
promises.push(p);
4141
});
4242

43-
// Once all of our HTML includes have fired, then we'll go ahead and record
44-
// the fact that our HTML includes have happened. This allows us to
45-
// introspect the state of our includes inside of our Selenium tests.
46-
Promise.all(promises).then(() => { window._WarehouseHTMLIncluded = true; });
43+
Promise.all(promises).then(() => {
44+
45+
// Once all of our HTML includes have fired, then we'll go ahead and record
46+
// the fact that our HTML includes have happened. This allows us to
47+
// introspect the state of our includes inside of our Selenium tests.
48+
window._WarehouseHTMLIncluded = true;
49+
50+
// Dispatch an event to any listeners that our CSI includes have loaded
51+
var event = new Event("CSILoaded");
52+
document.dispatchEvent(event);
53+
});
4754
};

warehouse/static/sass/blocks/_dropdown.scss

Lines changed: 30 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,17 @@
3333
position: relative;
3434
display: inline-block;
3535

36+
ul,
37+
li {
38+
margin: 0;
39+
padding: 0;
40+
list-style-type: none;
41+
}
42+
43+
&:hover .dropdown__content {
44+
display: block;
45+
}
46+
3647
&__trigger {
3748
cursor: pointer;
3849
}
@@ -54,14 +65,6 @@
5465
display: none;
5566
}
5667

57-
// Show content if triggered
58-
&__trigger:hover + &__content,
59-
&__trigger:focus + &__content,
60-
&__trigger:active + &__content,
61-
&__content:hover {
62-
display: block;
63-
}
64-
6568
&__link,
6669
button.dropdown__link {
6770
display: block;
@@ -75,12 +78,17 @@
7578
text-align: left;
7679
position: relative;
7780

78-
&:hover {
79-
background-color: $base-grey;
80-
color: $text-color;
81+
&:hover,
82+
&:focus {
83+
background-color: mix($white, $primary-color-light, 90%);
84+
color: $primary-color-dark;
8185
text-decoration: none;
8286
}
8387

88+
&:focus {
89+
border-bottom-color: $primary-color-light;
90+
}
91+
8492
.fa {
8593
font-size: 14px;
8694
position: absolute;
@@ -109,10 +117,20 @@
109117
background-color: $header-background-color;
110118
color: $white;
111119

112-
&:hover {
120+
a {
121+
color: $white;
122+
text-decoration: none;
123+
}
124+
125+
&:hover,
126+
&:focus {
113127
background-color: darken($header-background-color, 1.5);
114128
color: $white;
115129
}
130+
131+
&:focus {
132+
border-bottom-color: $primary-color-light;
133+
}
116134
}
117135
}
118136

warehouse/static/sass/blocks/_vertical-tabs.scss

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,6 @@
3636

3737
.vertical-tabs {
3838
@include clearfix;
39-
overflow: hidden;
4039
margin: $spacing-unit 0;
4140

4241
@media only screen and (max-width: $tablet) {

warehouse/static/sass/warehouse.scss

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,10 @@
189189
}
190190
}
191191

192+
.display-block {
193+
display: block;
194+
}
195+
192196
// Mobile specific visibility
193197
@media screen and (max-width: $mobile) {
194198
.hide-on-mobile {

warehouse/templates/includes/current-user-indicator.html

Lines changed: 51 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -14,53 +14,67 @@
1414

1515
{% if request.user %}
1616
<div id="user-indicator" class="horizontal-menu horizontal-menu--light horizontal-menu--tall">
17-
<div class="dropdown dropdown--on-menu dropdown--with-icons">
18-
<button type="button" class="horizontal-menu__link dropdown__trigger">
17+
<nav aria-label="Main navigation" class="dropdown dropdown--on-menu dropdown--with-icons">
18+
<button class="horizontal-menu__link dropdown__trigger" aria-haspopup="true" aria-expanded="false">
1919
<span class="hide-on-mobile">Welcome back, </span>{{ request.user.username }}
2020
<span class="dropdown__trigger-caret">
2121
<i class="fa fa-caret-down" aria-hidden="true"></i>
2222
<span class="sr-only">view submenu</span>
2323
</span>
2424
</button>
25-
<div class="dropdown__content">
25+
<ul class="dropdown__content">
2626
{% if request.has_permission("admin") %}
27-
<a class="dropdown__link" href="{{ request.route_path('admin.dashboard') }}">
28-
<i class="fa fa-wrench" aria-hidden="true"></i>
29-
Admin
30-
</a>
27+
<li>
28+
<a href="{{ request.route_path('admin.dashboard') }}" class="dropdown__link">
29+
<i class="fa fa-wrench" aria-hidden="true"></i>
30+
Admin
31+
</a>
32+
</li>
3133
{% endif %}
32-
<a class="dropdown__link" href="{{ request.route_path('manage.projects') }}">
33-
<i class="fa fa-cube" aria-hidden="true"></i>
34-
Your projects
35-
</a>
36-
<a class="dropdown__link" href="{{ request.route_path('manage.account') }}">
37-
<i class="fa fa-cog" aria-hidden="true"></i>
38-
Account settings
39-
</a>
40-
<a class="dropdown__link" href="{{ request.route_path('accounts.profile', username=request.user.username) }}">
41-
<i class="fa fa-user-circle" aria-hidden="true"></i>
42-
Public profile
43-
</a>
44-
<a class="dropdown__link" href="{{ request.route_path('help') }}">
45-
<i class="fa fa-question-circle" aria-hidden="true"></i>
46-
Get help
47-
</a>
48-
<a class="dropdown__link" href="https://donate.pypi.org">
49-
<i class="fa fa-heart" aria-hidden="true"></i>
50-
Donate
51-
</a>
52-
<form method="POST" action="{{ request.route_path('accounts.logout') }}">
53-
<input name="csrf_token" type="hidden" value="{{ request.session.get_csrf_token() }}">
54-
<button type="submit" value="log out" class="dropdown__link">
55-
<i class="fa fa-sign-out" aria-hidden="true"></i>
56-
Log out
57-
</button>
58-
</form>
59-
</div>
60-
</div>
34+
<li>
35+
<a href="{{ request.route_path('manage.projects') }}" class="dropdown__link">
36+
<i class="fa fa-cube" aria-hidden="true"></i>
37+
Your Projects
38+
</a>
39+
</li>
40+
<li>
41+
<a href="{{ request.route_path('manage.account') }}" class="dropdown__link">
42+
<i class="fa fa-cog" aria-hidden="true"></i>
43+
Account Settings
44+
</a>
45+
</li>
46+
<li>
47+
<a href="{{ request.route_path('accounts.profile', username=request.user.username) }}" class="dropdown__link">
48+
<i class="fa fa-user-circle" aria-hidden="true"></i>
49+
Public Profile
50+
</a>
51+
</li>
52+
<li>
53+
<a href="{{ request.route_path('help') }}" class="dropdown__link">
54+
<i class="fa fa-question-circle" aria-hidden="true"></i>
55+
Get Help
56+
</a>
57+
</li>
58+
<li>
59+
<a href="https://donate.pypi.org" class="dropdown__link">
60+
<i class="fa fa-heart" aria-hidden="true"></i>
61+
Donate
62+
</a>
63+
</li>
64+
<li>
65+
<form method="POST" action="{{ request.route_path('accounts.logout') }}">
66+
<input name="csrf_token" type="hidden" value="{{ request.session.get_csrf_token() }}">
67+
<button type="submit" value="log out" class="dropdown__link">
68+
<i class="fa fa-sign-out" aria-hidden="true"></i>
69+
Log Out
70+
</button>
71+
</form>
72+
</li>
73+
</ul>
74+
</nav>
6175
</div>
6276
{% else %}
63-
<nav id="user-indicator" class="horizontal-menu horizontal-menu--light horizontal-menu--tall">
77+
<nav id="user-indicator" class="horizontal-menu horizontal-menu--light horizontal-menu--tall" aria-label="Main navigation">
6478
<a class="horizontal-menu__link horizontal-menu__link--remove-on-mobile" href="{{ request.route_path('help') }}">Help</a>
6579
<a class="horizontal-menu__link horizontal-menu__link--remove-on-mobile" href="https://donate.pypi.org">Donate</a>
6680
<a class="horizontal-menu__link" href="{{ request.route_path('accounts.login') }}">Log in</a>

warehouse/templates/includes/manage/manage-project-menu.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
# See the License for the specific language governing permissions and
1212
# limitations under the License.
1313
-#}
14-
<nav>
14+
<nav aria-label="Secondary navigation">
1515
<a href="{{ request.route_path('manage.project.releases', project_name=project.normalized_name)}}" class="vertical-tabs__tab vertical-tabs__tab--with-icon {% if active_tab == 'releases' %}vertical-tabs__tab--is-active{% endif %} {% if mode == 'mobile' %}vertical-tabs__tab--mobile{% endif %}">
1616
<i class="fa fa-cube" aria-hidden="true"></i>
1717
Releases

warehouse/templates/manage/account.html

Lines changed: 41 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -66,49 +66,53 @@
6666
</td>
6767
<td class="table__action">
6868
{% if not email.verified or user.emails|length > 1 and not email.primary%}
69-
<div class="dropdown dropdown--with-icons dropdown--wide">
70-
<button type="button" class="dropdown__trigger button button--primary">
69+
<nav class="dropdown dropdown--with-icons dropdown--wide">
70+
<button class="button button--primary dropdown__trigger" aria-haspopup="true" aria-expanded="false">
7171
Options
7272
<span class="dropdown__trigger-caret">
7373
<i class="fa fa-caret-down" aria-hidden="true"></i>
7474
<span class="sr-only">view submenu</span>
7575
</span>
7676
</button>
77-
<div class="dropdown__content">
78-
{% if not email.verified %}
79-
<form method="POST">
80-
<input hidden name="reverify_email_id" value="{{ email.id }}">
81-
<input name="csrf_token" type="hidden" value="{{ request.session.get_csrf_token() }}">
82-
<button class="dropdown__link" type="submit" title="Send verification email">
83-
<i class="fa fa-envelope" aria-hidden="true"></i>
84-
Send verification email
85-
</button>
86-
</form>
87-
{% endif %}
88-
89-
{% if not email.primary and email.verified %}
90-
<form method="POST">
91-
<input hidden name="primary_email_id" value="{{ email.id }}">
92-
<input name="csrf_token" type="hidden" value="{{ request.session.get_csrf_token() }}">
93-
<button class="dropdown__link" type="submit" title="Set this email address as primary">
94-
<i class="fa fa-cog" aria-hidden="true"></i>
95-
Set as primary
96-
</button>
97-
</form>
98-
{% endif %}
99-
100-
{% if user.emails|length > 1 and not email.primary %}
101-
<form method="POST">
102-
<input hidden name="delete_email_id" value="{{ email.id }}">
103-
<input name="csrf_token" type="hidden" value="{{ request.session.get_csrf_token() }}">
104-
<button class="dropdown__link" type="submit" title="Remove this email address">
105-
<i class="fa fa-trash" aria-hidden="true"></i>
106-
Remove email
107-
</button>
108-
</form>
109-
{% endif %}
110-
</div>
111-
</div>
77+
<ul class="dropdown__content">
78+
<li>
79+
{% if not email.verified %}
80+
<form method="POST">
81+
<input hidden name="reverify_email_id" value="{{ email.id }}">
82+
<input name="csrf_token" type="hidden" value="{{ request.session.get_csrf_token() }}">
83+
<button class="dropdown__link" type="submit" title="Resend verification email">
84+
<i class="fa fa-envelope" aria-hidden="true"></i>
85+
Resend Verification Email
86+
</button>
87+
</form>
88+
{% endif %}
89+
</li>
90+
<li>
91+
{% if not email.primary and email.verified %}
92+
<form method="POST">
93+
<input hidden name="primary_email_id" value="{{ email.id }}">
94+
<input name="csrf_token" type="hidden" value="{{ request.session.get_csrf_token() }}">
95+
<button class="dropdown__link" type="submit" title="Set this email address as primary">
96+
<i class="fa fa-cog" aria-hidden="true"></i>
97+
Set as Primary
98+
</button>
99+
</form>
100+
{% endif %}
101+
</li>
102+
<li>
103+
{% if user.emails|length > 1 and not email.primary %}
104+
<form method="POST">
105+
<input hidden name="delete_email_id" value="{{ email.id }}">
106+
<input name="csrf_token" type="hidden" value="{{ request.session.get_csrf_token() }}">
107+
<button class="dropdown__link" type="submit" title="Remove this email address">
108+
<i class="fa fa-trash" aria-hidden="true"></i>
109+
Remove Email
110+
</button>
111+
</form>
112+
{% endif %}
113+
</li>
114+
</ul>
115+
</nav>
112116
{% endif %}
113117
</td>
114118
</tr>

0 commit comments

Comments
 (0)