@lattice-ui/combobox 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.
Files changed (46) hide show
  1. package/README.md +23 -0
  2. package/out/Combobox/ComboboxContent.d.ts +3 -0
  3. package/out/Combobox/ComboboxContent.luau +110 -0
  4. package/out/Combobox/ComboboxGroup.d.ts +3 -0
  5. package/out/Combobox/ComboboxGroup.luau +22 -0
  6. package/out/Combobox/ComboboxInput.d.ts +3 -0
  7. package/out/Combobox/ComboboxInput.luau +88 -0
  8. package/out/Combobox/ComboboxItem.d.ts +3 -0
  9. package/out/Combobox/ComboboxItem.luau +131 -0
  10. package/out/Combobox/ComboboxLabel.d.ts +3 -0
  11. package/out/Combobox/ComboboxLabel.luau +26 -0
  12. package/out/Combobox/ComboboxPortal.d.ts +3 -0
  13. package/out/Combobox/ComboboxPortal.luau +33 -0
  14. package/out/Combobox/ComboboxRoot.d.ts +4 -0
  15. package/out/Combobox/ComboboxRoot.luau +256 -0
  16. package/out/Combobox/ComboboxSeparator.d.ts +3 -0
  17. package/out/Combobox/ComboboxSeparator.luau +22 -0
  18. package/out/Combobox/ComboboxTrigger.d.ts +3 -0
  19. package/out/Combobox/ComboboxTrigger.luau +72 -0
  20. package/out/Combobox/ComboboxValue.d.ts +3 -0
  21. package/out/Combobox/ComboboxValue.luau +47 -0
  22. package/out/Combobox/context.d.ts +3 -0
  23. package/out/Combobox/context.luau +10 -0
  24. package/out/Combobox/logic.d.ts +10 -0
  25. package/out/Combobox/logic.luau +103 -0
  26. package/out/Combobox/types.d.ts +105 -0
  27. package/out/Combobox/types.luau +2 -0
  28. package/out/index.d.ts +24 -0
  29. package/out/init.luau +32 -0
  30. package/package.json +26 -0
  31. package/src/Combobox/ComboboxContent.tsx +142 -0
  32. package/src/Combobox/ComboboxGroup.tsx +19 -0
  33. package/src/Combobox/ComboboxInput.tsx +98 -0
  34. package/src/Combobox/ComboboxItem.tsx +134 -0
  35. package/src/Combobox/ComboboxLabel.tsx +27 -0
  36. package/src/Combobox/ComboboxPortal.tsx +28 -0
  37. package/src/Combobox/ComboboxRoot.tsx +202 -0
  38. package/src/Combobox/ComboboxSeparator.tsx +19 -0
  39. package/src/Combobox/ComboboxTrigger.tsx +89 -0
  40. package/src/Combobox/ComboboxValue.tsx +42 -0
  41. package/src/Combobox/context.ts +6 -0
  42. package/src/Combobox/logic.ts +56 -0
  43. package/src/Combobox/types.ts +119 -0
  44. package/src/index.ts +48 -0
  45. package/tsconfig.json +16 -0
  46. package/tsconfig.typecheck.json +35 -0
package/out/init.luau ADDED
@@ -0,0 +1,32 @@
1
+ -- Compiled with roblox-ts v3.0.0
2
+ local TS = _G[script]
3
+ local exports = {}
4
+ local ComboboxContent = TS.import(script, script, "Combobox", "ComboboxContent").ComboboxContent
5
+ local ComboboxGroup = TS.import(script, script, "Combobox", "ComboboxGroup").ComboboxGroup
6
+ local ComboboxInput = TS.import(script, script, "Combobox", "ComboboxInput").ComboboxInput
7
+ local ComboboxItem = TS.import(script, script, "Combobox", "ComboboxItem").ComboboxItem
8
+ local ComboboxLabel = TS.import(script, script, "Combobox", "ComboboxLabel").ComboboxLabel
9
+ local ComboboxPortal = TS.import(script, script, "Combobox", "ComboboxPortal").ComboboxPortal
10
+ local ComboboxRoot = TS.import(script, script, "Combobox", "ComboboxRoot").ComboboxRoot
11
+ local ComboboxSeparator = TS.import(script, script, "Combobox", "ComboboxSeparator").ComboboxSeparator
12
+ local ComboboxTrigger = TS.import(script, script, "Combobox", "ComboboxTrigger").ComboboxTrigger
13
+ local ComboboxValue = TS.import(script, script, "Combobox", "ComboboxValue").ComboboxValue
14
+ local Combobox = {
15
+ Root = ComboboxRoot,
16
+ Trigger = ComboboxTrigger,
17
+ Input = ComboboxInput,
18
+ Value = ComboboxValue,
19
+ Portal = ComboboxPortal,
20
+ Content = ComboboxContent,
21
+ Item = ComboboxItem,
22
+ Group = ComboboxGroup,
23
+ Label = ComboboxLabel,
24
+ Separator = ComboboxSeparator,
25
+ }
26
+ local _logic = TS.import(script, script, "Combobox", "logic")
27
+ exports.defaultComboboxFilter = _logic.defaultComboboxFilter
28
+ exports.filterComboboxOptions = _logic.filterComboboxOptions
29
+ exports.resolveComboboxInputValue = _logic.resolveComboboxInputValue
30
+ exports.resolveForcedComboboxValue = _logic.resolveForcedComboboxValue
31
+ exports.Combobox = Combobox
32
+ return exports
package/package.json ADDED
@@ -0,0 +1,26 @@
1
+ {
2
+ "name": "@lattice-ui/combobox",
3
+ "version": "0.3.0",
4
+ "private": false,
5
+ "main": "out/init.luau",
6
+ "types": "out/index.d.ts",
7
+ "dependencies": {
8
+ "@lattice-ui/core": "0.3.0",
9
+ "@lattice-ui/focus": "0.3.0",
10
+ "@lattice-ui/layer": "0.3.0",
11
+ "@lattice-ui/popper": "0.3.0"
12
+ },
13
+ "devDependencies": {
14
+ "@rbxts/react": "17.3.7-ts.1",
15
+ "@rbxts/react-roblox": "17.3.7-ts.1"
16
+ },
17
+ "peerDependencies": {
18
+ "@rbxts/react": "^17",
19
+ "@rbxts/react-roblox": "^17"
20
+ },
21
+ "scripts": {
22
+ "build": "rbxtsc -p tsconfig.json",
23
+ "typecheck": "tsc -p tsconfig.typecheck.json",
24
+ "watch": "rbxtsc -p tsconfig.json -w"
25
+ }
26
+ }
@@ -0,0 +1,142 @@
1
+ import { React, Slot } from "@lattice-ui/core";
2
+ import { RovingFocusGroup } from "@lattice-ui/focus";
3
+ import { DismissableLayer, Presence } from "@lattice-ui/layer";
4
+ import { usePopper } from "@lattice-ui/popper";
5
+ import { useComboboxContext } from "./context";
6
+ import type { ComboboxContentProps } from "./types";
7
+
8
+ type ComboboxContentImplProps = {
9
+ enabled: boolean;
10
+ visible: boolean;
11
+ onDismiss: () => void;
12
+ asChild?: boolean;
13
+ placement?: ComboboxContentProps["placement"];
14
+ offset?: ComboboxContentProps["offset"];
15
+ padding?: ComboboxContentProps["padding"];
16
+ } & Pick<ComboboxContentProps, "children" | "onEscapeKeyDown" | "onInteractOutside" | "onPointerDownOutside">;
17
+
18
+ function toGuiObject(instance: Instance | undefined) {
19
+ if (!instance || !instance.IsA("GuiObject")) {
20
+ return undefined;
21
+ }
22
+
23
+ return instance;
24
+ }
25
+
26
+ function ComboboxContentImpl(props: ComboboxContentImplProps) {
27
+ const comboboxContext = useComboboxContext();
28
+
29
+ const popper = usePopper({
30
+ anchorRef: comboboxContext.triggerRef,
31
+ contentRef: comboboxContext.contentRef,
32
+ placement: props.placement,
33
+ offset: props.offset,
34
+ padding: props.padding,
35
+ enabled: props.enabled,
36
+ });
37
+
38
+ const setContentRef = React.useCallback(
39
+ (instance: Instance | undefined) => {
40
+ comboboxContext.contentRef.current = toGuiObject(instance);
41
+ },
42
+ [comboboxContext.contentRef],
43
+ );
44
+
45
+ const contentNode = props.asChild ? (
46
+ (() => {
47
+ const child = props.children;
48
+ if (!React.isValidElement(child)) {
49
+ error("[ComboboxContent] `asChild` requires a child element.");
50
+ }
51
+
52
+ return (
53
+ <Slot AnchorPoint={popper.anchorPoint} Position={popper.position} Visible={props.visible} ref={setContentRef}>
54
+ {child}
55
+ </Slot>
56
+ );
57
+ })()
58
+ ) : (
59
+ <frame
60
+ AnchorPoint={popper.anchorPoint}
61
+ BackgroundTransparency={1}
62
+ BorderSizePixel={0}
63
+ Position={popper.position}
64
+ Size={UDim2.fromOffset(0, 0)}
65
+ Visible={props.visible}
66
+ ref={setContentRef}
67
+ >
68
+ {props.children}
69
+ </frame>
70
+ );
71
+
72
+ return (
73
+ <DismissableLayer
74
+ enabled={props.enabled}
75
+ modal={false}
76
+ onDismiss={props.onDismiss}
77
+ onEscapeKeyDown={props.onEscapeKeyDown}
78
+ onInteractOutside={props.onInteractOutside}
79
+ onPointerDownOutside={props.onPointerDownOutside}
80
+ >
81
+ <RovingFocusGroup active={props.enabled} autoFocus="first" loop={comboboxContext.loop} orientation="vertical">
82
+ {contentNode}
83
+ </RovingFocusGroup>
84
+ </DismissableLayer>
85
+ );
86
+ }
87
+
88
+ export function ComboboxContent(props: ComboboxContentProps) {
89
+ const comboboxContext = useComboboxContext();
90
+ const open = comboboxContext.open;
91
+ const forceMount = props.forceMount === true;
92
+
93
+ const handleDismiss = React.useCallback(() => {
94
+ comboboxContext.setOpen(false);
95
+ }, [comboboxContext]);
96
+
97
+ if (!open && !forceMount) {
98
+ return undefined;
99
+ }
100
+
101
+ if (forceMount) {
102
+ return (
103
+ <ComboboxContentImpl
104
+ asChild={props.asChild}
105
+ enabled={open}
106
+ offset={props.offset}
107
+ onDismiss={handleDismiss}
108
+ onEscapeKeyDown={props.onEscapeKeyDown}
109
+ onInteractOutside={props.onInteractOutside}
110
+ onPointerDownOutside={props.onPointerDownOutside}
111
+ padding={props.padding}
112
+ placement={props.placement}
113
+ visible={open}
114
+ >
115
+ {props.children}
116
+ </ComboboxContentImpl>
117
+ );
118
+ }
119
+
120
+ return (
121
+ <Presence
122
+ exitFallbackMs={0}
123
+ present={open}
124
+ render={(state) => (
125
+ <ComboboxContentImpl
126
+ asChild={props.asChild}
127
+ enabled={state.isPresent}
128
+ offset={props.offset}
129
+ onDismiss={handleDismiss}
130
+ onEscapeKeyDown={props.onEscapeKeyDown}
131
+ onInteractOutside={props.onInteractOutside}
132
+ onPointerDownOutside={props.onPointerDownOutside}
133
+ padding={props.padding}
134
+ placement={props.placement}
135
+ visible={state.isPresent}
136
+ >
137
+ {props.children}
138
+ </ComboboxContentImpl>
139
+ )}
140
+ />
141
+ );
142
+ }
@@ -0,0 +1,19 @@
1
+ import { React, Slot } from "@lattice-ui/core";
2
+ import type { ComboboxGroupProps } from "./types";
3
+
4
+ export function ComboboxGroup(props: ComboboxGroupProps) {
5
+ if (props.asChild) {
6
+ const child = props.children;
7
+ if (!child) {
8
+ error("[ComboboxGroup] `asChild` requires a child element.");
9
+ }
10
+
11
+ return <Slot>{child}</Slot>;
12
+ }
13
+
14
+ return (
15
+ <frame BackgroundTransparency={1} BorderSizePixel={0} Size={UDim2.fromOffset(220, 108)}>
16
+ {props.children}
17
+ </frame>
18
+ );
19
+ }
@@ -0,0 +1,98 @@
1
+ import { React, Slot } from "@lattice-ui/core";
2
+ import { useComboboxContext } from "./context";
3
+ import type { ComboboxInputProps } from "./types";
4
+
5
+ function toTextBox(instance: Instance | undefined) {
6
+ if (!instance || !instance.IsA("TextBox")) {
7
+ return undefined;
8
+ }
9
+
10
+ return instance;
11
+ }
12
+
13
+ export function ComboboxInput(props: ComboboxInputProps) {
14
+ const comboboxContext = useComboboxContext();
15
+ const disabled = comboboxContext.disabled || props.disabled === true;
16
+ const readOnly = comboboxContext.readOnly || props.readOnly === true;
17
+
18
+ const setInputRef = React.useCallback(
19
+ (instance: Instance | undefined) => {
20
+ comboboxContext.inputRef.current = toTextBox(instance);
21
+ },
22
+ [comboboxContext.inputRef],
23
+ );
24
+
25
+ const handleTextChanged = React.useCallback(
26
+ (textBox: TextBox) => {
27
+ if (disabled || readOnly) {
28
+ if (textBox.Text !== comboboxContext.inputValue) {
29
+ textBox.Text = comboboxContext.inputValue;
30
+ }
31
+
32
+ return;
33
+ }
34
+
35
+ comboboxContext.setInputValue(textBox.Text);
36
+ },
37
+ [comboboxContext, disabled, readOnly],
38
+ );
39
+
40
+ const handleFocusLost = React.useCallback(() => {
41
+ comboboxContext.setOpen(false);
42
+ comboboxContext.syncInputFromValue();
43
+ }, [comboboxContext]);
44
+
45
+ const handleInputBegan = React.useCallback(
46
+ (_rbx: GuiObject, inputObject: InputObject) => {
47
+ if (disabled) {
48
+ return;
49
+ }
50
+
51
+ const keyCode = inputObject.KeyCode;
52
+ if (keyCode === Enum.KeyCode.Down || keyCode === Enum.KeyCode.Up) {
53
+ comboboxContext.setOpen(true);
54
+ }
55
+ },
56
+ [comboboxContext, disabled],
57
+ );
58
+
59
+ const sharedProps = {
60
+ Active: !disabled,
61
+ ClearTextOnFocus: false,
62
+ PlaceholderText: props.placeholder ?? "Type to filter",
63
+ Selectable: !disabled,
64
+ Text: comboboxContext.inputValue,
65
+ TextEditable: !disabled && !readOnly,
66
+ Change: {
67
+ Text: handleTextChanged,
68
+ },
69
+ Event: {
70
+ FocusLost: handleFocusLost,
71
+ InputBegan: handleInputBegan,
72
+ },
73
+ ref: setInputRef,
74
+ };
75
+
76
+ if (props.asChild) {
77
+ const child = props.children;
78
+ if (!child) {
79
+ error("[ComboboxInput] `asChild` requires a child element.");
80
+ }
81
+
82
+ return <Slot {...sharedProps}>{child}</Slot>;
83
+ }
84
+
85
+ return (
86
+ <textbox
87
+ {...sharedProps}
88
+ BackgroundColor3={Color3.fromRGB(39, 46, 61)}
89
+ BorderSizePixel={0}
90
+ Size={UDim2.fromOffset(240, 36)}
91
+ TextColor3={disabled ? Color3.fromRGB(137, 145, 162) : Color3.fromRGB(235, 240, 248)}
92
+ TextSize={15}
93
+ TextXAlignment={Enum.TextXAlignment.Left}
94
+ >
95
+ <uipadding PaddingLeft={new UDim(0, 10)} PaddingRight={new UDim(0, 10)} />
96
+ </textbox>
97
+ );
98
+ }
@@ -0,0 +1,134 @@
1
+ import { React, Slot } from "@lattice-ui/core";
2
+ import { RovingFocusItem } from "@lattice-ui/focus";
3
+ import { useComboboxContext } from "./context";
4
+ import type { ComboboxItemProps } from "./types";
5
+
6
+ let nextItemId = 0;
7
+ let nextItemOrder = 0;
8
+
9
+ function toGuiObject(instance: Instance | undefined) {
10
+ if (!instance || !instance.IsA("GuiObject")) {
11
+ return undefined;
12
+ }
13
+
14
+ return instance;
15
+ }
16
+
17
+ export function ComboboxItem(props: ComboboxItemProps) {
18
+ const comboboxContext = useComboboxContext();
19
+ const itemRef = React.useRef<GuiObject>();
20
+
21
+ const itemQueryMatch = comboboxContext.filterFn(props.textValue ?? props.value, comboboxContext.inputValue);
22
+ const disabled = comboboxContext.disabled || props.disabled === true || !itemQueryMatch;
23
+ const textValue = props.textValue ?? props.value;
24
+
25
+ const disabledRef = React.useRef(disabled);
26
+ const textValueRef = React.useRef(textValue);
27
+
28
+ React.useEffect(() => {
29
+ disabledRef.current = disabled;
30
+ }, [disabled]);
31
+
32
+ React.useEffect(() => {
33
+ textValueRef.current = textValue;
34
+ }, [textValue]);
35
+
36
+ const itemIdRef = React.useRef(0);
37
+ if (itemIdRef.current === 0) {
38
+ nextItemId += 1;
39
+ itemIdRef.current = nextItemId;
40
+ }
41
+
42
+ const itemOrderRef = React.useRef(0);
43
+ if (itemOrderRef.current === 0) {
44
+ nextItemOrder += 1;
45
+ itemOrderRef.current = nextItemOrder;
46
+ }
47
+
48
+ React.useEffect(() => {
49
+ return comboboxContext.registerItem({
50
+ id: itemIdRef.current,
51
+ value: props.value,
52
+ order: itemOrderRef.current,
53
+ getNode: () => itemRef.current,
54
+ getDisabled: () => disabledRef.current,
55
+ getTextValue: () => textValueRef.current,
56
+ });
57
+ }, [comboboxContext, props.value]);
58
+
59
+ const setItemRef = React.useCallback((instance: Instance | undefined) => {
60
+ itemRef.current = toGuiObject(instance);
61
+ }, []);
62
+
63
+ const handleSelect = React.useCallback(() => {
64
+ if (disabled) {
65
+ return;
66
+ }
67
+
68
+ comboboxContext.setValue(props.value);
69
+ comboboxContext.setOpen(false);
70
+ }, [comboboxContext, disabled, props.value]);
71
+
72
+ const handleInputBegan = React.useCallback(
73
+ (_rbx: GuiObject, inputObject: InputObject) => {
74
+ if (disabled) {
75
+ return;
76
+ }
77
+
78
+ const keyCode = inputObject.KeyCode;
79
+ if (keyCode !== Enum.KeyCode.Return && keyCode !== Enum.KeyCode.Space) {
80
+ return;
81
+ }
82
+
83
+ comboboxContext.setValue(props.value);
84
+ comboboxContext.setOpen(false);
85
+ },
86
+ [comboboxContext, disabled, props.value],
87
+ );
88
+
89
+ const eventHandlers = React.useMemo(
90
+ () => ({
91
+ Activated: handleSelect,
92
+ InputBegan: handleInputBegan,
93
+ }),
94
+ [handleInputBegan, handleSelect],
95
+ );
96
+
97
+ if (props.asChild) {
98
+ const child = props.children;
99
+ if (!child) {
100
+ error("[ComboboxItem] `asChild` requires a child element.");
101
+ }
102
+
103
+ return (
104
+ <RovingFocusItem asChild disabled={disabled}>
105
+ <Slot Active={!disabled} Event={eventHandlers} Selectable={!disabled} Visible={itemQueryMatch} ref={setItemRef}>
106
+ {child}
107
+ </Slot>
108
+ </RovingFocusItem>
109
+ );
110
+ }
111
+
112
+ return (
113
+ <RovingFocusItem asChild disabled={disabled}>
114
+ <textbutton
115
+ Active={!disabled}
116
+ AutoButtonColor={false}
117
+ BackgroundColor3={Color3.fromRGB(47, 53, 68)}
118
+ BorderSizePixel={0}
119
+ Event={eventHandlers}
120
+ Selectable={!disabled}
121
+ Size={UDim2.fromOffset(220, 32)}
122
+ Text={textValue}
123
+ TextColor3={disabled ? Color3.fromRGB(134, 141, 156) : Color3.fromRGB(234, 239, 247)}
124
+ TextSize={15}
125
+ TextXAlignment={Enum.TextXAlignment.Left}
126
+ Visible={itemQueryMatch}
127
+ ref={setItemRef}
128
+ >
129
+ <uipadding PaddingLeft={new UDim(0, 10)} PaddingRight={new UDim(0, 10)} />
130
+ {props.children}
131
+ </textbutton>
132
+ </RovingFocusItem>
133
+ );
134
+ }
@@ -0,0 +1,27 @@
1
+ import { React, Slot } from "@lattice-ui/core";
2
+ import type { ComboboxLabelProps } from "./types";
3
+
4
+ export function ComboboxLabel(props: ComboboxLabelProps) {
5
+ if (props.asChild) {
6
+ const child = props.children;
7
+ if (!child) {
8
+ error("[ComboboxLabel] `asChild` requires a child element.");
9
+ }
10
+
11
+ return <Slot>{child}</Slot>;
12
+ }
13
+
14
+ return (
15
+ <textlabel
16
+ BackgroundTransparency={1}
17
+ BorderSizePixel={0}
18
+ Size={UDim2.fromOffset(220, 20)}
19
+ Text="Label"
20
+ TextColor3={Color3.fromRGB(168, 176, 191)}
21
+ TextSize={13}
22
+ TextXAlignment={Enum.TextXAlignment.Left}
23
+ >
24
+ {props.children}
25
+ </textlabel>
26
+ );
27
+ }
@@ -0,0 +1,28 @@
1
+ import { React } from "@lattice-ui/core";
2
+ import { Portal, PortalProvider, usePortalContext } from "@lattice-ui/layer";
3
+ import type { ComboboxPortalProps } from "./types";
4
+
5
+ function ComboboxPortalWithOverrides(props: ComboboxPortalProps) {
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 ComboboxPortal(props: ComboboxPortalProps) {
18
+ const hasOverrides = props.container !== undefined || props.displayOrderBase !== undefined;
19
+ if (hasOverrides) {
20
+ return (
21
+ <ComboboxPortalWithOverrides container={props.container} displayOrderBase={props.displayOrderBase}>
22
+ {props.children}
23
+ </ComboboxPortalWithOverrides>
24
+ );
25
+ }
26
+
27
+ return <Portal>{props.children}</Portal>;
28
+ }