Skip to content

Commit 1528684

Browse files
committed
fix: AFCL Tooltip is a native popover placed by CSS anchor positioning with a getBoundingClientRect fallback for older browsers, so core no longer loads flowbite AdminForth/2005
1 parent 08daf97 commit 1528684

8 files changed

Lines changed: 78 additions & 99 deletions

File tree

‎adminforth/design/adoption.md‎

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,8 +57,7 @@ The cards of the landing gallery (`documentation/src/pages/index.tsx`) show eith
5757

5858
## Left in core
5959

60-
- `afcl/Tooltip.vue` uses the flowbite `Tooltip`, `afcl/DatePicker.vue` and `components/CustomDateRangePicker.vue` use `flowbite-datepicker`.
61-
- The Tooltip import is the last place where core loads `flowbite`, and loading it also runs flowbite's auto-init of `data-*` attributes (`data-tooltip-target`, `data-dropdown-toggle`, …) on page load. Apps and plugins may rely on it (foreign-inline-list uses `data-tooltip-target`), so a native Tooltip has to come together with moving those usages to AFCL.
60+
- `afcl/DatePicker.vue` and `components/CustomDateRangePicker.vue` use `flowbite-datepicker`, the last flowbite package of core. `flowbite` itself is gone with its auto-init of `data-*` attributes (`data-tooltip-target`, `data-dropdown-toggle`, …), which ran only once on window `load`, so in the SPA it never reached elements rendered later.
6261
- Palette classes instead of tokens in `views/PageNotFound.vue`, `views/LoginView.vue`, `views/ListView.vue` (action badge), `renderers/URL.vue`, `afcl/BarChart.vue` (axis labels), the HTML in `utils/utils.ts` and a commented line in `components/Sidebar.vue`.
6362

6463
## Targets

‎adminforth/design/system-concepts.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,8 @@ The header of the list page is a container named `header`. Below 896px (`@4xl/he
5656

5757
flowbite is being removed step by step. Don't add new flowbite usages (`flowbite` JS components, `flowbite-datepicker`, `data-*-toggle` attributes). When you touch code which uses it, replace it with a native Vue implementation, usually an AFCL component. Example: the user menu moved from flowbite `Dropdown` to AFCL `DropdownMenu`.
5858

59+
A floating layer is a `popover` element positioned by CSS anchor positioning, like `afcl/Tooltip.vue`. The top layer frees it from z-index and the overflow of parents. Browsers without anchor positioning (Safari before 26, Firefox before 147) get the position from `getBoundingClientRect` under `CSS.supports('anchor-name: --a')`.
60+
5961
## SPA dependencies
6062

6163
Fonts and icons are dependencies of `adminforth/spa`. Apps install the SPA with npm or pnpm, so a new SPA dependency goes to both `spa/package-lock.json` and `spa/pnpm-lock.yaml`.

‎adminforth/pnpm-lock.yaml‎

Lines changed: 0 additions & 15 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎adminforth/spa/package-lock.json‎

Lines changed: 0 additions & 14 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎adminforth/spa/package.json‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,6 @@
5555
"eslint": "^8.57.0",
5656
"eslint-plugin-vue": "^9.23.0",
5757
"flag-icons": "^7.2.3",
58-
"flowbite": "^3.1.2",
5958
"i18n-iso-countries": "^7.12.0",
6059
"npm-run-all2": "^9.0.2",
6160
"portfinder": "^1.0.32",

‎adminforth/spa/pnpm-lock.yaml‎

Lines changed: 0 additions & 15 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 75 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,87 +1,111 @@
11
<template>
2-
<div ref="triggerEl" class="afcl-tooltip inline-flex items-center" @mouseenter="mouseOn" @mouseleave="mouseOff">
2+
<div ref="triggerEl" class="afcl-tooltip inline-flex items-center" :style="{ anchorName }" @mouseenter="show" @mouseleave="hide">
33
<slot></slot>
44
</div>
5-
<teleport to="body" v-if="showTooltip">
5+
<teleport to="body" v-if="isOpen">
66
<div
7+
ref="tooltipEl"
8+
popover="manual"
79
role="tooltip"
8-
class="absolute z-[100] invisible inline-block px-2.5 py-1.5 text-xs font-medium text-lightTooltipText dark:text-darkTooltipText transition-opacity duration-150 bg-lightTooltipBackground rounded-md shadow-popover opacity-0 tooltip dark:bg-darkTooltipBackground dark:shadow-darkPopover"
9-
ref="tooltip"
10+
:data-placement="placement"
11+
:style="{ positionAnchor: anchorName }"
12+
class="afcl-tooltip-popover inline-block px-2.5 py-1.5 text-xs font-medium text-lightTooltipText dark:text-darkTooltipText transition-opacity duration-150 bg-lightTooltipBackground rounded-md shadow-popover dark:bg-darkTooltipBackground dark:shadow-darkPopover"
1013
>
1114
<slot name="tooltip"></slot>
12-
<div class="tooltip-arrow" data-popper-arrow></div>
15+
<div class="afcl-tooltip-arrow" :style="{ left: `${arrowLeft}px` }"></div>
1316
</div>
1417
</teleport>
1518
</template>
1619

1720
<script setup lang="ts">
18-
import { ref, nextTick, type Ref } from 'vue';
19-
import { Tooltip } from 'flowbite';
21+
import { ref, nextTick, onBeforeUnmount, useId } from 'vue';
2022
21-
const triggerEl = ref(null);
22-
const tooltip = ref(null);
23-
const showTooltip = ref(false);
24-
const tp: Ref<Tooltip|null> = ref(null);
23+
const OFFSET = 8;
24+
const SUPPORTS_ANCHOR = CSS.supports('anchor-name: --a');
2525
26-
async function mouseOn() {
27-
showTooltip.value = true;
26+
const anchorName = `--afcl-tooltip-${useId()}`;
27+
const triggerEl = ref<HTMLElement>();
28+
const tooltipEl = ref<HTMLElement>();
29+
const isOpen = ref(false);
30+
const placement = ref<'top' | 'bottom'>('bottom');
31+
const arrowLeft = ref(0);
32+
33+
async function show() {
34+
isOpen.value = true;
2835
await nextTick();
36+
tooltipEl.value!.showPopover();
2937
30-
tp.value?.destroy();
38+
const trigger = triggerEl.value!.getBoundingClientRect();
39+
if (!SUPPORTS_ANCHOR) {
40+
placeByRect(trigger);
41+
}
42+
const tooltip = tooltipEl.value!.getBoundingClientRect();
43+
placement.value = tooltip.top < trigger.top ? 'top' : 'bottom';
44+
arrowLeft.value = trigger.left + trigger.width / 2 - tooltip.left;
3145
32-
tp.value = new Tooltip(
33-
tooltip.value,
34-
triggerEl.value,
35-
{
36-
placement: 'bottom',
37-
triggerType: 'hover',
38-
},
39-
);
46+
document.addEventListener('keydown', onKeydown);
47+
}
4048
41-
tp.value.show();
49+
function hide() {
50+
isOpen.value = false;
51+
document.removeEventListener('keydown', onKeydown);
4252
}
4353
44-
function mouseOff() {
45-
tp.value?.hide();
46-
tp.value?.destroy();
47-
tp.value = null;
48-
showTooltip.value = false;
54+
function onKeydown(e: KeyboardEvent) {
55+
if (e.key === 'Escape') {
56+
hide();
57+
}
4958
}
5059
60+
// Safari before 26 and Firefox before 147 have no CSS anchor positioning
61+
function placeByRect(trigger: DOMRect) {
62+
const el = tooltipEl.value!;
63+
const { width, height } = el.getBoundingClientRect();
64+
const fitsBelow = trigger.bottom + OFFSET + height <= window.innerHeight;
65+
const top = fitsBelow ? trigger.bottom + OFFSET : trigger.top - OFFSET - height;
66+
const left = Math.min(Math.max(trigger.left + (trigger.width - width) / 2, 0), window.innerWidth - width);
67+
el.style.top = `${top}px`;
68+
el.style.left = `${left}px`;
69+
}
70+
71+
onBeforeUnmount(hide);
5172
</script>
5273

5374
<style>
54-
.tooltip .tooltip-arrow,
55-
.tooltip .tooltip-arrow::before {
56-
position: absolute;
57-
width: 8px;
58-
height: 8px;
59-
background: inherit;
60-
}
61-
62-
.tooltip .tooltip-arrow {
63-
visibility: hidden;
75+
/* popover puts the tooltip into the top layer, so no z-index and no clipping by overflow of the parents */
76+
.afcl-tooltip-popover {
77+
inset: auto;
78+
margin: 0;
79+
overflow: visible;
6480
}
6581
66-
.tooltip .tooltip-arrow::before {
67-
visibility: visible;
68-
content: '';
69-
transform: rotate(45deg);
82+
@supports (anchor-name: --a) {
83+
.afcl-tooltip-popover {
84+
position-area: bottom;
85+
position-try-fallbacks: flip-block;
86+
margin-top: 8px;
87+
}
7088
}
7189
72-
.tooltip[data-popper-placement^='top'] > .tooltip-arrow {
73-
bottom: -4px;
90+
@starting-style {
91+
.afcl-tooltip-popover {
92+
opacity: 0;
93+
}
7494
}
7595
76-
.tooltip[data-popper-placement^='bottom'] > .tooltip-arrow {
77-
top: -4px;
96+
.afcl-tooltip-arrow {
97+
position: absolute;
98+
width: 8px;
99+
height: 8px;
100+
background: inherit;
101+
transform: translateX(-50%) rotate(45deg);
78102
}
79103
80-
.tooltip[data-popper-placement^='left'] > .tooltip-arrow {
81-
right: -4px;
104+
.afcl-tooltip-popover[data-placement='top'] > .afcl-tooltip-arrow {
105+
bottom: -4px;
82106
}
83107
84-
.tooltip[data-popper-placement^='right'] > .tooltip-arrow {
85-
left: -4px;
108+
.afcl-tooltip-popover[data-placement='bottom'] > .afcl-tooltip-arrow {
109+
top: -4px;
86110
}
87-
</style>
111+
</style>

‎adminforth/spa/src/views/ListView.vue‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,6 @@
3535
<template v-if="isResourceReady && checkboxes.length" #selection>
3636
<Button
3737
@click="()=>{checkboxes = []}"
38-
data-tooltip-target="tooltip-remove-all"
3938
variant="secondary"
4039
iconOnly
4140
>

0 commit comments

Comments
 (0)