Skip to content

Commit 549dbcf

Browse files
authored
Merge pull request #7200 from nextcloud-libraries/refactor/ncappnavigation-ts
refactor(NcAppNavigation): migrate component to Typescript and `script-setup`
2 parents 38a7f17 + 4b1e188 commit 549dbcf

7 files changed

Lines changed: 166 additions & 182 deletions

File tree

src/components/NcAppNavigation/NcAppNavigation.vue

Lines changed: 143 additions & 147 deletions
Original file line numberDiff line numberDiff line change
@@ -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

src/components/NcAppNavigationToggle/NcAppNavigationToggle.vue renamed to src/components/NcAppNavigation/NcAppNavigationToggle.vue

Lines changed: 4 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,12 @@
22
- SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
33
- SPDX-License-Identifier: AGPL-3.0-or-later
44
-->
5-
<!--
6-
- This component is only used for the NcAppNavigation component and not exported otherwise.
7-
-->
85

96
<script setup lang="ts">
7+
import { mdiMenu, mdiMenuOpen } from '@mdi/js'
108
import { computed } from 'vue'
11-
import MenuIcon from 'vue-material-design-icons/Menu.vue'
12-
import MenuOpenIcon from 'vue-material-design-icons/MenuOpen.vue'
139
import NcButton from '../NcButton/index.ts'
10+
import NcIconSvgWrapper from '../NcIconSvgWrapper/NcIconSvgWrapper.vue'
1411
import { t } from '../../l10n.ts'
1512
1613
/**
@@ -21,14 +18,6 @@ import { t } from '../../l10n.ts'
2118
const open = defineModel<boolean>('open', { required: true })
2219
2320
const title = computed(() => open.value ? t('Close navigation') : t('Open navigation'))
24-
25-
/**
26-
* Once the toggle has been clicked, emits the toggle status
27-
* so parent components can gauge the status of the navigation button
28-
*/
29-
function toggleNavigation(): void {
30-
open.value = !open.value
31-
}
3221
</script>
3322

3423
<template>
@@ -39,10 +28,9 @@ function toggleNavigation(): void {
3928
:aria-label="title"
4029
:title
4130
variant="tertiary"
42-
@click="toggleNavigation">
31+
@click="open = !open">
4332
<template #icon>
44-
<MenuOpenIcon v-if="open" :size="20" />
45-
<MenuIcon v-else :size="20" />
33+
<NcIconSvgWrapper :path="open ? mdiMenuOpen : mdiMenu" />
4634
</template>
4735
</NcButton>
4836
</div>

src/components/NcAppNavigationToggle/index.ts

Lines changed: 0 additions & 6 deletions
This file was deleted.

src/components/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ export { default as NcActionTextEditable } from './NcActionTextEditable/index.js
1818
export { default as NcAppContent } from './NcAppContent/index.js'
1919
export { default as NcAppContentDetails } from './NcAppContentDetails/index.ts'
2020
export { default as NcAppContentList } from './NcAppContentList/index.js'
21-
export { default as NcAppNavigation } from './NcAppNavigation/index.js'
21+
export { default as NcAppNavigation } from './NcAppNavigation/index.ts'
2222
export { default as NcAppNavigationCaption } from './NcAppNavigationCaption/index.js'
2323
export { default as NcAppNavigationIconBullet } from './NcAppNavigationIconBullet/index.js'
2424
export { default as NcAppNavigationItem } from './NcAppNavigationItem/index.js'

0 commit comments

Comments
 (0)