Skip to content

Commit

Permalink
feat(tooltip): support w3c WAI-ARIA pattern
Browse files Browse the repository at this point in the history
  • Loading branch information
mlmoravek committed Dec 14, 2024
1 parent 06f59cb commit 1cb4bce
Show file tree
Hide file tree
Showing 11 changed files with 107 additions and 118 deletions.
4 changes: 2 additions & 2 deletions packages/docs/components/Tooltip.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
| active | Whether tooltip is active or not, use v-model:active to make it two-way binding | boolean | - | <code style='white-space: nowrap; padding: 0;'>false</code> |
| always | Tooltip will be always active | boolean | - | <code style='white-space: nowrap; padding: 0;'>false</code> |
| animation | Tooltip default animation | string | - | <div><small>From <b>config</b>:</small></div><code style='white-space: nowrap; padding: 0;'>tooltip: {<br>&nbsp;&nbsp;animation: "fade"<br>}</code> |
| closeable | Tooltip auto close options (pressing escape, clicking the content or outside) | boolean \| string[] | `true`, `false`, `content`, `outside`, `escape` | <div><small>From <b>config</b>:</small></div><code style='white-space: nowrap; padding: 0;'>tooltip: {<br>&nbsp;&nbsp;closeable: ["escape","outside","content"]<br>}</code> |
| closeable | Tooltip auto close options (pressing escape, clicking the content or outside) | boolean \| ("escape" \| "outside" \| "content")[] | `true`, `false`, `content`, `outside`, `escape` | <div><small>From <b>config</b>:</small></div><code style='white-space: nowrap; padding: 0;'>tooltip: {<br>&nbsp;&nbsp;closeable: ["escape","outside","content"]<br>}</code> |
| delay | Tooltip delay before it appears (number in ms) | number | - | |
| disabled | Tooltip will be disabled | boolean | - | <code style='white-space: nowrap; padding: 0;'>false</code> |
| label | Tooltip text, unnecessary when content slot is used | string | - | |
Expand All @@ -48,7 +48,7 @@
| position | Position of the Tooltip relative to the trigger | "auto" \| "bottom-left" \| "bottom-right" \| "bottom" \| "left" \| "right" \| "top-left" \| "top-right" \| "top" | `auto`, `top`, `bottom`, `left`, `right`, `top-right`, `top-left`, `bottom-left`, `bottom-right` | <div><small>From <b>config</b>:</small></div><code style='white-space: nowrap; padding: 0;'>tooltip: {<br>&nbsp;&nbsp;position: "auto"<br>}</code> |
| teleport | Append the component to another part of the DOM.<br/>Set `true` to append the component to the body.<br/>In addition, any CSS selector string or an actual DOM node can be used. | boolean \| object \| string | - | <div><small>From <b>config</b>:</small></div><code style='white-space: nowrap; padding: 0;'>dropdown: {<br>&nbsp;&nbsp;teleport: false<br>}</code> |
| triggerTag | Tooltip trigger tag name | DynamicComponent | - | <div><small>From <b>config</b>:</small></div><code style='white-space: nowrap; padding: 0;'>tooltip: {<br>&nbsp;&nbsp;triggerTag: "div"<br>}</code> |
| triggers | Tooltip trigger events | ("click" \| "contextmenu" \| "focus" \| "hover")[] | `hover`, `click`, `focus`, `contextmenu` | <div><small>From <b>config</b>:</small></div><code style='white-space: nowrap; padding: 0;'>tooltip: {<br>&nbsp;&nbsp;triggers: ["hover"]<br>}</code> |
| triggers | Tooltip trigger events | ("click" \| "contextmenu" \| "focus" \| "hover")[] | `hover`, `click`, `focus`, `contextmenu` | <div><small>From <b>config</b>:</small></div><code style='white-space: nowrap; padding: 0;'>tooltip: {<br>&nbsp;&nbsp;triggers: ["hover","focus"]<br>}</code> |
| variant | Color of the tooltip | string | `primary`, `info`, `success`, `warning`, `danger`, `and any other custom color` | <div><small>From <b>config</b>:</small></div><code style='white-space: nowrap; padding: 0;'>tooltip: {<br>&nbsp;&nbsp;variant: undefined<br>}</code> |

### Events
Expand Down
13 changes: 8 additions & 5 deletions packages/oruga/src/components/dropdown/Dropdown.vue
Original file line number Diff line number Diff line change
Expand Up @@ -174,11 +174,14 @@ watch(
if (cancelOptions.value.includes("outside")) {
// set outside handler
eventCleanups.push(
useClickOutside(contentRef, onClickedOutside, {
ignore: [triggerRef],
immediate: true,
passive: true,
}),
useClickOutside(
[contentRef, triggerRef],
onClickedOutside,
{
immediate: true,
passive: true,
},
),
);
}
Expand Down
82 changes: 29 additions & 53 deletions packages/oruga/src/components/tooltip/Tooltip.vue
Original file line number Diff line number Diff line change
@@ -1,15 +1,11 @@
<script setup lang="ts">
import { ref, computed, watch, nextTick, type Component } from "vue";
import { ref, computed, watch, nextTick, useId, type Component } from "vue";
import PositionWrapper from "../utils/PositionWrapper.vue";
import { getDefault } from "@/utils/config";
import { isClient } from "@/utils/ssr";
import {
defineClasses,
useEventListener,
useClickOutside,
} from "@/composables";
import { defineClasses, useClickOutside } from "@/composables";
import type { TooltipProps } from "./props";
Expand All @@ -35,7 +31,7 @@ const props = withDefaults(defineProps<TooltipProps>(), {
animation: () => getDefault("tooltip.animation", "fade"),
multiline: false,
triggerTag: () => getDefault("tooltip.triggerTag", "div"),
triggers: () => getDefault("tooltip.triggers", ["hover"]),
triggers: () => getDefault("tooltip.triggers", ["hover", "focus"]),
delay: undefined,
closeable: () =>
getDefault("tooltip.closeable", ["escape", "outside", "content"]),
Expand All @@ -61,6 +57,8 @@ watch(isActive, (value) => {
else emits("close");
});
const tooltipId = useId();
const timer = ref();
const autoPosition = ref(props.position);
Expand All @@ -76,39 +74,6 @@ watch(
const contentRef = ref<HTMLElement | Component>();
const triggerRef = ref<HTMLElement>();
const eventCleanups: (() => void)[] = [];
watch(isActive, (value) => {
// on active set event handler
if (value && isClient) {
setTimeout(() => {
if (cancelOptions.value.indexOf("outside") >= 0) {
// set outside handler
eventCleanups.push(
useClickOutside(contentRef, onClickedOutside, {
ignore: [triggerRef],
immediate: true,
passive: true,
}),
);
}
if (cancelOptions.value.indexOf("escape") >= 0) {
// set keyup handler
eventCleanups.push(
useEventListener("keyup", onKeyPress, document, {
immediate: true,
}),
);
}
});
} else if (!value) {
// on close cleanup event handler
eventCleanups.forEach((fn) => fn());
eventCleanups.length = 0;
}
});
const cancelOptions = computed<string[]>(() =>
typeof props.closeable === "boolean"
? props.closeable
Expand All @@ -117,41 +82,48 @@ const cancelOptions = computed<string[]>(() =>
: props.closeable,
);
// set click outside handler
if (isClient && cancelOptions.value.includes("outside")) {
useClickOutside([contentRef, triggerRef], onClickedOutside, {
trigger: isActive,
passive: true,
});
}
/** Close tooltip if clicked outside. */
function onClickedOutside(): void {
if (!isActive.value || props.always) return;
if (cancelOptions.value.indexOf("outside") < 0) return;
if (!cancelOptions.value.includes("outside")) return;
isActive.value = false;
}
/** Keypress event that is bound to the document */
function onKeyPress(event: KeyboardEvent): void {
if (isActive.value && (event.key === "Escape" || event.key === "Esc")) {
if (cancelOptions.value.indexOf("escape") < 0) return;
isActive.value = false;
}
/** Escape keydown event that is bound to the trigger */
function onEscape(): void {
if (!isActive.value) return;
if (!cancelOptions.value.includes("escape")) return;
isActive.value = false;
}
function onClick(): void {
if (props.triggers.indexOf("click") < 0) return;
if (!props.triggers.includes("click")) return;
// if not active, toggle after clickOutside event
// this fixes toggling programmatic
nextTick(() => setTimeout(() => open()));
}
function onContextMenu(event: Event): void {
if (props.triggers.indexOf("contextmenu") < 0) return;
if (!props.triggers.includes("contextmenu")) return;
event.preventDefault();
open();
}
function onFocus(): void {
if (props.triggers.indexOf("focus") < 0) return;
if (!props.triggers.includes("focus")) return;
open();
}
function onHover(): void {
if (props.triggers.indexOf("hover") < 0) return;
if (!props.triggers.includes("hover")) return;
open();
}
Expand All @@ -168,7 +140,7 @@ function open(): void {
}
function onClose(): void {
if (cancelOptions.value.indexOf("content") < 0) return;
if (!cancelOptions.value.includes("content")) return;
isActive.value = !props.closeable;
if (timer.value && props.closeable) clearTimeout(timer.value);
}
Expand Down Expand Up @@ -239,6 +211,8 @@ const contentClasses = defineClasses(
ref="triggerRef"
:class="triggerClasses"
aria-haspopup="true"
:aria-describedby="tooltipId"
@keydown.escape="onEscape"
@click="onClick"
@contextmenu="onContextMenu"
@mouseenter="onHover"
Expand All @@ -263,8 +237,10 @@ const contentClasses = defineClasses(
<transition :name="animation">
<div
v-show="isActive || (always && !disabled)"
:id="tooltipId"
:ref="(el) => (contentRef = setContent(el as HTMLElement))"
:class="contentClasses">
:class="contentClasses"
role="tooltip">
<span :class="arrowClasses"></span>

<!--
Expand Down
8 changes: 4 additions & 4 deletions packages/oruga/src/components/tooltip/examples/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ import MultilineCode from "./multiline.vue?raw";
import Slot from "./slot.vue";
import SlotCode from "./slot.vue?raw";
import Toggle from "./toggle.vue";
import ToggleCode from "./toggle.vue?raw";
import Triggers from "./triggers.vue";
import TriggersCode from "./triggers.vue?raw";
</script>

<template>
Expand All @@ -34,6 +34,6 @@ import ToggleCode from "./toggle.vue?raw";
<h3 id="slot">Slot</h3>
<ExampleViewer :component="Slot" :code="SlotCode" />

<h3 id="toggle">Toggle</h3>
<ExampleViewer :component="Toggle" :code="ToggleCode" />
<h3 id="triggers">Triggers</h3>
<ExampleViewer :component="Triggers" :code="TriggersCode" />
</template>
8 changes: 2 additions & 6 deletions packages/oruga/src/components/tooltip/examples/multiline.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,16 @@
<o-tooltip
label="Tooltip multiline, probably because it's too long for a casual tooltip"
multiline>
<o-button label="Multiline (default)" />
<o-button label="Multiline" />
</o-tooltip>

<o-tooltip
label="It's not brief, but it's also not long"
size="small"
multiline>
<o-tooltip label="It's not brief, but it's also not long" multiline>
<o-button label="Multiline (small)" />
</o-tooltip>

<o-tooltip
label="Tooltip large multiline, because it's too long to be on a medium size. Did I tell you it's really long? Yes, it is — I assure you!"
position="bottom"
size="large"
multiline>
<o-button label="Multiline (large)" />
</o-tooltip>
Expand Down
31 changes: 0 additions & 31 deletions packages/oruga/src/components/tooltip/examples/toggle.vue

This file was deleted.

45 changes: 45 additions & 0 deletions packages/oruga/src/components/tooltip/examples/triggers.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<script setup lang="ts">
import { ref } from "vue";
const active = ref(true);
</script>

<template>
<section class="odocs-spaced">
<o-field>
<o-switch v-model="active" label="Toggle" />
</o-field>

<o-tooltip
label="I'm never closing"
:triggers="['click']"
:closeable="['outside']">
<o-button label="Click me" />
</o-tooltip>

<o-tooltip
label="I'm never closing"
:triggers="['contextmenu']"
:closeable="['outside']">
<o-button label="Right click me" />
</o-tooltip>

<o-tooltip
label="I'm never closing"
:active="active"
always
position="bottom">
<o-button label="Always" />
</o-tooltip>

<o-tooltip
variant="danger"
label="Tooltip right"
position="right"
:triggers="[]"
:closeable="false"
:active="active">
<o-button label="Toggled" />
</o-tooltip>
</section>
</template>
4 changes: 2 additions & 2 deletions packages/oruga/src/components/tooltip/props.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,14 +40,14 @@ export type TooltipProps = {
* Tooltip trigger events
* @values hover, click, focus, contextmenu
*/
triggers?: ("click" | "hover" | "contextmenu" | "focus")[];
triggers?: Array<"click" | "hover" | "contextmenu" | "focus">;
/** Tooltip delay before it appears (number in ms) */
delay?: number;
/**
* Tooltip auto close options (pressing escape, clicking the content or outside)
* @values true, false, content, outside, escape
*/
closeable?: string[] | boolean;
closeable?: Array<"content" | "outside" | "escape"> | boolean;
/**
* Append the component to another part of the DOM.
* Set `true` to append the component to the body.
Expand Down
2 changes: 1 addition & 1 deletion packages/oruga/src/components/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1496,7 +1496,7 @@ In addition, any CSS selector string or an actual DOM node can be used. */
/** Tooltip trigger events */
triggers: ("click" | "contextmenu" | "focus" | "hover")[];
/** Tooltip auto close options (pressing escape, clicking the content or outside) */
closeable: boolean | string[];
closeable: boolean | ("escape" | "outside" | "content")[];
/** Append the component to another part of the DOM.
Set `true` to append the component to the body.
In addition, any CSS selector string or an actual DOM node can be used. */
Expand Down
Loading

0 comments on commit 1cb4bce

Please sign in to comment.