Skip to content

Commit 78f273d

Browse files
committed
Fix keyboard accessibility
Signed-off-by: Carl Schwan <carl@carlschwan.eu>
1 parent a750e85 commit 78f273d

6 files changed

Lines changed: 135780 additions & 21 deletions

File tree

js/notifications-main.js

Lines changed: 98065 additions & 3 deletions
Large diffs are not rendered by default.

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.

js/notifications-settings.js

Lines changed: 37412 additions & 3 deletions
Large diffs are not rendered by default.

js/notifications-settings.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.

src/App.vue

Lines changed: 77 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,14 @@
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">
2+
<HeaderMenu
3+
v-if="!shutdown"
4+
id="notifications"
5+
class="Notifications"
6+
exclude-click-outside-classes="popover"
7+
:open.sync="open"
8+
:aria-label="t('notifications', 'Notifications')"
9+
@open="onOpen">
10+
11+
<template #trigger>
1312
<Bell v-if="notifications.length === 0"
1413
:size="20"
1514
:title="t('notifications', 'Notifications')"
@@ -26,7 +25,7 @@
2625
<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" />
2726
<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" />
2827
</svg>
29-
</div>
28+
</template>
3029

3130
<!-- Notifications list content -->
3231
<div ref="container" class="notification-container">
@@ -72,7 +71,7 @@
7271
</EmptyContent>
7372
</transition>
7473
</div>
75-
</div>
74+
</HeaderMenu>
7675
</template>
7776

7877
<script>
@@ -88,6 +87,7 @@ import { listen } from '@nextcloud/notify_push'
8887
import Bell from 'vue-material-design-icons/Bell'
8988
import EmptyContent from '@nextcloud/vue/dist/Components/EmptyContent'
9089
import { getCapabilities } from '@nextcloud/capabilities'
90+
import HeaderMenu from './Components/HeaderMenu'
9191
9292
export default {
9393
name: 'App',
@@ -97,6 +97,7 @@ export default {
9797
Close,
9898
Bell,
9999
EmptyContent,
100+
HeaderMenu,
100101
Notification,
101102
},
102103
@@ -121,6 +122,8 @@ export default {
121122
/** @type {number|null} */
122123
interval: null,
123124
pushEndpoints: null,
125+
126+
open: false,
124127
}
125128
},
126129
@@ -183,6 +186,9 @@ export default {
183186
},
184187
185188
methods: {
189+
onOpen() {
190+
this.requestWebNotificationPermissions()
191+
},
186192
handleNetworkOffline() {
187193
console.debug('Network is offline, slowing down pollingInterval to ' + this.pollIntervalBase * 10)
188194
this._setPollingInterval(this.pollIntervalBase * 10)
@@ -449,4 +455,62 @@ export default {
449455
width: 100%;
450456
}
451457
458+
.header-menu {
459+
&__trigger {
460+
display: flex;
461+
align-items: center;
462+
justify-content: center;
463+
width: 50px;
464+
height: 44px;
465+
margin: 2px 0;
466+
padding: 0;
467+
cursor: pointer;
468+
opacity: .85;
469+
}
470+
471+
&--opened &__trigger,
472+
&__trigger:hover,
473+
&__trigger:focus,
474+
&__trigger:active {
475+
opacity: 1;
476+
}
477+
478+
&__trigger:focus-visible {
479+
outline: none;
480+
}
481+
482+
&__wrapper {
483+
position: fixed;
484+
z-index: 2000;
485+
top: 50px;
486+
right: 0;
487+
box-sizing: border-box;
488+
margin: 0;
489+
border-radius: 0 0 var(--border-radius) var(--border-radius);
490+
background-color: var(--color-main-background);
491+
492+
filter: drop-shadow(0 1px 5px var(--color-box-shadow));
493+
}
494+
495+
&__carret {
496+
position: absolute;
497+
z-index: 2001; // Because __wrapper is 2000.
498+
left: calc(50% - 10px);
499+
bottom: 0;
500+
width: 0;
501+
height: 0;
502+
content: ' ';
503+
pointer-events: none;
504+
border: 10px solid transparent;
505+
border-bottom-color: var(--color-main-background);
506+
}
507+
508+
&__content {
509+
overflow: auto;
510+
width: 350px;
511+
max-width: 100vw;
512+
min-height: calc(44px * 1.5);
513+
max-height: calc(100vh - 50px * 2);
514+
}
515+
}
452516
</style>

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>

0 commit comments

Comments
 (0)