Fixed #4841 - TreeSelect: Accessibility Improvements

pull/4844/head
tugcekucukoglu 2023-11-17 21:10:06 +03:00
parent e0c634a1f8
commit d05054a5fc
1 changed files with 67 additions and 2 deletions

View File

@ -43,8 +43,19 @@
</slot> </slot>
</div> </div>
<Portal :appendTo="appendTo"> <Portal :appendTo="appendTo">
<transition name="p-connected-overlay" @enter="onOverlayEnter" @leave="onOverlayLeave" @after-leave="onOverlayAfterLeave" v-bind="ptm('transition')"> <transition name="p-connected-overlay" @enter="onOverlayEnter" @after-enter="onOverlayAfterEnter" @leave="onOverlayLeave" @after-leave="onOverlayAfterLeave" v-bind="ptm('transition')">
<div v-if="overlayVisible" :ref="overlayRef" @click="onOverlayClick" :class="[cx('panel'), panelClass]" @keydown="onOverlayKeydown" v-bind="{ ...panelProps, ...ptm('panel') }"> <div v-if="overlayVisible" :ref="overlayRef" @click="onOverlayClick" :class="[cx('panel'), panelClass]" @keydown="onOverlayKeydown" v-bind="{ ...panelProps, ...ptm('panel') }">
<span
ref="firstHiddenFocusableElementOnOverlay"
role="presentation"
aria-hidden="true"
class="p-hidden-accessible p-hidden-focusable"
:tabindex="0"
@focus="onFirstHiddenFocus"
v-bind="ptm('hiddenFirstFocusableEl')"
:data-p-hidden-accessible="true"
:data-p-hidden-focusable="true"
></span>
<slot name="header" :value="modelValue" :options="options"></slot> <slot name="header" :value="modelValue" :options="options"></slot>
<div :class="cx('wrapper')" :style="{ 'max-height': scrollHeight }" v-bind="ptm('wrapper')"> <div :class="cx('wrapper')" :style="{ 'max-height': scrollHeight }" v-bind="ptm('wrapper')">
<TSTree <TSTree
@ -78,6 +89,17 @@
</div> </div>
</div> </div>
<slot name="footer" :value="modelValue" :options="options"></slot> <slot name="footer" :value="modelValue" :options="options"></slot>
<span
ref="lastHiddenFocusableElementOnOverlay"
role="presentation"
aria-hidden="true"
class="p-hidden-accessible p-hidden-focusable"
:tabindex="0"
@focus="onLastHiddenFocus"
v-bind="ptm('hiddenLastFocusableEl')"
:data-p-hidden-accessible="true"
:data-p-hidden-focusable="true"
></span>
</div> </div>
</transition> </transition>
</Portal> </Portal>
@ -161,11 +183,15 @@ export default {
this.$emit('blur', event); this.$emit('blur', event);
}, },
onClick(event) { onClick(event) {
if (this.disabled) {
return;
}
if (!this.disabled && (!this.overlay || !this.overlay.contains(event.target))) { if (!this.disabled && (!this.overlay || !this.overlay.contains(event.target))) {
if (this.overlayVisible) this.hide(); if (this.overlayVisible) this.hide();
else this.show(); else this.show();
this.$refs.focusInput.focus(); DomHandler.focus(this.$refs.focusInput);
} }
}, },
onSelectionChange(keys) { onSelectionChange(keys) {
@ -186,6 +212,16 @@ export default {
onNodeToggle(keys) { onNodeToggle(keys) {
this.expandedKeys = keys; this.expandedKeys = keys;
}, },
onFirstHiddenFocus(event) {
const focusableEl = event.relatedTarget === this.$refs.focusInput ? DomHandler.getFirstFocusableElement(this.overlay, ':not([data-p-hidden-focusable="true"])') : this.$refs.focusInput;
DomHandler.focus(focusableEl);
},
onLastHiddenFocus(event) {
const focusableEl = event.relatedTarget === this.$refs.focusInput ? DomHandler.getLastFocusableElement(this.overlay, ':not([data-p-hidden-focusable="true"])') : this.$refs.focusInput;
DomHandler.focus(focusableEl);
},
onKeyDown(event) { onKeyDown(event) {
switch (event.code) { switch (event.code) {
case 'ArrowDown': case 'ArrowDown':
@ -199,7 +235,10 @@ export default {
case 'Escape': case 'Escape':
this.onEscapeKey(event); this.onEscapeKey(event);
break;
case 'Tab':
this.onTabKey(event);
break; break;
default: default:
@ -235,11 +274,30 @@ export default {
event.preventDefault(); event.preventDefault();
} }
}, },
onTabKey(event, pressedInInputText = false) {
if (!pressedInInputText) {
if (this.overlayVisible && this.hasFocusableElements()) {
DomHandler.focus(this.$refs.firstHiddenFocusableElementOnOverlay);
event.preventDefault();
} else {
if (this.focusedOptionIndex !== -1) {
this.onOptionSelect(event, this.visibleOptions[this.focusedOptionIndex]);
}
}
}
},
hasFocusableElements() {
return DomHandler.getFocusableElements(this.overlay, ':not([data-p-hidden-focusable="true"])').length > 0;
},
onOverlayEnter(el) { onOverlayEnter(el) {
ZIndexUtils.set('overlay', el, this.$primevue.config.zIndex.overlay); ZIndexUtils.set('overlay', el, this.$primevue.config.zIndex.overlay);
DomHandler.addStyles(el, { position: 'absolute', top: '0', left: '0' }); DomHandler.addStyles(el, { position: 'absolute', top: '0', left: '0' });
this.alignOverlay(); this.alignOverlay();
this.focus();
},
onOverlayAfterEnter() {
this.bindOutsideClickListener(); this.bindOutsideClickListener();
this.bindScrollListener(); this.bindScrollListener();
this.bindResizeListener(); this.bindResizeListener();
@ -256,6 +314,13 @@ export default {
onOverlayAfterLeave(el) { onOverlayAfterLeave(el) {
ZIndexUtils.clear(el); ZIndexUtils.clear(el);
}, },
focus() {
let focusableElements = DomHandler.getFocusableElements(this.overlay);
if (focusableElements && focusableElements.length > 0) {
focusableElements[0].focus();
}
},
alignOverlay() { alignOverlay() {
if (this.appendTo === 'self') { if (this.appendTo === 'self') {
DomHandler.relativePosition(this.overlay, this.$el); DomHandler.relativePosition(this.overlay, this.$el);