Add new temporary tooltip component. Improve tooltips and user avatars
This commit is contained in:
parent
4129f166e6
commit
99ef4f2992
|
@ -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 */
|
||||||
|
|
|
@ -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>
|
|
@ -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,
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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"}
|
||||||
|
|
|
@ -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>
|
|
||||||
|
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
@ -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: {
|
||||||
|
|
|
@ -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
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue