summaryrefslogtreecommitdiff
path: root/frontend/src/components/logviewer/ScrollableWindow.tsx
blob: c1d5e5b49bfaf5a1a72f3556d28113a6623862a2 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
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>
    );
};