Skip to content

Commit 3a76a0d

Browse files
authored
Merge pull request #60761 from nextcloud/backport/60665/stable34
[stable34] feat(core): Add centered search input to top bar
2 parents 3bb7b3d + f0371f5 commit 3a76a0d

7 files changed

Lines changed: 201 additions & 27 deletions

File tree

core/src/components/AppMenu.vue

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,9 @@
1111
:triggers="[]"
1212
placement="bottom-start"
1313
:skidding="popoverSkidding"
14-
:setReturnFocus="returnFocusTarget"
15-
popoverBaseClass="app-menu__popover-base"
16-
popupRole="menu"
14+
:set-return-focus="returnFocusTarget"
15+
popover-base-class="app-menu__popover-base"
16+
popup-role="menu"
1717
@update:shown="opened = $event">
1818
<template #trigger>
1919
<NcButton
@@ -40,7 +40,7 @@
4040
ref="items"
4141
:app="item"
4242
:outlined="item.id === 'more-apps' || item.id === 'app-store'"
43-
:newTab="item.id === 'app-store'"
43+
:new-tab="item.id === 'app-store'"
4444
:tabindex="i === focusedIndex ? 0 : -1" />
4545
</div>
4646
</div>
@@ -431,8 +431,7 @@ export default defineComponent({
431431
min-width: 0;
432432
}
433433
434-
// Hide on small screens (matches $breakpoint-small-mobile in @nextcloud/vue).
435-
@media only screen and (max-width: 512px) {
434+
@media only screen and (max-width: 1024px) {
436435
display: none !important;
437436
}
438437
}
Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
<!--
2+
- SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
3+
- SPDX-License-Identifier: AGPL-3.0-or-later
4+
-->
5+
<template>
6+
<search class="unified-search-input" :class="[{ 'unified-search-input--mobile': isSmallMobile }]">
7+
<NcHeaderButton
8+
v-if="isSmallMobile"
9+
:aria-label="placeholderText"
10+
aria-haspopup="dialog"
11+
:aria-expanded="expanded ? 'true' : 'false'"
12+
@click="$emit('click', $event)">
13+
<template #icon>
14+
<IconMagnify :size="20" />
15+
</template>
16+
</NcHeaderButton>
17+
<button
18+
v-else
19+
type="button"
20+
class="unified-search-input__button"
21+
aria-haspopup="dialog"
22+
:aria-expanded="expanded ? 'true' : 'false'"
23+
@click="$emit('click', $event)">
24+
<IconMagnify
25+
class="unified-search-input__icon"
26+
:size="20"
27+
aria-hidden="true" />
28+
<span class="unified-search-input__label">
29+
{{ placeholderText }}
30+
</span>
31+
</button>
32+
</search>
33+
</template>
34+
35+
<script setup lang="ts">
36+
import { t } from '@nextcloud/l10n'
37+
import { useIsSmallMobile } from '@nextcloud/vue/composables/useIsMobile'
38+
import NcHeaderButton from '@nextcloud/vue/components/NcHeaderButton'
39+
import IconMagnify from 'vue-material-design-icons/Magnify.vue'
40+
41+
/**
42+
* First phase of the unified-search input: a button styled to look like an
43+
* input field that opens the unified-search modal on click. A later phase
44+
* will replace the button with a real input that filters results inline.
45+
*
46+
* Implemented as a custom component because no `@nextcloud/vue` component
47+
* fits the design role here: NcInputField is a real input whose styling
48+
* assumes a light page background and clashes with the themed header,
49+
* and NcTextField has the same issue. On narrow viewports the trigger
50+
* collapses to a standard NcHeaderButton so it matches the visual
51+
* language of the other header items.
52+
*/
53+
54+
defineProps<{
55+
/** Whether the popup the input controls is currently open. Bound to aria-expanded. */
56+
expanded?: boolean
57+
}>()
58+
59+
defineEmits<{
60+
click: [mouseEvent: MouseEvent]
61+
}>()
62+
63+
const isSmallMobile = useIsSmallMobile()
64+
const placeholderText = t('core', 'Search apps, files, tags, messages …')
65+
</script>
66+
67+
<style lang="scss" scoped>
68+
.unified-search-input {
69+
&:not(.unified-search-input--mobile) {
70+
position: absolute;
71+
top: 0;
72+
bottom: 0;
73+
inset-inline: 0;
74+
margin-inline: auto;
75+
display: flex;
76+
align-items: center;
77+
width: clamp(200px, 35vw, 600px);
78+
max-width: calc(100% - 32px);
79+
pointer-events: none;
80+
}
81+
82+
&--mobile {
83+
display: contents;
84+
}
85+
86+
&__button {
87+
pointer-events: auto;
88+
display: flex;
89+
align-items: center;
90+
justify-content: center;
91+
gap: 8px;
92+
width: 100%;
93+
height: calc(var(--default-clickable-area) - 8px);
94+
padding: 0 12px;
95+
border: none;
96+
border-radius: var(--border-radius-element, 8px);
97+
background-color: rgba(0, 0, 0, 0.15);
98+
-webkit-backdrop-filter: var(--filter-background-blur);
99+
backdrop-filter: var(--filter-background-blur);
100+
box-shadow: inset 0 2px 0 rgba(0, 0, 0, 0.12);
101+
color: var(--color-background-plain-text);
102+
cursor: pointer;
103+
text-align: center;
104+
font: inherit;
105+
transition: background-color var(--animation-quick) ease-in-out;
106+
107+
&:hover {
108+
background-color: rgba(0, 0, 0, 0.22);
109+
}
110+
111+
&:focus-visible {
112+
background-color: rgba(0, 0, 0, 0.22);
113+
outline: 2px solid var(--color-background-plain-text);
114+
outline-offset: 2px;
115+
}
116+
117+
&:active {
118+
background-color: rgba(0, 0, 0, 0.28) !important;
119+
color: var(--color-background-plain-text) !important;
120+
outline: none;
121+
}
122+
}
123+
124+
&__icon {
125+
flex-shrink: 0;
126+
display: flex;
127+
align-items: center;
128+
}
129+
130+
&__label {
131+
min-width: 0;
132+
overflow: hidden;
133+
text-overflow: ellipsis;
134+
white-space: nowrap;
135+
}
136+
}
137+
138+
.unified-search-input--mobile :deep(.header-menu) {
139+
height: var(--default-clickable-area);
140+
}
141+
142+
.unified-search-input--mobile :deep(.header-menu__trigger) {
143+
--button-size: var(--default-clickable-area) !important;
144+
height: var(--default-clickable-area) !important;
145+
}
146+
147+
.unified-search-input--mobile :deep(.button-vue) {
148+
--color-main-text: var(--color-background-plain-text);
149+
color: var(--color-background-plain-text);
150+
border-radius: var(--border-radius-element) !important;
151+
152+
&:hover:not(:disabled) {
153+
background-color: rgba(0, 0, 0, 0.1) !important;
154+
}
155+
156+
&:active:not(:disabled) {
157+
background-color: rgba(0, 0, 0, 0.15) !important;
158+
}
159+
160+
&:focus-visible {
161+
background-color: rgba(0, 0, 0, 0.1) !important;
162+
outline: none !important;
163+
box-shadow: inset 0 0 0 2px var(--color-background-plain-text) !important;
164+
}
165+
}
166+
167+
[data-theme-dark] .unified-search-input__button,
168+
[data-theme-dark-highcontrast] .unified-search-input__button {
169+
background-color: color-mix(in srgb, var(--color-primary-element) 16%, transparent);
170+
171+
&:hover {
172+
background-color: color-mix(in srgb, var(--color-primary-element) 22%, transparent);
173+
}
174+
175+
&:focus-visible {
176+
background-color: color-mix(in srgb, var(--color-primary-element) 22%, transparent);
177+
}
178+
179+
&:active {
180+
background-color: color-mix(in srgb, var(--color-primary-element) 28%, transparent) !important;
181+
color: var(--color-background-plain-text) !important;
182+
outline: none;
183+
}
184+
}
185+
</style>

core/src/views/UnifiedSearch.vue

Lines changed: 5 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,9 @@
44
-->
55
<template>
66
<div class="unified-search-menu">
7-
<NcHeaderButton
8-
v-show="!showLocalSearch"
9-
id="unified-search"
10-
:aria-label="t('core', 'Unified search')"
11-
@click="toggleUnifiedSearch">
12-
<template #icon>
13-
<NcIconSvgWrapper :path="mdiMagnify" />
14-
</template>
15-
</NcHeaderButton>
7+
<UnifiedSearchInput
8+
:expanded="showUnifiedSearch || showLocalSearch"
9+
@click="toggleUnifiedSearch" />
1610
<UnifiedSearchLocalSearchBar
1711
v-if="supportsLocalSearch"
1812
:open.sync="showLocalSearch"
@@ -26,14 +20,12 @@
2620
</template>
2721

2822
<script lang="ts">
29-
import { mdiMagnify } from '@mdi/js'
3023
import { emit, subscribe } from '@nextcloud/event-bus'
3124
import { t } from '@nextcloud/l10n'
3225
import { useBrowserLocation } from '@vueuse/core'
3326
import debounce from 'debounce'
3427
import { defineComponent } from 'vue'
35-
import NcHeaderButton from '@nextcloud/vue/components/NcHeaderButton'
36-
import NcIconSvgWrapper from '@nextcloud/vue/components/NcIconSvgWrapper'
28+
import UnifiedSearchInput from '../components/UnifiedSearch/UnifiedSearchInput.vue'
3729
import UnifiedSearchLocalSearchBar from '../components/UnifiedSearch/UnifiedSearchLocalSearchBar.vue'
3830
import UnifiedSearchModal from '../components/UnifiedSearch/UnifiedSearchModal.vue'
3931
import logger from '../logger.js'
@@ -42,10 +34,9 @@ export default defineComponent({
4234
name: 'UnifiedSearch',
4335
4436
components: {
45-
NcHeaderButton,
46-
NcIconSvgWrapper,
4737
UnifiedSearchModal,
4838
UnifiedSearchLocalSearchBar,
39+
UnifiedSearchInput,
4940
},
5041
5142
setup() {
@@ -54,7 +45,6 @@ export default defineComponent({
5445
return {
5546
currentLocation,
5647
57-
mdiMagnify,
5848
t,
5949
}
6050
},

dist/core-main.js

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dist/core-main.js.map

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dist/core-unified-search.js

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dist/core-unified-search.js.map

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)