@@ -134,10 +134,150 @@ emit('toggle-navigation', {
134134
135135</docs >
136136
137+ <script setup lang="ts">
138+ import type { FocusTrap } from ' focus-trap'
139+ import type { Slot } from ' vue'
140+
141+ import { emit , subscribe , unsubscribe } from ' @nextcloud/event-bus'
142+ import { createFocusTrap } from ' focus-trap'
143+ import { inject , onMounted , onUnmounted , ref , useTemplateRef , warn , watch } from ' vue'
144+ import NcAppNavigationList from ' ../NcAppNavigationList/index.js'
145+ import NcAppNavigationToggle from ' ./NcAppNavigationToggle.vue'
146+ import { useIsMobile } from ' ../../composables/useIsMobile/index.ts'
147+ import { getTrapStack } from ' ../../utils/focusTrap.ts'
148+
149+ defineProps <{
150+ /**
151+ * The aria label to describe the navigation
152+ */
153+ ariaLabel? : string
154+
155+ /**
156+ * aria-labelledby attribute to describe the navigation
157+ */
158+ ariaLabelledby? : string
159+ }>()
160+
161+ defineSlots <{
162+ /**
163+ * The main content of the navigation.
164+ * If no list is passed to the `#list` slot, stretched vertically.
165+ */
166+ default? : Slot
167+ /**
168+ * Footer for e.g. `NcAppNavigationSettings`
169+ */
170+ footer? : Slot
171+ /**
172+ * List for Navigation list items.
173+ * Stretched between the main content and the footer
174+ */
175+ list? : Slot
176+ /**
177+ * For in-app search you can pass a `NcAppNavigationSearch` component as the slot content.
178+ */
179+ search? : Slot
180+ }>()
181+
182+ let focusTrap: FocusTrap
183+ const setHasAppNavigation = inject <(v : boolean ) => void >(' NcContent:setHasAppNavigation' , () => warn (' NcAppNavigation is not mounted inside NcContent, this is probably an error.' ), false )
184+
185+ const appNavigationContainer = useTemplateRef (' appNavigationContainer' )
186+ const isMobile = useIsMobile ()
187+ const open = ref (! isMobile .value )
188+
189+ watch (isMobile , () => {
190+ open .value = ! isMobile .value
191+ })
192+
193+ watch (open , () => {
194+ toggleFocusTrap ()
195+ })
196+
197+ onMounted (() => {
198+ setHasAppNavigation (true )
199+ subscribe (' toggle-navigation' , toggleNavigationByEventBus )
200+ // Emit an event with the initial state of the navigation
201+ emit (' navigation-toggled' , {
202+ open: open .value ,
203+ })
204+
205+ focusTrap = createFocusTrap (appNavigationContainer .value ! , {
206+ allowOutsideClick: true ,
207+ fallbackFocus: appNavigationContainer .value ! ,
208+ trapStack: getTrapStack (),
209+ escapeDeactivates: false ,
210+ })
211+ toggleFocusTrap ()
212+ })
213+
214+ onUnmounted (() => {
215+ setHasAppNavigation (false )
216+ unsubscribe (' toggle-navigation' , toggleNavigationByEventBus )
217+ focusTrap .deactivate ()
218+ })
219+
220+ /**
221+ * Toggle the navigation
222+ *
223+ * @param state set the state instead of inverting the current one
224+ */
225+ function toggleNavigation(state ? : boolean ): void {
226+ // Early return if already in that state
227+ if (open .value === state ) {
228+ emit (' navigation-toggled' , {
229+ open: open .value ,
230+ })
231+ return
232+ }
233+
234+ open .value = state === undefined ? ! open .value : state
235+ const bodyStyles = getComputedStyle (document .body )
236+ const animationLength = parseInt (bodyStyles .getPropertyValue (' --animation-quick' )) || 100
237+
238+ setTimeout (() => {
239+ emit (' navigation-toggled' , {
240+ open: open .value ,
241+ })
242+ // We wait for 1.5 times the animation length to give the animation time to really finish.
243+ }, 1.5 * animationLength )
244+ }
245+
246+ /**
247+ * Handler for the event-bus navigation event.
248+ *
249+ * @param context - The event bus context
250+ * @param context.open - The new navigation open state
251+ */
252+ function toggleNavigationByEventBus({ open }: { open: boolean }): void {
253+ return toggleNavigation (open )
254+ }
255+
256+ /**
257+ * Activate focus trap if it is currently needed, otherwise deactivate
258+ */
259+ function toggleFocusTrap(): void {
260+ if (isMobile .value && open .value ) {
261+ focusTrap .activate ()
262+ } else {
263+ focusTrap .deactivate ()
264+ }
265+ }
266+
267+ /**
268+ * Handle hotkey for closing the navigation.
269+ */
270+ function handleEsc(): void {
271+ if (isMobile .value ) {
272+ toggleNavigation (false )
273+ }
274+ }
275+ </script >
276+
137277<template >
138278 <div ref =" appNavigationContainer"
139279 class =" app-navigation"
140- :class =" {'app-navigation--close ':!open }" >
280+ :class =" {'app-navigation--closed ':!open }" >
141281 <nav id =" app-navigation-vue"
142282 :aria-hidden =" open ? 'false' : 'true'"
143283 :aria-label =" ariaLabel || undefined"
@@ -146,167 +286,23 @@ emit('toggle-navigation', {
146286 :inert =" !open || undefined"
147287 @keydown.esc =" handleEsc" >
148288 <div class =" app-navigation__search" >
149- <!-- @slot For in-app search you can pass a `NcAppNavigationSearch` component as the slot content. -->
150289 <slot name =" search" />
151290 </div >
152291
153292 <div class =" app-navigation__body" :class =" { 'app-navigation__body--no-list': !$slots.list }" >
154- <!-- @slot The main content of the navigation. If no list is passed to the #list slot, stretched vertically. -->
155293 <slot />
156294 </div >
157295
158296 <NcAppNavigationList v-if =" $slots.list" class =" app-navigation__list" >
159- <!-- List for Navigation list items. Stretched between the main content and the footer -->
160297 <slot name =" list" />
161298 </NcAppNavigationList >
162299
163- <!-- @slot Footer for e.g. NcAppNavigationSettings -->
164300 <slot name =" footer" />
165301 </nav >
166- <NcAppNavigationToggle :open = " open " @update:open =" toggleNavigation" />
302+ <NcAppNavigationToggle :open @update:open =" toggleNavigation" />
167303 </div >
168304</template >
169305
170- <script >
171- import { useIsMobile } from ' ../../composables/useIsMobile/index.js'
172- import { getTrapStack } from ' ../../utils/focusTrap.ts'
173- import { emit , subscribe , unsubscribe } from ' @nextcloud/event-bus'
174- import { createFocusTrap } from ' focus-trap'
175-
176- import NcAppNavigationList from ' ../NcAppNavigationList/index.js'
177- import NcAppNavigationToggle from ' ../NcAppNavigationToggle/index.ts'
178- import { warn } from ' vue'
179-
180- export default {
181- name: ' NcAppNavigation' ,
182-
183- components: {
184- NcAppNavigationList,
185- NcAppNavigationToggle,
186- },
187-
188- // Injected from NcContent
189- inject: {
190- setHasAppNavigation: {
191- default : () => () => warn (' NcAppNavigation is not mounted inside NcContent, this is probably an error.' ),
192- from: ' NcContent:setHasAppNavigation' ,
193- },
194- },
195-
196- props: {
197- /**
198- * The aria label to describe the navigation
199- */
200- ariaLabel: {
201- type: String ,
202- default: ' ' ,
203- },
204-
205- /**
206- * aria-labelledby attribute to describe the navigation
207- */
208- ariaLabelledby: {
209- type: String ,
210- default: ' ' ,
211- },
212- },
213-
214- setup () {
215- return {
216- isMobile: useIsMobile (),
217- }
218- },
219-
220- data () {
221- return {
222- open: ! this .isMobile ,
223- focusTrap: null ,
224- }
225- },
226-
227- watch: {
228- isMobile () {
229- this .open = ! this .isMobile
230- this .toggleFocusTrap ()
231- },
232- open () {
233- this .toggleFocusTrap ()
234- },
235- },
236-
237- mounted () {
238- this .setHasAppNavigation (true )
239- subscribe (' toggle-navigation' , this .toggleNavigationByEventBus )
240- // Emit an event with the initial state of the navigation
241- emit (' navigation-toggled' , {
242- open: this .open ,
243- })
244-
245- this .focusTrap = createFocusTrap (this .$refs .appNavigationContainer , {
246- allowOutsideClick: true ,
247- fallbackFocus: this .$refs .appNavigationContainer ,
248- trapStack: getTrapStack (),
249- escapeDeactivates: false ,
250- })
251- this .toggleFocusTrap ()
252- },
253- unmounted () {
254- this .setHasAppNavigation (false )
255- unsubscribe (' toggle-navigation' , this .toggleNavigationByEventBus )
256- this .focusTrap .deactivate ()
257- },
258-
259- methods: {
260- /**
261- * Toggle the navigation
262- *
263- * @param {boolean} [state] set the state instead of inverting the current one
264- */
265- toggleNavigation (state ) {
266- // Early return if already in that state
267- if (this .open === state) {
268- emit (' navigation-toggled' , {
269- open: this .open ,
270- })
271- return
272- }
273-
274- this .open = (typeof state === ' undefined' ) ? ! this .open : state
275- const bodyStyles = getComputedStyle (document .body )
276- const animationLength = parseInt (bodyStyles .getPropertyValue (' --animation-quick' )) || 100
277-
278- setTimeout (() => {
279- emit (' navigation-toggled' , {
280- open: this .open ,
281- })
282- // We wait for 1.5 times the animation length to give the animation time to really finish.
283- }, 1.5 * animationLength)
284- },
285-
286- toggleNavigationByEventBus ({ open }) {
287- this .toggleNavigation (open)
288- },
289-
290- /**
291- * Activate focus trap if it is currently needed, otherwise deactivate
292- */
293- toggleFocusTrap () {
294- if (this .isMobile && this .open ) {
295- this .focusTrap .activate ()
296- } else {
297- this .focusTrap .deactivate ()
298- }
299- },
300-
301- handleEsc () {
302- if (this .isMobile ) {
303- this .toggleNavigation (false )
304- }
305- },
306- },
307- }
308- </script >
309-
310306<style lang="scss">
311307.app-navigation ,
312308.app-content {
@@ -342,7 +338,7 @@ export default {
342338 -webkit-backdrop-filter : var (--filter-background-blur , none );
343339 backdrop-filter : var (--filter-background-blur , none );
344340
345- & --close {
341+ & --closed {
346342 margin-inline-start : calc (-1 * min ($navigation-width , var (--app-navigation-max-width )));
347343 }
348344
0 commit comments