-
Notifications
You must be signed in to change notification settings - Fork 8.3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Make Discover field chooser items keyboard accessible. #11591
Make Discover field chooser items keyboard accessible. #11591
Conversation
👍 Looks good. Seems to work. Think to address that issue completely we'll need to deal with the caret expand bits in the content rows as well. |
@snide Ah gotcha. BTW I just pushed a commit that also makes the links and +/- icons inside of the field chooser tabbable. I had to swap out the icons for buttons, and I used the MicroButton in the UI Framework, so there's a minor design change. BeforeAfterThe contrast goes down quite a bit. We could revert this change and create custom buttons that preserve the original appearance. OTOH the link color also contrasts poorly so maybe we should change the background color. Thoughts? |
if ([13, 32].includes(e.keyCode)) { | ||
$scope.toggleDetails(field); | ||
} | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we're going to need to do this often, do you think it would be worthwhile to create a custom attribute directive called kbn-buttonPress
or something like that, which could handle this keycode checking and also do the preventDefault for the space bar on keyDown? It might make this a bit easier to implement in other components.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for bringing this up! I think it's a great idea but I'm a little stumped for the best place and name for this module. Suggestions?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
src/ui/public/directives
?
I suggested kbn-buttonPress
because it is kind of similar to the angular event handling directives (ng-click
, ng-keyup
etc.) which we would be mimicking and a "buttonPress" (pressing space or enter on a button) is what we're trying to emulate.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sounds good to me. How do you feel about kbn-accessible-click
? I think it's good to emphasize that it's for aiding accessibility up-front (and I imagine we'll have other services and directives geared towards accessibility, so they'll be some cohesion here if they all have that word in their names), and I think there's nice symmetry with ng-click
since they both have the word "click" in them.
ng-bind="::field.display ? 'remove' : 'add'" | ||
class="discover-sidebar-item-action kuiButton kuiButton--small kuiButton--primary" | ||
data-test-subj="fieldToggle-{{::field.name}}" | ||
></button> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For some reason this button is not reacting to the space bar, even though it is a native button.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Which OS and browser are you using? When I tab to it and hit space, it's added to the selected fields list. I'm on Chrome on OS X.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Never mind, I can repro now that I implemented your preventDefault suggestion.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Accidentally submitted that last review before adding these additional notes:
- The filter buttons are grey now and don't match the ones in the doc table, is that intentional?
Pressing space to toggle the field details causes the page to scroll down, which doesn't happen with a native button. I played with this a bit, and it seems adding a preventDefault on keyDown will stop this from happening.
(probably not obvious, but I'm hitting space in this screenshot)
@Bargs Thanks for the great review. I believe I've addressed everything. Can you take another look? |
0b7fd50
to
6aa8a88
Compare
const elementType = element.prop('tagName'); | ||
|
||
if (elementType === 'BUTTON') { | ||
throw new Error(`kbnAccessibleClick doesn't need to be used a button.`); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think you lost an "on" :)
} | ||
|
||
// Prevent a scroll from occurring if the user has hit space. | ||
if (accessibleClickKeys[e.keyCode]) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This will preventDefault on both space and enter, but I'd lean toward only preventing space to lower the chance of any unintended side effects.
if (accessibleClickKeys[e.keyCode]) { | ||
// If a unique click handler has been specified, then call it. | ||
if (attrs.kbnAccessibleClick) { | ||
return scope.kbnAccessibleClick(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we want to keep this, we should pass the event here
} | ||
|
||
element.on('keyup', e => { | ||
// Support keyboard accessibility by toggling display on ENTER or SPACE keypress. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Comment seems out of date, should be something like
// Support keyboard accessibility by emulating mouse clicks on ENTER or SPACE keypress.
|
||
if (!ngClick) { | ||
throw new Error('kbnAccessibleClick requires a callback or ng-click defined on its element.'); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could you move this out of the event handler and into the main link function so that it fails early if ngClick is not specified?
throw new Error('kbnAccessibleClick requires a callback or ng-click defined on its element.'); | ||
} | ||
|
||
ngClick(scope.$parent); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I hadn't considered using this directive in conjunction with ng-click, it's a great idea! You've got me wondering, do we even want to provide the ability to specify a separate event handler on this directive, or should we always require ng-click? My gut says the latter because it would ensure the interaction is always the same regardless of whether you're using a keyboard or mouse to interact with the element.
I also think we could avoid manually parsing the ng-click attribute if we simply fire a click event and allow ng-click to do its work as usual. This block could be simplified to:
element.on('keyup', e => {
if (accessibleClickKeys[e.keyCode]) {
element.click();
}
});
One other question along this line of thought: you mentioned in #11513 that listening to the keyup event was necessary because "some browsers do not trigger onclick events for such elements when activated via the keyboard." Are there browsers that do trigger click events and if so will having an ng-click
and an kbn-accessible-click
on the same element cause the event handler to execute twice?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was afraid of being too prescriptive by requiring an ng-click, but I guess you're right. That's sort of the whole idea, so I'll remove the ability to provide a handler.
Great suggestion on delegating to the ng-click via a click event, I'll implement your suggestion.
I did a little research but I couldn't find any information on which browsers may trigger click events on non-interactive elements. Hopefully we can recruit the help of an expert to help us with this (@jimgoodwin is already looking into this).
Thanks @Bargs, this is ready for your 👀 again. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It looks like the default behavior of links is to only open the href on enter
, not on space
.
This means that, currently, there are a couple of links in the sidebar that aren't activated on space
: The "500/500" link, and the "Visualize" link (that appears as a button). Do we want to activate those links on space
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks like we're in pretty good shape! Left just a couple minor comments.
return { | ||
restrict: 'A', | ||
scope: { | ||
kbnAccessibleClick: '&', |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this scope variable can be removed now.
} | ||
|
||
// We're emulating a click action, so we should already have a regular click handler defined. | ||
if (!$parse(attrs.ngClick)) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It seems that $parse(undefined)
returns a noop function, so this error never throws. if (!attrs.ngClick)
should be sufficient.
import { uiModules } from 'ui/modules'; | ||
|
||
uiModules.get('kibana') | ||
.directive('kbnAccessibleClick', function ($parse) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
$parse
will be unused once that usage below is removed.
@lukasolson Thanks for pointing out that behavior. Per our research and convo, links shouldn't activate on SPACE keypress, so we should be OK on that front. @Bargs I've implemented your suggestions and added unit tests. Could you both please take another look? |
}); | ||
|
||
it('when the element is a link with an href', () => { | ||
const html = `<a href="#"" kbn-accessible-click></a>`; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks like there's an extra "
here.
const html = `<button kbn-accessible-click></button>`; | ||
expect(() => { | ||
$compile(html)($rootScope); | ||
}).to.throwError(`kbnAccessibleClick doesn't need to be used on a button.`); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
throwError
takes regular expressions, not strings, so these assertions aren't checking the error message currently.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Damn, we have tests elsewhere that pass strings into throwError
. Thanks for letting me know.
const html = `<a ng-click="noop" kbn-accessible-click></a>`; | ||
expect(() => { | ||
$compile(html)($rootScope); | ||
}).not.to.throwError(`kbnAccessibleClick doesn't need to be used on a link if it has a href attribute.`); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you switch this to a regex it will blow up. It seems that it's not possible to test for not throwing a specific exception, so you should just pass 0 args here.
const child = angular.element(`<button></button>`); | ||
element.append(child); | ||
const e = angular.element.Event('keydown'); // eslint-disable-line new-cap | ||
e.which = ENTER_KEY; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why are you setting e.which
here when you were setting e.keyCode
in the tests above?
}); | ||
}); | ||
|
||
it(`doesn't call ng-click when the originating element is a child`, () => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This doesn't seem to be working correctly. This check happens in the keydown
event handler. But the click gets fired in the keyup
event handler. I set a breakpoint in the keyup
event handler and it is still getting fired even if the event originated on a child. I think the only reason we're not noticing this with the "Add" button is that the element immediately gets destroyed.
If you switch line 115 from keydown
to keyup
and line 116 from e.which
to e.keyCode
you'll see the test fail.
child.trigger(e); | ||
expect(scope.handleClick.callCount).to.be(0); | ||
}); | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could you also add one more test for the preventDefault on keyDown
for spaces?
@Bargs OK man chickity check it. Thanks for the critique. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Other than the one small issue I mentioned inline, this LGTM. Should be good to merge after that's fixed.
Really nice improvement, it's great to see the keyboard working in Discover.
const child = angular.element(`<button></button>`); | ||
element.append(child); | ||
const e = angular.element.Event('keyup'); // eslint-disable-line new-cap | ||
e.keyCode = ENTER_KEY; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think you mean SPACE_KEY
here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great catch, as always.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM!
jenkins, test this |
6866324
to
7ac0a1e
Compare
jenkins, test this |
…he default scroll action.
7ac0a1e
to
c7f820b
Compare
* Make Discover field chooser items keyboard accessible. * Make records count link and plus/minus icons tabbable. * Prevent scrolling when you hit spacebar to toggle a field. * Add accessibleClickKeys service and kbnAccessibleClick directive, with tests.
* Make Discover field chooser items keyboard accessible. * Make records count link and plus/minus icons tabbable. * Prevent scrolling when you hit spacebar to toggle a field. * Add accessibleClickKeys service and kbnAccessibleClick directive, with tests.
* Make Discover field chooser items keyboard accessible. * Make records count link and plus/minus icons tabbable. * Prevent scrolling when you hit spacebar to toggle a field. * Add accessibleClickKeys service and kbnAccessibleClick directive, with tests.
* Make Discover field chooser items keyboard accessible. * Make records count link and plus/minus icons tabbable. * Prevent scrolling when you hit spacebar to toggle a field. * Add accessibleClickKeys service and kbnAccessibleClick directive, with tests.
Partially addresses #11541
The general idea here is that you should be able to tab to interactive elements and use enter/space to interact with them (i.e. trigger a click handler). Ideally we'd use button elements, but because buttons can't contain other buttons, we have to make the containing div accessible by adding
tabindex="0"
and a keypress handler. See #11513 for more context.