You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

HeaderMenu.vue 4.7KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238
  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.toString()"
  32. @click.prevent="toggleMenu">
  33. <slot name="trigger" />
  34. </a>
  35. <div v-show="opened" class="header-menu__carret" />
  36. <div v-show="opened"
  37. :id="`header-menu-${id}`"
  38. class="header-menu__wrapper"
  39. role="menu"
  40. @focusout="handleFocusOut">
  41. <div class="header-menu__content">
  42. <slot />
  43. </div>
  44. </div>
  45. </div>
  46. </template>
  47. <script>
  48. import { directive as ClickOutside } from 'v-click-outside'
  49. import excludeClickOutsideClasses from '@nextcloud/vue/dist/Mixins/excludeClickOutsideClasses'
  50. export default {
  51. name: 'HeaderMenu',
  52. directives: {
  53. ClickOutside,
  54. },
  55. mixins: [
  56. excludeClickOutsideClasses,
  57. ],
  58. props: {
  59. id: {
  60. type: String,
  61. required: true,
  62. },
  63. ariaLabel: {
  64. type: String,
  65. default: '',
  66. },
  67. open: {
  68. type: Boolean,
  69. default: false,
  70. },
  71. },
  72. data() {
  73. return {
  74. opened: this.open,
  75. clickOutsideConfig: {
  76. handler: this.closeMenu,
  77. middleware: this.clickOutsideMiddleware,
  78. },
  79. shortcutsDisabled: OCP.Accessibility.disableKeyboardShortcuts(),
  80. }
  81. },
  82. watch: {
  83. open(newVal) {
  84. this.opened = newVal
  85. this.$nextTick(() => {
  86. if (this.opened) {
  87. this.openMenu()
  88. } else {
  89. this.closeMenu()
  90. }
  91. })
  92. },
  93. },
  94. mounted() {
  95. document.addEventListener('keydown', this.onKeyDown)
  96. },
  97. beforeDestroy() {
  98. document.removeEventListener('keydown', this.onKeyDown)
  99. },
  100. methods: {
  101. /**
  102. * Toggle the current menu open state
  103. */
  104. toggleMenu() {
  105. // Toggling current state
  106. if (!this.opened) {
  107. this.openMenu()
  108. } else {
  109. this.closeMenu()
  110. }
  111. },
  112. /**
  113. * Close the current menu
  114. */
  115. closeMenu() {
  116. if (!this.opened) {
  117. return
  118. }
  119. this.opened = false
  120. this.$emit('close')
  121. this.$emit('update:open', false)
  122. },
  123. /**
  124. * Open the current menu
  125. */
  126. openMenu() {
  127. if (this.opened) {
  128. return
  129. }
  130. this.opened = true
  131. this.$emit('open')
  132. this.$emit('update:open', true)
  133. },
  134. onKeyDown(event) {
  135. if (this.shortcutsDisabled) {
  136. return
  137. }
  138. // If opened and escape pressed, close
  139. if (event.key === 'Escape' && this.opened) {
  140. event.preventDefault()
  141. /** user cancelled the menu by pressing escape */
  142. this.$emit('cancel')
  143. /** we do NOT fire a close event to differentiate cancel and close */
  144. this.opened = false
  145. this.$emit('update:open', false)
  146. }
  147. },
  148. handleFocusOut(event) {
  149. if (!event.currentTarget.contains(event.relatedTarget)) {
  150. this.closeMenu()
  151. }
  152. },
  153. },
  154. }
  155. </script>
  156. <style lang="scss" scoped>
  157. $externalMargin: 8px;
  158. .header-menu {
  159. &__trigger {
  160. display: flex;
  161. align-items: center;
  162. justify-content: center;
  163. width: 50px;
  164. height: 44px;
  165. margin: 2px 0;
  166. padding: 0;
  167. cursor: pointer;
  168. opacity: .85;
  169. }
  170. &--opened &__trigger,
  171. &__trigger:hover,
  172. &__trigger:focus,
  173. &__trigger:active {
  174. opacity: 1;
  175. }
  176. &__trigger:focus-visible {
  177. outline: none;
  178. }
  179. &__wrapper {
  180. position: fixed;
  181. z-index: 2000;
  182. top: 50px;
  183. right: 0;
  184. box-sizing: border-box;
  185. margin: 0 $externalMargin;
  186. border-radius: 0 0 var(--border-radius) var(--border-radius);
  187. background-color: var(--color-main-background);
  188. filter: drop-shadow(0 1px 5px var(--color-box-shadow));
  189. padding: 8px;
  190. border-radius: var(--border-radius-large);
  191. }
  192. &__carret {
  193. position: absolute;
  194. z-index: 2001; // Because __wrapper is 2000.
  195. left: calc(50% - 10px);
  196. bottom: 0;
  197. width: 0;
  198. height: 0;
  199. content: ' ';
  200. pointer-events: none;
  201. border: 10px solid transparent;
  202. border-bottom-color: var(--color-main-background);
  203. }
  204. &__content {
  205. overflow: auto;
  206. width: 350px;
  207. max-width: calc(100vw - 2 * $externalMargin);
  208. min-height: calc(44px * 1.5);
  209. max-height: calc(100vh - 50px * 2);
  210. }
  211. }
  212. </style>