add single store for both popups and tooltip (#5808)

Signed-off-by: Vyacheslav Tumanov <me@slavatumanov.me>
This commit is contained in:
Vyacheslav Tumanov 2024-06-21 14:38:06 +05:00 committed by GitHub
parent caf3fedd9c
commit 649c16fe13
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 89 additions and 53 deletions

View File

@ -13,7 +13,9 @@
// limitations under the License. // limitations under the License.
--> -->
<script lang="ts"> <script lang="ts">
import { popupstore as modal } from '../popups' import { popupstore as popups } from '../popups'
import { modalStore as modals } from '../modals'
import PopupInstance from './PopupInstance.svelte' import PopupInstance from './PopupInstance.svelte'
export let contentPanel: HTMLElement | undefined = undefined export let contentPanel: HTMLElement | undefined = undefined
@ -24,13 +26,13 @@
instances.forEach((p) => p.fitPopupInstance()) instances.forEach((p) => p.fitPopupInstance())
} }
$: instances.length = $modal.filter((p) => p.dock !== true).length $: instances.length = $popups.filter((p) => p.dock !== true).length
</script> </script>
{#if $modal.length > 0} {#if $popups.length > 0}
<slot name="popup-header" /> <slot name="popup-header" />
{/if} {/if}
{#each $modal.filter((p) => p.dock !== true) as popup, i (popup.id)} {#each $popups.filter((p) => p.dock !== true) as popup, i (popup.id)}
<PopupInstance <PopupInstance
bind:this={instances[i]} bind:this={instances[i]}
is={popup.is} is={popup.is}
@ -38,8 +40,8 @@
element={popup.element} element={popup.element}
onClose={popup.onClose} onClose={popup.onClose}
onUpdate={popup.onUpdate} onUpdate={popup.onUpdate}
zIndex={(i + 1) * 500} zIndex={($modals.findIndex((modal) => modal.type === 'popup' && modal.id === popup.id) ?? i) + 10000}
top={$modal.length - 1 === i} top={$popups.length - 1 === i}
close={popup.close} close={popup.close}
{contentPanel} {contentPanel}
overlay={popup.options.overlay} overlay={popup.options.overlay}

View File

@ -280,7 +280,7 @@
class:testing class:testing
class:anim={(element === 'float' || element === 'centered') && !testing && !drag} class:anim={(element === 'float' || element === 'centered') && !testing && !drag}
bind:this={modalHTML} bind:this={modalHTML}
style={`z-index: ${zIndex + 1};`} style={`z-index: ${zIndex};`}
style:top={options?.props?.top} style:top={options?.props?.top}
style:bottom={options?.props?.bottom} style:bottom={options?.props?.bottom}
style:left={options?.props?.left} style:left={options?.props?.left}
@ -331,7 +331,7 @@
class="modal-overlay" class="modal-overlay"
class:testing class:testing
class:antiOverlay={options?.showOverlay && !drag} class:antiOverlay={options?.showOverlay && !drag}
style={`z-index: ${zIndex};`} style={`z-index: ${zIndex - 1};`}
on:click={handleOverlayClick} on:click={handleOverlayClick}
on:keydown|stopPropagation|preventDefault={() => {}} on:keydown|stopPropagation|preventDefault={() => {}}
/> />

View File

@ -15,7 +15,8 @@
<script lang="ts"> <script lang="ts">
import { afterUpdate, onDestroy } from 'svelte' import { afterUpdate, onDestroy } from 'svelte'
import { resizeObserver } from '../resize' import { resizeObserver } from '../resize'
import { closeTooltip, showTooltip, tooltipstore as tooltip } from '../tooltips' import { closeTooltip, tooltipstore as tooltip } from '../tooltips'
import { modalStore as modals } from '../modals'
import type { TooltipAlignment } from '../types' import type { TooltipAlignment } from '../types'
import Component from './Component.svelte' import Component from './Component.svelte'
import Label from './Label.svelte' import Label from './Label.svelte'
@ -259,6 +260,7 @@
<!-- svelte-ignore a11y-no-static-element-interactions --> <!-- svelte-ignore a11y-no-static-element-interactions -->
<div <div
class="modal-overlay antiOverlay" class="modal-overlay antiOverlay"
style:z-index={($modals.findIndex((t) => t.type === 'tooltip') ?? 1) + 10000}
on:click|stopPropagation|preventDefault={() => { on:click|stopPropagation|preventDefault={() => {
closeTooltip() closeTooltip()
}} }}
@ -301,6 +303,7 @@
style:width={options.width} style:width={options.width}
style:height={options.height} style:height={options.height}
style:transform={options.transform} style:transform={options.transform}
style:z-index={($modals.findIndex((t) => t.type === 'tooltip') ?? 1) + 10000}
bind:this={tooltipHTML} bind:this={tooltipHTML}
> >
{#if $tooltip.label} {#if $tooltip.label}
@ -319,13 +322,18 @@
this={$tooltip.component} this={$tooltip.component}
{...$tooltip.props} {...$tooltip.props}
on:tooltip={(evt) => { on:tooltip={(evt) => {
$tooltip = { ...$tooltip, ...evt.detail } $modals = [...$modals.filter((t) => t.type !== 'tooltip'), { ...$tooltip, ...evt.detail }]
}} }}
on:update={onUpdate !== undefined ? onUpdate : async () => {}} on:update={onUpdate !== undefined ? onUpdate : async () => {}}
/> />
{/if} {/if}
</div> </div>
<div bind:this={nubHTML} class="nub {nubDirection ?? ''}" class:shown /> <div
bind:this={nubHTML}
style:z-index={($modals.findIndex((t) => t.type === 'tooltip') ?? 1) + 10000}
class="nub {nubDirection ?? ''}"
class:shown
/>
{:else if $tooltip.label && $tooltip.kind !== 'submenu'} {:else if $tooltip.label && $tooltip.kind !== 'submenu'}
<div <div
class="tooltip {dir ?? ''} {options.classList}" class="tooltip {dir ?? ''} {options.classList}"
@ -337,6 +345,7 @@
style:width={options.width} style:width={options.width}
style:height={options.height} style:height={options.height}
style:transform={options.transform} style:transform={options.transform}
style:z-index={($modals.findIndex((t) => t.type === 'tooltip') ?? 1) + 10000}
> >
<Label label={$tooltip.label} params={$tooltip.props ?? {}} /> <Label label={$tooltip.label} params={$tooltip.props ?? {}} />
{#if $tooltip.keys !== undefined} {#if $tooltip.keys !== undefined}
@ -372,6 +381,7 @@
style:width={options.width} style:width={options.width}
style:height={options.height} style:height={options.height}
style:transform={options.transform} style:transform={options.transform}
style:z-index={($modals.findIndex((t) => t.type === 'tooltip') ?? 1) + 10000}
bind:this={tooltipHTML} bind:this={tooltipHTML}
> >
{#if typeof $tooltip.component === 'string'} {#if typeof $tooltip.component === 'string'}
@ -396,7 +406,6 @@
width: auto; width: auto;
height: auto; height: auto;
border-radius: 0.5rem; border-radius: 0.5rem;
z-index: 10000;
} }
.popup-tooltip { .popup-tooltip {
overflow: hidden; overflow: hidden;
@ -412,7 +421,6 @@
box-shadow: var(--theme-popup-shadow); box-shadow: var(--theme-popup-shadow);
user-select: none; user-select: none;
opacity: 0; opacity: 0;
z-index: 10000;
&.doublePadding { &.doublePadding {
padding: 1rem; padding: 1rem;
@ -425,7 +433,6 @@
user-select: none; user-select: none;
pointer-events: none; pointer-events: none;
opacity: 0; opacity: 0;
z-index: 10000;
&::after, &::after,
&::before { &::before {
@ -522,7 +529,6 @@
border-radius: 0.25rem; border-radius: 0.25rem;
box-shadow: var(--theme-popup-shadow); box-shadow: var(--theme-popup-shadow);
user-select: none; user-select: none;
z-index: 10000;
display: flex; display: flex;
align-items: center; align-items: center;
@ -545,8 +551,6 @@
} }
} }
.modal-overlay { .modal-overlay {
z-index: 10000;
position: fixed; position: fixed;
top: 0; top: 0;
left: 0; left: 0;

View File

@ -0,0 +1,5 @@
import { writable } from 'svelte/store'
import { type CompAndProps } from './popups'
import { type LabelAndProps } from './types'
export const modalStore = writable<Array<LabelAndProps | CompAndProps>>([])

View File

@ -1,6 +1,6 @@
import { getResource } from '@hcengineering/platform' import { getResource } from '@hcengineering/platform'
import { type ComponentType } from 'svelte' import { type ComponentType } from 'svelte'
import { derived, get, writable } from 'svelte/store' import { derived, get } from 'svelte/store'
import type { import type {
AnyComponent, AnyComponent,
AnySvelteComponent, AnySvelteComponent,
@ -13,8 +13,10 @@ import type {
} from './types' } from './types'
import { Analytics } from '@hcengineering/analytics' import { Analytics } from '@hcengineering/analytics'
import { modalStore } from './modals'
export interface CompAndProps { export interface CompAndProps {
type?: 'popup'
id: string id: string
is: AnySvelteComponent | ComponentType is: AnySvelteComponent | ComponentType
props: any props: any
@ -41,26 +43,30 @@ export interface PopupResult {
update: (props: Record<string, any>) => void update: (props: Record<string, any>) => void
} }
export const popupstore = writable<CompAndProps[]>([]) export const popupstore = derived(modalStore, (modals) => {
return modals.filter((m) => m.type === 'popup') as CompAndProps[]
})
export const dockStore = derived(popupstore, (popups) => { export const dockStore = derived(modalStore, (modals) => {
return popups.find((popup) => popup.dock) return (modals.filter((m) => m.type === 'popup') as CompAndProps[]).find((popup: CompAndProps) => popup.dock)
}) })
export function updatePopup (id: string, props: Partial<CompAndProps>): void { export function updatePopup (id: string, props: Partial<CompAndProps>): void {
popupstore.update((popups) => { modalStore.update((modals) => {
const popupIndex = popups.findIndex((p) => p.id === id) const popupIndex = (modals.filter((m) => m.type === 'popup') as CompAndProps[]).findIndex(
(p: CompAndProps) => p.id === id
)
if (popupIndex !== -1) { if (popupIndex !== -1) {
popups[popupIndex].update?.(props) ;(modals[popupIndex] as CompAndProps).update?.(props)
} }
return popups return modals
}) })
} }
function addPopup (props: CompAndProps): void { function addPopup (props: CompAndProps): void {
popupstore.update((popups) => { modalStore.update((modals) => {
popups.push(props) modals.push(props)
return popups return modals
}) })
} }
@ -93,8 +99,8 @@ export function showPopup (
): PopupResult { ): PopupResult {
const id = `${popupId++}` const id = `${popupId++}`
const closePopupOp = (): void => { const closePopupOp = (): void => {
popupstore.update((popups) => { modalStore.update((popups) => {
const pos = popups.findIndex((p) => p.id === id) const pos = popups.findIndex((p) => (p as CompAndProps).id === id && p.type === 'popup')
if (pos !== -1) { if (pos !== -1) {
popups.splice(pos, 1) popups.splice(pos, 1)
} }
@ -109,7 +115,8 @@ export function showPopup (
onClose, onClose,
onUpdate, onUpdate,
close: closePopupOp, close: closePopupOp,
options options,
type: 'popup'
} }
if (checkDockPosition(options.refId)) { if (checkDockPosition(options.refId)) {
data.dock = true data.dock = true
@ -136,19 +143,22 @@ export function showPopup (
} }
export function closePopup (category?: string): void { export function closePopup (category?: string): void {
popupstore.update((popups) => { modalStore.update((popups) => {
if (category !== undefined) { if (category !== undefined) {
popups = popups.filter((p) => p.options.category !== category) popups = popups.filter((p) => p.type === 'popup' && p.options.category !== category)
} else { } else {
for (let i = popups.length - 1; i >= 0; i--) { for (let i = popups.length - 1; i >= 0; i--) {
if (popups[i].options.fixed !== true) { if (popups[i].type !== 'popup') continue
const isClosing = popups[i].closing ?? false if ((popups[i] as CompAndProps).options.fixed !== true) {
popups[i].closing = true const isClosing = (popups[i] as CompAndProps).closing ?? false
if (popups[i].type === 'popup') {
;(popups[i] as CompAndProps).closing = true
}
if (!isClosing) { if (!isClosing) {
// To prevent possible recursion, we need to check if we call some code from popup close, to do close. // To prevent possible recursion, we need to check if we call some code from popup close, to do close.
popups[i].onClose?.(undefined) ;(popups[i] as CompAndProps).onClose?.(undefined)
} }
popups[i].closing = false ;(popups[i] as CompAndProps).closing = false
popups.splice(i, 1) popups.splice(i, 1)
break break
} }
@ -448,9 +458,10 @@ export function getEventPositionElement (evt: MouseEvent): PopupAlignment | unde
} }
export function pin (id: string): void { export function pin (id: string): void {
popupstore.update((popups) => { modalStore.update((popups) => {
const current = popups.find((p) => p.id === id) const currentPopups = popups.filter((m) => m.type === 'popup') as CompAndProps[]
popups.forEach((p) => (p.dock = p.id === id)) const current = currentPopups.find((p) => p.id === id) as CompAndProps
;(popups.filter((m) => m.type === 'popup') as CompAndProps[]).forEach((p) => (p.dock = p.id === id))
if (current?.options.refId !== undefined) { if (current?.options.refId !== undefined) {
localStorage.setItem('dock-popup', current.options.refId) localStorage.setItem('dock-popup', current.options.refId)
} }
@ -459,8 +470,8 @@ export function pin (id: string): void {
} }
export function unpin (): void { export function unpin (): void {
popupstore.update((popups) => { modalStore.update((popups) => {
popups.forEach((p) => (p.dock = false)) ;(popups.filter((m) => m.type === 'popup') as CompAndProps[]).forEach((p) => (p.dock = false))
return popups return popups
}) })
localStorage.removeItem('dock-popup') localStorage.removeItem('dock-popup')

View File

@ -1,6 +1,7 @@
import { type IntlString } from '@hcengineering/platform' import { type IntlString } from '@hcengineering/platform'
import { writable } from 'svelte/store' import { derived } from 'svelte/store'
import type { AnyComponent, AnySvelteComponent, LabelAndProps, TooltipAlignment } from './types' import type { AnyComponent, AnySvelteComponent, LabelAndProps, TooltipAlignment } from './types'
import { modalStore } from './modals'
const emptyTooltip: LabelAndProps = { const emptyTooltip: LabelAndProps = {
label: undefined, label: undefined,
@ -14,7 +15,13 @@ const emptyTooltip: LabelAndProps = {
kind: 'tooltip' kind: 'tooltip'
} }
let storedValue: LabelAndProps = emptyTooltip let storedValue: LabelAndProps = emptyTooltip
export const tooltipstore = writable<LabelAndProps>(emptyTooltip) export const tooltipstore = derived(modalStore, (modals) => {
if (modals.length === 0) {
return emptyTooltip
}
const tooltip = modals.filter((m) => m?.type === 'tooltip')
return tooltip.length > 0 ? (tooltip[0] as LabelAndProps) : emptyTooltip
})
let toHandler: any let toHandler: any
export function tooltip (node: HTMLElement, options?: LabelAndProps): any { export function tooltip (node: HTMLElement, options?: LabelAndProps): any {
@ -39,7 +46,7 @@ export function tooltip (node: HTMLElement, options?: LabelAndProps): any {
opt.kind, opt.kind,
opt.keys opt.keys
) )
}, 250) }, 10)
} else { } else {
showTooltip( showTooltip(
opt.label, opt.label,
@ -107,23 +114,29 @@ export function showTooltip (
anchor, anchor,
onUpdate, onUpdate,
kind, kind,
keys keys,
type: 'tooltip'
} }
tooltipstore.update((old) => { modalStore.update((old) => {
if (old.component === storedValue.component) { const tooltip = old.find((m) => m?.type === 'tooltip') as LabelAndProps | undefined
if (old.kind !== undefined && storedValue.kind === undefined) { if (tooltip !== undefined && tooltip.component === storedValue.component) {
storedValue.kind = old.kind if (tooltip.kind !== undefined && storedValue.kind === undefined) {
storedValue.kind = tooltip.kind
} }
if (storedValue.kind === undefined) { if (storedValue.kind === undefined) {
storedValue.kind = 'tooltip' storedValue.kind = 'tooltip'
} }
} }
return storedValue old.push(storedValue)
return old
}) })
} }
export function closeTooltip (): void { export function closeTooltip (): void {
clearTimeout(toHandler) clearTimeout(toHandler)
storedValue = emptyTooltip storedValue = emptyTooltip
tooltipstore.set(emptyTooltip) modalStore.update((old) => {
old = old.filter((m) => m?.type !== 'tooltip')
return old
})
} }

View File

@ -282,6 +282,7 @@ export interface DateOrShift {
} }
export interface LabelAndProps { export interface LabelAndProps {
type?: 'tooltip'
label?: IntlString label?: IntlString
element?: HTMLElement element?: HTMLElement
direction?: TooltipAlignment direction?: TooltipAlignment