Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(OnyxTooltip): auto align tooltip #1821

Merged
merged 41 commits into from
Oct 25, 2024
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
797c5b9
test
Chrisiboiii Aug 30, 2024
3ecd118
test
ChristianBusshoff Sep 2, 2024
a955742
Fix float behavior
MajaZarkova Sep 2, 2024
3784894
fix: made tooltip-classes computed
Chrisiboiii Sep 2, 2024
680ad65
Merge branch 'main' into feat/730-auto-alignment-Tooltip
ChristianBusshoff Sep 2, 2024
00f336b
remove paddings in stories.ts
ChristianBusshoff Sep 3, 2024
aa50aa0
resolve comments
ChristianBusshoff Sep 3, 2024
8c3595b
change trigger type to object
ChristianBusshoff Sep 5, 2024
8d361b4
Merge branch 'main' into feat/730-auto-alignment-Tooltip
ChristianBusshoff Sep 5, 2024
4addc2f
fix: spacing
ChristianBusshoff Sep 5, 2024
0171679
Merge branch 'main' into feat/730-auto-alignment-Tooltip
ChristianBusshoff Sep 6, 2024
97c929d
chore: update Playwright screenshots (#1838)
github-actions[bot] Sep 6, 2024
acdf3c7
fix screeshottests: Checkbox (invalid), CheckboxGroup, Switch (invalid)
ChristianBusshoff Sep 8, 2024
dff028c
chore: update Playwright screenshots (#1840)
github-actions[bot] Sep 8, 2024
db05563
Merge branch 'main' into feat/730-auto-alignment-Tooltip
ChristianBusshoff Oct 1, 2024
88bf240
resolve comments
ChristianBusshoff Oct 1, 2024
89962e1
Merge branch 'main' into feat/730-auto-alignment-Tooltip
ChristianBusshoff Oct 2, 2024
bc52d98
fix: tooltip screenshot-test
ChristianBusshoff Oct 2, 2024
8f2cb08
Merge branch 'main' into feat/730-auto-alignment-Tooltip
ChristianBusshoff Oct 3, 2024
f8f4683
chore: update Playwright screenshots (#1919)
github-actions[bot] Oct 4, 2024
f415585
Merge branch 'main' into feat/730-auto-alignment-Tooltip
ChristianBusshoff Oct 9, 2024
803ad8b
Merge branch 'main' into feat/730-auto-alignment-Tooltip
ChristianBusshoff Oct 9, 2024
e20fe2a
resolved comments
ChristianBusshoff Oct 10, 2024
eeb30cf
fix WedgePosition import
ChristianBusshoff Oct 10, 2024
2aeef57
change tooltip type
ChristianBusshoff Oct 10, 2024
e48da24
Merge branch 'main' into feat/730-auto-alignment-Tooltip
ChristianBusshoff Oct 10, 2024
c0b9e54
change trigger type
ChristianBusshoff Oct 10, 2024
6e4b54c
chore: update Playwright screenshots (#1944)
github-actions[bot] Oct 10, 2024
5f39ca9
fix prettier issue
ChristianBusshoff Oct 10, 2024
6cf0a8a
Merge branch 'main' into feat/730-auto-alignment-Tooltip
ChristianBusshoff Oct 11, 2024
e23de9c
Merge branch 'main' into feat/730-auto-alignment-Tooltip
ChristianBusshoff Oct 14, 2024
a5b0326
Merge branch 'main' into feat/730-auto-alignment-Tooltip
ChristianBusshoff Oct 15, 2024
d671221
Merge branch 'main' into feat/730-auto-alignment-Tooltip
ChristianBusshoff Oct 15, 2024
bc6d8b4
remove tooltip error css
ChristianBusshoff Oct 16, 2024
a1452fd
Merge branch 'main' into feat/730-auto-alignment-Tooltip
ChristianBusshoff Oct 18, 2024
4725cbd
chore: update Playwright screenshots (#1975)
github-actions[bot] Oct 18, 2024
e5de85e
changed type declaration
ChristianBusshoff Oct 18, 2024
f1d710a
Merge branch 'main' into feat/730-auto-alignment-Tooltip
ChristianBusshoff Oct 21, 2024
e8be29b
Merge branch 'main' into feat/730-auto-alignment-Tooltip
ChristianBusshoff Oct 24, 2024
57ca2bb
fix: eslint error
ChristianBusshoff Oct 24, 2024
aa3f7a1
docs(changeset): Implement autoalignment feature for OnyxTooltip
ChristianBusshoff Oct 25, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 31 additions & 4 deletions apps/demo-app/src/views/HomeView.vue
larsrickert marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
Expand Up @@ -278,10 +278,37 @@ const currentPage = ref(1);
label="Show toast"
@click="toast.show({ headline: 'Example toast', color: 'success' })"
/>

<OnyxTooltip v-if="show('OnyxTooltip')" text="Example tooltip text">
Hover me to show tooltip
</OnyxTooltip>
<OnyxHeadline is="h2">Tooltip (auto alignment)</OnyxHeadline>

<div
:style="{
display: 'flex',
justifyContent: 'space-between',
width: '101%',
marginTop: '2rem',
}"
>
<OnyxTooltip v-if="show('OnyxTooltip')" text="Example tooltip text">
<template #default="{ trigger }">
<OnyxButton label="Left" v-bind="trigger" />
</template>
</OnyxTooltip>
<OnyxTooltip v-if="show('OnyxTooltip')" text="Example tooltip text">
<template #default="{ trigger }">
<OnyxButton label="Center" v-bind="trigger" />
</template>
</OnyxTooltip>
<OnyxTooltip v-if="show('OnyxTooltip')" text="Example tooltip text">
<template #default="{ trigger }">
<OnyxButton label="Center" v-bind="trigger" />
</template>
</OnyxTooltip>
<OnyxTooltip v-if="show('OnyxTooltip')" text="Example tooltip text">
<template #default="{ trigger }">
<OnyxButton label="Right" v-bind="trigger" />
</template>
</OnyxTooltip>
</div>
ChristianBusshoff marked this conversation as resolved.
Show resolved Hide resolved

<!-- Add new components alphabetically. -->
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ test.describe("Screenshot tests", () => {
<OnyxTooltip
text={column === "long-text" ? "Lorem ipsum dolor sit amet ".repeat(3) : "Test tooltip"}
color={row === "danger" ? "danger" : undefined}
position={row === "bottom" ? "bottom" : undefined}
position={row === "bottom" ? "bottom" : "top"}
icon={row === "icon" ? mockPlaywrightIcon : undefined}
fitParent={column === "fit-parent"}
open={true}
Expand Down Expand Up @@ -167,3 +167,51 @@ test.describe("Screenshot tests", () => {
},
});
});

test.describe("Alignment screenshot tests", () => {
executeMatrixScreenshotTest({
name: "Aligned tooltip",
columns: ["left", "center", "right"],
rows: ["top", "bottom"],
component: (column, row) => {
return (
<OnyxTooltip text="Test tooltip" position={row} open={true} float={column}>
<span
style={{
fontFamily: "var(--onyx-font-family)",
color: "var(--onyx-color-text-icons-neutral-intense)",
}}
>
Here goes the slot content
</span>
</OnyxTooltip>
);
},
// set component size to fully include the tooltip
beforeScreenshot: async (component, page, column, row) => {
const tooltipSize = await component
.getByRole("tooltip")
.evaluate((element) => [element.clientHeight, element.clientWidth]);

// set paddings to fit the full tooltip in the screenshot
await component.evaluate(
(element, { tooltipSize: [height, width], row }) => {
const verticalPadding = `${height + 12}px`;

if (row === "bottom") {
element.style.paddingBottom = verticalPadding;
element.style.marginTop = "0px";
} else element.style.paddingTop = verticalPadding;

const widthDiff = width - element.clientWidth;
if (widthDiff > 0) {
const padding = `${widthDiff / 2 + 20}px`;
element.style.paddingLeft = padding;
element.style.paddingRight = padding;
}
},
{ tooltipSize, row },
);
},
});
});
ChristianBusshoff marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,10 @@ export const Default = {
h(OnyxButton, { label: "Slot content goes here", ...(trigger as any) }),
icon: circleInformation,
open: true,
position: "auto",
color: "neutral",
float: "auto",
de,
ChristianBusshoff marked this conversation as resolved.
Show resolved Hide resolved
},
} satisfies Story;

Expand Down
91 changes: 81 additions & 10 deletions packages/sit-onyx/src/components/OnyxTooltip/OnyxTooltip.vue
Original file line number Diff line number Diff line change
@@ -1,8 +1,19 @@
<script lang="ts" setup>
import { createToggletip, createTooltip } from "@sit-onyx/headless";
import type { HTMLAttributes, MaybeRefOrGetter, Ref, VNode } from "vue";
import { computed, ref, shallowRef, toValue, watch } from "vue";
import {
computed,
nextTick,
onBeforeUnmount,
onMounted,
ref,
shallowRef,
toValue,
watch,
} from "vue";
import { useDensity } from "../../composables/density";
import { useOpenDirection } from "../../composables/useOpenDirection";
import { useWedgePosition } from "../../composables/useWedgePosition";
import { injectI18n } from "../../i18n";
import OnyxIcon from "../OnyxIcon/OnyxIcon.vue";
import type { OnyxTooltipProps } from "./types";
Expand All @@ -22,9 +33,10 @@ type CreateTooltipOptions = {

const props = withDefaults(defineProps<OnyxTooltipProps>(), {
color: "neutral",
position: "top",
position: "auto",
fitParent: false,
open: "hover",
float: "auto",
});

defineSlots<{
Expand Down Expand Up @@ -69,6 +81,21 @@ const type = computed(() => {
return "hover";
});

// classes for the tooltip | computed to drevent bugs
ChristianBusshoff marked this conversation as resolved.
Show resolved Hide resolved
const tooltipClasses = computed(() => {
return {
"onyx-tooltip--danger": props.color === "danger",
"onyx-tooltip--top": props.position === "top",
"onyx-tooltip--bottom": props.position === "bottom",
ChristianBusshoff marked this conversation as resolved.
Show resolved Hide resolved
["onyx-tooltip--" + openDirection]: props.position === "auto",
ChristianBusshoff marked this conversation as resolved.
Show resolved Hide resolved
"onyx-tooltip--fit-parent": props.fitParent,
"onyx-tooltip--hidden": !isVisible.value,
"onyx-tooltip--float--left": props.float === "left",
"onyx-tooltip--float--right": props.float === "right",
ChristianBusshoff marked this conversation as resolved.
Show resolved Hide resolved
["onyx-tooltip--float--" + wedgePosition]: wedgePosition && props.float === "auto",
ChristianBusshoff marked this conversation as resolved.
Show resolved Hide resolved
};
});

const createPattern = () =>
type.value === "hover"
? createTooltip(tooltipOptions.value)
Expand All @@ -79,19 +106,44 @@ watch(type, () => (ariaPattern.value = createPattern()));

const tooltip = computed(() => ariaPattern.value?.elements.tooltip);
const trigger = computed(() => toValue<HTMLAttributes>(ariaPattern.value?.elements.trigger));

const tooltipWrapperRef = ref<HTMLElement>();
const tooltipRef = ref<HTMLElement>();
const { openDirection, updateOpenDirection } = useOpenDirection(tooltipWrapperRef);
ChristianBusshoff marked this conversation as resolved.
Show resolved Hide resolved
const { wedgePosition, updateWedgePosition } = useWedgePosition(tooltipWrapperRef, tooltipRef);

// update open direction on resize and to ensure the tooltip is always visible
ChristianBusshoff marked this conversation as resolved.
Show resolved Hide resolved
onMounted(() => {
const updateOnEvent = () => {
ChristianBusshoff marked this conversation as resolved.
Show resolved Hide resolved
updateOpenDirection();
updateWedgePosition();
};

window.addEventListener("resize", updateOnEvent);

// initial update
updateOpenDirection();
updateWedgePosition();
});

onBeforeUnmount(() => {
window.removeEventListener("resize", updateOpenDirection);
window.removeEventListener("resize", updateWedgePosition);
});
// update open direction on visibliity changes ensure the tooltip is always visible
ChristianBusshoff marked this conversation as resolved.
Show resolved Hide resolved
watch(isVisible, async () => {
await nextTick();
updateOpenDirection();
updateWedgePosition();
});
</script>

<template>
<div :class="['onyx-tooltip-wrapper', densityClass]">
<div ref="tooltipWrapperRef" :class="['onyx-tooltip-wrapper', densityClass]">
<div
ref="tooltipRef"
v-bind="tooltip"
class="onyx-tooltip onyx-text--small onyx-truncation-multiline"
:class="{
'onyx-tooltip--danger': props.color === 'danger',
'onyx-tooltip--bottom': props.position === 'bottom',
'onyx-tooltip--fit-parent': props.fitParent,
'onyx-tooltip--hidden': !isVisible,
}"
:class="['onyx-tooltip', 'onyx-text--small', 'onyx-truncation-multiline', tooltipClasses]"
>
<OnyxIcon v-if="props.icon" :icon="props.icon" size="16px" />
<slot name="tooltip">{{ props.text }}</slot>
Expand Down Expand Up @@ -176,6 +228,25 @@ $wedge-size: 0.5rem;
border-color: transparent transparent var(--background-color);
}
}

&--float {
&--left {
left: var(--wedge-size);
transform: translateX(0);

&::after {
left: 2 * $wedge-size;
}
}
&--right {
left: 100%;
transform: translateX(-100%);

&::after {
left: calc(100% - 2 * $wedge-size);
}
}
}
}
}

Expand Down
8 changes: 7 additions & 1 deletion packages/sit-onyx/src/components/OnyxTooltip/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@ export type OnyxTooltipProps = DensityProp & {
* How to position the tooltip relative to the parent element.
*/
position?: TooltipPosition;
/**
* How to float the tooltip relative to the parent element.
*/
float?: TooltipFloat;
ChristianBusshoff marked this conversation as resolved.
Show resolved Hide resolved
/**
* If `true`, the tooltip will match the width of the parent/slot element.
*/
Expand All @@ -44,8 +48,10 @@ export type OnyxTooltipProps = DensityProp & {
open?: TooltipOpen;
};

export const TOOLTIP_POSITIONS = ["top", "bottom"] as const;
export const TOOLTIP_POSITIONS = ["top", "bottom", "auto"] as const;
export type TooltipPosition = (typeof TOOLTIP_POSITIONS)[number];
export const TOOLTIP_FLOAT = ["left", "right", "center", "auto"] as const;
ChristianBusshoff marked this conversation as resolved.
Show resolved Hide resolved
export type TooltipFloat = (typeof TOOLTIP_FLOAT)[number];
ChristianBusshoff marked this conversation as resolved.
Show resolved Hide resolved

export type TooltipOpen =
| "hover"
Expand Down
3 changes: 1 addition & 2 deletions packages/sit-onyx/src/composables/useOpenDirection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ export const useOpenDirection = (element: MaybeRef<Element | undefined>) => {

const parentTop = overflowParentRect?.top ?? window.visualViewport?.pageTop ?? 0;
const parentBottom = overflowParentRect?.bottom ?? window.visualViewport?.height ?? 0;

const freeSpaceBelow = parentBottom - elementRect.bottom;
const freeSpaceAbove = elementRect.top - parentTop;

Expand All @@ -31,7 +30,7 @@ export const useOpenDirection = (element: MaybeRef<Element | undefined>) => {
if (!element) return undefined;

const style = getComputedStyle(element);
if (style.overflow === "hidden") {
if (style.overflow === "hidden" || style.overflow === "hidden auto") {
ChristianBusshoff marked this conversation as resolved.
Show resolved Hide resolved
// if the element has hidden overflow, the flyout would be cut off by this element so we need to use
// this element as parent to calculate the open direction instead of the body.
return element;
Expand Down
80 changes: 80 additions & 0 deletions packages/sit-onyx/src/composables/useWedgePosition.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { ref, unref, type MaybeRef } from "vue";

export type WedgePosition = "center" | "left" | "right";

export const useWedgePosition = (
element: MaybeRef<Element | undefined>,
tooltipElement: MaybeRef<Element | undefined>,
) => {
const minMargin = 16;
const wedgePosition = ref<WedgePosition>("center");

const updateWedgePosition = () => {
const wrapperEl = unref(element);
const tooltipEl = unref(tooltipElement);

if (!wrapperEl || !tooltipEl) {
wedgePosition.value = "center";
return;
}

const overflowParentRect = findParentWithHiddenOverflow(wrapperEl)?.getBoundingClientRect();
const wrapperRect = wrapperEl.getBoundingClientRect();
const tooltipElementRect = tooltipEl.getBoundingClientRect();

if (tooltipElementRect.width < wrapperRect.width) {
wedgePosition.value = "center";
return;
}

const minSpace = (tooltipElementRect.width - wrapperRect.width + minMargin * 2) / 2;

const parentLeft = overflowParentRect?.left ?? window.visualViewport?.pageLeft ?? 0;
const parentRight =
overflowParentRect?.right ??
(window.visualViewport?.width ?? window.innerWidth) +
(window.visualViewport?.offsetLeft ?? 0) ??
Fixed Show fixed Hide fixed
0;

const freeSpaceLeft = wrapperRect.left - parentLeft;
const freeSpaceRight = parentRight - wrapperRect.right;

const enoughSpaceLeft = freeSpaceLeft >= minSpace;
const enoughSpaceRight = freeSpaceRight >= minSpace;

wedgePosition.value =
enoughSpaceLeft === enoughSpaceRight
? "center"
: freeSpaceLeft > freeSpaceRight
? "right"
: "left";
};

/**
* Recursively finds the first parent element with hidden overflow.
*/
const findParentWithHiddenOverflow = (element?: Element): Element | undefined => {
ChristianBusshoff marked this conversation as resolved.
Show resolved Hide resolved
if (!element) return undefined;

const style = getComputedStyle(element);
if (style.overflow === "hidden" || style.overflow === "hidden auto") {
// if the element has hidden overflow, the flyout would be cut off by this element so we need to use
// this element as parent to calculate the open direction instead of the body.
return element;
}

return element.parentElement ? findParentWithHiddenOverflow(element.parentElement) : undefined;
};

return {
/**
* Direction in which the flyout etc. should open to.
*/
wedgePosition,
/**
* Detects in which direction a flyout etc. should be opened, depending on the available space in each direction.
* Should only be called onBeforeMount or later to support server side rendering.
*/
updateWedgePosition,
};
};
Loading