diff options
Diffstat (limited to 'frontend/src/components/modals/DropdownMultiselect.tsx')
| -rw-r--r-- | frontend/src/components/modals/DropdownMultiselect.tsx | 121 |
1 files changed, 121 insertions, 0 deletions
diff --git a/frontend/src/components/modals/DropdownMultiselect.tsx b/frontend/src/components/modals/DropdownMultiselect.tsx new file mode 100644 index 00000000..5defbfa4 --- /dev/null +++ b/frontend/src/components/modals/DropdownMultiselect.tsx @@ -0,0 +1,121 @@ +import { + DialogButton, + DialogCheckbox, + DialogCheckboxProps, + Marquee, + Menu, + MenuItem, + findModuleChild, + showContextMenu, +} from 'decky-frontend-lib'; +import { FC, useCallback, useEffect, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { FaChevronDown } from 'react-icons/fa'; + +const dropDownControlButtonClass = findModuleChild((m) => { + if (typeof m !== 'object') return undefined; + for (const prop in m) { + if (m[prop]?.toString()?.includes('gamepaddropdown_DropDownControlButton')) { + return m[prop]; + } + } +}); + +const DropdownMultiselectItem: FC< + { + value: any; + onSelect: (checked: boolean, value: any) => void; + checked: boolean; + } & DialogCheckboxProps +> = ({ value, onSelect, checked: defaultChecked, ...rest }) => { + const [checked, setChecked] = useState(defaultChecked); + + useEffect(() => { + onSelect?.(checked, value); + }, [checked, onSelect, value]); + + return ( + <MenuItem bInteractableItem onClick={() => setChecked((x) => !x)}> + <DialogCheckbox + style={{ marginBottom: 0, padding: 0 }} + className="decky_DropdownMultiselectItem_DialogCheckbox" + bottomSeparator="none" + {...rest} + onClick={() => setChecked((x) => !x)} + onChange={(checked) => setChecked(checked)} + controlled + checked={checked} + /> + </MenuItem> + ); +}; + +const DropdownMultiselect: FC<{ + items: { + label: string; + value: string; + }[]; + selected: string[]; + onSelect: (selected: any[]) => void; + label: string; +}> = ({ label, items, selected, onSelect }) => { + const [itemsSelected, setItemsSelected] = useState<any>(selected); + const { t } = useTranslation(); + + const handleItemSelect = useCallback((checked, value) => { + setItemsSelected((x: any) => + checked ? [...x.filter((y: any) => y !== value), value] : x.filter((y: any) => y !== value), + ); + }, []); + + useEffect(() => { + onSelect(itemsSelected); + }, [itemsSelected, onSelect]); + + return ( + <DialogButton + style={{ + display: 'flex', + alignItems: 'center', + maxWidth: '100%', + }} + className={dropDownControlButtonClass} + onClick={(evt) => { + evt.preventDefault(); + showContextMenu( + <Menu label={label} cancelText={t('DropdownMultiselect.button.back') as string}> + <style> + {` + /* Inherit color from ".basiccontextmenu" */ + .decky_DropdownMultiselectItem_DialogCheckbox > .DialogToggle_Label { + color: inherit; + } + `} + </style> + <div style={{ marginTop: '10px' }}>{/*FIXME: Hack for missing padding under label menu*/}</div> + {items.map((x) => ( + <DropdownMultiselectItem + key={x.value} + label={x.label} + value={x.value} + checked={itemsSelected.includes(x.value)} + onSelect={handleItemSelect} + /> + ))} + </Menu>, + evt.currentTarget ?? window, + ); + }} + > + <Marquee> + {selected.length > 0 + ? selected.map((x: any) => items[items.findIndex((v) => v.value === x)].label).join(', ') + : '…'} + </Marquee> + <div style={{ flexGrow: 1, minWidth: '1ch' }} /> + <FaChevronDown style={{ height: '1em', flex: '0 0 1em' }} /> + </DialogButton> + ); +}; + +export default DropdownMultiselect; |
