@lattice-ui/tooltip 0.1.1

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.
@@ -0,0 +1,3 @@
1
+ import { React } from "@lattice-ui/core";
2
+ import type { TooltipContentProps } from "./types";
3
+ export declare function TooltipContent(props: TooltipContentProps): React.JSX.Element | undefined;
@@ -0,0 +1,111 @@
1
+ -- Compiled with roblox-ts v3.0.0
2
+ local TS = _G[script]
3
+ local _core = TS.import(script, TS.getModule(script, "@lattice-ui", "core").out)
4
+ local React = _core.React
5
+ local Slot = _core.Slot
6
+ local _layer = TS.import(script, TS.getModule(script, "@lattice-ui", "layer").out)
7
+ local DismissableLayer = _layer.DismissableLayer
8
+ local Presence = _layer.Presence
9
+ local usePopper = TS.import(script, TS.getModule(script, "@lattice-ui", "popper").out).usePopper
10
+ local useTooltipContext = TS.import(script, script.Parent, "context").useTooltipContext
11
+ local function toGuiObject(instance)
12
+ if not instance or not instance:IsA("GuiObject") then
13
+ return nil
14
+ end
15
+ return instance
16
+ end
17
+ local function TooltipContentImpl(props)
18
+ local tooltipContext = useTooltipContext()
19
+ local popper = usePopper({
20
+ anchorRef = tooltipContext.triggerRef,
21
+ contentRef = tooltipContext.contentRef,
22
+ placement = props.placement,
23
+ offset = props.offset,
24
+ padding = props.padding,
25
+ enabled = props.enabled,
26
+ })
27
+ local setContentRef = React.useCallback(function(instance)
28
+ tooltipContext.contentRef.current = toGuiObject(instance)
29
+ end, { tooltipContext.contentRef })
30
+ if props.asChild then
31
+ local child = props.children
32
+ if not React.isValidElement(child) then
33
+ error("[TooltipContent] `asChild` requires a child element.")
34
+ end
35
+ return React.createElement(DismissableLayer, {
36
+ enabled = props.enabled,
37
+ modal = false,
38
+ onDismiss = props.onDismiss,
39
+ onEscapeKeyDown = props.onEscapeKeyDown,
40
+ onInteractOutside = props.onInteractOutside,
41
+ onPointerDownOutside = props.onPointerDownOutside,
42
+ }, React.createElement(Slot, {
43
+ AnchorPoint = popper.anchorPoint,
44
+ Position = popper.position,
45
+ Visible = props.visible,
46
+ ref = setContentRef,
47
+ }, child))
48
+ end
49
+ return React.createElement(DismissableLayer, {
50
+ enabled = props.enabled,
51
+ modal = false,
52
+ onDismiss = props.onDismiss,
53
+ onEscapeKeyDown = props.onEscapeKeyDown,
54
+ onInteractOutside = props.onInteractOutside,
55
+ onPointerDownOutside = props.onPointerDownOutside,
56
+ }, React.createElement("frame", {
57
+ AnchorPoint = popper.anchorPoint,
58
+ BackgroundTransparency = 1,
59
+ BorderSizePixel = 0,
60
+ Position = popper.position,
61
+ Size = UDim2.fromOffset(0, 0),
62
+ Visible = props.visible,
63
+ ref = setContentRef,
64
+ }, props.children))
65
+ end
66
+ local function TooltipContent(props)
67
+ local tooltipContext = useTooltipContext()
68
+ local open = tooltipContext.open
69
+ local forceMount = props.forceMount == true
70
+ local handleDismiss = React.useCallback(function()
71
+ tooltipContext.close()
72
+ end, { tooltipContext })
73
+ if not open and not forceMount then
74
+ return nil
75
+ end
76
+ if forceMount then
77
+ return React.createElement(TooltipContentImpl, {
78
+ asChild = props.asChild,
79
+ enabled = open,
80
+ offset = props.offset,
81
+ onDismiss = handleDismiss,
82
+ onEscapeKeyDown = props.onEscapeKeyDown,
83
+ onInteractOutside = props.onInteractOutside,
84
+ onPointerDownOutside = props.onPointerDownOutside,
85
+ padding = props.padding,
86
+ placement = props.placement,
87
+ visible = open,
88
+ }, props.children)
89
+ end
90
+ return React.createElement(Presence, {
91
+ exitFallbackMs = 0,
92
+ present = open,
93
+ render = function(state)
94
+ return React.createElement(TooltipContentImpl, {
95
+ asChild = props.asChild,
96
+ enabled = state.isPresent,
97
+ offset = props.offset,
98
+ onDismiss = handleDismiss,
99
+ onEscapeKeyDown = props.onEscapeKeyDown,
100
+ onInteractOutside = props.onInteractOutside,
101
+ onPointerDownOutside = props.onPointerDownOutside,
102
+ padding = props.padding,
103
+ placement = props.placement,
104
+ visible = state.isPresent,
105
+ }, props.children)
106
+ end,
107
+ })
108
+ end
109
+ return {
110
+ TooltipContent = TooltipContent,
111
+ }
@@ -0,0 +1,3 @@
1
+ import { React } from "@lattice-ui/core";
2
+ import type { TooltipPortalProps } from "./types";
3
+ export declare function TooltipPortal(props: TooltipPortalProps): React.JSX.Element;
@@ -0,0 +1,33 @@
1
+ -- Compiled with roblox-ts v3.0.0
2
+ local TS = _G[script]
3
+ local React = TS.import(script, TS.getModule(script, "@lattice-ui", "core").out).React
4
+ local _layer = TS.import(script, TS.getModule(script, "@lattice-ui", "layer").out)
5
+ local Portal = _layer.Portal
6
+ local PortalProvider = _layer.PortalProvider
7
+ local usePortalContext = _layer.usePortalContext
8
+ local function TooltipPortalWithOverrides(props)
9
+ local portalContext = usePortalContext()
10
+ local container = props.container or portalContext.container
11
+ local _condition = props.displayOrderBase
12
+ if _condition == nil then
13
+ _condition = portalContext.displayOrderBase
14
+ end
15
+ local displayOrderBase = _condition
16
+ return React.createElement(PortalProvider, {
17
+ container = container,
18
+ displayOrderBase = displayOrderBase,
19
+ }, React.createElement(Portal, nil, props.children))
20
+ end
21
+ local function TooltipPortal(props)
22
+ local hasOverrides = props.container ~= nil or props.displayOrderBase ~= nil
23
+ if hasOverrides then
24
+ return React.createElement(TooltipPortalWithOverrides, {
25
+ container = props.container,
26
+ displayOrderBase = props.displayOrderBase,
27
+ }, props.children)
28
+ end
29
+ return React.createElement(Portal, nil, props.children)
30
+ end
31
+ return {
32
+ TooltipPortal = TooltipPortal,
33
+ }
@@ -0,0 +1,3 @@
1
+ import { React } from "@lattice-ui/core";
2
+ import type { TooltipProviderProps } from "./types";
3
+ export declare function TooltipProvider(props: TooltipProviderProps): React.JSX.Element;
@@ -0,0 +1,50 @@
1
+ -- Compiled with roblox-ts v3.0.0
2
+ local TS = _G[script]
3
+ local React = TS.import(script, TS.getModule(script, "@lattice-ui", "core").out).React
4
+ local TooltipProviderContext = TS.import(script, script.Parent, "context").TooltipProviderContext
5
+ local function TooltipProvider(props)
6
+ local _condition = props.delayDuration
7
+ if _condition == nil then
8
+ _condition = 700
9
+ end
10
+ local delayDuration = _condition
11
+ local _condition_1 = props.skipDelayDuration
12
+ if _condition_1 == nil then
13
+ _condition_1 = 300
14
+ end
15
+ local skipDelayDuration = _condition_1
16
+ local lastOpenTimestampRef = React.useRef()
17
+ local markOpen = React.useCallback(function()
18
+ lastOpenTimestampRef.current = os.clock()
19
+ end, {})
20
+ local resolveOpenDelay = React.useCallback(function(requestedDelay)
21
+ local _condition_2 = requestedDelay
22
+ if _condition_2 == nil then
23
+ _condition_2 = delayDuration
24
+ end
25
+ local baseDelay = _condition_2
26
+ local lastOpenTimestamp = lastOpenTimestampRef.current
27
+ if lastOpenTimestamp == nil then
28
+ return baseDelay
29
+ end
30
+ local elapsedMs = (os.clock() - lastOpenTimestamp) * 1000
31
+ if elapsedMs <= skipDelayDuration then
32
+ return math.min(baseDelay, skipDelayDuration)
33
+ end
34
+ return baseDelay
35
+ end, { delayDuration, skipDelayDuration })
36
+ local contextValue = React.useMemo(function()
37
+ return {
38
+ delayDuration = delayDuration,
39
+ skipDelayDuration = skipDelayDuration,
40
+ resolveOpenDelay = resolveOpenDelay,
41
+ markOpen = markOpen,
42
+ }
43
+ end, { delayDuration, markOpen, resolveOpenDelay, skipDelayDuration })
44
+ return React.createElement(TooltipProviderContext.Provider, {
45
+ value = contextValue,
46
+ }, props.children)
47
+ end
48
+ return {
49
+ TooltipProvider = TooltipProvider,
50
+ }
@@ -0,0 +1,3 @@
1
+ import { React } from "@lattice-ui/core";
2
+ import type { TooltipProps } from "./types";
3
+ export declare function Tooltip(props: TooltipProps): React.JSX.Element;
@@ -0,0 +1,78 @@
1
+ -- Compiled with roblox-ts v3.0.0
2
+ local TS = _G[script]
3
+ local _core = TS.import(script, TS.getModule(script, "@lattice-ui", "core").out)
4
+ local React = _core.React
5
+ local useControllableState = _core.useControllableState
6
+ local _context = TS.import(script, script.Parent, "context")
7
+ local TooltipContextProvider = _context.TooltipContextProvider
8
+ local useTooltipProviderContext = _context.useTooltipProviderContext
9
+ local function Tooltip(props)
10
+ local providerContext = useTooltipProviderContext()
11
+ local _object = {
12
+ value = props.open,
13
+ }
14
+ local _left = "defaultValue"
15
+ local _condition = props.defaultOpen
16
+ if _condition == nil then
17
+ _condition = false
18
+ end
19
+ _object[_left] = _condition
20
+ _object.onChange = props.onOpenChange
21
+ local _binding = useControllableState(_object)
22
+ local open = _binding[1]
23
+ local setOpenState = _binding[2]
24
+ local triggerRef = React.useRef()
25
+ local contentRef = React.useRef()
26
+ local openDelayTaskRef = React.useRef()
27
+ local cancelPendingOpen = React.useCallback(function()
28
+ local openDelayTask = openDelayTaskRef.current
29
+ if not openDelayTask then
30
+ return nil
31
+ end
32
+ task.cancel(openDelayTask)
33
+ openDelayTaskRef.current = nil
34
+ end, {})
35
+ local setOpen = React.useCallback(function(nextOpen)
36
+ setOpenState(nextOpen)
37
+ if nextOpen then
38
+ providerContext.markOpen()
39
+ end
40
+ end, { providerContext, setOpenState })
41
+ local openWithDelay = React.useCallback(function()
42
+ cancelPendingOpen()
43
+ local resolvedDelay = providerContext.resolveOpenDelay(props.delayDuration)
44
+ if resolvedDelay <= 0 then
45
+ setOpen(true)
46
+ return nil
47
+ end
48
+ openDelayTaskRef.current = task.delay(resolvedDelay / 1000, function()
49
+ openDelayTaskRef.current = nil
50
+ setOpen(true)
51
+ end)
52
+ end, { cancelPendingOpen, props.delayDuration, providerContext, setOpen })
53
+ local close = React.useCallback(function()
54
+ cancelPendingOpen()
55
+ setOpen(false)
56
+ end, { cancelPendingOpen, setOpen })
57
+ React.useEffect(function()
58
+ return function()
59
+ cancelPendingOpen()
60
+ end
61
+ end, { cancelPendingOpen })
62
+ local contextValue = React.useMemo(function()
63
+ return {
64
+ open = open,
65
+ setOpen = setOpen,
66
+ openWithDelay = openWithDelay,
67
+ close = close,
68
+ triggerRef = triggerRef,
69
+ contentRef = contentRef,
70
+ }
71
+ end, { close, open, openWithDelay, setOpen })
72
+ return React.createElement(TooltipContextProvider, {
73
+ value = contextValue,
74
+ }, props.children)
75
+ end
76
+ return {
77
+ Tooltip = Tooltip,
78
+ }
@@ -0,0 +1,3 @@
1
+ import { React } from "@lattice-ui/core";
2
+ import type { TooltipTriggerProps } from "./types";
3
+ export declare function TooltipTrigger(props: TooltipTriggerProps): React.JSX.Element;
@@ -0,0 +1,61 @@
1
+ -- Compiled with roblox-ts v3.0.0
2
+ local TS = _G[script]
3
+ local _core = TS.import(script, TS.getModule(script, "@lattice-ui", "core").out)
4
+ local React = _core.React
5
+ local Slot = _core.Slot
6
+ local useTooltipContext = TS.import(script, script.Parent, "context").useTooltipContext
7
+ local function toGuiObject(instance)
8
+ if not instance or not instance:IsA("GuiObject") then
9
+ return nil
10
+ end
11
+ return instance
12
+ end
13
+ local function TooltipTrigger(props)
14
+ local tooltipContext = useTooltipContext()
15
+ local setTriggerRef = React.useCallback(function(instance)
16
+ tooltipContext.triggerRef.current = toGuiObject(instance)
17
+ end, { tooltipContext.triggerRef })
18
+ local handleOpen = React.useCallback(function()
19
+ if props.disabled then
20
+ return nil
21
+ end
22
+ tooltipContext.openWithDelay()
23
+ end, { props.disabled, tooltipContext })
24
+ local handleClose = React.useCallback(function()
25
+ tooltipContext.close()
26
+ end, { tooltipContext })
27
+ local eventHandlers = React.useMemo(function()
28
+ return {
29
+ MouseEnter = handleOpen,
30
+ MouseLeave = handleClose,
31
+ SelectionGained = handleOpen,
32
+ SelectionLost = handleClose,
33
+ }
34
+ end, { handleClose, handleOpen })
35
+ if props.asChild then
36
+ local child = props.children
37
+ if not child then
38
+ error("[TooltipTrigger] `asChild` requires a child element.")
39
+ end
40
+ return React.createElement(Slot, {
41
+ Event = eventHandlers,
42
+ ref = setTriggerRef,
43
+ }, child)
44
+ end
45
+ return React.createElement("textbutton", {
46
+ Active = props.disabled ~= true,
47
+ AutoButtonColor = false,
48
+ BackgroundTransparency = 1,
49
+ BorderSizePixel = 0,
50
+ Event = eventHandlers,
51
+ Selectable = props.disabled ~= true,
52
+ Size = UDim2.fromOffset(140, 36),
53
+ Text = "Tooltip Trigger",
54
+ TextColor3 = Color3.fromRGB(240, 244, 250),
55
+ TextSize = 15,
56
+ ref = setTriggerRef,
57
+ }, props.children)
58
+ end
59
+ return {
60
+ TooltipTrigger = TooltipTrigger,
61
+ }
@@ -0,0 +1,6 @@
1
+ import { React } from "@lattice-ui/core";
2
+ import type { TooltipContextValue, TooltipProviderContextValue } from "./types";
3
+ declare const TooltipContextProvider: React.Provider<TooltipContextValue | undefined>, useTooltipContext: () => TooltipContextValue;
4
+ declare const TooltipProviderContext: React.Context<TooltipProviderContextValue>;
5
+ declare function useTooltipProviderContext(): TooltipProviderContextValue;
6
+ export { TooltipContextProvider, TooltipProviderContext, useTooltipContext, useTooltipProviderContext };
@@ -0,0 +1,32 @@
1
+ -- Compiled with roblox-ts v3.0.0
2
+ local TS = _G[script]
3
+ local _core = TS.import(script, TS.getModule(script, "@lattice-ui", "core").out)
4
+ local createStrictContext = _core.createStrictContext
5
+ local React = _core.React
6
+ local _binding = createStrictContext("Tooltip")
7
+ local TooltipContextProvider = _binding[1]
8
+ local useTooltipContext = _binding[2]
9
+ local DEFAULT_TOOLTIP_PROVIDER_CONTEXT = {
10
+ delayDuration = 700,
11
+ skipDelayDuration = 300,
12
+ resolveOpenDelay = function(requestedDelay)
13
+ local _condition = requestedDelay
14
+ if _condition == nil then
15
+ _condition = 700
16
+ end
17
+ return _condition
18
+ end,
19
+ markOpen = function()
20
+ -- default no-op
21
+ end,
22
+ }
23
+ local TooltipProviderContext = React.createContext(DEFAULT_TOOLTIP_PROVIDER_CONTEXT)
24
+ local function useTooltipProviderContext()
25
+ return React.useContext(TooltipProviderContext)
26
+ end
27
+ return {
28
+ TooltipContextProvider = TooltipContextProvider,
29
+ TooltipProviderContext = TooltipProviderContext,
30
+ useTooltipContext = useTooltipContext,
31
+ useTooltipProviderContext = useTooltipProviderContext,
32
+ }
@@ -0,0 +1,51 @@
1
+ import type { LayerInteractEvent } from "@lattice-ui/layer";
2
+ import type { PopperPlacement } from "@lattice-ui/popper";
3
+ import type React from "@rbxts/react";
4
+ export type TooltipSetOpen = (open: boolean) => void;
5
+ export type TooltipContextValue = {
6
+ open: boolean;
7
+ setOpen: TooltipSetOpen;
8
+ openWithDelay: () => void;
9
+ close: () => void;
10
+ triggerRef: React.MutableRefObject<GuiObject | undefined>;
11
+ contentRef: React.MutableRefObject<GuiObject | undefined>;
12
+ };
13
+ export type TooltipProviderContextValue = {
14
+ delayDuration: number;
15
+ skipDelayDuration: number;
16
+ resolveOpenDelay: (requestedDelay?: number) => number;
17
+ markOpen: () => void;
18
+ };
19
+ export type TooltipProviderProps = {
20
+ delayDuration?: number;
21
+ skipDelayDuration?: number;
22
+ children?: React.ReactNode;
23
+ };
24
+ export type TooltipProps = {
25
+ open?: boolean;
26
+ defaultOpen?: boolean;
27
+ delayDuration?: number;
28
+ onOpenChange?: (open: boolean) => void;
29
+ children?: React.ReactNode;
30
+ };
31
+ export type TooltipTriggerProps = {
32
+ asChild?: boolean;
33
+ disabled?: boolean;
34
+ children?: React.ReactElement;
35
+ };
36
+ export type TooltipPortalProps = {
37
+ container?: BasePlayerGui;
38
+ displayOrderBase?: number;
39
+ children?: React.ReactNode;
40
+ };
41
+ export type TooltipContentProps = {
42
+ asChild?: boolean;
43
+ forceMount?: boolean;
44
+ placement?: PopperPlacement;
45
+ offset?: Vector2;
46
+ padding?: number;
47
+ onEscapeKeyDown?: (event: LayerInteractEvent) => void;
48
+ onPointerDownOutside?: (event: LayerInteractEvent) => void;
49
+ onInteractOutside?: (event: LayerInteractEvent) => void;
50
+ children?: React.ReactNode;
51
+ };
@@ -0,0 +1,2 @@
1
+ -- Compiled with roblox-ts v3.0.0
2
+ return nil
package/out/index.d.ts ADDED
@@ -0,0 +1,6 @@
1
+ export { TooltipContent } from "./Tooltip/TooltipContent";
2
+ export { TooltipPortal } from "./Tooltip/TooltipPortal";
3
+ export { TooltipProvider } from "./Tooltip/TooltipProvider";
4
+ export { Tooltip } from "./Tooltip/TooltipRoot";
5
+ export { TooltipTrigger } from "./Tooltip/TooltipTrigger";
6
+ export type { TooltipContentProps, TooltipPortalProps, TooltipProps, TooltipProviderProps, TooltipTriggerProps, } from "./Tooltip/types";
package/out/init.luau ADDED
@@ -0,0 +1,9 @@
1
+ -- Compiled with roblox-ts v3.0.0
2
+ local TS = _G[script]
3
+ local exports = {}
4
+ exports.TooltipContent = TS.import(script, script, "Tooltip", "TooltipContent").TooltipContent
5
+ exports.TooltipPortal = TS.import(script, script, "Tooltip", "TooltipPortal").TooltipPortal
6
+ exports.TooltipProvider = TS.import(script, script, "Tooltip", "TooltipProvider").TooltipProvider
7
+ exports.Tooltip = TS.import(script, script, "Tooltip", "TooltipRoot").Tooltip
8
+ exports.TooltipTrigger = TS.import(script, script, "Tooltip", "TooltipTrigger").TooltipTrigger
9
+ return exports
package/package.json ADDED
@@ -0,0 +1,25 @@
1
+ {
2
+ "name": "@lattice-ui/tooltip",
3
+ "version": "0.1.1",
4
+ "private": false,
5
+ "main": "out/init.luau",
6
+ "types": "out/index.d.ts",
7
+ "dependencies": {
8
+ "@lattice-ui/core": "0.1.1",
9
+ "@lattice-ui/layer": "0.1.1",
10
+ "@lattice-ui/popper": "0.1.1"
11
+ },
12
+ "devDependencies": {
13
+ "@rbxts/react": "17.3.7-ts.1",
14
+ "@rbxts/react-roblox": "17.3.7-ts.1"
15
+ },
16
+ "peerDependencies": {
17
+ "@rbxts/react": "^17",
18
+ "@rbxts/react-roblox": "^17"
19
+ },
20
+ "scripts": {
21
+ "build": "rbxtsc -p tsconfig.json",
22
+ "watch": "rbxtsc -p tsconfig.json -w",
23
+ "typecheck": "tsc -p tsconfig.typecheck.json"
24
+ }
25
+ }
@@ -0,0 +1,144 @@
1
+ import { React, Slot } from "@lattice-ui/core";
2
+ import { DismissableLayer, Presence } from "@lattice-ui/layer";
3
+ import { usePopper } from "@lattice-ui/popper";
4
+ import { useTooltipContext } from "./context";
5
+ import type { TooltipContentProps } from "./types";
6
+
7
+ type TooltipContentImplProps = {
8
+ enabled: boolean;
9
+ visible: boolean;
10
+ onDismiss: () => void;
11
+ asChild?: boolean;
12
+ placement?: TooltipContentProps["placement"];
13
+ offset?: TooltipContentProps["offset"];
14
+ padding?: TooltipContentProps["padding"];
15
+ } & Pick<TooltipContentProps, "children" | "onEscapeKeyDown" | "onInteractOutside" | "onPointerDownOutside">;
16
+
17
+ function toGuiObject(instance: Instance | undefined) {
18
+ if (!instance || !instance.IsA("GuiObject")) {
19
+ return undefined;
20
+ }
21
+
22
+ return instance;
23
+ }
24
+
25
+ function TooltipContentImpl(props: TooltipContentImplProps) {
26
+ const tooltipContext = useTooltipContext();
27
+
28
+ const popper = usePopper({
29
+ anchorRef: tooltipContext.triggerRef,
30
+ contentRef: tooltipContext.contentRef,
31
+ placement: props.placement,
32
+ offset: props.offset,
33
+ padding: props.padding,
34
+ enabled: props.enabled,
35
+ });
36
+
37
+ const setContentRef = React.useCallback(
38
+ (instance: Instance | undefined) => {
39
+ tooltipContext.contentRef.current = toGuiObject(instance);
40
+ },
41
+ [tooltipContext.contentRef],
42
+ );
43
+
44
+ if (props.asChild) {
45
+ const child = props.children;
46
+ if (!React.isValidElement(child)) {
47
+ error("[TooltipContent] `asChild` requires a child element.");
48
+ }
49
+
50
+ return (
51
+ <DismissableLayer
52
+ enabled={props.enabled}
53
+ modal={false}
54
+ onDismiss={props.onDismiss}
55
+ onEscapeKeyDown={props.onEscapeKeyDown}
56
+ onInteractOutside={props.onInteractOutside}
57
+ onPointerDownOutside={props.onPointerDownOutside}
58
+ >
59
+ <Slot AnchorPoint={popper.anchorPoint} Position={popper.position} Visible={props.visible} ref={setContentRef}>
60
+ {child}
61
+ </Slot>
62
+ </DismissableLayer>
63
+ );
64
+ }
65
+
66
+ return (
67
+ <DismissableLayer
68
+ enabled={props.enabled}
69
+ modal={false}
70
+ onDismiss={props.onDismiss}
71
+ onEscapeKeyDown={props.onEscapeKeyDown}
72
+ onInteractOutside={props.onInteractOutside}
73
+ onPointerDownOutside={props.onPointerDownOutside}
74
+ >
75
+ <frame
76
+ AnchorPoint={popper.anchorPoint}
77
+ BackgroundTransparency={1}
78
+ BorderSizePixel={0}
79
+ Position={popper.position}
80
+ Size={UDim2.fromOffset(0, 0)}
81
+ Visible={props.visible}
82
+ ref={setContentRef}
83
+ >
84
+ {props.children}
85
+ </frame>
86
+ </DismissableLayer>
87
+ );
88
+ }
89
+
90
+ export function TooltipContent(props: TooltipContentProps) {
91
+ const tooltipContext = useTooltipContext();
92
+ const open = tooltipContext.open;
93
+ const forceMount = props.forceMount === true;
94
+
95
+ const handleDismiss = React.useCallback(() => {
96
+ tooltipContext.close();
97
+ }, [tooltipContext]);
98
+
99
+ if (!open && !forceMount) {
100
+ return undefined;
101
+ }
102
+
103
+ if (forceMount) {
104
+ return (
105
+ <TooltipContentImpl
106
+ asChild={props.asChild}
107
+ enabled={open}
108
+ offset={props.offset}
109
+ onDismiss={handleDismiss}
110
+ onEscapeKeyDown={props.onEscapeKeyDown}
111
+ onInteractOutside={props.onInteractOutside}
112
+ onPointerDownOutside={props.onPointerDownOutside}
113
+ padding={props.padding}
114
+ placement={props.placement}
115
+ visible={open}
116
+ >
117
+ {props.children}
118
+ </TooltipContentImpl>
119
+ );
120
+ }
121
+
122
+ return (
123
+ <Presence
124
+ exitFallbackMs={0}
125
+ present={open}
126
+ render={(state) => (
127
+ <TooltipContentImpl
128
+ asChild={props.asChild}
129
+ enabled={state.isPresent}
130
+ offset={props.offset}
131
+ onDismiss={handleDismiss}
132
+ onEscapeKeyDown={props.onEscapeKeyDown}
133
+ onInteractOutside={props.onInteractOutside}
134
+ onPointerDownOutside={props.onPointerDownOutside}
135
+ padding={props.padding}
136
+ placement={props.placement}
137
+ visible={state.isPresent}
138
+ >
139
+ {props.children}
140
+ </TooltipContentImpl>
141
+ )}
142
+ />
143
+ );
144
+ }
@@ -0,0 +1,28 @@
1
+ import { React } from "@lattice-ui/core";
2
+ import { Portal, PortalProvider, usePortalContext } from "@lattice-ui/layer";
3
+ import type { TooltipPortalProps } from "./types";
4
+
5
+ function TooltipPortalWithOverrides(props: TooltipPortalProps) {
6
+ const portalContext = usePortalContext();
7
+ const container = props.container ?? portalContext.container;
8
+ const displayOrderBase = props.displayOrderBase ?? portalContext.displayOrderBase;
9
+
10
+ return (
11
+ <PortalProvider container={container} displayOrderBase={displayOrderBase}>
12
+ <Portal>{props.children}</Portal>
13
+ </PortalProvider>
14
+ );
15
+ }
16
+
17
+ export function TooltipPortal(props: TooltipPortalProps) {
18
+ const hasOverrides = props.container !== undefined || props.displayOrderBase !== undefined;
19
+ if (hasOverrides) {
20
+ return (
21
+ <TooltipPortalWithOverrides container={props.container} displayOrderBase={props.displayOrderBase}>
22
+ {props.children}
23
+ </TooltipPortalWithOverrides>
24
+ );
25
+ }
26
+
27
+ return <Portal>{props.children}</Portal>;
28
+ }
@@ -0,0 +1,43 @@
1
+ import { React } from "@lattice-ui/core";
2
+ import { TooltipProviderContext } from "./context";
3
+ import type { TooltipProviderContextValue, TooltipProviderProps } from "./types";
4
+
5
+ export function TooltipProvider(props: TooltipProviderProps) {
6
+ const delayDuration = props.delayDuration ?? 700;
7
+ const skipDelayDuration = props.skipDelayDuration ?? 300;
8
+ const lastOpenTimestampRef = React.useRef<number>();
9
+
10
+ const markOpen = React.useCallback(() => {
11
+ lastOpenTimestampRef.current = os.clock();
12
+ }, []);
13
+
14
+ const resolveOpenDelay = React.useCallback(
15
+ (requestedDelay?: number) => {
16
+ const baseDelay = requestedDelay ?? delayDuration;
17
+ const lastOpenTimestamp = lastOpenTimestampRef.current;
18
+ if (lastOpenTimestamp === undefined) {
19
+ return baseDelay;
20
+ }
21
+
22
+ const elapsedMs = (os.clock() - lastOpenTimestamp) * 1000;
23
+ if (elapsedMs <= skipDelayDuration) {
24
+ return math.min(baseDelay, skipDelayDuration);
25
+ }
26
+
27
+ return baseDelay;
28
+ },
29
+ [delayDuration, skipDelayDuration],
30
+ );
31
+
32
+ const contextValue = React.useMemo<TooltipProviderContextValue>(
33
+ () => ({
34
+ delayDuration,
35
+ skipDelayDuration,
36
+ resolveOpenDelay,
37
+ markOpen,
38
+ }),
39
+ [delayDuration, markOpen, resolveOpenDelay, skipDelayDuration],
40
+ );
41
+
42
+ return <TooltipProviderContext.Provider value={contextValue}>{props.children}</TooltipProviderContext.Provider>;
43
+ }
@@ -0,0 +1,76 @@
1
+ import { React, useControllableState } from "@lattice-ui/core";
2
+ import { TooltipContextProvider, useTooltipProviderContext } from "./context";
3
+ import type { TooltipProps } from "./types";
4
+
5
+ export function Tooltip(props: TooltipProps) {
6
+ const providerContext = useTooltipProviderContext();
7
+ const [open, setOpenState] = useControllableState<boolean>({
8
+ value: props.open,
9
+ defaultValue: props.defaultOpen ?? false,
10
+ onChange: props.onOpenChange,
11
+ });
12
+
13
+ const triggerRef = React.useRef<GuiObject>();
14
+ const contentRef = React.useRef<GuiObject>();
15
+ const openDelayTaskRef = React.useRef<thread>();
16
+
17
+ const cancelPendingOpen = React.useCallback(() => {
18
+ const openDelayTask = openDelayTaskRef.current;
19
+ if (!openDelayTask) {
20
+ return;
21
+ }
22
+
23
+ task.cancel(openDelayTask);
24
+ openDelayTaskRef.current = undefined;
25
+ }, []);
26
+
27
+ const setOpen = React.useCallback(
28
+ (nextOpen: boolean) => {
29
+ setOpenState(nextOpen);
30
+ if (nextOpen) {
31
+ providerContext.markOpen();
32
+ }
33
+ },
34
+ [providerContext, setOpenState],
35
+ );
36
+
37
+ const openWithDelay = React.useCallback(() => {
38
+ cancelPendingOpen();
39
+
40
+ const resolvedDelay = providerContext.resolveOpenDelay(props.delayDuration);
41
+ if (resolvedDelay <= 0) {
42
+ setOpen(true);
43
+ return;
44
+ }
45
+
46
+ openDelayTaskRef.current = task.delay(resolvedDelay / 1000, () => {
47
+ openDelayTaskRef.current = undefined;
48
+ setOpen(true);
49
+ });
50
+ }, [cancelPendingOpen, props.delayDuration, providerContext, setOpen]);
51
+
52
+ const close = React.useCallback(() => {
53
+ cancelPendingOpen();
54
+ setOpen(false);
55
+ }, [cancelPendingOpen, setOpen]);
56
+
57
+ React.useEffect(() => {
58
+ return () => {
59
+ cancelPendingOpen();
60
+ };
61
+ }, [cancelPendingOpen]);
62
+
63
+ const contextValue = React.useMemo(
64
+ () => ({
65
+ open,
66
+ setOpen,
67
+ openWithDelay,
68
+ close,
69
+ triggerRef,
70
+ contentRef,
71
+ }),
72
+ [close, open, openWithDelay, setOpen],
73
+ );
74
+
75
+ return <TooltipContextProvider value={contextValue}>{props.children}</TooltipContextProvider>;
76
+ }
@@ -0,0 +1,75 @@
1
+ import { React, Slot } from "@lattice-ui/core";
2
+ import { useTooltipContext } from "./context";
3
+ import type { TooltipTriggerProps } from "./types";
4
+
5
+ function toGuiObject(instance: Instance | undefined) {
6
+ if (!instance || !instance.IsA("GuiObject")) {
7
+ return undefined;
8
+ }
9
+
10
+ return instance;
11
+ }
12
+
13
+ export function TooltipTrigger(props: TooltipTriggerProps) {
14
+ const tooltipContext = useTooltipContext();
15
+
16
+ const setTriggerRef = React.useCallback(
17
+ (instance: Instance | undefined) => {
18
+ tooltipContext.triggerRef.current = toGuiObject(instance);
19
+ },
20
+ [tooltipContext.triggerRef],
21
+ );
22
+
23
+ const handleOpen = React.useCallback(() => {
24
+ if (props.disabled) {
25
+ return;
26
+ }
27
+
28
+ tooltipContext.openWithDelay();
29
+ }, [props.disabled, tooltipContext]);
30
+
31
+ const handleClose = React.useCallback(() => {
32
+ tooltipContext.close();
33
+ }, [tooltipContext]);
34
+
35
+ const eventHandlers = React.useMemo(
36
+ () => ({
37
+ MouseEnter: handleOpen,
38
+ MouseLeave: handleClose,
39
+ SelectionGained: handleOpen,
40
+ SelectionLost: handleClose,
41
+ }),
42
+ [handleClose, handleOpen],
43
+ );
44
+
45
+ if (props.asChild) {
46
+ const child = props.children;
47
+ if (!child) {
48
+ error("[TooltipTrigger] `asChild` requires a child element.");
49
+ }
50
+
51
+ return (
52
+ <Slot Event={eventHandlers} ref={setTriggerRef}>
53
+ {child}
54
+ </Slot>
55
+ );
56
+ }
57
+
58
+ return (
59
+ <textbutton
60
+ Active={props.disabled !== true}
61
+ AutoButtonColor={false}
62
+ BackgroundTransparency={1}
63
+ BorderSizePixel={0}
64
+ Event={eventHandlers}
65
+ Selectable={props.disabled !== true}
66
+ Size={UDim2.fromOffset(140, 36)}
67
+ Text="Tooltip Trigger"
68
+ TextColor3={Color3.fromRGB(240, 244, 250)}
69
+ TextSize={15}
70
+ ref={setTriggerRef}
71
+ >
72
+ {props.children}
73
+ </textbutton>
74
+ );
75
+ }
@@ -0,0 +1,21 @@
1
+ import { createStrictContext, React } from "@lattice-ui/core";
2
+ import type { TooltipContextValue, TooltipProviderContextValue } from "./types";
3
+
4
+ const [TooltipContextProvider, useTooltipContext] = createStrictContext<TooltipContextValue>("Tooltip");
5
+
6
+ const DEFAULT_TOOLTIP_PROVIDER_CONTEXT: TooltipProviderContextValue = {
7
+ delayDuration: 700,
8
+ skipDelayDuration: 300,
9
+ resolveOpenDelay: (requestedDelay) => requestedDelay ?? 700,
10
+ markOpen: () => {
11
+ // default no-op
12
+ },
13
+ };
14
+
15
+ const TooltipProviderContext = React.createContext<TooltipProviderContextValue>(DEFAULT_TOOLTIP_PROVIDER_CONTEXT);
16
+
17
+ function useTooltipProviderContext() {
18
+ return React.useContext(TooltipProviderContext);
19
+ }
20
+
21
+ export { TooltipContextProvider, TooltipProviderContext, useTooltipContext, useTooltipProviderContext };
@@ -0,0 +1,59 @@
1
+ import type { LayerInteractEvent } from "@lattice-ui/layer";
2
+ import type { PopperPlacement } from "@lattice-ui/popper";
3
+ import type React from "@rbxts/react";
4
+
5
+ export type TooltipSetOpen = (open: boolean) => void;
6
+
7
+ export type TooltipContextValue = {
8
+ open: boolean;
9
+ setOpen: TooltipSetOpen;
10
+ openWithDelay: () => void;
11
+ close: () => void;
12
+ triggerRef: React.MutableRefObject<GuiObject | undefined>;
13
+ contentRef: React.MutableRefObject<GuiObject | undefined>;
14
+ };
15
+
16
+ export type TooltipProviderContextValue = {
17
+ delayDuration: number;
18
+ skipDelayDuration: number;
19
+ resolveOpenDelay: (requestedDelay?: number) => number;
20
+ markOpen: () => void;
21
+ };
22
+
23
+ export type TooltipProviderProps = {
24
+ delayDuration?: number;
25
+ skipDelayDuration?: number;
26
+ children?: React.ReactNode;
27
+ };
28
+
29
+ export type TooltipProps = {
30
+ open?: boolean;
31
+ defaultOpen?: boolean;
32
+ delayDuration?: number;
33
+ onOpenChange?: (open: boolean) => void;
34
+ children?: React.ReactNode;
35
+ };
36
+
37
+ export type TooltipTriggerProps = {
38
+ asChild?: boolean;
39
+ disabled?: boolean;
40
+ children?: React.ReactElement;
41
+ };
42
+
43
+ export type TooltipPortalProps = {
44
+ container?: BasePlayerGui;
45
+ displayOrderBase?: number;
46
+ children?: React.ReactNode;
47
+ };
48
+
49
+ export type TooltipContentProps = {
50
+ asChild?: boolean;
51
+ forceMount?: boolean;
52
+ placement?: PopperPlacement;
53
+ offset?: Vector2;
54
+ padding?: number;
55
+ onEscapeKeyDown?: (event: LayerInteractEvent) => void;
56
+ onPointerDownOutside?: (event: LayerInteractEvent) => void;
57
+ onInteractOutside?: (event: LayerInteractEvent) => void;
58
+ children?: React.ReactNode;
59
+ };
package/src/index.ts ADDED
@@ -0,0 +1,12 @@
1
+ export { TooltipContent } from "./Tooltip/TooltipContent";
2
+ export { TooltipPortal } from "./Tooltip/TooltipPortal";
3
+ export { TooltipProvider } from "./Tooltip/TooltipProvider";
4
+ export { Tooltip } from "./Tooltip/TooltipRoot";
5
+ export { TooltipTrigger } from "./Tooltip/TooltipTrigger";
6
+ export type {
7
+ TooltipContentProps,
8
+ TooltipPortalProps,
9
+ TooltipProps,
10
+ TooltipProviderProps,
11
+ TooltipTriggerProps,
12
+ } from "./Tooltip/types";
package/tsconfig.json ADDED
@@ -0,0 +1,16 @@
1
+ {
2
+ "extends": "../../tsconfig.base.json",
3
+ "compilerOptions": {
4
+ "rootDir": "src",
5
+ "outDir": "out",
6
+ "declaration": true,
7
+ "typeRoots": [
8
+ "./node_modules/@rbxts",
9
+ "../../node_modules/@rbxts",
10
+ "./node_modules/@lattice-ui",
11
+ "../../node_modules/@lattice-ui"
12
+ ],
13
+ "types": ["types", "compiler-types"]
14
+ },
15
+ "include": ["src"]
16
+ }
@@ -0,0 +1,25 @@
1
+ {
2
+ "extends": "./tsconfig.json",
3
+ "compilerOptions": {
4
+ "noEmit": true,
5
+ "baseUrl": "..",
6
+ "rootDir": "..",
7
+ "paths": {
8
+ "@lattice-ui/checkbox": ["checkbox/src/index.ts"],
9
+ "@lattice-ui/core": ["core/src/index.ts"],
10
+ "@lattice-ui/dialog": ["dialog/src/index.ts"],
11
+ "@lattice-ui/focus": ["focus/src/index.ts"],
12
+ "@lattice-ui/layer": ["layer/src/index.ts"],
13
+ "@lattice-ui/menu": ["menu/src/index.ts"],
14
+ "@lattice-ui/popover": ["popover/src/index.ts"],
15
+ "@lattice-ui/popper": ["popper/src/index.ts"],
16
+ "@lattice-ui/radio-group": ["radio-group/src/index.ts"],
17
+ "@lattice-ui/style": ["style/src/index.ts"],
18
+ "@lattice-ui/switch": ["switch/src/index.ts"],
19
+ "@lattice-ui/system": ["system/src/index.ts"],
20
+ "@lattice-ui/tabs": ["tabs/src/index.ts"],
21
+ "@lattice-ui/toggle-group": ["toggle-group/src/index.ts"],
22
+ "@lattice-ui/tooltip": ["tooltip/src/index.ts"]
23
+ }
24
+ }
25
+ }