Merge lp:~sinzui/launchpad/bugs-subscriptons-wcag-love into lp:launchpad

Proposed by Curtis Hovey on 2012-12-13
Status: Merged
Approved by: Curtis Hovey on 2012-12-14
Approved revision: no longer in the source branch.
Merged at revision: 16373
Proposed branch: lp:~sinzui/launchpad/bugs-subscriptons-wcag-love
Merge into: lp:launchpad
Diff against target: 299 lines (+113/-19)
9 files modified
lib/lp/app/javascript/confirmationoverlay/assets/confirmationoverlay-core.css (+0/-4)
lib/lp/app/javascript/formoverlay/assets/formoverlay-core.css (+0/-4)
lib/lp/app/javascript/formoverlay/formoverlay.js (+29/-4)
lib/lp/app/javascript/formoverlay/tests/test_formoverlay.js (+33/-0)
lib/lp/app/javascript/overlay/overlay.js (+6/-3)
lib/lp/app/javascript/overlay/tests/test_overlay.js (+3/-0)
lib/lp/registry/javascript/structural-subscription.js (+4/-1)
lib/lp/registry/javascript/tests/test_structural_subscription.html (+2/-0)
lib/lp/registry/javascript/tests/test_structural_subscription.js (+36/-3)
To merge this branch: bzr merge lp:~sinzui/launchpad/bugs-subscriptons-wcag-love
Reviewer Review Type Date Requested Status
j.c.sackett (community) 2012-12-13 Approve on 2012-12-14
Review via email: mp+139812@code.launchpad.net

Commit Message

Fix the bugs subscription overlay's keyboard navigation.

Description of the Change

The javascript controls for bug subscription are hard to use with screen
readers. The overlay does not take focus, the user can tab out of the
overlay, the accordion widget does not expand/contract as expected.

RULES

    Pre-implementation: wgrant, stevenk
    * Make the bug subscription widget uses the features provided by
      its base classes.
      * The first element is not being focused.
      * The user can tab out of the overlay because the list of tabable
        elements disagrees with what the browser supports...it's my
        arch nemesis visibility=hidden.
      * The problem is compounded by the overflow=hidden rules for
        the accordion. The item is not visible to the sighted user,
        but it is officially visible and was given space.

QA

    * Visit https://qastaging.launchpad.net/pocket-lint/trunk
    * View the Create milestone form and immediately press esc.
    * Verify the overlay closes, maybe you saw the close button in the
      upper right.
    * View the Create milestone form and press tab.
    * Verify the name field is focused.
    * View the Subscribe to bug mail link
    * press tab and verify the focus moves to the team select, but the
      team radio is NOT selected.
    * Tab a dozen times and confirm that the tabs cycle though the widget.
    * Tab to "are added or changed in any way", press enter, then
      tab a dozen times to cycle though the links again.
    * Tab to "Bugs must match this filter" and press enter.
    * Tab many times and verify that you tab into unseen links :(,
      but you can persevere and tab to the Create button.

LINT

    lib/lp/app/javascript/confirmationoverlay/assets/confirmationoverlay-core.css
    lib/lp/app/javascript/formoverlay/formoverlay.js
    lib/lp/app/javascript/formoverlay/assets/formoverlay-core.css
    lib/lp/app/javascript/formoverlay/tests/test_formoverlay.js
    lib/lp/app/javascript/overlay/overlay.js
    lib/lp/app/javascript/overlay/tests/test_overlay.js
    lib/lp/registry/javascript/structural-subscription.js
    lib/lp/registry/javascript/tests/test_structural_subscription.html
    lib/lp/registry/javascript/tests/test_structural_subscription.js

LoC

    I have more than 10,000 lines of credit this week.

TEST

    ./bin/test -vvc --layer=YUITest -t subscription -t overlay lp

IMPLEMENTATION

I updated the overlay's tabbing rules to also exclude items that the
browser computes to be hidden. This fixed most of the tab problems within
the overlay if a field ever got focus. When the accordion is visible,
it is still possible to tab into a hidden link/field :(. Blind users are
fine, users with pointers are fine, users not filtering are fine, sighted
keyboard users doing filtering will be annoyed if they try to skip a
section. At least they can use the keyboard.
    lib/lp/app/javascript/overlay/overlay.js
    lib/lp/app/javascript/overlay/tests/test_overlay.js

OMG. The code tries to focus non-visible inputs like <input type=hidden
/>. Even when the proper elements were focuses. There was a corner-case
though in the formoverlay/overlay widget. It is possible to show the
overlay, but there is no elements that can be focused, so I removed the
default rule that hide the close button. The close button is needed is
made the focus in most overlay cases, but a few overlays hid it -- which
has been noted as a inconsistency in the past. The CSS fix ensures all
overlays will get focus and can be closed with esc before any content it
loaded.
    lib/lp/app/javascript/formoverlay/formoverlay.js
    lib/lp/app/javascript/formoverlay/tests/test_formoverlay.js
    lib/lp/app/javascript/formoverlay/assets/formoverlay-core.css
    lib/lp/app/javascript/confirmationoverlay/assets/confirmationoverlay-core.css

The SS widget was hacking content into the overlay. The SS code replaces
the supposed download form with its own. The simple fix was to require
the focus rules which I extracted to a method in the formoverlay. There
was a nasty issue where the user tabs from the personal email radio button
to the team select box, which inadvertently then set the user's choice
to team email. The documentation said the code was doing the radio change
on the select box change, so it updated the event to work with onchange
per the docs.
    lib/lp/registry/javascript/structural-subscription.js
    lib/lp/registry/javascript/tests/test_structural_subscription.html
    lib/lp/registry/javascript/tests/test_structural_subscription.js

To post a comment you must log in.
j.c.sackett (jcsackett) wrote :

This looks good, Curtis. Thanks.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/lp/app/javascript/confirmationoverlay/assets/confirmationoverlay-core.css'
2--- lib/lp/app/javascript/confirmationoverlay/assets/confirmationoverlay-core.css 2011-08-30 15:15:11 +0000
3+++ lib/lp/app/javascript/confirmationoverlay/assets/confirmationoverlay-core.css 2012-12-14 15:21:27 +0000
4@@ -32,7 +32,3 @@
5 .yui3-lp-app-confirmationoverlay-form-header {
6 margin-top: 1em;
7 }
8-
9-.yui3-lp-app-confirmationoverlay a.close-button {
10- visibility: hidden;
11- }
12
13=== modified file 'lib/lp/app/javascript/formoverlay/assets/formoverlay-core.css'
14--- lib/lp/app/javascript/formoverlay/assets/formoverlay-core.css 2012-07-31 00:01:15 +0000
15+++ lib/lp/app/javascript/formoverlay/assets/formoverlay-core.css 2012-12-14 15:21:27 +0000
16@@ -64,7 +64,3 @@
17 .yui3-lazr-formoverlay-form-header {
18 margin-top: 1em;
19 }
20-
21-.yui3-lazr-formoverlay a.close-button {
22- visibility: hidden;
23- }
24
25=== modified file 'lib/lp/app/javascript/formoverlay/formoverlay.js'
26--- lib/lp/app/javascript/formoverlay/formoverlay.js 2012-09-11 14:30:45 +0000
27+++ lib/lp/app/javascript/formoverlay/formoverlay.js 2012-12-14 15:21:27 +0000
28@@ -313,10 +313,7 @@
29 if (this.get('centered')){
30 this.centered();
31 }
32- var form_elem = Y.Node.getDOMNode(this.form_node);
33- if (form_elem.elements.length > 0) {
34- Y.one(form_elem.elements[0]).focus();
35- }
36+ this._focusChild();
37 }
38 });
39 },
40@@ -382,6 +379,34 @@
41 Y.WidgetStdMod.BODY, body_node, Y.WidgetStdMod.REPLACE);
42 },
43
44+
45+ /**
46+ * Focus the first form element or the close button if the form
47+ * is not yet loaded or was removed.
48+ *
49+ * @method _focusChild
50+ * @private
51+ */
52+ _focusChild: function() {
53+ var all_inputs = this.form_node.all('input,select,textarea,button');
54+ var sane_inputs = [];
55+ all_inputs.each(function(item, index, node_list) {
56+ var displayed = item.get('region').height > 0;
57+ var visible = item.getComputedStyle('visibility') === 'visible';
58+ var not_input_hidden = !(
59+ item.get('tagName') === 'INPUT' &&
60+ item.get('type') === 'hidden');
61+ if (displayed && visible && not_input_hidden) {
62+ sane_inputs.push(item);
63+ }
64+ });
65+ if (sane_inputs.length > 0) {
66+ sane_inputs[0].focus();
67+ } else {
68+ this.get('boundingBox').one('.close-button').focus();
69+ }
70+ },
71+
72 /**
73 * Extract the form data and pass it to the user-provided submit
74 * callback.
75
76=== modified file 'lib/lp/app/javascript/formoverlay/tests/test_formoverlay.js'
77--- lib/lp/app/javascript/formoverlay/tests/test_formoverlay.js 2012-10-26 10:00:20 +0000
78+++ lib/lp/app/javascript/formoverlay/tests/test_formoverlay.js 2012-12-14 15:21:27 +0000
79@@ -123,6 +123,39 @@
80 "when the overlay is shown.");
81 },
82
83+ test_close_button_has_focus: function() {
84+ // The close button has focus if the form is not loaded,
85+ // or the form is removed.
86+ this.form_overlay.form_node.empty();
87+ var close_button = this.form_overlay.get(
88+ 'boundingBox').one('.close-button');
89+ var focused = false;
90+ close_button.on('focus', function(e) {
91+ focused = true;
92+ });
93+ this.form_overlay.hide();
94+ close_button.blur();
95+ this.form_overlay.show();
96+ Assert.isTrue(focused, "The close-button was not focused.");
97+ },
98+ test_focusChild_form_with_hidden_inputs: function() {
99+ // When focusing a form element, the method skips non-visible
100+ // form elements
101+ this.form_overlay.form_node.empty();
102+ this.form_overlay.form_node.append(
103+ '<input type="hidden" name="a" />' +
104+ '<input type="text" name="b" style="display:none" />' +
105+ '<input type="text" name="c" id="c" />'
106+ );
107+ var visible_input = this.form_overlay.get('boundingBox').one('#c');
108+ var focused = false;
109+ visible_input.on('focus', function(e) {
110+ focused = true;
111+ });
112+ this.form_overlay._focusChild();
113+ Assert.isTrue(focused, "The visble input was not focused.");
114+ },
115+
116 test_form_submit_in_body_content: function() {
117 // The body content should include the submit button.
118 var body_content = this.form_overlay.getStdModNode("body");
119
120=== modified file 'lib/lp/app/javascript/overlay/overlay.js'
121--- lib/lp/app/javascript/overlay/overlay.js 2012-09-11 14:30:45 +0000
122+++ lib/lp/app/javascript/overlay/overlay.js 2012-12-14 15:21:27 +0000
123@@ -318,9 +318,12 @@
124 var all_tab_nodes = bounding_box.all(TABBABLE_SELECTOR);
125 var tab_nodes = new Y.NodeList([]);
126 all_tab_nodes.each(function(item, index, node_list) {
127- if (item.get('region').height > 0) {
128- // The node takes up space on the page.
129- // This rule will skip empty links which are invalid.
130+ var displayed = item.get('region').height > 0;
131+ var visible = item.getComputedStyle(
132+ 'visibility') === 'visible';
133+ if (displayed && visible) {
134+ // The node takes up space and can be seen on the page.
135+ // This rule will skip empty links that cannot be focused.
136 tab_nodes.push(item);
137 }
138 });
139
140=== modified file 'lib/lp/app/javascript/overlay/tests/test_overlay.js'
141--- lib/lp/app/javascript/overlay/tests/test_overlay.js 2012-10-26 10:00:20 +0000
142+++ lib/lp/app/javascript/overlay/tests/test_overlay.js 2012-12-14 15:21:27 +0000
143@@ -261,6 +261,9 @@
144 '<input type="text" value="2"/>',
145 '<a style="display:none;">ignored</a>',
146 '<span style="display:none;"><button>nil</button></span>',
147+ '<span style="visibility:hidden;">',
148+ ' <button>nil</button>',
149+ '</span>',
150 '</div>'].join(' ')
151 });
152 this.overlay.render();
153
154=== modified file 'lib/lp/registry/javascript/structural-subscription.js'
155--- lib/lp/registry/javascript/structural-subscription.js 2012-09-10 20:52:27 +0000
156+++ lib/lp/registry/javascript/structural-subscription.js 2012-12-14 15:21:27 +0000
157@@ -416,6 +416,9 @@
158 'click', clean_up);
159 namespace._add_subscription_overlay.on('submit', clean_up);
160 namespace._add_subscription_overlay.on('cancel', clean_up);
161+ Y.after(
162+ namespace._add_subscription_overlay._focusChild,
163+ namespace._add_subscription_overlay, 'show');
164 }
165
166
167@@ -787,7 +790,7 @@
168 .set('value', teams[i].link));
169 }
170 select.on(
171- 'focus',
172+ 'change',
173 function () {
174 Y.one('input[value="team"][name="recipient"]').set(
175 'checked', true);
176
177=== modified file 'lib/lp/registry/javascript/tests/test_structural_subscription.html'
178--- lib/lp/registry/javascript/tests/test_structural_subscription.html 2012-10-26 09:54:28 +0000
179+++ lib/lp/registry/javascript/tests/test_structural_subscription.html 2012-12-14 15:21:27 +0000
180@@ -13,6 +13,8 @@
181 src="../../../../../build/js/yui/yui/yui.js">
182 </script>
183 <link rel="stylesheet"
184+ href="../../../app/javascript/formoverlay/assets/formoverlay-core.css" />
185+ <link rel="stylesheet"
186 href="../../../../../build/js/yui/console/assets/console-core.css" />
187 <link rel="stylesheet"
188 href="../../../../../build/js/yui/test-console/assets/skins/sam/test-console.css" />
189
190=== modified file 'lib/lp/registry/javascript/tests/test_structural_subscription.js'
191--- lib/lp/registry/javascript/tests/test_structural_subscription.js 2012-10-26 10:00:20 +0000
192+++ lib/lp/registry/javascript/tests/test_structural_subscription.js 2012-12-14 15:21:27 +0000
193@@ -475,6 +475,22 @@
194 Assert.isTrue(called);
195 },
196
197+ test_focused_form_child: function() {
198+ // When the overlay is shown, the first form child is focused.
199+ module.setup(this.configuration);
200+ module._show_add_overlay(this.configuration);
201+ var first_input = Y.Node.create('<input type="text" name="t" />');
202+ var focused = false;
203+ first_input.on('focus', function(e) {
204+ focused = true;
205+ });
206+ module._add_subscription_overlay.form_node.insert(first_input, 0);
207+ module._add_subscription_overlay.hide();
208+ first_input.blur();
209+ module._add_subscription_overlay.show();
210+ Assert.isTrue(focused, "The first input was not focused.");
211+ },
212+
213 test_setup_overlay_missing_content_box: function() {
214 // Pass in a content_box with a missing id to trigger an error.
215 this.configuration.content_box =
216@@ -494,6 +510,10 @@
217 var accordion_wrapper = Y.one('#accordion-wrapper');
218 Assert.isTrue(filter_wrapper.hasClass('lazr-closed'));
219 Assert.isTrue(accordion_wrapper.hasClass('lazr-closed'));
220+ var close_link = Y.one('a.close-button');
221+ Y.Assert.isTrue(close_link.get('region').height > 0);
222+ Y.Assert.isTrue(
223+ close_link.getComputedStyle('visibility') === 'visible');
224 },
225
226 test_added_or_changed_toggles: function() {
227@@ -734,6 +754,7 @@
228 tearDown: function() {
229 window.LP = this.original_lp;
230 remove_test_node();
231+ Y.one('#request-notifications').empty();
232 delete this.content_node;
233 },
234
235@@ -1142,6 +1163,7 @@
236 tearDown: function() {
237 window.LP = this.original_lp;
238 remove_test_node();
239+ Y.one('#request-notifications').empty();
240 delete this.content_node;
241 },
242
243@@ -1207,6 +1229,13 @@
244 };
245 },
246
247+ tearDown: function() {
248+ window.LP = this.original_lp;
249+ remove_test_node();
250+ Y.one('#request-notifications').empty();
251+ delete this.content_node;
252+ },
253+
254 test_description_is_added: function() {
255 // If we add a subscription on a page that doesn't display
256 // subcription details then we need to add an "informational
257@@ -1294,6 +1323,7 @@
258 tearDown: function() {
259 window.LP = this.original_lp;
260 remove_test_node();
261+ Y.one('#request-notifications').empty();
262 delete this.content_node;
263 },
264
265@@ -1396,6 +1426,7 @@
266 tearDown: function() {
267 window.LP = this.original_lp;
268 remove_test_node();
269+ Y.one('#request-notifications').empty();
270 delete this.content_node;
271 },
272
273@@ -1452,6 +1483,7 @@
274 tearDown: function() {
275 window.LP = this.original_lp;
276 remove_test_node();
277+ Y.one('#request-notifications').empty();
278 delete this.content_box;
279 },
280
281@@ -1819,6 +1851,7 @@
282 tearDown: function() {
283 window.LP = this.original_lp;
284 remove_test_node();
285+ Y.one('#request-notifications').empty();
286 delete this.content_node;
287 },
288
289@@ -1838,7 +1871,7 @@
290 }));
291
292 }, '0.1', {
293- requires: ['lp.testing.runner', 'lp.testing.helpers', 'test', 'test-console',
294- 'node', 'node-event-simulate', 'lp.ui.effects', 'lp.client',
295- 'lp.registry.structural_subscription']
296+ requires: ['lp.testing.runner', 'lp.testing.helpers', 'test',
297+ 'test-console', 'node', 'node-event-simulate', 'lp.ui.effects',
298+ 'lp.client', 'lp.registry.structural_subscription']
299 });