summaryrefslogtreecommitdiff
path: root/frontend/src/components/logviewer/ScrollableWindow.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'frontend/src/components/logviewer/ScrollableWindow.tsx')
-rw-r--r--frontend/src/components/logviewer/ScrollableWindow.tsx107
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