Skip to content

Commit fd88239

Browse files
committed
Fix keyboard accessibility
Signed-off-by: Carl Schwan <carl@carlschwan.eu>
1 parent 871e50e commit fd88239

File tree

8 files changed

+254
-29
lines changed

8 files changed

+254
-29
lines changed

js/notifications-main.js

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

js/notifications-main.js.LICENSE.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,8 @@
6868

6969
/*! For license information please see UserBubble.js.LICENSE.txt */
7070

71+
/*! For license information please see excludeClickOutsideClasses.js.LICENSE.txt */
72+
7173
/*! ieee754. BSD-3-Clause License. Feross Aboukhadijeh <https://feross.org/opensource> */
7274

7375
/**

js/notifications-main.js.map

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package-lock.json

Lines changed: 7 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
"@nextcloud/router": "^2.0.0",
2828
"@nextcloud/vue": "^5.3.1",
2929
"howler": "^2.2.3",
30+
"v-click-outside": "^3.2.0",
3031
"vue": "^2.7.4",
3132
"vue-material-design-icons": "^5.1.2",
3233
"vue-tooltip": "^0.1.0"

src/Components/HeaderMenu.vue

Lines changed: 224 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,224 @@
1+
<!--
2+
- @copyright Copyright (c) 2020 John Molakvoæ <skjnldsv@protonmail.com>
3+
-
4+
- @author John Molakvoæ <skjnldsv@protonmail.com>
5+
-
6+
- @license GNU AGPL version 3 or any later version
7+
-
8+
- This program is free software: you can redistribute it and/or modify
9+
- it under the terms of the GNU Affero General Public License as
10+
- published by the Free Software Foundation, either version 3 of the
11+
- License, or (at your option) any later version.
12+
-
13+
- This program is distributed in the hope that it will be useful,
14+
- but WITHOUT ANY WARRANTY; without even the implied warranty of
15+
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16+
- GNU Affero General Public License for more details.
17+
-
18+
- You should have received a copy of the GNU Affero General Public License
19+
- along with this program. If not, see <http://www.gnu.org/licenses/>.
20+
-
21+
-->
22+
<template>
23+
<div :id="id"
24+
v-click-outside="clickOutsideConfig"
25+
:class="{ 'header-menu--opened': opened }"
26+
class="header-menu">
27+
<a class="header-menu__trigger"
28+
href="#"
29+
:aria-label="ariaLabel"
30+
:aria-controls="`header-menu-${id}`"
31+
:aria-expanded="opened"
32+
aria-haspopup="menu"
33+
@click.prevent="toggleMenu">
34+
<slot name="trigger" />
35+
</a>
36+
<div v-show="opened" class="header-menu__carret" />
37+
<div v-show="opened"
38+
:id="`header-menu-${id}`"
39+
class="header-menu__wrapper"
40+
role="menu">
41+
<div class="header-menu__content">
42+
<slot />
43+
</div>
44+
</div>
45+
</div>
46+
</template>
47+
48+
<script>
49+
import { directive as ClickOutside } from 'v-click-outside'
50+
import excludeClickOutsideClasses from '@nextcloud/vue/dist/Mixins/excludeClickOutsideClasses'
51+
52+
export default {
53+
name: 'HeaderMenu',
54+
55+
directives: {
56+
ClickOutside,
57+
},
58+
59+
mixins: [
60+
excludeClickOutsideClasses,
61+
],
62+
63+
props: {
64+
id: {
65+
type: String,
66+
required: true,
67+
},
68+
ariaLabel: {
69+
type: String,
70+
default: '',
71+
},
72+
open: {
73+
type: Boolean,
74+
default: false,
75+
},
76+
},
77+
78+
data() {
79+
return {
80+
opened: this.open,
81+
clickOutsideConfig: {
82+
handler: this.closeMenu,
83+
middleware: this.clickOutsideMiddleware,
84+
},
85+
}
86+
},
87+
88+
watch: {
89+
open(newVal) {
90+
this.opened = newVal
91+
this.$nextTick(() => {
92+
if (this.opened) {
93+
this.openMenu()
94+
} else {
95+
this.closeMenu()
96+
}
97+
})
98+
},
99+
},
100+
101+
mounted() {
102+
document.addEventListener('keydown', this.onKeyDown)
103+
},
104+
beforeDestroy() {
105+
document.removeEventListener('keydown', this.onKeyDown)
106+
},
107+
108+
methods: {
109+
/**
110+
* Toggle the current menu open state
111+
*/
112+
toggleMenu() {
113+
// Toggling current state
114+
if (!this.opened) {
115+
this.openMenu()
116+
} else {
117+
this.closeMenu()
118+
}
119+
},
120+
121+
/**
122+
* Close the current menu
123+
*/
124+
closeMenu() {
125+
if (!this.opened) {
126+
return
127+
}
128+
129+
this.opened = false
130+
this.$emit('close')
131+
this.$emit('update:open', false)
132+
},
133+
134+
/**
135+
* Open the current menu
136+
*/
137+
openMenu() {
138+
if (this.opened) {
139+
return
140+
}
141+
142+
this.opened = true
143+
this.$emit('open')
144+
this.$emit('update:open', true)
145+
},
146+
147+
onKeyDown(event) {
148+
// If opened and escape pressed, close
149+
if (event.key === 'Escape' && this.opened) {
150+
event.preventDefault()
151+
152+
/** user cancelled the menu by pressing escape */
153+
this.$emit('cancel')
154+
155+
/** we do NOT fire a close event to differentiate cancel and close */
156+
this.opened = false
157+
this.$emit('update:open', false)
158+
}
159+
},
160+
},
161+
}
162+
</script>
163+
164+
<style lang="scss" scoped>
165+
.header-menu {
166+
&__trigger {
167+
display: flex;
168+
align-items: center;
169+
justify-content: center;
170+
width: 50px;
171+
height: 44px;
172+
margin: 2px 0;
173+
padding: 0;
174+
cursor: pointer;
175+
opacity: .85;
176+
}
177+
178+
&--opened &__trigger,
179+
&__trigger:hover,
180+
&__trigger:focus,
181+
&__trigger:active {
182+
opacity: 1;
183+
}
184+
185+
&__trigger:focus-visible {
186+
outline: none;
187+
}
188+
189+
&__wrapper {
190+
position: fixed;
191+
z-index: 2000;
192+
top: 50px;
193+
right: 0;
194+
box-sizing: border-box;
195+
margin: 0;
196+
border-radius: 0 0 var(--border-radius) var(--border-radius);
197+
background-color: var(--color-main-background);
198+
199+
filter: drop-shadow(0 1px 5px var(--color-box-shadow));
200+
}
201+
202+
&__carret {
203+
position: absolute;
204+
z-index: 2001; // Because __wrapper is 2000.
205+
left: calc(50% - 10px);
206+
bottom: 0;
207+
width: 0;
208+
height: 0;
209+
content: ' ';
210+
pointer-events: none;
211+
border: 10px solid transparent;
212+
border-bottom-color: var(--color-main-background);
213+
}
214+
215+
&__content {
216+
overflow: auto;
217+
width: 350px;
218+
max-width: 100vw;
219+
min-height: calc(44px * 1.5);
220+
max-height: calc(100vh - 50px * 2);
221+
}
222+
}
223+
224+
</style>

src/NotificationsApp.vue

Lines changed: 17 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,12 @@
11
<template>
2-
<div v-if="!shutdown" class="notifications">
3-
<div ref="button"
4-
class="notifications-button menutoggle"
5-
:class="{ hasNotifications: notifications.length }"
6-
tabindex="0"
7-
role="button"
8-
:aria-label="t('notifications', 'Notifications')"
9-
aria-haspopup="true"
10-
aria-controls="notification-container"
11-
aria-expanded="false"
12-
@click="requestWebNotificationPermissions"
13-
@keydown.enter="requestWebNotificationPermissions">
2+
<HeaderMenu v-if="!shutdown"
3+
id="notifications"
4+
class="Notifications"
5+
exclude-click-outside-classes="popover"
6+
:open.sync="open"
7+
:aria-label="t('notifications', 'Notifications')"
8+
@open="onOpen">
9+
<template #trigger>
1410
<Bell v-if="notifications.length === 0"
1511
:size="20"
1612
:title="t('notifications', 'Notifications')"
@@ -27,7 +23,7 @@
2723
<path d="M 19,11.79 C 18.5,11.92 18,12 17.5,12 14.47,12 12,9.53 12,6.5 12,5.03 12.58,3.7 13.5,2.71 13.15,2.28 12.61,2 12,2 10.9,2 10,2.9 10,4 V 4.29 C 7.03,5.17 5,7.9 5,11 v 6 l -2,2 v 1 H 21 V 19 L 19,17 V 11.79 M 12,23 c 1.11,0 2,-0.89 2,-2 h -4 c 0,1.11 0.9,2 2,2 z" />
2824
<path :class="isRedThemed ? 'notification__dot--white' : ''" class="notification__dot" d="M 21,6.5 C 21,8.43 19.43,10 17.5,10 15.57,10 14,8.43 14,6.5 14,4.57 15.57,3 17.5,3 19.43,3 21,4.57 21,6.5" />
2925
</svg>
30-
</div>
26+
</template>
3127

3228
<!-- Notifications list content -->
3329
<div ref="container" class="notification-container">
@@ -71,7 +67,7 @@
7167
</EmptyContent>
7268
</transition>
7369
</div>
74-
</div>
70+
</HeaderMenu>
7571
</template>
7672

7773
<script>
@@ -87,6 +83,7 @@ import { listen } from '@nextcloud/notify_push'
8783
import Bell from 'vue-material-design-icons/Bell'
8884
import EmptyContent from '@nextcloud/vue/dist/Components/EmptyContent'
8985
import { getCapabilities } from '@nextcloud/capabilities'
86+
import HeaderMenu from './Components/HeaderMenu'
9087
9188
export default {
9289
name: 'NotificationsApp',
@@ -96,6 +93,7 @@ export default {
9693
Close,
9794
Bell,
9895
EmptyContent,
96+
HeaderMenu,
9997
Notification,
10098
},
10199
@@ -120,6 +118,8 @@ export default {
120118
/** @type {number|null} */
121119
interval: null,
122120
pushEndpoints: null,
121+
122+
open: false,
123123
}
124124
},
125125
@@ -182,6 +182,9 @@ export default {
182182
},
183183
184184
methods: {
185+
onOpen() {
186+
this.requestWebNotificationPermissions()
187+
},
185188
handleNetworkOffline() {
186189
console.debug('Network is offline, slowing down pollingInterval to ' + this.pollIntervalBase * 10)
187190
this._setPollingInterval(this.pollIntervalBase * 10)
@@ -447,5 +450,4 @@ export default {
447450
.list-leave-active {
448451
width: 100%;
449452
}
450-
451453
</style>

src/styles/styles.scss

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -59,11 +59,6 @@ svg {
5959
}
6060

6161
.notification-container {
62-
display: none;
63-
right: 13px;
64-
width: 350px;
65-
max-width: 90%;
66-
6762
.notification-wrapper {
6863
display: flex;
6964
flex-direction: column;

0 commit comments

Comments
 (0)