Skip to content

Commit

Permalink
feat(NcAppNavigation): Provide consistent in-app search
Browse files Browse the repository at this point in the history
`NcAppNaviation` now provides an optional in-app search when `show-search` is set.
This allows apps which have in app filtering / search to use a consistent layout.

Signed-off-by: Ferdinand Thiessen <opensource@fthiessen.de>
  • Loading branch information
susnux committed Jul 18, 2024
1 parent 29922ce commit aa44c07
Show file tree
Hide file tree
Showing 4 changed files with 248 additions and 1 deletion.
3 changes: 3 additions & 0 deletions l10n/messages.pot
Original file line number Diff line number Diff line change
Expand Up @@ -332,6 +332,9 @@ msgstr ""
msgid "Search for time zone"
msgstr ""

msgid "Search in app…"
msgstr ""

msgid "Search results"
msgstr ""

Expand Down
135 changes: 134 additions & 1 deletion src/components/NcAppNavigation/NcAppNavigation.vue
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,103 @@ emit('toggle-navigation', {
})
```

#### With in-app search

```vue
<template>
<div class="styleguide-wrapper">
<NcContent app-name="styleguide-app-navigation" class="content-styleguidist">
<NcAppNavigation show-search :search.sync="searchQuery">
<template #search-actions>
<NcActionButton>
<template #icon>
<IconFilter :size="20" />
</template>
Filter
</NcActionButton>
<NcActionButton>
<template #icon>
<IconSearchGlobal :size="20" />
</template>
Search globally
</NcActionButton>
</template>
<template #list>
<NcAppNavigationItem name="First navigation entry">
<template #icon>
<IconStar :size="20" />
</template>
</NcAppNavigationItem>
<NcAppNavigationItem name="Second navigation entry">
<template #icon>
<IconStar :size="20" />
</template>
</NcAppNavigationItem>
</template>
</NcAppNavigation>
<NcAppContent>
<ul class="fake-content">
<li>Search query: {{ searchQuery }}</li>
<li v-for="(item, index) in items" :key="index">
{{ item }}
</li>
</ul>
</NcAppContent>
</NcContent>
</div>
</template>
<script>
import IconFilter from 'vue-material-design-icons/Filter.vue'
import IconSearchGlobal from 'vue-material-design-icons/CloudSearch.vue'
import IconStar from 'vue-material-design-icons/Star.vue'
const exampleItem = ['Mary', 'Patricia', 'James', 'Michael']
export default {
components: {
IconFilter,
IconSearchGlobal,
IconStar,
},
data() {
return {
searchQuery: '',
}
},
computed: {
items() {
return exampleItem.filter((item) => item.toLocaleLowerCase().includes(this.searchQuery.toLocaleLowerCase()))
},
},
}
</script>
<style scoped>
/* This styles just mock NcContent and NcAppContent */
.content-styleguidist {
position: relative !important;
margin: 0 !important;
/* prvent jumping */
min-height: 200px;
}
.content-styleguidist > * {
height: auto;
}
.fake-content {
padding: var(--app-navigation-padding);
padding-top: calc(2 * var(--app-navigation-padding) + var(--default-clickable-area));
}
.styleguide-wrapper {
background-color: var(--color-background-plain);
padding: var(--body-container-margin);
}
</style>
```

</docs>

<template>
Expand All @@ -47,6 +144,16 @@ emit('toggle-navigation', {
:inert="!open || undefined"
@keydown.esc="handleEsc">
<div class="app-navigation__body" :class="{ 'app-navigation__body--no-list': !$scopedSlots.list }">
<NcAppNavigationSearch v-if="showSearch"
:label="searchLabel || undefined"
:value="search"
@update:value="$emit('update:search', $event)">
<template #actions>
<!-- @slot Optional NcAction* to show next to the search input -->
<slot name="search-actions" />
</template>
</NcAppNavigationSearch>

<!-- The main content of the navigation. If no list is passed to the #list slot, stretched vertically. -->
<slot />
</div>
Expand All @@ -69,15 +176,17 @@ import { getTrapStack } from '../../utils/focusTrap.js'
import { emit, subscribe, unsubscribe } from '@nextcloud/event-bus'
import { createFocusTrap } from 'focus-trap'
import NcAppNavigationToggle from '../NcAppNavigationToggle/index.js'
import NcAppNavigationList from '../NcAppNavigationList/index.js'
import NcAppNavigationSearch from './NcAppNavigationSearch.vue'
import NcAppNavigationToggle from '../NcAppNavigationToggle/index.js'
import Vue from 'vue'
export default {
name: 'NcAppNavigation',
components: {
NcAppNavigationList,
NcAppNavigationSearch,
NcAppNavigationToggle,
},
Expand Down Expand Up @@ -105,6 +214,30 @@ export default {
type: String,
default: '',
},
/**
* If set an in-app search is shown as the first entry
*/
showSearch: {
type: Boolean,
default: false,
},
/**
* The current search query
*/
search: {
type: String,
default: '',
},
/**
* Label of in-app search input
*/
searchLabel: {
type: String,
default: null,
}

Check warning on line 240 in src/components/NcAppNavigation/NcAppNavigation.vue

View workflow job for this annotation

GitHub Actions / NPM lint

Missing trailing comma
},
setup() {
Expand Down
110 changes: 110 additions & 0 deletions src/components/NcAppNavigation/NcAppNavigationSearch.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
<!--
- SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
- SPDX-License-Identifier: AGPL-3.0-or-later
-->
<template>
<div class="app-navigation-search"
:class="{
'app-navigation-search--has-actions': hasActions,
}">
<NcInputField ref="inputElement"
:aria-label="label"
class="app-navigation-search__input"
label-outside
:pill="isLegacyVersion"
:placeholder="label"
show-trailing-button
:trailing-button-label="t('Clear search')"
type="search"
:value="value"
@update:value="$emit('update:value', $event)"
@trailing-button-click="onCloseSearch">
<template #trailing-button-icon>
<IconClose :size="20" />
</template>
</NcInputField>
<Transition name="slide-fade">
<NcActions v-if="hasActions"
v-show="showActions"
ref="actions"
class="app-navigation-search__actions"
:inline="1">
<slot name="actions" />
</NcActions>
</Transition>
</div>
</template>

<script setup>
import { useFocusWithin } from '@vueuse/core'
import { computed, ref, nextTick, useSlots, watch } from 'vue'
import { t } from '../../l10n.js'
import IconClose from 'vue-material-design-icons/Close.vue'
import NcInputField from '../NcInputField/NcInputField.vue'
defineProps({
value: {
type: String,
default: '',
},
label: {
type: String,
default: t('Search in app…'),
},
})
const emit = defineEmits(['update:value'])
const slots = useSlots()
const isLegacyVersion = (window.OC?.config?.version?.split('.')[0] ?? 30) < 30
const inputElement = ref()
const { focused: inputHasFocus } = useFocusWithin(inputElement)
/**
* @type {import('vue').Ref<import('vue').ComponentPublicInstance>}
*/
const actions = ref()
const hasActions = computed(() => !!slots.actions)
const showActions = ref(true)
watch([inputHasFocus], () => { showActions.value = !inputHasFocus.value })
/**
* Handle close the search
*/
function onCloseSearch() {
emit('update:value', '')
if (hasActions.value) {
showActions.value = true
nextTick(() => actions.value.$el.querySelector('button').focus())
}
}
</script>
<style scoped lang="scss">
.app-navigation-search {
display: flex;
gap: var(--app-navigation-padding);
padding: var(--app-navigation-padding);
&--has-actions &__input {
flex-grow: 1;
z-index: 3;
}
&__actions {
margin-inline-start: 0;
}
}
.slide-fade-enter-active,
.slide-fade-leave-active {
transition: margin-inline-start var(--animation-quick);
}
.slide-fade-enter,
.slide-fade-leave-to {
margin-inline-start: calc(-1 * var(--default-clickable-area));
}
</style>
1 change: 1 addition & 0 deletions styleguide.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,7 @@ module.exports = async () => {
'src/components/NcAppNavigation*/*.vue',
],
ignore: [
'src/components/NcAppNavigation/NcAppNavigationSearch.vue',
'src/components/NcAppNavigationItem/NcAppNavigationIconCollapsible.vue',
'src/components/NcAppNavigationItem/NcInputConfirmCancel.vue',
],
Expand Down

0 comments on commit aa44c07

Please sign in to comment.