@lattice-ui/layer 0.1.1 → 0.3.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/package.json CHANGED
@@ -1,11 +1,15 @@
1
1
  {
2
2
  "name": "@lattice-ui/layer",
3
- "version": "0.1.1",
3
+ "version": "0.3.0",
4
4
  "private": false,
5
5
  "main": "out/init.luau",
6
6
  "types": "out/index.d.ts",
7
+ "files": [
8
+ "out",
9
+ "README.md"
10
+ ],
7
11
  "dependencies": {
8
- "@lattice-ui/core": "0.1.1"
12
+ "@lattice-ui/core": "0.3.0"
9
13
  },
10
14
  "devDependencies": {
11
15
  "@rbxts/react": "17.3.7-ts.1",
@@ -17,7 +21,7 @@
17
21
  },
18
22
  "scripts": {
19
23
  "build": "rbxtsc -p tsconfig.json",
20
- "watch": "rbxtsc -p tsconfig.json -w",
21
- "typecheck": "tsc -p tsconfig.typecheck.json"
24
+ "typecheck": "tsc -p tsconfig.typecheck.json",
25
+ "watch": "rbxtsc -p tsconfig.json -w"
22
26
  }
23
27
  }
@@ -1,118 +0,0 @@
1
- import { React } from "@lattice-ui/core";
2
- import { DEFAULT_LAYER_IGNORE_GUI_INSET } from "../internals/constants";
3
- import { Portal } from "../portal/Portal";
4
- import { usePortalContext } from "../portal/PortalProvider";
5
- import { isOutsidePointerEvent } from "./events";
6
- import { registerLayer, unregisterLayer } from "./layerStack";
7
- import type { DismissableLayerProps, LayerInteractEvent } from "./types";
8
-
9
- function useLatest<T>(value: T) {
10
- const ref = React.useRef(value);
11
- React.useEffect(() => {
12
- ref.current = value;
13
- }, [value]);
14
- return ref;
15
- }
16
-
17
- export function DismissableLayer(props: DismissableLayerProps) {
18
- const enabled = props.enabled ?? true;
19
- const shouldBlockOutsidePointer = props.modal === true || props.disableOutsidePointerEvents === true;
20
- const layerIgnoresGuiInset = DEFAULT_LAYER_IGNORE_GUI_INSET;
21
-
22
- const portalContext = usePortalContext();
23
- const contentRootRef = React.useRef<Frame>();
24
- const [stackOrder, setStackOrder] = React.useState(0);
25
-
26
- const enabledRef = useLatest(enabled);
27
- const onDismissRef = useLatest(props.onDismiss);
28
- const onPointerDownOutsideRef = useLatest(props.onPointerDownOutside);
29
- const onInteractOutsideRef = useLatest(props.onInteractOutside);
30
- const onEscapeKeyDownRef = useLatest(props.onEscapeKeyDown);
31
-
32
- const callPointerDownOutside = React.useCallback((event: LayerInteractEvent) => {
33
- onPointerDownOutsideRef.current?.(event);
34
- }, []);
35
-
36
- const callInteractOutside = React.useCallback((event: LayerInteractEvent) => {
37
- onInteractOutsideRef.current?.(event);
38
- }, []);
39
-
40
- const callEscape = React.useCallback((event: LayerInteractEvent) => {
41
- onEscapeKeyDownRef.current?.(event);
42
- }, []);
43
-
44
- const callDismiss = React.useCallback(() => {
45
- onDismissRef.current?.();
46
- }, []);
47
-
48
- React.useEffect(() => {
49
- const registration = registerLayer({
50
- getEnabled: () => enabledRef.current,
51
- isPointerOutside: (inputObject) => {
52
- const contentRoot = contentRootRef.current;
53
- if (!contentRoot) {
54
- return false;
55
- }
56
- return isOutsidePointerEvent(inputObject, portalContext.container, contentRoot, {
57
- layerIgnoresGuiInset,
58
- });
59
- },
60
- onPointerDownOutside: callPointerDownOutside,
61
- onInteractOutside: callInteractOutside,
62
- onEscapeKeyDown: callEscape,
63
- onDismiss: callDismiss,
64
- });
65
-
66
- setStackOrder(registration.mountOrder);
67
-
68
- return () => {
69
- unregisterLayer(registration.id);
70
- };
71
- }, [
72
- callDismiss,
73
- callEscape,
74
- callInteractOutside,
75
- callPointerDownOutside,
76
- enabledRef,
77
- layerIgnoresGuiInset,
78
- portalContext.container,
79
- ]);
80
-
81
- return (
82
- <Portal>
83
- <screengui
84
- key={`Layer_${stackOrder}`}
85
- DisplayOrder={portalContext.displayOrderBase + stackOrder}
86
- IgnoreGuiInset={layerIgnoresGuiInset}
87
- ResetOnSpawn={false}
88
- ZIndexBehavior={Enum.ZIndexBehavior.Sibling}
89
- >
90
- {shouldBlockOutsidePointer ? (
91
- <textbutton
92
- Active={true}
93
- AutoButtonColor={false}
94
- BackgroundTransparency={1}
95
- BorderSizePixel={0}
96
- Modal={true}
97
- Position={UDim2.fromScale(0, 0)}
98
- Selectable={false}
99
- Size={UDim2.fromScale(1, 1)}
100
- Text=""
101
- TextTransparency={1}
102
- ZIndex={0}
103
- />
104
- ) : undefined}
105
- <frame
106
- BackgroundTransparency={1}
107
- BorderSizePixel={0}
108
- Position={UDim2.fromScale(0, 0)}
109
- Size={UDim2.fromScale(1, 1)}
110
- ref={contentRootRef}
111
- ZIndex={1}
112
- >
113
- {props.children}
114
- </frame>
115
- </screengui>
116
- </Portal>
117
- );
118
- }
@@ -1,78 +0,0 @@
1
- import { getGuiInsetTopLeft } from "../internals/env";
2
- import type { LayerInteractEvent } from "./types";
3
-
4
- type OutsidePointerOptions = {
5
- layerIgnoresGuiInset: boolean;
6
- };
7
-
8
- export function isPointerInput(inputObject: InputObject) {
9
- return (
10
- inputObject.UserInputType === Enum.UserInputType.MouseButton1 ||
11
- inputObject.UserInputType === Enum.UserInputType.Touch
12
- );
13
- }
14
-
15
- export function toLayerInteractEvent(originalEvent: InputObject): LayerInteractEvent {
16
- const event: LayerInteractEvent = {
17
- originalEvent,
18
- defaultPrevented: false,
19
- preventDefault: () => {
20
- event.defaultPrevented = true;
21
- },
22
- };
23
- return event;
24
- }
25
-
26
- function addUniqueSample(samples: Array<Vector2>, sampleKeys: Record<string, true>, x: number, y: number) {
27
- const roundedX = math.round(x);
28
- const roundedY = math.round(y);
29
- const key = `${roundedX}:${roundedY}`;
30
- if (sampleKeys[key]) {
31
- return;
32
- }
33
-
34
- sampleKeys[key] = true;
35
- samples.push(new Vector2(roundedX, roundedY));
36
- }
37
-
38
- function getPointerSamples(pointerPosition: Vector2, options: OutsidePointerOptions) {
39
- const insetTopLeft = getGuiInsetTopLeft();
40
-
41
- const samples = new Array<Vector2>();
42
- const sampleKeys: Record<string, true> = {};
43
-
44
- addUniqueSample(samples, sampleKeys, pointerPosition.X, pointerPosition.Y);
45
- addUniqueSample(samples, sampleKeys, pointerPosition.X + insetTopLeft.X, pointerPosition.Y + insetTopLeft.Y);
46
- addUniqueSample(samples, sampleKeys, pointerPosition.X - insetTopLeft.X, pointerPosition.Y - insetTopLeft.Y);
47
-
48
- if (options.layerIgnoresGuiInset) {
49
- addUniqueSample(samples, sampleKeys, pointerPosition.X, pointerPosition.Y + insetTopLeft.Y);
50
- addUniqueSample(samples, sampleKeys, pointerPosition.X, pointerPosition.Y - insetTopLeft.Y);
51
- addUniqueSample(samples, sampleKeys, pointerPosition.X + insetTopLeft.X, pointerPosition.Y);
52
- addUniqueSample(samples, sampleKeys, pointerPosition.X - insetTopLeft.X, pointerPosition.Y);
53
- }
54
-
55
- return samples;
56
- }
57
-
58
- export function isOutsidePointerEvent(
59
- inputObject: InputObject,
60
- container: BasePlayerGui,
61
- contentRoot: GuiObject,
62
- options: OutsidePointerOptions,
63
- ) {
64
- const rawPointerPosition = inputObject.Position;
65
- const pointerPosition = new Vector2(rawPointerPosition.X, rawPointerPosition.Y);
66
- const pointerSamples = getPointerSamples(pointerPosition, options);
67
-
68
- for (const sample of pointerSamples) {
69
- const hitGuiObjects = container.GetGuiObjectsAtPosition(sample.X, sample.Y);
70
- for (const hitObject of hitGuiObjects) {
71
- if (hitObject.IsDescendantOf(contentRoot)) {
72
- return false;
73
- }
74
- }
75
- }
76
-
77
- return true;
78
- }
@@ -1,147 +0,0 @@
1
- import { GuiService, UserInputService } from "../internals/env";
2
- import { isPointerInput, toLayerInteractEvent } from "./events";
3
- import type { LayerInteractEvent } from "./types";
4
-
5
- type LayerEntry = {
6
- id: number;
7
- mountOrder: number;
8
- getEnabled: () => boolean;
9
- isPointerOutside: (inputObject: InputObject) => boolean;
10
- onPointerDownOutside?: (event: LayerInteractEvent) => void;
11
- onInteractOutside?: (event: LayerInteractEvent) => void;
12
- onEscapeKeyDown?: (event: LayerInteractEvent) => void;
13
- onDismiss?: () => void;
14
- };
15
-
16
- type RegisterLayerParams = Omit<LayerEntry, "id" | "mountOrder">;
17
-
18
- export type LayerRegistration = {
19
- id: number;
20
- mountOrder: number;
21
- };
22
-
23
- const layerEntries = new Array<LayerEntry>();
24
- let nextLayerId = 0;
25
- let nextMountOrder = 0;
26
- let inputConnection: RBXScriptConnection | undefined;
27
-
28
- function getTopMostEnabledLayer() {
29
- for (let index = layerEntries.size() - 1; index >= 0; index--) {
30
- const entry = layerEntries[index];
31
- if (entry.getEnabled()) {
32
- return entry;
33
- }
34
- }
35
- return undefined;
36
- }
37
-
38
- function handleDismissEvent(entry: LayerEntry, event: LayerInteractEvent) {
39
- if (!event.defaultPrevented) {
40
- entry.onDismiss?.();
41
- }
42
- }
43
-
44
- function shouldIgnoreEscapeDismiss() {
45
- const focusedTextBox = UserInputService.GetFocusedTextBox();
46
- if (focusedTextBox) {
47
- return true;
48
- }
49
-
50
- const selectedObject = GuiService.SelectedObject;
51
- if (selectedObject && selectedObject.IsA("TextBox")) {
52
- return true;
53
- }
54
-
55
- return false;
56
- }
57
-
58
- function handleInputBegan(inputObject: InputObject, gameProcessedEvent: boolean) {
59
- if (gameProcessedEvent) {
60
- return;
61
- }
62
-
63
- const topLayer = getTopMostEnabledLayer();
64
- if (!topLayer) {
65
- return;
66
- }
67
-
68
- if (inputObject.KeyCode === Enum.KeyCode.Escape) {
69
- if (shouldIgnoreEscapeDismiss()) {
70
- return;
71
- }
72
-
73
- const escapeEvent = toLayerInteractEvent(inputObject);
74
- topLayer.onEscapeKeyDown?.(escapeEvent);
75
- handleDismissEvent(topLayer, escapeEvent);
76
- return;
77
- }
78
-
79
- if (!isPointerInput(inputObject)) {
80
- return;
81
- }
82
-
83
- if (!topLayer.isPointerOutside(inputObject)) {
84
- return;
85
- }
86
-
87
- const outsideEvent = toLayerInteractEvent(inputObject);
88
- topLayer.onPointerDownOutside?.(outsideEvent);
89
- topLayer.onInteractOutside?.(outsideEvent);
90
- handleDismissEvent(topLayer, outsideEvent);
91
- }
92
-
93
- function startInputListener() {
94
- if (inputConnection) {
95
- return;
96
- }
97
-
98
- inputConnection = UserInputService.InputBegan.Connect((inputObject, gameProcessedEvent) => {
99
- handleInputBegan(inputObject, gameProcessedEvent);
100
- });
101
- }
102
-
103
- function stopInputListener() {
104
- if (!inputConnection) {
105
- return;
106
- }
107
-
108
- inputConnection.Disconnect();
109
- inputConnection = undefined;
110
- }
111
-
112
- function syncInputListener() {
113
- if (layerEntries.size() > 0) {
114
- startInputListener();
115
- } else {
116
- stopInputListener();
117
- }
118
- }
119
-
120
- export function registerLayer(params: RegisterLayerParams): LayerRegistration {
121
- nextLayerId += 1;
122
- nextMountOrder += 1;
123
-
124
- const entry: LayerEntry = {
125
- id: nextLayerId,
126
- mountOrder: nextMountOrder,
127
- ...params,
128
- };
129
-
130
- layerEntries.push(entry);
131
- syncInputListener();
132
-
133
- return {
134
- id: entry.id,
135
- mountOrder: entry.mountOrder,
136
- };
137
- }
138
-
139
- export function unregisterLayer(layerId: number) {
140
- const layerIndex = layerEntries.findIndex((entry) => entry.id === layerId);
141
- if (layerIndex === -1) {
142
- return;
143
- }
144
-
145
- layerEntries.remove(layerIndex);
146
- syncInputListener();
147
- }
@@ -1,18 +0,0 @@
1
- import type React from "@rbxts/react";
2
-
3
- export type LayerInteractEvent = {
4
- originalEvent: InputObject;
5
- defaultPrevented: boolean;
6
- preventDefault: () => void;
7
- };
8
-
9
- export type DismissableLayerProps = {
10
- children?: React.ReactNode;
11
- enabled?: boolean;
12
- modal?: boolean;
13
- disableOutsidePointerEvents?: boolean;
14
- onPointerDownOutside?: (event: LayerInteractEvent) => void;
15
- onInteractOutside?: (event: LayerInteractEvent) => void;
16
- onEscapeKeyDown?: (event: LayerInteractEvent) => void;
17
- onDismiss?: () => void;
18
- };
package/src/index.ts DELETED
@@ -1,5 +0,0 @@
1
- export * from "./dismissable/DismissableLayer";
2
- export * from "./dismissable/types";
3
- export * from "./portal/Portal";
4
- export * from "./portal/PortalProvider";
5
- export * from "./presence/Presence";
@@ -1,3 +0,0 @@
1
- export const DEFAULT_DISPLAY_ORDER_BASE = 1000;
2
- export const DEFAULT_PRESENCE_EXIT_FALLBACK_MS = 500;
3
- export const DEFAULT_LAYER_IGNORE_GUI_INSET = true;
@@ -1,7 +0,0 @@
1
- export const UserInputService = game.GetService("UserInputService");
2
- export const GuiService = game.GetService("GuiService");
3
-
4
- export function getGuiInsetTopLeft() {
5
- const [topLeftInset] = GuiService.GetGuiInset();
6
- return topLeftInset;
7
- }
@@ -1,9 +0,0 @@
1
- import { ReactRoblox } from "@lattice-ui/core";
2
- import { usePortalContext } from "./PortalProvider";
3
- import type { PortalProps } from "./types";
4
-
5
- export function Portal(props: PortalProps) {
6
- const contextValue = usePortalContext();
7
- const target = props.container ?? contextValue.container;
8
- return ReactRoblox.createPortal(props.children, target);
9
- }
@@ -1,20 +0,0 @@
1
- import { createStrictContext, React } from "@lattice-ui/core";
2
- import { DEFAULT_DISPLAY_ORDER_BASE } from "../internals/constants";
3
- import type { PortalContextValue, PortalProviderProps } from "./types";
4
-
5
- const [PortalContextProvider, usePortalContext] = createStrictContext<PortalContextValue>("PortalProvider");
6
-
7
- export function PortalProvider(props: PortalProviderProps) {
8
- const displayOrderBase = props.displayOrderBase ?? DEFAULT_DISPLAY_ORDER_BASE;
9
- const contextValue = React.useMemo(
10
- () => ({
11
- container: props.container,
12
- displayOrderBase,
13
- }),
14
- [displayOrderBase, props.container],
15
- );
16
-
17
- return <PortalContextProvider value={contextValue}>{props.children}</PortalContextProvider>;
18
- }
19
-
20
- export { usePortalContext };
@@ -1,17 +0,0 @@
1
- import type React from "@rbxts/react";
2
-
3
- export type PortalContextValue = {
4
- container: BasePlayerGui;
5
- displayOrderBase: number;
6
- };
7
-
8
- export type PortalProviderProps = {
9
- container: BasePlayerGui;
10
- displayOrderBase?: number;
11
- children?: React.ReactNode;
12
- };
13
-
14
- export type PortalProps = {
15
- children?: React.ReactNode;
16
- container?: Instance;
17
- };
@@ -1,93 +0,0 @@
1
- import { React } from "@lattice-ui/core";
2
- import { DEFAULT_PRESENCE_EXIT_FALLBACK_MS } from "../internals/constants";
3
- import type { PresenceProps } from "./types";
4
-
5
- export function Presence(props: PresenceProps) {
6
- const [mounted, setMounted] = React.useState(props.present);
7
- const [isPresent, setIsPresent] = React.useState(props.present);
8
- const mountedRef = React.useRef(mounted);
9
- const fallbackTaskRef = React.useRef<thread>();
10
- const onExitCompleteRef = React.useRef(props.onExitComplete);
11
-
12
- React.useEffect(() => {
13
- onExitCompleteRef.current = props.onExitComplete;
14
- }, [props.onExitComplete]);
15
-
16
- React.useEffect(() => {
17
- mountedRef.current = mounted;
18
- }, [mounted]);
19
-
20
- const completeExit = React.useCallback(() => {
21
- if (!mountedRef.current) {
22
- return;
23
- }
24
-
25
- const fallbackTask = fallbackTaskRef.current;
26
- if (fallbackTask) {
27
- task.cancel(fallbackTask);
28
- fallbackTaskRef.current = undefined;
29
- }
30
-
31
- mountedRef.current = false;
32
- setMounted(false);
33
- onExitCompleteRef.current?.();
34
- }, []);
35
-
36
- React.useEffect(() => {
37
- if (props.present) {
38
- const fallbackTask = fallbackTaskRef.current;
39
- if (fallbackTask) {
40
- task.cancel(fallbackTask);
41
- fallbackTaskRef.current = undefined;
42
- }
43
-
44
- if (!mountedRef.current) {
45
- mountedRef.current = true;
46
- setMounted(true);
47
- }
48
- setIsPresent(true);
49
- return;
50
- }
51
-
52
- if (!mountedRef.current) {
53
- return;
54
- }
55
-
56
- setIsPresent(false);
57
-
58
- const fallbackTask = fallbackTaskRef.current;
59
- if (fallbackTask) {
60
- task.cancel(fallbackTask);
61
- fallbackTaskRef.current = undefined;
62
- }
63
-
64
- const timeoutMs = props.exitFallbackMs ?? DEFAULT_PRESENCE_EXIT_FALLBACK_MS;
65
- fallbackTaskRef.current = task.delay(timeoutMs / 1000, () => {
66
- completeExit();
67
- });
68
- }, [completeExit, props.exitFallbackMs, props.present]);
69
-
70
- React.useEffect(() => {
71
- return () => {
72
- const fallbackTask = fallbackTaskRef.current;
73
- if (fallbackTask) {
74
- task.cancel(fallbackTask);
75
- fallbackTaskRef.current = undefined;
76
- }
77
- };
78
- }, []);
79
-
80
- if (!mounted) {
81
- return undefined;
82
- }
83
-
84
- const render = props.render ?? props.children;
85
- if (!render) {
86
- return undefined;
87
- }
88
-
89
- return render({
90
- isPresent,
91
- onExitComplete: completeExit,
92
- });
93
- }
@@ -1,16 +0,0 @@
1
- import type React from "@rbxts/react";
2
-
3
- export type PresenceRenderState = {
4
- isPresent: boolean;
5
- onExitComplete: () => void;
6
- };
7
-
8
- export type PresenceRender = (state: PresenceRenderState) => React.ReactElement | undefined;
9
-
10
- export type PresenceProps = {
11
- present: boolean;
12
- exitFallbackMs?: number;
13
- onExitComplete?: () => void;
14
- children?: PresenceRender;
15
- render?: PresenceRender;
16
- };
package/tsconfig.json DELETED
@@ -1,16 +0,0 @@
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
- }
@@ -1,25 +0,0 @@
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
- }