Skip to content

Commit

Permalink
Fixed #3236 - Improve ConfirmPopup implementation for Accessibility
Browse files Browse the repository at this point in the history
  • Loading branch information
tugcekucukoglu committed Nov 11, 2022
1 parent 0268bf9 commit aa51413
Show file tree
Hide file tree
Showing 2 changed files with 175 additions and 5 deletions.
58 changes: 53 additions & 5 deletions src/components/confirmpopup/ConfirmPopup.vue
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<template>
<Portal>
<transition name="p-confirm-popup" @enter="onEnter" @leave="onLeave" @after-leave="onAfterLeave">
<div v-if="visible" :ref="containerRef" :class="containerClass" v-bind="$attrs" @click="onOverlayClick">
<div v-if="visible" :ref="containerRef" v-focustrap role="alertdialog" :class="containerClass" :aria-modal="visible" @click="onOverlayClick" @keydown="onOverlayKeydown" v-bind="$attrs">
<template v-if="!$slots.message">
<div class="p-confirm-popup-content">
<i :class="iconClass" />
Expand All @@ -10,20 +10,21 @@
</template>
<component v-else :is="$slots.message" :message="confirmation"></component>
<div class="p-confirm-popup-footer">
<CPButton :label="rejectLabel" :icon="rejectIcon" :class="rejectClass" @click="reject()" />
<CPButton :label="acceptLabel" :icon="acceptIcon" :class="acceptClass" @click="accept()" autofocus />
<CPButton :label="rejectLabel" :icon="rejectIcon" :class="rejectClass" @click="reject()" @keydown="onRejectKeydown" />
<CPButton :label="acceptLabel" :icon="acceptIcon" :class="acceptClass" @click="accept()" @keydown="onAcceptKeydown" autofocus />
</div>
</div>
</transition>
</Portal>
</template>

<script>
import Button from 'primevue/button';
import ConfirmationEventBus from 'primevue/confirmationeventbus';
import { ConnectedOverlayScrollHandler, DomHandler, ZIndexUtils } from 'primevue/utils';
import FocusTrap from 'primevue/focustrap';
import OverlayEventBus from 'primevue/overlayeventbus';
import Button from 'primevue/button';
import Portal from 'primevue/portal';
import { ConnectedOverlayScrollHandler, DomHandler, ZIndexUtils } from 'primevue/utils';
export default {
name: 'ConfirmPopup',
Expand Down Expand Up @@ -53,6 +54,11 @@ export default {
if (options.group === this.group) {
this.confirmation = options;
this.target = options.target;
if (this.confirmation.onShow) {
this.confirmation.onShow();
}
this.visible = true;
}
};
Expand Down Expand Up @@ -101,7 +107,29 @@ export default {
this.visible = false;
},
onHide() {
if (this.confirmation.onHide) {
this.confirmation.onHide();
}
this.visible = false;
},
onAcceptKeydown(event) {
if (event.code === 'Space' || event.code === 'Enter') {
this.accept();
DomHandler.focus(this.target);
event.preventDefault();
}
},
onRejectKeydown(event) {
if (event.code === 'Space' || event.code === 'Enter') {
this.reject();
DomHandler.focus(this.target);
event.preventDefault();
}
},
onEnter(el) {
this.focus();
this.bindOutsideClickListener();
this.bindScrollListener();
this.bindResizeListener();
Expand Down Expand Up @@ -137,6 +165,10 @@ export default {
if (!this.outsideClickListener) {
this.outsideClickListener = (event) => {
if (this.visible && this.container && !this.container.contains(event.target) && !this.isTargetClicked(event)) {
if (this.confirmation.onHide) {
this.confirmation.onHide();
}
this.visible = false;
} else {
this.alignOverlay();
Expand Down Expand Up @@ -185,6 +217,13 @@ export default {
this.resizeListener = null;
}
},
focus() {
let focusTarget = this.container.querySelector('[autofocus]');
if (focusTarget) {
focusTarget.focus();
}
},
isTargetClicked(event) {
return this.target && (this.target === event.target || this.target.contains(event.target));
},
Expand All @@ -196,6 +235,12 @@ export default {
originalEvent: event,
target: this.target
});
},
onOverlayKeydown(event) {
if (event.code === 'Escape') {
ConfirmationEventBus.emit('close', this.closeListener);
DomHandler.focus(this.target);
}
}
},
computed: {
Expand Down Expand Up @@ -236,6 +281,9 @@ export default {
components: {
CPButton: Button,
Portal: Portal
},
directives: {
focustrap: FocusTrap
}
};
</script>
Expand Down
122 changes: 122 additions & 0 deletions src/views/confirmpopup/ConfirmPopupDoc.vue
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,12 @@ export default {
},
reject: () => {
//callback to execute when user rejects the action
},
onShow: () => {
//callback to execute when popup is shown
},
onHide: () => {
//callback to execute when popup is hidden
}
});
},
Expand Down Expand Up @@ -73,6 +79,12 @@ export default defineComponent({
},
reject: () => {
//callback to execute when user rejects the action
},
onShow: () => {
//callback to execute when popup is shown
},
onHide: () => {
//callback to execute when popup is hidden
}
});
}
Expand Down Expand Up @@ -160,6 +172,18 @@ export default {
<td>null</td>
<td>Callback to execute when action is rejected.</td>
</tr>
<tr>
<td>onShow</td>
<td>Function</td>
<td>null</td>
<td>Callback to execute when popup is shown.</td>
</tr>
<tr>
<td>onHide</td>
<td>Function</td>
<td>null</td>
<td>Callback to execute when popup is hidden.</td>
</tr>
<tr>
<td>acceptLabel</td>
<td>string</td>
Expand Down Expand Up @@ -301,6 +325,104 @@ export default {
</table>
</div>

<h5>Accessibility</h5>
<h6>Screen Reader</h6>
<p>
ConfirmPopup component uses <i>alertdialog</i> role and since any attribute is passed to the root element you may define attributes like <i>aria-label</i> or <i>aria-labelledby</i> to describe the popup contents. In addition
<i>aria-modal</i> is added since focus is kept within the popup.
</p>
<p>
When <i>require</i> method of the <i>$confirm</i> instance is used and a trigger is passed as a parameter, ConfirmDialog adds <i>aria-expanded</i> state attribute and <i>aria-controls</i> to the trigger so that the relation between the
trigger and the dialog is defined.
</p>

<pre v-code><code>
&lt;ConfirmPopup id="confirm" aria-label="popup" /&gt;

&lt;Button @click="openPopup($event)" label="Confirm" id="confirmButton" :aria-expanded="isVisible" :aria-controls="isVisible ? 'confirm' : null" /&gt;

</code></pre>

<pre v-code.script><code>
setup() {
const confirm = useConfirm();
const isVisible = ref(false);

const openPopup = (event) => {
confirm.require({
target: event.currentTarget,
message: 'Are you sure you want to proceed?',
header: 'Confirmation',
onShow: () => {
isVisible.value = true;
},
onHide: () => {
isVisible.value = false;
}
});
}

return {isVisible, openPopup};
}

</code></pre>

<h6>Overlay 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>tab</i>
</td>
<td>Moves focus to the next the focusable element within the popup.</td>
</tr>
<tr>
<td><i>shift</i> + <i>tab</i></td>
<td>Moves focus to the previous the focusable element within the popup.</td>
</tr>
<tr>
<td>
<i>escape</i>
</td>
<td>Closes the popup and moves focus to the trigger.</td>
</tr>
</tbody>
</table>
</div>

<h6>Buttons 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>enter</i>
</td>
<td>Triggers the action, closes the popup and moves focus to the trigger.</td>
</tr>
<tr>
<td>
<i>space</i>
</td>
<td>Triggers the action, closes the popup and moves focus to the trigger.</td>
</tr>
</tbody>
</table>
</div>

<h5>Dependencies</h5>
<p>None.</p>
</AppDoc>
Expand Down

0 comments on commit aa51413

Please sign in to comment.