mirror of
https://github.com/nextcloud/server.git
synced 2026-07-12 11:06:50 -04:00
feat(core): support unified search on mobile viewports
Signed-off-by: Peter Ringelmann <peter.ringelmann@nextcloud.com>
This commit is contained in:
parent
bb4b70cf3a
commit
755680829b
58 changed files with 453 additions and 174 deletions
|
|
@ -2,4 +2,4 @@
|
|||
* SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-FileCopyrightText: 2016 ownCloud, Inc.
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/#skip-actions{position:absolute;overflow:hidden;z-index:9999;top:-999px;inset-inline-start:3px;padding:11px;display:flex;flex-wrap:wrap;gap:11px}#skip-actions:focus-within{top:var(--header-height)}#header{-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}#header:not(.header-guest){display:inline-flex;position:absolute;top:0;width:100%;z-index:2000;height:var(--header-height);box-sizing:border-box;justify-content:space-between}#header #nextcloud{padding:5px 0;padding-inline-start:86px;position:relative;height:calc(100% - var(--default-grid-baseline));box-sizing:border-box;opacity:1;align-items:center;display:flex;flex-wrap:wrap;overflow:hidden;margin:2px}#header #nextcloud:hover,#header #nextcloud:active{opacity:1}#header #nextcloud .logo{display:inline-flex;background-image:var(--image-logoheader, var(--image-logo, url("../img/logo/logo.svg")));background-repeat:no-repeat;background-size:contain;background-position:center;width:62px;position:absolute;inset-inline-start:12px;top:1px;bottom:1px;filter:var(--image-logoheader-custom, var(--background-image-invert-if-bright))}#header #nextcloud:focus-visible,#header .header-menu button:first-of-type:focus-visible{outline:none}#header #nextcloud:focus-visible::after,#header .header-menu button:first-of-type:focus-visible::after{content:" ";position:absolute;inset-block-end:2px;transform:translateX(-50%);width:12px;height:2px;border-radius:3px;background-color:var(--color-background-plain-text);inset-inline-start:50%;opacity:1}#header .header-start{display:inline-flex;align-items:center;flex:1 0;white-space:nowrap;min-width:0}#header .header-center{display:inline-flex;align-items:center;justify-content:flex-end;flex:0 1 auto;white-space:nowrap;min-width:0}#header .header-end{display:inline-flex;align-items:center;justify-content:flex-end;flex-shrink:1;margin-inline-end:calc(3*var(--default-grid-baseline))}@media(min-width: 512px){#body-user #header .header-end{flex:1 0}}#header .header-appname{color:var(--color-background-plain-text);font-size:16px;font-weight:bold;margin:0;padding:0;padding-inline-end:5px;overflow:hidden;text-overflow:ellipsis;flex:1 1 100%}#header .header-appname .header-info{display:flex;flex-direction:column;overflow:hidden}#header .header-appname .header-info .header-title{overflow:hidden;text-overflow:ellipsis}#header .header-appname .header-info .header-shared-by{color:var(--color-background-plain-text);position:relative;font-weight:300;font-size:var(--font-size-small);line-height:var(--font-size-small);overflow:hidden;text-overflow:ellipsis}/*# sourceMappingURL=header.css.map */
|
||||
*/#skip-actions{position:absolute;overflow:hidden;z-index:9999;top:-999px;inset-inline-start:3px;padding:11px;display:flex;flex-wrap:wrap;gap:11px}#skip-actions:focus-within{top:var(--header-height)}#header{-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}#header:not(.header-guest){display:inline-flex;position:absolute;top:0;width:100%;z-index:2000;height:var(--header-height);box-sizing:border-box;justify-content:space-between}#header #nextcloud{padding:5px 0;padding-inline-start:86px;position:relative;height:calc(100% - var(--default-grid-baseline));box-sizing:border-box;opacity:1;align-items:center;display:flex;flex-wrap:wrap;overflow:hidden;margin:2px}#header #nextcloud:hover,#header #nextcloud:active{opacity:1}#header #nextcloud .logo{display:inline-flex;background-image:var(--image-logoheader, var(--image-logo, url("../img/logo/logo.svg")));background-repeat:no-repeat;background-size:contain;background-position:center;width:62px;position:absolute;inset-inline-start:12px;top:1px;bottom:1px;filter:var(--image-logoheader-custom, var(--background-image-invert-if-bright))}#header #nextcloud:focus-visible,#header .header-menu button:first-of-type:focus-visible{outline:none}#header #nextcloud:focus-visible::after,#header .header-menu button:first-of-type:focus-visible::after{content:" ";position:absolute;inset-block-end:2px;transform:translateX(-50%);width:12px;height:2px;border-radius:3px;background-color:var(--color-background-plain-text);inset-inline-start:50%;opacity:1}#header .header-start{display:inline-flex;align-items:center;flex:1 0;white-space:nowrap;min-width:0}#header .header-center{display:inline-flex;align-items:center;justify-content:flex-end;flex:0 1 auto;white-space:nowrap;min-width:0}#header .header-end{display:inline-flex;align-items:center;justify-content:flex-end;flex-shrink:1;margin-inline-end:calc(3*var(--default-grid-baseline))}@media(min-width: 512px){#body-user #header .header-end,#body-settings #header .header-end{flex:1 0}}#header .header-appname{color:var(--color-background-plain-text);font-size:16px;font-weight:bold;margin:0;padding:0;padding-inline-end:5px;overflow:hidden;text-overflow:ellipsis;flex:1 1 100%}#header .header-appname .header-info{display:flex;flex-direction:column;overflow:hidden}#header .header-appname .header-info .header-title{overflow:hidden;text-overflow:ellipsis}#header .header-appname .header-info .header-shared-by{color:var(--color-background-plain-text);position:relative;font-weight:300;font-size:var(--font-size-small);line-height:var(--font-size-small);overflow:hidden;text-overflow:ellipsis}/*# sourceMappingURL=header.css.map */
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
{"version":3,"sourceRoot":"","sources":["header.scss"],"names":[],"mappings":"AAAA;AAAA;AAAA;AAAA;AAAA,GAYA,cACC,kBACA,gBACA,aACA,WACA,uBACA,aACA,aACA,eACA,SAEA,2BACC,yBAKF,QAEC,yBACA,sBACA,qBACA,iBAGA,2BACC,oBACA,kBACA,MACA,WACA,aACA,4BACA,sBACA,8BAID,mBACC,cACA,0BACA,kBACA,iDACA,sBACA,UACA,mBACA,aACA,eACA,gBACA,WAEA,mDAEC,UAID,yBACC,oBACA,yFACA,4BACA,wBACA,2BACA,WACA,kBACA,wBACA,QACA,WAEA,gFAMF,yFAEC,aAEA,uGACC,YACA,kBACA,oBACA,2BACA,WACA,WACA,kBACA,oDACA,uBACA,UAOF,sBACC,oBACA,mBACA,SACA,mBACA,YAKD,uBACC,oBACA,mBACA,yBACA,cACA,mBACA,YAKD,oBACC,oBACA,mBACA,yBACA,cAEA,uDAQA,yBACC,+BACC,UAOH,wBACC,yCACA,eACA,iBACA,SACA,UACA,uBACA,gBACA,uBAEA,cAGA,qCACC,aACA,sBACA,gBAEA,mDACC,gBACA,uBAGD,uDACC,yCACA,kBACA,gBACA,iCACA,mCACA,gBACA","file":"header.css"}
|
||||
{"version":3,"sourceRoot":"","sources":["header.scss"],"names":[],"mappings":"AAAA;AAAA;AAAA;AAAA;AAAA,GAYA,cACC,kBACA,gBACA,aACA,WACA,uBACA,aACA,aACA,eACA,SAEA,2BACC,yBAKF,QAEC,yBACA,sBACA,qBACA,iBAGA,2BACC,oBACA,kBACA,MACA,WACA,aACA,4BACA,sBACA,8BAID,mBACC,cACA,0BACA,kBACA,iDACA,sBACA,UACA,mBACA,aACA,eACA,gBACA,WAEA,mDAEC,UAID,yBACC,oBACA,yFACA,4BACA,wBACA,2BACA,WACA,kBACA,wBACA,QACA,WAEA,gFAMF,yFAEC,aAEA,uGACC,YACA,kBACA,oBACA,2BACA,WACA,WACA,kBACA,oDACA,uBACA,UAOF,sBACC,oBACA,mBACA,SACA,mBACA,YAKD,uBACC,oBACA,mBACA,yBACA,cACA,mBACA,YAKD,oBACC,oBACA,mBACA,yBACA,cAEA,uDASA,yBACC,kEAEC,UAOH,wBACC,yCACA,eACA,iBACA,SACA,UACA,uBACA,gBACA,uBAEA,cAGA,qCACC,aACA,sBACA,gBAEA,mDACC,gBACA,uBAGD,uDACC,yCACA,kBACA,gBACA,iCACA,mCACA,gBACA","file":"header.css"}
|
||||
|
|
@ -134,14 +134,16 @@
|
|||
// Add some spacing so the last entry looks ok
|
||||
margin-inline-end: calc(3 * var(--default-grid-baseline));
|
||||
|
||||
// Only grow to match header-start on desktop, and only
|
||||
// in the user layout where header-center exists.
|
||||
// Below the mobile breakpoint the search collapses to an icon,
|
||||
// and header-end must stay content-sized so header-start pushes
|
||||
// that icon flush to the right edge. The public layout has
|
||||
// no centered search, so it keeps the content-sized header-end.
|
||||
// Grow to match header-start (which always grows) so the centered search
|
||||
// stays centered. Only on desktop and only in the layouts that render
|
||||
// header-center: the user layout and the settings layout (#body-settings).
|
||||
// Below the mobile breakpoint the search collapses to an icon, and
|
||||
// header-end must stay content-sized so header-start pushes that icon flush
|
||||
// to the right edge. The public layout has no centered search, so it keeps
|
||||
// the content-sized header-end.
|
||||
@media (min-width: 512px) {
|
||||
#body-user & {
|
||||
#body-user &,
|
||||
#body-settings & {
|
||||
flex: 1 0;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@
|
|||
* SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-FileCopyrightText: 2016 ownCloud, Inc.
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/#skip-actions{position:absolute;overflow:hidden;z-index:9999;top:-999px;inset-inline-start:3px;padding:11px;display:flex;flex-wrap:wrap;gap:11px}#skip-actions:focus-within{top:var(--header-height)}#header{-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}#header:not(.header-guest){display:inline-flex;position:absolute;top:0;width:100%;z-index:2000;height:var(--header-height);box-sizing:border-box;justify-content:space-between}#header #nextcloud{padding:5px 0;padding-inline-start:86px;position:relative;height:calc(100% - var(--default-grid-baseline));box-sizing:border-box;opacity:1;align-items:center;display:flex;flex-wrap:wrap;overflow:hidden;margin:2px}#header #nextcloud:hover,#header #nextcloud:active{opacity:1}#header #nextcloud .logo{display:inline-flex;background-image:var(--image-logoheader, var(--image-logo, url("../img/logo/logo.svg")));background-repeat:no-repeat;background-size:contain;background-position:center;width:62px;position:absolute;inset-inline-start:12px;top:1px;bottom:1px;filter:var(--image-logoheader-custom, var(--background-image-invert-if-bright))}#header #nextcloud:focus-visible,#header .header-menu button:first-of-type:focus-visible{outline:none}#header #nextcloud:focus-visible::after,#header .header-menu button:first-of-type:focus-visible::after{content:" ";position:absolute;inset-block-end:2px;transform:translateX(-50%);width:12px;height:2px;border-radius:3px;background-color:var(--color-background-plain-text);inset-inline-start:50%;opacity:1}#header .header-start{display:inline-flex;align-items:center;flex:1 0;white-space:nowrap;min-width:0}#header .header-center{display:inline-flex;align-items:center;justify-content:flex-end;flex:0 1 auto;white-space:nowrap;min-width:0}#header .header-end{display:inline-flex;align-items:center;justify-content:flex-end;flex-shrink:1;margin-inline-end:calc(3*var(--default-grid-baseline))}@media(min-width: 512px){#body-user #header .header-end{flex:1 0}}#header .header-appname{color:var(--color-background-plain-text);font-size:16px;font-weight:bold;margin:0;padding:0;padding-inline-end:5px;overflow:hidden;text-overflow:ellipsis;flex:1 1 100%}#header .header-appname .header-info{display:flex;flex-direction:column;overflow:hidden}#header .header-appname .header-info .header-title{overflow:hidden;text-overflow:ellipsis}#header .header-appname .header-info .header-shared-by{color:var(--color-background-plain-text);position:relative;font-weight:300;font-size:var(--font-size-small);line-height:var(--font-size-small);overflow:hidden;text-overflow:ellipsis}/*!
|
||||
*/#skip-actions{position:absolute;overflow:hidden;z-index:9999;top:-999px;inset-inline-start:3px;padding:11px;display:flex;flex-wrap:wrap;gap:11px}#skip-actions:focus-within{top:var(--header-height)}#header{-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}#header:not(.header-guest){display:inline-flex;position:absolute;top:0;width:100%;z-index:2000;height:var(--header-height);box-sizing:border-box;justify-content:space-between}#header #nextcloud{padding:5px 0;padding-inline-start:86px;position:relative;height:calc(100% - var(--default-grid-baseline));box-sizing:border-box;opacity:1;align-items:center;display:flex;flex-wrap:wrap;overflow:hidden;margin:2px}#header #nextcloud:hover,#header #nextcloud:active{opacity:1}#header #nextcloud .logo{display:inline-flex;background-image:var(--image-logoheader, var(--image-logo, url("../img/logo/logo.svg")));background-repeat:no-repeat;background-size:contain;background-position:center;width:62px;position:absolute;inset-inline-start:12px;top:1px;bottom:1px;filter:var(--image-logoheader-custom, var(--background-image-invert-if-bright))}#header #nextcloud:focus-visible,#header .header-menu button:first-of-type:focus-visible{outline:none}#header #nextcloud:focus-visible::after,#header .header-menu button:first-of-type:focus-visible::after{content:" ";position:absolute;inset-block-end:2px;transform:translateX(-50%);width:12px;height:2px;border-radius:3px;background-color:var(--color-background-plain-text);inset-inline-start:50%;opacity:1}#header .header-start{display:inline-flex;align-items:center;flex:1 0;white-space:nowrap;min-width:0}#header .header-center{display:inline-flex;align-items:center;justify-content:flex-end;flex:0 1 auto;white-space:nowrap;min-width:0}#header .header-end{display:inline-flex;align-items:center;justify-content:flex-end;flex-shrink:1;margin-inline-end:calc(3*var(--default-grid-baseline))}@media(min-width: 512px){#body-user #header .header-end,#body-settings #header .header-end{flex:1 0}}#header .header-appname{color:var(--color-background-plain-text);font-size:16px;font-weight:bold;margin:0;padding:0;padding-inline-end:5px;overflow:hidden;text-overflow:ellipsis;flex:1 1 100%}#header .header-appname .header-info{display:flex;flex-direction:column;overflow:hidden}#header .header-appname .header-info .header-title{overflow:hidden;text-overflow:ellipsis}#header .header-appname .header-info .header-shared-by{color:var(--color-background-plain-text);position:relative;font-weight:300;font-size:var(--font-size-small);line-height:var(--font-size-small);overflow:hidden;text-overflow:ellipsis}/*!
|
||||
* SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-FileCopyrightText: 2016 ownCloud, Inc.
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -8,7 +8,8 @@
|
|||
:class="{ 'unified-search-input--mobile': isSmallMobile }">
|
||||
<NcHeaderButton
|
||||
v-if="isSmallMobile"
|
||||
:aria-label="placeholderText"
|
||||
id="unified-search-trigger"
|
||||
:ariaLabel="placeholderText"
|
||||
aria-haspopup="dialog"
|
||||
:aria-expanded="expanded ? 'true' : 'false'"
|
||||
@click="$emit('click', $event)">
|
||||
|
|
@ -20,14 +21,16 @@
|
|||
v-else
|
||||
class="unified-search-input__field"
|
||||
:class="{ 'unified-search-input__field--active': isActive }">
|
||||
<!-- Resting overlay: centred magnifier + placeholder, purely decorative.
|
||||
A full-width input cannot group an icon with its own placeholder, so we
|
||||
paint the resting look on top and let clicks fall through to the input. -->
|
||||
<div class="unified-search-input__resting" aria-hidden="true">
|
||||
<!-- Decorative overlay: an input can't group an icon with its own placeholder,
|
||||
so we paint the magnifier + placeholder on top and let clicks fall through.
|
||||
It slides to the leading edge on focus (see styles). -->
|
||||
<div
|
||||
class="unified-search-input__resting"
|
||||
:class="{ 'unified-search-input__resting--filled': query.length > 0 }"
|
||||
aria-hidden="true">
|
||||
<IconMagnify :size="20" />
|
||||
<span>{{ placeholderText }}</span>
|
||||
<span class="unified-search-input__label">{{ placeholderText }}</span>
|
||||
</div>
|
||||
<IconMagnify class="unified-search-input__icon" :size="20" aria-hidden="true" />
|
||||
<input
|
||||
ref="inputRef"
|
||||
type="text"
|
||||
|
|
@ -36,9 +39,8 @@
|
|||
aria-autocomplete="list"
|
||||
:aria-expanded="expanded ? 'true' : 'false'"
|
||||
:aria-label="placeholderText"
|
||||
:placeholder="isActive ? placeholderText : ''"
|
||||
:value="query"
|
||||
@focus="onFocus"
|
||||
@focus="isFocused = true"
|
||||
@blur="isFocused = false"
|
||||
@input="onInput">
|
||||
<NcButton
|
||||
|
|
@ -83,30 +85,18 @@ const props = defineProps<{
|
|||
|
||||
const emit = defineEmits<{
|
||||
click: [mouseEvent: MouseEvent]
|
||||
focus: [focusEvent: FocusEvent]
|
||||
'update:query': [query: string]
|
||||
}>()
|
||||
|
||||
const isSmallMobile = useIsSmallMobile()
|
||||
const placeholderText = t('core', 'Search apps, files, tags, messages …')
|
||||
const placeholderText = t('core', 'Apps, files, messages, and more')
|
||||
|
||||
const inputRef = ref<HTMLInputElement>()
|
||||
const isFocused = ref(false)
|
||||
|
||||
/** Active = focused or holding a query; drives the input-vs-button styling. */
|
||||
/** Active = focused or holding a query; drives the resting-vs-active styling. */
|
||||
const isActive = computed(() => isFocused.value || props.query.length > 0)
|
||||
|
||||
/**
|
||||
* Track focus for the active styling and bubble the event so the parent can
|
||||
* react to it.
|
||||
*
|
||||
* @param event The focus event
|
||||
*/
|
||||
function onFocus(event: FocusEvent) {
|
||||
isFocused.value = true
|
||||
emit('focus', event)
|
||||
}
|
||||
|
||||
/**
|
||||
* Relay the typed value upward.
|
||||
*
|
||||
|
|
@ -121,15 +111,12 @@ function clearQuery() {
|
|||
emit('update:query', '')
|
||||
inputRef.value?.focus()
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
getInputElement: (): HTMLInputElement | null => inputRef.value ?? null,
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.unified-search-input {
|
||||
// Positioned so z-index applies and the input paints above the search scrim
|
||||
// Paints above the modal root (z-index: 50) so the header input stays clickable
|
||||
// over the scrim while the popover is open. Keep 51 one above that value.
|
||||
position: relative;
|
||||
z-index: 51;
|
||||
|
||||
|
|
@ -147,7 +134,18 @@ defineExpose({
|
|||
&__field {
|
||||
--resting-background: rgba(0, 0, 0, 0.15);
|
||||
--resting-background-hover: rgba(0, 0, 0, 0.22);
|
||||
// Shared geometry: the resting group and the input's leading padding read the
|
||||
// same tokens so the placeholder and the typed value line up.
|
||||
--search-icon-pad: 12px;
|
||||
--search-icon-size: 20px;
|
||||
--search-icon-gap: 8px;
|
||||
// One shared timing for every focus transition (background, the icon/label
|
||||
// slide, the recolour) so they move together. easeOutQuart = soft landing.
|
||||
--search-anim-duration: 240ms;
|
||||
--search-anim-easing: cubic-bezier(0.22, 1, 0.36, 1);
|
||||
position: relative;
|
||||
// Query container so the resting group can centre itself with cqi units
|
||||
container-type: inline-size;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
// Match the default clickable area so the inner <input> (which the global
|
||||
|
|
@ -160,7 +158,10 @@ defineExpose({
|
|||
background-color: var(--resting-background);
|
||||
-webkit-backdrop-filter: var(--filter-background-blur);
|
||||
backdrop-filter: var(--filter-background-blur);
|
||||
transition: background-color var(--animation-quick) ease-in-out;
|
||||
// Blue tint -> white surface on the shared timing, in step with the slide.
|
||||
transition:
|
||||
background-color var(--search-anim-duration) var(--search-anim-easing),
|
||||
box-shadow var(--search-anim-duration) var(--search-anim-easing);
|
||||
|
||||
&:hover:not(.unified-search-input__field--active) {
|
||||
background-color: var(--resting-background-hover);
|
||||
|
|
@ -173,44 +174,53 @@ defineExpose({
|
|||
}
|
||||
}
|
||||
|
||||
// Resting look: magnifier + placeholder centred as a group, painted over the
|
||||
// (empty, pointer-transparent) input. Fades out as the field becomes active.
|
||||
// Anchored at the leading edge and translated to the centre while at rest; on
|
||||
// focus (--active) the translate goes to 0 and it slides into place. Centre offset
|
||||
// is pure CSS: half the field (50cqi) minus half the group (50%) minus the pad, so
|
||||
// it self-corrects for any placeholder length or field width.
|
||||
&__resting {
|
||||
--slide-sign: 1;
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
inset-block: 0;
|
||||
inset-inline-start: var(--search-icon-pad);
|
||||
max-width: calc(100% - 2 * var(--search-icon-pad));
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
padding-inline: 12px;
|
||||
gap: var(--search-icon-gap);
|
||||
pointer-events: none;
|
||||
color: color-mix(in srgb, var(--color-background-plain-text) 70%, var(--color-background-plain));
|
||||
transition: opacity var(--animation-quick) ease-in-out;
|
||||
|
||||
span {
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
transform: translateX(calc(var(--slide-sign) * (50cqi - 50% - var(--search-icon-pad))));
|
||||
transition:
|
||||
transform var(--search-anim-duration) var(--search-anim-easing),
|
||||
color var(--search-anim-duration) var(--search-anim-easing);
|
||||
|
||||
.unified-search-input__field--active & {
|
||||
opacity: 0;
|
||||
transform: translateX(0);
|
||||
color: var(--color-text-maxcontrast);
|
||||
}
|
||||
}
|
||||
|
||||
// Left magnifier for the active state; hidden while the resting overlay shows
|
||||
&__icon {
|
||||
position: absolute;
|
||||
inset-inline-start: 12px;
|
||||
display: flex;
|
||||
color: var(--color-main-text);
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transition: opacity var(--animation-quick) ease-in-out;
|
||||
// Placeholder text inside the resting group. Ellipsised, and hidden once typing
|
||||
// starts so it doesn't overlap the value. Scoped to the label class so the sibling
|
||||
// magnifier (also rendered as a <span>) stays visible.
|
||||
&__label {
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
transition: opacity var(--search-anim-duration) var(--search-anim-easing);
|
||||
}
|
||||
|
||||
.unified-search-input__field--active & {
|
||||
opacity: 1;
|
||||
}
|
||||
&__resting--filled &__label {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
// The material-design icon <svg> is inline (baseline-aligned), which leaves a
|
||||
// descender gap and makes the glyph sit high even when its box is centred.
|
||||
// Render it as a block so it fills its box, then nudge 1px down to sit on the
|
||||
// text's optical centre (a geometrically centred glyph reads slightly high).
|
||||
&__resting :deep(.material-design-icon__svg) {
|
||||
display: block;
|
||||
transform: translateY(1px);
|
||||
}
|
||||
|
||||
// Only visible once active (at rest it's empty and covered by the overlay),
|
||||
|
|
@ -220,8 +230,9 @@ defineExpose({
|
|||
min-width: 0;
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
// Leading space for the active magnifier
|
||||
padding-inline: calc(var(--default-clickable-area) - var(--default-grid-baseline)) 12px;
|
||||
// Leading space so the placeholder/value starts one gap past the magnifier,
|
||||
// matching the resting group exactly. Trailing padding mirrors the leading pad.
|
||||
padding-inline: calc(var(--search-icon-pad) + var(--search-icon-size) + var(--search-icon-gap)) var(--search-icon-pad);
|
||||
// Opt out of NC's global input chrome (core/css/inputs.scss adds a border,
|
||||
// radius and focus box-shadow to any text input not in its exclusion list).
|
||||
// !important because that global focus rule outweighs a scoped class.
|
||||
|
|
@ -256,6 +267,21 @@ defineExpose({
|
|||
--resting-background-hover: color-mix(in srgb, var(--color-primary-element) 22%, transparent);
|
||||
}
|
||||
|
||||
// translateX is physical, so flip the resting slide under RTL to keep it moving
|
||||
// toward the leading (right) edge.
|
||||
[dir=rtl] .unified-search-input__resting {
|
||||
--slide-sign: -1;
|
||||
}
|
||||
|
||||
// Respect reduced-motion: keep the end states but drop the slide/fade so nothing
|
||||
// animates on focus.
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.unified-search-input__resting,
|
||||
.unified-search-input__resting span {
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
|
||||
// Mobile: NcHeaderButton styling to match the other header items
|
||||
.unified-search-input--mobile :deep(.header-menu) {
|
||||
height: var(--default-clickable-area);
|
||||
|
|
|
|||
|
|
@ -14,11 +14,29 @@
|
|||
@set:customDateRange="setCustomDateRange"
|
||||
@update:isOpen="showDateRangeModal = $event" />
|
||||
|
||||
<div ref="panel" class="unified-search-modal-container">
|
||||
<div ref="panel" class="unified-search-modal__container">
|
||||
<!-- Unified search form -->
|
||||
<div class="unified-search-modal__header">
|
||||
<div v-if="isSmallMobile" class="unified-search-modal__mobile-input">
|
||||
<NcTextField
|
||||
type="search"
|
||||
:label="t('core', 'Apps, files, messages, and more')"
|
||||
:modelValue="searchQuery"
|
||||
:showTrailingButton="searchQuery.length > 0"
|
||||
:trailingButtonLabel="t('core', 'Clear search')"
|
||||
@update:modelValue="onMobileSearchInput"
|
||||
@trailing-button-click="searchQuery = ''" />
|
||||
<NcButton
|
||||
variant="tertiary"
|
||||
:aria-label="t('core', 'Close search')"
|
||||
@click="onUpdateOpen(false)">
|
||||
<template #icon>
|
||||
<IconClose :size="20" />
|
||||
</template>
|
||||
</NcButton>
|
||||
</div>
|
||||
<div class="unified-search-modal__filters" data-cy-unified-search-filters>
|
||||
<NcActions v-model:open="providerActionMenuIsOpen" :menuName="t('core', 'Places')" data-cy-unified-search-filter="places">
|
||||
<NcActions :open.sync="providerActionMenuIsOpen" :menu-name="t('core', 'Places')" data-cy-unified-search-filter="places">
|
||||
<template #icon>
|
||||
<IconListBox :size="20" />
|
||||
</template>
|
||||
|
|
@ -35,7 +53,7 @@
|
|||
{{ provider.name }}
|
||||
</NcActionButton>
|
||||
</NcActions>
|
||||
<NcActions v-model:open="dateActionMenuIsOpen" :menuName="t('core', 'Date')" data-cy-unified-search-filter="date">
|
||||
<NcActions :open.sync="dateActionMenuIsOpen" :menu-name="t('core', 'Date')" data-cy-unified-search-filter="date">
|
||||
<template #icon>
|
||||
<IconCalendarRange :size="20" />
|
||||
</template>
|
||||
|
|
@ -182,28 +200,33 @@
|
|||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<div class="unified-search-modal-scrim" @click="onUpdateOpen(false)" />
|
||||
<div class="unified-search-modal__scrim" @click="onUpdateOpen(false)" />
|
||||
</div>
|
||||
</transition>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import type { FocusTrap } from 'focus-trap'
|
||||
|
||||
import { subscribe } from '@nextcloud/event-bus'
|
||||
import { loadState } from '@nextcloud/initial-state'
|
||||
import { getCanonicalLocale, n, t } from '@nextcloud/l10n'
|
||||
import { useIsSmallMobile } from '@nextcloud/vue/composables/useIsMobile'
|
||||
import { useBrowserLocation } from '@vueuse/core'
|
||||
import debounce from 'debounce'
|
||||
import { type FocusTrap,createFocusTrap } from 'focus-trap'
|
||||
import { type PropType,defineComponent, markRaw } from 'vue'
|
||||
import { createFocusTrap } from 'focus-trap'
|
||||
import { defineComponent, markRaw } from 'vue'
|
||||
import NcActionButton from '@nextcloud/vue/components/NcActionButton'
|
||||
import NcActions from '@nextcloud/vue/components/NcActions'
|
||||
import NcAvatar from '@nextcloud/vue/components/NcAvatar'
|
||||
import NcButton from '@nextcloud/vue/components/NcButton'
|
||||
import NcCheckboxRadioSwitch from '@nextcloud/vue/components/NcCheckboxRadioSwitch'
|
||||
import NcEmptyContent from '@nextcloud/vue/components/NcEmptyContent'
|
||||
import NcTextField from '@nextcloud/vue/components/NcTextField'
|
||||
import IconAccountGroup from 'vue-material-design-icons/AccountGroupOutline.vue'
|
||||
import IconArrowRight from 'vue-material-design-icons/ArrowRight.vue'
|
||||
import IconCalendarRange from 'vue-material-design-icons/CalendarRangeOutline.vue'
|
||||
import IconClose from 'vue-material-design-icons/Close.vue'
|
||||
import IconDotsHorizontal from 'vue-material-design-icons/DotsHorizontal.vue'
|
||||
import IconFilter from 'vue-material-design-icons/Filter.vue'
|
||||
import IconListBox from 'vue-material-design-icons/ListBox.vue'
|
||||
|
|
@ -222,6 +245,7 @@ export default defineComponent({
|
|||
IconArrowRight,
|
||||
IconAccountGroup,
|
||||
IconCalendarRange,
|
||||
IconClose,
|
||||
IconDotsHorizontal,
|
||||
IconFilter,
|
||||
IconListBox,
|
||||
|
|
@ -235,6 +259,7 @@ export default defineComponent({
|
|||
NcButton,
|
||||
NcEmptyContent,
|
||||
NcCheckboxRadioSwitch,
|
||||
NcTextField,
|
||||
SearchableList,
|
||||
SearchResult,
|
||||
},
|
||||
|
|
@ -263,14 +288,6 @@ export default defineComponent({
|
|||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
|
||||
/**
|
||||
* The header input element (lives in a sibling component), for the popover's focus trap
|
||||
*/
|
||||
inputElement: {
|
||||
type: (null as unknown) as PropType<HTMLElement | null>,
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
|
||||
emits: ['update:open', 'update:query'],
|
||||
|
|
@ -281,11 +298,13 @@ export default defineComponent({
|
|||
*/
|
||||
const currentLocation = useBrowserLocation()
|
||||
const searchStore = useSearchStore()
|
||||
const isSmallMobile = useIsSmallMobile()
|
||||
return {
|
||||
t,
|
||||
|
||||
currentLocation,
|
||||
externalFilters: searchStore.externalFilters,
|
||||
isSmallMobile,
|
||||
}
|
||||
},
|
||||
|
||||
|
|
@ -314,7 +333,6 @@ export default defineComponent({
|
|||
results: [],
|
||||
contacts: [],
|
||||
showDateRangeModal: false,
|
||||
internalIsVisible: this.open,
|
||||
initialized: false,
|
||||
searchExternalResources: false,
|
||||
minSearchLength: loadState('unified-search', 'min-search-length', 1),
|
||||
|
|
@ -378,6 +396,12 @@ export default defineComponent({
|
|||
return this.filters.some((filter) => filter.type === 'date' || filter.type === 'person')
|
||||
},
|
||||
|
||||
// Any teleported overlay (a filter menu or the date-range dialog) that renders
|
||||
// outside the focus trap and needs it paused while open.
|
||||
isOverlayOpen() {
|
||||
return this.providerActionMenuIsOpen || this.dateActionMenuIsOpen || this.showDateRangeModal
|
||||
},
|
||||
|
||||
filteredResults() {
|
||||
const isInFolderAtRoot = (result) => {
|
||||
if (result.id !== 'in-folder') {
|
||||
|
|
@ -447,19 +471,7 @@ export default defineComponent({
|
|||
}
|
||||
},
|
||||
|
||||
// Filter menus and the date-range dialog teleport outside the trap; pause it
|
||||
// while any is open so focus can move into them, then resume.
|
||||
providerActionMenuIsOpen() {
|
||||
this.toggleFocusTrapForOverlays()
|
||||
},
|
||||
|
||||
dateActionMenuIsOpen() {
|
||||
this.toggleFocusTrapForOverlays()
|
||||
},
|
||||
|
||||
showDateRangeModal() {
|
||||
this.toggleFocusTrapForOverlays()
|
||||
},
|
||||
isOverlayOpen: 'syncFocusTrapToOverlays',
|
||||
|
||||
query: {
|
||||
immediate: true,
|
||||
|
|
@ -471,7 +483,12 @@ export default defineComponent({
|
|||
searchQuery: {
|
||||
handler() {
|
||||
this.$emit('update:query', this.searchQuery)
|
||||
this.debouncedFind(this.searchQuery)
|
||||
// Only search while open: the query prop keeps flowing from the header even
|
||||
// when closed (e.g. the local search bar on deck), so a hidden modal must
|
||||
// not fire background searches.
|
||||
if (this.open) {
|
||||
this.debouncedFind(this.searchQuery)
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
|
|
@ -499,6 +516,16 @@ export default defineComponent({
|
|||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* NcTextField's `update:modelValue` is typed `string | number`; the search
|
||||
* query is always a string, so normalize here rather than casting in the template.
|
||||
*
|
||||
* @param value The new value from the mobile search field
|
||||
*/
|
||||
onMobileSearchInput(value: string | number) {
|
||||
this.searchQuery = String(value)
|
||||
},
|
||||
|
||||
/**
|
||||
* Close the search on Escape. Sub-menus / sub-dialogs (filter actions, the
|
||||
* date-range dialog) handle Escape themselves, so only close the popover
|
||||
|
|
@ -510,7 +537,7 @@ export default defineComponent({
|
|||
if (event.key !== 'Escape') {
|
||||
return
|
||||
}
|
||||
if (this.providerActionMenuIsOpen || this.dateActionMenuIsOpen || this.showDateRangeModal) {
|
||||
if (this.isOverlayOpen) {
|
||||
return
|
||||
}
|
||||
event.preventDefault()
|
||||
|
|
@ -529,13 +556,17 @@ export default defineComponent({
|
|||
if (!panel) {
|
||||
return
|
||||
}
|
||||
// focus-trap collects tabbables *inside* each container, not the container
|
||||
// element itself. The bare <input> is a leaf, so pass its wrapper (which
|
||||
// holds the input and the clear button); input first to match DOM order.
|
||||
const inputContainer = (this.inputElement?.closest('.unified-search-input') ?? null) as HTMLElement | null
|
||||
// focus-trap collects tabbables *inside* each container. The header input is
|
||||
// in a sibling component under the shared .unified-search-menu ancestor, so we
|
||||
// query its wrapper from the DOM at activation (not via a prop, which goes
|
||||
// stale across the mobile/desktop input swap). Input container first for DOM order.
|
||||
const menu = (this.$el as HTMLElement)?.closest?.('.unified-search-menu') ?? null
|
||||
const inputContainer = (menu?.querySelector('.unified-search-input') ?? null) as HTMLElement | null
|
||||
const containers = inputContainer ? [inputContainer, panel] : [panel]
|
||||
this.focusTrap = markRaw(createFocusTrap(containers, {
|
||||
initialFocus: this.inputElement ?? panel,
|
||||
// Prefer the mobile search field (rendered inside the panel) when present;
|
||||
// falls back to the header input, then the panel itself.
|
||||
initialFocus: () => panel.querySelector('input[type="search"]') ?? inputContainer?.querySelector('input') ?? panel,
|
||||
// We own closing via Escape (onEscapeKey) and scrim click
|
||||
escapeDeactivates: false,
|
||||
// Let scrim clicks reach their handler instead of being swallowed
|
||||
|
|
@ -552,15 +583,12 @@ export default defineComponent({
|
|||
this.focusTrap = null
|
||||
},
|
||||
|
||||
/**
|
||||
* Pause the trap while a teleported menu / dialog is open so focus can move
|
||||
* into it, and resume once they have all closed.
|
||||
*/
|
||||
toggleFocusTrapForOverlays() {
|
||||
// Filter menus and the date-range dialog teleport outside the trap; pause it
|
||||
// while any is open so focus can move into them, then resume once all close.
|
||||
syncFocusTrapToOverlays(overlayOpen: boolean) {
|
||||
if (!this.focusTrap) {
|
||||
return
|
||||
}
|
||||
const overlayOpen = this.providerActionMenuIsOpen || this.dateActionMenuIsOpen || this.showDateRangeModal
|
||||
if (overlayOpen) {
|
||||
this.focusTrap.pause()
|
||||
} else {
|
||||
|
|
@ -969,15 +997,17 @@ export default defineComponent({
|
|||
position: absolute;
|
||||
inset-block-start: 100%;
|
||||
inset-inline: 0;
|
||||
// One below the header input (z-index: 51) and above the page. !important wins
|
||||
// the stacking cascade inside the themed #header.
|
||||
z-index: 50 !important;
|
||||
margin-block-start: 8px;
|
||||
margin-block-start: 6px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
// Backdrop, mirrors NcModal's .modal-mask. Fixed so it covers the whole viewport
|
||||
// regardless of the anchored root.
|
||||
.unified-search-modal-scrim {
|
||||
.unified-search-modal__scrim {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 0;
|
||||
|
|
@ -987,7 +1017,7 @@ export default defineComponent({
|
|||
|
||||
// Dialog panel: NcModal's "normal" chrome, but width-matched to the header input
|
||||
// and anchored under it, growing downward and scrolling internally when tall.
|
||||
.unified-search-modal-container {
|
||||
.unified-search-modal__container {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
|
|
@ -1005,13 +1035,24 @@ export default defineComponent({
|
|||
background-color: var(--color-main-background);
|
||||
color: var(--color-main-text);
|
||||
box-shadow: 0 0 40px rgba(0, 0, 0, 0.2);
|
||||
// The panel slides down into place; the enter/leave classes set the start offset
|
||||
transition: transform 100ms ease-out;
|
||||
// The panel slides down into place; the enter/leave classes set the start offset.
|
||||
// Same easeOutQuart curve as the header input so the whole search UI moves in step.
|
||||
transition: transform 240ms cubic-bezier(0.22, 1, 0.36, 1);
|
||||
}
|
||||
|
||||
// Fullscreen on small viewports, mirrors NcModal's responsive breakpoint
|
||||
@media only screen and ((max-width: 512px) or (max-height: 400px)) {
|
||||
.unified-search-modal-container {
|
||||
.unified-search-modal-root {
|
||||
// Fill the viewport below the header bar, leaving it visible and interactive
|
||||
// (matches the previous unified search and the rest of the mobile chrome).
|
||||
position: fixed;
|
||||
inset-block-start: var(--header-height);
|
||||
inset-inline: 0;
|
||||
inset-block-end: 0;
|
||||
margin-block-start: 0;
|
||||
}
|
||||
|
||||
.unified-search-modal__container {
|
||||
width: 100%;
|
||||
max-width: initial;
|
||||
height: 100%;
|
||||
|
|
@ -1031,11 +1072,24 @@ export default defineComponent({
|
|||
opacity: 0;
|
||||
}
|
||||
|
||||
.unified-search-modal-enter .unified-search-modal-container,
|
||||
.unified-search-modal-leave-to .unified-search-modal-container {
|
||||
.unified-search-modal-enter .unified-search-modal__container,
|
||||
.unified-search-modal-leave-to .unified-search-modal__container {
|
||||
transform: translateY(-6px);
|
||||
}
|
||||
|
||||
// Respect reduced-motion: keep the backdrop cross-fade (opacity is not motion) but
|
||||
// drop the panel slide so nothing moves on open/close.
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.unified-search-modal__container {
|
||||
transition: none;
|
||||
}
|
||||
|
||||
.unified-search-modal-enter .unified-search-modal__container,
|
||||
.unified-search-modal-leave-to .unified-search-modal__container {
|
||||
transform: none;
|
||||
}
|
||||
}
|
||||
|
||||
.unified-search-modal {
|
||||
&__header {
|
||||
// Add background to prevent leaking scrolled content (because of sticky position)
|
||||
|
|
@ -1049,6 +1103,17 @@ export default defineComponent({
|
|||
top: 6px;
|
||||
}
|
||||
|
||||
&__mobile-input {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
margin-block-end: 8px;
|
||||
|
||||
:deep(.input-field) {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
}
|
||||
|
||||
&__filters {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
|
|
|||
65
core/src/tests/components/UnifiedSearch.spec.ts
Normal file
65
core/src/tests/components/UnifiedSearch.spec.ts
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
/*!
|
||||
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
import { shallowMount } from '@vue/test-utils'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { ref } from 'vue'
|
||||
|
||||
const mobile = ref(false)
|
||||
vi.mock('@nextcloud/vue/composables/useIsMobile', () => ({
|
||||
useIsSmallMobile: () => mobile,
|
||||
useIsMobile: () => ref(false),
|
||||
}))
|
||||
vi.mock('@nextcloud/event-bus', () => ({ emit: vi.fn(), subscribe: vi.fn() }))
|
||||
|
||||
import UnifiedSearch from '../../views/UnifiedSearch.vue'
|
||||
|
||||
function factory() {
|
||||
return shallowMount(UnifiedSearch, {
|
||||
global: { mocks: { t: (_: string, s: string) => s, OCP: {} } },
|
||||
})
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
mobile.value = false
|
||||
window.OCP = { Accessibility: { disableKeyboardShortcuts: () => true } }
|
||||
})
|
||||
afterEach(() => vi.clearAllMocks())
|
||||
|
||||
describe('UnifiedSearch open-state model', () => {
|
||||
it('desktop: typing opens, clearing closes', async () => {
|
||||
const wrapper = factory()
|
||||
wrapper.vm.queryText = 'abc'
|
||||
await wrapper.vm.$nextTick()
|
||||
expect(wrapper.vm.showUnifiedSearch).toBe(true)
|
||||
wrapper.vm.queryText = ''
|
||||
await wrapper.vm.$nextTick()
|
||||
expect(wrapper.vm.showUnifiedSearch).toBe(false)
|
||||
})
|
||||
|
||||
it('mobile: typing does NOT open the modal', async () => {
|
||||
mobile.value = true
|
||||
const wrapper = factory()
|
||||
wrapper.vm.queryText = 'abc'
|
||||
await wrapper.vm.$nextTick()
|
||||
expect(wrapper.vm.showUnifiedSearch).toBe(false)
|
||||
})
|
||||
|
||||
it('mobile: header button click opens the modal', async () => {
|
||||
mobile.value = true
|
||||
const wrapper = factory()
|
||||
wrapper.findComponent({ name: 'UnifiedSearchInput' }).vm.$emit('click')
|
||||
await wrapper.vm.$nextTick()
|
||||
expect(wrapper.vm.showUnifiedSearch).toBe(true)
|
||||
})
|
||||
|
||||
it('mobile: clearing the query does NOT close an open modal', async () => {
|
||||
mobile.value = true
|
||||
const wrapper = factory()
|
||||
wrapper.vm.showUnifiedSearch = true
|
||||
wrapper.vm.queryText = ''
|
||||
await wrapper.vm.$nextTick()
|
||||
expect(wrapper.vm.showUnifiedSearch).toBe(true)
|
||||
})
|
||||
})
|
||||
75
core/src/tests/components/UnifiedSearchModal.spec.ts
Normal file
75
core/src/tests/components/UnifiedSearchModal.spec.ts
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
/*!
|
||||
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
import { shallowMount } from '@vue/test-utils'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { ref } from 'vue'
|
||||
|
||||
const mobile = ref(false)
|
||||
vi.mock('@nextcloud/vue/composables/useIsMobile', () => ({
|
||||
useIsSmallMobile: () => mobile,
|
||||
useIsMobile: () => ref(false),
|
||||
}))
|
||||
vi.mock('@nextcloud/event-bus', () => ({ subscribe: vi.fn(), emit: vi.fn() }))
|
||||
vi.mock('@nextcloud/initial-state', () => ({ loadState: vi.fn(() => ([])) }))
|
||||
vi.mock('../../services/UnifiedSearchService.js', () => ({
|
||||
getProviders: vi.fn(() => Promise.resolve([])),
|
||||
getContacts: vi.fn(() => Promise.resolve([])),
|
||||
search: vi.fn(() => ({ request: Promise.resolve({ data: { ocs: { data: { entries: [] } } } }), cancel: vi.fn() })),
|
||||
}))
|
||||
vi.mock('../../store/unified-search-external-filters.js', () => ({
|
||||
useSearchStore: () => ({ externalFilters: [], scopeToApp: false }),
|
||||
}))
|
||||
|
||||
import UnifiedSearchModal from '../../components/UnifiedSearch/UnifiedSearchModal.vue'
|
||||
|
||||
function factory(open = true) {
|
||||
return shallowMount(UnifiedSearchModal, {
|
||||
propsData: { open, query: '', localSearch: false },
|
||||
global: { mocks: { t: (_: string, s: string) => s, n: (_: string, s: string) => s } },
|
||||
})
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
mobile.value = false
|
||||
})
|
||||
afterEach(() => vi.clearAllMocks())
|
||||
|
||||
describe('UnifiedSearchModal mobile input', () => {
|
||||
it('renders the search field only on mobile', () => {
|
||||
mobile.value = true
|
||||
expect(factory().findComponent({ name: 'NcTextField' }).exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('has no in-modal search field on desktop', () => {
|
||||
expect(factory().findComponent({ name: 'NcTextField' }).exists()).toBe(false)
|
||||
})
|
||||
|
||||
it('mobile close button requests close', async () => {
|
||||
mobile.value = true
|
||||
const wrapper = factory()
|
||||
wrapper.find('.unified-search-modal__mobile-input').findComponent({ name: 'NcButton' }).vm.$emit('click')
|
||||
await wrapper.vm.$nextTick()
|
||||
expect(wrapper.emitted('update:open')?.at(-1)).toEqual([false])
|
||||
})
|
||||
|
||||
// Guards the kebab event binding: Vue 2.7 does not normalize v-on names, so a
|
||||
// camelCase listener would silently miss NcTextField's `trailing-button-click`.
|
||||
it('mobile clear (trailing) button empties the query', async () => {
|
||||
mobile.value = true
|
||||
const wrapper = factory()
|
||||
wrapper.vm.searchQuery = 'hello'
|
||||
await wrapper.vm.$nextTick()
|
||||
wrapper.findComponent({ name: 'NcTextField' }).vm.$emit('trailing-button-click')
|
||||
await wrapper.vm.$nextTick()
|
||||
expect(wrapper.vm.searchQuery).toBe('')
|
||||
})
|
||||
|
||||
it('onMobileSearchInput normalises the emitted value to a string', () => {
|
||||
mobile.value = true
|
||||
const wrapper = factory()
|
||||
wrapper.vm.onMobileSearchInput(5)
|
||||
expect(wrapper.vm.searchQuery).toBe('5')
|
||||
})
|
||||
})
|
||||
|
|
@ -5,9 +5,9 @@
|
|||
<template>
|
||||
<div class="unified-search-menu">
|
||||
<UnifiedSearchInput
|
||||
ref="searchInput"
|
||||
:query="queryText"
|
||||
:expanded="showUnifiedSearch || showLocalSearch"
|
||||
@click="openModal"
|
||||
@update:query="queryText = $event" />
|
||||
<UnifiedSearchLocalSearchBar
|
||||
v-if="supportsLocalSearch"
|
||||
|
|
@ -18,7 +18,6 @@
|
|||
@update:query="queryText = $event" />
|
||||
<UnifiedSearchModal
|
||||
:localSearch="supportsLocalSearch"
|
||||
:inputElement="inputElement"
|
||||
:query="queryText"
|
||||
:open="showUnifiedSearch"
|
||||
@update:query="queryText = $event"
|
||||
|
|
@ -29,6 +28,7 @@
|
|||
<script lang="ts">
|
||||
import { emit, subscribe } from '@nextcloud/event-bus'
|
||||
import { t } from '@nextcloud/l10n'
|
||||
import { useIsSmallMobile } from '@nextcloud/vue/composables/useIsMobile'
|
||||
import { useBrowserLocation } from '@vueuse/core'
|
||||
import debounce from 'debounce'
|
||||
import { defineComponent } from 'vue'
|
||||
|
|
@ -48,9 +48,11 @@ export default defineComponent({
|
|||
|
||||
setup() {
|
||||
const currentLocation = useBrowserLocation()
|
||||
const isSmallMobile = useIsSmallMobile()
|
||||
|
||||
return {
|
||||
currentLocation,
|
||||
isSmallMobile,
|
||||
|
||||
t,
|
||||
}
|
||||
|
|
@ -64,8 +66,6 @@ export default defineComponent({
|
|||
showUnifiedSearch: false,
|
||||
/** Open state of the local search bar */
|
||||
showLocalSearch: false,
|
||||
/** The header input element, read from the child and handed to the modal's focus trap */
|
||||
inputElement: null as HTMLElement | null,
|
||||
}
|
||||
},
|
||||
|
||||
|
|
@ -104,8 +104,9 @@ export default defineComponent({
|
|||
*/
|
||||
queryText() {
|
||||
this.debouncedQueryUpdate()
|
||||
// Open the search popover only once something is typed; close it when cleared.
|
||||
if (!this.supportsLocalSearch) {
|
||||
// Desktop opens/closes the popover as you type; mobile is driven by the
|
||||
// header button + the modal close paths, so clearing must not collapse it.
|
||||
if (!this.supportsLocalSearch && !this.isSmallMobile) {
|
||||
this.showUnifiedSearch = this.queryText.length > 0
|
||||
}
|
||||
},
|
||||
|
|
@ -131,9 +132,6 @@ export default defineComponent({
|
|||
emit('nextcloud:unified-search.search', { query })
|
||||
})
|
||||
|
||||
// Read the header input element from the child so the modal's focus trap can include it
|
||||
this.inputElement = (this.$refs.searchInput as { getInputElement?: () => HTMLElement | null })?.getInputElement?.() ?? null
|
||||
|
||||
// all done
|
||||
logger.debug('Unified search initialized!')
|
||||
},
|
||||
|
|
|
|||
4
dist/2426-2426.js
vendored
4
dist/2426-2426.js
vendored
File diff suppressed because one or more lines are too long
3
dist/2426-2426.js.license
vendored
3
dist/2426-2426.js.license
vendored
|
|
@ -128,6 +128,9 @@ This file is generated from multiple sources. Included packages:
|
|||
- escape-html
|
||||
- version: 1.0.3
|
||||
- license: MIT
|
||||
- focus-trap
|
||||
- version: 8.2.2
|
||||
- license: MIT
|
||||
- hookable
|
||||
- version: 5.5.3
|
||||
- license: MIT
|
||||
|
|
|
|||
2
dist/2426-2426.js.map
vendored
2
dist/2426-2426.js.map
vendored
File diff suppressed because one or more lines are too long
4
dist/core-common.js
vendored
4
dist/core-common.js
vendored
File diff suppressed because one or more lines are too long
3
dist/core-common.js.license
vendored
3
dist/core-common.js.license
vendored
|
|
@ -360,6 +360,9 @@ This file is generated from multiple sources. Included packages:
|
|||
- floating-vue
|
||||
- version: 1.0.0-beta.19
|
||||
- license: MIT
|
||||
- focus-trap
|
||||
- version: 8.2.2
|
||||
- license: MIT
|
||||
- hast-to-hyperscript
|
||||
- version: 10.0.3
|
||||
- license: MIT
|
||||
|
|
|
|||
2
dist/core-common.js.map
vendored
2
dist/core-common.js.map
vendored
File diff suppressed because one or more lines are too long
4
dist/core-login.js
vendored
4
dist/core-login.js
vendored
File diff suppressed because one or more lines are too long
3
dist/core-login.js.license
vendored
3
dist/core-login.js.license
vendored
|
|
@ -221,6 +221,9 @@ This file is generated from multiple sources. Included packages:
|
|||
- filter-obj
|
||||
- version: 5.1.0
|
||||
- license: MIT
|
||||
- focus-trap
|
||||
- version: 8.2.2
|
||||
- license: MIT
|
||||
- hookable
|
||||
- version: 5.5.3
|
||||
- license: MIT
|
||||
|
|
|
|||
2
dist/core-login.js.map
vendored
2
dist/core-login.js.map
vendored
File diff suppressed because one or more lines are too long
4
dist/core-login_flow.js
vendored
4
dist/core-login_flow.js
vendored
|
|
@ -1,2 +1,2 @@
|
|||
(()=>{"use strict";var e,r,t,o={43925(e,r,t){var o=t(21777),a=t(81222),n=t(85471);t.nc=(0,o.aV)();const l=(0,n.$V)(()=>Promise.all([t.e(4208),t.e(2311)]).then(t.bind(t,72311))),i=(0,n.$V)(()=>Promise.all([t.e(4208),t.e(2426)]).then(t.bind(t,32426))),c=(0,n.$V)(()=>Promise.all([t.e(4208),t.e(9811)]).then(t.bind(t,59811))),d=(0,a.C)("core","loginFlowState");new n.Ay({render:e=>e("auth"===d?l:"grant"===d?i:c)}).$mount("#core-loginflow")}},a={};function n(e){var r=a[e];if(void 0!==r)return r.exports;var t=a[e]={id:e,loaded:!1,exports:{}};return o[e].call(t.exports,t,t.exports,n),t.loaded=!0,t.exports}n.m=o,e=[],n.O=(r,t,o,a)=>{if(!t){var l=1/0;for(u=0;u<e.length;u++){for(var[t,o,a]=e[u],i=!0,c=0;c<t.length;c++)(!1&a||l>=a)&&Object.keys(n.O).every(e=>n.O[e](t[c]))?t.splice(c--,1):(i=!1,a<l&&(l=a));if(i){e.splice(u--,1);var d=o();void 0!==d&&(r=d)}}return r}a=a||0;for(var u=e.length;u>0&&e[u-1][2]>a;u--)e[u]=e[u-1];e[u]=[t,o,a]},n.n=e=>{var r=e&&e.__esModule?()=>e.default:()=>e;return n.d(r,{a:r}),r},n.d=(e,r)=>{for(var t in r)n.o(r,t)&&!n.o(e,t)&&Object.defineProperty(e,t,{enumerable:!0,get:r[t]})},n.f={},n.e=e=>Promise.all(Object.keys(n.f).reduce((r,t)=>(n.f[t](e,r),r),[])),n.u=e=>e+"-"+e+".js?v="+{2311:"851ce89957dd56c95c18",2426:"579790c86922198cbd2a",9811:"975f353a252dd0095921"}[e],n.o=(e,r)=>Object.prototype.hasOwnProperty.call(e,r),r={},t="nextcloud-ui-legacy:",n.l=(e,o,a,l)=>{if(r[e])r[e].push(o);else{var i,c;if(void 0!==a)for(var d=document.getElementsByTagName("script"),u=0;u<d.length;u++){var s=d[u];if(s.getAttribute("src")==e||s.getAttribute("data-webpack")==t+a){i=s;break}}i||(c=!0,(i=document.createElement("script")).charset="utf-8",n.nc&&i.setAttribute("nonce",n.nc),i.setAttribute("data-webpack",t+a),i.src=e),r[e]=[o];var p=(t,o)=>{i.onerror=i.onload=null,clearTimeout(f);var a=r[e];if(delete r[e],i.parentNode&&i.parentNode.removeChild(i),a&&a.forEach(e=>e(o)),t)return t(o)},f=setTimeout(p.bind(null,void 0,{type:"timeout",target:i}),12e4);i.onerror=p.bind(null,i.onerror),i.onload=p.bind(null,i.onload),c&&document.head.appendChild(i)}},n.r=e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},n.nmd=e=>(e.paths=[],e.children||(e.children=[]),e),n.j=3181,(()=>{var e;globalThis.importScripts&&(e=globalThis.location+"");var r=globalThis.document;if(!e&&r&&(r.currentScript&&"SCRIPT"===r.currentScript.tagName.toUpperCase()&&(e=r.currentScript.src),!e)){var t=r.getElementsByTagName("script");if(t.length)for(var o=t.length-1;o>-1&&(!e||!/^http(s?):/.test(e));)e=t[o--].src}if(!e)throw new Error("Automatic publicPath is not supported in this browser");e=e.replace(/^blob:/,"").replace(/#.*$/,"").replace(/\?.*$/,"").replace(/\/[^\/]+$/,"/"),n.p=e})(),(()=>{n.b="undefined"!=typeof document&&document.baseURI||self.location.href;var e={3181:0};n.f.j=(r,t)=>{var o=n.o(e,r)?e[r]:void 0;if(0!==o)if(o)t.push(o[2]);else{var a=new Promise((t,a)=>o=e[r]=[t,a]);t.push(o[2]=a);var l=n.p+n.u(r),i=new Error;n.l(l,t=>{if(n.o(e,r)&&(0!==(o=e[r])&&(e[r]=void 0),o)){var a=t&&("load"===t.type?"missing":t.type),l=t&&t.target&&t.target.src;i.message="Loading chunk "+r+" failed.\n("+a+": "+l+")",i.name="ChunkLoadError",i.type=a,i.request=l,o[1](i)}},"chunk-"+r,r)}},n.O.j=r=>0===e[r];var r=(r,t)=>{var o,a,[l,i,c]=t,d=0;if(l.some(r=>0!==e[r])){for(o in i)n.o(i,o)&&(n.m[o]=i[o]);if(c)var u=c(n)}for(r&&r(t);d<l.length;d++)a=l[d],n.o(e,a)&&e[a]&&e[a][0](),e[a]=0;return n.O(u)},t=globalThis.webpackChunknextcloud_ui_legacy=globalThis.webpackChunknextcloud_ui_legacy||[];t.forEach(r.bind(null,0)),t.push=r.bind(null,t.push.bind(t))})(),n.nc=void 0;var l=n.O(void 0,[4208],()=>n(43925));l=n.O(l)})();
|
||||
//# sourceMappingURL=core-login_flow.js.map?v=36a4298a97527bc500f9
|
||||
(()=>{"use strict";var e,r,t,o={43925(e,r,t){var o=t(21777),a=t(81222),n=t(85471);t.nc=(0,o.aV)();const l=(0,n.$V)(()=>Promise.all([t.e(4208),t.e(2311)]).then(t.bind(t,72311))),i=(0,n.$V)(()=>Promise.all([t.e(4208),t.e(2426)]).then(t.bind(t,32426))),c=(0,n.$V)(()=>Promise.all([t.e(4208),t.e(9811)]).then(t.bind(t,59811))),d=(0,a.C)("core","loginFlowState");new n.Ay({render:e=>e("auth"===d?l:"grant"===d?i:c)}).$mount("#core-loginflow")}},a={};function n(e){var r=a[e];if(void 0!==r)return r.exports;var t=a[e]={id:e,loaded:!1,exports:{}};return o[e].call(t.exports,t,t.exports,n),t.loaded=!0,t.exports}n.m=o,e=[],n.O=(r,t,o,a)=>{if(!t){var l=1/0;for(u=0;u<e.length;u++){for(var[t,o,a]=e[u],i=!0,c=0;c<t.length;c++)(!1&a||l>=a)&&Object.keys(n.O).every(e=>n.O[e](t[c]))?t.splice(c--,1):(i=!1,a<l&&(l=a));if(i){e.splice(u--,1);var d=o();void 0!==d&&(r=d)}}return r}a=a||0;for(var u=e.length;u>0&&e[u-1][2]>a;u--)e[u]=e[u-1];e[u]=[t,o,a]},n.n=e=>{var r=e&&e.__esModule?()=>e.default:()=>e;return n.d(r,{a:r}),r},n.d=(e,r)=>{for(var t in r)n.o(r,t)&&!n.o(e,t)&&Object.defineProperty(e,t,{enumerable:!0,get:r[t]})},n.f={},n.e=e=>Promise.all(Object.keys(n.f).reduce((r,t)=>(n.f[t](e,r),r),[])),n.u=e=>e+"-"+e+".js?v="+{2311:"851ce89957dd56c95c18",2426:"ad80104d3c7b9fa0e6ae",9811:"975f353a252dd0095921"}[e],n.o=(e,r)=>Object.prototype.hasOwnProperty.call(e,r),r={},t="nextcloud-ui-legacy:",n.l=(e,o,a,l)=>{if(r[e])r[e].push(o);else{var i,c;if(void 0!==a)for(var d=document.getElementsByTagName("script"),u=0;u<d.length;u++){var s=d[u];if(s.getAttribute("src")==e||s.getAttribute("data-webpack")==t+a){i=s;break}}i||(c=!0,(i=document.createElement("script")).charset="utf-8",n.nc&&i.setAttribute("nonce",n.nc),i.setAttribute("data-webpack",t+a),i.src=e),r[e]=[o];var p=(t,o)=>{i.onerror=i.onload=null,clearTimeout(f);var a=r[e];if(delete r[e],i.parentNode&&i.parentNode.removeChild(i),a&&a.forEach(e=>e(o)),t)return t(o)},f=setTimeout(p.bind(null,void 0,{type:"timeout",target:i}),12e4);i.onerror=p.bind(null,i.onerror),i.onload=p.bind(null,i.onload),c&&document.head.appendChild(i)}},n.r=e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},n.nmd=e=>(e.paths=[],e.children||(e.children=[]),e),n.j=3181,(()=>{var e;globalThis.importScripts&&(e=globalThis.location+"");var r=globalThis.document;if(!e&&r&&(r.currentScript&&"SCRIPT"===r.currentScript.tagName.toUpperCase()&&(e=r.currentScript.src),!e)){var t=r.getElementsByTagName("script");if(t.length)for(var o=t.length-1;o>-1&&(!e||!/^http(s?):/.test(e));)e=t[o--].src}if(!e)throw new Error("Automatic publicPath is not supported in this browser");e=e.replace(/^blob:/,"").replace(/#.*$/,"").replace(/\?.*$/,"").replace(/\/[^\/]+$/,"/"),n.p=e})(),(()=>{n.b="undefined"!=typeof document&&document.baseURI||self.location.href;var e={3181:0};n.f.j=(r,t)=>{var o=n.o(e,r)?e[r]:void 0;if(0!==o)if(o)t.push(o[2]);else{var a=new Promise((t,a)=>o=e[r]=[t,a]);t.push(o[2]=a);var l=n.p+n.u(r),i=new Error;n.l(l,t=>{if(n.o(e,r)&&(0!==(o=e[r])&&(e[r]=void 0),o)){var a=t&&("load"===t.type?"missing":t.type),l=t&&t.target&&t.target.src;i.message="Loading chunk "+r+" failed.\n("+a+": "+l+")",i.name="ChunkLoadError",i.type=a,i.request=l,o[1](i)}},"chunk-"+r,r)}},n.O.j=r=>0===e[r];var r=(r,t)=>{var o,a,[l,i,c]=t,d=0;if(l.some(r=>0!==e[r])){for(o in i)n.o(i,o)&&(n.m[o]=i[o]);if(c)var u=c(n)}for(r&&r(t);d<l.length;d++)a=l[d],n.o(e,a)&&e[a]&&e[a][0](),e[a]=0;return n.O(u)},t=globalThis.webpackChunknextcloud_ui_legacy=globalThis.webpackChunknextcloud_ui_legacy||[];t.forEach(r.bind(null,0)),t.push=r.bind(null,t.push.bind(t))})(),n.nc=void 0;var l=n.O(void 0,[4208],()=>n(43925));l=n.O(l)})();
|
||||
//# sourceMappingURL=core-login_flow.js.map?v=5e97ca85bd50d607a4bd
|
||||
2
dist/core-login_flow.js.map
vendored
2
dist/core-login_flow.js.map
vendored
File diff suppressed because one or more lines are too long
4
dist/core-main.js
vendored
4
dist/core-main.js
vendored
File diff suppressed because one or more lines are too long
3
dist/core-main.js.license
vendored
3
dist/core-main.js.license
vendored
|
|
@ -253,6 +253,9 @@ This file is generated from multiple sources. Included packages:
|
|||
- floating-vue
|
||||
- version: 1.0.0-beta.19
|
||||
- license: MIT
|
||||
- focus-trap
|
||||
- version: 8.2.2
|
||||
- license: MIT
|
||||
- hookable
|
||||
- version: 5.5.3
|
||||
- license: MIT
|
||||
|
|
|
|||
2
dist/core-main.js.map
vendored
2
dist/core-main.js.map
vendored
File diff suppressed because one or more lines are too long
4
dist/core-recommendedapps.js
vendored
4
dist/core-recommendedapps.js
vendored
File diff suppressed because one or more lines are too long
3
dist/core-recommendedapps.js.license
vendored
3
dist/core-recommendedapps.js.license
vendored
|
|
@ -129,6 +129,9 @@ This file is generated from multiple sources. Included packages:
|
|||
- escape-html
|
||||
- version: 1.0.3
|
||||
- license: MIT
|
||||
- focus-trap
|
||||
- version: 8.2.2
|
||||
- license: MIT
|
||||
- hookable
|
||||
- version: 5.5.3
|
||||
- license: MIT
|
||||
|
|
|
|||
2
dist/core-recommendedapps.js.map
vendored
2
dist/core-recommendedapps.js.map
vendored
File diff suppressed because one or more lines are too long
3
dist/core-unified-search.js.license
vendored
3
dist/core-unified-search.js.license
vendored
|
|
@ -115,6 +115,9 @@ This file is generated from multiple sources. Included packages:
|
|||
- floating-vue
|
||||
- version: 1.0.0-beta.19
|
||||
- license: MIT
|
||||
- focus-trap
|
||||
- version: 8.2.2
|
||||
- license: MIT
|
||||
- ieee754
|
||||
- version: 1.2.1
|
||||
- license: BSD-3-Clause
|
||||
|
|
|
|||
4
dist/settings-declarative-settings-forms.js
vendored
4
dist/settings-declarative-settings-forms.js
vendored
File diff suppressed because one or more lines are too long
|
|
@ -217,6 +217,9 @@ This file is generated from multiple sources. Included packages:
|
|||
- floating-vue
|
||||
- version: 1.0.0-beta.19
|
||||
- license: MIT
|
||||
- focus-trap
|
||||
- version: 8.2.2
|
||||
- license: MIT
|
||||
- hookable
|
||||
- version: 5.5.3
|
||||
- license: MIT
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -191,6 +191,9 @@ This file is generated from multiple sources. Included packages:
|
|||
- escape-html
|
||||
- version: 1.0.3
|
||||
- license: MIT
|
||||
- focus-trap
|
||||
- version: 8.2.2
|
||||
- license: MIT
|
||||
- hookable
|
||||
- version: 5.5.3
|
||||
- license: MIT
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -217,6 +217,9 @@ This file is generated from multiple sources. Included packages:
|
|||
- floating-vue
|
||||
- version: 1.0.0-beta.19
|
||||
- license: MIT
|
||||
- focus-trap
|
||||
- version: 8.2.2
|
||||
- license: MIT
|
||||
- hookable
|
||||
- version: 5.5.3
|
||||
- license: MIT
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
4
dist/settings-vue-settings-admin-mail.js
vendored
4
dist/settings-vue-settings-admin-mail.js
vendored
File diff suppressed because one or more lines are too long
|
|
@ -294,6 +294,9 @@ This file is generated from multiple sources. Included packages:
|
|||
- floating-vue
|
||||
- version: 1.0.0-beta.19
|
||||
- license: MIT
|
||||
- focus-trap
|
||||
- version: 8.2.2
|
||||
- license: MIT
|
||||
- hast-to-hyperscript
|
||||
- version: 10.0.3
|
||||
- license: MIT
|
||||
|
|
|
|||
2
dist/settings-vue-settings-admin-mail.js.map
vendored
2
dist/settings-vue-settings-admin-mail.js.map
vendored
File diff suppressed because one or more lines are too long
4
dist/settings-vue-settings-admin-security.js
vendored
4
dist/settings-vue-settings-admin-security.js
vendored
File diff suppressed because one or more lines are too long
|
|
@ -218,6 +218,9 @@ This file is generated from multiple sources. Included packages:
|
|||
- floating-vue
|
||||
- version: 1.0.0-beta.19
|
||||
- license: MIT
|
||||
- focus-trap
|
||||
- version: 8.2.2
|
||||
- license: MIT
|
||||
- hookable
|
||||
- version: 5.5.3
|
||||
- license: MIT
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
4
dist/settings-vue-settings-personal-info.js
vendored
4
dist/settings-vue-settings-personal-info.js
vendored
File diff suppressed because one or more lines are too long
|
|
@ -230,6 +230,9 @@ This file is generated from multiple sources. Included packages:
|
|||
- floating-vue
|
||||
- version: 1.0.0-beta.19
|
||||
- license: MIT
|
||||
- focus-trap
|
||||
- version: 8.2.2
|
||||
- license: MIT
|
||||
- hookable
|
||||
- version: 5.5.3
|
||||
- license: MIT
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -205,6 +205,9 @@ This file is generated from multiple sources. Included packages:
|
|||
- floating-vue
|
||||
- version: 1.0.0-beta.19
|
||||
- license: MIT
|
||||
- focus-trap
|
||||
- version: 8.2.2
|
||||
- license: MIT
|
||||
- hookable
|
||||
- version: 5.5.3
|
||||
- license: MIT
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -196,6 +196,9 @@ This file is generated from multiple sources. Included packages:
|
|||
- floating-vue
|
||||
- version: 1.0.0-beta.19
|
||||
- license: MIT
|
||||
- focus-trap
|
||||
- version: 8.2.2
|
||||
- license: MIT
|
||||
- hookable
|
||||
- version: 5.5.3
|
||||
- license: MIT
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -322,6 +322,9 @@ This file is generated from multiple sources. Included packages:
|
|||
- floating-vue
|
||||
- version: 1.0.0-beta.19
|
||||
- license: MIT
|
||||
- focus-trap
|
||||
- version: 8.2.2
|
||||
- license: MIT
|
||||
- hast-to-hyperscript
|
||||
- version: 10.0.3
|
||||
- license: MIT
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
4
dist/workflowengine-workflowengine.js
vendored
4
dist/workflowengine-workflowengine.js
vendored
File diff suppressed because one or more lines are too long
|
|
@ -229,6 +229,9 @@ This file is generated from multiple sources. Included packages:
|
|||
- floating-vue
|
||||
- version: 1.0.0-beta.19
|
||||
- license: MIT
|
||||
- focus-trap
|
||||
- version: 8.2.2
|
||||
- license: MIT
|
||||
- hookable
|
||||
- version: 5.5.3
|
||||
- license: MIT
|
||||
|
|
|
|||
2
dist/workflowengine-workflowengine.js.map
vendored
2
dist/workflowengine-workflowengine.js.map
vendored
File diff suppressed because one or more lines are too long
Loading…
Reference in a new issue