TSK-1451: Fix focus issues + jump workaround (#3167)

Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
This commit is contained in:
Andrey Sobolev 2023-05-12 13:41:27 +07:00 committed by GitHub
parent a57f3b8c2f
commit 53c3f58e9d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 144 additions and 57 deletions

View File

@ -34,6 +34,7 @@
"@hcengineering/model-view": "^0.6.0", "@hcengineering/model-view": "^0.6.0",
"@hcengineering/view": "^0.6.6", "@hcengineering/view": "^0.6.6",
"@hcengineering/workbench": "^0.6.6", "@hcengineering/workbench": "^0.6.6",
"@hcengineering/model-workbench": "^0.6.1",
"@hcengineering/notification": "^0.6.12", "@hcengineering/notification": "^0.6.12",
"@hcengineering/setting": "^0.6.7" "@hcengineering/setting": "^0.6.7"
} }

View File

@ -34,6 +34,7 @@ import { ArrOf, Builder, Index, Mixin, Model, Prop, TypeRef, TypeString, UX } fr
import core, { TAttachedDoc, TClass, TDoc } from '@hcengineering/model-core' import core, { TAttachedDoc, TClass, TDoc } from '@hcengineering/model-core'
import preference, { TPreference } from '@hcengineering/model-preference' import preference, { TPreference } from '@hcengineering/model-preference'
import view, { createAction } from '@hcengineering/model-view' import view, { createAction } from '@hcengineering/model-view'
import workbench from '@hcengineering/model-workbench'
import { import {
DocUpdates, DocUpdates,
EmailNotification, EmailNotification,
@ -50,7 +51,6 @@ import {
import type { Asset, IntlString } from '@hcengineering/platform' import type { Asset, IntlString } from '@hcengineering/platform'
import setting from '@hcengineering/setting' import setting from '@hcengineering/setting'
import { AnyComponent } from '@hcengineering/ui' import { AnyComponent } from '@hcengineering/ui'
import workbench from '@hcengineering/workbench'
import notification from './plugin' import notification from './plugin'
export { notificationId } from '@hcengineering/notification' export { notificationId } from '@hcengineering/notification'
@ -284,6 +284,23 @@ export function createModel (builder: Builder): void {
builder.mixin(notification.class.DocUpdates, core.class.Class, view.mixin.IgnoreActions, { builder.mixin(notification.class.DocUpdates, core.class.Class, view.mixin.IgnoreActions, {
actions: [view.action.Delete, view.action.Open] actions: [view.action.Delete, view.action.Open]
}) })
createAction(builder, {
action: workbench.actionImpl.Navigate,
actionProps: {
mode: 'app',
application: notificationId,
special: notificationId
},
label: notification.string.Inbox,
icon: view.icon.ArrowRight,
input: 'none',
category: view.category.Navigation,
target: core.class.Doc,
context: {
mode: ['workbench', 'browser', 'editor', 'panel', 'popup']
}
})
} }
export function generateClassNotificationTypes ( export function generateClassNotificationTypes (

View File

@ -213,15 +213,17 @@
export let focusIndex = -1 export let focusIndex = -1
const { idx, focusManager } = registerFocus(focusIndex, { const { idx, focusManager } = registerFocus(focusIndex, {
focus: () => { focus: () => {
focused = true const editable = textEditor.isEditable()
textEditor.focus() if (editable) {
return textEditor.isEditable() focused = true
textEditor.focus()
}
return editable
}, },
isFocus: () => focused isFocus: () => focused
}) })
const updateFocus = () => { const updateFocus = () => {
if (focusIndex !== -1) { if (focusIndex !== -1) {
console.trace('focuse')
focusManager?.setFocus(idx) focusManager?.setFocus(idx)
} }
} }

View File

@ -104,9 +104,12 @@
export let focusIndex = -1 export let focusIndex = -1
const { idx, focusManager } = registerFocus(focusIndex, { const { idx, focusManager } = registerFocus(focusIndex, {
focus: () => { focus: () => {
focused = true const editable = textEditor.isEditable()
focus() if (editable) {
return textEditor.isEditable() focused = true
focus()
}
return editable
}, },
isFocus: () => focused isFocus: () => focused
}) })

View File

@ -2,9 +2,10 @@
import { FocusManager } from '../focus' import { FocusManager } from '../focus'
export let manager: FocusManager export let manager: FocusManager
export let isEnabled: boolean = true
function handleKey (evt: KeyboardEvent): void { function handleKey (evt: KeyboardEvent): void {
if (evt.code === 'Tab') { if (evt.code === 'Tab' && isEnabled) {
evt.preventDefault() evt.preventDefault()
evt.stopPropagation() evt.stopPropagation()
manager.next(evt.shiftKey ? -1 : 1) manager.next(evt.shiftKey ? -1 : 1)

View File

@ -22,6 +22,7 @@
import IconUpOutline from './icons/UpOutline.svelte' import IconUpOutline from './icons/UpOutline.svelte'
import IconDownOutline from './icons/DownOutline.svelte' import IconDownOutline from './icons/DownOutline.svelte'
import HalfUpDown from './icons/HalfUpDown.svelte' import HalfUpDown from './icons/HalfUpDown.svelte'
import { isSafari } from '../utils'
export let padding: string | undefined = undefined export let padding: string | undefined = undefined
export let autoscroll: boolean = false export let autoscroll: boolean = false
@ -291,7 +292,9 @@
} }
const scrollDown = (): void => { const scrollDown = (): void => {
if (divScroll) divScroll.scrollTop = divScroll.scrollHeight - divHeight + 2 if (divScroll) {
divScroll.scrollTop = divScroll.scrollHeight - divHeight + 2
}
} }
$: if (scrolling && belowContent && belowContent > 0) scrollDown() $: if (scrolling && belowContent && belowContent > 0) scrollDown()
@ -444,6 +447,7 @@
(orientir === 'horizontal' && (maskH === 'left' || maskH === 'both')) (orientir === 'horizontal' && (maskH === 'left' || maskH === 'both'))
? 'visible' ? 'visible'
: 'hidden' : 'hidden'
let scrollY: number = 0
</script> </script>
<svelte:window on:resize={_resize} /> <svelte:window on:resize={_resize} />
@ -467,8 +471,17 @@
}} }}
class="scroll relative flex-shrink" class="scroll relative flex-shrink"
class:overflow-x={horizontal ? 'auto' : 'hidden'} class:overflow-x={horizontal ? 'auto' : 'hidden'}
on:scroll={() => { on:scroll={(evt) => {
if ($tooltipstore.label !== undefined) closeTooltip() if ($tooltipstore.label !== undefined) closeTooltip()
const newPos = divScroll?.scrollTop ?? 0
// TODO: Workaround: https://front.hc.engineering/workbench/platform/tracker/TSK-760
// In Safari scroll could jump on click, with no particular reason.
if (scrollY !== 0 && Math.abs(newPos - scrollY) > 100 && divScroll !== undefined && isSafari()) {
divScroll.scrollTop = scrollY
}
scrollY = divScroll?.scrollTop ?? 0
}} }}
> >
<div <div

View File

@ -60,6 +60,9 @@ class FocusManagerImpl implements FocusManager {
} }
setFocusPos (order: number): void { setFocusPos (order: number): void {
if (order === -1) {
return
}
const idx = this.elements.findIndex((it) => it.order === order) const idx = this.elements.findIndex((it) => it.order === order)
if (idx !== undefined) { if (idx !== undefined) {
this.current = idx this.current = idx

View File

@ -16,11 +16,14 @@
import { generateId } from '@hcengineering/core' import { generateId } from '@hcengineering/core'
import type { Metadata } from '@hcengineering/platform' import type { Metadata } from '@hcengineering/platform'
import { setMetadata } from '@hcengineering/platform' import { setMetadata } from '@hcengineering/platform'
import { writable } from 'svelte/store'
import autolinker from 'autolinker' import autolinker from 'autolinker'
import { writable } from 'svelte/store'
import { Notification, NotificationPosition, NotificationSeverity, notificationsStore } from '.' import { Notification, NotificationPosition, NotificationSeverity, notificationsStore } from '.'
import { AnyComponent, AnySvelteComponent } from './types' import { AnyComponent, AnySvelteComponent } from './types'
/**
* @public
*/
export function setMetadataLocalStorage<T> (id: Metadata<T>, value: T | null): void { export function setMetadataLocalStorage<T> (id: Metadata<T>, value: T | null): void {
if (value != null) { if (value != null) {
localStorage.setItem(id, typeof value === 'string' ? value : JSON.stringify(value)) localStorage.setItem(id, typeof value === 'string' ? value : JSON.stringify(value))
@ -30,6 +33,9 @@ export function setMetadataLocalStorage<T> (id: Metadata<T>, value: T | null): v
setMetadata(id, value) setMetadata(id, value)
} }
/**
* @public
*/
export function fetchMetadataLocalStorage<T> (id: Metadata<T>): T | null { export function fetchMetadataLocalStorage<T> (id: Metadata<T>): T | null {
const data = localStorage.getItem(id) const data = localStorage.getItem(id)
if (data === null) { if (data === null) {
@ -45,14 +51,27 @@ export function fetchMetadataLocalStorage<T> (id: Metadata<T>): T | null {
} }
} }
/**
* @public
*/
export function checkMobile (): boolean { export function checkMobile (): boolean {
return /Android|webOS|iPhone|iPad|iPod|BlackBerry|Mobile|Opera Mini/i.test(navigator.userAgent) return /Android|webOS|iPhone|iPad|iPod|BlackBerry|Mobile|Opera Mini/i.test(navigator.userAgent)
} }
/**
* @public
*/
export function isSafari (): boolean {
return navigator.userAgent.toLowerCase().includes('safari/')
}
export function floorFractionDigits (n: number | string, amount: number): number { export function floorFractionDigits (n: number | string, amount: number): number {
return Number(Number(n).toFixed(amount)) return Number(Number(n).toFixed(amount))
} }
/**
* @public
*/
export function addNotification ( export function addNotification (
title: string, title: string,
subTitle: string, subTitle: string,

View File

@ -79,10 +79,12 @@
} else { } else {
const uri = avatar.split('://')[1] const uri = avatar.split('://')[1]
const color = (await getResource(avatarProvider.getUrl))(uri, size) const color: string | undefined = (await getResource(avatarProvider.getUrl))(uri, size)
style = `background-color: ${color}` if (color != null) {
accentColor = hexToRgb(color) style = `background-color: ${color}`
dispatch('accent-color', accentColor) accentColor = hexToRgb(color)
dispatch('accent-color', accentColor)
}
} }
} }
$: updateStyle(avatar, avatarProvider) $: updateStyle(avatar, avatarProvider)

View File

@ -18,7 +18,7 @@
import { Class, Doc, getCurrentAccount, Ref } from '@hcengineering/core' import { Class, Doc, getCurrentAccount, Ref } from '@hcengineering/core'
import notification, { DocUpdates } from '@hcengineering/notification' import notification, { DocUpdates } from '@hcengineering/notification'
import { createQuery, getClient } from '@hcengineering/presentation' import { createQuery, getClient } from '@hcengineering/presentation'
import { AnyComponent, Component, Label, Loading, Scroller } from '@hcengineering/ui' import { AnyComponent, Component, Label, ListView, Loading, Scroller } from '@hcengineering/ui'
import view from '@hcengineering/view' import view from '@hcengineering/view'
import { ActionContext, ListSelectionProvider, SelectDirection } from '@hcengineering/view-resources' import { ActionContext, ListSelectionProvider, SelectDirection } from '@hcengineering/view-resources'
import NotificationView from './NotificationView.svelte' import NotificationView from './NotificationView.svelte'
@ -94,6 +94,7 @@
const value = selected + offset const value = selected + offset
if (docs[value] !== undefined) { if (docs[value] !== undefined) {
selected = value selected = value
listView?.select(selected)
} }
} }
}) })
@ -104,6 +105,7 @@
}) })
let selected = 0 let selected = 0
let listView: ListView
</script> </script>
<ActionContext <ActionContext
@ -124,16 +126,18 @@
{#if loading} {#if loading}
<Loading /> <Loading />
{:else} {:else}
{#each docs as doc, i} <ListView bind:this={listView} count={docs.length} selection={selected}>
<NotificationView <svelte:fragment slot="item" let:item>
value={doc} <NotificationView
selected={selected === i} value={docs[item]}
{viewlets} selected={selected === item}
on:click={() => { {viewlets}
selected = i on:click={() => {
}} selected = item
/> }}
{/each} />
</svelte:fragment>
</ListView>
{/if} {/if}
</Scroller> </Scroller>
</div> </div>

View File

@ -34,7 +34,7 @@
navigate, navigate,
showPopup showPopup
} from '@hcengineering/ui' } from '@hcengineering/ui'
import { ActionContext, ContextMenu, DocNavLink, UpDownNavigator } from '@hcengineering/view-resources' import { ActionContext, ContextMenu, DocNavLink, UpDownNavigator, contextStore } from '@hcengineering/view-resources'
import { createEventDispatcher, onDestroy, onMount } from 'svelte' import { createEventDispatcher, onDestroy, onMount } from 'svelte'
import { generateIssueShortLink, getIssueId } from '../../../issues' import { generateIssueShortLink, getIssueId } from '../../../issues'
import tracker from '../../../plugin' import tracker from '../../../plugin'
@ -149,14 +149,20 @@
} }
return true return true
} }
// If it is embedded
$: lastCtx = $contextStore.getLastContext()
$: isContextEnabled = lastCtx?.mode === 'editor' || lastCtx?.mode === 'browser'
</script> </script>
<FocusHandler {manager} /> {#if !embedded}
<ActionContext <FocusHandler {manager} isEnabled={isContextEnabled} />
context={{ <ActionContext
mode: 'editor' context={{
}} mode: 'editor'
/> }}
/>
{/if}
{#if issue !== undefined} {#if issue !== undefined}
<Panel <Panel
@ -204,6 +210,7 @@
placeholder={tracker.string.IssueTitlePlaceholder} placeholder={tracker.string.IssueTitlePlaceholder}
kind="large-style" kind="large-style"
on:blur={save} on:blur={save}
focus={!embedded}
/> />
<div class="w-full mt-6"> <div class="w-full mt-6">
{#key issue._id} {#key issue._id}

View File

@ -1,22 +1,21 @@
import { Class, Doc, DocumentQuery, Hierarchy, Ref, Space, TxResult } from '@hcengineering/core' import { Class, Doc, DocumentQuery, Hierarchy, Ref, Space, TxResult } from '@hcengineering/core'
import { Asset, getResource, IntlString, Resource } from '@hcengineering/platform' import { Asset, IntlString, Resource, getResource } from '@hcengineering/platform'
import { getClient, MessageBox, updateAttribute } from '@hcengineering/presentation' import { MessageBox, getClient, updateAttribute } from '@hcengineering/presentation'
import { import {
AnyComponent, AnyComponent,
AnySvelteComponent, AnySvelteComponent,
PopupAlignment,
PopupPosAlignment,
closeTooltip, closeTooltip,
isPopupPosAlignment, isPopupPosAlignment,
navigate, navigate,
PopupAlignment,
PopupPosAlignment,
showPanel, showPanel,
showPopup showPopup
} from '@hcengineering/ui' } from '@hcengineering/ui'
import { ViewContext } from '@hcengineering/view'
import MoveView from './components/Move.svelte' import MoveView from './components/Move.svelte'
import { contextStore } from './context' import { ContextStore, contextStore } from './context'
import view from './plugin' import view from './plugin'
import { FocusSelection, focusStore, previewDocument, SelectDirection, selectionStore } from './selection' import { FocusSelection, SelectDirection, focusStore, previewDocument, selectionStore } from './selection'
import { deleteObjects, getObjectLinkFragment } from './utils' import { deleteObjects, getObjectLinkFragment } from './utils'
/** /**
@ -100,7 +99,7 @@ focusStore.subscribe((it) => {
$focusStore = it $focusStore = it
}) })
let $contextStore: ViewContext[] let $contextStore: ContextStore
contextStore.subscribe((it) => { contextStore.subscribe((it) => {
$contextStore = it $contextStore = it
}) })
@ -153,7 +152,7 @@ const MoveRight = (doc: Doc | undefined, evt: Event): void => select(evt, 1, $fo
function ShowActions (doc: Doc | Doc[] | undefined, evt: Event): void { function ShowActions (doc: Doc | Doc[] | undefined, evt: Event): void {
evt.preventDefault() evt.preventDefault()
showPopup(view.component.ActionsPopup, { viewContext: $contextStore[$contextStore.length - 1] }, 'top') showPopup(view.component.ActionsPopup, { viewContext: $contextStore.getLastContext() }, 'top')
} }
function ShowPreview (doc: Doc | Doc[] | undefined, evt: Event): void { function ShowPreview (doc: Doc | Doc[] | undefined, evt: Event): void {

View File

@ -16,34 +16,34 @@
import { generateId } from '@hcengineering/core' import { generateId } from '@hcengineering/core'
import { ViewContext } from '@hcengineering/view' import { ViewContext } from '@hcengineering/view'
import { onDestroy } from 'svelte' import { onDestroy } from 'svelte'
import { contextStore } from '../context' import { ContextStore, contextStore } from '../context'
export let context: ViewContext export let context: ViewContext
const id = generateId() const id = generateId()
$: len = $contextStore.findIndex((it) => (it as any).id === id) $: len = $contextStore.contexts.findIndex((it) => (it as any).id === id)
onDestroy(() => { onDestroy(() => {
contextStore.update((t) => { contextStore.update((t) => {
return t.slice(0, len ?? 0) return new ContextStore(t.contexts.slice(0, len ?? 0))
}) })
}) })
$: { $: {
contextStore.update((cur) => { contextStore.update((cur) => {
const pos = cur.findIndex((it) => (it as any).id === id) const pos = cur.contexts.findIndex((it) => (it as any).id === id)
const newCur = { const newCur = {
id, id,
mode: context.mode, mode: context.mode,
application: context.application ?? cur[(pos !== -1 ? pos : cur.length) - 1]?.application application: context.application ?? cur.contexts[(pos !== -1 ? pos : cur.contexts.length) - 1]?.application
} }
if (pos === -1) { if (pos === -1) {
len = cur.length len = cur.contexts.length
return [...cur, newCur] return new ContextStore([...cur.contexts, newCur])
} }
len = pos len = pos
return [...cur.slice(0, pos), newCur] return new ContextStore([...cur.contexts.slice(0, pos), newCur])
}) })
} }
</script> </script>

View File

@ -60,9 +60,9 @@
return await getContextActions(client, docs, context) return await getContextActions(client, docs, context)
} }
$: ctx = $contextStore[$contextStore.length - 1] $: ctx = $contextStore.getLastContext()
$: mode = $contextStore[$contextStore.length - 1]?.mode $: mode = $contextStore.getLastContext()?.mode
$: application = $contextStore[$contextStore.length - 1]?.application $: application = $contextStore.getLastContext()?.application
function keyPrefix (key: KeyboardEvent): string { function keyPrefix (key: KeyboardEvent): string {
return ( return (
@ -158,7 +158,7 @@
} }
// For none we ignore all actions. // For none we ignore all actions.
if (ctx.mode === 'none') { if (ctx?.mode === 'none') {
return return
} }
clearTimeout(timer) clearTimeout(timer)

View File

@ -1,7 +1,23 @@
import { ViewContext } from '@hcengineering/view' import { ViewContext, ViewContextType } from '@hcengineering/view'
import { writable } from 'svelte/store' import { writable } from 'svelte/store'
/** /**
* @public * @public
*/ */
export const contextStore = writable<ViewContext[]>([]) export class ContextStore {
constructor (readonly contexts: ViewContext[]) {}
getLastContext (): ViewContext | undefined {
return this.contexts[this.contexts.length - 1]
}
isIncludes (type: ViewContextType): boolean {
return (
this.contexts.find((it) => it.mode === type || (Array.isArray(it.mode) && it.mode.includes(type))) !== undefined
)
}
}
/**
* @public
*/
export const contextStore = writable<ContextStore>(new ContextStore([]))