feat: refactor hover list

This commit is contained in:
Torsten Dittmann
2022-11-11 14:51:48 +01:00
parent 122c6ac19a
commit 224458dc9c
5 changed files with 224 additions and 277 deletions
+114
View File
@@ -0,0 +1,114 @@
<script lang="ts" context="module">
export type Placement = 'top-start' | 'top-end' | 'bottom-start' | 'bottom-end';
</script>
<script lang="ts">
import { createPopper, type Instance } from '@popperjs/core';
import { onDestroy, onMount } from 'svelte';
export let show = false;
export let noArrow = false;
export let placement: Placement = 'bottom-start';
export let childStart = false;
let element: HTMLDivElement;
let tooltip: HTMLDivElement;
let arrow: HTMLDivElement;
let instance: Instance;
onMount(() => {
instance = createPopper(element, tooltip, {
placement,
modifiers: [
{
name: 'arrow',
options: {
element: arrow
}
},
{
name: 'offset',
options: {
offset: [0, noArrow ? 0 : 6]
}
},
{
name: 'flip',
options: {
fallbackPlacements: ['bottom-start', 'bottom-end', 'top-start', 'top-end']
}
}
]
});
});
$: if (show) {
instance?.update();
}
onDestroy(() => {
instance?.destroy();
});
const onBlur = (event: MouseEvent) => {
if (
show &&
!(
event.target === element ||
element.contains(event.target as Node) ||
event.target === tooltip ||
tooltip.contains(event.target as Node)
)
) {
show = false;
}
};
</script>
<svelte:window on:click={onBlur} />
<div class="drop-wrapper" class:u-cross-child-start={childStart} bind:this={element}>
<slot />
</div>
<div class="drop-tooltip" bind:this={tooltip} style="z-index: 10">
<div class="drop-arrow" class:u-hide={!show || (show && noArrow)} bind:this={arrow} />
{#if show}
<slot name="list" />
{/if}
</div>
<style global lang="scss">
.drop-tooltip[data-popper-placement^='top'] > .drop-arrow {
bottom: -4px;
}
.drop-tooltip[data-popper-placement^='bottom'] > .drop-arrow {
top: -4px;
}
.drop-tooltip[data-popper-placement^='left'] > .drop-arrow {
right: -4px;
}
.drop-tooltip[data-popper-placement^='right'] > .drop-arrow {
left: -4px;
}
.drop-arrow,
.drop-arrow::before {
position: absolute;
width: 8px;
height: 8px;
z-index: -1;
}
.drop-arrow::before {
content: '';
transform: rotate(45deg);
background: hsl(var(--color-neutral-200));
body.theme-light & {
background: hsl(var(--color-neutral-10));
}
}
</style>
+7 -102
View File
@@ -1,77 +1,17 @@
<script lang="ts">
import { createPopper, type Instance } from '@popperjs/core';
import { onDestroy, onMount } from 'svelte';
import { Drop } from '.';
import type { Placement } from './drop.svelte';
export let show = false;
export let noArrow = false;
export let placement: Placement = 'bottom-start';
export let scrollable = false;
export let childStart = false;
type Placement = 'top-start' | 'top-end' | 'bottom-start' | 'bottom-end';
let element: HTMLDivElement;
let tooltip: HTMLDivElement;
let arrow: HTMLDivElement;
let instance: Instance;
onMount(() => {
instance = createPopper(element, tooltip, {
placement,
modifiers: [
{
name: 'arrow',
options: {
element: arrow
}
},
{
name: 'offset',
options: {
offset: [0, noArrow ? 0 : 6]
}
},
{
name: 'flip',
options: {
fallbackPlacements: ['top-start', 'top-end', 'bottom-start', 'bottom-end']
}
}
]
});
});
$: if (show) {
instance?.update();
}
onDestroy(() => {
instance?.destroy();
});
const onBlur = (event: MouseEvent) => {
if (
show &&
!(
event.target === element ||
element.contains(event.target as Node) ||
event.target === tooltip ||
tooltip.contains(event.target as Node)
)
) {
show = false;
}
};
export let noArrow = false;
</script>
<svelte:window on:click={onBlur} />
<div class="drop-wrapper" class:u-cross-child-start={childStart} bind:this={element}>
<Drop bind:show {placement} {childStart} {noArrow}>
<slot />
</div>
<div class="drop-tooltip" bind:this={tooltip} style="z-index: 10">
<div class="drop-arrow" class:u-hide={!show} bind:this={arrow} />
{#if show}
<svelte:fragment slot="list">
<div class="drop is-no-arrow" style="position: revert">
<section
class:u-overflow-y-auto={scrollable}
@@ -83,40 +23,5 @@
</section>
<slot name="other" />
</div>
{/if}
</div>
<style global lang="scss">
.drop-tooltip[data-popper-placement^='top'] > .drop-arrow {
bottom: -4px;
}
.drop-tooltip[data-popper-placement^='bottom'] > .drop-arrow {
top: -4px;
}
.drop-tooltip[data-popper-placement^='left'] > .drop-arrow {
right: -4px;
}
.drop-tooltip[data-popper-placement^='right'] > .drop-arrow {
left: -4px;
}
.drop-arrow,
.drop-arrow::before {
position: absolute;
width: 8px;
height: 8px;
z-index: -1;
}
.drop-arrow::before {
content: '';
transform: rotate(45deg);
background: hsl(var(--color-neutral-200));
body.theme-light & {
background: hsl(var(--color-neutral-10));
}
}
</style>
</svelte:fragment>
</Drop>
-141
View File
@@ -1,141 +0,0 @@
<script lang="ts">
import { createPopper, type Instance } from '@popperjs/core';
import { onDestroy, onMount } from 'svelte';
export let show = false;
export let noArrow = false;
export let placement: Placement = 'bottom-start';
export let scrollable = false;
export let childStart = false;
type Placement = 'top-start' | 'top-end' | 'bottom-start' | 'bottom-end';
let element: HTMLDivElement;
let tooltip: HTMLDivElement;
let instance: Instance;
let currentArrow: Placement;
let childHover = false;
$: [arrowHorizontal, arrowPosition] = applyArrow(currentArrow);
function applyArrow(value: Placement): [string: 'start' | 'end', string: 'top' | 'bottom'] {
switch (value) {
case 'bottom-start':
return ['start', 'bottom'];
case 'bottom-end':
return ['end', 'bottom'];
case 'top-start':
return ['start', 'top'];
case 'top-end':
return ['end', 'top'];
default:
return ['start', 'bottom'];
}
}
onMount(() => {
instance = createPopper(element, tooltip, {
placement,
onFirstUpdate(state) {
if (currentArrow !== state.placement) {
currentArrow = state.placement as Placement;
}
},
modifiers: [
{
name: 'offset',
options: {
offset: [0, noArrow ? 0 : 12]
}
},
{
name: 'flip',
options: {
fallbackPlacements: ['top-start', 'top-end', 'bottom-start', 'bottom-end']
}
},
{
name: 'placementLogger',
enabled: true,
phase: 'main',
fn({ state }) {
if (currentArrow !== state.placement) {
currentArrow = state.placement as Placement;
}
}
}
]
});
});
$: if (show) {
instance?.update();
}
onDestroy(() => {
instance?.destroy();
});
const onBlur = (event: MouseEvent) => {
if (
show &&
!(
event.target === element ||
element.contains(event.target as Node) ||
event.target === tooltip ||
tooltip.contains(event.target as Node)
)
) {
show = false;
}
};
function enter() {
show = true;
}
function leave() {
setTimeout(() => {
if (!childHover) {
show = false;
}
}, 100);
}
</script>
<svelte:window on:click={onBlur} />
<div
on:mouseover={enter}
on:focus={enter}
on:mouseout={leave}
on:blur={leave}
class:u-cross-child-start={childStart}
bind:this={element}>
<slot />
<div
on:mouseleave={() => (childHover = false)}
on:mouseenter={() => (childHover = true)}
bind:this={tooltip}
style="z-index: 10">
{#if show}
<div
class="drop"
style="position: revert"
class:is-no-arrow={noArrow}
class:is-arrow-start={arrowHorizontal === 'start'}
class:is-arrow-end={arrowHorizontal === 'end'}
class:is-block-start={arrowPosition === 'top'}
class:is-block-end={arrowPosition === 'bottom'}>
<section
class:u-overflow-y-auto={scrollable}
class:u-max-height-200={scrollable}
class="drop-section ">
<ul class="drop-list">
<slot name="list" />
</ul>
</section>
<slot name="other" />
</div>
{/if}
</div>
</div>
+1 -1
View File
@@ -14,7 +14,7 @@ export { default as List } from './list.svelte';
export { default as ListItem } from './listItem.svelte';
export { default as Empty } from './empty.svelte';
export { default as EmptySearch } from './emptySearch.svelte';
export { default as HoverList } from './hoverList.svelte';
export { default as Drop } from './drop.svelte';
export { default as DropList } from './dropList.svelte';
export { default as DropListItem } from './dropListItem.svelte';
export { default as DropListLink } from './dropListLink.svelte';
+102 -33
View File
@@ -1,11 +1,15 @@
<script lang="ts">
import { tooltip } from '$lib/actions/tooltip';
import { sdkForProject } from '$lib/stores/sdk';
import type { Models } from '@aw-labs/appwrite-console';
import { AvatarInitials, HoverList } from '../';
import { tick } from 'svelte';
import { AvatarInitials } from '../';
import Output from '../output.svelte';
export let role: string;
export let showDropdown = false;
let content: HTMLDivElement;
let data = null;
async function getData(
permission: string
@@ -23,7 +27,7 @@
}
</script>
<div class="u-flex u-cross-center u-gap-8">
<div class="u-flex u-cross-center u-gap-8 tippy-user">
<div>
{#if role === 'users'}
<div>Users</div>
@@ -32,36 +36,101 @@
{:else if role === 'any'}
<div>Any</div>
{:else}
<HoverList bind:show={showDropdown} placement="bottom-start">
<div class="u-trim-1">
{role}
</div>
<svelte:fragment slot="list">
{#await getData(role) then data}
<div class="u-flex u-cross-center u-gap-16">
<AvatarInitials name={data.name} size={40} />
<div>
<p class="text body-text-1">
{data.name
? data.name
: data?.email
? data?.email
: data?.phone
? data?.phone
: '-'}
</p>
<Output value={data.$id}>{role}</Output>
</div>
</div>
{#if data?.email || data?.phone}
<p class="text u-small">{data?.email}</p>
<p class="text u-small">{data?.phone}</p>
{:else if data?.total}
<p class="text u-small">Members:{data?.total}</p>
{/if}
{/await}
</svelte:fragment>
</HoverList>
<div
class="u-trim-1"
use:tooltip={{
interactive: true,
allowHTML: true,
onTrigger(instance) {
instance.hide();
getData(role)
.then((n) => {
data = n;
})
.finally(() => {
tick().then(() => {
instance.setContent(content.innerHTML);
instance.show();
});
});
}
}}>
{role}
</div>
<div class="u-hide" bind:this={content}>
{#if data}
<div class="user-profile">
<AvatarInitials name={data.name} size={40} />
<span class="user-profile-info is-only-desktop">
<span class="name">
{data.name
? data.name
: data?.email
? data?.email
: data?.phone
? data?.phone
: '-'}
</span>
<Output value={data.$id}>{role}</Output>
</span>
<span class="user-profile-sep" />
<span class="user-profile-empty-column" />
<span class="user-profile-info is-only-desktop">
{#if role.startsWith('user')}
<p class="text u-x-small">{data?.email}</p>
<p class="text u-x-small">{data?.phone}</p>
{:else if role.startsWith('team')}
<p class="text u-x-small">Members: {data?.total}</p>
{/if}
</span>
</div>
{:else}
Not found.
{/if}
</div>
{/if}
</div>
</div>
<style lang="scss" global>
.tippy-user .tippy-box {
--p-drop-bg-color: var(--color-neutral-500);
--p-drop-border-color: var(--color-neutral-200);
body.theme-light & {
--p-drop-bg-color: var(--color-neutral-0);
--p-drop-border-color: var(--color-neutral-10);
}
inset-inline-start: -0.625rem;
inset-block-end: calc(100% + 0.625rem);
background-color: hsl(var(--p-drop-bg-color));
border: solid 0.0625rem hsl(var(--p-drop-border-color));
border-radius: var(--border-radius-small);
box-shadow: var(--shadow-small);
font-size: var(--font-size-0);
color: hsl(var(--p-body-text-color));
max-inline-size: 32.5rem;
margin-inline: auto;
line-height: 1.5;
.tippy-content {
padding: 1rem;
}
&[data-placement^='top'] > .tippy-arrow::before {
border-top-color: hsl(var(--p-drop-bg-color));
}
&[data-placement^='bottom'] > .tippy-arrow::before {
border-bottom-color: hsl(var(--p-drop-bg-color));
}
&[data-placement^='left'] > .tippy-arrow::before {
border-left-color: hsl(var(--p-drop-bg-color));
}
&[data-placement^='right'] > .tippy-arrow::before {
border-right-color: hsl(var(--p-drop-bg-color));
}
}
</style>