tgui-core 1.0.2 → 1.0.4
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/{src/components → components}/AnimatedNumber.tsx +185 -185
- package/{src/components → components}/BlockQuote.tsx +15 -15
- package/{src/components → components}/BodyZoneSelector.tsx +149 -149
- package/{src/components → components}/Box.tsx +255 -255
- package/{src/components → components}/Button.tsx +415 -415
- package/{src/components → components}/ByondUi.jsx +121 -121
- package/{src/components → components}/Chart.tsx +160 -160
- package/{src/components → components}/ColorBox.tsx +30 -30
- package/{src/components → components}/Dimmer.tsx +19 -19
- package/{src/components → components}/Divider.tsx +26 -26
- package/{src/components → components}/DmIcon.tsx +72 -72
- package/{src/components → components}/DraggableControl.jsx +282 -282
- package/{src/components → components}/Dropdown.tsx +246 -246
- package/{src/components → components}/Flex.tsx +105 -105
- package/{src/components → components}/Icon.tsx +91 -91
- package/{src/components → components}/Input.tsx +181 -181
- package/{src/components → components}/KeyListener.tsx +40 -40
- package/{src/components → components}/Knob.tsx +185 -185
- package/{src/components → components}/LabeledList.tsx +130 -130
- package/{src/components → components}/MenuBar.tsx +233 -238
- package/{src/components → components}/Modal.tsx +25 -25
- package/{src/components → components}/NoticeBox.tsx +48 -48
- package/{src/components → components}/NumberInput.tsx +328 -328
- package/{src/components → components}/ProgressBar.tsx +79 -79
- package/{src/components → components}/RestrictedInput.jsx +301 -301
- package/{src/components → components}/RoundGauge.tsx +189 -189
- package/{src/components → components}/Section.tsx +125 -125
- package/{src/components → components}/Slider.tsx +173 -173
- package/{src/components → components}/Stack.tsx +101 -101
- package/{src/components → components}/Table.tsx +90 -90
- package/{src/components → components}/Tabs.tsx +90 -90
- package/{src/components → components}/TextArea.tsx +198 -198
- package/{src/components → components}/TimeDisplay.jsx +64 -64
- package/components/index.ts +51 -0
- package/{src/debug/KitchenSink.jsx → debug/KitchenSink.tsx} +56 -56
- package/{src/debug/actions.js → debug/actions.ts} +11 -11
- package/{src/debug/hooks.js → debug/hooks.ts} +10 -10
- package/{src/debug/middleware.js → debug/middleware.ts} +67 -86
- package/{src/debug/reducer.js → debug/reducer.ts} +27 -22
- package/{src/debug/selectors.js → debug/selectors.ts} +7 -7
- package/{src/layouts → layouts}/Layout.tsx +75 -75
- package/{src/layouts → layouts}/NtosWindow.tsx +162 -162
- package/{src/layouts → layouts}/Pane.tsx +56 -56
- package/{src/layouts → layouts}/Window.tsx +227 -227
- package/layouts/index.ts +10 -0
- package/package.json +3 -2
- package/src/assets.ts +43 -43
- package/src/backend.ts +368 -369
- package/src/drag.ts +280 -280
- package/src/events.ts +237 -237
- package/src/hotkeys.ts +212 -212
- package/src/renderer.ts +50 -50
- package/stories/Blink.stories.tsx +20 -0
- package/stories/BlockQuote.stories.tsx +23 -0
- package/stories/Box.stories.tsx +27 -0
- package/stories/Button.stories.tsx +68 -0
- package/stories/ByondUi.stories.tsx +45 -0
- package/stories/Collapsible.stories.tsx +23 -0
- package/stories/Flex.stories.tsx +68 -0
- package/stories/Input.stories.tsx +124 -0
- package/stories/LabeledList.stories.tsx +73 -0
- package/stories/Popper.stories.tsx +58 -0
- package/stories/ProgressBar.stories.tsx +58 -0
- package/stories/Stack.stories.tsx +55 -0
- package/stories/Storage.stories.tsx +46 -0
- package/stories/Themes.stories.tsx +30 -0
- package/stories/Tooltip.stories.tsx +48 -0
- package/stories/common.tsx +19 -0
- package/tsconfig.json +0 -21
- package/src/components/Grid.tsx +0 -44
- /package/{src/common → common}/collections.ts +0 -0
- /package/{src/common → common}/color.ts +0 -0
- /package/{src/common → common}/events.ts +0 -0
- /package/{src/common → common}/exhaustive.ts +0 -0
- /package/{src/common → common}/fp.ts +0 -0
- /package/{src/common → common}/keycodes.ts +0 -0
- /package/{src/common → common}/keys.ts +0 -0
- /package/{src/common → common}/math.ts +0 -0
- /package/{src/common → common}/perf.ts +0 -0
- /package/{src/common → common}/random.ts +0 -0
- /package/{src/common → common}/react.ts +0 -0
- /package/{src/common → common}/redux.ts +0 -0
- /package/{src/common → common}/storage.js +0 -0
- /package/{src/common → common}/string.ts +0 -0
- /package/{src/common → common}/timer.ts +0 -0
- /package/{src/common → common}/type-utils.ts +0 -0
- /package/{src/common → common}/types.ts +0 -0
- /package/{src/common → common}/uuid.ts +0 -0
- /package/{src/common → common}/vector.ts +0 -0
- /package/{src/components → components}/Autofocus.tsx +0 -0
- /package/{src/components → components}/Blink.jsx +0 -0
- /package/{src/components → components}/Collapsible.tsx +0 -0
- /package/{src/components → components}/Dialog.tsx +0 -0
- /package/{src/components → components}/FakeTerminal.jsx +0 -0
- /package/{src/components → components}/FitText.tsx +0 -0
- /package/{src/components → components}/Image.tsx +0 -0
- /package/{src/components → components}/InfinitePlane.jsx +0 -0
- /package/{src/components → components}/LabeledControls.tsx +0 -0
- /package/{src/components → components}/Popper.tsx +0 -0
- /package/{src/components → components}/StyleableSection.tsx +0 -0
- /package/{src/components → components}/Tooltip.tsx +0 -0
- /package/{src/components → components}/TrackOutsideClicks.tsx +0 -0
- /package/{src/components → components}/VirtualList.tsx +0 -0
- /package/{src/debug → debug}/index.ts +0 -0
- /package/{src/styles → styles}/base.scss +0 -0
- /package/{src/styles → styles}/colors.scss +0 -0
- /package/{src/styles → styles}/components/BlockQuote.scss +0 -0
- /package/{src/styles → styles}/components/Button.scss +0 -0
- /package/{src/styles → styles}/components/ColorBox.scss +0 -0
- /package/{src/styles → styles}/components/Dialog.scss +0 -0
- /package/{src/styles → styles}/components/Dimmer.scss +0 -0
- /package/{src/styles → styles}/components/Divider.scss +0 -0
- /package/{src/styles → styles}/components/Dropdown.scss +0 -0
- /package/{src/styles → styles}/components/Flex.scss +0 -0
- /package/{src/styles → styles}/components/Icon.scss +0 -0
- /package/{src/styles → styles}/components/Input.scss +0 -0
- /package/{src/styles → styles}/components/Knob.scss +0 -0
- /package/{src/styles → styles}/components/LabeledList.scss +0 -0
- /package/{src/styles → styles}/components/MenuBar.scss +0 -0
- /package/{src/styles → styles}/components/Modal.scss +0 -0
- /package/{src/styles → styles}/components/NoticeBox.scss +0 -0
- /package/{src/styles → styles}/components/NumberInput.scss +0 -0
- /package/{src/styles → styles}/components/ProgressBar.scss +0 -0
- /package/{src/styles → styles}/components/RoundGauge.scss +0 -0
- /package/{src/styles → styles}/components/Section.scss +0 -0
- /package/{src/styles → styles}/components/Slider.scss +0 -0
- /package/{src/styles → styles}/components/Stack.scss +0 -0
- /package/{src/styles → styles}/components/Table.scss +0 -0
- /package/{src/styles → styles}/components/Tabs.scss +0 -0
- /package/{src/styles → styles}/components/TextArea.scss +0 -0
- /package/{src/styles → styles}/components/Tooltip.scss +0 -0
- /package/{src/styles → styles}/functions.scss +0 -0
- /package/{src/styles → styles}/layouts/Layout.scss +0 -0
- /package/{src/styles → styles}/layouts/NtosHeader.scss +0 -0
- /package/{src/styles → styles}/layouts/NtosWindow.scss +0 -0
- /package/{src/styles → styles}/layouts/TitleBar.scss +0 -0
- /package/{src/styles → styles}/layouts/Window.scss +0 -0
- /package/{src/styles → styles}/main.scss +0 -0
- /package/{src/styles → styles}/reset.scss +0 -0
- /package/{src/styles → styles}/themes/abductor.scss +0 -0
- /package/{src/styles → styles}/themes/admin.scss +0 -0
- /package/{src/styles → styles}/themes/cardtable.scss +0 -0
- /package/{src/styles → styles}/themes/hackerman.scss +0 -0
- /package/{src/styles → styles}/themes/malfunction.scss +0 -0
- /package/{src/styles → styles}/themes/neutral.scss +0 -0
- /package/{src/styles → styles}/themes/ntOS95.scss +0 -0
- /package/{src/styles → styles}/themes/ntos.scss +0 -0
- /package/{src/styles → styles}/themes/ntos_cat.scss +0 -0
- /package/{src/styles → styles}/themes/ntos_darkmode.scss +0 -0
- /package/{src/styles → styles}/themes/ntos_lightmode.scss +0 -0
- /package/{src/styles → styles}/themes/ntos_spooky.scss +0 -0
- /package/{src/styles → styles}/themes/ntos_synth.scss +0 -0
- /package/{src/styles → styles}/themes/ntos_terminal.scss +0 -0
- /package/{src/styles → styles}/themes/paper.scss +0 -0
- /package/{src/styles → styles}/themes/retro.scss +0 -0
- /package/{src/styles → styles}/themes/spookyconsole.scss +0 -0
- /package/{src/styles → styles}/themes/syndicate.scss +0 -0
- /package/{src/styles → styles}/themes/wizard.scss +0 -0
|
@@ -1,246 +1,246 @@
|
|
|
1
|
-
import { classes } from '../common/react';
|
|
2
|
-
import { ReactNode, useEffect, useRef, useState } from 'react';
|
|
3
|
-
|
|
4
|
-
import { BoxProps, unit } from './Box';
|
|
5
|
-
import { Button } from './Button';
|
|
6
|
-
import { Icon } from './Icon';
|
|
7
|
-
import { Popper } from './Popper';
|
|
8
|
-
|
|
9
|
-
export type DropdownEntry = {
|
|
10
|
-
displayText: ReactNode;
|
|
11
|
-
value: string | number;
|
|
12
|
-
};
|
|
13
|
-
|
|
14
|
-
type DropdownOption = string | DropdownEntry;
|
|
15
|
-
|
|
16
|
-
type Props = {
|
|
17
|
-
/** An array of strings which will be displayed in the
|
|
18
|
-
dropdown when open. See Dropdown.tsx for more advanced usage with DropdownEntry */
|
|
19
|
-
options: DropdownOption[];
|
|
20
|
-
/** Called when a value is picked from the list, `value` is the value that was picked */
|
|
21
|
-
onSelected: (value: any) => void;
|
|
22
|
-
/** Currently selected entry to display. Can be left stateless to permanently display this value. */
|
|
23
|
-
selected: DropdownOption | null | undefined;
|
|
24
|
-
} & Partial<{
|
|
25
|
-
/** Whether to scroll automatically on open. Defaults to true */
|
|
26
|
-
autoScroll: boolean;
|
|
27
|
-
/** Whether to display previous / next buttons */
|
|
28
|
-
buttons: boolean;
|
|
29
|
-
/** Whether to clip the selected text */
|
|
30
|
-
clipSelectedText: boolean;
|
|
31
|
-
/** Color of dropdown button */
|
|
32
|
-
color: string;
|
|
33
|
-
/** Disables the dropdown */
|
|
34
|
-
disabled: boolean;
|
|
35
|
-
/** Overwrites selection text with this. Good for objects etc. */
|
|
36
|
-
displayText: ReactNode;
|
|
37
|
-
/** Icon to display in dropdown button */
|
|
38
|
-
icon: string;
|
|
39
|
-
/** Angle of the icon */
|
|
40
|
-
iconRotation: number;
|
|
41
|
-
/** Whether or not the icon should spin */
|
|
42
|
-
iconSpin: boolean;
|
|
43
|
-
/** Width of the dropdown menu. Default: 15rem */
|
|
44
|
-
menuWidth: string;
|
|
45
|
-
/** Whether or not the arrow on the right hand side of the dropdown button is visible */
|
|
46
|
-
noChevron: boolean;
|
|
47
|
-
/** Called when dropdown button is clicked */
|
|
48
|
-
onClick: (event) => void;
|
|
49
|
-
/** Dropdown renders over instead of below */
|
|
50
|
-
over: boolean;
|
|
51
|
-
/** Text to show when nothing has been selected. */
|
|
52
|
-
placeholder: string;
|
|
53
|
-
}> &
|
|
54
|
-
BoxProps;
|
|
55
|
-
|
|
56
|
-
enum DIRECTION {
|
|
57
|
-
Previous = 'previous',
|
|
58
|
-
Next = 'next',
|
|
59
|
-
Current = 'current',
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
const NONE = -1;
|
|
63
|
-
|
|
64
|
-
function getOptionValue(option: DropdownOption) {
|
|
65
|
-
return typeof option === 'string' ? option : option.value;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
export function Dropdown(props: Props) {
|
|
69
|
-
const {
|
|
70
|
-
autoScroll = true,
|
|
71
|
-
buttons,
|
|
72
|
-
className,
|
|
73
|
-
clipSelectedText = true,
|
|
74
|
-
color = 'default',
|
|
75
|
-
disabled,
|
|
76
|
-
displayText,
|
|
77
|
-
icon,
|
|
78
|
-
iconRotation,
|
|
79
|
-
iconSpin,
|
|
80
|
-
menuWidth = '15rem',
|
|
81
|
-
noChevron,
|
|
82
|
-
onClick,
|
|
83
|
-
onSelected,
|
|
84
|
-
options = [],
|
|
85
|
-
over,
|
|
86
|
-
placeholder = 'Select...',
|
|
87
|
-
selected,
|
|
88
|
-
width = '15rem',
|
|
89
|
-
} = props;
|
|
90
|
-
|
|
91
|
-
const [open, setOpen] = useState(false);
|
|
92
|
-
const adjustedOpen = over ? !open : open;
|
|
93
|
-
const innerRef = useRef<HTMLDivElement>(null);
|
|
94
|
-
|
|
95
|
-
const selectedIndex =
|
|
96
|
-
options.findIndex((option) => getOptionValue(option) === selected) || 0;
|
|
97
|
-
|
|
98
|
-
function scrollTo(position: number) {
|
|
99
|
-
let scrollPos = position;
|
|
100
|
-
if (position < selectedIndex) {
|
|
101
|
-
scrollPos = position < 2 ? 0 : position - 2;
|
|
102
|
-
} else {
|
|
103
|
-
scrollPos =
|
|
104
|
-
position > options.length - 3 ? options.length - 1 : position - 2;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
const element = innerRef.current?.children[scrollPos];
|
|
108
|
-
element?.scrollIntoView({ block: 'nearest' });
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
/** Update the selected value when clicking the left/right buttons */
|
|
112
|
-
function updateSelected(direction: DIRECTION) {
|
|
113
|
-
if (options.length < 1 || disabled) {
|
|
114
|
-
return;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
const startIndex = 0;
|
|
118
|
-
const endIndex = options.length - 1;
|
|
119
|
-
|
|
120
|
-
let newIndex: number;
|
|
121
|
-
if (selectedIndex < 0) {
|
|
122
|
-
newIndex = direction === 'next' ? endIndex : startIndex; // No selection yet
|
|
123
|
-
} else if (direction === 'next') {
|
|
124
|
-
newIndex = selectedIndex === endIndex ? startIndex : selectedIndex + 1; // Move to next option
|
|
125
|
-
} else {
|
|
126
|
-
newIndex = selectedIndex === startIndex ? endIndex : selectedIndex - 1; // Move to previous option
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
if (open && autoScroll) {
|
|
130
|
-
scrollTo(newIndex);
|
|
131
|
-
}
|
|
132
|
-
onSelected?.(getOptionValue(options[newIndex]));
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
/** Allows the menu to be scrollable on open */
|
|
136
|
-
useEffect(() => {
|
|
137
|
-
if (!open) {
|
|
138
|
-
return;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
if (autoScroll && selectedIndex !== NONE) {
|
|
142
|
-
scrollTo(selectedIndex);
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
innerRef.current?.focus();
|
|
146
|
-
}, [open]);
|
|
147
|
-
|
|
148
|
-
return (
|
|
149
|
-
<Popper
|
|
150
|
-
isOpen={open}
|
|
151
|
-
onClickOutside={() => setOpen(false)}
|
|
152
|
-
placement={over ? 'top-start' : 'bottom-start'}
|
|
153
|
-
content={
|
|
154
|
-
<div
|
|
155
|
-
className="Layout Dropdown__menu"
|
|
156
|
-
style={{ minWidth: menuWidth }}
|
|
157
|
-
ref={innerRef}
|
|
158
|
-
>
|
|
159
|
-
{options.length === 0 && (
|
|
160
|
-
<div className="Dropdown__menuentry">No options</div>
|
|
161
|
-
)}
|
|
162
|
-
|
|
163
|
-
{options.map((option, index) => {
|
|
164
|
-
const value = getOptionValue(option);
|
|
165
|
-
|
|
166
|
-
return (
|
|
167
|
-
<div
|
|
168
|
-
className={classes([
|
|
169
|
-
'Dropdown__menuentry',
|
|
170
|
-
selected === value && 'selected',
|
|
171
|
-
])}
|
|
172
|
-
key={index}
|
|
173
|
-
onClick={() => {
|
|
174
|
-
setOpen(false);
|
|
175
|
-
onSelected?.(value);
|
|
176
|
-
}}
|
|
177
|
-
>
|
|
178
|
-
{typeof option === 'string' ? option : option.displayText}
|
|
179
|
-
</div>
|
|
180
|
-
);
|
|
181
|
-
})}
|
|
182
|
-
</div>
|
|
183
|
-
}
|
|
184
|
-
>
|
|
185
|
-
<div className="Dropdown" style={{ width: unit(width) }}>
|
|
186
|
-
<div
|
|
187
|
-
className={classes([
|
|
188
|
-
'Dropdown__control',
|
|
189
|
-
'Button',
|
|
190
|
-
'Button--dropdown',
|
|
191
|
-
'Button--color--' + color,
|
|
192
|
-
disabled && 'Button--disabled',
|
|
193
|
-
className,
|
|
194
|
-
])}
|
|
195
|
-
onClick={(event) => {
|
|
196
|
-
if (disabled && !open) {
|
|
197
|
-
return;
|
|
198
|
-
}
|
|
199
|
-
setOpen(!open);
|
|
200
|
-
onClick?.(event);
|
|
201
|
-
}}
|
|
202
|
-
>
|
|
203
|
-
{icon && (
|
|
204
|
-
<Icon mr={1} name={icon} rotation={iconRotation} spin={iconSpin} />
|
|
205
|
-
)}
|
|
206
|
-
<span
|
|
207
|
-
className="Dropdown__selected-text"
|
|
208
|
-
style={{
|
|
209
|
-
overflow: clipSelectedText ? 'hidden' : 'visible',
|
|
210
|
-
}}
|
|
211
|
-
>
|
|
212
|
-
{displayText ||
|
|
213
|
-
(selected && getOptionValue(selected)) ||
|
|
214
|
-
placeholder}
|
|
215
|
-
</span>
|
|
216
|
-
{!noChevron && (
|
|
217
|
-
<span className="Dropdown__arrow-button">
|
|
218
|
-
<Icon name={adjustedOpen ? 'chevron-up' : 'chevron-down'} />
|
|
219
|
-
</span>
|
|
220
|
-
)}
|
|
221
|
-
</div>
|
|
222
|
-
{buttons && (
|
|
223
|
-
<>
|
|
224
|
-
<Button
|
|
225
|
-
disabled={disabled}
|
|
226
|
-
height={1.8}
|
|
227
|
-
icon="chevron-left"
|
|
228
|
-
onClick={() => {
|
|
229
|
-
updateSelected(DIRECTION.Previous);
|
|
230
|
-
}}
|
|
231
|
-
/>
|
|
232
|
-
|
|
233
|
-
<Button
|
|
234
|
-
disabled={disabled}
|
|
235
|
-
height={1.8}
|
|
236
|
-
icon="chevron-right"
|
|
237
|
-
onClick={() => {
|
|
238
|
-
updateSelected(DIRECTION.Next);
|
|
239
|
-
}}
|
|
240
|
-
/>
|
|
241
|
-
</>
|
|
242
|
-
)}
|
|
243
|
-
</div>
|
|
244
|
-
</Popper>
|
|
245
|
-
);
|
|
246
|
-
}
|
|
1
|
+
import { classes } from '../common/react';
|
|
2
|
+
import { ReactNode, useEffect, useRef, useState } from 'react';
|
|
3
|
+
|
|
4
|
+
import { BoxProps, unit } from './Box';
|
|
5
|
+
import { Button } from './Button';
|
|
6
|
+
import { Icon } from './Icon';
|
|
7
|
+
import { Popper } from './Popper';
|
|
8
|
+
|
|
9
|
+
export type DropdownEntry = {
|
|
10
|
+
displayText: ReactNode;
|
|
11
|
+
value: string | number;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
type DropdownOption = string | DropdownEntry;
|
|
15
|
+
|
|
16
|
+
type Props = {
|
|
17
|
+
/** An array of strings which will be displayed in the
|
|
18
|
+
dropdown when open. See Dropdown.tsx for more advanced usage with DropdownEntry */
|
|
19
|
+
options: DropdownOption[];
|
|
20
|
+
/** Called when a value is picked from the list, `value` is the value that was picked */
|
|
21
|
+
onSelected: (value: any) => void;
|
|
22
|
+
/** Currently selected entry to display. Can be left stateless to permanently display this value. */
|
|
23
|
+
selected: DropdownOption | null | undefined;
|
|
24
|
+
} & Partial<{
|
|
25
|
+
/** Whether to scroll automatically on open. Defaults to true */
|
|
26
|
+
autoScroll: boolean;
|
|
27
|
+
/** Whether to display previous / next buttons */
|
|
28
|
+
buttons: boolean;
|
|
29
|
+
/** Whether to clip the selected text */
|
|
30
|
+
clipSelectedText: boolean;
|
|
31
|
+
/** Color of dropdown button */
|
|
32
|
+
color: string;
|
|
33
|
+
/** Disables the dropdown */
|
|
34
|
+
disabled: boolean;
|
|
35
|
+
/** Overwrites selection text with this. Good for objects etc. */
|
|
36
|
+
displayText: ReactNode;
|
|
37
|
+
/** Icon to display in dropdown button */
|
|
38
|
+
icon: string;
|
|
39
|
+
/** Angle of the icon */
|
|
40
|
+
iconRotation: number;
|
|
41
|
+
/** Whether or not the icon should spin */
|
|
42
|
+
iconSpin: boolean;
|
|
43
|
+
/** Width of the dropdown menu. Default: 15rem */
|
|
44
|
+
menuWidth: string;
|
|
45
|
+
/** Whether or not the arrow on the right hand side of the dropdown button is visible */
|
|
46
|
+
noChevron: boolean;
|
|
47
|
+
/** Called when dropdown button is clicked */
|
|
48
|
+
onClick: (event) => void;
|
|
49
|
+
/** Dropdown renders over instead of below */
|
|
50
|
+
over: boolean;
|
|
51
|
+
/** Text to show when nothing has been selected. */
|
|
52
|
+
placeholder: string;
|
|
53
|
+
}> &
|
|
54
|
+
BoxProps;
|
|
55
|
+
|
|
56
|
+
enum DIRECTION {
|
|
57
|
+
Previous = 'previous',
|
|
58
|
+
Next = 'next',
|
|
59
|
+
Current = 'current',
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const NONE = -1;
|
|
63
|
+
|
|
64
|
+
function getOptionValue(option: DropdownOption) {
|
|
65
|
+
return typeof option === 'string' ? option : option.value;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function Dropdown(props: Props) {
|
|
69
|
+
const {
|
|
70
|
+
autoScroll = true,
|
|
71
|
+
buttons,
|
|
72
|
+
className,
|
|
73
|
+
clipSelectedText = true,
|
|
74
|
+
color = 'default',
|
|
75
|
+
disabled,
|
|
76
|
+
displayText,
|
|
77
|
+
icon,
|
|
78
|
+
iconRotation,
|
|
79
|
+
iconSpin,
|
|
80
|
+
menuWidth = '15rem',
|
|
81
|
+
noChevron,
|
|
82
|
+
onClick,
|
|
83
|
+
onSelected,
|
|
84
|
+
options = [],
|
|
85
|
+
over,
|
|
86
|
+
placeholder = 'Select...',
|
|
87
|
+
selected,
|
|
88
|
+
width = '15rem',
|
|
89
|
+
} = props;
|
|
90
|
+
|
|
91
|
+
const [open, setOpen] = useState(false);
|
|
92
|
+
const adjustedOpen = over ? !open : open;
|
|
93
|
+
const innerRef = useRef<HTMLDivElement>(null);
|
|
94
|
+
|
|
95
|
+
const selectedIndex =
|
|
96
|
+
options.findIndex((option) => getOptionValue(option) === selected) || 0;
|
|
97
|
+
|
|
98
|
+
function scrollTo(position: number) {
|
|
99
|
+
let scrollPos = position;
|
|
100
|
+
if (position < selectedIndex) {
|
|
101
|
+
scrollPos = position < 2 ? 0 : position - 2;
|
|
102
|
+
} else {
|
|
103
|
+
scrollPos =
|
|
104
|
+
position > options.length - 3 ? options.length - 1 : position - 2;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const element = innerRef.current?.children[scrollPos];
|
|
108
|
+
element?.scrollIntoView({ block: 'nearest' });
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Update the selected value when clicking the left/right buttons */
|
|
112
|
+
function updateSelected(direction: DIRECTION) {
|
|
113
|
+
if (options.length < 1 || disabled) {
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const startIndex = 0;
|
|
118
|
+
const endIndex = options.length - 1;
|
|
119
|
+
|
|
120
|
+
let newIndex: number;
|
|
121
|
+
if (selectedIndex < 0) {
|
|
122
|
+
newIndex = direction === 'next' ? endIndex : startIndex; // No selection yet
|
|
123
|
+
} else if (direction === 'next') {
|
|
124
|
+
newIndex = selectedIndex === endIndex ? startIndex : selectedIndex + 1; // Move to next option
|
|
125
|
+
} else {
|
|
126
|
+
newIndex = selectedIndex === startIndex ? endIndex : selectedIndex - 1; // Move to previous option
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (open && autoScroll) {
|
|
130
|
+
scrollTo(newIndex);
|
|
131
|
+
}
|
|
132
|
+
onSelected?.(getOptionValue(options[newIndex]));
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** Allows the menu to be scrollable on open */
|
|
136
|
+
useEffect(() => {
|
|
137
|
+
if (!open) {
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (autoScroll && selectedIndex !== NONE) {
|
|
142
|
+
scrollTo(selectedIndex);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
innerRef.current?.focus();
|
|
146
|
+
}, [open]);
|
|
147
|
+
|
|
148
|
+
return (
|
|
149
|
+
<Popper
|
|
150
|
+
isOpen={open}
|
|
151
|
+
onClickOutside={() => setOpen(false)}
|
|
152
|
+
placement={over ? 'top-start' : 'bottom-start'}
|
|
153
|
+
content={
|
|
154
|
+
<div
|
|
155
|
+
className="Layout Dropdown__menu"
|
|
156
|
+
style={{ minWidth: menuWidth }}
|
|
157
|
+
ref={innerRef}
|
|
158
|
+
>
|
|
159
|
+
{options.length === 0 && (
|
|
160
|
+
<div className="Dropdown__menuentry">No options</div>
|
|
161
|
+
)}
|
|
162
|
+
|
|
163
|
+
{options.map((option, index) => {
|
|
164
|
+
const value = getOptionValue(option);
|
|
165
|
+
|
|
166
|
+
return (
|
|
167
|
+
<div
|
|
168
|
+
className={classes([
|
|
169
|
+
'Dropdown__menuentry',
|
|
170
|
+
selected === value && 'selected',
|
|
171
|
+
])}
|
|
172
|
+
key={index}
|
|
173
|
+
onClick={() => {
|
|
174
|
+
setOpen(false);
|
|
175
|
+
onSelected?.(value);
|
|
176
|
+
}}
|
|
177
|
+
>
|
|
178
|
+
{typeof option === 'string' ? option : option.displayText}
|
|
179
|
+
</div>
|
|
180
|
+
);
|
|
181
|
+
})}
|
|
182
|
+
</div>
|
|
183
|
+
}
|
|
184
|
+
>
|
|
185
|
+
<div className="Dropdown" style={{ width: unit(width) }}>
|
|
186
|
+
<div
|
|
187
|
+
className={classes([
|
|
188
|
+
'Dropdown__control',
|
|
189
|
+
'Button',
|
|
190
|
+
'Button--dropdown',
|
|
191
|
+
'Button--color--' + color,
|
|
192
|
+
disabled && 'Button--disabled',
|
|
193
|
+
className,
|
|
194
|
+
])}
|
|
195
|
+
onClick={(event) => {
|
|
196
|
+
if (disabled && !open) {
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
setOpen(!open);
|
|
200
|
+
onClick?.(event);
|
|
201
|
+
}}
|
|
202
|
+
>
|
|
203
|
+
{icon && (
|
|
204
|
+
<Icon mr={1} name={icon} rotation={iconRotation} spin={iconSpin} />
|
|
205
|
+
)}
|
|
206
|
+
<span
|
|
207
|
+
className="Dropdown__selected-text"
|
|
208
|
+
style={{
|
|
209
|
+
overflow: clipSelectedText ? 'hidden' : 'visible',
|
|
210
|
+
}}
|
|
211
|
+
>
|
|
212
|
+
{displayText ||
|
|
213
|
+
(selected && getOptionValue(selected)) ||
|
|
214
|
+
placeholder}
|
|
215
|
+
</span>
|
|
216
|
+
{!noChevron && (
|
|
217
|
+
<span className="Dropdown__arrow-button">
|
|
218
|
+
<Icon name={adjustedOpen ? 'chevron-up' : 'chevron-down'} />
|
|
219
|
+
</span>
|
|
220
|
+
)}
|
|
221
|
+
</div>
|
|
222
|
+
{buttons && (
|
|
223
|
+
<>
|
|
224
|
+
<Button
|
|
225
|
+
disabled={disabled}
|
|
226
|
+
height={1.8}
|
|
227
|
+
icon="chevron-left"
|
|
228
|
+
onClick={() => {
|
|
229
|
+
updateSelected(DIRECTION.Previous);
|
|
230
|
+
}}
|
|
231
|
+
/>
|
|
232
|
+
|
|
233
|
+
<Button
|
|
234
|
+
disabled={disabled}
|
|
235
|
+
height={1.8}
|
|
236
|
+
icon="chevron-right"
|
|
237
|
+
onClick={() => {
|
|
238
|
+
updateSelected(DIRECTION.Next);
|
|
239
|
+
}}
|
|
240
|
+
/>
|
|
241
|
+
</>
|
|
242
|
+
)}
|
|
243
|
+
</div>
|
|
244
|
+
</Popper>
|
|
245
|
+
);
|
|
246
|
+
}
|