Skip to content

Commit da51177

Browse files
committed
- BsTooltip: prevent displaying tooltip beyond the screen viewport by shifting its coordinates or reversing its placement.
- BsTooltip: automatically adjust arrow positioning. - Update CHANGELOG.md - Bump to version 2.1.4
1 parent a642dd7 commit da51177

File tree

6 files changed

+123
-53
lines changed

6 files changed

+123
-53
lines changed

CHANGELOG.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,18 @@
33
> All notable changes to this project will be documented in this file.
44
55

6+
## v2.1.4
7+
8+
Released: December 28, 2024
9+
10+
### Features & Improvements
11+
12+
- **BsTooltip**:
13+
- Prevent displaying tooltip beyond the screen viewport by shifting its
14+
coordinates or reversing its placement.
15+
- Automatically adjust arrow positioning.
16+
17+
618
## v2.1.3
719

820
Released: December 10, 2024

package.json

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "vue-mdbootstrap",
3-
"version": "2.1.3",
3+
"version": "2.1.4",
44
"description": "Bootstrap5 Material Design Components for Vue.js",
55
"author": {
66
"name": "Ahmad Fajar",
@@ -71,30 +71,30 @@
7171
"dependencies": {
7272
"axios": "^1.7.9",
7373
"body-scroll-lock": "^4.0.0-beta.0",
74-
"fast-xml-parser": "^4.5.0",
74+
"fast-xml-parser": "^4.5.1",
7575
"lodash": "^4.17.21",
7676
"luxon": "^3.5.0",
7777
"resize-observer-polyfill": "^1.5.1",
7878
"vue": "^3.5.13"
7979
},
8080
"devDependencies": {
81-
"@rollup/plugin-node-resolve": "^15.3.0",
81+
"@rollup/plugin-node-resolve": "^16.0.0",
8282
"@rollup/plugin-terser": "^0.4.4",
8383
"@tsconfig/node22": "^22.0.0",
8484
"@types/body-scroll-lock": "^3.1.2",
8585
"@types/lodash": "^4.17.13",
8686
"@types/luxon": "^3.4.2",
87-
"@types/node": "^22.10.1",
87+
"@types/node": "^22.10.2",
8888
"@vue/tsconfig": "^0.7.0",
8989
"bootstrap": "5.2.3",
9090
"clean-css-cli": "^5.6.3",
9191
"npm-run-all": "^4.1.5",
9292
"rimraf": "^6.0.1",
9393
"rollup-plugin-dts": "^6.1.1",
94-
"sass": "^1.82.0",
94+
"sass": "^1.83.0",
9595
"terser": "^5.37.0",
9696
"typescript": "^5.7.2",
97-
"vite": "^6.0.3",
97+
"vite": "^6.0.6",
9898
"vue-router": "^4.5.0"
9999
},
100100
"browserslist": [

scss/banner.scss

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*!
2-
* Vue MDBootstrap v2.1.3
2+
* Vue MDBootstrap v2.1.4
33
* Released under the BSD-3 License.
44
* Copyright Ahmad Fajar (https://ahmadfajar.github.io).
55
*/

src/components/Tooltip/BsTooltip.ts

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -64,12 +64,14 @@ export default defineComponent<TBsTooltip>({
6464
setup(props, { slots }) {
6565
const thisProps = props as Readonly<TTooltipOptionProps>;
6666
const tooltip = ref<Element | null>(null);
67+
const tooltipArrow = ref<Element | null>(null);
6768
const activator = ref<Element | null>(null);
6869
const active = ref<boolean>(false);
6970
const isDisabled = ref<boolean>(thisProps.disabled ?? false);
7071
const isActive = computed(() => active.value || thisProps.show);
71-
const transitionName = computed(() => `${cssPrefix}tooltip-${thisProps.placement}`);
72-
const classNames = computed(() => [`${cssPrefix}tooltip`, transitionName.value]);
72+
const placement = ref<TPlacementPosition>(thisProps.placement ?? 'bottom');
73+
const transition = computed(() => `${cssPrefix}tooltip-${placement.value}`);
74+
const classNames = computed(() => [`${cssPrefix}tooltip`, transition.value]);
7375
const styles = computed(() => ({
7476
width: thisProps.width === 'auto' ? undefined : Helper.cssUnit(thisProps.width),
7577
'max-width': Helper.cssUnit(thisProps.maxWidth),
@@ -78,7 +80,9 @@ export default defineComponent<TBsTooltip>({
7880
[`--${cssPrefix}tooltip-arrow-width`]: thisProps.arrowOff ? 0 : undefined,
7981
}));
8082
const setPosition = () => {
81-
nextTick().then(() => useSetTooltipPosition(activator, tooltip, thisProps.placement));
83+
nextTick().then(() =>
84+
useSetTooltipPosition(activator, tooltip, tooltipArrow, placement)
85+
);
8286
};
8387

8488
let instance: ComponentInternalInstance | null;
@@ -92,12 +96,13 @@ export default defineComponent<TBsTooltip>({
9296
instance = getCurrentInstance();
9397
useAddTooltipListener(
9498
tooltip,
99+
tooltipArrow,
95100
activator,
101+
placement,
96102
active,
97103
isDisabled,
98104
instance,
99-
thisProps.activator,
100-
thisProps.placement
105+
thisProps.activator
101106
);
102107
});
103108
onBeforeUnmount(() => useRemoveTooltipListener(activator));
@@ -107,7 +112,7 @@ export default defineComponent<TBsTooltip>({
107112
h(
108113
Teleport,
109114
{ to: 'body' },
110-
useRenderTransition({ name: transitionName.value }, [
115+
useRenderTransition({ name: transition.value }, [
111116
isActive.value
112117
? withDirectives(
113118
h(
@@ -119,7 +124,10 @@ export default defineComponent<TBsTooltip>({
119124
},
120125
[
121126
!thisProps.arrowOff &&
122-
h('div', { class: 'tooltip-arrow' }),
127+
h('div', {
128+
ref: tooltipArrow,
129+
class: 'tooltip-arrow',
130+
}),
123131
h(
124132
'div',
125133
{

src/components/Tooltip/mixins/tooltipApi.ts

Lines changed: 79 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -16,60 +16,103 @@ const SPACE = 4;
1616
/**
1717
* Calculate Tooltip left offset.
1818
*
19+
* @param placementRef Tooltip placement.
1920
* @param activatorEl Activator Element
2021
* @param tooltipWidth Tooltip element width
21-
* @param placement Tooltip placement.
2222
* @returns Tooltip left offset
2323
*/
2424
function getTooltipLeftPosition(
25+
placementRef: Ref<TPlacementPosition>,
2526
activatorEl: Element,
26-
tooltipWidth: number,
27-
placement?: TPlacementPosition
28-
) {
27+
tooltipWidth: number
28+
): number {
2929
const domRect = activatorEl.getBoundingClientRect();
3030
const parentRect = activatorEl.parentElement?.getBoundingClientRect();
31+
const maxLeft = window.innerWidth - SPACE - tooltipWidth;
32+
const plX = domRect.left - tooltipWidth - SPACE;
33+
const prX = domRect.left + domRect.width + SPACE;
3134

32-
switch (placement) {
35+
switch (placementRef.value) {
3336
case 'left':
34-
return domRect.left - tooltipWidth - SPACE;
37+
if (plX >= SPACE) {
38+
return plX;
39+
} else {
40+
placementRef.value = 'right';
41+
return prX;
42+
}
3543
case 'right':
36-
return domRect.left + domRect.width + SPACE;
44+
if (prX <= maxLeft) {
45+
return prX;
46+
} else {
47+
placementRef.value = 'left';
48+
return plX;
49+
}
3750
case 'top':
3851
case 'bottom':
3952
default:
40-
return (
53+
const tx =
4154
domRect.left +
4255
Math.min(domRect.width / 2, (parentRect?.width ?? domRect.width) / 2) -
43-
tooltipWidth / 2
44-
);
56+
tooltipWidth / 2;
57+
return Math.min(maxLeft, tx);
4558
}
4659
}
4760

4861
/**
4962
* Calculate Tooltip top offset.
5063
*
64+
* @param placementRef Tooltip placement reference.
5165
* @param activatorEl Activator Element
5266
* @param tooltipHeight Tooltip element height
53-
* @param placement Tooltip placement.
5467
* @returns Tooltip top offset
5568
*/
5669
function getTooltipTopPosition(
70+
placementRef: Ref<TPlacementPosition>,
5771
activatorEl: Element,
58-
tooltipHeight: number,
59-
placement?: TPlacementPosition
60-
) {
61-
const rect = activatorEl.getBoundingClientRect();
72+
tooltipHeight: number
73+
): number {
74+
const domRect = activatorEl.getBoundingClientRect();
75+
const ptY = domRect.top - tooltipHeight - SPACE;
76+
const pbY = domRect.top + domRect.height + SPACE;
6277

63-
switch (placement) {
78+
switch (placementRef.value) {
6479
case 'top':
65-
return rect.top - tooltipHeight - SPACE;
80+
if (ptY >= SPACE) {
81+
return ptY;
82+
} else {
83+
placementRef.value = 'bottom';
84+
return pbY;
85+
}
6686
case 'bottom':
67-
return rect.top + rect.height + SPACE;
87+
const maxY = domRect.bottom + tooltipHeight + SPACE;
88+
if (pbY + tooltipHeight <= maxY) {
89+
return pbY;
90+
} else {
91+
placementRef.value = 'top';
92+
return ptY;
93+
}
6894
case 'left':
6995
case 'right':
7096
default:
71-
return rect.top + rect.height / 2 - tooltipHeight / 2;
97+
return domRect.top + domRect.height / 2 - tooltipHeight / 2;
98+
}
99+
}
100+
101+
function getArrowLeftPosition(
102+
activatorEl: Element,
103+
tooltipEl: Element,
104+
placement?: TPlacementPosition
105+
): number {
106+
const domRect = activatorEl.getBoundingClientRect();
107+
const tooltipRect = tooltipEl.getBoundingClientRect();
108+
const domWidth = domRect.width / 2;
109+
const arrow = 13 / 2;
110+
111+
if (placement === 'top' || placement === 'bottom') {
112+
return domRect.left - tooltipRect.left + domWidth - arrow;
72113
}
114+
115+
return 0;
73116
}
74117

75118
/**
@@ -120,10 +163,12 @@ function findActivatorElement(
120163
export function useSetTooltipPosition(
121164
activatorRef: Ref<Element | null>,
122165
tooltipRef: Ref<Element | null>,
123-
placement?: TPlacementPosition
166+
tooltipArrowRef: Ref<Element | null>,
167+
placementRef: Ref<TPlacementPosition>
124168
): void {
125169
const activatorEl = unref(activatorRef) as Element | null;
126170
const tooltipEl = unref(tooltipRef) as HTMLElement | null;
171+
const arrowEl = unref(tooltipArrowRef) as HTMLElement | null;
127172

128173
if (!activatorEl || !tooltipEl) {
129174
return;
@@ -133,20 +178,26 @@ export function useSetTooltipPosition(
133178
const tooltipRect = tooltipEl.getBoundingClientRect();
134179

135180
tooltipEl.style.top =
136-
getTooltipTopPosition(activatorEl, tooltipRect.height, placement) + 'px';
181+
getTooltipTopPosition(placementRef, activatorEl, tooltipRect.height) + 'px';
137182
tooltipEl.style.left =
138-
getTooltipLeftPosition(activatorEl, tooltipRect.width, placement) + 'px';
183+
getTooltipLeftPosition(placementRef, activatorEl, tooltipRect.width) + 'px';
184+
185+
if (arrowEl) {
186+
const px = getArrowLeftPosition(activatorEl, tooltipEl, placementRef.value);
187+
arrowEl.style.left = px > 0 ? `${px}px` : '';
188+
}
139189
}
140190
}
141191

142192
export function useAddTooltipListener(
143193
tooltipRef: Ref<Element | null>,
194+
tooltipArrowRef: Ref<Element | null>,
144195
activatorRef: Ref<Element | null>,
196+
placementRef: Ref<TPlacementPosition>,
145197
active: Ref<boolean>,
146198
disabled: Ref<boolean>,
147199
instance: ComponentInternalInstance | null,
148-
trigger?: string | Element | ComponentPublicInstance,
149-
placement?: TPlacementPosition
200+
trigger?: string | Element | ComponentPublicInstance
150201
) {
151202
if (!instance) {
152203
return;
@@ -158,7 +209,7 @@ export function useAddTooltipListener(
158209
}
159210

160211
window.requestAnimationFrame(() => {
161-
useSetTooltipPosition(activatorRef, tooltipRef, placement);
212+
useSetTooltipPosition(activatorRef, tooltipRef, tooltipArrowRef, placementRef);
162213
instance.emit('update:show', true);
163214
active.value = true;
164215
});
@@ -173,11 +224,10 @@ export function useAddTooltipListener(
173224
activatorRef.value = activatorEl;
174225

175226
if (activatorEl) {
176-
const options = { capture: true, passive: false };
177-
227+
// const options = { capture: true, passive: false };
178228
(activatorEl as IBindingElement).__mouseEvents = {
179-
mouseEnter: EventListener.listen(activatorEl, 'mouseenter', showTooltip, options),
180-
mouseLeave: EventListener.listen(activatorEl, 'mouseleave', hideTooltip, options),
229+
mouseEnter: EventListener.listen(activatorEl, 'mouseenter', showTooltip),
230+
mouseLeave: EventListener.listen(activatorEl, 'mouseleave', hideTooltip),
181231
focus: EventListener.listen(activatorEl, 'focus', showTooltip),
182232
blur: EventListener.listen(activatorEl, 'blur', hideTooltip),
183233
};

src/mixins/DomHelper.ts

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,32 +4,32 @@ export class EventListener {
44
/**
55
* Listen to DOM events during the bubble phase.
66
*
7-
* @param {IHTMLElement} context DOM element to register listener on.
8-
* @param {string} eventType Event type, e.g. 'click' or 'mouseover'.
7+
* @param {IHTMLElement} target DOM element to register listener on.
8+
* @param {string} eventName Event name, e.g. 'click' or 'mouseover'.
99
* @param {function} callback Callback function.
1010
* @param {boolean|AddEventListenerOptions} options Listener options.
1111
* @returns {object} Object with a `remove` method.
1212
*/
1313
static listen(
14-
context: IHTMLElement,
15-
eventType: string,
14+
target: IHTMLElement,
15+
eventName: string,
1616
callback: EventListenerOrEventListenerObject,
1717
options?: boolean | AddEventListenerOptions
1818
): IEventResult | undefined {
19-
if (context.addEventListener) {
20-
context.addEventListener(eventType, callback, options);
19+
if (target.addEventListener) {
20+
target.addEventListener(eventName, callback, options);
2121

2222
return {
2323
remove() {
24-
context.removeEventListener(eventType, callback, options);
24+
target.removeEventListener(eventName, callback, options);
2525
},
2626
};
27-
} else if (context.attachEvent) {
28-
context.attachEvent('on' + eventType, callback);
27+
} else if (target.attachEvent) {
28+
target.attachEvent('on' + eventName, callback);
2929

3030
return {
3131
remove() {
32-
context.detachEvent('on' + eventType, callback);
32+
target.detachEvent('on' + eventName, callback);
3333
},
3434
};
3535
}

0 commit comments

Comments
 (0)