mirror of
https://github.com/HabitRPG/habitica.git
synced 2025-12-19 15:48:04 +01:00
Issue 11450 Adding keyboard accessibility to task board controls and purchases (#12363)
* intial draft adding keyboard accessibility to task board controls * cleanup * finish adding keyboard accessibility for task dropdown, rewards * add notEnough conditions to disable purchase button * fix(lint): remove console.log from buy modal * add missing comma, use focus-within instead of focus-visible * missed one more focus visible * override browser default focus styling * move focus styling to tasks only * add rounded border * fix element height on focus, rounded borders * fix dropdown margin to avoid element resizing * styling updates on focus * fix spacing around task checklist * fix border around dropdown item * remove spacing that made tasks with notes jump * keep disabled habit control styling when not focused * revert unintended spacing Co-authored-by: Matteo Pagliazzi <matteopagliazzi@gmail.com>
This commit is contained in:
@@ -295,7 +295,10 @@
|
|||||||
&-control {
|
&-control {
|
||||||
&-bg { background: $gray-600; }
|
&-bg { background: $gray-600; }
|
||||||
&-inner {
|
&-inner {
|
||||||
border: 1px solid $gray-300;
|
&:not(:focus) {
|
||||||
|
border: 1px solid $gray-300 !important;
|
||||||
|
}
|
||||||
|
|
||||||
opacity: 0.75;
|
opacity: 0.75;
|
||||||
|
|
||||||
.negative, .positive {
|
.negative, .positive {
|
||||||
|
|||||||
@@ -8,6 +8,8 @@
|
|||||||
v-if="withPin"
|
v-if="withPin"
|
||||||
class="badge-dialog"
|
class="badge-dialog"
|
||||||
@click.prevent.stop="togglePinned()"
|
@click.prevent.stop="togglePinned()"
|
||||||
|
@keypress.enter.prevent.stop="togglePinned()"
|
||||||
|
tabindex="0"
|
||||||
>
|
>
|
||||||
<pin-badge
|
<pin-badge
|
||||||
:pinned="isPinned"
|
:pinned="isPinned"
|
||||||
@@ -18,6 +20,8 @@
|
|||||||
class="svg-icon icon-12 close-icon"
|
class="svg-icon icon-12 close-icon"
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
@click="hideDialog()"
|
@click="hideDialog()"
|
||||||
|
@keypress.enter="hideDialog()"
|
||||||
|
tabindex="0"
|
||||||
v-html="icons.close"
|
v-html="icons.close"
|
||||||
></span>
|
></span>
|
||||||
</div>
|
</div>
|
||||||
@@ -145,10 +149,13 @@
|
|||||||
v-else
|
v-else
|
||||||
class="btn btn-primary"
|
class="btn btn-primary"
|
||||||
:disabled="item.key === 'gem' && gemsLeft === 0 ||
|
:disabled="item.key === 'gem' && gemsLeft === 0 ||
|
||||||
attemptingToPurchaseMoreGemsThanAreLeft || numberInvalid || item.locked"
|
attemptingToPurchaseMoreGemsThanAreLeft || numberInvalid || item.locked ||
|
||||||
|
!preventHealthPotion ||
|
||||||
|
!enoughCurrency(getPriceClass(), item.value * selectedAmountToBuy)"
|
||||||
:class="{'notEnough': !preventHealthPotion ||
|
:class="{'notEnough': !preventHealthPotion ||
|
||||||
!enoughCurrency(getPriceClass(), item.value * selectedAmountToBuy)}"
|
!enoughCurrency(getPriceClass(), item.value * selectedAmountToBuy)}"
|
||||||
@click="buyItem()"
|
@click="buyItem()"
|
||||||
|
tabindex="0"
|
||||||
>
|
>
|
||||||
{{ $t('buyNow') }}
|
{{ $t('buyNow') }}
|
||||||
</button>
|
</button>
|
||||||
@@ -289,6 +296,10 @@
|
|||||||
margin-top: 24px;
|
margin-top: 24px;
|
||||||
margin-bottom: 24px;
|
margin-bottom: 24px;
|
||||||
min-width: 6rem;
|
min-width: 6rem;
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
border: 2px solid black;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.balance {
|
.balance {
|
||||||
|
|||||||
@@ -4,6 +4,8 @@
|
|||||||
:id="itemId"
|
:id="itemId"
|
||||||
class="item-wrapper"
|
class="item-wrapper"
|
||||||
@click="click()"
|
@click="click()"
|
||||||
|
@keypress.enter="click()"
|
||||||
|
tabindex="0"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="item"
|
class="item"
|
||||||
@@ -63,7 +65,7 @@
|
|||||||
<b-popover
|
<b-popover
|
||||||
v-if="showPopover"
|
v-if="showPopover"
|
||||||
:target="itemId"
|
:target="itemId"
|
||||||
triggers="hover"
|
triggers="hover, focus"
|
||||||
:placement="popoverPosition"
|
:placement="popoverPosition"
|
||||||
>
|
>
|
||||||
<slot
|
<slot
|
||||||
|
|||||||
@@ -31,6 +31,8 @@
|
|||||||
class="filter small-text"
|
class="filter small-text"
|
||||||
:class="{active: activeFilter.label === filter}"
|
:class="{active: activeFilter.label === filter}"
|
||||||
@click="activateFilter(type, filter)"
|
@click="activateFilter(type, filter)"
|
||||||
|
@keypress.enter="activateFilter(type, filter)"
|
||||||
|
tabindex="0"
|
||||||
>
|
>
|
||||||
{{ $t(filter) }}
|
{{ $t(filter) }}
|
||||||
</div>
|
</div>
|
||||||
@@ -128,6 +130,7 @@
|
|||||||
<span
|
<span
|
||||||
class="badge-top"
|
class="badge-top"
|
||||||
@click.prevent.stop="togglePinned(ctx.item)"
|
@click.prevent.stop="togglePinned(ctx.item)"
|
||||||
|
@keypress.enter.prevent.stop="togglePinned(ctx.item)"
|
||||||
>
|
>
|
||||||
<pin-badge
|
<pin-badge
|
||||||
:pinned="ctx.item.pinned"
|
:pinned="ctx.item.pinned"
|
||||||
@@ -156,6 +159,10 @@
|
|||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.item:focus-within .badge-pin {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
.tasks-column {
|
.tasks-column {
|
||||||
min-height: 556px;
|
min-height: 556px;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,6 +38,9 @@
|
|||||||
}, controlClass.up.inner]"
|
}, controlClass.up.inner]"
|
||||||
@click="(isUser && task.up && (!task.group.approval.requested
|
@click="(isUser && task.up && (!task.group.approval.requested
|
||||||
|| task.group.approval.approved)) ? score('up') : null"
|
|| task.group.approval.approved)) ? score('up') : null"
|
||||||
|
@keypress.enter="(isUser && task.up && (!task.group.approval.requested
|
||||||
|
|| task.group.approval.approved)) ? score('up') : null"
|
||||||
|
tabindex="0"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
v-if="!isUser"
|
v-if="!isUser"
|
||||||
@@ -65,6 +68,9 @@
|
|||||||
:class="controlClass.inner"
|
:class="controlClass.inner"
|
||||||
@click="isUser && !task.group.approval.requested
|
@click="isUser && !task.group.approval.requested
|
||||||
? score(task.completed ? 'down' : 'up' ) : null"
|
? score(task.completed ? 'down' : 'up' ) : null"
|
||||||
|
@keypress.enter="isUser && !task.group.approval.requested
|
||||||
|
? score(task.completed ? 'down' : 'up' ) : null"
|
||||||
|
tabindex="0"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
v-if="!isUser"
|
v-if="!isUser"
|
||||||
@@ -92,6 +98,8 @@
|
|||||||
class="task-clickable-area"
|
class="task-clickable-area"
|
||||||
:class="{'task-clickable-area-user': isUser}"
|
:class="{'task-clickable-area-user': isUser}"
|
||||||
@click="edit($event, task)"
|
@click="edit($event, task)"
|
||||||
|
@keypress.enter="edit($event, task)"
|
||||||
|
tabindex="0"
|
||||||
>
|
>
|
||||||
<div class="d-flex justify-content-between">
|
<div class="d-flex justify-content-between">
|
||||||
<h3
|
<h3
|
||||||
@@ -103,6 +111,7 @@
|
|||||||
v-if="!isRunningYesterdailies && showOptions"
|
v-if="!isRunningYesterdailies && showOptions"
|
||||||
ref="taskDropdown"
|
ref="taskDropdown"
|
||||||
v-b-tooltip.hover.top="$t('options')"
|
v-b-tooltip.hover.top="$t('options')"
|
||||||
|
tabindex="0"
|
||||||
class="task-dropdown"
|
class="task-dropdown"
|
||||||
:right="task.type === 'reward'"
|
:right="task.type === 'reward'"
|
||||||
>
|
>
|
||||||
@@ -117,6 +126,8 @@
|
|||||||
v-if="showEdit"
|
v-if="showEdit"
|
||||||
ref="editTaskItem"
|
ref="editTaskItem"
|
||||||
class="dropdown-item edit-task-item"
|
class="dropdown-item edit-task-item"
|
||||||
|
tabindex="0"
|
||||||
|
@keypress.enter="edit($event, task)"
|
||||||
>
|
>
|
||||||
<span class="dropdown-icon-item">
|
<span class="dropdown-icon-item">
|
||||||
<span
|
<span
|
||||||
@@ -130,6 +141,8 @@
|
|||||||
v-if="isUser"
|
v-if="isUser"
|
||||||
class="dropdown-item"
|
class="dropdown-item"
|
||||||
@click="moveToTop"
|
@click="moveToTop"
|
||||||
|
tabindex="0"
|
||||||
|
@keypress.enter="moveToTop"
|
||||||
>
|
>
|
||||||
<span class="dropdown-icon-item">
|
<span class="dropdown-icon-item">
|
||||||
<span
|
<span
|
||||||
@@ -143,6 +156,8 @@
|
|||||||
v-if="isUser"
|
v-if="isUser"
|
||||||
class="dropdown-item"
|
class="dropdown-item"
|
||||||
@click="moveToBottom"
|
@click="moveToBottom"
|
||||||
|
tabindex="0"
|
||||||
|
@keypress.enter="moveToBottom"
|
||||||
>
|
>
|
||||||
<span class="dropdown-icon-item">
|
<span class="dropdown-icon-item">
|
||||||
<span
|
<span
|
||||||
@@ -156,6 +171,8 @@
|
|||||||
v-if="showDelete"
|
v-if="showDelete"
|
||||||
class="dropdown-item"
|
class="dropdown-item"
|
||||||
@click="destroy"
|
@click="destroy"
|
||||||
|
tabindex="0"
|
||||||
|
@keypress.enter="destroy"
|
||||||
>
|
>
|
||||||
<span class="dropdown-icon-item delete-task-item">
|
<span class="dropdown-icon-item delete-task-item">
|
||||||
<span
|
<span
|
||||||
@@ -185,7 +202,9 @@
|
|||||||
? 'expand': 'collapse'}Checklist`)"
|
? 'expand': 'collapse'}Checklist`)"
|
||||||
class="collapse-checklist mb-2 d-flex align-items-center expand-toggle"
|
class="collapse-checklist mb-2 d-flex align-items-center expand-toggle"
|
||||||
:class="{open: !task.collapseChecklist}"
|
:class="{open: !task.collapseChecklist}"
|
||||||
|
tabindex="0"
|
||||||
@click="collapseChecklist(task)"
|
@click="collapseChecklist(task)"
|
||||||
|
@keypress.enter="collapseChecklist(task)"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
v-once
|
v-once
|
||||||
@@ -207,10 +226,12 @@
|
|||||||
<input
|
<input
|
||||||
:id="`checklist-${item.id}-${random}`"
|
:id="`checklist-${item.id}-${random}`"
|
||||||
class="custom-control-input"
|
class="custom-control-input"
|
||||||
|
tabindex="0"
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
:checked="item.completed"
|
:checked="item.completed"
|
||||||
:disabled="castingSpell || !isUser"
|
:disabled="castingSpell || !isUser"
|
||||||
@change="toggleChecklistItem(item)"
|
@change="toggleChecklistItem(item)"
|
||||||
|
@keypress.enter="toggleChecklistItem(item)"
|
||||||
>
|
>
|
||||||
<label
|
<label
|
||||||
v-markdown="item.text"
|
v-markdown="item.text"
|
||||||
@@ -330,6 +351,9 @@
|
|||||||
}, controlClass.down.inner]"
|
}, controlClass.down.inner]"
|
||||||
@click="(isUser && task.down && (!task.group.approval.requested
|
@click="(isUser && task.down && (!task.group.approval.requested
|
||||||
|| task.group.approval.approved)) ? score('down') : null"
|
|| task.group.approval.approved)) ? score('down') : null"
|
||||||
|
@keypress.enter="(isUser && task.down && (!task.group.approval.requested
|
||||||
|
|| task.group.approval.approved)) ? score('down') : null"
|
||||||
|
tabindex="0"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
v-if="!isUser"
|
v-if="!isUser"
|
||||||
@@ -350,6 +374,8 @@
|
|||||||
class="right-control d-flex align-items-center justify-content-center reward-control"
|
class="right-control d-flex align-items-center justify-content-center reward-control"
|
||||||
:class="controlClass.bg"
|
:class="controlClass.bg"
|
||||||
@click="isUser ? score('down') : null"
|
@click="isUser ? score('down') : null"
|
||||||
|
@keypress.enter="isUser ? score('down') : null"
|
||||||
|
tabindex="0"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="svg-icon"
|
class="svg-icon"
|
||||||
@@ -373,6 +399,19 @@
|
|||||||
<!-- eslint-disable max-len -->
|
<!-- eslint-disable max-len -->
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@import '~@/assets/scss/colors.scss';
|
@import '~@/assets/scss/colors.scss';
|
||||||
|
.task-best-control-inner-habit:focus {
|
||||||
|
transition: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
*:focus {
|
||||||
|
outline: none;
|
||||||
|
transition: none;
|
||||||
|
border: $purple-400 solid 1px;
|
||||||
|
|
||||||
|
:not(task-best-control-inner-habit) { // round icon
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.control-bottom-box {
|
.control-bottom-box {
|
||||||
border-bottom-left-radius: 0 !important;
|
border-bottom-left-radius: 0 !important;
|
||||||
@@ -395,14 +434,16 @@
|
|||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
&:hover:not(.task-not-editable) {
|
&:hover:not(.task-not-editable),
|
||||||
|
&:focus-within:not(.task-not-editable) {
|
||||||
box-shadow: 0 1px 8px 0 rgba($black, 0.12), 0 4px 4px 0 rgba($black, 0.16);
|
box-shadow: 0 1px 8px 0 rgba($black, 0.12), 0 4px 4px 0 rgba($black, 0.16);
|
||||||
z-index: 11;
|
z-index: 11;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.task:not(.groupTask) {
|
.task:not(.groupTask) {
|
||||||
&:hover {
|
&:hover,
|
||||||
|
&:focus-within {
|
||||||
.left-control, .right-control, .task-content {
|
.left-control, .right-control, .task-content {
|
||||||
border-color: $purple-400;
|
border-color: $purple-400;
|
||||||
}
|
}
|
||||||
@@ -410,7 +451,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.task.groupTask {
|
.task.groupTask {
|
||||||
&:hover:not(.task-not-editable) {
|
&:hover:not(.task-not-editable),
|
||||||
|
&:focus-within:not(.task-not-editable) {
|
||||||
border: $purple-400 solid 1px;
|
border: $purple-400 solid 1px;
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
margin: -1px; // to counter the border width
|
margin: -1px; // to counter the border width
|
||||||
@@ -455,10 +497,16 @@
|
|||||||
.task-clickable-area {
|
.task-clickable-area {
|
||||||
padding: 7px 8px;
|
padding: 7px 8px;
|
||||||
padding-bottom: 0px;
|
padding-bottom: 0px;
|
||||||
|
border: transparent solid 1px;
|
||||||
|
|
||||||
&-user {
|
&-user {
|
||||||
padding-right: 0px;
|
padding-right: 0px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
border-radius: 2px;
|
||||||
|
border: $purple-400 solid 1px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.task-title + .task-dropdown ::v-deep .dropdown-menu {
|
.task-title + .task-dropdown ::v-deep .dropdown-menu {
|
||||||
@@ -482,6 +530,20 @@
|
|||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.task:focus-within ::v-deep .habitica-menu-dropdown .habitica-menu-dropdown-toggle {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task ::v-deep .habitica-menu-dropdown:focus-within {
|
||||||
|
opacity: 1;
|
||||||
|
border: $purple-400 solid 1px;
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task ::v-deep .habitica-menu-dropdown {
|
||||||
|
border: transparent solid 1px;
|
||||||
|
}
|
||||||
|
|
||||||
.task-clickable-area ::v-deep .habitica-menu-dropdown.open .habitica-menu-dropdown-toggle {
|
.task-clickable-area ::v-deep .habitica-menu-dropdown.open .habitica-menu-dropdown-toggle {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
|
|
||||||
@@ -494,20 +556,26 @@
|
|||||||
color: $purple-400 !important;
|
color: $purple-400 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.task-clickable-area ::v-deep .habitica-menu-dropdown .habitica-menu-dropdown-toggle:focus-within .svg-icon {
|
||||||
|
color: $purple-400 !important;
|
||||||
|
}
|
||||||
|
|
||||||
.task-dropdown {
|
.task-dropdown {
|
||||||
max-height: 16px;
|
max-height: 18px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.task-dropdown ::v-deep .dropdown-menu {
|
.task-dropdown ::v-deep .dropdown-menu {
|
||||||
.dropdown-item {
|
.dropdown-item {
|
||||||
cursor: pointer !important;
|
cursor: pointer !important;
|
||||||
transition: none;
|
transition: none;
|
||||||
|
border: transparent solid 1px;
|
||||||
|
|
||||||
* {
|
* {
|
||||||
transition: none;
|
transition: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:hover {
|
&:hover,
|
||||||
|
&:focus {
|
||||||
color: $purple-300;
|
color: $purple-300;
|
||||||
|
|
||||||
.svg-icon.push-to-top, .svg-icon.push-to-bottom {
|
.svg-icon.push-to-top, .svg-icon.push-to-bottom {
|
||||||
@@ -516,6 +584,11 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
border-radius: 2px;
|
||||||
|
border: $purple-400 solid 1px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -551,12 +624,8 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.checklist {
|
.checklist.isOpen {
|
||||||
&.isOpen {
|
margin-bottom: 2px;
|
||||||
margin-bottom: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
margin-top: -3px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.collapse-checklist {
|
.collapse-checklist {
|
||||||
@@ -567,8 +636,10 @@
|
|||||||
line-height: 1.2;
|
line-height: 1.2;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
color: $gray-200;
|
color: $gray-200;
|
||||||
|
border: transparent solid 1px;
|
||||||
|
|
||||||
&.open {
|
&:focus {
|
||||||
|
border: $purple-400 solid 1px;
|
||||||
}
|
}
|
||||||
|
|
||||||
span {
|
span {
|
||||||
@@ -706,6 +777,11 @@
|
|||||||
transition-duration: 0.15s;
|
transition-duration: 0.15s;
|
||||||
transition-property: border-color, background, color;
|
transition-property: border-color, background, color;
|
||||||
transition-timing-function: ease-in;
|
transition-timing-function: ease-in;
|
||||||
|
border: transparent solid 1px;
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
border: $purple-400 solid 1px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.left-control {
|
.left-control {
|
||||||
border-top-left-radius: 2px;
|
border-top-left-radius: 2px;
|
||||||
@@ -1012,7 +1088,6 @@ export default {
|
|||||||
},
|
},
|
||||||
edit (e, task) {
|
edit (e, task) {
|
||||||
if (this.isRunningYesterdailies || !this.showEdit) return;
|
if (this.isRunningYesterdailies || !this.showEdit) return;
|
||||||
|
|
||||||
const target = e.target || e.srcElement;
|
const target = e.target || e.srcElement;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|||||||
Reference in New Issue
Block a user