Skip to content

Commit

Permalink
feat(QSplitter): add keyboard navigation; improve a11y quasarframewor…
Browse files Browse the repository at this point in the history
  • Loading branch information
pdanpdan committed Mar 21, 2023
1 parent bee6d1f commit 937c1f7
Show file tree
Hide file tree
Showing 3 changed files with 168 additions and 29 deletions.
32 changes: 25 additions & 7 deletions ui/dev/src/pages/components/splitter.vue
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,9 @@
style="height: 700px; border: 1px solid black"
>
<template v-slot:before>
<div class="q-layout-padding">
<div class="q-layout-padding q-focusable relative-position" tabindex="0">
<div class="q-focus-helper" />

<div class="text-h1 q-mb-md">
Before
</div>
Expand All @@ -56,11 +58,14 @@
slot="separator"
size="40px"
name="drag_indicator"
tabindex="-1"
@click="separatorLog"
/>

<template v-slot:after>
<div class="q-layout-padding">
<div class="q-layout-padding q-focusable relative-position" tabindex="0">
<div class="q-focus-helper" />

<div class="text-h1 q-mb-md">
After
</div>
Expand All @@ -81,9 +86,12 @@

class="q-mt-md stylish-splitter"
separator-class="bg-deep-orange"
:tabindex="showSeparator ? -1 : 0"
>
<template v-slot:before>
<div class="q-layout-padding">
<div class="q-layout-padding q-focusable relative-position" tabindex="0">
<div class="q-focus-helper" />

<div class="text-h1 q-mb-md">
Before
</div>
Expand All @@ -109,17 +117,22 @@
separator-class="bg-deep-orange"
class="bg-white rounded-borders"
style="width: 50vw; height: 30vh"
@keydown.stop
>
<template v-slot:before>
<div class="q-layout-padding">
<div class="q-layout-padding q-focusable relative-position" tabindex="0">
<div class="q-focus-helper" />

<div v-for="n in 20" :key="n" class="q-my-md">
{{ n }}. Lorem ipsum dolor sit.
</div>
</div>
</template>

<template v-slot:after>
<div class="q-layout-padding">
<div class="q-layout-padding q-focusable relative-position" tabindex="0">
<div class="q-focus-helper" />

<div v-for="n in 20" :key="n" class="q-my-md">
{{ n }}. Lorem ipsum dolor sit.
</div>
Expand All @@ -135,9 +148,12 @@
horizontal
:disable="disable"
separator-class="bg-deep-orange"
:tabindex="showSeparator ? -1 : 0"
>
<template v-slot:before>
<div class="q-layout-padding">
<div class="q-layout-padding q-focusable relative-position" tabindex="0">
<div class="q-focus-helper" />

<div class="text-h1 q-mb-md">
After - Before
</div>
Expand All @@ -158,7 +174,9 @@
/>

<template v-slot:after>
<div class="q-layout-padding">
<div class="q-layout-padding q-focusable relative-position" tabindex="0">
<div class="q-focus-helper" />

<div class="text-h1 q-mb-md">
After - After
</div>
Expand Down
160 changes: 138 additions & 22 deletions ui/src/components/splitter/QSplitter.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,16 @@ import DarkMixin from '../../mixins/dark.js'
import ListenersMixin from '../../mixins/listeners.js'

import { slot, mergeSlot } from '../../utils/private/slot.js'
import { stop } from '../../utils/event.js'
import { stop, stopAndPrevent } from '../../utils/event.js'
import cache from '../../utils/private/cache.js'
import uid from '../../utils/uid.js'

const keyDirections = {
37: 'left',
38: 'up',
39: 'right',
40: 'down'
}

export default Vue.extend({
name: 'QSplitter',
Expand Down Expand Up @@ -44,6 +52,8 @@ export default Vue.extend({
horizontal: Boolean,
disable: Boolean,

tabindex: [String, Number],

beforeClass: [Array, String, Object],
afterClass: [Array, String, Object],

Expand Down Expand Up @@ -113,31 +123,79 @@ export default Vue.extend({
}
}]
}
},

separatorAttrs () {
const attrs = this.disable === true
? { tabindex: -1, 'aria-disabled': 'true' }
: { tabindex: this.tabindex || 0 }
const ariaValue = this.__getAriaValue(this.value)

return {
role: 'separator',
'aria-orientation': this.horizontal === true ? 'horizontal' : 'vertical',
'aria-controls': this.targetUid,
'aria-valuemin': this.computedLimits[0],
'aria-valuemax': this.computedLimits[1],
'aria-valuenow': ariaValue.now,
'aria-valuetext': ariaValue.text,
...attrs
}
},

separatorEvents () {
return this.disable === true
? void 0
: { keydown: this.__panKeydown }
}
},

methods: {
__panStart () {
const size = this.$el.getBoundingClientRect()[this.prop]

this.__dir = this.horizontal === true ? 'up' : 'left'
this.__maxValue = this.unit === '%' ? 100 : size
this.__value = Math.min(this.__maxValue, this.computedLimits[1], Math.max(this.computedLimits[0], this.value))
this.__multiplier = (this.reverse !== true ? 1 : -1) *
(this.horizontal === true ? 1 : (this.$q.lang.rtl === true ? -1 : 1)) *
(this.unit === '%' ? (size === 0 ? 0 : 100 / size) : 1)

this.$el.classList.add('q-splitter--active')
},

__panProgress (val) {
this.__normalized = Math.min(this.__maxValue, this.computedLimits[1], Math.max(this.computedLimits[0], val))

this.$refs[this.side].style[this.prop] = this.__getCSSValue(this.__normalized)

const ariaValue = this.__getAriaValue(this.__normalized)
this.$refs.separator.setAttribute('aria-valuenow', ariaValue.now)
this.$refs.separator.setAttribute('aria-valuetext', ariaValue.text)

if (this.emitImmediately === true && this.value !== this.__normalized) {
this.$emit('input', this.__normalized)
}
},

__panEnd () {
this.__panCleanup !== void 0 && this.__panCleanup()

if (this.__normalized !== this.value) {
this.$emit('input', this.__normalized)
}

this.$el.classList.remove('q-splitter--active')
},

__pan (evt) {
if (evt.isFinal === true) {
if (this.__normalized !== this.value) {
this.$emit('input', this.__normalized)
}

this.$el.classList.remove('q-splitter--active')
this.__panEnd()
return
}

if (evt.isFirst === true) {
const size = this.$el.getBoundingClientRect()[this.prop]

this.__dir = this.horizontal === true ? 'up' : 'left'
this.__maxValue = this.unit === '%' ? 100 : size
this.__value = Math.min(this.__maxValue, this.computedLimits[1], Math.max(this.computedLimits[0], this.value))
this.__multiplier = (this.reverse !== true ? 1 : -1) *
(this.horizontal === true ? 1 : (this.$q.lang.rtl === true ? -1 : 1)) *
(this.unit === '%' ? (size === 0 ? 0 : 100 / size) : 1)

this.$el.classList.add('q-splitter--active')
this.__panStart()
return
}

Expand All @@ -146,13 +204,47 @@ export default Vue.extend({
(evt.direction === this.__dir ? -1 : 1) *
evt.distance[this.horizontal === true ? 'y' : 'x']

this.__normalized = Math.min(this.__maxValue, this.computedLimits[1], Math.max(this.computedLimits[0], val))
this.__panProgress(val)
},

this.$refs[this.side].style[this.prop] = this.__getCSSValue(this.__normalized)
__panKeydown (evt) {
this.qListeners.keydown !== void 0 && this.$emit('keydown', evt)

if (this.emitImmediately === true && this.value !== this.__normalized) {
this.$emit('input', this.__normalized)
if (
this.disable === true ||
evt.defaultPrevented === true ||
(this.horizontal !== true && [ 37, 39 ].indexOf(evt.keyCode) === -1) ||
(this.horizontal === true && [ 38, 40 ].indexOf(evt.keyCode) === -1)
) {
return
}

stopAndPrevent(evt)

if (this.__panCleanup === void 0) {
document.addEventListener('keyup', this.__panEnd)
document.addEventListener('focusout', this.__panEnd)

this.__panCleanup = () => {
this.__panCleanup = void 0
document.removeEventListener('keyup', this.__panEnd)
document.removeEventListener('focusout', this.__panEnd)

this.__panEnd()
}

this.__panStart()
this.__normalized = this.__value
}

const direction = keyDirections[evt.keyCode]

const val = this.__normalized +
this.__multiplier *
(direction === this.__dir ? -1 : 1) *
(evt.shiftKey === true ? 1 : 10)

this.__panProgress(val)
},

__normalize (val, limits) {
Expand All @@ -164,27 +256,50 @@ export default Vue.extend({
}
},

__getAriaValue (value) {
const now = this.unit === '%' ? value : Math.round(value)

return {
now,
text: (Math.round(now * 100) / 100) + this.unit
}
},

__getCSSValue (value) {
return (this.unit === '%' ? value : Math.round(value)) + this.unit
}
},

created () {
this.targetUid = `sp_${uid()}`
},

beforeDestroy () {
this.__panCleanup !== void 0 && this.__panCleanup()
},

render (h) {
const attrs = this.disable === true ? { 'aria-disabled': 'true' } : void 0
const attrs = {
[this.side]: { id: this.targetUid }
}

const child = [
h('div', {
ref: 'before',
staticClass: 'q-splitter__panel q-splitter__before' + (this.reverse === true ? ' col' : ''),
style: this.styles.before,
class: this.beforeClass,
attrs: attrs.before,
on: cache(this, 'stop', { input: stop })
}, slot(this, 'before')),

h('div', {
staticClass: 'q-splitter__separator',
ref: 'separator',
style: this.separatorStyle,
class: this.separatorClass,
attrs
attrs: this.separatorAttrs,
on: this.separatorEvents
}, [
h('div', {
staticClass: 'absolute-full q-splitter__separator-area',
Expand All @@ -197,6 +312,7 @@ export default Vue.extend({
staticClass: 'q-splitter__panel q-splitter__after' + (this.reverse === true ? '' : ' col'),
style: this.styles.after,
class: this.afterClass,
attrs: attrs.after,
on: cache(this, 'stop', { input: stop })
}, slot(this, 'after'))
]
Expand Down
5 changes: 5 additions & 0 deletions ui/src/components/splitter/QSplitter.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,11 @@
"category": "content|model"
},

"tabindex": {
"extends": "tabindex",
"addedIn": "v1.18.6"
},

"disable": {
"extends": "disable"
},
Expand Down

0 comments on commit 937c1f7

Please sign in to comment.