Skip to content

Commit 487040e

Browse files
committed
[Toolkit][Shadcn] Add Tooltip component
1 parent 8cd5baa commit 487040e

File tree

22 files changed

+597
-1
lines changed

22 files changed

+597
-1
lines changed

src/Toolkit/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
## 2.33.0
44

55
- [Shadcn] Add `accordion` recipe
6+
- [Shadcn] Add `tooltip` recipe
67
- [Shadcn] Rework templates of `avatar` recipe
78
- [Shadcn] Rework templates of `badge` recipe
89
- [Shadcn] Rework templates of `card` recipe

src/Toolkit/kits/shadcn/accordion/templates/components/Accordion.html.twig

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
{# @prop id string Unique identifier for the accordion #}
1+
{# @prop id string Unique identifier for the Accordion #}
22
{# @prop multiple boolean Whether multiple items can be opened at once, default to `false` #}
33
{# @prop defaultValue string|array<string>|null Value(s) of the item(s) to open by default #}
44
{# @prop orientation 'vertical'|'horizontal'The visual orientation of the accordion. Controls whether roving focus uses left/right or up/down arrow keys, default to `vertical` #}
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
import { Controller } from '@hotwired/stimulus';
2+
3+
export default class extends Controller {
4+
static values = {
5+
delayDuration: Number,
6+
// Using targets does not work if the elements are moved in the DOM (document.body.appendChild)
7+
// and using outlets does not work either if elements are children of the controller element.
8+
wrapperSelector: String,
9+
contentSelector: String,
10+
arrowSelector: String,
11+
}
12+
static targets = ['trigger'];
13+
14+
connect() {
15+
this.wrapperElement = document.querySelector(this.wrapperSelectorValue);
16+
this.contentElement = document.querySelector(this.contentSelectorValue);
17+
this.arrowElement = document.querySelector(this.arrowSelectorValue);
18+
19+
this.side = this.wrapperElement.getAttribute('data-side') || 'top';
20+
this.sideOffset = parseInt(this.wrapperElement.getAttribute('data-side-offset'), 10) || 0;
21+
22+
this.showTimeout = null;
23+
this.hideTimeout = null;
24+
25+
document.body.appendChild(this.wrapperElement);
26+
}
27+
28+
disconnect() {
29+
this.clearTimeouts();
30+
this.element.appendChild(this.wrapperElement);
31+
}
32+
33+
clearTimeouts() {
34+
if (this.showTimeout) {
35+
clearTimeout(this.showTimeout);
36+
this.showTimeout = null;
37+
}
38+
if (this.hideTimeout) {
39+
clearTimeout(this.hideTimeout);
40+
this.hideTimeout = null;
41+
}
42+
}
43+
44+
show() {
45+
this.clearTimeouts();
46+
47+
const delay = this.hasDelayDurationValue ? this.delayDurationValue : 0;
48+
49+
this.showTimeout = setTimeout(() => {
50+
this.doShow();
51+
this.showTimeout = null;
52+
}, delay);
53+
}
54+
55+
hide() {
56+
this.clearTimeouts();
57+
this.doHide();
58+
}
59+
60+
positionElements() {
61+
const triggerRect = this.triggerTarget.getBoundingClientRect();
62+
const contentRect = this.contentElement.getBoundingClientRect();
63+
const arrowRect = this.arrowElement.getBoundingClientRect();
64+
65+
let wrapperLeft = 0;
66+
let wrapperTop = 0;
67+
let arrowLeft = null;
68+
let arrowTop = null;
69+
switch (this.side) {
70+
case 'left':
71+
wrapperLeft = triggerRect.left - contentRect.width - (arrowRect.width / 2) - this.sideOffset;
72+
wrapperTop = triggerRect.top - (contentRect.height / 2) + (triggerRect.height / 2);
73+
arrowTop = contentRect.height / 2 - (arrowRect.height / 2);
74+
break;
75+
case 'top':
76+
wrapperLeft = triggerRect.left - (contentRect.width / 2) + (triggerRect.width / 2);
77+
wrapperTop = triggerRect.top - contentRect.height - (arrowRect.height / 2) - this.sideOffset;
78+
arrowLeft = contentRect.width / 2 - (arrowRect.width / 2);
79+
break;
80+
case 'right':
81+
wrapperLeft = triggerRect.right + (arrowRect.width / 2) + this.sideOffset;
82+
wrapperTop = triggerRect.top - (contentRect.height / 2) + (triggerRect.height / 2);
83+
arrowTop = contentRect.height / 2 - (arrowRect.height / 2);
84+
break;
85+
case 'bottom':
86+
wrapperLeft = triggerRect.left - (contentRect.width / 2) + (triggerRect.width / 2);
87+
wrapperTop = triggerRect.bottom + (arrowRect.height / 2) + this.sideOffset;
88+
arrowLeft = contentRect.width / 2 - (arrowRect.width / 2);
89+
break;
90+
}
91+
92+
this.wrapperElement.style.transform = `translate3d(${wrapperLeft}px, ${wrapperTop}px, 0)`;
93+
if (arrowLeft !== null) {
94+
this.arrowElement.style.left = `${arrowLeft}px`;
95+
}
96+
if (arrowTop !== null) {
97+
this.arrowElement.style.top = `${arrowTop}px`;
98+
}
99+
}
100+
101+
doShow() {
102+
this.wrapperElement.setAttribute('open', '');
103+
this.contentElement.setAttribute('open', '');
104+
this.arrowElement.setAttribute('open', '');
105+
this.positionElements();
106+
}
107+
108+
doHide() {
109+
this.wrapperElement.removeAttribute('open');
110+
this.contentElement.removeAttribute('open');
111+
this.arrowElement.removeAttribute('open');
112+
}
113+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<twig:Tooltip id="tooltip-demo">
2+
<twig:Tooltip:Trigger>
3+
<twig:Button {{ ...trigger_attrs }} variant="outline">Hover</twig:Button>
4+
</twig:Tooltip:Trigger>
5+
<twig:Tooltip:Content>
6+
<p>Add to library</p>
7+
</twig:Tooltip:Content>
8+
</twig:Tooltip>
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<twig:Tooltip id="tooltip-disabled-button">
2+
<twig:Tooltip:Trigger>
3+
<twig:Button {{ ...trigger_attrs }} variant="outline" disabled>Disabled</twig:Button>
4+
</twig:Tooltip:Trigger>
5+
<twig:Tooltip:Content>
6+
<p>This feature is currently unavailable</p>
7+
</twig:Tooltip:Content>
8+
</twig:Tooltip>
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<div class="flex flex-wrap gap-2">
2+
{% for side in ['left', 'top', 'bottom', 'right'] %}
3+
<twig:Tooltip id="tooltip-side-{{ side }}">
4+
<twig:Tooltip:Trigger>
5+
<twig:Button {{ ...trigger_attrs }} variant="outline" class="w-fit">
6+
{{ side|capitalize }}
7+
</twig:Button>
8+
</twig:Tooltip:Trigger>
9+
<twig:Tooltip:Content side="{{ side }}">
10+
<p>Add to library</p>
11+
</twig:Tooltip:Content>
12+
</twig:Tooltip>
13+
{% endfor %}
14+
</div>
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<twig:Tooltip id="my-tooltip">
2+
<twig:Tooltip:Trigger>
3+
<twig:Button {{ ...trigger_attrs }}>Hover</twig:Button>
4+
</twig:Tooltip:Trigger>
5+
<twig:Tooltip:Content>
6+
<p>Add to library</p>
7+
</twig:Tooltip:Content>
8+
</twig:Tooltip>
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<twig:Tooltip id="tooltip-with-keyboard-shortcut">
2+
<twig:Tooltip:Trigger>
3+
<twig:Button {{ ...trigger_attrs }} variant="outline" size="icon-sm">
4+
<twig:ux:icon name="lucide:save" />
5+
</twig:Button>
6+
</twig:Tooltip:Trigger>
7+
<twig:Tooltip:Content class="pr-1.5">
8+
<div class="flex items-center gap-2">
9+
Save Changes <twig:Kbd>S</twig:Kbd>
10+
</div>
11+
</twig:Tooltip:Content>
12+
</twig:Tooltip>
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{
2+
"$schema": "../../../schema-kit-recipe-v1.json",
3+
"type": "component",
4+
"name": "Tooltip",
5+
"description": "A popup that displays information related to an element when the element receives keyboard focus or the mouse hovers over it.",
6+
"copy-files": {
7+
"assets/": "assets/",
8+
"templates/": "templates/"
9+
},
10+
"dependencies": {
11+
"composer": ["twig/extra-bundle", "twig/html-extra:^3.12.0", "tales-from-a-dev/twig-tailwind-extra:^1.0.0"]
12+
}
13+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
{# @prop id string Unique identifier for the Tooltip #}
2+
{# @prop delayDuration number Delay duration in milliseconds before showing the tooltip, default to `0` #}
3+
{# @block content The default block #}
4+
{%- props id, delayDuration = 0 -%}
5+
{%- set _tooltip_id = id -%}
6+
{%- set _tooltip_trigger_id = id ~ '_trigger' -%}
7+
{%- set _tooltip_wrapper_id = id ~ '_wrapper' -%}
8+
{%- set _tooltip_content_id = id ~ '_content' -%}
9+
{%- set _tooltip_arrow_id = id ~ '_arrow' -%}
10+
{%- set _tooltip_delay_duration = delayDuration -%}
11+
<div
12+
class="{{ 'relative inline-block' ~ attributes.render('class')|tailwind_merge }}"
13+
{{ attributes.defaults({
14+
id: _tooltip_id,
15+
'data-slot': 'tooltip',
16+
'data-controller': 'tooltip',
17+
'data-tooltip-delay-duration-value': _tooltip_delay_duration,
18+
'data-tooltip-wrapper-selector-value': '#' ~ _tooltip_wrapper_id,
19+
'data-tooltip-content-selector-value': '#' ~ _tooltip_content_id,
20+
'data-tooltip-arrow-selector-value': '#' ~ _tooltip_arrow_id,
21+
}) }}
22+
>
23+
{%- block content %}{% endblock -%}
24+
</div>

0 commit comments

Comments
 (0)