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/view": "^0.6.6",
"@hcengineering/workbench": "^0.6.6",
"@hcengineering/model-workbench": "^0.6.1",
"@hcengineering/notification": "^0.6.12",
"@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 preference, { TPreference } from '@hcengineering/model-preference'
import view, { createAction } from '@hcengineering/model-view'
import workbench from '@hcengineering/model-workbench'
import {
DocUpdates,
EmailNotification,
@ -50,7 +51,6 @@ import {
import type { Asset, IntlString } from '@hcengineering/platform'
import setting from '@hcengineering/setting'
import { AnyComponent } from '@hcengineering/ui'
import workbench from '@hcengineering/workbench'
import notification from './plugin'
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, {
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 (

View File

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

View File

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

View File

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

View File

@ -22,6 +22,7 @@
import IconUpOutline from './icons/UpOutline.svelte'
import IconDownOutline from './icons/DownOutline.svelte'
import HalfUpDown from './icons/HalfUpDown.svelte'
import { isSafari } from '../utils'
export let padding: string | undefined = undefined
export let autoscroll: boolean = false
@ -291,7 +292,9 @@
}
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()
@ -444,6 +447,7 @@
(orientir === 'horizontal' && (maskH === 'left' || maskH === 'both'))
? 'visible'
: 'hidden'
let scrollY: number = 0
</script>
<svelte:window on:resize={_resize} />
@ -467,8 +471,17 @@
}}
class="scroll relative flex-shrink"
class:overflow-x={horizontal ? 'auto' : 'hidden'}
on:scroll={() => {
on:scroll={(evt) => {
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

View File

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

View File

@ -16,11 +16,14 @@
import { generateId } from '@hcengineering/core'
import type { Metadata } from '@hcengineering/platform'
import { setMetadata } from '@hcengineering/platform'
import { writable } from 'svelte/store'
import autolinker from 'autolinker'
import { writable } from 'svelte/store'
import { Notification, NotificationPosition, NotificationSeverity, notificationsStore } from '.'
import { AnyComponent, AnySvelteComponent } from './types'
/**
* @public
*/
export function setMetadataLocalStorage<T> (id: Metadata<T>, value: T | null): void {
if (value != null) {
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)
}
/**
* @public
*/
export function fetchMetadataLocalStorage<T> (id: Metadata<T>): T | null {
const data = localStorage.getItem(id)
if (data === null) {
@ -45,14 +51,27 @@ export function fetchMetadataLocalStorage<T> (id: Metadata<T>): T | null {
}
}
/**
* @public
*/
export function checkMobile (): boolean {
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 {
return Number(Number(n).toFixed(amount))
}
/**
* @public
*/
export function addNotification (
title: string,
subTitle: string,

View File

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

View File

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

View File

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

View File

@ -1,22 +1,21 @@
import { Class, Doc, DocumentQuery, Hierarchy, Ref, Space, TxResult } from '@hcengineering/core'
import { Asset, getResource, IntlString, Resource } from '@hcengineering/platform'
import { getClient, MessageBox, updateAttribute } from '@hcengineering/presentation'
import { Asset, IntlString, Resource, getResource } from '@hcengineering/platform'
import { MessageBox, getClient, updateAttribute } from '@hcengineering/presentation'
import {
AnyComponent,
AnySvelteComponent,
PopupAlignment,
PopupPosAlignment,
closeTooltip,
isPopupPosAlignment,
navigate,
PopupAlignment,
PopupPosAlignment,
showPanel,
showPopup
} from '@hcengineering/ui'
import { ViewContext } from '@hcengineering/view'
import MoveView from './components/Move.svelte'
import { contextStore } from './context'
import { ContextStore, contextStore } from './context'
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'
/**
@ -100,7 +99,7 @@ focusStore.subscribe((it) => {
$focusStore = it
})
let $contextStore: ViewContext[]
let $contextStore: ContextStore
contextStore.subscribe((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 {
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 {

View File

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

View File

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

View File

@ -1,7 +1,23 @@
import { ViewContext } from '@hcengineering/view'
import { ViewContext, ViewContextType } from '@hcengineering/view'
import { writable } from 'svelte/store'
/**
* @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([]))