Basic focus management support (#1719)

Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
This commit is contained in:
Andrey Sobolev 2022-05-12 11:10:52 +07:00 committed by GitHub
parent fe109a6b30
commit fee0f28b07
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 573 additions and 138 deletions

View File

@ -389,7 +389,7 @@ export function createModel (builder: Builder): void {
action: view.actionImpl.SelectItem, action: view.actionImpl.SelectItem,
keyBinding: ['keyX'], keyBinding: ['keyX'],
category: view.category.General, category: view.category.General,
input: 'focus', input: 'any',
target: core.class.Doc, target: core.class.Doc,
context: { mode: 'browser' } context: { mode: 'browser' }
}, },
@ -434,7 +434,7 @@ export function createModel (builder: Builder): void {
input: 'none', input: 'none',
target: core.class.Doc, target: core.class.Doc,
context: { context: {
mode: ['workbench', 'browser', 'popup', 'panel', 'editor', 'input'] mode: ['workbench', 'browser', 'panel', 'editor', 'input']
} }
}, },
view.action.ShowActions view.action.ShowActions

View File

@ -45,7 +45,7 @@
{#if $$slots.space} {#if $$slots.space}
<slot name="space" /> <slot name="space" />
{:else if spaceClass && spaceLabel && spacePlaceholder} {:else if spaceClass && spaceLabel && spacePlaceholder}
<SpaceSelect _class={spaceClass} {spaceQuery} label={spaceLabel} bind:value={space} /> <SpaceSelect focus focusIndex={-10} _class={spaceClass} {spaceQuery} label={spaceLabel} bind:value={space} />
{/if} {/if}
<span class="antiCard-header__divider"></span> <span class="antiCard-header__divider"></span>
{/if} {/if}
@ -53,6 +53,7 @@
</div> </div>
<div class="buttons-group small-gap"> <div class="buttons-group small-gap">
<Button <Button
focusIndex={10002}
icon={IconClose} icon={IconClose}
kind={'transparent'} kind={'transparent'}
on:click={() => { on:click={() => {
@ -73,6 +74,7 @@
<MiniToggle label={presentation.string.CreateMore} bind:on={createMore} /> <MiniToggle label={presentation.string.CreateMore} bind:on={createMore} />
{/if} {/if}
<Button <Button
focusIndex={10001}
disabled={!canSave} disabled={!canSave}
label={okLabel} label={okLabel}
kind={'primary'} kind={'primary'}

View File

@ -16,7 +16,7 @@
import type { IntlString } from '@anticrm/platform' import type { IntlString } from '@anticrm/platform'
import { getClient } from '../utils' import { getClient } from '../utils'
import { Label, showPopup, IconFolder, Button, eventToHTMLElement } from '@anticrm/ui' import { Label, showPopup, IconFolder, Button, eventToHTMLElement, getFocusManager } from '@anticrm/ui'
import SpacesPopup from './SpacesPopup.svelte' import SpacesPopup from './SpacesPopup.svelte'
import type { Ref, Class, Space, DocumentQuery } from '@anticrm/core' import type { Ref, Class, Space, DocumentQuery } from '@anticrm/core'
@ -25,11 +25,14 @@
export let spaceQuery: DocumentQuery<Space> | undefined = { archived: false } export let spaceQuery: DocumentQuery<Space> | undefined = { archived: false }
export let label: IntlString export let label: IntlString
export let value: Ref<Space> | undefined export let value: Ref<Space> | undefined
export let focusIndex = -1
export let focus = false
let selected: Space | undefined let selected: Space | undefined
const client = getClient() const client = getClient()
const mgr = getFocusManager()
async function updateSelected (value: Ref<Space> | undefined) { async function updateSelected (value: Ref<Space> | undefined) {
selected = value !== undefined ? await client.findOne(_class, { ...(spaceQuery ?? {}), _id: value }) : undefined selected = value !== undefined ? await client.findOne(_class, { ...(spaceQuery ?? {}), _id: value }) : undefined
} }
@ -38,6 +41,8 @@
</script> </script>
<Button <Button
{focus}
{focusIndex}
icon={IconFolder} icon={IconFolder}
size={'small'} size={'small'}
kind={'no-border'} kind={'no-border'}
@ -45,6 +50,7 @@
showPopup(SpacesPopup, { _class, spaceQuery }, eventToHTMLElement(ev), (result) => { showPopup(SpacesPopup, { _class, spaceQuery }, eventToHTMLElement(ev), (result) => {
if (result) { if (result) {
value = result._id value = result._id
mgr?.setFocusPos(focusIndex)
} }
}) })
}} }}

View File

@ -19,6 +19,7 @@
import { createQuery } from '../utils' import { createQuery } from '../utils'
import SpaceInfo from './SpaceInfo.svelte' import SpaceInfo from './SpaceInfo.svelte'
import presentation from '..' import presentation from '..'
import { ListView } from '@anticrm/ui'
export let _class: Ref<Class<Space>> export let _class: Ref<Class<Space>>
export let spaceQuery: DocumentQuery<Space> | undefined export let spaceQuery: DocumentQuery<Space> | undefined
@ -42,24 +43,64 @@
onMount(() => { onMount(() => {
if (input) input.focus() if (input) input.focus()
}) })
let selection = 0
let list: ListView
async function handleSelection (evt: Event | undefined, selection: number): Promise<void> {
const space = objects[selection]
dispatch('close', space)
}
function onKeydown (key: KeyboardEvent): void {
if (key.code === 'ArrowUp') {
key.stopPropagation()
key.preventDefault()
list.select(selection - 1)
}
if (key.code === 'ArrowDown') {
key.stopPropagation()
key.preventDefault()
list.select(selection + 1)
}
if (key.code === 'Enter') {
key.preventDefault()
key.stopPropagation()
handleSelection(key, selection)
}
if (key.code === 'Escape') {
key.preventDefault()
key.stopPropagation()
dispatch('close')
}
}
</script> </script>
<div class="selectPopup"> <div class="selectPopup" on:keydown={onKeydown}>
<div class="header"> <div class="header">
<input bind:this={input} type="text" bind:value={search} placeholder={phTraslate} on:input={() => {}} on:change /> <input bind:this={input} type="text" bind:value={search} placeholder={phTraslate} on:input={() => {}} on:change />
</div> </div>
<div class="scroll"> <div class="scroll">
<div class="box"> <div class="box">
{#each objects as space} <ListView
<button bind:this={list}
class="menu-item flex-between" count={objects.length}
on:click={() => { bind:selection
dispatch('close', space) on:click={(evt) => handleSelection(evt, evt.detail)}
}} >
> <svelte:fragment slot="item" let:item>
<SpaceInfo size={'large'} value={space} /> {@const space = objects[item]}
</button>
{/each} <button
class="menu-item flex-between"
on:click={() => {
handleSelection(undefined, item)
}}
>
<SpaceInfo size={'large'} value={space} />
</button>
</svelte:fragment>
</ListView>
</div> </div>
</div> </div>
</div> </div>

View File

@ -17,7 +17,7 @@
import contact, { Contact, formatName } from '@anticrm/contact' import contact, { Contact, formatName } from '@anticrm/contact'
import type { Class, Ref } from '@anticrm/core' import type { Class, Ref } from '@anticrm/core'
import type { IntlString } from '@anticrm/platform' import type { IntlString } from '@anticrm/platform'
import type { TooltipAlignment, ButtonKind, ButtonSize } from '@anticrm/ui' import { TooltipAlignment, ButtonKind, ButtonSize, getFocusManager } from '@anticrm/ui'
import { Button, Label, showPopup, Tooltip } from '@anticrm/ui' import { Button, Label, showPopup, Tooltip } from '@anticrm/ui'
import { createEventDispatcher } from 'svelte' import { createEventDispatcher } from 'svelte'
import presentation from '..' import presentation from '..'
@ -38,6 +38,7 @@
export let justify: 'left' | 'center' = 'center' export let justify: 'left' | 'center' = 'center'
export let width: string | undefined = undefined export let width: string | undefined = undefined
export let labelDirection: TooltipAlignment | undefined = undefined export let labelDirection: TooltipAlignment | undefined = undefined
export let focusIndex = -1
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
@ -56,11 +57,13 @@
const isPerson = client.getHierarchy().isDerived(obj._class, contact.class.Person) const isPerson = client.getHierarchy().isDerived(obj._class, contact.class.Person)
return isPerson ? formatName(obj.name) : obj.name return isPerson ? formatName(obj.name) : obj.name
} }
const mgr = getFocusManager()
</script> </script>
<div bind:this={container} class="min-w-0"> <div bind:this={container} class="min-w-0">
<Tooltip {label} fill={width === '100%'} direction={labelDirection}> <Tooltip {label} fill={width === '100%'} direction={labelDirection}>
<Button <Button
{focusIndex}
icon={size === 'x-large' && selected ? undefined : IconPerson} icon={size === 'x-large' && selected ? undefined : IconPerson}
width={width ?? 'min-content'} width={width ?? 'min-content'}
{size} {size}
@ -81,6 +84,7 @@
value = result._id value = result._id
dispatch('change', value) dispatch('change', value)
} }
mgr?.setFocusPos(focusIndex)
} }
) )
} }

View File

@ -17,7 +17,7 @@
import { translate } from '@anticrm/platform' import { translate } from '@anticrm/platform'
import { createEventDispatcher, onMount } from 'svelte' import { createEventDispatcher, onMount } from 'svelte'
import { Tooltip, CheckBox } from '@anticrm/ui' import { Tooltip, CheckBox, ListView } from '@anticrm/ui'
import UserInfo from './UserInfo.svelte' import UserInfo from './UserInfo.svelte'
import type { Ref, Class } from '@anticrm/core' import type { Ref, Class } from '@anticrm/core'
@ -70,43 +70,85 @@
onMount(() => { onMount(() => {
if (input) input.focus() if (input) input.focus()
}) })
let selection = 0
let list: ListView
async function handleSelection (evt: Event | undefined, selection: number): Promise<void> {
const person = objects[selection]
if (!multiSelect) {
selected = person._id === selected ? undefined : person._id
dispatch('close', selected !== undefined ? person : undefined)
} else {
checkSelected(person)
}
}
function onKeydown (key: KeyboardEvent): void {
if (key.code === 'ArrowUp') {
key.stopPropagation()
key.preventDefault()
list.select(selection - 1)
}
if (key.code === 'ArrowDown') {
key.stopPropagation()
key.preventDefault()
list.select(selection + 1)
}
if (key.code === 'Enter') {
key.preventDefault()
key.stopPropagation()
handleSelection(key, selection)
}
if (key.code === 'Escape') {
key.preventDefault()
key.stopPropagation()
dispatch('close')
}
}
</script> </script>
<div class="selectPopup" class:plainContainer={!shadows}> <div class="selectPopup" class:plainContainer={!shadows} on:keydown={onKeydown}>
<div class="header"> <div class="header">
<input bind:this={input} type="text" bind:value={search} placeholder={phTraslate} on:change /> <input bind:this={input} type="text" bind:value={search} placeholder={phTraslate} on:change />
</div> </div>
<div class="scroll"> <div class="scroll">
<div class="box"> <div class="box">
{#each objects as person} <ListView
<button bind:this={list}
class="menu-item" count={objects.length}
on:click={() => { bind:selection
if (!multiSelect) { on:click={(evt) => handleSelection(evt, evt.detail)}
selected = person._id === selected ? undefined : person._id >
dispatch('close', selected !== undefined ? person : undefined) <svelte:fragment slot="item" let:item>
} else checkSelected(person) {@const person = objects[item]}
}} <button
> class="menu-item w-full"
{#if multiSelect} on:click={() => {
<div class="check pointer-events-none"> handleSelection(undefined, item)
<CheckBox checked={isSelected(person)} primary /> }}
</div> >
{/if} {#if multiSelect}
<UserInfo size={'x-small'} value={person} /> <div class="check pointer-events-none">
{#if allowDeselect && person._id === selected} <CheckBox checked={isSelected(person)} primary />
<div class="check-right pointer-events-none"> </div>
{#if titleDeselect} {/if}
<Tooltip label={titleDeselect ?? presentation.string.Deselect}> <UserInfo size={'x-small'} value={person} />
{#if allowDeselect && person._id === selected}
<div class="check-right pointer-events-none">
{#if titleDeselect}
<Tooltip label={titleDeselect ?? presentation.string.Deselect}>
<CheckBox checked circle primary />
</Tooltip>
{:else}
<CheckBox checked circle primary /> <CheckBox checked circle primary />
</Tooltip> {/if}
{:else} </div>
<CheckBox checked circle primary /> {/if}
{/if} </button>
</div> </svelte:fragment>
{/if} </ListView>
</button>
{/each}
</div> </div>
</div> </div>
</div> </div>

View File

@ -13,12 +13,13 @@
// limitations under the License. // limitations under the License.
--> -->
<script lang="ts"> <script lang="ts">
import type { IntlString, Asset } from '@anticrm/platform' import type { Asset, IntlString } from '@anticrm/platform'
import type { AnySvelteComponent, ButtonKind, ButtonSize } from '../types'
import Spinner from './Spinner.svelte'
import Label from './Label.svelte'
import Icon from './Icon.svelte'
import { onMount } from 'svelte' import { onMount } from 'svelte'
import { registerFocus } from '../focus'
import type { AnySvelteComponent, ButtonKind, ButtonSize } from '../types'
import Icon from './Icon.svelte'
import Label from './Label.svelte'
import Spinner from './Spinner.svelte'
export let label: IntlString | undefined = undefined export let label: IntlString | undefined = undefined
export let labelParams: Record<string, any> = {} export let labelParams: Record<string, any> = {}
@ -38,7 +39,6 @@
export let title: string | undefined = undefined export let title: string | undefined = undefined
export let borderStyle: 'solid' | 'dashed' = 'solid' export let borderStyle: 'solid' | 'dashed' = 'solid'
export let id: string | undefined = undefined export let id: string | undefined = undefined
export let input: HTMLButtonElement | undefined = undefined export let input: HTMLButtonElement | undefined = undefined
$: iconOnly = label === undefined && $$slots.content === undefined $: iconOnly = label === undefined && $$slots.content === undefined
@ -53,8 +53,31 @@
click = false click = false
} }
}) })
// Focusable control with index
export let focusIndex = -1
const { idx, focusManager } = registerFocus(focusIndex, {
focus: () => {
if (!disabled) {
input?.focus()
}
return !disabled && input != null
},
isFocus: () => document.activeElement === input
})
$: if (idx !== -1 && focusManager) {
focusManager.updateFocus(idx, focusIndex)
}
$: if (input != null) {
input.addEventListener('focus', () => {
focusManager?.setFocus(idx)
})
}
</script> </script>
<!-- {focusIndex} -->
<button <button
bind:this={input} bind:this={input}
class="button {kind} {size} jf-{justify}" class="button {kind} {size} jf-{justify}"
@ -172,7 +195,7 @@
} }
} }
&:focus { &:focus {
border-color: var(--primary-edit-border-color); border-color: var(--accent-color) !important;
} }
&:disabled { &:disabled {
color: rgb(var(--caption-color) / 40%); color: rgb(var(--caption-color) / 40%);

View File

@ -15,8 +15,8 @@
<script lang="ts"> <script lang="ts">
import type { IntlString } from '@anticrm/platform' import type { IntlString } from '@anticrm/platform'
import { translate } from '@anticrm/platform' import { translate } from '@anticrm/platform'
import { createEventDispatcher } from 'svelte' import { createEventDispatcher, onMount } from 'svelte'
import { getPlatformColor } from '..' import { getPlatformColor, ListView } from '..'
export let placeholder: IntlString | undefined = undefined export let placeholder: IntlString | undefined = undefined
export let placeholderParam: any | undefined = undefined export let placeholderParam: any | undefined = undefined
@ -33,27 +33,79 @@
} }
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
$: objects = value.filter((el) => el.label.toLowerCase().includes(search.toLowerCase()))
let selection = 0
let list: ListView
async function handleSelection (evt: Event | undefined, selection: number): Promise<void> {
const space = objects[selection]
dispatch('close', space)
}
function onKeydown (key: KeyboardEvent): void {
if (key.code === 'ArrowUp') {
key.stopPropagation()
key.preventDefault()
list.select(selection - 1)
}
if (key.code === 'ArrowDown') {
key.stopPropagation()
key.preventDefault()
list.select(selection + 1)
}
if (key.code === 'Enter') {
key.preventDefault()
key.stopPropagation()
handleSelection(key, selection)
}
if (key.code === 'Escape') {
key.preventDefault()
key.stopPropagation()
dispatch('close')
}
}
let input: HTMLElement
onMount(() => {
if (input) input.focus()
})
</script> </script>
<div class="selectPopup"> <div class="selectPopup" on:keydown={onKeydown}>
{#if searchable} {#if searchable}
<div class="header"> <div class="header">
<input type="text" bind:value={search} placeholder={phTraslate} on:input={(ev) => {}} on:change /> <input
bind:this={input}
type="text"
bind:value={search}
placeholder={phTraslate}
on:input={(ev) => {}}
on:change
/>
</div> </div>
{/if} {/if}
<div class="scroll"> <div class="scroll">
<div class="box"> <div class="box">
{#each value.filter((el) => el.label.toLowerCase().includes(search.toLowerCase())) as item} <ListView
<button bind:this={list}
class="menu-item" count={objects.length}
on:click={() => { bind:selection
dispatch('close', item) on:click={(evt) => handleSelection(evt, evt.detail)}
}} >
> <svelte:fragment slot="item" let:item>
<div class="color" style="background-color: {getPlatformColor(item.color)}" /> {@const itemValue = objects[item]}
<span class="label">{item.label}</span> <button
</button> class="menu-item"
{/each} on:click={() => {
dispatch('close', itemValue)
}}
>
<div class="color" style="background-color: {getPlatformColor(itemValue.color)}" />
<span class="label">{itemValue.label}</span>
</button>
</svelte:fragment>
</ListView>
</div> </div>
</div> </div>
</div> </div>

View File

@ -13,13 +13,14 @@
// limitations under the License. // limitations under the License.
--> -->
<script lang="ts"> <script lang="ts">
import { createEventDispatcher, onMount, afterUpdate } from 'svelte' import type { Asset, IntlString } from '@anticrm/platform'
import type { IntlString, Asset } from '@anticrm/platform'
import { translate } from '@anticrm/platform' import { translate } from '@anticrm/platform'
import type { AnySvelteComponent } from '../types' import { afterUpdate, createEventDispatcher, onMount } from 'svelte'
import Label from './Label.svelte' import { registerFocus } from '../focus'
import Icon from './Icon.svelte'
import plugin from '../plugin' import plugin from '../plugin'
import type { AnySvelteComponent } from '../types'
import Icon from './Icon.svelte'
import Label from './Label.svelte'
export let label: IntlString | undefined = undefined export let label: IntlString | undefined = undefined
export let icon: Asset | AnySvelteComponent | undefined = undefined export let icon: Asset | AnySvelteComponent | undefined = undefined
@ -66,6 +67,22 @@
afterUpdate(() => { afterUpdate(() => {
computeSize(input) computeSize(input)
}) })
// Focusable control with index
export let focusIndex = -1
const { idx, focusManager } = registerFocus(focusIndex, {
focus: () => {
input?.focus()
return input != null
},
isFocus: () => document.activeElement === input
})
$: if (input) {
input.addEventListener('focus', () => {
focusManager?.setFocus(idx)
})
}
</script> </script>
<div <div
@ -74,6 +91,7 @@
input.focus() input.focus()
}} }}
> >
<!-- {focusIndex} -->
<div class="hidden-text {kind}" bind:this={text} /> <div class="hidden-text {kind}" bind:this={text} />
{#if label}<div class="label"><Label {label} /></div>{/if} {#if label}<div class="label"><Label {label} /></div>{/if}
<div class="{kind} flex-row-center clear-mins"> <div class="{kind} flex-row-center clear-mins">

View File

@ -0,0 +1,16 @@
<script lang="ts">
import { FocusManager } from '../focus'
export let manager: FocusManager
function handleKey (evt: KeyboardEvent): void {
if (evt.code === 'Tab' && manager.hasFocus()) {
evt.preventDefault()
evt.stopPropagation()
manager.next(evt.shiftKey ? -1 : 1)
}
}
</script>
<svelte:window on:keydown={(evt) => handleKey(evt)} />
<slot />

View File

@ -13,11 +13,11 @@
// limitations under the License. // limitations under the License.
--> -->
<script lang="ts"> <script lang="ts">
import { createEventDispatcher, afterUpdate, onMount } from 'svelte' import { afterUpdate, createEventDispatcher, onMount } from 'svelte'
import ui from '../plugin'
import { Action } from '../types' import { Action } from '../types'
import Icon from './Icon.svelte' import Icon from './Icon.svelte'
import Label from './Label.svelte' import Label from './Label.svelte'
import ui from '../plugin'
export let actions: Action[] = [] export let actions: Action[] = []
export let ctx: any = undefined export let ctx: any = undefined
@ -26,6 +26,9 @@
const btns: HTMLButtonElement[] = [] const btns: HTMLButtonElement[] = []
const keyDown = (ev: KeyboardEvent, n: number): void => { const keyDown = (ev: KeyboardEvent, n: number): void => {
if (ev.key === 'Tab') {
dispatch('close')
}
if (ev.key === 'ArrowDown') { if (ev.key === 'ArrowDown') {
if (n === btns.length - 1) btns[0].focus() if (n === btns.length - 1) btns[0].focus()
else btns[n + 1].focus() else btns[n + 1].focus()

View File

@ -108,7 +108,7 @@
</div> </div>
</div> </div>
{#if props.element !== 'content'} {#if props.element !== 'content'}
<div class="modal-overlay" class:show on:click={() => escapeClose()} /> <div class="modal-overlay" class:show on:click={() => escapeClose()} on:keydown={() => {}} on:keyup={() => {}} />
{/if} {/if}
{/if} {/if}
{/if} {/if}

View File

@ -14,8 +14,8 @@
--> -->
<script lang="ts"> <script lang="ts">
import { afterUpdate, onDestroy } from 'svelte' import { afterUpdate, onDestroy } from 'svelte'
import { tooltipstore as tooltip, closeTooltip, Component } from '..'
import type { TooltipAlignment } from '..' import type { TooltipAlignment } from '..'
import { closeTooltip, Component, tooltipstore as tooltip } from '..'
import Label from './Label.svelte' import Label from './Label.svelte'
let tooltipHTML: HTMLElement let tooltipHTML: HTMLElement
@ -151,6 +151,13 @@
on:mousemove={(ev) => { on:mousemove={(ev) => {
whileShow(ev) whileShow(ev)
}} }}
on:keydown={(evt) => {
if (($tooltip.component || $tooltip.label) && evt.key === 'Escape') {
evt.preventDefault()
evt.stopImmediatePropagation()
hideTooltip()
}
}}
/> />
{#if $tooltip.component} {#if $tooltip.component}
<div class="popup-tooltip" class:doublePadding={$tooltip.label} bind:clientWidth={clWidth} bind:this={tooltipHTML}> <div class="popup-tooltip" class:doublePadding={$tooltip.label} bind:clientWidth={clWidth} bind:this={tooltipHTML}>

115
packages/ui/src/focus.ts Normal file
View File

@ -0,0 +1,115 @@
import { getContext, setContext } from 'svelte'
/**
* @public
*/
export interface FocusManager {
next: (inc?: 1 | -1) => void
setFocus: (idx: number) => void
setFocusPos: (order: number) => void
updateFocus: (idx: number, order: number) => void
// Check if current manager has focus
hasFocus: () => boolean
}
class FocusManagerImpl implements FocusManager {
counter = 0
elements: Array<{
id: number
order: number
focus: () => boolean
isFocus: () => boolean
}> = []
current = 0
register (order: number, focus: () => boolean, isFocus: () => boolean): number {
const el = { id: this.counter++, order, focus, isFocus }
this.elements.push(el)
this.sort()
return el.id
}
sort (): void {
// this.needSort = 0
this.elements.sort((a, b) => {
return a.order - b.order
})
}
next (inc?: 1 | -1): void {
while (true) {
this.current = this.current + (inc ?? 1)
if (this.elements[Math.abs(this.current) % this.elements.length].focus()) {
break
}
}
}
setFocus (idx: number): void {
this.current = this.elements.findIndex((it) => it.id === idx) ?? 0
this.elements[Math.abs(this.current) % this.elements.length].focus()
}
setFocusPos (order: number): void {
const idx = this.elements.findIndex((it) => it.order === order)
if (idx !== undefined) {
this.current = idx
this.elements[Math.abs(this.current) % this.elements.length].focus()
}
}
updateFocus (idx: number, order: number): void {
const el = this.elements.find((it) => it.id === idx)
if (el !== undefined) {
if (el.order !== order) {
el.order = order
this.sort()
}
}
}
hasFocus (): boolean {
for (const el of this.elements) {
if (el.isFocus()) {
return true
}
}
return false
}
}
/**
* @public
*/
export function createFocusManager (): FocusManager {
const mgr = new FocusManagerImpl()
setFocusManager(mgr)
return mgr
}
export function setFocusManager (manager: FocusManager): void {
setContext('ui.focus.elements', manager)
}
/**
* @public
*/
export function getFocusManager (): FocusManager | undefined {
return getContext('ui.focus.elements')
}
/**
* Register new focus reciever if order !== -1
* @public
*/
export function registerFocus (
order: number,
item: { focus: () => boolean, isFocus: () => boolean }
): { idx: number, focusManager?: FocusManager } {
const focusManager = getFocusManager() as FocusManagerImpl
if (order === -1) {
return { idx: -1, focusManager }
}
return { idx: focusManager?.register(order, item.focus, item.isFocus) ?? -1, focusManager }
}

View File

@ -128,11 +128,22 @@ export { default as IconDetails } from './components/icons/Details.svelte'
export { default as PanelInstance } from './components/PanelInstance.svelte' export { default as PanelInstance } from './components/PanelInstance.svelte'
export { default as Panel } from './components/Panel.svelte' export { default as Panel } from './components/Panel.svelte'
export { default as MonthCalendar } from './components/calendar/MonthCalendar.svelte'
export { default as YearCalendar } from './components/calendar/YearCalendar.svelte'
export { default as WeekCalendar } from './components/calendar/WeekCalendar.svelte'
export { default as FocusHandler } from './components/FocusHandler.svelte'
export { default as ListView } from './components/ListView.svelte'
export * from './types'
export * from './location'
export * from './utils' export * from './utils'
export * from './popups' export * from './popups'
export * from './tooltips' export * from './tooltips'
export * from './panelup' export * from './panelup'
export * from './components/calendar/internal/DateUtils' export * from './components/calendar/internal/DateUtils'
export * from './colors'
export * from './focus'
export function createApp (target: HTMLElement): SvelteComponent { export function createApp (target: HTMLElement): SvelteComponent {
return new Root({ target }) return new Root({ target })
@ -151,8 +162,3 @@ addStringsLoader(uiId, async (lang: string) => {
addLocation(uiId, async () => ({ default: async () => ({}) })) addLocation(uiId, async () => ({ default: async () => ({}) }))
export { default } from './plugin' export { default } from './plugin'
export * from './colors'
export { default as MonthCalendar } from './components/calendar/MonthCalendar.svelte'
export { default as YearCalendar } from './components/calendar/YearCalendar.svelte'
export { default as WeekCalendar } from './components/calendar/WeekCalendar.svelte'

View File

@ -1,8 +1,8 @@
import { AnySvelteComponent, AnyComponent, LabelAndProps, TooltipAlignment } from './types'
import { IntlString } from '@anticrm/platform' import { IntlString } from '@anticrm/platform'
import { writable } from 'svelte/store' import { writable } from 'svelte/store'
import { AnyComponent, AnySvelteComponent, LabelAndProps, TooltipAlignment } from './types'
export const tooltipstore = writable<LabelAndProps>({ const emptyTooltip: LabelAndProps = {
label: undefined, label: undefined,
element: undefined, element: undefined,
direction: undefined, direction: undefined,
@ -10,7 +10,8 @@ export const tooltipstore = writable<LabelAndProps>({
props: undefined, props: undefined,
anchor: undefined, anchor: undefined,
onUpdate: undefined onUpdate: undefined
}) }
export const tooltipstore = writable<LabelAndProps>(emptyTooltip)
export function showTooltip ( export function showTooltip (
label: IntlString | undefined, label: IntlString | undefined,
@ -33,13 +34,5 @@ export function showTooltip (
} }
export function closeTooltip (): void { export function closeTooltip (): void {
tooltipstore.set({ tooltipstore.set(emptyTooltip)
label: undefined,
element: undefined,
direction: undefined,
component: undefined,
props: undefined,
anchor: undefined,
onUpdate: undefined
})
} }

View File

@ -13,9 +13,9 @@
// limitations under the License. // limitations under the License.
// //
import type { Asset, IntlString } from '@anticrm/platform'
import { /* Metadata, Plugin, plugin, */ Resource /*, Service */ } from '@anticrm/platform' import { /* Metadata, Plugin, plugin, */ Resource /*, Service */ } from '@anticrm/platform'
import { /* getContext, */ SvelteComponent } from 'svelte' import { /* getContext, */ SvelteComponent } from 'svelte'
import type { Asset, IntlString } from '@anticrm/platform'
/** /**
* Describe a browser URI location parsed to path, query and fragment. * Describe a browser URI location parsed to path, query and fragment.

View File

@ -16,8 +16,9 @@
import { createEventDispatcher, onMount } from 'svelte' import { createEventDispatcher, onMount } from 'svelte'
import type { IntlString } from '@anticrm/platform' import type { IntlString } from '@anticrm/platform'
import { translate } from '@anticrm/platform' import { translate } from '@anticrm/platform'
import { Button, IconClose, closeTooltip, IconBlueCheck } from '@anticrm/ui' import { Button, IconClose, closeTooltip, IconBlueCheck, registerFocus, createFocusManager } from '@anticrm/ui'
import IconCopy from './icons/Copy.svelte' import IconCopy from './icons/Copy.svelte'
import { FocusHandler } from '@anticrm/ui'
export let value: string = '' export let value: string = ''
export let placeholder: IntlString export let placeholder: IntlString
@ -31,8 +32,25 @@
onMount(() => { onMount(() => {
if (input) input.focus() if (input) input.focus()
}) })
const mgr = createFocusManager()
const { idx } = registerFocus(1, {
focus: () => {
input?.focus()
return true
},
isFocus: () => document.activeElement === input
})
$: if (input) {
input.addEventListener('focus', () => {
mgr.setFocus(idx)
})
}
</script> </script>
<FocusHandler manager={mgr} />
<div class="buttons-group xsmall-gap"> <div class="buttons-group xsmall-gap">
{#if editable} {#if editable}
<input <input
@ -51,6 +69,7 @@
on:change on:change
/> />
<Button <Button
focusIndex={2}
kind={'transparent'} kind={'transparent'}
size={'small'} size={'small'}
icon={IconClose} icon={IconClose}
@ -66,6 +85,7 @@
<span>{value}</span> <span>{value}</span>
{/if} {/if}
<Button <Button
focusIndex={3}
kind={'transparent'} kind={'transparent'}
size={'small'} size={'small'}
icon={IconCopy} icon={IconCopy}
@ -75,6 +95,7 @@
/> />
{#if editable} {#if editable}
<Button <Button
focusIndex={4}
kind={'transparent'} kind={'transparent'}
size={'small'} size={'small'}
icon={IconBlueCheck} icon={IconBlueCheck}

View File

@ -14,17 +14,28 @@
// limitations under the License. // limitations under the License.
--> -->
<script lang="ts"> <script lang="ts">
import { createEventDispatcher } from 'svelte'
import type { Channel, ChannelProvider } from '@anticrm/contact' import type { Channel, ChannelProvider } from '@anticrm/contact'
import contact from '@anticrm/contact' import contact from '@anticrm/contact'
import type { AttachedData, Doc, Ref, Timestamp } from '@anticrm/core' import type { AttachedData, Doc, Ref, Timestamp } from '@anticrm/core'
import { NotificationClientImpl } from '@anticrm/notification-resources'
import type { Asset, IntlString } from '@anticrm/platform' import type { Asset, IntlString } from '@anticrm/platform'
import { AnyComponent, showPopup, Button, Menu, showTooltip, closeTooltip, eventToHTMLElement } from '@anticrm/ui'
import type { Action, ButtonKind, ButtonSize } from '@anticrm/ui'
import presentation from '@anticrm/presentation' import presentation from '@anticrm/presentation'
import {
Action,
AnyComponent,
Button,
ButtonKind,
ButtonSize,
closeTooltip,
eventToHTMLElement,
getFocusManager,
Menu,
showPopup,
showTooltip
} from '@anticrm/ui'
import { createEventDispatcher, tick } from 'svelte'
import { getChannelProviders } from '../utils' import { getChannelProviders } from '../utils'
import ChannelEditor from './ChannelEditor.svelte' import ChannelEditor from './ChannelEditor.svelte'
import { NotificationClientImpl } from '@anticrm/notification-resources'
export let value: AttachedData<Channel>[] | Channel | null export let value: AttachedData<Channel>[] | Channel | null
export let editable: boolean = false export let editable: boolean = false
@ -33,6 +44,7 @@
export let length: 'short' | 'full' = 'full' export let length: 'short' | 'full' = 'full'
export let shape: 'circle' | undefined = undefined export let shape: 'circle' | undefined = undefined
export let integrations: Set<Ref<Doc>> = new Set<Ref<Doc>>() export let integrations: Set<Ref<Doc>> = new Set<Ref<Doc>>()
export let focusIndex = -1
const notificationClient = NotificationClientImpl.getClient() const notificationClient = NotificationClientImpl.getClient()
const lastViews = notificationClient.getLastViews() const lastViews = notificationClient.getLastViews()
@ -79,7 +91,7 @@
} }
async function update (value: AttachedData<Channel>[] | Channel | null, lastViews: Map<Ref<Doc>, Timestamp>) { async function update (value: AttachedData<Channel>[] | Channel | null, lastViews: Map<Ref<Doc>, Timestamp>) {
if (value === null) { if (value == null) {
displayItems = [] displayItems = []
return return
} }
@ -99,7 +111,7 @@
} }
} }
displayItems = result displayItems = result
updateMenu() updateMenu(displayItems)
} }
$: if (value) update(value, $lastViews) $: if (value) update(value, $lastViews)
@ -112,23 +124,30 @@
let anchor: HTMLElement let anchor: HTMLElement
function filterUndefined (channels: AttachedData<Channel>[]): AttachedData<Channel>[] { function filterUndefined (channels: AttachedData<Channel>[]): AttachedData<Channel>[] {
return channels.filter((channel) => channel.value !== undefined && channel.value.length > 0) return channels.filter((channel) => channel.value !== undefined)
} }
const focusManager = getFocusManager()
getChannelProviders().then((pr) => (providers = pr)) getChannelProviders().then((pr) => (providers = pr))
const updateMenu = (): void => { const updateMenu = (_displayItems: Item[]): void => {
actions = [] actions = []
providers.forEach((pr) => { providers.forEach((pr) => {
if (displayItems.filter((it) => it.provider === pr._id).length === 0) { if (_displayItems.filter((it) => it.provider === pr._id).length === 0) {
actions.push({ actions.push({
icon: pr.icon ?? contact.icon.SocialEdit, icon: pr.icon ?? contact.icon.SocialEdit,
label: pr.label, label: pr.label,
action: async () => { action: async () => {
const provider = getProvider({ provider: pr._id, value: '' }, providers, $lastViews) const provider = getProvider({ provider: pr._id, value: '' }, providers, $lastViews)
if (provider !== undefined) { if (provider !== undefined) {
if (displayItems.filter((it) => it.provider === pr._id).length === 0) { if (_displayItems.filter((it) => it.provider === pr._id).length === 0) {
displayItems = [...displayItems, provider] displayItems = [..._displayItems, provider]
if (focusIndex !== -1) {
await tick()
focusManager?.setFocusPos(focusIndex + displayItems.length)
await tick()
editChannel(btns[displayItems.length - 1], displayItems.length - 1, provider)
}
} }
} }
} }
@ -136,7 +155,7 @@
} }
}) })
} }
$: if (providers) updateMenu() $: if (providers) updateMenu(displayItems)
const dropItem = (n: number): Item[] => { const dropItem = (n: number): Item[] => {
return displayItems.filter((it, i) => i !== n) return displayItems.filter((it, i) => i !== n)
@ -144,11 +163,15 @@
const saveItems = (): void => { const saveItems = (): void => {
value = filterUndefined(displayItems) value = filterUndefined(displayItems)
dispatch('change', value) dispatch('change', value)
updateMenu() updateMenu(displayItems)
} }
const showMenu = (ev: MouseEvent): void => { const showMenu = (ev: MouseEvent): void => {
showPopup(Menu, { actions }, ev.target as HTMLElement) showPopup(Menu, { actions }, ev.target as HTMLElement, (result) => {
if (result == null) {
focusManager?.setFocusPos(focusIndex + 2 + displayItems.length)
}
})
} }
const editChannel = (el: HTMLElement, n: number, item: Item): void => { const editChannel = (el: HTMLElement, n: number, item: Item): void => {
@ -157,13 +180,21 @@
el, el,
undefined, undefined,
ChannelEditor, ChannelEditor,
{ value: item.value, placeholder: item.placeholder, editable }, {
value: item.value,
placeholder: item.placeholder,
editable
},
anchor, anchor,
(result) => { (result) => {
if (result.detail !== undefined) { if (result.detail != null) {
if (result.detail === '') displayItems = dropItem(n) if (result.detail === '') {
else displayItems[n].value = result.detail displayItems = dropItem(n)
} else {
displayItems[n].value = result.detail
}
saveItems() saveItems()
focusManager?.setFocusPos(focusIndex + 1 + n)
} }
} }
) )
@ -183,6 +214,7 @@
> >
{#each displayItems as item, i} {#each displayItems as item, i}
<Button <Button
focusIndex={focusIndex === -1 ? focusIndex : focusIndex + 1 + i}
id={item.label} id={item.label}
bind:input={btns[i]} bind:input={btns[i]}
icon={item.icon} icon={item.icon}
@ -193,18 +225,19 @@
on:mousemove={(ev) => { on:mousemove={(ev) => {
_focus(ev, i, item) _focus(ev, i, item)
}} }}
on:focus={(ev) => {
_focus(ev, i, item)
}}
on:click={(ev) => { on:click={(ev) => {
if (editable) editChannel(eventToHTMLElement(ev), i, item) if (editable) {
else closeTooltip() editChannel(eventToHTMLElement(ev), i, item)
} else {
closeTooltip()
}
dispatch('open', item) dispatch('open', item)
}} }}
/> />
{/each} {/each}
{#if actions.length > 0 && editable} {#if actions.length > 0 && editable}
<Button <Button
focusIndex={focusIndex === -1 ? focusIndex : focusIndex + 2 + displayItems.length}
id={presentation.string.AddSocialLinks} id={presentation.string.AddSocialLinks}
bind:input={addBtn} bind:input={addBtn}
icon={contact.icon.SocialEdit} icon={contact.icon.SocialEdit}

View File

@ -12,7 +12,7 @@
"VacancyCreateLabel": "Vacancy", "VacancyCreateLabel": "Vacancy",
"VacancyPlaceholder": "Software Engineer", "VacancyPlaceholder": "Software Engineer",
"MakePrivateDescription": "Only members can see it", "MakePrivateDescription": "Only members can see it",
"CreateAnApplication": "Create an Application", "CreateAnApplication": "New Application",
"NoApplicationsForCandidate": "There are no applications for this candidate.", "NoApplicationsForCandidate": "There are no applications for this candidate.",
"CreateApplication": "Create an Application", "CreateApplication": "Create an Application",
"ApplicationCreateLabel": "Application", "ApplicationCreateLabel": "Application",
@ -93,7 +93,8 @@
"GotoSkills": "Go to Skills", "GotoSkills": "Go to Skills",
"GotoAssigned": "Go to my Assigned", "GotoAssigned": "Go to my Assigned",
"GotoApplicants": "Go to Applications", "GotoApplicants": "Go to Applications",
"GotoRecruitApplication": "Switch to Recruit Application" "GotoRecruitApplication": "Switch to Recruit Application",
"AddDropHere": "Add or drop resume"
}, },
"status": { "status": {
"CandidateRequired": "Please select candidate", "CandidateRequired": "Please select candidate",

View File

@ -95,7 +95,8 @@
"GotoSkills": "Перейти к навыкам", "GotoSkills": "Перейти к навыкам",
"GotoAssigned": "Перейти к моим назначениям", "GotoAssigned": "Перейти к моим назначениям",
"GotoApplicants": "Перейти к претендентам", "GotoApplicants": "Перейти к претендентам",
"GotoRecruitApplication": "Перейти к Приложению Рекрутинг" "GotoRecruitApplication": "Перейти к Приложению Рекрутинг",
"AddDropHere": "Добавить или перетянуть резюме"
}, },
"status": { "status": {
"CandidateRequired": "Пожалуйста выберите кандидата", "CandidateRequired": "Пожалуйста выберите кандидата",

View File

@ -23,7 +23,9 @@
import ui, { import ui, {
Button, Button,
ColorPopup, ColorPopup,
createFocusManager,
eventToHTMLElement, eventToHTMLElement,
FocusHandler,
getPlatformColor, getPlatformColor,
showPopup, showPopup,
Status as StatusControl Status as StatusControl
@ -163,8 +165,11 @@
{ sort: { rank: SortingOrder.Ascending } } { sort: { rank: SortingOrder.Ascending } }
) )
} }
const manager = createFocusManager()
</script> </script>
<FocusHandler {manager} />
<Card <Card
label={recruit.string.CreateApplication} label={recruit.string.CreateApplication}
okAction={createApplication} okAction={createApplication}
@ -183,6 +188,7 @@
<svelte:fragment slot="pool"> <svelte:fragment slot="pool">
{#if !preserveCandidate} {#if !preserveCandidate}
<UserBox <UserBox
focusIndex={1}
_class={contact.class.Person} _class={contact.class.Person}
label={recruit.string.Candidate} label={recruit.string.Candidate}
placeholder={recruit.string.Candidates} placeholder={recruit.string.Candidates}
@ -192,6 +198,7 @@
/> />
{/if} {/if}
<UserBox <UserBox
focusIndex={2}
_class={contact.class.Employee} _class={contact.class.Employee}
label={recruit.string.AssignRecruiter} label={recruit.string.AssignRecruiter}
placeholder={recruit.string.Recruiters} placeholder={recruit.string.Recruiters}
@ -203,6 +210,7 @@
/> />
{#if states && doc.space} {#if states && doc.space}
<Button <Button
focusIndex={3}
width="min-content" width="min-content"
size="small" size="small"
kind="no-border" kind="no-border"
@ -217,6 +225,7 @@
selectedState = result selectedState = result
selectedState.title = result.label selectedState.title = result.label
} }
manager.setFocusPos(3)
} }
) )
}} }}

View File

@ -35,7 +35,9 @@
import { import {
Button, Button,
Component, Component,
createFocusManager,
EditBox, EditBox,
FocusHandler,
getColorNumberByText, getColorNumberByText,
IconFile as FileIcon, IconFile as FileIcon,
IconInfo, IconInfo,
@ -351,6 +353,7 @@
if (file !== undefined) { if (file !== undefined) {
createAttachment(file) createAttachment(file)
} }
manager.setFocusPos(102)
} }
function onAvatarDone (e: any) { function onAvatarDone (e: any) {
@ -386,8 +389,12 @@
function removeAvatar (): void { function removeAvatar (): void {
avatar = undefined avatar = undefined
} }
const manager = createFocusManager()
</script> </script>
<FocusHandler {manager} />
<Card <Card
label={recruit.string.CreateCandidate} label={recruit.string.CreateCandidate}
okAction={createCandidate} okAction={createCandidate}
@ -416,17 +423,31 @@
kind={'large-style'} kind={'large-style'}
maxWidth={'32rem'} maxWidth={'32rem'}
focus focus
focusIndex={1}
/> />
<EditBox <EditBox
placeholder={recruit.string.PersonLastNamePlaceholder} placeholder={recruit.string.PersonLastNamePlaceholder}
bind:value={lastName} bind:value={lastName}
kind={'large-style'} kind={'large-style'}
maxWidth={'32rem'} maxWidth={'32rem'}
focusIndex={2}
/> />
<div class="mt-1"> <div class="mt-1">
<EditBox placeholder={recruit.string.Title} bind:value={object.title} kind={'small-style'} maxWidth={'32rem'} /> <EditBox
placeholder={recruit.string.Title}
bind:value={object.title}
kind={'small-style'}
maxWidth={'32rem'}
focusIndex={3}
/>
</div> </div>
<EditBox placeholder={recruit.string.Location} bind:value={object.city} kind={'small-style'} maxWidth={'32rem'} /> <EditBox
placeholder={recruit.string.Location}
bind:value={object.city}
kind={'small-style'}
maxWidth={'32rem'}
focusIndex={4}
/>
</div> </div>
<div class="ml-4"> <div class="ml-4">
<EditableAvatar <EditableAvatar
@ -439,12 +460,23 @@
</div> </div>
</div> </div>
<svelte:fragment slot="pool"> <svelte:fragment slot="pool">
<ChannelsDropdown bind:value={channels} editable /> <ChannelsDropdown focusIndex={10} bind:value={channels} editable />
<YesNo label={recruit.string.Onsite} tooltip={recruit.string.WorkLocationPreferences} bind:value={object.onsite} /> <YesNo
<YesNo label={recruit.string.Remote} tooltip={recruit.string.WorkLocationPreferences} bind:value={object.remote} /> focusIndex={100}
label={recruit.string.Onsite}
tooltip={recruit.string.WorkLocationPreferences}
bind:value={object.onsite}
/>
<YesNo
focusIndex={101}
label={recruit.string.Remote}
tooltip={recruit.string.WorkLocationPreferences}
bind:value={object.remote}
/>
<Component <Component
is={tags.component.TagsDropdownEditor} is={tags.component.TagsDropdownEditor}
props={{ props={{
focusIndex: 102,
items: skills, items: skills,
key, key,
targetClass: recruit.mixin.Candidate, targetClass: recruit.mixin.Candidate,
@ -474,20 +506,26 @@
on:drop|preventDefault|stopPropagation={drop} on:drop|preventDefault|stopPropagation={drop}
> >
{#if resume.uuid} {#if resume.uuid}
<Link <Button
label={resume.name} kind={'transparent'}
focusIndex={103}
icon={FileIcon} icon={FileIcon}
maxLenght={16}
on:click={() => { on:click={() => {
showPopup(PDFViewer, { file: resume.uuid, name: resume.name }, 'right') showPopup(PDFViewer, { file: resume.uuid, name: resume.name }, 'right')
}} }}
/> >
<svelte:fragment slot="content">
{resume.name}
</svelte:fragment>
</Button>
{:else} {:else}
{#if loading} {#if loading}
<Link label={'Uploading...'} icon={Spinner} disabled /> <Link label={'Uploading...'} icon={Spinner} disabled />
{:else} {:else}
<Link <Button
label={'Add or drop resume'} kind={'transparent'}
focusIndex={103}
label={recruit.string.AddDropHere}
icon={FileUpload} icon={FileUpload}
on:click={() => { on:click={() => {
inputFile.click() inputFile.click()

View File

@ -14,8 +14,8 @@
--> -->
<script lang="ts"> <script lang="ts">
import type { IntlString } from '@anticrm/platform' import type { IntlString } from '@anticrm/platform'
import { Label, Tooltip, Button } from '@anticrm/ui' import type { ButtonKind, ButtonSize, TooltipAlignment } from '@anticrm/ui'
import type { TooltipAlignment, ButtonKind, ButtonSize } from '@anticrm/ui' import { Button, Label, Tooltip } from '@anticrm/ui'
export let label: IntlString export let label: IntlString
export let tooltip: IntlString export let tooltip: IntlString
@ -27,10 +27,13 @@
export let size: ButtonSize = 'small' export let size: ButtonSize = 'small'
export let justify: 'left' | 'center' = 'center' export let justify: 'left' | 'center' = 'center'
export let width: string | undefined = 'fit-content' export let width: string | undefined = 'fit-content'
export let focusIndex = -1
</script> </script>
<Tooltip direction={labelDirection} label={tooltip}> <Tooltip direction={labelDirection} label={tooltip}>
<Button <Button
{focusIndex}
{kind} {kind}
{size} {size}
{justify} {justify}

View File

@ -103,7 +103,8 @@ export default mergeIds(recruitId, recruit, {
DueDate: '' as IntlString, DueDate: '' as IntlString,
CandidateReviews: '' as IntlString, CandidateReviews: '' as IntlString,
AddDescription: '' as IntlString, AddDescription: '' as IntlString,
NumberSkills: '' as IntlString NumberSkills: '' as IntlString,
AddDropHere: '' as IntlString
}, },
space: { space: {
CandidatesPublic: '' as Ref<Space> CandidatesPublic: '' as Ref<Space>

View File

@ -23,7 +23,7 @@
import view from '../plugin' import view from '../plugin'
import { focusStore, selectionStore } from '../selection' import { focusStore, selectionStore } from '../selection'
import ActionContext from './ActionContext.svelte' import ActionContext from './ActionContext.svelte'
import ListView from './ListView.svelte' import { ListView } from '@anticrm/ui'
import ObjectPresenter from './ObjectPresenter.svelte' import ObjectPresenter from './ObjectPresenter.svelte'
export let viewContext: ViewContext export let viewContext: ViewContext

View File

@ -438,6 +438,7 @@
{/if} {/if}
</div> </div>
<div bind:this={cover} class="cover" /> <div bind:this={cover} class="cover" />
<TooltipInstance />
<PanelInstance bind:this={panelInstance} {contentPanel}> <PanelInstance bind:this={panelInstance} {contentPanel}>
<svelte:fragment slot="panel-header"> <svelte:fragment slot="panel-header">
<ActionContext context={{ mode: 'panel' }} /> <ActionContext context={{ mode: 'panel' }} />
@ -448,7 +449,6 @@
<ActionContext context={{ mode: 'popup' }} /> <ActionContext context={{ mode: 'popup' }} />
</svelte:fragment> </svelte:fragment>
</Popup> </Popup>
<TooltipInstance />
<DatePickerPopup /> <DatePickerPopup />
{:else} {:else}
No client No client