From 5ef40fa6bc137a022d8bf2d8a54268c1475f04ff Mon Sep 17 00:00:00 2001 From: SkyratBot <59378654+SkyratBot@users.noreply.github.com> Date: Sun, 19 Dec 2021 23:17:52 +0100 Subject: [PATCH] Fixes list input submit button not working. (#10146) Co-authored-by: AnturK --- .../tgui/interfaces/ListInputModal.tsx | 210 ++++++++++++++++++ 1 file changed, 210 insertions(+) create mode 100644 tgui/packages/tgui/interfaces/ListInputModal.tsx diff --git a/tgui/packages/tgui/interfaces/ListInputModal.tsx b/tgui/packages/tgui/interfaces/ListInputModal.tsx new file mode 100644 index 00000000000..5a758a52d64 --- /dev/null +++ b/tgui/packages/tgui/interfaces/ListInputModal.tsx @@ -0,0 +1,210 @@ +import { Loader } from './common/Loader'; +import { InputButtons, Preferences, Validator } from './common/InputButtons'; +import { Button, Input, Section, Stack } from '../components'; +import { KEY_ENTER, KEY_DOWN, KEY_UP, KEY_ESCAPE } from '../../common/keycodes'; +import { Window } from '../layouts'; +import { useBackend, useLocalState } from '../backend'; + +type ListInputData = { + items: string[]; + message: string; + preferences: Preferences; + timeout: number; + title: string; +}; + +export const ListInputModal = (_, context) => { + const { act, data } = useBackend(context); + const { items = [], message, preferences, timeout, title } = data; + const { large_buttons } = preferences; + const [selected, setSelected] = useLocalState( + context, + 'selected', + 0 + ); + const [searchBarVisible, setSearchBarVisible] = useLocalState( + context, + 'searchBarVisible', + items.length > 9 + ); + const [searchQuery, setSearchQuery] = useLocalState( + context, + 'searchQuery', + '' + ); + const [inputIsValid, setInputIsValid] = useLocalState( + context, + 'inputIsValid', + { isValid: true, error: null } + ); + // User presses up or down on keyboard + // Simulates clicking an item + const onArrowKey = (key: number) => { + const len = filteredItems.length - 1; + if (key === KEY_DOWN) { + if (selected === null || selected === len) { + onClick(0); + } else { + onClick(selected + 1); + } + } else if (key === KEY_UP) { + if (selected === null || selected === 0) { + onClick(len); + } else { + onClick(selected - 1); + } + } + }; + // User selects an item with mouse + const onClick = (index: number) => { + if (isNaN(index) || index === selected) { + setInputIsValid({ isValid: false, error: 'No selection' }); + setSelected(null); + } else { + setInputIsValid({ isValid: true, error: null }); + setSelected(index); + document!.getElementById(index.toString())?.focus(); + } + }; + // User doesn't have search bar visible & presses a key + const onLetterKey = (key: number) => { + const keyChar = String.fromCharCode(key); + const foundItem = items.find((item) => { + return item?.toLowerCase().startsWith(keyChar?.toLowerCase()); + }); + if (foundItem) { + setSelected(filteredItems.indexOf(foundItem)); + document!.getElementById(filteredItems + .indexOf(foundItem) + .toString())?.focus(); + } + }; + // User types into search bar + const onSearch = (query: string) => { + setSearchQuery(query); + }; + // User presses the search button + const onSearchBarToggle = () => { + setSearchBarVisible(!searchBarVisible); + setSearchQuery(''); + }; + const filteredItems = items.filter((item) => + item?.toLowerCase().includes(searchQuery.toLowerCase()) + ); + // Dynamically changes the window height based on the message. + const windowHeight + = 325 + Math.ceil(message?.length / 3) + (large_buttons ? 5 : 0); + + return ( + + {timeout && } + { + const keyCode = window.event ? event.which : event.keyCode; + if (keyCode === KEY_DOWN || keyCode === KEY_UP) { + event.preventDefault(); + onArrowKey(keyCode); + } + if (!searchBarVisible && keyCode >= 65 && keyCode <= 90) { + event.preventDefault(); + onLetterKey(keyCode); + } + if (keyCode === KEY_ESCAPE) { + event.preventDefault(); + act('cancel'); + } + }}> +
onSearchBarToggle()} + /> + } + className="ListInput__Section" + fill + title={message}> + + + + + {searchBarVisible && } + + + + +
+
+
+ ); +}; + +/** + * Displays the list of selectable items. + * If a search query is provided, filters the items. + */ +const ListDisplay = (props, context) => { + const { act } = useBackend(context); + const { filteredItems, isValid, onClick, selected } = props; + + return ( +
+ {filteredItems.map((item, index) => { + return ( + + ); + })} +
+ ); +}; + +/** + * Renders a search bar input. + * Closing the bar defaults input to an empty string. + */ +const SearchBar = (props) => { + const { onSearch, searchQuery } = props; + + return ( + { + onSearch(value); + }} + placeholder="Search..." + value={searchQuery} + /> + ); +};