Fixed #2920 - Improve ScrollPanel implementation for Accessibility
parent
e2a631657b
commit
1f65c0892f
|
@ -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
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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>
|
||||||
|
<ScrollPanel style="width: 100%; height: 200px" :step="10"3>
|
||||||
|
content
|
||||||
|
</ScrollPanel>
|
||||||
|
|
||||||
</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: `
|
||||||
|
|
Loading…
Reference in New Issue