@behivetech/atoms.tooltip 0.0.0-stage → 0.2.2

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/README.md CHANGED
@@ -1,3 +1,46 @@
1
- # Temporary Holding Version
1
+ # Tooltip
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Names a control on hover and on keyboard focus, built on
4
+ [`@radix-ui/react-tooltip`](https://www.radix-ui.com/primitives/docs/components/tooltip).
5
+
6
+ Intended for icon-only controls, where the tooltip is the only thing naming the
7
+ control. It replaces the `title` attribute, which delays about a second, is
8
+ OS-styled with no theming, has no touch support, and gives no control over
9
+ placement.
10
+
11
+ ## Usage
12
+
13
+ ```jsx
14
+ import { Tooltip } from "@behivetech/atoms.tooltip";
15
+ import { BoxIcon } from "@behivetech/atoms.icon";
16
+
17
+ <Tooltip label="Rectangle" side="right">
18
+ <button type="button">
19
+ <BoxIcon />
20
+ </button>
21
+ </Tooltip>;
22
+ ```
23
+
24
+ The child is rendered as-is via Radix's `asChild`, so the trigger stays your own
25
+ element. `Tooltip` mounts its own provider — you do not need to add one.
26
+
27
+ ## Disabled triggers
28
+
29
+ A disabled element fires no pointer events, so a bare Radix tooltip never opens
30
+ on one. That is the moment a tooltip matters most: when a control is unavailable
31
+ and the user wants to know why.
32
+
33
+ `Tooltip` handles it. When the child is `disabled` it is wrapped in a focusable
34
+ span that carries the listeners, and the child is made pointer-transparent so
35
+ hover lands on the wrapper. The control stays disabled, keeps its own accessible
36
+ name, and the explanation is reachable by keyboard and screen reader.
37
+
38
+ ```jsx
39
+ <Tooltip label="You do not have permission to edit roads">
40
+ <button type="button" disabled>
41
+ <RoadIcon />
42
+ </button>
43
+ </Tooltip>
44
+ ```
45
+
46
+ No wrapper is added when the trigger is enabled.
package/dist/index.cjs ADDED
@@ -0,0 +1,198 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __commonJS = (cb, mod) => function __require() {
9
+ return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
10
+ };
11
+ var __export = (target, all) => {
12
+ for (var name in all)
13
+ __defProp(target, name, { get: all[name], enumerable: true });
14
+ };
15
+ var __copyProps = (to, from, except, desc) => {
16
+ if (from && typeof from === "object" || typeof from === "function") {
17
+ for (let key of __getOwnPropNames(from))
18
+ if (!__hasOwnProp.call(to, key) && key !== except)
19
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
20
+ }
21
+ return to;
22
+ };
23
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
24
+ // If the importer is in node compatibility mode or this is not an ESM
25
+ // file that has been converted to a CommonJS file using a Babel-
26
+ // compatible transform (i.e. "__esModule" has not been set), then set
27
+ // "default" to the CommonJS "module.exports" for node compatibility.
28
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
29
+ mod
30
+ ));
31
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
32
+
33
+ // ../../../../node_modules/.pnpm/classnames@2.5.1/node_modules/classnames/index.js
34
+ var require_classnames = __commonJS({
35
+ "../../../../node_modules/.pnpm/classnames@2.5.1/node_modules/classnames/index.js"(exports2, module2) {
36
+ "use strict";
37
+ (function() {
38
+ "use strict";
39
+ var hasOwn = {}.hasOwnProperty;
40
+ function classNames() {
41
+ var classes = "";
42
+ for (var i = 0; i < arguments.length; i++) {
43
+ var arg = arguments[i];
44
+ if (arg) {
45
+ classes = appendClass(classes, parseValue(arg));
46
+ }
47
+ }
48
+ return classes;
49
+ }
50
+ function parseValue(arg) {
51
+ if (typeof arg === "string" || typeof arg === "number") {
52
+ return arg;
53
+ }
54
+ if (typeof arg !== "object") {
55
+ return "";
56
+ }
57
+ if (Array.isArray(arg)) {
58
+ return classNames.apply(null, arg);
59
+ }
60
+ if (arg.toString !== Object.prototype.toString && !arg.toString.toString().includes("[native code]")) {
61
+ return arg.toString();
62
+ }
63
+ var classes = "";
64
+ for (var key in arg) {
65
+ if (hasOwn.call(arg, key) && arg[key]) {
66
+ classes = appendClass(classes, key);
67
+ }
68
+ }
69
+ return classes;
70
+ }
71
+ function appendClass(value, newClass) {
72
+ if (!newClass) {
73
+ return value;
74
+ }
75
+ if (value) {
76
+ return value + " " + newClass;
77
+ }
78
+ return value + newClass;
79
+ }
80
+ if (typeof module2 !== "undefined" && module2.exports) {
81
+ classNames.default = classNames;
82
+ module2.exports = classNames;
83
+ } else if (typeof define === "function" && typeof define.amd === "object" && define.amd) {
84
+ define("classnames", [], function() {
85
+ return classNames;
86
+ });
87
+ } else {
88
+ window.classNames = classNames;
89
+ }
90
+ })();
91
+ }
92
+ });
93
+
94
+ // index.ts
95
+ var index_exports = {};
96
+ __export(index_exports, {
97
+ Tooltip: () => Tooltip
98
+ });
99
+ module.exports = __toCommonJS(index_exports);
100
+
101
+ // tooltip.tsx
102
+ var import_react = require("react");
103
+ var RadixTooltip = __toESM(require("@radix-ui/react-tooltip"), 1);
104
+
105
+ // ../../../../packages/registry/class-names/dist/index.js
106
+ var import_classnames = __toESM(require_classnames(), 1);
107
+ var getClassName = ({
108
+ className,
109
+ modifiers = {},
110
+ rootClass,
111
+ styles = {}
112
+ }) => {
113
+ function getModifiers() {
114
+ const updatedModifiers = {};
115
+ const modifierKeys = Object.keys(modifiers);
116
+ if (modifierKeys.length) {
117
+ modifierKeys.forEach((modKey) => {
118
+ const classKey = `${rootClass}--${modKey}`;
119
+ const newClassName = styles[classKey] || classKey;
120
+ updatedModifiers[newClassName] = modifiers[modKey] ?? false;
121
+ });
122
+ }
123
+ return updatedModifiers;
124
+ }
125
+ function getChildClass(childClassName) {
126
+ const classKey = `${rootClass}__${childClassName}`;
127
+ return styles[classKey] || classKey;
128
+ }
129
+ function getRootStyles() {
130
+ return (0, import_classnames.default)(
131
+ className,
132
+ {
133
+ ...getModifiers()
134
+ },
135
+ styles[rootClass] || rootClass
136
+ );
137
+ }
138
+ return [getRootStyles(), getChildClass];
139
+ };
140
+
141
+ // tooltip.module.scss
142
+ var tooltip_module_default = {
143
+ "BHT__Tooltip__content": "BHT__Tooltip__content",
144
+ "tooltip-in": "tooltip-in",
145
+ "BHT__Tooltip__arrow": "BHT__Tooltip__arrow",
146
+ "BHT__Tooltip__disabledTrigger": "BHT__Tooltip__disabledTrigger"
147
+ };
148
+
149
+ // tooltip.tsx
150
+ var import_jsx_runtime = require("react/jsx-runtime");
151
+ var DELAY_DURATION = 300;
152
+ function isDisabled(element) {
153
+ if (!(0, import_react.isValidElement)(element)) return false;
154
+ const { disabled } = element.props;
155
+ return disabled === true;
156
+ }
157
+ var Tooltip = ({
158
+ label,
159
+ side = "top",
160
+ children,
161
+ className
162
+ }) => {
163
+ const [, getChildClass] = getClassName({
164
+ className,
165
+ rootClass: "BHT__Tooltip",
166
+ styles: tooltip_module_default
167
+ });
168
+ const disabled = isDisabled(children);
169
+ const trigger = disabled ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: getChildClass("disabledTrigger"), tabIndex: 0, children }) : children;
170
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(RadixTooltip.Provider, { delayDuration: DELAY_DURATION, children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(RadixTooltip.Root, { children: [
171
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(RadixTooltip.Trigger, { asChild: true, children: trigger }),
172
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(RadixTooltip.Portal, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
173
+ RadixTooltip.Content,
174
+ {
175
+ className: getChildClass("content"),
176
+ side,
177
+ sideOffset: 6,
178
+ children: [
179
+ label,
180
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(RadixTooltip.Arrow, { className: getChildClass("arrow") })
181
+ ]
182
+ }
183
+ ) })
184
+ ] }) });
185
+ };
186
+ // Annotate the CommonJS export names for ESM import in node:
187
+ 0 && (module.exports = {
188
+ Tooltip
189
+ });
190
+ /*! Bundled license information:
191
+
192
+ classnames/index.js:
193
+ (*!
194
+ Copyright (c) 2018 Jed Watson.
195
+ Licensed under the MIT License (MIT), see
196
+ http://jedwatson.github.io/classnames
197
+ *)
198
+ */
package/dist/index.css ADDED
@@ -0,0 +1,39 @@
1
+ /* esbuild-sass-plugin:css-chunktooltip.module.scss */
2
+ .BHT__Tooltip__content {
3
+ padding: var(--spacing-1, 0.25rem) var(--spacing-2, 0.5rem);
4
+ background: var(--md-sys-color-inverse-surface, var(--color-on-surface));
5
+ color: var(--md-sys-color-inverse-on-surface, var(--color-surface));
6
+ border-radius: var(--md-sys-shape-corner-extra-small, 4px);
7
+ font-family: var(--md-sys-typescale-body-small-font, var(--font-body));
8
+ font-size: var(--md-sys-typescale-body-small-size, 0.75rem);
9
+ font-weight: var(--md-sys-typescale-body-small-weight, 400);
10
+ line-height: var(--md-sys-typescale-body-small-line-height, 1rem);
11
+ letter-spacing: var(--md-sys-typescale-body-small-tracking, 0.0333em);
12
+ max-width: 16rem;
13
+ z-index: var(--z-tooltip, 600);
14
+ user-select: none;
15
+ }
16
+ .BHT__Tooltip__content[data-state=delayed-open] {
17
+ animation: tooltip-in var(--md-sys-motion-duration-short-3, 150ms) var(--md-sys-motion-easing-standard-decelerate, ease-out);
18
+ }
19
+ .BHT__Tooltip__arrow {
20
+ fill: var(--md-sys-color-inverse-surface, var(--color-on-surface));
21
+ }
22
+ .BHT__Tooltip__disabledTrigger {
23
+ display: inline-flex;
24
+ }
25
+ .BHT__Tooltip__disabledTrigger > * {
26
+ pointer-events: none;
27
+ }
28
+ .BHT__Tooltip__disabledTrigger:focus-visible {
29
+ outline: 3px solid var(--md-sys-color-secondary, var(--color-border-focus));
30
+ outline-offset: 2px;
31
+ }
32
+ @keyframes tooltip-in {
33
+ from {
34
+ opacity: 0;
35
+ }
36
+ to {
37
+ opacity: 1;
38
+ }
39
+ }
@@ -0,0 +1,30 @@
1
+ import * as react from 'react';
2
+ import { ReactElement } from 'react';
3
+
4
+ type TooltipSide = "top" | "right" | "bottom" | "left";
5
+ interface TooltipProps {
6
+ /** Text shown in the tooltip. This is the control's name when the trigger is icon-only. */
7
+ label: string;
8
+ /** Which side of the trigger to place the tooltip on; defaults to "top" */
9
+ side?: TooltipSide;
10
+ /** The element the tooltip describes — rendered as-is via Radix's `asChild` */
11
+ children: ReactElement;
12
+ /** Additional class names to merge with the tooltip content element */
13
+ className?: string;
14
+ }
15
+ /**
16
+ * Names a control on hover and on keyboard focus, built on Radix's `Tooltip`.
17
+ *
18
+ * Mounts its own `Tooltip.Provider`, so a caller never has to add one.
19
+ *
20
+ * Works on a disabled trigger, which a bare Radix tooltip does not: a disabled
21
+ * element fires no pointer events, so the trigger never sees hover or focus.
22
+ * When the child is disabled it is wrapped in a focusable span that carries the
23
+ * listeners instead, and the child is made pointer-transparent so hover lands
24
+ * on the wrapper. That keeps the explanation reachable at the one moment it
25
+ * matters most — when the control is unavailable and the user wants to know
26
+ * why — including by keyboard, since a disabled control is not focusable.
27
+ */
28
+ declare const Tooltip: ({ label, side, children, className, }: TooltipProps) => react.JSX.Element;
29
+
30
+ export { Tooltip, type TooltipProps, type TooltipSide };
package/dist/index.js ADDED
@@ -0,0 +1,184 @@
1
+ var __create = Object.create;
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __getProtoOf = Object.getPrototypeOf;
6
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
7
+ var __commonJS = (cb, mod) => function __require() {
8
+ return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
19
+ // If the importer is in node compatibility mode or this is not an ESM
20
+ // file that has been converted to a CommonJS file using a Babel-
21
+ // compatible transform (i.e. "__esModule" has not been set), then set
22
+ // "default" to the CommonJS "module.exports" for node compatibility.
23
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
24
+ mod
25
+ ));
26
+
27
+ // ../../../../node_modules/.pnpm/classnames@2.5.1/node_modules/classnames/index.js
28
+ var require_classnames = __commonJS({
29
+ "../../../../node_modules/.pnpm/classnames@2.5.1/node_modules/classnames/index.js"(exports, module) {
30
+ "use strict";
31
+ (function() {
32
+ "use strict";
33
+ var hasOwn = {}.hasOwnProperty;
34
+ function classNames() {
35
+ var classes = "";
36
+ for (var i = 0; i < arguments.length; i++) {
37
+ var arg = arguments[i];
38
+ if (arg) {
39
+ classes = appendClass(classes, parseValue(arg));
40
+ }
41
+ }
42
+ return classes;
43
+ }
44
+ function parseValue(arg) {
45
+ if (typeof arg === "string" || typeof arg === "number") {
46
+ return arg;
47
+ }
48
+ if (typeof arg !== "object") {
49
+ return "";
50
+ }
51
+ if (Array.isArray(arg)) {
52
+ return classNames.apply(null, arg);
53
+ }
54
+ if (arg.toString !== Object.prototype.toString && !arg.toString.toString().includes("[native code]")) {
55
+ return arg.toString();
56
+ }
57
+ var classes = "";
58
+ for (var key in arg) {
59
+ if (hasOwn.call(arg, key) && arg[key]) {
60
+ classes = appendClass(classes, key);
61
+ }
62
+ }
63
+ return classes;
64
+ }
65
+ function appendClass(value, newClass) {
66
+ if (!newClass) {
67
+ return value;
68
+ }
69
+ if (value) {
70
+ return value + " " + newClass;
71
+ }
72
+ return value + newClass;
73
+ }
74
+ if (typeof module !== "undefined" && module.exports) {
75
+ classNames.default = classNames;
76
+ module.exports = classNames;
77
+ } else if (typeof define === "function" && typeof define.amd === "object" && define.amd) {
78
+ define("classnames", [], function() {
79
+ return classNames;
80
+ });
81
+ } else {
82
+ window.classNames = classNames;
83
+ }
84
+ })();
85
+ }
86
+ });
87
+
88
+ // tooltip.tsx
89
+ import { isValidElement } from "react";
90
+ import * as RadixTooltip from "@radix-ui/react-tooltip";
91
+
92
+ // ../../../../packages/registry/class-names/dist/index.js
93
+ var import_classnames = __toESM(require_classnames(), 1);
94
+ var getClassName = ({
95
+ className,
96
+ modifiers = {},
97
+ rootClass,
98
+ styles = {}
99
+ }) => {
100
+ function getModifiers() {
101
+ const updatedModifiers = {};
102
+ const modifierKeys = Object.keys(modifiers);
103
+ if (modifierKeys.length) {
104
+ modifierKeys.forEach((modKey) => {
105
+ const classKey = `${rootClass}--${modKey}`;
106
+ const newClassName = styles[classKey] || classKey;
107
+ updatedModifiers[newClassName] = modifiers[modKey] ?? false;
108
+ });
109
+ }
110
+ return updatedModifiers;
111
+ }
112
+ function getChildClass(childClassName) {
113
+ const classKey = `${rootClass}__${childClassName}`;
114
+ return styles[classKey] || classKey;
115
+ }
116
+ function getRootStyles() {
117
+ return (0, import_classnames.default)(
118
+ className,
119
+ {
120
+ ...getModifiers()
121
+ },
122
+ styles[rootClass] || rootClass
123
+ );
124
+ }
125
+ return [getRootStyles(), getChildClass];
126
+ };
127
+
128
+ // tooltip.module.scss
129
+ var tooltip_module_default = {
130
+ "BHT__Tooltip__content": "BHT__Tooltip__content",
131
+ "tooltip-in": "tooltip-in",
132
+ "BHT__Tooltip__arrow": "BHT__Tooltip__arrow",
133
+ "BHT__Tooltip__disabledTrigger": "BHT__Tooltip__disabledTrigger"
134
+ };
135
+
136
+ // tooltip.tsx
137
+ import { jsx, jsxs } from "react/jsx-runtime";
138
+ var DELAY_DURATION = 300;
139
+ function isDisabled(element) {
140
+ if (!isValidElement(element)) return false;
141
+ const { disabled } = element.props;
142
+ return disabled === true;
143
+ }
144
+ var Tooltip = ({
145
+ label,
146
+ side = "top",
147
+ children,
148
+ className
149
+ }) => {
150
+ const [, getChildClass] = getClassName({
151
+ className,
152
+ rootClass: "BHT__Tooltip",
153
+ styles: tooltip_module_default
154
+ });
155
+ const disabled = isDisabled(children);
156
+ const trigger = disabled ? /* @__PURE__ */ jsx("span", { className: getChildClass("disabledTrigger"), tabIndex: 0, children }) : children;
157
+ return /* @__PURE__ */ jsx(RadixTooltip.Provider, { delayDuration: DELAY_DURATION, children: /* @__PURE__ */ jsxs(RadixTooltip.Root, { children: [
158
+ /* @__PURE__ */ jsx(RadixTooltip.Trigger, { asChild: true, children: trigger }),
159
+ /* @__PURE__ */ jsx(RadixTooltip.Portal, { children: /* @__PURE__ */ jsxs(
160
+ RadixTooltip.Content,
161
+ {
162
+ className: getChildClass("content"),
163
+ side,
164
+ sideOffset: 6,
165
+ children: [
166
+ label,
167
+ /* @__PURE__ */ jsx(RadixTooltip.Arrow, { className: getChildClass("arrow") })
168
+ ]
169
+ }
170
+ ) })
171
+ ] }) });
172
+ };
173
+ export {
174
+ Tooltip
175
+ };
176
+ /*! Bundled license information:
177
+
178
+ classnames/index.js:
179
+ (*!
180
+ Copyright (c) 2018 Jed Watson.
181
+ Licensed under the MIT License (MIT), see
182
+ http://jedwatson.github.io/classnames
183
+ *)
184
+ */
package/docs.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "$schema": "https://github.com/behivetech/bht-component-registry/docs.schema.json",
3
+ "version": 2,
4
+ "generatedAt": "2026-10-20T05:49:21.196Z",
5
+ "docs": [
6
+ {
7
+ "displayName": "Tooltip",
8
+ "description": "Names a control on hover and on keyboard focus, built on Radix's `Tooltip`.\n\nMounts its own `Tooltip.Provider`, so a caller never has to add one.\n\nWorks on a disabled trigger, which a bare Radix tooltip does not: a disabled\nelement fires no pointer events, so the trigger never sees hover or focus.\nWhen the child is disabled it is wrapped in a focusable span that carries the\nlisteners instead, and the child is made pointer-transparent so hover lands\non the wrapper. That keeps the explanation reachable at the one moment it\nmatters most — when the control is unavailable and the user wants to know\nwhy — including by keyboard, since a disabled control is not focusable.",
9
+ "extendsNative": false,
10
+ "props": [
11
+ {
12
+ "name": "children",
13
+ "type": "ReactElement<unknown, string | JSXElementConstructor<any>>",
14
+ "required": true,
15
+ "defaultValue": null,
16
+ "description": "The element the tooltip describes — rendered as-is via Radix's `asChild`"
17
+ },
18
+ {
19
+ "name": "label",
20
+ "type": "string",
21
+ "required": true,
22
+ "defaultValue": null,
23
+ "description": "Text shown in the tooltip. This is the control's name when the trigger is icon-only."
24
+ },
25
+ {
26
+ "name": "className",
27
+ "type": "string",
28
+ "required": false,
29
+ "defaultValue": null,
30
+ "description": "Additional class names to merge with the tooltip content element"
31
+ },
32
+ {
33
+ "name": "side",
34
+ "type": "\"top\" | \"right\" | \"bottom\" | \"left\"",
35
+ "required": false,
36
+ "defaultValue": "top",
37
+ "description": "Which side of the trigger to place the tooltip on; defaults to \"top\""
38
+ }
39
+ ]
40
+ }
41
+ ],
42
+ "functions": []
43
+ }
package/index.ts ADDED
@@ -0,0 +1,2 @@
1
+ export { Tooltip } from "./tooltip.js";
2
+ export type { TooltipProps, TooltipSide } from "./tooltip.js";
package/package.json CHANGED
@@ -1,6 +1,62 @@
1
1
  {
2
2
  "name": "@behivetech/atoms.tooltip",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.2.2",
4
+ "license": "MIT",
5
+ "type": "module",
6
+ "private": false,
7
+ "exports": {
8
+ ".": {
9
+ "types": "./dist/index.d.ts",
10
+ "import": "./dist/index.js",
11
+ "require": "./dist/index.cjs"
12
+ },
13
+ "./styles.css": "./dist/index.css"
14
+ },
15
+ "files": [
16
+ "dist",
17
+ "README.md",
18
+ "index.ts",
19
+ "tooltip.tsx",
20
+ "tooltip.module.scss",
21
+ "*.example.tsx",
22
+ "docs.json"
23
+ ],
24
+ "registry": {
25
+ "status": "stable",
26
+ "tags": []
27
+ },
28
+ "dependencies": {
29
+ "@radix-ui/react-tooltip": "^1.2.16"
30
+ },
31
+ "peerDependencies": {
32
+ "react": "^19.2.7",
33
+ "react-dom": "^19.2.7"
34
+ },
35
+ "devDependencies": {
36
+ "@types/react": "19.2.17",
37
+ "@types/react-dom": "19.2.3",
38
+ "eslint": "^9.39.5",
39
+ "tsup": "^8.5.1",
40
+ "typescript": "^6.0.3",
41
+ "vitest": "^4.1.10",
42
+ "@bht-component-registry/class-names": "0.0.6",
43
+ "@bht-component-registry/env": "0.1.0"
44
+ },
45
+ "repository": {
46
+ "type": "git",
47
+ "url": "https://github.com/behivetech/bht-galaxy.git",
48
+ "directory": "registry/behivetech/atoms/tooltip"
49
+ },
50
+ "publishConfig": {
51
+ "registry": "https://registry.npmjs.org",
52
+ "access": "public",
53
+ "provenance": true
54
+ },
55
+ "scripts": {
56
+ "build": "tsup && bht-registry-docgen",
57
+ "dev": "tsup --watch",
58
+ "lint": "eslint . --max-warnings 0",
59
+ "check-types": "tsc --noEmit",
60
+ "test": "vitest run"
61
+ }
6
62
  }
@@ -0,0 +1,33 @@
1
+ import { Tooltip } from "./tooltip.js";
2
+
3
+ export const BasicTooltip = () => (
4
+ <Tooltip label="Rectangle">
5
+ <button type="button">Rect</button>
6
+ </Tooltip>
7
+ );
8
+
9
+ export const TooltipSides = () => (
10
+ <div style={{ display: "flex", gap: "1.5rem", padding: "3rem" }}>
11
+ <Tooltip label="Above" side="top">
12
+ <button type="button">Top</button>
13
+ </Tooltip>
14
+ <Tooltip label="To the right" side="right">
15
+ <button type="button">Right</button>
16
+ </Tooltip>
17
+ <Tooltip label="Below" side="bottom">
18
+ <button type="button">Bottom</button>
19
+ </Tooltip>
20
+ <Tooltip label="To the left" side="left">
21
+ <button type="button">Left</button>
22
+ </Tooltip>
23
+ </div>
24
+ );
25
+
26
+ /** The case a bare Radix tooltip cannot do — and the reason this is an atom. */
27
+ export const DisabledTrigger = () => (
28
+ <Tooltip label="You do not have permission to edit roads" side="right">
29
+ <button type="button" disabled>
30
+ Road
31
+ </button>
32
+ </Tooltip>
33
+ );
@@ -0,0 +1,55 @@
1
+ // Material Design 3 plain tooltip. `inverse-surface` has no pre-MD3 name, so
2
+ // its fallback is the old on-surface/surface pair swapped — which is exactly
3
+ // what "inverse" means.
4
+
5
+ .BHT__Tooltip {
6
+ &__content {
7
+ padding: var(--spacing-1, 0.25rem) var(--spacing-2, 0.5rem);
8
+ background: var(--md-sys-color-inverse-surface, var(--color-on-surface));
9
+ color: var(--md-sys-color-inverse-on-surface, var(--color-surface));
10
+ border-radius: var(--md-sys-shape-corner-extra-small, 4px);
11
+ font-family: var(--md-sys-typescale-body-small-font, var(--font-body));
12
+ font-size: var(--md-sys-typescale-body-small-size, 0.75rem);
13
+ font-weight: var(--md-sys-typescale-body-small-weight, 400);
14
+ line-height: var(--md-sys-typescale-body-small-line-height, 1rem);
15
+ letter-spacing: var(--md-sys-typescale-body-small-tracking, 0.0333em);
16
+ max-width: 16rem;
17
+ z-index: var(--z-tooltip, 600);
18
+ user-select: none;
19
+
20
+ &[data-state="delayed-open"] {
21
+ animation: tooltip-in var(--md-sys-motion-duration-short-3, 150ms)
22
+ var(--md-sys-motion-easing-standard-decelerate, ease-out);
23
+ }
24
+ }
25
+
26
+ &__arrow {
27
+ fill: var(--md-sys-color-inverse-surface, var(--color-on-surface));
28
+ }
29
+
30
+ // A disabled control fires no pointer events of its own, so the wrapper takes
31
+ // the listeners and the control is made transparent to the pointer.
32
+ &__disabledTrigger {
33
+ display: inline-flex;
34
+
35
+ > * {
36
+ pointer-events: none;
37
+ }
38
+
39
+ &:focus-visible {
40
+ outline: 3px solid
41
+ var(--md-sys-color-secondary, var(--color-border-focus));
42
+ outline-offset: 2px;
43
+ }
44
+ }
45
+ }
46
+
47
+ @keyframes tooltip-in {
48
+ from {
49
+ opacity: 0;
50
+ }
51
+
52
+ to {
53
+ opacity: 1;
54
+ }
55
+ }
package/tooltip.tsx ADDED
@@ -0,0 +1,87 @@
1
+ import { isValidElement, type ReactElement } from "react";
2
+ import * as RadixTooltip from "@radix-ui/react-tooltip";
3
+ import { getClassName } from "@bht-component-registry/class-names";
4
+ import styles from "./tooltip.module.scss";
5
+
6
+ /**
7
+ * Shared open delay. Lives here rather than on each caller so tooltips feel the
8
+ * same everywhere — the `title` attribute's roughly-one-second delay is one of
9
+ * the reasons this component exists.
10
+ */
11
+ const DELAY_DURATION = 300;
12
+
13
+ export type TooltipSide = "top" | "right" | "bottom" | "left";
14
+
15
+ export interface TooltipProps {
16
+ /** Text shown in the tooltip. This is the control's name when the trigger is icon-only. */
17
+ label: string;
18
+ /** Which side of the trigger to place the tooltip on; defaults to "top" */
19
+ side?: TooltipSide;
20
+ /** The element the tooltip describes — rendered as-is via Radix's `asChild` */
21
+ children: ReactElement;
22
+ /** Additional class names to merge with the tooltip content element */
23
+ className?: string;
24
+ }
25
+
26
+ function isDisabled(element: ReactElement): boolean {
27
+ if (!isValidElement(element)) return false;
28
+ const { disabled } = element.props as { disabled?: boolean };
29
+ return disabled === true;
30
+ }
31
+
32
+ /**
33
+ * Names a control on hover and on keyboard focus, built on Radix's `Tooltip`.
34
+ *
35
+ * Mounts its own `Tooltip.Provider`, so a caller never has to add one.
36
+ *
37
+ * Works on a disabled trigger, which a bare Radix tooltip does not: a disabled
38
+ * element fires no pointer events, so the trigger never sees hover or focus.
39
+ * When the child is disabled it is wrapped in a focusable span that carries the
40
+ * listeners instead, and the child is made pointer-transparent so hover lands
41
+ * on the wrapper. That keeps the explanation reachable at the one moment it
42
+ * matters most — when the control is unavailable and the user wants to know
43
+ * why — including by keyboard, since a disabled control is not focusable.
44
+ */
45
+ export const Tooltip = ({
46
+ label,
47
+ side = "top",
48
+ children,
49
+ className,
50
+ }: TooltipProps) => {
51
+ const [, getChildClass] = getClassName({
52
+ className,
53
+ rootClass: "BHT__Tooltip",
54
+ styles,
55
+ });
56
+
57
+ const disabled = isDisabled(children);
58
+
59
+ // The wrapper is deliberately left unlabelled: the disabled control inside
60
+ // keeps its own accessible name, and Radix points the wrapper's
61
+ // `aria-describedby` at the tooltip, so focusing it announces both.
62
+ const trigger = disabled ? (
63
+ <span className={getChildClass("disabledTrigger")} tabIndex={0}>
64
+ {children}
65
+ </span>
66
+ ) : (
67
+ children
68
+ );
69
+
70
+ return (
71
+ <RadixTooltip.Provider delayDuration={DELAY_DURATION}>
72
+ <RadixTooltip.Root>
73
+ <RadixTooltip.Trigger asChild>{trigger}</RadixTooltip.Trigger>
74
+ <RadixTooltip.Portal>
75
+ <RadixTooltip.Content
76
+ className={getChildClass("content")}
77
+ side={side}
78
+ sideOffset={6}
79
+ >
80
+ {label}
81
+ <RadixTooltip.Arrow className={getChildClass("arrow")} />
82
+ </RadixTooltip.Content>
83
+ </RadixTooltip.Portal>
84
+ </RadixTooltip.Root>
85
+ </RadixTooltip.Provider>
86
+ );
87
+ };