zabi-components 8.0.0 → 8.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +1924 -0
- package/README.md +243 -10
- package/THEME.md +149 -332
- package/THEMING.md +1192 -266
- package/dist/atoms/ActionPanel.svelte +15 -5
- package/dist/atoms/Avatar.svelte +104 -0
- package/dist/atoms/Avatar.svelte.d.ts +34 -0
- package/dist/atoms/Badge.svelte +36 -7
- package/dist/atoms/Badge.svelte.d.ts +2 -2
- package/dist/atoms/Button.svelte +177 -27
- package/dist/atoms/Button.svelte.d.ts +30 -2
- package/dist/atoms/Card.svelte +4 -4
- package/dist/atoms/CardHeader.svelte +16 -4
- package/dist/atoms/Checkbox.svelte +48 -21
- package/dist/atoms/Checkbox.svelte.d.ts +4 -2
- package/dist/atoms/CodeBlock.svelte +20 -2
- package/dist/atoms/CodeBlock.svelte.d.ts +4 -0
- package/dist/atoms/ColorPicker.svelte +259 -49
- package/dist/atoms/ColorPicker.svelte.d.ts +3 -0
- package/dist/atoms/Container.svelte +16 -8
- package/dist/atoms/DateField.svelte +107 -0
- package/dist/atoms/DateField.svelte.d.ts +53 -0
- package/dist/atoms/FieldMessages.svelte +69 -0
- package/dist/atoms/FieldMessages.svelte.d.ts +23 -0
- package/dist/atoms/FloatingActionButton.svelte +174 -0
- package/dist/atoms/FloatingActionButton.svelte.d.ts +51 -0
- package/dist/atoms/Heading.svelte +37 -5
- package/dist/atoms/Heading.svelte.d.ts +7 -0
- package/dist/atoms/IconButton.svelte +220 -24
- package/dist/atoms/IconButton.svelte.d.ts +49 -4
- package/dist/atoms/Input.svelte +253 -52
- package/dist/atoms/Input.svelte.d.ts +32 -2
- package/dist/atoms/List.svelte +4 -1
- package/dist/atoms/ListItem.svelte +44 -11
- package/dist/atoms/ListItem.svelte.d.ts +5 -0
- package/dist/atoms/Progress.svelte +27 -8
- package/dist/atoms/Progress.svelte.d.ts +4 -0
- package/dist/atoms/Radio.svelte +19 -3
- package/dist/atoms/Radio.svelte.d.ts +4 -2
- package/dist/atoms/Rating.svelte +494 -0
- package/dist/atoms/Rating.svelte.d.ts +56 -0
- package/dist/atoms/Select.svelte +433 -91
- package/dist/atoms/Select.svelte.d.ts +49 -3
- package/dist/atoms/SelectionControl.svelte +94 -17
- package/dist/atoms/SelectionControl.svelte.d.ts +4 -3
- package/dist/atoms/Skeleton.svelte +1 -1
- package/dist/atoms/Slider.svelte +309 -0
- package/dist/atoms/Slider.svelte.d.ts +40 -0
- package/dist/atoms/Spinner.svelte +57 -0
- package/dist/atoms/Spinner.svelte.d.ts +19 -0
- package/dist/atoms/Table.svelte +151 -6
- package/dist/atoms/Table.svelte.d.ts +15 -3
- package/dist/atoms/TemporalField.svelte +140 -0
- package/dist/atoms/TemporalField.svelte.d.ts +32 -0
- package/dist/atoms/Text.svelte +23 -11
- package/dist/atoms/Text.svelte.d.ts +6 -3
- package/dist/atoms/Textarea.svelte +55 -44
- package/dist/atoms/Textarea.svelte.d.ts +14 -2
- package/dist/atoms/ThemeToggle.svelte +223 -44
- package/dist/atoms/ThemeToggle.svelte.d.ts +26 -3
- package/dist/atoms/TimeField.svelte +108 -0
- package/dist/atoms/TimeField.svelte.d.ts +54 -0
- package/dist/atoms/Toast.svelte +17 -4
- package/dist/atoms/Toast.svelte.d.ts +2 -0
- package/dist/atoms/Toggle.svelte +41 -8
- package/dist/atoms/Toggle.svelte.d.ts +11 -2
- package/dist/atoms/Tooltip.svelte +508 -23
- package/dist/atoms/Tooltip.svelte.d.ts +11 -0
- package/dist/atoms/ZabiStringsProvider.svelte +45 -0
- package/dist/atoms/ZabiStringsProvider.svelte.d.ts +27 -0
- package/dist/atoms/index.d.ts +17 -0
- package/dist/atoms/index.js +12 -0
- package/dist/atoms/selection-control.styles.d.ts +36 -5
- package/dist/atoms/selection-control.styles.js +40 -9
- package/dist/components/atoms/index.d.ts +17 -0
- package/dist/components/atoms/index.d.ts.map +1 -1
- package/dist/components/atoms/selection-control.styles.d.ts +36 -5
- package/dist/components/atoms/selection-control.styles.d.ts.map +1 -1
- package/dist/components/index.d.ts +80 -2
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/molecules/index.d.ts +46 -0
- package/dist/components/molecules/index.d.ts.map +1 -1
- package/dist/components/molecules/toast-store.d.ts +53 -7
- package/dist/components/molecules/toast-store.d.ts.map +1 -1
- package/dist/components/organisms/index.d.ts +5 -0
- package/dist/components/organisms/index.d.ts.map +1 -1
- package/dist/components/types/variants.d.ts +15 -1
- package/dist/components/types/variants.d.ts.map +1 -1
- package/dist/components/util/app-shell.d.ts +28 -0
- package/dist/components/util/app-shell.d.ts.map +1 -0
- package/dist/components/util/avatar.d.ts +36 -0
- package/dist/components/util/avatar.d.ts.map +1 -0
- package/dist/components/util/bottom-sheet.d.ts +30 -0
- package/dist/components/util/bottom-sheet.d.ts.map +1 -0
- package/dist/components/util/bottom-tab-bar.d.ts +35 -0
- package/dist/components/util/bottom-tab-bar.d.ts.map +1 -0
- package/dist/components/util/calendar.d.ts +56 -0
- package/dist/components/util/calendar.d.ts.map +1 -0
- package/dist/components/util/collapsible.d.ts +58 -0
- package/dist/components/util/collapsible.d.ts.map +1 -0
- package/dist/components/util/confirm-dialog.d.ts +12 -0
- package/dist/components/util/confirm-dialog.d.ts.map +1 -0
- package/dist/components/util/date.d.ts +96 -0
- package/dist/components/util/date.d.ts.map +1 -0
- package/dist/components/util/drawer.d.ts +13 -0
- package/dist/components/util/drawer.d.ts.map +1 -0
- package/dist/components/util/dropdown.d.ts +47 -0
- package/dist/components/util/dropdown.d.ts.map +1 -0
- package/dist/components/util/field.d.ts +39 -0
- package/dist/components/util/field.d.ts.map +1 -0
- package/dist/components/util/fit-in-viewport.d.ts +201 -0
- package/dist/components/util/fit-in-viewport.d.ts.map +1 -0
- package/dist/components/util/focus-utils.d.ts +75 -2
- package/dist/components/util/focus-utils.d.ts.map +1 -1
- package/dist/components/util/hydration.d.ts +45 -0
- package/dist/components/util/hydration.d.ts.map +1 -0
- package/dist/components/util/keyboard-inset.d.ts +18 -0
- package/dist/components/util/keyboard-inset.d.ts.map +1 -0
- package/dist/components/util/media-grid.d.ts +51 -0
- package/dist/components/util/media-grid.d.ts.map +1 -0
- package/dist/components/util/media.d.ts +6 -0
- package/dist/components/util/media.d.ts.map +1 -0
- package/dist/components/util/overlay-chrome.d.ts +53 -0
- package/dist/components/util/overlay-chrome.d.ts.map +1 -0
- package/dist/components/util/overlay.d.ts +72 -0
- package/dist/components/util/overlay.d.ts.map +1 -0
- package/dist/components/util/photo.d.ts +215 -0
- package/dist/components/util/photo.d.ts.map +1 -0
- package/dist/components/util/portal.d.ts +23 -0
- package/dist/components/util/portal.d.ts.map +1 -0
- package/dist/components/util/pull-to-refresh.d.ts +27 -0
- package/dist/components/util/pull-to-refresh.d.ts.map +1 -0
- package/dist/components/util/radio-keys.d.ts +25 -0
- package/dist/components/util/radio-keys.d.ts.map +1 -0
- package/dist/components/util/rating.d.ts +23 -0
- package/dist/components/util/rating.d.ts.map +1 -0
- package/dist/components/util/ready-made-strings.d.ts +77 -0
- package/dist/components/util/ready-made-strings.d.ts.map +1 -0
- package/dist/components/util/scroll-padding.d.ts +20 -0
- package/dist/components/util/scroll-padding.d.ts.map +1 -0
- package/dist/components/util/segmented-control.d.ts +16 -0
- package/dist/components/util/segmented-control.d.ts.map +1 -0
- package/dist/components/util/select.d.ts +41 -0
- package/dist/components/util/select.d.ts.map +1 -0
- package/dist/components/util/sheet-drag.d.ts +60 -0
- package/dist/components/util/sheet-drag.d.ts.map +1 -0
- package/dist/components/util/sidebar.d.ts +63 -0
- package/dist/components/util/sidebar.d.ts.map +1 -0
- package/dist/components/util/sortable-list.d.ts +65 -0
- package/dist/components/util/sortable-list.d.ts.map +1 -0
- package/dist/components/util/stepper.d.ts +40 -0
- package/dist/components/util/stepper.d.ts.map +1 -0
- package/dist/components/util/swipeable-list-item.d.ts +36 -0
- package/dist/components/util/swipeable-list-item.d.ts.map +1 -0
- package/dist/components/util/theme-mode.d.ts +80 -0
- package/dist/components/util/theme-mode.d.ts.map +1 -0
- package/dist/components/util/toaster.d.ts +93 -0
- package/dist/components/util/toaster.d.ts.map +1 -0
- package/dist/components/util/tooltip.d.ts +10 -0
- package/dist/components/util/tooltip.d.ts.map +1 -0
- package/dist/components/util/top-navbar.d.ts +11 -0
- package/dist/components/util/top-navbar.d.ts.map +1 -0
- package/dist/components/util/touch-target.d.ts +31 -0
- package/dist/components/util/touch-target.d.ts.map +1 -0
- package/dist/components/util/zabi-strings.d.ts +133 -0
- package/dist/components/util/zabi-strings.d.ts.map +1 -0
- package/dist/create-theme/cli.js +169 -0
- package/dist/create-theme/index.d.ts +176 -0
- package/dist/create-theme/index.js +817 -0
- package/dist/create-theme/lib/ramp-math.js +128 -0
- package/dist/create-theme/lib/resolve.js +141 -0
- package/dist/create-theme/theme-data.js +1116 -0
- package/dist/index.d.ts +80 -2
- package/dist/index.js +57 -2
- package/dist/molecules/Alert.svelte +33 -5
- package/dist/molecules/Alert.svelte.d.ts +2 -0
- package/dist/molecules/AppBar.svelte +415 -0
- package/dist/molecules/AppBar.svelte.d.ts +69 -0
- package/dist/molecules/AvatarGroup.svelte +116 -0
- package/dist/molecules/AvatarGroup.svelte.d.ts +40 -0
- package/dist/molecules/BottomSheet.svelte +540 -0
- package/dist/molecules/BottomSheet.svelte.d.ts +93 -0
- package/dist/molecules/BottomTabBar.svelte +297 -0
- package/dist/molecules/BottomTabBar.svelte.d.ts +64 -0
- package/dist/molecules/Calendar.svelte +597 -0
- package/dist/molecules/Calendar.svelte.d.ts +72 -0
- package/dist/molecules/Collapsible.svelte +243 -0
- package/dist/molecules/Collapsible.svelte.d.ts +44 -0
- package/dist/molecules/CollapsibleGroup.svelte +126 -0
- package/dist/molecules/CollapsibleGroup.svelte.d.ts +14 -0
- package/dist/molecules/ComponentDemo.svelte +16 -4
- package/dist/molecules/ComponentDemo.svelte.d.ts +3 -0
- package/dist/molecules/ConfirmDialog.svelte +253 -0
- package/dist/molecules/ConfirmDialog.svelte.d.ts +55 -0
- package/dist/molecules/ContactForm.svelte +34 -17
- package/dist/molecules/ContactForm.svelte.d.ts +8 -0
- package/dist/molecules/Drawer.svelte +394 -0
- package/dist/molecules/Drawer.svelte.d.ts +55 -0
- package/dist/molecules/Dropdown.svelte +509 -48
- package/dist/molecules/Dropdown.svelte.d.ts +45 -6
- package/dist/molecules/DropdownItem.svelte +122 -0
- package/dist/molecules/DropdownItem.svelte.d.ts +28 -0
- package/dist/molecules/EmptyState.svelte +55 -10
- package/dist/molecules/EmptyState.svelte.d.ts +8 -0
- package/dist/molecules/FormField.svelte +9 -1
- package/dist/molecules/FormField.svelte.d.ts +2 -0
- package/dist/molecules/Header.svelte +1 -1
- package/dist/molecules/ImageUpload.svelte +383 -70
- package/dist/molecules/ImageUpload.svelte.d.ts +47 -2
- package/dist/molecules/MediaGrid.svelte +527 -0
- package/dist/molecules/MediaGrid.svelte.d.ts +77 -0
- package/dist/molecules/Modal.svelte +288 -72
- package/dist/molecules/Modal.svelte.d.ts +51 -2
- package/dist/molecules/NavigationMenuContent.svelte +58 -1
- package/dist/molecules/NavigationMenuItem.svelte +0 -1
- package/dist/molecules/NavigationMenuLink.svelte +1 -1
- package/dist/molecules/NavigationMenuList.svelte +10 -3
- package/dist/molecules/NavigationMenuTrigger.svelte +2 -2
- package/dist/molecules/Page.svelte +32 -2
- package/dist/molecules/Page.svelte.d.ts +11 -0
- package/dist/molecules/PhotoGrid.svelte +433 -0
- package/dist/molecules/PhotoGrid.svelte.d.ts +62 -0
- package/dist/molecules/PhotoViewer.svelte +1116 -0
- package/dist/molecules/PhotoViewer.svelte.d.ts +56 -0
- package/dist/molecules/PropsTable.svelte +22 -10
- package/dist/molecules/PropsTable.svelte.d.ts +3 -0
- package/dist/molecules/PullToRefresh.svelte +263 -0
- package/dist/molecules/PullToRefresh.svelte.d.ts +52 -0
- package/dist/molecules/RadioGroup.svelte +52 -14
- package/dist/molecules/RadioGroup.svelte.d.ts +14 -3
- package/dist/molecules/Section.svelte +1 -1
- package/dist/molecules/SegmentedControl.svelte +323 -0
- package/dist/molecules/SegmentedControl.svelte.d.ts +34 -0
- package/dist/molecules/SidebarBrandHeader.svelte +11 -1
- package/dist/molecules/SidebarBrandHeader.svelte.d.ts +3 -0
- package/dist/molecules/SidebarFooter.svelte +29 -6
- package/dist/molecules/SidebarFooter.svelte.d.ts +3 -0
- package/dist/molecules/SlideUp.svelte +220 -61
- package/dist/molecules/SlideUp.svelte.d.ts +26 -0
- package/dist/molecules/SortableList.svelte +632 -0
- package/dist/molecules/SortableList.svelte.d.ts +69 -0
- package/dist/molecules/Stepper.svelte +548 -0
- package/dist/molecules/Stepper.svelte.d.ts +39 -0
- package/dist/molecules/StickyActionBar.svelte +191 -0
- package/dist/molecules/StickyActionBar.svelte.d.ts +65 -0
- package/dist/molecules/SwipeableListItem.svelte +334 -0
- package/dist/molecules/SwipeableListItem.svelte.d.ts +54 -0
- package/dist/molecules/Tabs.svelte +233 -9
- package/dist/molecules/Tabs.svelte.d.ts +7 -0
- package/dist/molecules/Toaster.svelte +350 -5
- package/dist/molecules/Toaster.svelte.d.ts +95 -2
- package/dist/molecules/ToasterToast.svelte +253 -67
- package/dist/molecules/ToasterToast.svelte.d.ts +9 -0
- package/dist/molecules/UnsavedChangesBar.svelte +212 -0
- package/dist/molecules/UnsavedChangesBar.svelte.d.ts +43 -0
- package/dist/molecules/index.d.ts +46 -0
- package/dist/molecules/index.js +27 -0
- package/dist/molecules/toast-store.d.ts +53 -7
- package/dist/molecules/toast-store.js +21 -0
- package/dist/organisms/AppShell.svelte +196 -0
- package/dist/organisms/AppShell.svelte.d.ts +61 -0
- package/dist/organisms/SidebarAccountPanel.svelte +107 -12
- package/dist/organisms/SidebarAccountPanel.svelte.d.ts +19 -0
- package/dist/organisms/SidebarNavigation.svelte +96 -15
- package/dist/organisms/SidebarNavigation.svelte.d.ts +29 -1
- package/dist/organisms/SidebarPanel.svelte +31 -10
- package/dist/organisms/SidebarShell.svelte +162 -11
- package/dist/organisms/SidebarShell.svelte.d.ts +56 -2
- package/dist/organisms/TopNavbar.svelte +256 -31
- package/dist/organisms/TopNavbar.svelte.d.ts +26 -0
- package/dist/organisms/index.d.ts +5 -0
- package/dist/organisms/index.js +3 -0
- package/dist/types/components.d.ts +4 -0
- package/dist/types/components.d.ts.map +1 -1
- package/dist/types/index.d.ts +605 -25
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/variants.ts +17 -1
- package/dist/util/app-shell.d.ts +27 -0
- package/dist/util/app-shell.js +72 -0
- package/dist/util/avatar.d.ts +35 -0
- package/dist/util/avatar.js +52 -0
- package/dist/util/bottom-sheet.d.ts +29 -0
- package/dist/util/bottom-sheet.js +43 -0
- package/dist/util/bottom-tab-bar.d.ts +34 -0
- package/dist/util/bottom-tab-bar.js +54 -0
- package/dist/util/calendar.d.ts +55 -0
- package/dist/util/calendar.js +74 -0
- package/dist/util/collapsible.d.ts +57 -0
- package/dist/util/collapsible.js +23 -0
- package/dist/util/confirm-dialog.d.ts +11 -0
- package/dist/util/confirm-dialog.js +2 -0
- package/dist/util/date.d.ts +95 -0
- package/dist/util/date.js +237 -0
- package/dist/util/drawer.d.ts +12 -0
- package/dist/util/drawer.js +7 -0
- package/dist/util/dropdown.d.ts +46 -0
- package/dist/util/dropdown.js +21 -0
- package/dist/util/field.d.ts +38 -0
- package/dist/util/field.js +38 -0
- package/dist/util/fit-in-viewport.d.ts +200 -0
- package/dist/util/fit-in-viewport.js +399 -0
- package/dist/util/focus-utils.d.ts +75 -2
- package/dist/util/focus-utils.js +182 -9
- package/dist/util/hydration.d.ts +44 -0
- package/dist/util/hydration.js +58 -0
- package/dist/util/keyboard-inset.d.ts +17 -0
- package/dist/util/keyboard-inset.js +53 -0
- package/dist/util/media-grid.d.ts +50 -0
- package/dist/util/media-grid.js +81 -0
- package/dist/util/media.d.ts +5 -0
- package/dist/util/media.js +10 -0
- package/dist/util/overlay-chrome.d.ts +52 -0
- package/dist/util/overlay-chrome.js +56 -0
- package/dist/util/overlay.d.ts +71 -0
- package/dist/util/overlay.js +240 -0
- package/dist/util/photo.d.ts +214 -0
- package/dist/util/photo.js +206 -0
- package/dist/util/portal.d.ts +22 -0
- package/dist/util/portal.js +50 -0
- package/dist/util/pull-to-refresh.d.ts +26 -0
- package/dist/util/pull-to-refresh.js +30 -0
- package/dist/util/radio-keys.d.ts +24 -0
- package/dist/util/radio-keys.js +49 -0
- package/dist/util/rating.d.ts +22 -0
- package/dist/util/rating.js +26 -0
- package/dist/util/ready-made-strings.d.ts +76 -0
- package/dist/util/ready-made-strings.js +67 -0
- package/dist/util/scroll-padding.d.ts +19 -0
- package/dist/util/scroll-padding.js +44 -0
- package/dist/util/segmented-control.d.ts +15 -0
- package/dist/util/segmented-control.js +1 -0
- package/dist/util/select.d.ts +40 -0
- package/dist/util/select.js +63 -0
- package/dist/util/sheet-drag.d.ts +59 -0
- package/dist/util/sheet-drag.js +241 -0
- package/dist/util/sidebar.d.ts +62 -0
- package/dist/util/sidebar.js +38 -0
- package/dist/util/sortable-list.d.ts +64 -0
- package/dist/util/sortable-list.js +53 -0
- package/dist/util/stepper.d.ts +39 -0
- package/dist/util/stepper.js +38 -0
- package/dist/util/swipeable-list-item.d.ts +35 -0
- package/dist/util/swipeable-list-item.js +32 -0
- package/dist/util/theme-mode.d.ts +79 -0
- package/dist/util/theme-mode.js +121 -0
- package/dist/util/toaster.d.ts +92 -0
- package/dist/util/toaster.js +75 -0
- package/dist/util/tooltip.d.ts +9 -0
- package/dist/util/tooltip.js +18 -0
- package/dist/util/top-navbar.d.ts +10 -0
- package/dist/util/top-navbar.js +5 -0
- package/dist/util/touch-target.d.ts +30 -0
- package/dist/util/touch-target.js +30 -0
- package/dist/util/zabi-strings.d.ts +132 -0
- package/dist/util/zabi-strings.js +105 -0
- package/dist/zabi-components-colors.css +5 -2
- package/dist/zabi-components-theme-dark-only.css +543 -241
- package/dist/zabi-components-theme-dark.css +543 -241
- package/dist/zabi-components-theme-only.css +1584 -367
- package/dist/zabi-components-theme.css +1584 -367
- package/dist/zabi-components.css +3885 -518
- package/docs/lucide-icons.md +40 -17
- package/docs/theme-imports.md +81 -11
- package/package.json +35 -8
|
@@ -1,8 +1,15 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import type { Snippet } from "svelte";
|
|
3
3
|
import type { HTMLAttributes } from "svelte/elements";
|
|
4
|
-
import { onDestroy } from "svelte";
|
|
4
|
+
import { onDestroy, untrack } from "svelte";
|
|
5
5
|
import { generateId } from "../util/ssr-safe.js";
|
|
6
|
+
import { claimOpenTooltip, releaseOpenTooltip } from "../util/tooltip.js";
|
|
7
|
+
import {
|
|
8
|
+
insideTriangle,
|
|
9
|
+
pickSide,
|
|
10
|
+
shiftIntoViewport,
|
|
11
|
+
type FloatingSide,
|
|
12
|
+
} from "../util/fit-in-viewport.js";
|
|
6
13
|
|
|
7
14
|
import { cn } from "../util/cn.js";
|
|
8
15
|
type Props = Omit<HTMLAttributes<HTMLDivElement>, "class"> & {
|
|
@@ -25,6 +32,17 @@
|
|
|
25
32
|
* at the rail edge and left only the arrow showing.
|
|
26
33
|
*/
|
|
27
34
|
fixed?: boolean;
|
|
35
|
+
/**
|
|
36
|
+
* On a touch screen a tap on the trigger opens the tooltip, and the
|
|
37
|
+
* trigger still does what it does. It stays until a second tap, a tap
|
|
38
|
+
* elsewhere, Escape, scrolling or focus leaving: the tap also puts
|
|
39
|
+
* focus on the trigger, and a tooltip that went by itself while its
|
|
40
|
+
* trigger still had focus would be gone before it was read. Give a
|
|
41
|
+
* number of milliseconds to have it close by itself after that long
|
|
42
|
+
* as well, for a button whose tooltip is in the way of what the tap
|
|
43
|
+
* did. A mouse and a keyboard are not affected.
|
|
44
|
+
*/
|
|
45
|
+
touchDuration?: number;
|
|
28
46
|
class?: string;
|
|
29
47
|
children?: Snippet;
|
|
30
48
|
};
|
|
@@ -32,6 +50,14 @@
|
|
|
32
50
|
/** First matching focusable inside the slot receives `aria-describedby` (wrapper div is skipped). */
|
|
33
51
|
const FOCUSABLE_SELECTOR =
|
|
34
52
|
'button:not([disabled]), a[href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])';
|
|
53
|
+
/**
|
|
54
|
+
* With nothing focusable in the slot, the first control of any kind is
|
|
55
|
+
* described instead: a disabled button is where a tooltip most often says
|
|
56
|
+
* something that matters (why it is disabled). It cannot take focus, so
|
|
57
|
+
* the keyboard never opens that tooltip: `aria-disabled="true"` on a
|
|
58
|
+
* button that stays focusable is the pattern that reaches everyone.
|
|
59
|
+
*/
|
|
60
|
+
const CONTROL_SELECTOR = 'button, a[href], input, select, textarea, [role="button"], [tabindex]';
|
|
35
61
|
|
|
36
62
|
let {
|
|
37
63
|
content = "",
|
|
@@ -40,6 +66,7 @@
|
|
|
40
66
|
disabled = false,
|
|
41
67
|
block = false,
|
|
42
68
|
fixed = false,
|
|
69
|
+
touchDuration = 0,
|
|
43
70
|
class: className = "",
|
|
44
71
|
children,
|
|
45
72
|
...restProps
|
|
@@ -49,19 +76,125 @@
|
|
|
49
76
|
|
|
50
77
|
/** Gap must match --tooltip-gap's default (0.5rem). */
|
|
51
78
|
const FIXED_GAP = 8;
|
|
79
|
+
/** How close to the edge of the viewport the bubble may come, in px. */
|
|
80
|
+
const EDGE_MARGIN = 8;
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* The side the bubble is on when that is not `placement`: there was no
|
|
84
|
+
* room there, or the screen is too narrow to put anything beside the
|
|
85
|
+
* trigger. Decided each time the tooltip opens.
|
|
86
|
+
*/
|
|
87
|
+
let flippedTo = $state<FloatingSide | null>(null);
|
|
88
|
+
const side = $derived(flippedTo ?? placement);
|
|
89
|
+
/** How far the bubble is moved along its side to stay on screen, in px. */
|
|
90
|
+
let shift = $state({ x: 0, y: 0 });
|
|
91
|
+
/** The arrow moves back by as much, so it still points at the trigger. */
|
|
92
|
+
let arrowShift = $state({ x: 0, y: 0 });
|
|
52
93
|
|
|
53
94
|
function positionFixed(): void {
|
|
54
95
|
if (!fixed || !triggerElement) return;
|
|
55
96
|
const r = triggerElement.getBoundingClientRect();
|
|
97
|
+
// The styles centre the bubble on the cross axis only. Above and to
|
|
98
|
+
// the left it also has to clear the trigger by its own size, or it
|
|
99
|
+
// lies over it: `left` and `top` are where its near corner goes.
|
|
100
|
+
const width = bubbleElement?.offsetWidth ?? 0;
|
|
101
|
+
const height = bubbleElement?.offsetHeight ?? 0;
|
|
56
102
|
const coords = {
|
|
57
|
-
top: [r.left + r.width / 2, r.top - FIXED_GAP],
|
|
103
|
+
top: [r.left + r.width / 2, r.top - FIXED_GAP - height],
|
|
58
104
|
bottom: [r.left + r.width / 2, r.bottom + FIXED_GAP],
|
|
59
|
-
left: [r.left - FIXED_GAP, r.top + r.height / 2],
|
|
105
|
+
left: [r.left - FIXED_GAP - width, r.top + r.height / 2],
|
|
60
106
|
right: [r.right + FIXED_GAP, r.top + r.height / 2],
|
|
61
|
-
}[
|
|
107
|
+
}[side];
|
|
62
108
|
fixedStyle = `left:${coords[0]}px;top:${coords[1]}px;`;
|
|
63
109
|
}
|
|
64
110
|
|
|
111
|
+
/**
|
|
112
|
+
* Picks the side and the shift that keep the bubble in the viewport. It is
|
|
113
|
+
* worked out from the trigger's rectangle and the bubble's size, not from
|
|
114
|
+
* where the bubble is drawn: that is mid-transition when this runs.
|
|
115
|
+
*/
|
|
116
|
+
function fitToViewport(): void {
|
|
117
|
+
const trigger = triggerElement;
|
|
118
|
+
const bubble = bubbleElement;
|
|
119
|
+
if (!trigger || !bubble) return;
|
|
120
|
+
const width = bubble.offsetWidth;
|
|
121
|
+
const height = bubble.offsetHeight;
|
|
122
|
+
// No layout (a test DOM, a hidden subtree): nothing to fit.
|
|
123
|
+
if (!width || !height) return;
|
|
124
|
+
const root = document.documentElement;
|
|
125
|
+
const viewport = {
|
|
126
|
+
width: root.clientWidth || window.innerWidth,
|
|
127
|
+
height: root.clientHeight || window.innerHeight,
|
|
128
|
+
};
|
|
129
|
+
const r = trigger.getBoundingClientRect();
|
|
130
|
+
// Below 640px nothing fits beside a trigger: above it, or below.
|
|
131
|
+
const narrow =
|
|
132
|
+
typeof window.matchMedia === "function" &&
|
|
133
|
+
window.matchMedia("(max-width: 640px)").matches;
|
|
134
|
+
const preferred: FloatingSide =
|
|
135
|
+
narrow && (placement === "left" || placement === "right") ? "top" : placement;
|
|
136
|
+
// `left` and `right` are the start and the end side in the styles
|
|
137
|
+
// below, so in a right-to-left page they are the other way round on
|
|
138
|
+
// screen. The fixed strategy places by `left` and `top` and is not.
|
|
139
|
+
const mirrored = !fixed && getComputedStyle(trigger).direction === "rtl";
|
|
140
|
+
isMirrored = mirrored;
|
|
141
|
+
const onScreen = (name: FloatingSide): FloatingSide =>
|
|
142
|
+
mirrored && name === "left" ? "right" : mirrored && name === "right" ? "left" : name;
|
|
143
|
+
const next = pickSide(onScreen(preferred), r, { width, height }, viewport, {
|
|
144
|
+
gap: FIXED_GAP,
|
|
145
|
+
margin: EDGE_MARGIN,
|
|
146
|
+
});
|
|
147
|
+
// Back to the name the styles know it by.
|
|
148
|
+
const named = onScreen(next);
|
|
149
|
+
flippedTo = named === placement ? null : named;
|
|
150
|
+
|
|
151
|
+
const centreX = r.left + r.width / 2;
|
|
152
|
+
const centreY = r.top + r.height / 2;
|
|
153
|
+
const [left, top] = {
|
|
154
|
+
top: [centreX - width / 2, r.top - FIXED_GAP - height],
|
|
155
|
+
bottom: [centreX - width / 2, r.bottom + FIXED_GAP],
|
|
156
|
+
left: [r.left - FIXED_GAP - width, centreY - height / 2],
|
|
157
|
+
right: [r.right + FIXED_GAP, centreY - height / 2],
|
|
158
|
+
}[next];
|
|
159
|
+
const move = shiftIntoViewport(
|
|
160
|
+
{ left, top, right: left + width, bottom: top + height },
|
|
161
|
+
viewport,
|
|
162
|
+
EDGE_MARGIN,
|
|
163
|
+
);
|
|
164
|
+
// Rounded, so a sub-pixel difference does not blur the text.
|
|
165
|
+
const x = Math.round(move.x);
|
|
166
|
+
const y = Math.round(move.y);
|
|
167
|
+
if (x !== shift.x || y !== shift.y) shift = { x, y };
|
|
168
|
+
// The arrow stays on the bubble: 12px clear of its corners.
|
|
169
|
+
const reach = (extent: number) => Math.max(0, extent / 2 - 12);
|
|
170
|
+
const vertical = next === "top" || next === "bottom";
|
|
171
|
+
const arrowX = vertical ? -Math.max(-reach(width), Math.min(reach(width), x)) : 0;
|
|
172
|
+
const arrowY = vertical ? 0 : -Math.max(-reach(height), Math.min(reach(height), y));
|
|
173
|
+
if (arrowX !== arrowShift.x || arrowY !== arrowShift.y) {
|
|
174
|
+
arrowShift = { x: arrowX, y: arrowY };
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
$effect(() => {
|
|
179
|
+
if (!isVisible) return;
|
|
180
|
+
untrack(fitToViewport);
|
|
181
|
+
const onMove = () => fitToViewport();
|
|
182
|
+
window.addEventListener("scroll", onMove, true);
|
|
183
|
+
window.addEventListener("resize", onMove);
|
|
184
|
+
return () => {
|
|
185
|
+
window.removeEventListener("scroll", onMove, true);
|
|
186
|
+
window.removeEventListener("resize", onMove);
|
|
187
|
+
};
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
const bubbleStyle = $derived(
|
|
191
|
+
(fixed ? fixedStyle : "") +
|
|
192
|
+
(shift.x || shift.y ? `translate:${shift.x}px ${shift.y}px;` : "") +
|
|
193
|
+
(arrowShift.x || arrowShift.y
|
|
194
|
+
? `--tooltip-arrow-shift:${arrowShift.x}px ${arrowShift.y}px;`
|
|
195
|
+
: ""),
|
|
196
|
+
);
|
|
197
|
+
|
|
65
198
|
$effect(() => {
|
|
66
199
|
if (!fixed || !isVisible) return;
|
|
67
200
|
positionFixed();
|
|
@@ -78,10 +211,85 @@
|
|
|
78
211
|
const triggerId = generateId("tooltip-trigger");
|
|
79
212
|
const tooltipId = generateId("tooltip");
|
|
80
213
|
let isVisible = $state(false);
|
|
214
|
+
let containerElement: HTMLElement | null = $state(null);
|
|
81
215
|
let triggerElement: HTMLElement | null = $state(null);
|
|
216
|
+
let bubbleElement: HTMLElement | null = $state(null);
|
|
82
217
|
let showDelayTimeout: ReturnType<typeof setTimeout> | null = null;
|
|
83
218
|
let hideBlurTimeout: ReturnType<typeof setTimeout> | null = null;
|
|
84
219
|
|
|
220
|
+
/**
|
|
221
|
+
* A hidden bubble still takes part in layout, and one that reaches past
|
|
222
|
+
* the right edge of a 320px screen makes the whole page scroll sideways.
|
|
223
|
+
* While it is closed it is parked in the corner of the viewport, where it
|
|
224
|
+
* widens nothing; it is parked once it has faded out, not before.
|
|
225
|
+
*/
|
|
226
|
+
let parked = $state(true);
|
|
227
|
+
let parkTimeout: ReturnType<typeof setTimeout> | null = null;
|
|
228
|
+
/** Longer than the 200ms fade. */
|
|
229
|
+
const PARK_AFTER = 250;
|
|
230
|
+
|
|
231
|
+
/** The pointer that last touched the trigger: "mouse", "touch" or "pen". */
|
|
232
|
+
let lastPointerType = "";
|
|
233
|
+
/** When a finger last went down on, or came up from, the trigger. */
|
|
234
|
+
let touchedAt = 0;
|
|
235
|
+
/** Open because of a tap: closes on a tap elsewhere, on scroll, and after `touchDuration`. */
|
|
236
|
+
let openedByTouch = $state(false);
|
|
237
|
+
let touchHideTimeout: ReturnType<typeof setTimeout> | null = null;
|
|
238
|
+
/** A tap also focuses the trigger; for this long that focus is the tap's, not new input. */
|
|
239
|
+
const TOUCH_ECHO = 700;
|
|
240
|
+
/** True while a mouse is over the trigger, the bubble or the gap between them. */
|
|
241
|
+
let hovering = false;
|
|
242
|
+
/** The finger that is down on the trigger, and where it went down. */
|
|
243
|
+
let touchStart: { id: number; x: number; y: number } | null = null;
|
|
244
|
+
/** Further than this, in px, and it was a drag, not a tap. */
|
|
245
|
+
const TAP_SLOP = 10;
|
|
246
|
+
|
|
247
|
+
function clearTouchHide(): void {
|
|
248
|
+
if (touchHideTimeout !== null) {
|
|
249
|
+
clearTimeout(touchHideTimeout);
|
|
250
|
+
touchHideTimeout = null;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/** True in a right-to-left page, where the start side is the right: the side arrows turn round. */
|
|
255
|
+
let isMirrored = $state(false);
|
|
256
|
+
|
|
257
|
+
function show(): void {
|
|
258
|
+
stopCrossing();
|
|
259
|
+
// One at a time: the tooltip that was open closes.
|
|
260
|
+
claimOpenTooltip(hide);
|
|
261
|
+
if (parkTimeout !== null) {
|
|
262
|
+
clearTimeout(parkTimeout);
|
|
263
|
+
parkTimeout = null;
|
|
264
|
+
}
|
|
265
|
+
// Placed before it is let out of the corner: laid out where the styles
|
|
266
|
+
// alone put it, a bubble near the edge would widen the page for a
|
|
267
|
+
// moment, and in a right-to-left page that moves what is under the finger.
|
|
268
|
+
if (!isVisible) fitToViewport();
|
|
269
|
+
parked = false;
|
|
270
|
+
isVisible = true;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function hide(): void {
|
|
274
|
+
clearTouchHide();
|
|
275
|
+
stopCrossing();
|
|
276
|
+
openedByTouch = false;
|
|
277
|
+
releaseOpenTooltip(hide);
|
|
278
|
+
if (!isVisible) return;
|
|
279
|
+
isVisible = false;
|
|
280
|
+
if (parkTimeout !== null) clearTimeout(parkTimeout);
|
|
281
|
+
parkTimeout = setTimeout(() => {
|
|
282
|
+
parkTimeout = null;
|
|
283
|
+
parked = true;
|
|
284
|
+
flippedTo = null;
|
|
285
|
+
shift = { x: 0, y: 0 };
|
|
286
|
+
arrowShift = { x: 0, y: 0 };
|
|
287
|
+
}, PARK_AFTER);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const isTouch = (type: string) => type === "touch" || type === "pen";
|
|
291
|
+
const afterTouch = () => isTouch(lastPointerType) && Date.now() - touchedAt < TOUCH_ECHO;
|
|
292
|
+
|
|
85
293
|
function clearShowDelay(): void {
|
|
86
294
|
if (showDelayTimeout !== null) {
|
|
87
295
|
clearTimeout(showDelayTimeout);
|
|
@@ -97,7 +305,9 @@
|
|
|
97
305
|
}
|
|
98
306
|
|
|
99
307
|
function findDescribedTarget(root: HTMLElement): HTMLElement | null {
|
|
100
|
-
const found =
|
|
308
|
+
const found =
|
|
309
|
+
root.querySelector<HTMLElement>(FOCUSABLE_SELECTOR) ??
|
|
310
|
+
root.querySelector<HTMLElement>(CONTROL_SELECTOR);
|
|
101
311
|
if (found) {
|
|
102
312
|
return found;
|
|
103
313
|
}
|
|
@@ -135,19 +345,45 @@
|
|
|
135
345
|
if (!content || disabled) {
|
|
136
346
|
clearShowDelay();
|
|
137
347
|
clearHideBlurTimeout();
|
|
348
|
+
// Switched off while open: closed, and no longer listening for a tap elsewhere.
|
|
349
|
+
untrack(hide);
|
|
138
350
|
}
|
|
139
351
|
});
|
|
140
352
|
|
|
141
353
|
onDestroy(() => {
|
|
354
|
+
stopCrossing();
|
|
355
|
+
releaseOpenTooltip(hide);
|
|
142
356
|
clearShowDelay();
|
|
143
357
|
clearHideBlurTimeout();
|
|
358
|
+
clearTouchHide();
|
|
359
|
+
if (parkTimeout !== null) clearTimeout(parkTimeout);
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
// A tooltip opened by a tap has no hover to end and, where a tap does not
|
|
363
|
+
// focus a button (Safari), no blur either: a tap anywhere else closes it,
|
|
364
|
+
// and so does scrolling, which would otherwise carry it over other content.
|
|
365
|
+
$effect(() => {
|
|
366
|
+
if (!isVisible || !openedByTouch) return;
|
|
367
|
+
const onPointerDown = (event: PointerEvent) => {
|
|
368
|
+
if (!containerElement?.contains(event.target as Node | null)) hide();
|
|
369
|
+
};
|
|
370
|
+
const onScroll = () => hide();
|
|
371
|
+
document.addEventListener("pointerdown", onPointerDown, true);
|
|
372
|
+
window.addEventListener("scroll", onScroll, true);
|
|
373
|
+
return () => {
|
|
374
|
+
document.removeEventListener("pointerdown", onPointerDown, true);
|
|
375
|
+
window.removeEventListener("scroll", onScroll, true);
|
|
376
|
+
};
|
|
144
377
|
});
|
|
145
378
|
|
|
146
379
|
function handleKeydown(event: KeyboardEvent) {
|
|
147
380
|
if (event.key === "Escape" && isVisible) {
|
|
381
|
+
// Capture phase, and marked as handled: the first Escape dismisses
|
|
382
|
+
// the tooltip only, not the modal or menu it sits in (WCAG 1.4.13).
|
|
383
|
+
event.preventDefault();
|
|
148
384
|
clearShowDelay();
|
|
149
385
|
clearHideBlurTimeout();
|
|
150
|
-
|
|
386
|
+
hide();
|
|
151
387
|
// Keep focus on the described control; never steal it when the tooltip was hover-only.
|
|
152
388
|
const target = triggerElement ? findDescribedTarget(triggerElement) : null;
|
|
153
389
|
if (target && triggerElement?.contains(document.activeElement)) {
|
|
@@ -159,8 +395,10 @@
|
|
|
159
395
|
function handleFocus() {
|
|
160
396
|
clearShowDelay();
|
|
161
397
|
clearHideBlurTimeout();
|
|
398
|
+
// The focus a tap gives the trigger: the tap has already opened or closed it.
|
|
399
|
+
if (afterTouch()) return;
|
|
162
400
|
if (!disabled && content) {
|
|
163
|
-
|
|
401
|
+
show();
|
|
164
402
|
}
|
|
165
403
|
}
|
|
166
404
|
|
|
@@ -169,33 +407,165 @@
|
|
|
169
407
|
// Defer hide so brief focus moves within the trigger subtree do not flash the tooltip off.
|
|
170
408
|
hideBlurTimeout = setTimeout(() => {
|
|
171
409
|
hideBlurTimeout = null;
|
|
172
|
-
|
|
410
|
+
// A press on the bubble itself (to select its text) takes focus
|
|
411
|
+
// off the trigger. The pointer is still on the tooltip: it stays,
|
|
412
|
+
// and leaving it, a press elsewhere or Escape closes it.
|
|
413
|
+
if (hovering || containerElement?.contains(document.activeElement)) return;
|
|
414
|
+
hide();
|
|
173
415
|
}, 100);
|
|
174
416
|
}
|
|
175
417
|
|
|
176
418
|
function handleMouseEnter() {
|
|
419
|
+
// The mouse events a browser makes up after a tap. Opening here would
|
|
420
|
+
// undo a tap that closed the tooltip, and on iOS a hover handler that
|
|
421
|
+
// shows something makes the first tap a hover and drops its click.
|
|
422
|
+
if (isTouch(lastPointerType)) return;
|
|
423
|
+
hovering = true;
|
|
424
|
+
stopCrossing();
|
|
177
425
|
clearShowDelay();
|
|
178
426
|
clearHideBlurTimeout();
|
|
179
427
|
if (!disabled && content) {
|
|
180
428
|
if (delay > 0) {
|
|
181
429
|
showDelayTimeout = setTimeout(() => {
|
|
182
430
|
showDelayTimeout = null;
|
|
183
|
-
|
|
431
|
+
show();
|
|
184
432
|
}, delay);
|
|
185
433
|
} else {
|
|
186
|
-
|
|
434
|
+
show();
|
|
187
435
|
}
|
|
188
436
|
}
|
|
189
437
|
}
|
|
190
438
|
|
|
191
|
-
|
|
439
|
+
/** Stops watching a pointer that is on its way from the trigger to the bubble. */
|
|
440
|
+
let stopCrossing: () => void = () => {};
|
|
441
|
+
/** A pointer that has stopped short of the bubble is given this long without moving, in ms. */
|
|
442
|
+
const CROSSING_TIME = 300;
|
|
443
|
+
|
|
444
|
+
/**
|
|
445
|
+
* The pointer has left the tooltip. Straight across the gap it never
|
|
446
|
+
* does, but from a small trigger to the far corner of a wide bubble the
|
|
447
|
+
* way is diagonal and leaves through the side. While it stays in the
|
|
448
|
+
* triangle between where it left and the near edge of the bubble it is
|
|
449
|
+
* still coming, and the tooltip waits: until it arrives, strays, or stops.
|
|
450
|
+
*/
|
|
451
|
+
function handleMouseLeave(event: MouseEvent) {
|
|
452
|
+
hovering = false;
|
|
453
|
+
if (isTouch(lastPointerType)) return;
|
|
192
454
|
clearShowDelay();
|
|
193
455
|
clearHideBlurTimeout();
|
|
194
|
-
|
|
456
|
+
const box = isVisible ? bubbleElement?.getBoundingClientRect() : undefined;
|
|
457
|
+
// No bubble on screen (or no layout to measure): nothing to cross to.
|
|
458
|
+
if (!box || box.width === 0 || box.height === 0) {
|
|
459
|
+
hide();
|
|
460
|
+
return;
|
|
461
|
+
}
|
|
462
|
+
const left = { x: event.clientX, y: event.clientY };
|
|
463
|
+
// The edge of the bubble that faces the trigger, a little wider than it is.
|
|
464
|
+
const reach = 4;
|
|
465
|
+
const [near, far] = {
|
|
466
|
+
top: [
|
|
467
|
+
{ x: box.left - reach, y: box.bottom },
|
|
468
|
+
{ x: box.right + reach, y: box.bottom },
|
|
469
|
+
],
|
|
470
|
+
bottom: [
|
|
471
|
+
{ x: box.left - reach, y: box.top },
|
|
472
|
+
{ x: box.right + reach, y: box.top },
|
|
473
|
+
],
|
|
474
|
+
left: [
|
|
475
|
+
{ x: box.right, y: box.top - reach },
|
|
476
|
+
{ x: box.right, y: box.bottom + reach },
|
|
477
|
+
],
|
|
478
|
+
right: [
|
|
479
|
+
{ x: box.left, y: box.top - reach },
|
|
480
|
+
{ x: box.left, y: box.bottom + reach },
|
|
481
|
+
],
|
|
482
|
+
}[
|
|
483
|
+
// On screen: the start side is the right in a right-to-left page.
|
|
484
|
+
isMirrored && side === "left" ? "right" : isMirrored && side === "right" ? "left" : side
|
|
485
|
+
];
|
|
486
|
+
stopCrossing();
|
|
487
|
+
let timer = setTimeout(hide, CROSSING_TIME);
|
|
488
|
+
const onMove = (move: MouseEvent) => {
|
|
489
|
+
if (insideTriangle({ x: move.clientX, y: move.clientY }, left, near, far)) {
|
|
490
|
+
// Still coming: the wait starts again.
|
|
491
|
+
clearTimeout(timer);
|
|
492
|
+
timer = setTimeout(hide, CROSSING_TIME);
|
|
493
|
+
return;
|
|
494
|
+
}
|
|
495
|
+
// Not on the way: unless it has arrived, which `mouseenter` says.
|
|
496
|
+
if (containerElement?.contains(move.target as Node | null)) return;
|
|
497
|
+
hide();
|
|
498
|
+
};
|
|
499
|
+
document.addEventListener("mousemove", onMove, true);
|
|
500
|
+
stopCrossing = () => {
|
|
501
|
+
clearTimeout(timer);
|
|
502
|
+
document.removeEventListener("mousemove", onMove, true);
|
|
503
|
+
stopCrossing = () => {};
|
|
504
|
+
};
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
/** Which kind of pointer is over the trigger; a mouse arriving after a finger counts again. */
|
|
508
|
+
function handlePointerEnter(event: PointerEvent) {
|
|
509
|
+
lastPointerType = event.pointerType;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
/**
|
|
513
|
+
* A tap of a finger or a pen on the trigger toggles the tooltip: when the
|
|
514
|
+
* finger comes up where it went down, not when it goes down. A finger
|
|
515
|
+
* that goes down to scroll the page would otherwise open the tooltip and
|
|
516
|
+
* close it again as the scroll began. Nothing is prevented: the tap goes
|
|
517
|
+
* on to become the trigger's own click.
|
|
518
|
+
*/
|
|
519
|
+
function handlePointerDown(event: PointerEvent) {
|
|
520
|
+
lastPointerType = event.pointerType;
|
|
521
|
+
if (!isTouch(event.pointerType)) return;
|
|
522
|
+
touchedAt = Date.now();
|
|
523
|
+
// On the bubble itself: neither a tap on the trigger nor one elsewhere.
|
|
524
|
+
if (bubbleElement?.contains(event.target as Node | null)) {
|
|
525
|
+
touchStart = null;
|
|
526
|
+
// It may take focus off the trigger; the tooltip stays, and from
|
|
527
|
+
// here on a tap elsewhere or scrolling is what closes it.
|
|
528
|
+
clearHideBlurTimeout();
|
|
529
|
+
hovering = true;
|
|
530
|
+
if (isVisible) openedByTouch = true;
|
|
531
|
+
return;
|
|
532
|
+
}
|
|
533
|
+
hovering = false;
|
|
534
|
+
touchStart = { id: event.pointerId, x: event.clientX, y: event.clientY };
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
function handlePointerUp(event: PointerEvent) {
|
|
538
|
+
if (!isTouch(event.pointerType)) return;
|
|
539
|
+
touchedAt = Date.now();
|
|
540
|
+
const start = touchStart;
|
|
541
|
+
touchStart = null;
|
|
542
|
+
if (!start || start.id !== event.pointerId) return;
|
|
543
|
+
if (Math.hypot(event.clientX - start.x, event.clientY - start.y) > TAP_SLOP) return;
|
|
544
|
+
clearShowDelay();
|
|
545
|
+
clearHideBlurTimeout();
|
|
546
|
+
if (isVisible) {
|
|
547
|
+
hide();
|
|
548
|
+
return;
|
|
549
|
+
}
|
|
550
|
+
if (disabled || !content) return;
|
|
551
|
+
show();
|
|
552
|
+
openedByTouch = true;
|
|
553
|
+
clearTouchHide();
|
|
554
|
+
if (touchDuration > 0) {
|
|
555
|
+
touchHideTimeout = setTimeout(() => {
|
|
556
|
+
touchHideTimeout = null;
|
|
557
|
+
hide();
|
|
558
|
+
}, touchDuration);
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
/** The browser took the touch for itself: to scroll, most often. */
|
|
563
|
+
function handlePointerCancel() {
|
|
564
|
+
touchStart = null;
|
|
195
565
|
}
|
|
196
566
|
</script>
|
|
197
567
|
|
|
198
|
-
<svelte:window
|
|
568
|
+
<svelte:window onkeydowncapture={handleKeydown} />
|
|
199
569
|
|
|
200
570
|
<div
|
|
201
571
|
class={cn(
|
|
@@ -203,11 +573,17 @@
|
|
|
203
573
|
block ? "block w-full" : "inline-block",
|
|
204
574
|
className,
|
|
205
575
|
)}
|
|
206
|
-
|
|
576
|
+
bind:this={containerElement}
|
|
577
|
+
data-placement={side}
|
|
207
578
|
data-strategy={fixed ? "fixed" : "absolute"}
|
|
579
|
+
data-mirrored={isMirrored ? "true" : undefined}
|
|
208
580
|
data-disabled={disabled}
|
|
209
581
|
onmouseenter={handleMouseEnter}
|
|
210
582
|
onmouseleave={handleMouseLeave}
|
|
583
|
+
onpointerenter={handlePointerEnter}
|
|
584
|
+
onpointerdown={handlePointerDown}
|
|
585
|
+
onpointerup={handlePointerUp}
|
|
586
|
+
onpointercancel={handlePointerCancel}
|
|
211
587
|
onfocusin={handleFocus}
|
|
212
588
|
onfocusout={handleBlur}
|
|
213
589
|
{...restProps}
|
|
@@ -218,13 +594,15 @@
|
|
|
218
594
|
|
|
219
595
|
{#if content && !disabled}
|
|
220
596
|
<div
|
|
597
|
+
bind:this={bubbleElement}
|
|
221
598
|
id={tooltipId}
|
|
222
599
|
class="tooltip pointer-events-none invisible absolute z-tooltip whitespace-normal wrap-break-word rounded-control bg-tooltip-bg px-3 py-2 text-sm leading-5 text-tooltip-fg opacity-0 transition-[opacity,visibility,transform] duration-200 ease-in-out"
|
|
223
600
|
role="tooltip"
|
|
224
601
|
aria-hidden={!isVisible}
|
|
225
602
|
data-visible={isVisible}
|
|
226
|
-
data-
|
|
227
|
-
|
|
603
|
+
data-parked={parked}
|
|
604
|
+
data-placement={side}
|
|
605
|
+
style={bubbleStyle || undefined}
|
|
228
606
|
>
|
|
229
607
|
{content}
|
|
230
608
|
</div>
|
|
@@ -240,9 +618,11 @@
|
|
|
240
618
|
max-width: var(--tooltip-max-width, min(24rem, calc(100vw - 2rem)));
|
|
241
619
|
}
|
|
242
620
|
|
|
621
|
+
/* Centred over the trigger with `left`, not `inset-inline-start`: the
|
|
622
|
+
shift back by half is to the left whichever way the text runs. */
|
|
243
623
|
.tooltip-container[data-placement="top"] .tooltip {
|
|
244
624
|
inset-block-end: 100%;
|
|
245
|
-
|
|
625
|
+
left: 50%;
|
|
246
626
|
transform: translateX(-50%) translateY(4px) scale(0.95);
|
|
247
627
|
margin-block-end: var(--tooltip-gap, 0.5rem);
|
|
248
628
|
}
|
|
@@ -255,7 +635,7 @@
|
|
|
255
635
|
|
|
256
636
|
.tooltip-container[data-placement="bottom"] .tooltip {
|
|
257
637
|
inset-block-start: 100%;
|
|
258
|
-
|
|
638
|
+
left: 50%;
|
|
259
639
|
transform: translateX(-50%) translateY(-4px) scale(0.95);
|
|
260
640
|
margin-block-start: var(--tooltip-gap, 0.5rem);
|
|
261
641
|
}
|
|
@@ -301,24 +681,92 @@
|
|
|
301
681
|
margin: 0;
|
|
302
682
|
}
|
|
303
683
|
|
|
684
|
+
/* Closed and faded out: parked in the corner of the viewport, so a bubble
|
|
685
|
+
that would reach past the edge of a narrow screen does not widen the
|
|
686
|
+
page while nobody can see it. The selector outweighs the placement
|
|
687
|
+
rules, the fixed strategy and the narrow-screen rules below. */
|
|
688
|
+
.tooltip-container[data-strategy] .tooltip[data-parked="true"] {
|
|
689
|
+
position: fixed !important;
|
|
690
|
+
inset-block: 0 auto !important;
|
|
691
|
+
inset-inline: 0 auto !important;
|
|
692
|
+
margin: 0 !important;
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
/* Open, the bubble can be pointed at, so the pointer can move onto it and
|
|
696
|
+
its text can be selected (WCAG 1.4.13). The strip behind it covers the
|
|
697
|
+
gap to the trigger: the pointer never leaves the tooltip on the way
|
|
698
|
+
across. Closed or fading, neither takes the pointer. */
|
|
699
|
+
.tooltip[data-visible="true"] {
|
|
700
|
+
pointer-events: auto;
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
.tooltip::after {
|
|
704
|
+
content: "";
|
|
705
|
+
position: absolute;
|
|
706
|
+
pointer-events: none;
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
.tooltip[data-visible="true"]::after {
|
|
710
|
+
pointer-events: auto;
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
.tooltip-container[data-placement="top"] .tooltip::after {
|
|
714
|
+
inset-inline: 0;
|
|
715
|
+
inset-block-start: 100%;
|
|
716
|
+
height: var(--tooltip-gap, 0.5rem);
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
.tooltip-container[data-placement="bottom"] .tooltip::after {
|
|
720
|
+
inset-inline: 0;
|
|
721
|
+
inset-block-end: 100%;
|
|
722
|
+
height: var(--tooltip-gap, 0.5rem);
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
.tooltip-container[data-placement="left"] .tooltip::after {
|
|
726
|
+
inset-block: 0;
|
|
727
|
+
inset-inline-start: 100%;
|
|
728
|
+
width: var(--tooltip-gap, 0.5rem);
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
.tooltip-container[data-placement="right"] .tooltip::after {
|
|
732
|
+
inset-block: 0;
|
|
733
|
+
inset-inline-end: 100%;
|
|
734
|
+
width: var(--tooltip-gap, 0.5rem);
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
/* The fixed strategy places the bubble by `left` and `top`, so in a
|
|
738
|
+
right-to-left page it is still on the side it is named after: the strip
|
|
739
|
+
is on the physical side, not the logical one. */
|
|
740
|
+
.tooltip-container[data-strategy="fixed"][data-placement="left"] .tooltip::after {
|
|
741
|
+
right: auto;
|
|
742
|
+
left: 100%;
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
.tooltip-container[data-strategy="fixed"][data-placement="right"] .tooltip::after {
|
|
746
|
+
right: 100%;
|
|
747
|
+
left: auto;
|
|
748
|
+
}
|
|
749
|
+
|
|
304
750
|
.tooltip::before {
|
|
305
751
|
content: "";
|
|
306
752
|
position: absolute;
|
|
307
753
|
width: calc(var(--tooltip-arrow-size, 4px) * 2);
|
|
308
754
|
height: calc(var(--tooltip-arrow-size, 4px) * 2);
|
|
309
755
|
background-color: var(--color-tooltip-bg);
|
|
756
|
+
/* Set when the bubble was moved to stay on screen: the arrow moves back. */
|
|
757
|
+
translate: var(--tooltip-arrow-shift, 0px 0px);
|
|
310
758
|
}
|
|
311
759
|
|
|
312
760
|
.tooltip-container[data-placement="top"] .tooltip::before {
|
|
313
761
|
inset-block-start: 100%;
|
|
314
|
-
|
|
762
|
+
left: 50%;
|
|
315
763
|
transform: translateX(-50%);
|
|
316
764
|
clip-path: polygon(50% 100%, 0 0, 100% 0);
|
|
317
765
|
}
|
|
318
766
|
|
|
319
767
|
.tooltip-container[data-placement="bottom"] .tooltip::before {
|
|
320
768
|
inset-block-end: 100%;
|
|
321
|
-
|
|
769
|
+
left: 50%;
|
|
322
770
|
transform: translateX(-50%);
|
|
323
771
|
clip-path: polygon(0 100%, 50% 0, 100% 100%);
|
|
324
772
|
}
|
|
@@ -337,16 +785,44 @@
|
|
|
337
785
|
clip-path: polygon(0 50%, 100% 0, 100% 100%);
|
|
338
786
|
}
|
|
339
787
|
|
|
788
|
+
/* In a right-to-left page `left` is the start side, which is the right:
|
|
789
|
+
the bubble is on the other side of the trigger, and so is the arrow,
|
|
790
|
+
which has to point the other way too. */
|
|
791
|
+
.tooltip-container[data-mirrored="true"][data-placement="left"] .tooltip::before {
|
|
792
|
+
clip-path: polygon(0 50%, 100% 0, 100% 100%);
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
.tooltip-container[data-mirrored="true"][data-placement="right"] .tooltip::before {
|
|
796
|
+
clip-path: polygon(0 0, 100% 50%, 0 100%);
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
/* As for the strip above: the fixed strategy is not mirrored, so its
|
|
800
|
+
arrow is on the physical side that faces the trigger. */
|
|
801
|
+
.tooltip-container[data-strategy="fixed"][data-placement="left"] .tooltip::before {
|
|
802
|
+
right: auto;
|
|
803
|
+
left: 100%;
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
.tooltip-container[data-strategy="fixed"][data-placement="right"] .tooltip::before {
|
|
807
|
+
right: 100%;
|
|
808
|
+
left: auto;
|
|
809
|
+
}
|
|
810
|
+
|
|
340
811
|
@media (max-width: 640px) {
|
|
341
812
|
.tooltip {
|
|
342
813
|
--tooltip-max-width: calc(100vw - 2rem);
|
|
343
|
-
|
|
344
|
-
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
/* Not the fixed strategy: its bubble is placed by inline left and top,
|
|
817
|
+
which an !important inset would override. */
|
|
818
|
+
.tooltip-container:not([data-strategy="fixed"]) .tooltip {
|
|
819
|
+
left: 50% !important;
|
|
820
|
+
right: auto !important;
|
|
345
821
|
transform: translateX(-50%) scale(0.95) !important;
|
|
346
822
|
margin: var(--tooltip-gap, 0.5rem) 0 !important;
|
|
347
823
|
}
|
|
348
824
|
|
|
349
|
-
.tooltip[data-visible="true"] {
|
|
825
|
+
.tooltip-container:not([data-strategy="fixed"]) .tooltip[data-visible="true"] {
|
|
350
826
|
transform: translateX(-50%) scale(1) !important;
|
|
351
827
|
}
|
|
352
828
|
|
|
@@ -361,6 +837,15 @@
|
|
|
361
837
|
}
|
|
362
838
|
}
|
|
363
839
|
|
|
840
|
+
/* Forced colours repaint the fill as the page's own canvas, and the
|
|
841
|
+
bubble has no border: without an edge its text lies loose over the
|
|
842
|
+
content under it. An outline takes no room, so nothing moves. */
|
|
843
|
+
@media (forced-colors: active) {
|
|
844
|
+
.tooltip {
|
|
845
|
+
outline: 1px solid CanvasText;
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
|
|
364
849
|
@media (prefers-reduced-motion: reduce) {
|
|
365
850
|
.tooltip {
|
|
366
851
|
transition:
|