Add new temporary tooltip component. Improve tooltips and user avatars

This commit is contained in:
Andrew Kingston 2023-07-07 14:46:41 +01:00
parent 4129f166e6
commit 99ef4f2992
10 changed files with 186 additions and 137 deletions

View File

@ -5,7 +5,6 @@
Bottom: "bottom", Bottom: "bottom",
Left: "left", Left: "left",
} }
export const TooltipType = { export const TooltipType = {
Default: "default", Default: "default",
Info: "info", Info: "info",
@ -23,14 +22,27 @@
export let type = TooltipType.Default export let type = TooltipType.Default
export let text = "" export let text = ""
export let fixed = false export let fixed = false
export let color = null
let wrapper let wrapper
let hovered = false let hovered = false
let left = 0 let left = 0
let top = 0 let top = 0
let visible = false
let timeout
$: visible = hovered || fixed $: {
if (hovered || fixed) {
timeout = setTimeout(show, 200)
} else {
clearTimeout(timeout)
hide()
}
}
$: tooltipStyle = color ? `background:${color};` : null
$: tipStyle = color ? `border-top-color:${color};` : null
// Computes the position of the tooltip then shows it
const show = () => { const show = () => {
const node = wrapper?.children?.[0] const node = wrapper?.children?.[0]
if (!node) { if (!node) {
@ -38,6 +50,7 @@
} }
const bounds = node.getBoundingClientRect() const bounds = node.getBoundingClientRect()
// Determine where to render tooltip based on position prop
if (position === TooltipPosition.Top) { if (position === TooltipPosition.Top) {
left = bounds.left + bounds.width / 2 left = bounds.left + bounds.width / 2
top = bounds.top top = bounds.top
@ -54,36 +67,36 @@
return return
} }
hovered = true visible = true
} }
// Hides the tooltip
const hide = () => { const hide = () => {
hovered = false visible = false
} }
</script> </script>
{#if text} <div
<div bind:this={wrapper}
bind:this={wrapper} class="abs-tooltip"
class="abs-tooltip" on:focus={null}
on:mouseover={show} on:mouseover={() => (hovered = true)}
on:mouseleave={hide} on:mouseleave={() => (hovered = false)}
> >
<slot />
</div>
{#if visible}
<Portal target=".spectrum">
<span
class="spectrum-Tooltip spectrum-Tooltip--{type} spectrum-Tooltip--{position} is-open"
style="left:{left}px;top:{top}px;"
transition:fade|local={{ duration: 130 }}
>
<span class="spectrum-Tooltip-label">{text}</span>
<span class="spectrum-Tooltip-tip" />
</span>
</Portal>
{/if}
{:else}
<slot /> <slot />
</div>
{#if visible && text}
<Portal target=".spectrum">
<span
class="spectrum-Tooltip spectrum-Tooltip--{type} spectrum-Tooltip--{position} is-open"
style={`left:${left}px;top:${top}px;${tooltipStyle}`}
transition:fade|local={{ duration: 130 }}
>
<span class="spectrum-Tooltip-label">{text}</span>
<span class="spectrum-Tooltip-tip" style={tipStyle} />
</span>
</Portal>
{/if} {/if}
<style> <style>
@ -101,6 +114,8 @@
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
font-size: 12px;
font-weight: 600;
} }
/* Colour overrides for default type */ /* Colour overrides for default type */

View File

@ -0,0 +1,39 @@
<script>
import AbsTooltip from "./AbsTooltip.svelte"
import { onDestroy } from "svelte"
export let text = null
export let condition = true
export let duration = 3000
export let position
export let type
let visible = false
let timeout
$: {
if (condition) {
showTooltip()
} else {
hideTooltip()
}
}
const showTooltip = () => {
visible = true
timeout = setTimeout(() => {
visible = false
}, duration)
}
const hideTooltip = () => {
visible = false
clearTimeout(timeout)
}
onDestroy(hideTooltip)
</script>
<AbsTooltip {position} {type} text={visible ? text : null} fixed={visible}>
<slot />
</AbsTooltip>

View File

@ -36,6 +36,7 @@ export { default as Layout } from "./Layout/Layout.svelte"
export { default as Page } from "./Layout/Page.svelte" export { default as Page } from "./Layout/Page.svelte"
export { default as Link } from "./Link/Link.svelte" export { default as Link } from "./Link/Link.svelte"
export { default as Tooltip } from "./Tooltip/Tooltip.svelte" export { default as Tooltip } from "./Tooltip/Tooltip.svelte"
export { default as TempTooltip } from "./Tooltip/TempTooltip.svelte"
export { export {
default as AbsTooltip, default as AbsTooltip,
TooltipPosition, TooltipPosition,

View File

@ -2,6 +2,7 @@
import { Icon } from "@budibase/bbui" import { Icon } from "@budibase/bbui"
import { createEventDispatcher, getContext } from "svelte" import { createEventDispatcher, getContext } from "svelte"
import { helpers } from "@budibase/shared-core" import { helpers } from "@budibase/shared-core"
import { UserAvatar } from "@budibase/frontend-core"
export let icon export let icon
export let withArrow = false export let withArrow = false
@ -98,21 +99,29 @@
<Icon color={iconColor} size="S" name={icon} /> <Icon color={iconColor} size="S" name={icon} />
</div> </div>
{/if} {/if}
<div class="text" title={showTooltip ? text : null}>{text}</div> <div class="text" title={showTooltip ? text : null}>
{text}
{#if selectedBy}
<UserAvatar user={selectedBy} size="XS" />
{/if}
</div>
{#if withActions} {#if withActions}
<div class="actions"> <div class="actions">
<slot /> <slot />
</div> </div>
{/if} {/if}
{#if $$slots.right} {#if $$slots.right}
<div class="right"> <div class="right">
<slot name="right" /> <slot name="right" />
</div> </div>
{/if} {/if}
</div> </div>
{#if selectedBy}
<div class="selected-by-label">{helpers.getUserLabel(selectedBy)}</div> <!--{#if selectedBy}-->
{/if} <!-- <div class="selected-by-label">{helpers.getUserLabel(selectedBy)}</div>-->
<!--{/if}-->
</div> </div>
<style> <style>
@ -159,36 +168,36 @@
padding-left: var(--spacing-l); padding-left: var(--spacing-l);
} }
/* Selected user styles */ /*!* Selected user styles *!*/
.nav-item.selectedBy:after { /*.nav-item.selectedBy:after {*/
content: ""; /* content: "";*/
position: absolute; /* position: absolute;*/
width: calc(100% - 4px); /* width: calc(100% - 4px);*/
height: 28px; /* height: 28px;*/
border: 2px solid var(--selected-by-color); /* border: 2px solid var(--selected-by-color);*/
left: 0; /* left: 0;*/
top: 0; /* top: 0;*/
border-radius: 2px; /* border-radius: 2px;*/
pointer-events: none; /* pointer-events: none;*/
} /*}*/
.selected-by-label { /*.selected-by-label {*/
position: absolute; /* position: absolute;*/
top: 0; /* top: 0;*/
right: 0; /* right: 0;*/
background: var(--selected-by-color); /* background: var(--selected-by-color);*/
padding: 2px 4px; /* padding: 2px 4px;*/
font-size: 12px; /* font-size: 12px;*/
color: white; /* color: white;*/
transform: translateY(calc(1px - 100%)); /* transform: translateY(calc(1px - 100%));*/
border-top-right-radius: 2px; /* border-top-right-radius: 2px;*/
border-top-left-radius: 2px; /* border-top-left-radius: 2px;*/
pointer-events: none; /* pointer-events: none;*/
opacity: 0; /* opacity: 0;*/
transition: opacity 130ms ease-out; /* transition: opacity 130ms ease-out;*/
} /*}*/
.nav-item.selectedBy:hover .selected-by-label { /*.nav-item.selectedBy:hover .selected-by-label {*/
opacity: 1; /* opacity: 1;*/
} /*}*/
/* Needed to fully display the actions icon */ /* Needed to fully display the actions icon */
.nav-item.scrollable .nav-item-content { .nav-item.scrollable .nav-item-content {
@ -245,6 +254,9 @@
color: var(--spectrum-global-color-gray-900); color: var(--spectrum-global-color-gray-900);
order: 2; order: 2;
width: 0; width: 0;
display: flex;
align-items: center;
gap: 8px;
} }
.scrollable .text { .scrollable .text {
flex: 0 0 auto; flex: 0 0 auto;

View File

@ -1,5 +1,6 @@
<script> <script>
import { UserAvatar } from "@budibase/frontend-core" import { UserAvatar } from "@budibase/frontend-core"
import { TooltipPosition } from "@budibase/bbui"
export let users = [] export let users = []
@ -15,14 +16,21 @@
</script> </script>
<div class="avatars"> <div class="avatars">
{#each uniqueUsers as user} {#each uniqueUsers as user, idx}
<UserAvatar {user} tooltipDirection="bottom" /> <span style="z-index:{100 - idx};">
<UserAvatar {user} tooltipPosition={TooltipPosition.Bottom} />
</span>
{/each} {/each}
</div> </div>
<style> <style>
.avatars { .avatars {
display: flex; display: flex;
gap: 4px; }
.avatars :global(> *:not(:first-child)) {
margin-left: -12px;
}
.avatars :global(.spectrum-Avatar) {
border: 2px solid var(--background);
} }
</style> </style>

View File

@ -46,7 +46,7 @@
/> />
<div class="delete-action"> <div class="delete-action">
<AbsTooltip <AbsTooltip
position={TooltipPosition.Right} position={TooltipPosition.Bottom}
text={$isOnlyUser text={$isOnlyUser
? null ? null
: "Unavailable - another user is editing this app"} : "Unavailable - another user is editing this app"}

View File

@ -1,60 +1,23 @@
<script> <script>
import { Avatar, Tooltip } from "@budibase/bbui" import { Avatar, AbsTooltip, TooltipPosition } from "@budibase/bbui"
import { helpers } from "@budibase/shared-core" import { helpers } from "@budibase/shared-core"
export let user export let user
export let size export let size
export let tooltipDirection = "top" export let tooltipPosition = TooltipPosition.Top
export let showTooltip = true export let showTooltip = true
$: tooltipStyle = getTooltipStyle(tooltipDirection)
const getTooltipStyle = direction => {
if (!direction) {
return ""
}
if (direction === "top") {
return "transform: translateX(-50%) translateY(-100%);"
} else if (direction === "bottom") {
return "transform: translateX(-50%) translateY(100%);"
}
}
</script> </script>
{#if user} {#if user}
<div class="user-avatar"> <AbsTooltip
text={showTooltip ? helpers.getUserLabel(user) : null}
position={tooltipPosition}
color={helpers.getUserColor(user)}
>
<Avatar <Avatar
{size} {size}
initials={helpers.getUserInitials(user)} initials={helpers.getUserInitials(user)}
color={helpers.getUserColor(user)} color={helpers.getUserColor(user)}
/> />
{#if showTooltip} </AbsTooltip>
<div class="tooltip" style={tooltipStyle}>
<Tooltip
direction={tooltipDirection}
textWrapping
text={helpers.getUserLabel(user)}
size="S"
/>
</div>
{/if}
</div>
{/if} {/if}
<style>
.user-avatar {
position: relative;
}
.tooltip {
position: absolute;
top: 0;
left: 50%;
white-space: nowrap;
pointer-events: none;
opacity: 0;
transition: opacity 130ms ease-out;
}
.user-avatar:hover .tooltip {
opacity: 1;
}
</style>

View File

@ -4,7 +4,7 @@
import HeaderCell from "../cells/HeaderCell.svelte" import HeaderCell from "../cells/HeaderCell.svelte"
import { import {
Icon, Icon,
AbsTooltip, TempTooltip,
TooltipType, TooltipType,
TooltipPosition, TooltipPosition,
} from "@budibase/bbui" } from "@budibase/bbui"
@ -16,10 +16,11 @@
hiddenColumnsWidth, hiddenColumnsWidth,
width, width,
config, config,
hasNonAutoColumn,
} = getContext("grid") } = getContext("grid")
$: columnsWidth = $renderedColumns.reduce( $: columnsWidth = $renderedColumns.reduce(
(total, col) => (total += col.width), (total, col) => total + col.width,
0 0
) )
$: end = $hiddenColumnsWidth + columnsWidth - 1 - $scroll.left $: end = $hiddenColumnsWidth + columnsWidth - 1 - $scroll.left
@ -35,19 +36,23 @@
</div> </div>
</GridScrollWrapper> </GridScrollWrapper>
{#if $config.allowSchemaChanges} {#if $config.allowSchemaChanges}
<AbsTooltip {#key left}
text="Click here to create your first column" <TempTooltip
position={TooltipPosition.Bottom} text="Click here to create your first column"
type={TooltipType.Info} position={TooltipPosition.Top}
> type={TooltipType.Info}
<div duration={3000}
class="add" condition={!$hasNonAutoColumn}
style="left:{left}px"
on:click={() => dispatch("add-column")}
> >
<Icon name="Add" /> <div
</div> class="add"
</AbsTooltip> style="left:{left}px"
on:click={() => dispatch("add-column")}
>
<Icon name="Add" />
</div>
</TempTooltip>
{/key}
{/if} {/if}
</div> </div>

View File

@ -83,6 +83,21 @@ export const deriveStores = context => {
await saveChanges() await saveChanges()
} }
// Derive if we have any normal columns
const hasNonAutoColumn = derived(
[columns, stickyColumn],
([$columns, $stickyColumn]) => {
let allCols = $columns || []
if ($stickyColumn) {
allCols = [...allCols, $stickyColumn]
}
const normalCols = allCols.filter(column => {
return !column.schema?.autocolumn
})
return normalCols.length > 0
}
)
// Persists column changes by saving metadata against table schema // Persists column changes by saving metadata against table schema
const saveChanges = async () => { const saveChanges = async () => {
const $columns = get(columns) const $columns = get(columns)
@ -128,6 +143,7 @@ export const deriveStores = context => {
} }
return { return {
hasNonAutoColumn,
columns: { columns: {
...columns, ...columns,
actions: { actions: {

View File

@ -70,8 +70,7 @@ export const deriveStores = context => {
rowHeight, rowHeight,
stickyColumn, stickyColumn,
width, width,
columns, hasNonAutoColumn,
stickyColumns,
config, config,
} = context } = context
@ -117,18 +116,9 @@ export const deriveStores = context => {
// Derive if we're able to add rows // Derive if we're able to add rows
const canAddRows = derived( const canAddRows = derived(
[config, columns, stickyColumn], [config, hasNonAutoColumn],
([$config, $columns, $stickyColumn]) => { ([$config, $hasNonAutoColumn]) => {
// Check if we have a normal column return $config.allowAddRows && $hasNonAutoColumn
let allCols = $columns || []
if ($stickyColumn) {
allCols = [...allCols, $stickyColumn]
}
const normalCols = allCols.filter(column => {
return column.visible && !column.schema?.autocolumn
})
// Check if we're allowed to add rows
return $config.allowAddRows && normalCols.length > 0
} }
) )