Ver código fonte

Merge pull request #36203 from nextcloud/enh/a11y-search-menu

Port global search menu to focus trapped NcHeaderMenu
tags/v26.0.0beta1
John Molakvoæ 1 ano atrás
pai
commit
eb40c069e1
Nenhuma conta vinculada ao e-mail do autor do commit

+ 0
- 238
core/src/components/HeaderMenu.vue Ver arquivo

@@ -1,238 +0,0 @@
<!--
- @copyright Copyright (c) 2020 John Molakvoæ <skjnldsv@protonmail.com>
-
- @author John Molakvoæ <skjnldsv@protonmail.com>
-
- @license GNU AGPL version 3 or any later version
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as
- published by the Free Software Foundation, either version 3 of the
- License, or (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see <http://www.gnu.org/licenses/>.
-
-->
<template>
<div :id="id"
v-click-outside="clickOutsideConfig"
:class="{ 'header-menu--opened': opened }"
class="header-menu">
<a class="header-menu__trigger"
href="#"
:aria-label="ariaLabel"
:aria-controls="`header-menu-${id}`"
:aria-expanded="opened.toString()"
@click.prevent="toggleMenu">
<slot name="trigger" />
</a>
<div v-show="opened" class="header-menu__carret" />
<div v-show="opened"
:id="`header-menu-${id}`"
class="header-menu__wrapper"
role="menu"
@focusout="handleFocusOut">
<div class="header-menu__content">
<slot />
</div>
</div>
</div>
</template>

<script>
import { directive as ClickOutside } from 'v-click-outside'
import excludeClickOutsideClasses from '@nextcloud/vue/dist/Mixins/excludeClickOutsideClasses'

export default {
name: 'HeaderMenu',

directives: {
ClickOutside,
},

mixins: [
excludeClickOutsideClasses,
],

props: {
id: {
type: String,
required: true,
},
ariaLabel: {
type: String,
default: '',
},
open: {
type: Boolean,
default: false,
},
},

data() {
return {
opened: this.open,
clickOutsideConfig: {
handler: this.closeMenu,
middleware: this.clickOutsideMiddleware,
},
shortcutsDisabled: OCP.Accessibility.disableKeyboardShortcuts(),
}
},

watch: {
open(newVal) {
this.opened = newVal
this.$nextTick(() => {
if (this.opened) {
this.openMenu()
} else {
this.closeMenu()
}
})
},
},

mounted() {
document.addEventListener('keydown', this.onKeyDown)
},
beforeDestroy() {
document.removeEventListener('keydown', this.onKeyDown)
},

methods: {
/**
* Toggle the current menu open state
*/
toggleMenu() {
// Toggling current state
if (!this.opened) {
this.openMenu()
} else {
this.closeMenu()
}
},

/**
* Close the current menu
*/
closeMenu() {
if (!this.opened) {
return
}

this.opened = false
this.$emit('close')
this.$emit('update:open', false)
},

/**
* Open the current menu
*/
openMenu() {
if (this.opened) {
return
}

this.opened = true
this.$emit('open')
this.$emit('update:open', true)
},

onKeyDown(event) {
if (this.shortcutsDisabled) {
return
}

// If opened and escape pressed, close
if (event.key === 'Escape' && this.opened) {
event.preventDefault()

/** user cancelled the menu by pressing escape */
this.$emit('cancel')

/** we do NOT fire a close event to differentiate cancel and close */
this.opened = false
this.$emit('update:open', false)
}
},

handleFocusOut(event) {
if (!event.currentTarget.contains(event.relatedTarget)) {
this.closeMenu()
}
},
},
}
</script>

<style lang="scss" scoped>
$externalMargin: 8px;

.header-menu {
&__trigger {
display: flex;
align-items: center;
justify-content: center;
width: 50px;
height: 44px;
margin: 2px 0;
padding: 0;
cursor: pointer;
opacity: .85;
}

&--opened &__trigger,
&__trigger:hover,
&__trigger:focus,
&__trigger:active {
opacity: 1;
}

&__trigger:focus-visible {
outline: none;
}

&__wrapper {
position: fixed;
z-index: 2000;
top: 50px;
right: 0;
box-sizing: border-box;
margin: 0 $externalMargin;
border-radius: 0 0 var(--border-radius) var(--border-radius);
background-color: var(--color-main-background);
filter: drop-shadow(0 1px 5px var(--color-box-shadow));
padding: 8px;
border-radius: var(--border-radius-large);
}

&__carret {
position: absolute;
z-index: 2001; // Because __wrapper is 2000.
left: calc(50% - 10px);
bottom: 0;
width: 0;
height: 0;
content: ' ';
pointer-events: none;
border: 10px solid transparent;
border-bottom-color: var(--color-main-background);
}

&__content {
overflow: auto;
width: 350px;
max-width: calc(100vw - 2 * $externalMargin);
min-height: calc(44px * 1.5);
max-height: calc(100vh - 50px * 2);
}
}

</style>

+ 16
- 14
core/src/views/UnifiedSearch.vue Ver arquivo

@@ -20,7 +20,7 @@
-
-->
<template>
<HeaderMenu id="unified-search"
<NcHeaderMenu id="unified-search"
class="unified-search"
exclude-click-outside-classes="popover"
:open.sync="open"
@@ -150,24 +150,26 @@
</li>
</ul>
</template>
</HeaderMenu>
</NcHeaderMenu>
</template>

<script>
import debounce from 'debounce'
import { emit, subscribe, unsubscribe } from '@nextcloud/event-bus'
import { minSearchLength, getTypes, search, defaultLimit, regexFilterIn, regexFilterNot, enableLiveSearch } from '../services/UnifiedSearchService'
import { showError } from '@nextcloud/dialogs'

import NcActionButton from '@nextcloud/vue/dist/Components/NcActionButton'
import NcActions from '@nextcloud/vue/dist/Components/NcActions'
import debounce from 'debounce'
import NcEmptyContent from '@nextcloud/vue/dist/Components/NcEmptyContent'
import NcHighlight from '@nextcloud/vue/dist/Components/NcHighlight'
import Magnify from 'vue-material-design-icons/Magnify'
import NcActionButton from '@nextcloud/vue/dist/Components/NcActionButton.js'
import NcActions from '@nextcloud/vue/dist/Components/NcActions.js'
import NcEmptyContent from '@nextcloud/vue/dist/Components/NcEmptyContent.js'
import NcHeaderMenu from '@nextcloud/vue/dist/Components/NcHeaderMenu.js'
import NcHighlight from '@nextcloud/vue/dist/Components/NcHighlight.js'

import Magnify from 'vue-material-design-icons/Magnify.vue'

import HeaderMenu from '../components/HeaderMenu'
import SearchResult from '../components/UnifiedSearch/SearchResult'
import SearchResultPlaceholders from '../components/UnifiedSearch/SearchResultPlaceholders'
import SearchResult from '../components/UnifiedSearch/SearchResult.vue'
import SearchResultPlaceholders from '../components/UnifiedSearch/SearchResultPlaceholders.vue'

import { minSearchLength, getTypes, search, defaultLimit, regexFilterIn, regexFilterNot, enableLiveSearch } from '../services/UnifiedSearchService.js'

const REQUEST_FAILED = 0
const REQUEST_OK = 1
@@ -177,12 +179,12 @@ export default {
name: 'UnifiedSearch',

components: {
Magnify,
NcActionButton,
NcActions,
NcEmptyContent,
HeaderMenu,
NcHeaderMenu,
NcHighlight,
Magnify,
SearchResult,
SearchResultPlaceholders,
},

+ 2
- 2
dist/core-common.js
Diferenças do arquivo suprimidas por serem muito extensas
Ver arquivo


+ 0
- 2
dist/core-common.js.LICENSE.txt Ver arquivo

@@ -392,8 +392,6 @@ object-assign

/*! For license information please see NcTextField.js.LICENSE.txt */

/*! For license information please see excludeClickOutsideClasses.js.LICENSE.txt */

/*! For license information please see index.module.js.LICENSE.txt */

/*! Hammer.JS - v2.0.7 - 2016-04-22

+ 1
- 1
dist/core-common.js.map
Diferenças do arquivo suprimidas por serem muito extensas
Ver arquivo


+ 2
- 2
dist/core-unified-search.js
Diferenças do arquivo suprimidas por serem muito extensas
Ver arquivo


+ 1
- 1
dist/core-unified-search.js.map
Diferenças do arquivo suprimidas por serem muito extensas
Ver arquivo


Carregando…
Cancelar
Salvar