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

View File

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

View File

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

View File

@ -19,6 +19,7 @@
import { createQuery } from '../utils'
import SpaceInfo from './SpaceInfo.svelte'
import presentation from '..'
import { ListView } from '@anticrm/ui'
export let _class: Ref<Class<Space>>
export let spaceQuery: DocumentQuery<Space> | undefined
@ -42,24 +43,64 @@
onMount(() => {
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>
<div class="selectPopup">
<div class="selectPopup" on:keydown={onKeydown}>
<div class="header">
<input bind:this={input} type="text" bind:value={search} placeholder={phTraslate} on:input={() => {}} on:change />
</div>
<div class="scroll">
<div class="box">
{#each objects as space}
<button
class="menu-item flex-between"
on:click={() => {
dispatch('close', space)
}}
>
<SpaceInfo size={'large'} value={space} />
</button>
{/each}
<ListView
bind:this={list}
count={objects.length}
bind:selection
on:click={(evt) => handleSelection(evt, evt.detail)}
>
<svelte:fragment slot="item" let:item>
{@const space = objects[item]}
<button
class="menu-item flex-between"
on:click={() => {
handleSelection(undefined, item)
}}
>
<SpaceInfo size={'large'} value={space} />
</button>
</svelte:fragment>
</ListView>
</div>
</div>
</div>

View File

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

View File

@ -17,7 +17,7 @@
import { translate } from '@anticrm/platform'
import { createEventDispatcher, onMount } from 'svelte'
import { Tooltip, CheckBox } from '@anticrm/ui'
import { Tooltip, CheckBox, ListView } from '@anticrm/ui'
import UserInfo from './UserInfo.svelte'
import type { Ref, Class } from '@anticrm/core'
@ -70,43 +70,85 @@
onMount(() => {
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>
<div class="selectPopup" class:plainContainer={!shadows}>
<div class="selectPopup" class:plainContainer={!shadows} on:keydown={onKeydown}>
<div class="header">
<input bind:this={input} type="text" bind:value={search} placeholder={phTraslate} on:change />
</div>
<div class="scroll">
<div class="box">
{#each objects as person}
<button
class="menu-item"
on:click={() => {
if (!multiSelect) {
selected = person._id === selected ? undefined : person._id
dispatch('close', selected !== undefined ? person : undefined)
} else checkSelected(person)
}}
>
{#if multiSelect}
<div class="check pointer-events-none">
<CheckBox checked={isSelected(person)} primary />
</div>
{/if}
<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}>
<ListView
bind:this={list}
count={objects.length}
bind:selection
on:click={(evt) => handleSelection(evt, evt.detail)}
>
<svelte:fragment slot="item" let:item>
{@const person = objects[item]}
<button
class="menu-item w-full"
on:click={() => {
handleSelection(undefined, item)
}}
>
{#if multiSelect}
<div class="check pointer-events-none">
<CheckBox checked={isSelected(person)} primary />
</div>
{/if}
<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 />
</Tooltip>
{:else}
<CheckBox checked circle primary />
{/if}
</div>
{/if}
</button>
{/each}
{/if}
</div>
{/if}
</button>
</svelte:fragment>
</ListView>
</div>
</div>
</div>

View File

@ -13,12 +13,13 @@
// limitations under the License.
-->
<script lang="ts">
import type { IntlString, Asset } 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 type { Asset, IntlString } from '@anticrm/platform'
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 labelParams: Record<string, any> = {}
@ -38,7 +39,6 @@
export let title: string | undefined = undefined
export let borderStyle: 'solid' | 'dashed' = 'solid'
export let id: string | undefined = undefined
export let input: HTMLButtonElement | undefined = undefined
$: iconOnly = label === undefined && $$slots.content === undefined
@ -53,8 +53,31 @@
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>
<!-- {focusIndex} -->
<button
bind:this={input}
class="button {kind} {size} jf-{justify}"
@ -172,7 +195,7 @@
}
}
&:focus {
border-color: var(--primary-edit-border-color);
border-color: var(--accent-color) !important;
}
&:disabled {
color: rgb(var(--caption-color) / 40%);

View File

@ -15,8 +15,8 @@
<script lang="ts">
import type { IntlString } from '@anticrm/platform'
import { translate } from '@anticrm/platform'
import { createEventDispatcher } from 'svelte'
import { getPlatformColor } from '..'
import { createEventDispatcher, onMount } from 'svelte'
import { getPlatformColor, ListView } from '..'
export let placeholder: IntlString | undefined = undefined
export let placeholderParam: any | undefined = undefined
@ -33,27 +33,79 @@
}
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>
<div class="selectPopup">
<div class="selectPopup" on:keydown={onKeydown}>
{#if searchable}
<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>
{/if}
<div class="scroll">
<div class="box">
{#each value.filter((el) => el.label.toLowerCase().includes(search.toLowerCase())) as item}
<button
class="menu-item"
on:click={() => {
dispatch('close', item)
}}
>
<div class="color" style="background-color: {getPlatformColor(item.color)}" />
<span class="label">{item.label}</span>
</button>
{/each}
<ListView
bind:this={list}
count={objects.length}
bind:selection
on:click={(evt) => handleSelection(evt, evt.detail)}
>
<svelte:fragment slot="item" let:item>
{@const itemValue = objects[item]}
<button
class="menu-item"
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>

View File

@ -13,13 +13,14 @@
// limitations under the License.
-->
<script lang="ts">
import { createEventDispatcher, onMount, afterUpdate } from 'svelte'
import type { IntlString, Asset } from '@anticrm/platform'
import type { Asset, IntlString } from '@anticrm/platform'
import { translate } from '@anticrm/platform'
import type { AnySvelteComponent } from '../types'
import Label from './Label.svelte'
import Icon from './Icon.svelte'
import { afterUpdate, createEventDispatcher, onMount } from 'svelte'
import { registerFocus } from '../focus'
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 icon: Asset | AnySvelteComponent | undefined = undefined
@ -66,6 +67,22 @@
afterUpdate(() => {
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>
<div
@ -74,6 +91,7 @@
input.focus()
}}
>
<!-- {focusIndex} -->
<div class="hidden-text {kind}" bind:this={text} />
{#if label}<div class="label"><Label {label} /></div>{/if}
<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.
-->
<script lang="ts">
import { createEventDispatcher, afterUpdate, onMount } from 'svelte'
import { afterUpdate, createEventDispatcher, onMount } from 'svelte'
import ui from '../plugin'
import { Action } from '../types'
import Icon from './Icon.svelte'
import Label from './Label.svelte'
import ui from '../plugin'
export let actions: Action[] = []
export let ctx: any = undefined
@ -26,6 +26,9 @@
const btns: HTMLButtonElement[] = []
const keyDown = (ev: KeyboardEvent, n: number): void => {
if (ev.key === 'Tab') {
dispatch('close')
}
if (ev.key === 'ArrowDown') {
if (n === btns.length - 1) btns[0].focus()
else btns[n + 1].focus()

View File

@ -108,7 +108,7 @@
</div>
</div>
{#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}

View File

@ -14,8 +14,8 @@
-->
<script lang="ts">
import { afterUpdate, onDestroy } from 'svelte'
import { tooltipstore as tooltip, closeTooltip, Component } from '..'
import type { TooltipAlignment } from '..'
import { closeTooltip, Component, tooltipstore as tooltip } from '..'
import Label from './Label.svelte'
let tooltipHTML: HTMLElement
@ -151,6 +151,13 @@
on:mousemove={(ev) => {
whileShow(ev)
}}
on:keydown={(evt) => {
if (($tooltip.component || $tooltip.label) && evt.key === 'Escape') {
evt.preventDefault()
evt.stopImmediatePropagation()
hideTooltip()
}
}}
/>
{#if $tooltip.component}
<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 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 './popups'
export * from './tooltips'
export * from './panelup'
export * from './components/calendar/internal/DateUtils'
export * from './colors'
export * from './focus'
export function createApp (target: HTMLElement): SvelteComponent {
return new Root({ target })
@ -151,8 +162,3 @@ addStringsLoader(uiId, async (lang: string) => {
addLocation(uiId, async () => ({ default: async () => ({}) }))
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 { writable } from 'svelte/store'
import { AnyComponent, AnySvelteComponent, LabelAndProps, TooltipAlignment } from './types'
export const tooltipstore = writable<LabelAndProps>({
const emptyTooltip: LabelAndProps = {
label: undefined,
element: undefined,
direction: undefined,
@ -10,7 +10,8 @@ export const tooltipstore = writable<LabelAndProps>({
props: undefined,
anchor: undefined,
onUpdate: undefined
})
}
export const tooltipstore = writable<LabelAndProps>(emptyTooltip)
export function showTooltip (
label: IntlString | undefined,
@ -33,13 +34,5 @@ export function showTooltip (
}
export function closeTooltip (): void {
tooltipstore.set({
label: undefined,
element: undefined,
direction: undefined,
component: undefined,
props: undefined,
anchor: undefined,
onUpdate: undefined
})
tooltipstore.set(emptyTooltip)
}

View File

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

View File

@ -16,8 +16,9 @@
import { createEventDispatcher, onMount } from 'svelte'
import type { IntlString } 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 { FocusHandler } from '@anticrm/ui'
export let value: string = ''
export let placeholder: IntlString
@ -31,8 +32,25 @@
onMount(() => {
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>
<FocusHandler manager={mgr} />
<div class="buttons-group xsmall-gap">
{#if editable}
<input
@ -51,6 +69,7 @@
on:change
/>
<Button
focusIndex={2}
kind={'transparent'}
size={'small'}
icon={IconClose}
@ -66,6 +85,7 @@
<span>{value}</span>
{/if}
<Button
focusIndex={3}
kind={'transparent'}
size={'small'}
icon={IconCopy}
@ -75,6 +95,7 @@
/>
{#if editable}
<Button
focusIndex={4}
kind={'transparent'}
size={'small'}
icon={IconBlueCheck}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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