diff options
Diffstat (limited to 'frontend/src/components/logviewer/ScrollableWindow.tsx')
| -rw-r--r-- | frontend/src/components/logviewer/ScrollableWindow.tsx | 107 |
1 files changed, 107 insertions, 0 deletions
diff --git a/frontend/src/components/logviewer/ScrollableWindow.tsx b/frontend/src/components/logviewer/ScrollableWindow.tsx new file mode 100644 index 00000000..c1d5e5b4 --- /dev/null +++ b/frontend/src/components/logviewer/ScrollableWindow.tsx @@ -0,0 +1,107 @@ +/* +Big thanks to @jessebofil for this +https://discord.com/channels/960281551428522045/960284327445418044/1209253688363716648 +*/ + +import { Focusable, ModalPosition, GamepadButton, ScrollPanelGroup, gamepadDialogClasses, scrollPanelClasses, FooterLegendProps } from "decky-frontend-lib"; +import { FC, useLayoutEffect, useRef, useState } from "react"; + +export interface ScrollableWindowProps extends FooterLegendProps { + height: string; + fadeAmount?: string; + scrollBarWidth?: string; + alwaysFocus?: boolean; + noScrollDescription?: boolean; + + onActivate?: (e: CustomEvent) => void; + onCancel?: (e: CustomEvent) => void; +} + +const ScrollableWindow: FC<ScrollableWindowProps> = ({ height, fadeAmount, scrollBarWidth, alwaysFocus, noScrollDescription, children, actionDescriptionMap, ...focusableProps }) => { + const fade = fadeAmount === undefined || fadeAmount === '' ? '10px' : fadeAmount; + const barWidth = scrollBarWidth === undefined || scrollBarWidth === '' ? '4px' : scrollBarWidth; + const [isOverflowing, setIsOverflowing] = useState(false); + const scrollPanelRef = useRef<HTMLElement>(); + + useLayoutEffect(() => { + const { current } = scrollPanelRef; + const trigger = () => { + if (current) { + const hasOverflow = current.scrollHeight > current.clientHeight; + setIsOverflowing(hasOverflow); + } + }; + if (current) trigger(); + }, [children, height]); + + const panel = ( + <ScrollPanelGroup + //@ts-ignore + ref={scrollPanelRef} focusable={false} style={{ flex: 1, minHeight: 0 }}> + <Focusable + //@ts-ignore + focusable={alwaysFocus || isOverflowing} + key={'scrollable-window-focusable-element'} + noFocusRing={true} + actionDescriptionMap={Object.assign(noScrollDescription ? {} : + { + [GamepadButton.DIR_UP]: 'Scroll Up', + [GamepadButton.DIR_DOWN]: 'Scroll Down' + }, + actionDescriptionMap ?? {} + )} + {...focusableProps} + > + {children} + </Focusable> + </ScrollPanelGroup> + ); + + return ( + <> + <style> + {`.modal-position-container .${gamepadDialogClasses.ModalPosition} { + top: 0; + bottom: 0; + padding: 0; + } + .modal-position-container .${scrollPanelClasses.ScrollPanel}::-webkit-scrollbar { + display: initial !important; + width: ${barWidth}; + } + .modal-position-container .${scrollPanelClasses.ScrollPanel}::-webkit-scrollbar-thumb { + border: 0; + }`} + </style> + <div + className='modal-position-container' + style={{ + position: 'relative', + height: height, + WebkitMask: `linear-gradient(to right , transparent, transparent calc(100% - ${barWidth}), white calc(100% - ${barWidth})), linear-gradient(to bottom, transparent, black ${fade}, black calc(100% - ${fade}), transparent 100%)` + }}> + {isOverflowing ? ( + <ModalPosition key={'scrollable-window-modal-position'}> + {panel} + </ModalPosition> + ) : ( + <div className={`${gamepadDialogClasses.ModalPosition} ${gamepadDialogClasses.WithStandardPadding} Panel`} key={'modal-position'}> + {panel} + </div> + )} + </div> + </> + ); +}; + +interface ScrollableWindowAutoProps extends Omit<ScrollableWindowProps, 'height'> { + heightPercent?: number; +} + +export const ScrollableWindowRelative: FC<ScrollableWindowAutoProps> = ({ heightPercent, ...props }) => { + return ( + <div style={{ flex: 'auto' }}> + <ScrollableWindow height={`${heightPercent ?? 100}%`} {...props} /> + </div> + ); +};
\ No newline at end of file |
