@eifi1/ui-kit 0.4.0 → 0.5.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/LICENSE +32 -0
- package/README.md +498 -57
- package/dist/chart.d.ts +10 -0
- package/dist/chart.js +10 -0
- package/dist/chart.js.map +1 -0
- package/dist/components/account-settings.d.ts +11 -0
- package/dist/components/account-settings.js +19 -12
- package/dist/components/account-settings.js.map +1 -1
- package/dist/components/alert-banner.d.ts +10 -6
- package/dist/components/alert-banner.js +20 -16
- package/dist/components/alert-banner.js.map +1 -1
- package/dist/components/amount-input.d.ts +30 -4
- package/dist/components/amount-input.js +36 -14
- package/dist/components/amount-input.js.map +1 -1
- package/dist/components/calculator.d.ts +24 -7
- package/dist/components/calculator.js +37 -20
- package/dist/components/calculator.js.map +1 -1
- package/dist/components/chart-zoom.d.ts +156 -0
- package/dist/components/chart-zoom.js +262 -0
- package/dist/components/chart-zoom.js.map +1 -0
- package/dist/components/chart.d.ts +28 -3
- package/dist/components/chart.js +71 -32
- package/dist/components/chart.js.map +1 -1
- package/dist/components/checkbox.d.ts +51 -0
- package/dist/components/checkbox.js +123 -0
- package/dist/components/checkbox.js.map +1 -0
- package/dist/components/chip.d.ts +128 -0
- package/dist/components/chip.js +357 -0
- package/dist/components/chip.js.map +1 -0
- package/dist/components/choice-parts.d.ts +24 -0
- package/dist/components/choice-parts.js +18 -0
- package/dist/components/choice-parts.js.map +1 -0
- package/dist/components/combobox-core.d.ts +52 -9
- package/dist/components/combobox-core.js +91 -20
- package/dist/components/combobox-core.js.map +1 -1
- package/dist/components/combobox.d.ts +43 -33
- package/dist/components/combobox.js +409 -340
- package/dist/components/combobox.js.map +1 -1
- package/dist/components/currency-select.d.ts +48 -8
- package/dist/components/currency-select.js +107 -65
- package/dist/components/currency-select.js.map +1 -1
- package/dist/components/data-table-filter-popover.d.ts +15 -3
- package/dist/components/data-table-filter-popover.js +23 -17
- package/dist/components/data-table-filter-popover.js.map +1 -1
- package/dist/components/data-table-filters.d.ts +1 -1
- package/dist/components/data-table-filters.js +3 -2
- package/dist/components/data-table-filters.js.map +1 -1
- package/dist/components/data-table-labels.d.ts +53 -2
- package/dist/components/data-table-labels.js +31 -2
- package/dist/components/data-table-labels.js.map +1 -1
- package/dist/components/data-table-pagination.d.ts +3 -1
- package/dist/components/data-table-pagination.js +16 -10
- package/dist/components/data-table-pagination.js.map +1 -1
- package/dist/components/data-table-sort.js +1 -0
- package/dist/components/data-table-sort.js.map +1 -1
- package/dist/components/data-table.d.ts +1 -1
- package/dist/components/data-table.js +201 -85
- package/dist/components/data-table.js.map +1 -1
- package/dist/components/date-picker.d.ts +25 -9
- package/dist/components/date-picker.js +232 -94
- package/dist/components/date-picker.js.map +1 -1
- package/dist/components/dropdown.d.ts +96 -20
- package/dist/components/dropdown.js +77 -33
- package/dist/components/dropdown.js.map +1 -1
- package/dist/components/entity-combobox.d.ts +5 -4
- package/dist/components/entity-combobox.js +107 -76
- package/dist/components/entity-combobox.js.map +1 -1
- package/dist/components/facing-pair.d.ts +36 -0
- package/dist/components/facing-pair.js +40 -0
- package/dist/components/facing-pair.js.map +1 -0
- package/dist/components/field-sync.d.ts +164 -0
- package/dist/components/field-sync.js +264 -0
- package/dist/components/field-sync.js.map +1 -0
- package/dist/components/file-dropzone.d.ts +38 -6
- package/dist/components/file-dropzone.js +34 -9
- package/dist/components/file-dropzone.js.map +1 -1
- package/dist/components/full-bleed-dialog.d.ts +37 -26
- package/dist/components/full-bleed-dialog.js +20 -5
- package/dist/components/full-bleed-dialog.js.map +1 -1
- package/dist/components/grouped-picker.d.ts +21 -10
- package/dist/components/grouped-picker.js +66 -52
- package/dist/components/grouped-picker.js.map +1 -1
- package/dist/components/hover-menu.d.ts +20 -4
- package/dist/components/hover-menu.js +15 -4
- package/dist/components/hover-menu.js.map +1 -1
- package/dist/components/mini-calendar.d.ts +102 -14
- package/dist/components/mini-calendar.js +256 -70
- package/dist/components/mini-calendar.js.map +1 -1
- package/dist/components/modal.d.ts +9 -3
- package/dist/components/modal.js +34 -45
- package/dist/components/modal.js.map +1 -1
- package/dist/components/month-picker.d.ts +79 -0
- package/dist/components/month-picker.js +342 -0
- package/dist/components/month-picker.js.map +1 -0
- package/dist/components/multi-entity-combobox.d.ts +7 -6
- package/dist/components/multi-entity-combobox.js +112 -72
- package/dist/components/multi-entity-combobox.js.map +1 -1
- package/dist/components/multi-select.d.ts +13 -11
- package/dist/components/multi-select.js +161 -84
- package/dist/components/multi-select.js.map +1 -1
- package/dist/components/number-field.d.ts +92 -0
- package/dist/components/number-field.js +111 -0
- package/dist/components/number-field.js.map +1 -0
- package/dist/components/number-input.d.ts +28 -2
- package/dist/components/number-input.js +12 -2
- package/dist/components/number-input.js.map +1 -1
- package/dist/components/numpad-sheet.d.ts +32 -4
- package/dist/components/numpad-sheet.js +30 -13
- package/dist/components/numpad-sheet.js.map +1 -1
- package/dist/components/page-contents.d.ts +90 -0
- package/dist/components/page-contents.js +170 -0
- package/dist/components/page-contents.js.map +1 -0
- package/dist/components/password-strength.d.ts +106 -0
- package/dist/components/password-strength.js +132 -0
- package/dist/components/password-strength.js.map +1 -0
- package/dist/components/picker-sheet.d.ts +23 -15
- package/dist/components/picker-sheet.js +34 -14
- package/dist/components/picker-sheet.js.map +1 -1
- package/dist/components/popover.d.ts +62 -4
- package/dist/components/popover.js +52 -2
- package/dist/components/popover.js.map +1 -1
- package/dist/components/search-field.d.ts +25 -7
- package/dist/components/search-field.js +7 -5
- package/dist/components/search-field.js.map +1 -1
- package/dist/components/series-chart-labels.d.ts +14 -0
- package/dist/components/series-chart-labels.js +11 -0
- package/dist/components/series-chart-labels.js.map +1 -0
- package/dist/components/series-chart.d.ts +207 -0
- package/dist/components/series-chart.js +223 -0
- package/dist/components/series-chart.js.map +1 -0
- package/dist/components/settings-fields.d.ts +14 -4
- package/dist/components/settings-fields.js +19 -8
- package/dist/components/settings-fields.js.map +1 -1
- package/dist/components/signature-pad.d.ts +119 -0
- package/dist/components/signature-pad.js +405 -0
- package/dist/components/signature-pad.js.map +1 -0
- package/dist/components/slider.d.ts +80 -0
- package/dist/components/slider.js +155 -0
- package/dist/components/slider.js.map +1 -0
- package/dist/components/sparkline.d.ts +93 -0
- package/dist/components/sparkline.js +205 -0
- package/dist/components/sparkline.js.map +1 -0
- package/dist/components/stat-tile.d.ts +151 -0
- package/dist/components/stat-tile.js +236 -0
- package/dist/components/stat-tile.js.map +1 -0
- package/dist/components/swipeable-row.d.ts +25 -2
- package/dist/components/swipeable-row.js +31 -0
- package/dist/components/swipeable-row.js.map +1 -1
- package/dist/components/switch.d.ts +60 -0
- package/dist/components/switch.js +81 -0
- package/dist/components/switch.js.map +1 -0
- package/dist/components/time-input.d.ts +58 -0
- package/dist/components/time-input.js +50 -0
- package/dist/components/time-input.js.map +1 -0
- package/dist/components/toggle-group.d.ts +18 -3
- package/dist/components/toggle-group.js +10 -6
- package/dist/components/toggle-group.js.map +1 -1
- package/dist/components/toggle-legend.d.ts +115 -0
- package/dist/components/toggle-legend.js +114 -0
- package/dist/components/toggle-legend.js.map +1 -0
- package/dist/components/tooltip.d.ts +40 -11
- package/dist/components/tooltip.js +77 -20
- package/dist/components/tooltip.js.map +1 -1
- package/dist/components/treemap.d.ts +132 -0
- package/dist/components/treemap.js +179 -0
- package/dist/components/treemap.js.map +1 -0
- package/dist/components/ui.d.ts +159 -46
- package/dist/components/ui.js +220 -115
- package/dist/components/ui.js.map +1 -1
- package/dist/components/use-mobile-reveal.js +1 -0
- package/dist/components/use-mobile-reveal.js.map +1 -1
- package/dist/components/use-table-state.d.ts +1 -1
- package/dist/components/use-table-state.js +38 -17
- package/dist/components/use-table-state.js.map +1 -1
- package/dist/components/user-avatar.d.ts +15 -4
- package/dist/components/user-avatar.js +5 -4
- package/dist/components/user-avatar.js.map +1 -1
- package/dist/components/wizard-stepper.js +3 -2
- package/dist/components/wizard-stepper.js.map +1 -1
- package/dist/{data-table-filters-aXyf0Xub.d.ts → data-table-filters-DM1auUfH.d.ts} +27 -5
- package/dist/data-table.d.ts +7 -0
- package/dist/data-table.js +12 -0
- package/dist/data-table.js.map +1 -0
- package/dist/feedback/feedback-attachment.js +12 -11
- package/dist/feedback/feedback-attachment.js.map +1 -1
- package/dist/feedback/feedback-dialog.js +2 -1
- package/dist/feedback/feedback-dialog.js.map +1 -1
- package/dist/feedback/feedback-inbox.d.ts +33 -7
- package/dist/feedback/feedback-inbox.js +49 -37
- package/dist/feedback/feedback-inbox.js.map +1 -1
- package/dist/feedback.d.ts +5 -0
- package/dist/feedback.js +5 -0
- package/dist/feedback.js.map +1 -0
- package/dist/hooks/use-anchored-panel.js +1 -0
- package/dist/hooks/use-anchored-panel.js.map +1 -1
- package/dist/hooks/use-anchored-rect.js +1 -0
- package/dist/hooks/use-anchored-rect.js.map +1 -1
- package/dist/hooks/use-announce.d.ts +46 -0
- package/dist/hooks/use-announce.js +37 -0
- package/dist/hooks/use-announce.js.map +1 -0
- package/dist/hooks/use-body-scroll-lock.js +1 -0
- package/dist/hooks/use-body-scroll-lock.js.map +1 -1
- package/dist/hooks/use-close-transition.js +1 -0
- package/dist/hooks/use-close-transition.js.map +1 -1
- package/dist/hooks/use-dismiss.js +1 -0
- package/dist/hooks/use-dismiss.js.map +1 -1
- package/dist/hooks/use-focus-trap.d.ts +16 -0
- package/dist/hooks/use-focus-trap.js +67 -0
- package/dist/hooks/use-focus-trap.js.map +1 -0
- package/dist/hooks/use-media-query.js +1 -0
- package/dist/hooks/use-media-query.js.map +1 -1
- package/dist/hooks/use-overlay-history.js +27 -1
- package/dist/hooks/use-overlay-history.js.map +1 -1
- package/dist/hooks/use-row-swipe.js +1 -0
- package/dist/hooks/use-row-swipe.js.map +1 -1
- package/dist/i18n/defaults.d.ts +31 -0
- package/dist/i18n/defaults.js +63 -0
- package/dist/i18n/defaults.js.map +1 -0
- package/dist/i18n/kit-labels.d.ts +250 -0
- package/dist/i18n/kit-labels.js +184 -0
- package/dist/i18n/kit-labels.js.map +1 -0
- package/dist/index.d.ts +47 -22
- package/dist/index.js +28 -0
- package/dist/index.js.map +1 -1
- package/dist/lib/calc.js +1 -0
- package/dist/lib/calc.js.map +1 -1
- package/dist/lib/cn.js +1 -0
- package/dist/lib/cn.js.map +1 -1
- package/dist/lib/dates.js +1 -0
- package/dist/lib/dates.js.map +1 -1
- package/dist/lib/logger.js +1 -0
- package/dist/lib/logger.js.map +1 -1
- package/dist/lib/safe-storage.js +1 -0
- package/dist/lib/safe-storage.js.map +1 -1
- package/dist/search/command-palette.d.ts +11 -1
- package/dist/search/command-palette.js +21 -15
- package/dist/search/command-palette.js.map +1 -1
- package/dist/search.d.ts +2 -0
- package/dist/search.js +3 -0
- package/dist/search.js.map +1 -0
- package/dist/shell/app-shell.d.ts +33 -4
- package/dist/shell/app-shell.js +275 -78
- package/dist/shell/app-shell.js.map +1 -1
- package/dist/shell/option-switcher-menu.js +3 -2
- package/dist/shell/option-switcher-menu.js.map +1 -1
- package/dist/shell/role-switcher.d.ts +6 -0
- package/dist/shell/role-switcher.js +7 -4
- package/dist/shell/role-switcher.js.map +1 -1
- package/dist/shell/top-bar.d.ts +15 -7
- package/dist/shell/top-bar.js +3 -5
- package/dist/shell/top-bar.js.map +1 -1
- package/dist/shell/topbar-action-menu.js +3 -2
- package/dist/shell/topbar-action-menu.js.map +1 -1
- package/dist/shell/topbar-controls.d.ts +5 -2
- package/dist/shell/topbar-controls.js +20 -15
- package/dist/shell/topbar-controls.js.map +1 -1
- package/dist/shell.d.ts +13 -0
- package/dist/shell.js +8 -0
- package/dist/shell.js.map +1 -0
- package/dist/theme/chart-palette.d.ts +19 -12
- package/dist/theme/chart-palette.js +24 -30
- package/dist/theme/chart-palette.js.map +1 -1
- package/dist/theme/color.d.ts +78 -0
- package/dist/theme/color.js +148 -0
- package/dist/theme/color.js.map +1 -0
- package/dist/theme/palette-derive.d.ts +140 -0
- package/dist/theme/palette-derive.js +376 -0
- package/dist/theme/palette-derive.js.map +1 -0
- package/dist/theme/palette-presets.d.ts +27 -5
- package/dist/theme/palette-presets.js +231 -33
- package/dist/theme/palette-presets.js.map +1 -1
- package/dist/theme/palette-store.js +1 -0
- package/dist/theme/palette-store.js.map +1 -1
- package/dist/theme/theme-store.js +1 -0
- package/dist/theme/theme-store.js.map +1 -1
- package/dist/tour/tour.d.ts +2 -1
- package/dist/tour/tour.js +15 -9
- package/dist/tour/tour.js.map +1 -1
- package/dist/tour.d.ts +2 -0
- package/dist/tour.js +3 -0
- package/dist/tour.js.map +1 -0
- package/dist/wizard/stepper-nav.js +10 -8
- package/dist/wizard/stepper-nav.js.map +1 -1
- package/dist/wizard/types.d.ts +18 -2
- package/dist/wizard/types.js +4 -1
- package/dist/wizard/types.js.map +1 -1
- package/dist/wizard/use-wizard.js +12 -4
- package/dist/wizard/use-wizard.js.map +1 -1
- package/dist/wizard/validation.js +1 -0
- package/dist/wizard/validation.js.map +1 -1
- package/dist/wizard/wizard-context.js +1 -0
- package/dist/wizard/wizard-context.js.map +1 -1
- package/dist/wizard/wizard-step.js +1 -0
- package/dist/wizard/wizard-step.js.map +1 -1
- package/dist/wizard/wizard-summary.js +5 -3
- package/dist/wizard/wizard-summary.js.map +1 -1
- package/dist/wizard.d.ts +8 -0
- package/dist/wizard.js +9 -0
- package/dist/wizard.js.map +1 -0
- package/package.json +67 -14
- package/src/chart.ts +22 -0
- package/src/components/account-settings.tsx +275 -0
- package/src/components/alert-banner.tsx +75 -0
- package/src/components/amount-input.tsx +471 -0
- package/src/components/calculator.tsx +252 -0
- package/src/components/chart-zoom.tsx +499 -0
- package/src/components/chart.tsx +363 -0
- package/src/components/checkbox.tsx +222 -0
- package/src/components/chip.tsx +570 -0
- package/src/components/choice-parts.ts +33 -0
- package/src/components/combobox-core.tsx +540 -0
- package/src/components/combobox.tsx +1026 -0
- package/src/components/currency-select.tsx +269 -0
- package/src/components/data-table-filter-popover.tsx +252 -0
- package/src/components/data-table-filters.ts +176 -0
- package/src/components/data-table-labels.ts +190 -0
- package/src/components/data-table-pagination.tsx +126 -0
- package/src/components/data-table-sort.ts +70 -0
- package/src/components/data-table.tsx +1740 -0
- package/src/components/date-picker.tsx +631 -0
- package/src/components/dropdown.tsx +341 -0
- package/src/components/entity-combobox.tsx +215 -0
- package/src/components/facing-pair.ts +86 -0
- package/src/components/field-sync.tsx +472 -0
- package/src/components/file-dropzone.tsx +181 -0
- package/src/components/full-bleed-dialog.tsx +179 -0
- package/src/components/grouped-picker.tsx +157 -0
- package/src/components/hover-menu.tsx +183 -0
- package/src/components/mini-calendar.tsx +554 -0
- package/src/components/modal.tsx +287 -0
- package/src/components/month-picker.tsx +549 -0
- package/src/components/multi-entity-combobox.tsx +226 -0
- package/src/components/multi-select.tsx +303 -0
- package/src/components/number-field.tsx +225 -0
- package/src/components/number-input.tsx +211 -0
- package/src/components/numpad-sheet.tsx +264 -0
- package/src/components/page-contents.tsx +289 -0
- package/src/components/password-strength.tsx +258 -0
- package/src/components/picker-sheet.tsx +251 -0
- package/src/components/popover.tsx +195 -0
- package/src/components/search-field.tsx +146 -0
- package/src/components/series-chart-labels.ts +26 -0
- package/src/components/series-chart.tsx +481 -0
- package/src/components/settings-fields.tsx +68 -0
- package/src/components/signature-pad.tsx +630 -0
- package/src/components/slider.tsx +313 -0
- package/src/components/sparkline.tsx +329 -0
- package/src/components/stat-tile.tsx +448 -0
- package/src/components/swipeable-row.tsx +271 -0
- package/src/components/switch.tsx +170 -0
- package/src/components/time-input.tsx +107 -0
- package/src/components/toggle-group.tsx +153 -0
- package/src/components/toggle-legend.tsx +229 -0
- package/src/components/tooltip.tsx +477 -0
- package/src/components/treemap.tsx +390 -0
- package/src/components/ui.tsx +1284 -0
- package/src/components/use-mobile-reveal.ts +54 -0
- package/src/components/use-table-state.ts +295 -0
- package/src/components/user-avatar.tsx +61 -0
- package/src/components/wizard-stepper.tsx +37 -0
- package/src/data-table.ts +19 -0
- package/src/feedback/feedback-attachment.tsx +246 -0
- package/src/feedback/feedback-dialog.tsx +209 -0
- package/src/feedback/feedback-inbox.tsx +615 -0
- package/src/feedback.ts +8 -0
- package/src/hooks/use-anchored-panel.ts +169 -0
- package/src/hooks/use-anchored-rect.ts +78 -0
- package/src/hooks/use-announce.ts +85 -0
- package/src/hooks/use-body-scroll-lock.ts +94 -0
- package/src/hooks/use-close-transition.ts +99 -0
- package/src/hooks/use-dismiss.ts +77 -0
- package/src/hooks/use-focus-trap.ts +160 -0
- package/src/hooks/use-media-query.ts +23 -0
- package/src/hooks/use-overlay-history.ts +345 -0
- package/src/hooks/use-row-swipe.ts +212 -0
- package/src/i18n/defaults.ts +68 -0
- package/src/i18n/kit-labels.tsx +447 -0
- package/src/index.ts +169 -0
- package/src/lib/calc.ts +309 -0
- package/src/lib/cn.ts +5 -0
- package/src/lib/dates.ts +187 -0
- package/src/lib/logger.ts +73 -0
- package/src/lib/safe-storage.ts +37 -0
- package/src/search/command-palette.tsx +342 -0
- package/src/search.ts +3 -0
- package/src/shell/app-shell.tsx +772 -0
- package/src/shell/option-switcher-menu.tsx +72 -0
- package/src/shell/role-switcher.tsx +46 -0
- package/src/shell/top-bar.tsx +39 -0
- package/src/shell/topbar-action-menu.tsx +91 -0
- package/src/shell/topbar-controls.tsx +229 -0
- package/src/shell.ts +10 -0
- package/src/theme/chart-palette.ts +145 -0
- package/src/theme/color.ts +266 -0
- package/src/theme/palette-derive.ts +681 -0
- package/src/theme/palette-presets.ts +760 -0
- package/src/theme/palette-store.ts +94 -0
- package/src/theme/theme-store.ts +145 -0
- package/src/tour/tour.tsx +586 -0
- package/src/tour.ts +3 -0
- package/src/vite-env.d.ts +26 -0
- package/src/wizard/stepper-nav.tsx +221 -0
- package/src/wizard/types.ts +178 -0
- package/src/wizard/use-wizard.ts +345 -0
- package/src/wizard/validation.ts +37 -0
- package/src/wizard/wizard-context.tsx +29 -0
- package/src/wizard/wizard-step.tsx +14 -0
- package/src/wizard/wizard-summary.tsx +69 -0
- package/src/wizard.ts +14 -0
- package/tokens.css +222 -20
|
@@ -0,0 +1,477 @@
|
|
|
1
|
+
import {
|
|
2
|
+
cloneElement,
|
|
3
|
+
isValidElement,
|
|
4
|
+
useId,
|
|
5
|
+
useLayoutEffect,
|
|
6
|
+
useRef,
|
|
7
|
+
useState,
|
|
8
|
+
type ComponentPropsWithoutRef,
|
|
9
|
+
type ReactElement,
|
|
10
|
+
type ReactNode,
|
|
11
|
+
} from "react";
|
|
12
|
+
import { createPortal } from "react-dom";
|
|
13
|
+
import { cn } from "../lib/cn";
|
|
14
|
+
import { useEscapeKey } from "../hooks/use-dismiss";
|
|
15
|
+
import { useAnchoredRect, type AnchorRect } from "../hooks/use-anchored-rect";
|
|
16
|
+
|
|
17
|
+
type TooltipSide = "top" | "bottom" | "left" | "right";
|
|
18
|
+
|
|
19
|
+
/** The floating bubble itself. Uses the shared surface/border/text tokens so it
|
|
20
|
+
* reads as part of the app's chrome (like the top bar and cards) rather than the
|
|
21
|
+
* cold slate pill it used to be.
|
|
22
|
+
*
|
|
23
|
+
* `w-max` keeps a short label on one line — the old `whitespace-nowrap` did that
|
|
24
|
+
* too, but it also let a sentence-length label grow without bound, and a bubble
|
|
25
|
+
* wider than the space beside its trigger gets clipped by whatever overflow
|
|
26
|
+
* container it sits in. So cap it and let long text wrap instead. The cap tracks
|
|
27
|
+
* the viewport as well, for narrow screens where 20rem is already most of it.
|
|
28
|
+
* `side` is a preference rather than an instruction for the PORTALLED variant,
|
|
29
|
+
* which measures the bubble and turns it round when it would not fit
|
|
30
|
+
* (Steering Design feedback #126). The CSS-only one never learns its own size,
|
|
31
|
+
* so there `side` is still the whole of the placement. */
|
|
32
|
+
const TOOLTIP_SURFACE =
|
|
33
|
+
"w-max max-w-[min(20rem,calc(100vw-1rem))] rounded-md border border-[var(--border)] bg-[var(--bg-surface)] px-2 py-1 text-xs font-medium text-[var(--text-primary)] shadow-lg";
|
|
34
|
+
|
|
35
|
+
const sidePositionClass: Record<TooltipSide, string> = {
|
|
36
|
+
top: "bottom-full left-1/2 -translate-x-1/2 mb-1",
|
|
37
|
+
bottom: "top-full left-1/2 -translate-x-1/2 mt-1",
|
|
38
|
+
left: "right-full top-1/2 -translate-y-1/2 mr-1",
|
|
39
|
+
right: "left-full top-1/2 -translate-y-1/2 ml-1",
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* `extends ComponentPropsWithoutRef<"span">` because the wrapper this renders IS a span,
|
|
44
|
+
* and a tooltip is the component a caller most often needs to reach past: it sits
|
|
45
|
+
* between the layout and the control, so a `data-tour` anchor, a test id or an
|
|
46
|
+
* `aria-label` aimed at the trigger used to be swallowed by it.
|
|
47
|
+
*
|
|
48
|
+
* ⚠️ On the empty-label branch there is no wrapper at all, and therefore nothing for
|
|
49
|
+
* those attributes to land on — see the note in the body.
|
|
50
|
+
*/
|
|
51
|
+
export interface TooltipProps extends ComponentPropsWithoutRef<"span"> {
|
|
52
|
+
label: ReactNode;
|
|
53
|
+
side?: TooltipSide;
|
|
54
|
+
className?: string;
|
|
55
|
+
portal?: boolean;
|
|
56
|
+
/** Tag the bubble `data-private`, for a label that repeats the user's own data. */
|
|
57
|
+
redact?: boolean;
|
|
58
|
+
children: ReactNode;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** What each variant below takes: the resolved `side`, and every span attribute the
|
|
62
|
+
* caller handed {@link Tooltip}, forwarded to that variant's own wrapper. */
|
|
63
|
+
type TooltipVariantProps = Omit<TooltipProps, "side" | "portal"> & { side: TooltipSide };
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Hover/focus label for a control.
|
|
67
|
+
*
|
|
68
|
+
* Two implementations, and the choice matters more than it looks. The default is
|
|
69
|
+
* CSS-only: the bubble is always mounted next to the trigger and fades in on
|
|
70
|
+
* `:hover`, which costs no state and works in a plain render test. The `portal`
|
|
71
|
+
* variant mounts the bubble in `document.body` only while hovered, positioned by
|
|
72
|
+
* measurement.
|
|
73
|
+
*
|
|
74
|
+
* ⚠️ **A bubble that repeats a value has to be redactable.** The consuming app blurs
|
|
75
|
+
* `[data-private]` under a `demo-mode` class on `<html>` — and the portalled bubble is
|
|
76
|
+
* mounted on `document.body`, which is INSIDE that class, so the rule reaches it as
|
|
77
|
+
* long as the bubble is tagged. It is not tagged by default, because most labels are
|
|
78
|
+
* UI strings; pass `redact` on the ones that repeat the user's own data (a truncated
|
|
79
|
+
* payee, an account name, a memo). Getting this wrong is silent: the trigger blurs,
|
|
80
|
+
* the bubble spells the value out on hover.
|
|
81
|
+
*
|
|
82
|
+
* ⚠️ **An empty label renders nothing at all.** `title={payee ?? ""}` is an ordinary
|
|
83
|
+
* shape at a call site that reveals truncated text, and the native attribute answers
|
|
84
|
+
* it by showing no tooltip. A component that faithfully rendered an empty bubble
|
|
85
|
+
* would be a worse `title`, so the emptiness check is here rather than at every call
|
|
86
|
+
* site that could forget it.
|
|
87
|
+
*
|
|
88
|
+
* ⚠️ **The bubble describes its trigger, which means cloning it.** `role="tooltip"` is
|
|
89
|
+
* a name for a box, not a relationship — so for as long as nothing referenced the
|
|
90
|
+
* bubble, the label reached the pointer and nobody else. That is worst on the call
|
|
91
|
+
* sites that need it most: the kit's icon-only buttons, where the tooltip IS the
|
|
92
|
+
* label. `aria-describedby` has to sit on the focusable element, which is the
|
|
93
|
+
* caller's child and not this component's wrapper, so the child is CLONED to carry
|
|
94
|
+
* it. A description the caller already set is appended to, never replaced — a field's
|
|
95
|
+
* error text and its hint bubble both describe it. Children that cannot take props (a
|
|
96
|
+
* fragment, a bare string, several elements) are left exactly as they were.
|
|
97
|
+
*
|
|
98
|
+
* ⚠️ **Escape dismisses it (WCAG 1.4.13).** Anything that appears on hover or focus has
|
|
99
|
+
* to be dismissible without moving the pointer, and a bubble is opaque: it lands over
|
|
100
|
+
* the row, field or figure you were reading, and the only way out of it used to be to
|
|
101
|
+
* point somewhere else — which is precisely what you cannot do when what you need to
|
|
102
|
+
* read is underneath it. The listener is the document's rather than the wrapper's
|
|
103
|
+
* because the pointer opens this with the keyboard focus somewhere else entirely, and
|
|
104
|
+
* it is subscribed only while a bubble is actually up: the CSS variant is always
|
|
105
|
+
* mounted, and a table of forty tooltips must not mean forty keydown listeners.
|
|
106
|
+
*
|
|
107
|
+
* ⚠️ **Inside a scroll container, use `portal`.** An always-mounted bubble is
|
|
108
|
+
* absolutely positioned, but an absolutely positioned descendant still counts
|
|
109
|
+
* towards its scroll-container ancestor's scrollable overflow — so an invisible
|
|
110
|
+
* bubble on a control near the right edge makes the container scroll sideways
|
|
111
|
+
* with nothing to reveal. That is what Keksdose feedback dev#488 reported on the
|
|
112
|
+
* admin roster: 66px of horizontal scroll on a table that fit, 44px of it owed to
|
|
113
|
+
* tooltips nobody could see. The portalled bubble is `position: fixed` and absent
|
|
114
|
+
* until hovered, so it adds no width — and, being outside the container, it also
|
|
115
|
+
* cannot be clipped by it.
|
|
116
|
+
*/
|
|
117
|
+
export function Tooltip({
|
|
118
|
+
label,
|
|
119
|
+
side = "top",
|
|
120
|
+
className,
|
|
121
|
+
portal = false,
|
|
122
|
+
redact = false,
|
|
123
|
+
children,
|
|
124
|
+
...rest
|
|
125
|
+
}: TooltipProps) {
|
|
126
|
+
// No label, no bubble — and no wrapper either, so a conditional tooltip costs the
|
|
127
|
+
// layout nothing on the branch where it does not apply. `...rest` goes with the
|
|
128
|
+
// wrapper on this branch, which is the documented limit of the pass-through: there is
|
|
129
|
+
// no element left to put an attribute on.
|
|
130
|
+
if (isEmptyLabel(label)) return <>{children}</>;
|
|
131
|
+
if (portal) {
|
|
132
|
+
return (
|
|
133
|
+
<PortalTooltip
|
|
134
|
+
label={label}
|
|
135
|
+
side={side}
|
|
136
|
+
className={className}
|
|
137
|
+
redact={redact}
|
|
138
|
+
{...rest}
|
|
139
|
+
>
|
|
140
|
+
{children}
|
|
141
|
+
</PortalTooltip>
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
// Both variants are separate components so that `Tooltip` itself can keep calling NO
|
|
145
|
+
// hooks: the empty-label branch above returns before either of them, and a hook after
|
|
146
|
+
// a conditional return is a hooks-order bug rather than a style violation.
|
|
147
|
+
return (
|
|
148
|
+
<CssTooltip label={label} side={side} className={className} redact={redact} {...rest}>
|
|
149
|
+
{children}
|
|
150
|
+
</CssTooltip>
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** The always-mounted variant: the bubble sits next to the trigger and CSS fades it in.
|
|
155
|
+
*
|
|
156
|
+
* It holds the little state it does for the two things CSS cannot express — which
|
|
157
|
+
* element to point `aria-describedby` at, and Escape — and not for the fade, which is
|
|
158
|
+
* still `group-hover`/`group-focus-within` and still costs a render nothing. */
|
|
159
|
+
function CssTooltip({
|
|
160
|
+
label,
|
|
161
|
+
side,
|
|
162
|
+
className,
|
|
163
|
+
redact,
|
|
164
|
+
children,
|
|
165
|
+
...rest
|
|
166
|
+
}: TooltipVariantProps) {
|
|
167
|
+
const id = useId();
|
|
168
|
+
const [hovered, setHovered] = useState(false);
|
|
169
|
+
const [focused, setFocused] = useState(false);
|
|
170
|
+
const [dismissed, setDismissed] = useState(false);
|
|
171
|
+
useEscapeKey(() => setDismissed(true), (hovered || focused) && !dismissed);
|
|
172
|
+
return (
|
|
173
|
+
<span
|
|
174
|
+
// `...rest` first: the four handlers below are what decides whether a bubble is
|
|
175
|
+
// up, and a caller passing an `onFocus` of its own must not replace them.
|
|
176
|
+
{...rest}
|
|
177
|
+
className={cn("relative inline-flex group/tooltip", className)}
|
|
178
|
+
// These four track WHETHER A BUBBLE IS UP. They activate nothing — the only thing
|
|
179
|
+
// here that can be activated is the caller's child, which keeps every handler it
|
|
180
|
+
// arrived with — so this wrapper needs no role and no key handling of its own.
|
|
181
|
+
// `jsx-a11y/no-static-element-interactions` warns about it all the same, as it
|
|
182
|
+
// already does about the portal variant's identical trigger below; both are left
|
|
183
|
+
// visible rather than silenced, because a rule this package ratchets should be
|
|
184
|
+
// argued with in the backlog and not in a disable comment.
|
|
185
|
+
//
|
|
186
|
+
// Re-armed by the next hover or focus rather than by an effect watching those
|
|
187
|
+
// flags: coming back to a trigger is a fresh request for its label, and an effect
|
|
188
|
+
// would also re-show the bubble under a pointer that never left.
|
|
189
|
+
onMouseEnter={() => {
|
|
190
|
+
setHovered(true);
|
|
191
|
+
setDismissed(false);
|
|
192
|
+
}}
|
|
193
|
+
onMouseLeave={() => setHovered(false)}
|
|
194
|
+
onFocus={() => {
|
|
195
|
+
setFocused(true);
|
|
196
|
+
setDismissed(false);
|
|
197
|
+
}}
|
|
198
|
+
onBlur={() => setFocused(false)}
|
|
199
|
+
>
|
|
200
|
+
{describedBy(children, dismissed ? undefined : id)}
|
|
201
|
+
<span
|
|
202
|
+
id={id}
|
|
203
|
+
role="tooltip"
|
|
204
|
+
// The `hidden` ATTRIBUTE, not an opacity class: dismissing has to take the
|
|
205
|
+
// bubble out of the accessibility tree as well as off the screen, or a screen
|
|
206
|
+
// reader still reads out the description of a bubble the user just closed.
|
|
207
|
+
hidden={dismissed || undefined}
|
|
208
|
+
data-private={redact ? "" : undefined}
|
|
209
|
+
className={cn(
|
|
210
|
+
TOOLTIP_SURFACE,
|
|
211
|
+
"pointer-events-none absolute z-50 opacity-0 group-hover/tooltip:opacity-100 group-focus-within/tooltip:opacity-100",
|
|
212
|
+
sidePositionClass[side],
|
|
213
|
+
)}
|
|
214
|
+
>
|
|
215
|
+
{label}
|
|
216
|
+
</span>
|
|
217
|
+
</span>
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/** Hand `children` the bubble's id as an `aria-describedby`, if it is an element that
|
|
222
|
+
* can hold one. `id` is undefined while there is no bubble to point at — a dangling
|
|
223
|
+
* reference describes the trigger as nothing at all, which is worse than silence. */
|
|
224
|
+
function describedBy(children: ReactNode, id: string | undefined): ReactNode {
|
|
225
|
+
if (id === undefined || !isValidElement(children)) return children;
|
|
226
|
+
const child = children as ReactElement<{ "aria-describedby"?: string }>;
|
|
227
|
+
// Fragments, Suspense and friends are symbol-typed and take no DOM props; cloning one
|
|
228
|
+
// with an aria attribute warns in development and drops it in production.
|
|
229
|
+
if (typeof child.type === "symbol") return children;
|
|
230
|
+
const own = child.props["aria-describedby"];
|
|
231
|
+
return cloneElement(child, { "aria-describedby": own ? `${own} ${id}` : id });
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/** "Would this bubble be blank." Only the values a call site actually produces when
|
|
235
|
+
* it has nothing to say — `""`, `null`, `undefined`, `false` from a `&&` guard. A
|
|
236
|
+
* numeric `0` is a real label and stays one. */
|
|
237
|
+
function isEmptyLabel(label: ReactNode): boolean {
|
|
238
|
+
return (
|
|
239
|
+
label == null ||
|
|
240
|
+
label === false ||
|
|
241
|
+
(typeof label === "string" && label.trim() === "")
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const TOOLTIP_GAP = 4;
|
|
246
|
+
|
|
247
|
+
/** How close to the viewport edge a bubble may sit. Not zero: a label flush
|
|
248
|
+
* against the glass reads as clipped even when every character is on screen. */
|
|
249
|
+
const TOOLTIP_MARGIN = 4;
|
|
250
|
+
|
|
251
|
+
const portalTransformBySide: Record<TooltipSide, string> = {
|
|
252
|
+
right: "translate(0, -50%)",
|
|
253
|
+
left: "translate(-100%, -50%)",
|
|
254
|
+
top: "translate(-50%, -100%)",
|
|
255
|
+
bottom: "translate(-50%, 0)",
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
/** Anchor point (viewport px) for the tooltip on the given side of `r`. Paired
|
|
259
|
+
* with {@link portalTransformBySide}, which shifts the box onto that point. */
|
|
260
|
+
function tooltipAnchor(
|
|
261
|
+
r: AnchorRect,
|
|
262
|
+
side: TooltipSide,
|
|
263
|
+
): { left: number; top: number } {
|
|
264
|
+
switch (side) {
|
|
265
|
+
case "right":
|
|
266
|
+
return { left: r.right + TOOLTIP_GAP, top: r.top + r.height / 2 };
|
|
267
|
+
case "left":
|
|
268
|
+
return { left: r.left - TOOLTIP_GAP, top: r.top + r.height / 2 };
|
|
269
|
+
case "top":
|
|
270
|
+
return { left: r.left + r.width / 2, top: r.top - TOOLTIP_GAP };
|
|
271
|
+
case "bottom":
|
|
272
|
+
return { left: r.left + r.width / 2, top: r.bottom + TOOLTIP_GAP };
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
export interface TooltipSize {
|
|
277
|
+
width: number;
|
|
278
|
+
height: number;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
export interface TooltipViewport {
|
|
282
|
+
width: number;
|
|
283
|
+
height: number;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
export interface TooltipPlacement {
|
|
287
|
+
left: number;
|
|
288
|
+
top: number;
|
|
289
|
+
/** Which side it ended up on, which need not be the one that was asked for. */
|
|
290
|
+
side: TooltipSide;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const opposite: Record<TooltipSide, TooltipSide> = {
|
|
294
|
+
left: "right",
|
|
295
|
+
right: "left",
|
|
296
|
+
top: "bottom",
|
|
297
|
+
bottom: "top",
|
|
298
|
+
};
|
|
299
|
+
|
|
300
|
+
/** Whether the bubble clears the viewport edge on `side` of the trigger. */
|
|
301
|
+
function roomOn(
|
|
302
|
+
r: AnchorRect,
|
|
303
|
+
side: TooltipSide,
|
|
304
|
+
size: TooltipSize,
|
|
305
|
+
viewport: TooltipViewport,
|
|
306
|
+
): boolean {
|
|
307
|
+
switch (side) {
|
|
308
|
+
case "left":
|
|
309
|
+
return r.left - TOOLTIP_GAP - size.width >= TOOLTIP_MARGIN;
|
|
310
|
+
case "right":
|
|
311
|
+
return r.right + TOOLTIP_GAP + size.width <= viewport.width - TOOLTIP_MARGIN;
|
|
312
|
+
case "top":
|
|
313
|
+
return r.top - TOOLTIP_GAP - size.height >= TOOLTIP_MARGIN;
|
|
314
|
+
case "bottom":
|
|
315
|
+
return r.bottom + TOOLTIP_GAP + size.height <= viewport.height - TOOLTIP_MARGIN;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function sameRoom(
|
|
320
|
+
a: { size: TooltipSize; viewport: TooltipViewport },
|
|
321
|
+
b: { size: TooltipSize; viewport: TooltipViewport },
|
|
322
|
+
): boolean {
|
|
323
|
+
return (
|
|
324
|
+
a.size.width === b.size.width &&
|
|
325
|
+
a.size.height === b.size.height &&
|
|
326
|
+
a.viewport.width === b.viewport.width &&
|
|
327
|
+
a.viewport.height === b.viewport.height
|
|
328
|
+
);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function clamp(value: number, low: number, high: number): number {
|
|
332
|
+
// `high` first, so a bubble taller or wider than the viewport is pinned to the
|
|
333
|
+
// top-left corner rather than to the bottom-right one — the start of a label
|
|
334
|
+
// is the half worth keeping.
|
|
335
|
+
return Math.max(low, Math.min(value, high));
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Where the bubble actually goes, given how big it turned out to be.
|
|
340
|
+
*
|
|
341
|
+
* Two rules, and they are separate because they fix separate failures.
|
|
342
|
+
*
|
|
343
|
+
* **Turn round when the preferred side has no room.** `side` says which side of
|
|
344
|
+
* the trigger the label reads best on, and on a form near the left edge of the
|
|
345
|
+
* window that side is off the screen — the capped bubble can only wrap, not
|
|
346
|
+
* move, so what the reader gets is a sentence with its first half outside the
|
|
347
|
+
* glass. Flipped only when the *other* side is genuinely better: a trigger in a
|
|
348
|
+
* viewport too narrow for the bubble either way keeps the side it asked for, and
|
|
349
|
+
* the clamp below does what it can.
|
|
350
|
+
*
|
|
351
|
+
* **Then clamp both axes.** The cross axis is the one that needs it — a `top`
|
|
352
|
+
* bubble is centred on the trigger, so a trigger near the left edge pushes half
|
|
353
|
+
* the label off even though the side it is on is right — and clamping the main
|
|
354
|
+
* axis too costs nothing and covers the flip having nowhere to land.
|
|
355
|
+
*
|
|
356
|
+
* Pure, and measured in viewport pixels throughout, so it can be tested without
|
|
357
|
+
* a layout: the caller supplies the trigger's rect, the bubble's own size and
|
|
358
|
+
* the window.
|
|
359
|
+
*/
|
|
360
|
+
export function placeTooltip(
|
|
361
|
+
r: AnchorRect,
|
|
362
|
+
side: TooltipSide,
|
|
363
|
+
size: TooltipSize,
|
|
364
|
+
viewport: TooltipViewport,
|
|
365
|
+
): TooltipPlacement {
|
|
366
|
+
const chosen =
|
|
367
|
+
roomOn(r, side, size, viewport) || !roomOn(r, opposite[side], size, viewport)
|
|
368
|
+
? side
|
|
369
|
+
: opposite[side];
|
|
370
|
+
const point = tooltipAnchor(r, chosen);
|
|
371
|
+
const box =
|
|
372
|
+
chosen === "left"
|
|
373
|
+
? { left: point.left - size.width, top: point.top - size.height / 2 }
|
|
374
|
+
: chosen === "right"
|
|
375
|
+
? { left: point.left, top: point.top - size.height / 2 }
|
|
376
|
+
: chosen === "top"
|
|
377
|
+
? { left: point.left - size.width / 2, top: point.top - size.height }
|
|
378
|
+
: { left: point.left - size.width / 2, top: point.top };
|
|
379
|
+
return {
|
|
380
|
+
left: clamp(box.left, TOOLTIP_MARGIN, viewport.width - size.width - TOOLTIP_MARGIN),
|
|
381
|
+
top: clamp(box.top, TOOLTIP_MARGIN, viewport.height - size.height - TOOLTIP_MARGIN),
|
|
382
|
+
side: chosen,
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function PortalTooltip({
|
|
387
|
+
label,
|
|
388
|
+
side,
|
|
389
|
+
className,
|
|
390
|
+
redact,
|
|
391
|
+
children,
|
|
392
|
+
...rest
|
|
393
|
+
}: TooltipVariantProps) {
|
|
394
|
+
const triggerRef = useRef<HTMLSpanElement | null>(null);
|
|
395
|
+
const bubbleRef = useRef<HTMLSpanElement | null>(null);
|
|
396
|
+
const [visible, setVisible] = useState(false);
|
|
397
|
+
const id = useId();
|
|
398
|
+
// Escape closes it outright, since this variant's bubble only exists while it is
|
|
399
|
+
// shown. The next mouseenter/focus brings it back, which is the behaviour WCAG
|
|
400
|
+
// 1.4.13 asks for: dismissible now, still available when you ask again.
|
|
401
|
+
useEscapeKey(() => setVisible(false), visible);
|
|
402
|
+
// The measure + scroll/resize-tracking lifecycle is owned by useAnchoredRect;
|
|
403
|
+
// here we only map the rect to a side-specific anchor point.
|
|
404
|
+
const rect = useAnchoredRect(triggerRef, visible);
|
|
405
|
+
// The bubble's own size and the window it has to fit in — neither of which is
|
|
406
|
+
// knowable in render: the width is whatever the label wrapped to inside the
|
|
407
|
+
// cap, and reading `window` while rendering is not a pure thing to do. Both
|
|
408
|
+
// are taken in a LAYOUT effect, so the correction lands before the browser
|
|
409
|
+
// paints and there is no frame in which the label sits off the screen.
|
|
410
|
+
const [room, setRoom] = useState<{ size: TooltipSize; viewport: TooltipViewport } | null>(null);
|
|
411
|
+
useLayoutEffect(() => {
|
|
412
|
+
const measured = visible ? bubbleRef.current?.getBoundingClientRect() : undefined;
|
|
413
|
+
// eslint-disable-next-line react-hooks/set-state-in-effect -- a measurement is the one thing a layout effect is for
|
|
414
|
+
setRoom((previous) => {
|
|
415
|
+
if (!measured) return null;
|
|
416
|
+
const next = {
|
|
417
|
+
size: { width: measured.width, height: measured.height },
|
|
418
|
+
viewport: { width: window.innerWidth, height: window.innerHeight },
|
|
419
|
+
};
|
|
420
|
+
// Only publish what actually CHANGED: every re-measure allocates a fresh
|
|
421
|
+
// object, and a new object on every scroll event would re-render the
|
|
422
|
+
// bubble forever.
|
|
423
|
+
return previous && sameRoom(previous, next) ? previous : next;
|
|
424
|
+
});
|
|
425
|
+
}, [visible, rect, label]);
|
|
426
|
+
|
|
427
|
+
const point = rect ? tooltipAnchor(rect, side) : null;
|
|
428
|
+
// Unmeasured on the very first pass, where the anchor point plus the side's
|
|
429
|
+
// own transform is exactly what this always did. One layout effect later the
|
|
430
|
+
// size is known and the placement is decided properly.
|
|
431
|
+
const placed = rect && room ? placeTooltip(rect, side, room.size, room.viewport) : null;
|
|
432
|
+
|
|
433
|
+
return (
|
|
434
|
+
<>
|
|
435
|
+
<span
|
|
436
|
+
// As in `CssTooltip`: the caller's attributes first, the four handlers that run
|
|
437
|
+
// this component after them. The BUBBLE is deliberately not given them — it is
|
|
438
|
+
// portalled to `<body>`, and an id or a tour anchor duplicated onto a node that
|
|
439
|
+
// only exists while hovered would match twice or match nothing.
|
|
440
|
+
{...rest}
|
|
441
|
+
ref={triggerRef}
|
|
442
|
+
className={cn("relative inline-flex", className)}
|
|
443
|
+
onMouseEnter={() => setVisible(true)}
|
|
444
|
+
onMouseLeave={() => setVisible(false)}
|
|
445
|
+
onFocus={() => setVisible(true)}
|
|
446
|
+
onBlur={() => setVisible(false)}
|
|
447
|
+
>
|
|
448
|
+
{describedBy(children, visible ? id : undefined)}
|
|
449
|
+
</span>
|
|
450
|
+
{visible &&
|
|
451
|
+
point &&
|
|
452
|
+
typeof document !== "undefined" &&
|
|
453
|
+
createPortal(
|
|
454
|
+
<span
|
|
455
|
+
ref={bubbleRef}
|
|
456
|
+
id={id}
|
|
457
|
+
role="tooltip"
|
|
458
|
+
data-private={redact ? "" : undefined}
|
|
459
|
+
style={
|
|
460
|
+
placed
|
|
461
|
+
? { position: "fixed", left: placed.left, top: placed.top }
|
|
462
|
+
: {
|
|
463
|
+
position: "fixed",
|
|
464
|
+
left: point.left,
|
|
465
|
+
top: point.top,
|
|
466
|
+
transform: portalTransformBySide[side],
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
className={cn(TOOLTIP_SURFACE, "pointer-events-none z-50")}
|
|
470
|
+
>
|
|
471
|
+
{label}
|
|
472
|
+
</span>,
|
|
473
|
+
document.body,
|
|
474
|
+
)}
|
|
475
|
+
</>
|
|
476
|
+
);
|
|
477
|
+
}
|