Skip to content

Commit

Permalink
Fixed #2920 - Improve ScrollPanel implementation for Accessibility
Browse files Browse the repository at this point in the history
  • Loading branch information
tugcekucukoglu committed Sep 1, 2022
1 parent e2a6316 commit 1f65c08
Show file tree
Hide file tree
Showing 4 changed files with 208 additions and 11 deletions.
12 changes: 11 additions & 1 deletion api-generator/components/scrollpanel.js
Original file line number Diff line number Diff line change
@@ -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 = {
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
}
};
5 changes: 5 additions & 0 deletions src/components/scrollpanel/ScrollPanel.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@ import { VNode } from 'vue';
import { ClassComponent, GlobalComponentConstructor } from '../ts-helpers';

export interface ScrollPanelProps {
/**
* Step factor to scroll the content while pressing the arrow keys.
* Default value is 5.
*/
step?: number | undefined;
}

export interface ScrollPanelSlots {
Expand Down
133 changes: 125 additions & 8 deletions src/components/scrollpanel/ScrollPanel.vue
Original file line number Diff line number Diff line change
@@ -1,20 +1,26 @@
<template>
<div class="p-scrollpanel p-component">
<div class="p-scrollpanel-wrapper">
<div ref="content" class="p-scrollpanel-content" @scroll="moveBar" @mouseenter="moveBar">
<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" :id="id + '_scrollpanel'">
<div ref="content" class="p-scrollpanel-content" @scroll="onScroll" @mouseenter="moveBar">
<slot></slot>
</div>
</div>
<div ref="xBar" class="p-scrollpanel-bar p-scrollpanel-bar-x" @mousedown="onXBarMouseDown"></div>
<div ref="yBar" class="p-scrollpanel-bar p-scrollpanel-bar-y" @mousedown="onYBarMouseDown"></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" tabindex="0" @mousedown="onYBarMouseDown" @keydown="onKeyDown($event)" @keyup="onKeyUp" @focus="onFocus"></div>
</div>
</template>

<script>
import {DomHandler} from 'primevue/utils';
import {DomHandler,UniqueComponentId} from 'primevue/utils';
export default {
name: 'ScrollPanel',
props: {
step: {
type: Number,
default: 5
}
},
initialized: false,
documentResizeListener: null,
documentMouseMoveListener: null,
Expand All @@ -26,6 +32,16 @@ export default {
isYBarClicked: false,
lastPageX: null,
lastPageY: null,
timer: null,
outsideClickListener: null,
data() {
return {
id: UniqueComponentId(),
orientation: 'vertical',
lastScrollTop: 0,
lastScrollLeft: 0
}
},
mounted() {
if (this.$el.offsetParent) {
this.initialize();
Expand Down Expand Up @@ -55,7 +71,7 @@ export default {
pureContainerHeight = DomHandler.getHeight(this.$el) - parseInt(xBarStyles['height'], 10);
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'];
}
else {
Expand Down Expand Up @@ -98,6 +114,7 @@ export default {
},
onYBarMouseDown(e) {
this.isYBarClicked = true;
this.$refs.yBar.focus();
this.lastPageY = e.pageY;
DomHandler.addClass(this.$refs.yBar, 'p-scrollpanel-grabbed');
DomHandler.addClass(document.body, 'p-scrollpanel-grabbed');
Expand All @@ -107,13 +124,97 @@ export default {
},
onXBarMouseDown(e) {
this.isXBarClicked = true;
this.$refs.xBar.focus();
this.lastPageX = e.pageX;
DomHandler.addClass(this.$refs.xBar, 'p-scrollpanel-grabbed');
DomHandler.addClass(document.body, 'p-scrollpanel-grabbed');
this.bindDocumentMouseListeners();
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) {
if (this.isXBarClicked) {
this.onMouseMoveForXBar(e);
Expand Down Expand Up @@ -142,6 +243,19 @@ export default {
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() {
DomHandler.removeClass(this.$refs.yBar, 'p-scrollpanel-grabbed');
DomHandler.removeClass(this.$refs.xBar, 'p-scrollpanel-grabbed');
Expand All @@ -161,7 +275,10 @@ export default {
scrollTop(scrollTop) {
let scrollableHeight = this.$refs.content.scrollHeight - this.$refs.content.clientHeight;
scrollTop = scrollTop > scrollableHeight ? scrollableHeight : scrollTop > 0 ? scrollTop : 0;
this.$refs.contentscrollTop = scrollTop;
this.$refs.content.scrollTop = scrollTop;
},
timeoutFrame(fn) {
setTimeout(fn, 0);
},
bindDocumentMouseListeners() {
if (!this.documentMouseMoveListener) {
Expand Down
69 changes: 67 additions & 2 deletions src/views/scrollpanel/ScrollPanelDoc.vue
Original file line number Diff line number Diff line change
Expand Up @@ -46,10 +46,39 @@ import ScrollPanel from 'primevue/scrollpanel';
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>

<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>
<p>Following is the list of structural style classes, for theming classes visit <router-link to="/theming">theming</router-link> page.</p>
Expand Down Expand Up @@ -90,6 +119,42 @@ import ScrollPanel from 'primevue/scrollpanel';
</table>
</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>
<p>None.</p>
</AppDoc>
Expand All @@ -99,7 +164,7 @@ import ScrollPanel from 'primevue/scrollpanel';
export default {
data() {
return {
sources: {
sources: {
'options-api': {
tabName: 'Options API Source',
content: `
Expand Down

0 comments on commit 1f65c08

Please sign in to comment.