Skip to content

Commit

Permalink
Merge pull request #185 from marcellamaki/kdropdown-keyboard-navigation
Browse files Browse the repository at this point in the history
Kdropdown keyboard navigation
  • Loading branch information
rtibbles authored May 24, 2022
2 parents 28111df + fcb8cb0 commit 26c0c30
Show file tree
Hide file tree
Showing 5 changed files with 137 additions and 23 deletions.
45 changes: 35 additions & 10 deletions lib/KDropdownMenu.vue
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
@open="handleOpen"
>
<UiMenu
ref="menu"
:options="options"
@select="handleSelection"
/>
Expand Down Expand Up @@ -97,11 +98,15 @@
},
},
beforeDestroy() {
window.removeEventListener('keyup', this.handleKeyUp, true);
window.removeEventListener('keydown', this.handleOpenMenuNavigation, true);
},
methods: {
handleOpen() {
window.addEventListener('keyup', this.handleKeyUp, true);
this.$nextTick(() => this.setFocus());
window.addEventListener('keydown', this.handleOpenMenuNavigation, true);
},
setFocus() {
this.$refs.menu.$el.querySelector('li').focus();
},
handleClose() {
const focusedElement = document.activeElement;
Expand All @@ -116,14 +121,34 @@
}
window.removeEventListener('keyup', this.handleKeyUp, true);
},
handleKeyUp(event) {
if (event.shiftKey && event.keyCode == 9) {
const popover = this.$refs.popover.$el;
const popoverIsOpen = popover.clientWidth > 0 && popover.clientHeight > 0;
if (popoverIsOpen && !popover.contains(document.activeElement)) {
this.closePopover();
this.focusOnButton();
}
handleOpenMenuNavigation(event) {
// identify the menu state and length
if (!this.$refs.popover && !this.$refs.popover.$el) {
return;
}
const popover = this.$refs.popover.$el;
const menuElements = this.$refs.menu.$el.querySelector('div').children;
const lastChild = menuElements[menuElements.length - 1];
const popoverIsOpen = popover.clientWidth > 0 && popover.clientHeight > 0;
// set current element and its siblings
let focusedElement = document.activeElement;
let sibling = focusedElement.nextElementSibling;
let prevSibling = focusedElement.previousElementSibling;
// manage rotating through the options using arrow keys
// UP arrow: .keyCode is depricated and should used only as a fallback
if ((event.key == 'ArrowUp' || event.keyCode == 38) && popoverIsOpen) {
event.preventDefault();
prevSibling
? this.$nextTick(() => prevSibling.focus())
: this.$nextTick(() => lastChild.focus());
// DOWN arrow
} else if ((event.key == 'ArrowDown' || event.keyCode == 40) && popoverIsOpen) {
event.preventDefault();
sibling ? this.$nextTick(() => sibling.focus()) : this.$nextTick(() => this.setFocus());
// if a TAB key, not an arrow key, close the popover and advance to next item in the tab index
} else if ((event.key == 'Tab' || event.keyCode == 9) && popoverIsOpen) {
this.closePopover();
}
},
handleSelection(selection) {
Expand Down
6 changes: 4 additions & 2 deletions lib/keen/UiMenu.vue
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
lazy

:class="classes"
:contain-focus="containFocus"
>
<UiMenuOption
v-for="(option, index) in options"
Expand All @@ -27,6 +26,7 @@
@keydown.enter.native="selectOption(option)"

@keydown.esc.native.esc="closeMenu"
:style="[ activeOutline ]"
>
<slot name="option" :option="option"></slot>
</UiMenuOption>
Expand Down Expand Up @@ -101,6 +101,9 @@
'has-secondary-text': this.hasSecondaryText,
};
},
activeOutline() {
return this.isActive ? this.$coreOutline : {};
},
},
methods: {
Expand All @@ -112,7 +115,6 @@
this.$emit('select', option);
this.closeMenu();
},
closeMenu() {
this.$emit('close');
},
Expand Down
19 changes: 18 additions & 1 deletion lib/keen/UiMenuOption.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,12 @@
<component
:is="isAnchor ? 'a' : 'li'"
class="ui-menu-option"

supports-modality=keyboard
@focus="isActive = true"
@blur="isActive = false"
role="menu-item"
:class="classes"
:style="activeStyle"
:href="isAnchor ? (disabled ? null : href) : null"
:tabindex="(isDivider || isAnchor || disabled) ? null : '0'"
:target="isAnchor ? (disabled ? null : target) : null"
Expand Down Expand Up @@ -41,6 +44,8 @@
<script>
import UiIcon from './UiIcon.vue';
import globalThemeState from '../styles/globalThemeState';
export default {
name: 'UiMenuOption',
Expand All @@ -49,6 +54,10 @@
UiIcon,
},
data:() => ({
isActive: false,
}),
props: {
type: String,
label: String,
Expand Down Expand Up @@ -77,6 +86,9 @@
};
},
activeStyle() {
return this.isActive ? {...this.$coreOutline, outlineOffset: '-2px' } : {}
},
isDivider() {
return this.type === 'divider';
},
Expand Down Expand Up @@ -125,6 +137,11 @@
background-color: $ui-menu-item-hover-color;
}
&:focus:not(.is-disabled),
body[modality='keyboard'] &:focus {
background-color: $ui-menu-item-hover-color;
}
&.is-disabled {
color: $secondary-text-color;
cursor: default;
Expand Down
88 changes: 79 additions & 9 deletions lib/styles/trackInputModality.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,68 @@

import globalThemeState from './globalThemeState';

// only keys listed here will change modality to keyboard
const KEYS_WHITELIST = ['Tab'];

function setUpEventHandlers(disableFocusRingByDefault) {
let recentKeyboardEvent = null;
let hadKeyboardEvent = false;
let hadClickEvent = false;
const keyboardModalityDefaultElements = [
'input:not([type])',
'input[type=text]',
'input[type=radio]',
'input[type=checkbox]',
'input[type=number]',
'input[type=date]',
'input[type=time]',
'input[type=datetime]',
'textarea',
'[role=textbox]',
'a',
'button',
].join(',');

// add this to any element to allow keyboard navigation, regardless of focus event
const keyboardModalityOverride = ['[supports-modality=keyboard]'];

let isHandlingKeyboardThrottle;

const matcher = (() => {
const el = document.body;

if (el.matchesSelector) {
return el.matchesSelector;
}

if (el.webkitMatchesSelector) {
return el.webkitMatchesSelector;
}

if (el.mozMatchesSelector) {
return el.mozMatchesSelector;
}

if (el.msMatchesSelector) {
return el.msMatchesSelector;
}
})();

const focusTriggersKeyboardModality = function(el) {
let triggers = false;

if (matcher) {
triggers =
matcher.call(el, keyboardModalityDefaultElements) && matcher.call(el, ':not([readonly])');
}
return triggers;
};

const focusSetExplicitly = function(el) {
let triggers = false;

if (matcher) {
triggers = matcher.call(el, keyboardModalityOverride) && matcher.call(el, ':not([readonly])');
}
return triggers;
};

if (disableFocusRingByDefault) {
const css = 'body:not([modality=keyboard]) :focus { outline: none; }';
const head = document.head || document.getElementsByTagName('head')[0];
Expand All @@ -33,25 +88,39 @@ function setUpEventHandlers(disableFocusRingByDefault) {

document.body.addEventListener(
'keydown',
event => {
recentKeyboardEvent = event;
() => {
hadKeyboardEvent = true;
hadClickEvent = false;

if (isHandlingKeyboardThrottle) {
clearTimeout(isHandlingKeyboardThrottle);
}

isHandlingKeyboardThrottle = setTimeout(() => {
recentKeyboardEvent = null;
hadKeyboardEvent = false;
}, 100);
},
true
);

document.body.addEventListener('mousedown', () => {
hadClickEvent = true;
hadKeyboardEvent = false;
});

document.body.addEventListener(
'focus',
() => {
if (recentKeyboardEvent && KEYS_WHITELIST.includes(recentKeyboardEvent.key)) {
e => {
if (
(hadKeyboardEvent && focusTriggersKeyboardModality(e.target)) ||
(focusSetExplicitly(e.target) && !hadClickEvent)
) {
// both the JS state and the body attribute for keyboard modality
globalThemeState.inputModality = 'keyboard';
document.body.setAttribute('modality', 'keyboard');
} else {
globalThemeState.inputModality = null;
document.body.setAttribute('modality', '');
}
},
true
Expand All @@ -61,6 +130,7 @@ function setUpEventHandlers(disableFocusRingByDefault) {
'blur',
() => {
globalThemeState.inputModality = null;
document.body.setAttribute('modality', '');
},
true
);
Expand Down
2 changes: 1 addition & 1 deletion yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -9060,7 +9060,7 @@ knuth-shuffle-seeded@^1.0.6:

"kolibri-design-system@git://github.com/learningequality/kolibri-design-system#65ee3a69a2ab072ea92e034d95118687678b4f5f":
version "0.2.2-beta-2"
resolved "git://github.com/learningequality/kolibri-design-system#65ee3a69a2ab072ea92e034d95118687678b4f5f"
resolved "https://github.com/learningequality/kolibri-design-system#65ee3a69a2ab072ea92e034d95118687678b4f5f"
dependencies:
aphrodite "https://github.com/learningequality/aphrodite/"
autosize "^3.0.21"
Expand Down

0 comments on commit 26c0c30

Please sign in to comment.