Fixed #2920 - Improve ScrollPanel implementation for Accessibility

pull/2929/head
Tuğçe Küçükoğlu 2022-09-01 15:47:49 +03:00
parent e2a631657b
commit 1f65c0892f
4 changed files with 208 additions and 11 deletions

View File

@ -1,6 +1,16 @@
const ScrollPanelProps = [
{
name: "step",
type: "number",
default: "5",
description: "Step factor to scroll the content while pressing the arrow keys."
}
];
module.exports = { module.exports = {
scrollpanel: { scrollpanel: {
name: "ScrollPanel", name: "ScrollPanel",
description: "ScrollPanel is a cross browser, lightweight and themable alternative to native browser scrollbar." description: "ScrollPanel is a cross browser, lightweight and themable alternative to native browser scrollbar.",
props: ScrollPanelProps
} }
}; };

View File

@ -2,6 +2,11 @@ import { VNode } from 'vue';
import { ClassComponent, GlobalComponentConstructor } from '../ts-helpers'; import { ClassComponent, GlobalComponentConstructor } from '../ts-helpers';
export interface ScrollPanelProps { export interface ScrollPanelProps {
/**
* Step factor to scroll the content while pressing the arrow keys.
* Default value is 5.
*/
step?: number | undefined;
} }
export interface ScrollPanelSlots { export interface ScrollPanelSlots {

View File

@ -1,20 +1,26 @@
<template> <template>
<div class="p-scrollpanel p-component"> <div class="p-scrollpanel p-component" role="scrollbar" :aria-orientation="orientation" :aria-valuenow="orientation === 'vertical' ? lastScrollTop : lastScrollLeft" :aria-controls="id + '_scrollpanel'">
<div class="p-scrollpanel-wrapper"> <div class="p-scrollpanel-wrapper" :id="id + '_scrollpanel'">
<div ref="content" class="p-scrollpanel-content" @scroll="moveBar" @mouseenter="moveBar"> <div ref="content" class="p-scrollpanel-content" @scroll="onScroll" @mouseenter="moveBar">
<slot></slot> <slot></slot>
</div> </div>
</div> </div>
<div ref="xBar" class="p-scrollpanel-bar p-scrollpanel-bar-x" @mousedown="onXBarMouseDown"></div> <div ref="xBar" class="p-scrollpanel-bar p-scrollpanel-bar-x" tabindex="0" @mousedown="onXBarMouseDown" @keydown="onKeyDown($event)" @keyup="onKeyUp" @focus="onFocus" @blur="onBlur"></div>
<div ref="yBar" class="p-scrollpanel-bar p-scrollpanel-bar-y" @mousedown="onYBarMouseDown"></div> <div ref="yBar" class="p-scrollpanel-bar p-scrollpanel-bar-y" tabindex="0" @mousedown="onYBarMouseDown" @keydown="onKeyDown($event)" @keyup="onKeyUp" @focus="onFocus"></div>
</div> </div>
</template> </template>
<script> <script>
import {DomHandler} from 'primevue/utils'; import {DomHandler,UniqueComponentId} from 'primevue/utils';
export default { export default {
name: 'ScrollPanel', name: 'ScrollPanel',
props: {
step: {
type: Number,
default: 5
}
},
initialized: false, initialized: false,
documentResizeListener: null, documentResizeListener: null,
documentMouseMoveListener: null, documentMouseMoveListener: null,
@ -26,6 +32,16 @@ export default {
isYBarClicked: false, isYBarClicked: false,
lastPageX: null, lastPageX: null,
lastPageY: null, lastPageY: null,
timer: null,
outsideClickListener: null,
data() {
return {
id: UniqueComponentId(),
orientation: 'vertical',
lastScrollTop: 0,
lastScrollLeft: 0
}
},
mounted() { mounted() {
if (this.$el.offsetParent) { if (this.$el.offsetParent) {
this.initialize(); this.initialize();
@ -55,7 +71,7 @@ export default {
pureContainerHeight = DomHandler.getHeight(this.$el) - parseInt(xBarStyles['height'], 10); pureContainerHeight = DomHandler.getHeight(this.$el) - parseInt(xBarStyles['height'], 10);
if (containerStyles['max-height'] !== "none" && pureContainerHeight === 0) { if (containerStyles['max-height'] !== "none" && pureContainerHeight === 0) {
if(this.$refs.content.offsetHeight + parseInt(xBarStyles['height'], 10) > parseInt(containerStyles['max-height'], 10)) { if (this.$refs.content.offsetHeight + parseInt(xBarStyles['height'], 10) > parseInt(containerStyles['max-height'], 10)) {
this.$el.style.height = containerStyles['max-height']; this.$el.style.height = containerStyles['max-height'];
} }
else { else {
@ -98,6 +114,7 @@ export default {
}, },
onYBarMouseDown(e) { onYBarMouseDown(e) {
this.isYBarClicked = true; this.isYBarClicked = true;
this.$refs.yBar.focus();
this.lastPageY = e.pageY; this.lastPageY = e.pageY;
DomHandler.addClass(this.$refs.yBar, 'p-scrollpanel-grabbed'); DomHandler.addClass(this.$refs.yBar, 'p-scrollpanel-grabbed');
DomHandler.addClass(document.body, 'p-scrollpanel-grabbed'); DomHandler.addClass(document.body, 'p-scrollpanel-grabbed');
@ -107,6 +124,7 @@ export default {
}, },
onXBarMouseDown(e) { onXBarMouseDown(e) {
this.isXBarClicked = true; this.isXBarClicked = true;
this.$refs.xBar.focus();
this.lastPageX = e.pageX; this.lastPageX = e.pageX;
DomHandler.addClass(this.$refs.xBar, 'p-scrollpanel-grabbed'); DomHandler.addClass(this.$refs.xBar, 'p-scrollpanel-grabbed');
DomHandler.addClass(document.body, 'p-scrollpanel-grabbed'); DomHandler.addClass(document.body, 'p-scrollpanel-grabbed');
@ -114,6 +132,89 @@ export default {
this.bindDocumentMouseListeners(); this.bindDocumentMouseListeners();
e.preventDefault(); e.preventDefault();
}, },
onScroll(event) {
if (this.lastScrollLeft !== event.target.scrollLeft) {
this.lastScrollLeft = event.target.scrollLeft;
this.orientation = 'horizontal';
}
else if (this.lastScrollTop !== event.target.scrollTop) {
this.lastScrollTop = event.target.scrollTop;
this.orientation = 'vertical';
}
this.moveBar();
},
onKeyDown(event) {
if (this.orientation === 'vertical') {
switch(event.code) {
case 'ArrowDown': {
this.setTimer('scrollTop', this.step);
event.preventDefault();
break;
}
case 'ArrowUp': {
this.setTimer('scrollTop', this.step * -1);
event.preventDefault();
break;
}
case 'ArrowLeft':
case 'ArrowRight': {
event.preventDefault();
break;
}
default:
//no op
break;
}
}
else if (this.orientation === 'horizontal') {
switch(event.code) {
case 'ArrowRight': {
this.setTimer('scrollLeft', this.step);
event.preventDefault();
break;
}
case 'ArrowLeft': {
this.setTimer('scrollLeft', this.step * -1);
event.preventDefault();
break;
}
case 'ArrowDown':
case 'ArrowUp': {
event.preventDefault();
break;
}
default:
//no op
break;
}
}
},
onKeyUp() {
this.clearTimer();
},
repeat(bar, step) {
this.$refs.content[bar] += step;
this.moveBar();
},
setTimer(bar, step) {
this.clearTimer();
this.timer = setTimeout(() => {
this.repeat(bar, step);
}, 40);
},
clearTimer() {
if (this.timer) {
clearTimeout(this.timer);
}
},
onDocumentMouseMove(e) { onDocumentMouseMove(e) {
if (this.isXBarClicked) { if (this.isXBarClicked) {
this.onMouseMoveForXBar(e); this.onMouseMoveForXBar(e);
@ -142,6 +243,19 @@ export default {
this.$refs.content.scrollTop += deltaY / this.scrollYRatio; this.$refs.content.scrollTop += deltaY / this.scrollYRatio;
}); });
}, },
onFocus(event) {
if (this.$refs.xBar.isSameNode(event.target)) {
this.orientation = 'horizontal';
}
else if (this.$refs.yBar.isSameNode(event.target)) {
this.orientation = 'vertical';
}
},
onBlur() {
if (this.orientation === 'horizontal') {
this.orientation = 'vertical';
}
},
onDocumentMouseUp() { onDocumentMouseUp() {
DomHandler.removeClass(this.$refs.yBar, 'p-scrollpanel-grabbed'); DomHandler.removeClass(this.$refs.yBar, 'p-scrollpanel-grabbed');
DomHandler.removeClass(this.$refs.xBar, 'p-scrollpanel-grabbed'); DomHandler.removeClass(this.$refs.xBar, 'p-scrollpanel-grabbed');
@ -161,7 +275,10 @@ export default {
scrollTop(scrollTop) { scrollTop(scrollTop) {
let scrollableHeight = this.$refs.content.scrollHeight - this.$refs.content.clientHeight; let scrollableHeight = this.$refs.content.scrollHeight - this.$refs.content.clientHeight;
scrollTop = scrollTop > scrollableHeight ? scrollableHeight : scrollTop > 0 ? scrollTop : 0; scrollTop = scrollTop > scrollableHeight ? scrollableHeight : scrollTop > 0 ? scrollTop : 0;
this.$refs.contentscrollTop = scrollTop; this.$refs.content.scrollTop = scrollTop;
},
timeoutFrame(fn) {
setTimeout(fn, 0);
}, },
bindDocumentMouseListeners() { bindDocumentMouseListeners() {
if (!this.documentMouseMoveListener) { if (!this.documentMouseMoveListener) {

View File

@ -46,10 +46,39 @@ import ScrollPanel from 'primevue/scrollpanel';
background-color: #135ba1; background-color: #135ba1;
} }
</code></pre>
<h5>Steps</h5>
<p>Step factor is 5px by default and can be customized with <i>step</i> option.</p>
<pre v-code><code>
&lt;ScrollPanel style="width: 100%; height: 200px" :step="10"3&gt;
content
&lt;/ScrollPanel&gt;
</code></pre> </code></pre>
<h5>Properties</h5> <h5>Properties</h5>
<p>Any property such as style and class are passed to the main container element. There are no component specific properties.</p> <p>Any property such as style and class are passed to the main container element. Following are the additional properties to configure the component.</p>
<div class="doc-tablewrapper">
<table class="doc-table">
<thead>
<tr>
<th>Name</th>
<th>Type</th>
<th>Default</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td>step</td>
<td>number</td>
<td>1</td>
<td>Step factor to scroll the content while pressing the arrow keys.</td>
</tr>
</tbody>
</table>
</div>
<h5>Styling</h5> <h5>Styling</h5>
<p>Following is the list of structural style classes, for theming classes visit <router-link to="/theming">theming</router-link> page.</p> <p>Following is the list of structural style classes, for theming classes visit <router-link to="/theming">theming</router-link> page.</p>
@ -90,6 +119,42 @@ import ScrollPanel from 'primevue/scrollpanel';
</table> </table>
</div> </div>
<h5>Accessibility</h5>
<DevelopmentSection>
<h6>Screen Reader</h6>
<p>Scrollbars of the ScrollPanel has a <i>scrollbar</i> role along with the <i>aria-controls</i> attribute that refers to the id of the scrollable content container and the <i>aria-orientation</i> to indicate the orientation of scrolling.</p>
<h6>Header Keyboard Support</h6>
<div class="doc-tablewrapper">
<table class="doc-table">
<thead>
<tr>
<th>Key</th>
<th>Function</th>
</tr>
</thead>
<tbody>
<tr>
<td><i>down arrow</i></td>
<td>Scrolls content down when vertical scrolling is available.</td>
</tr>
<tr>
<td><i>up arrow</i></td>
<td>Scrolls content up when vertical scrolling is available.</td>
</tr>
<tr>
<td><i>left</i></td>
<td>Scrolls content left when horizontal scrolling is available.</td>
</tr>
<tr>
<td><i>right</i></td>
<td>Scrolls content right when horizontal scrolling is available.</td>
</tr>
</tbody>
</table>
</div>
</DevelopmentSection>
<h5>Dependencies</h5> <h5>Dependencies</h5>
<p>None.</p> <p>None.</p>
</AppDoc> </AppDoc>
@ -99,7 +164,7 @@ import ScrollPanel from 'primevue/scrollpanel';
export default { export default {
data() { data() {
return { return {
sources: { sources: {
'options-api': { 'options-api': {
tabName: 'Options API Source', tabName: 'Options API Source',
content: ` content: `