diff options
author | 2022-09-08 02:51:52 +0200 | |
---|---|---|
committer | 2022-09-08 15:37:49 +0200 | |
commit | 6d1f78af96e4c0d1ad76c5791b93b0c2d83810e1 (patch) | |
tree | c005aca6da89629fac5d4001a475babd2fcd590c /subprojects/frontend/src/editor/EditorAreaDecorations.tsx | |
parent | feat(frontend): check for updates periodically (diff) | |
download | refinery-6d1f78af96e4c0d1ad76c5791b93b0c2d83810e1.tar.gz refinery-6d1f78af96e4c0d1ad76c5791b93b0c2d83810e1.tar.zst refinery-6d1f78af96e4c0d1ad76c5791b93b0c2d83810e1.zip |
feat(frontend): editor area scroll shadow styling
Diffstat (limited to 'subprojects/frontend/src/editor/EditorAreaDecorations.tsx')
-rw-r--r-- | subprojects/frontend/src/editor/EditorAreaDecorations.tsx | 149 |
1 files changed, 149 insertions, 0 deletions
diff --git a/subprojects/frontend/src/editor/EditorAreaDecorations.tsx b/subprojects/frontend/src/editor/EditorAreaDecorations.tsx new file mode 100644 index 00000000..f33bf25f --- /dev/null +++ b/subprojects/frontend/src/editor/EditorAreaDecorations.tsx | |||
@@ -0,0 +1,149 @@ | |||
1 | import { type CSSObject, type Theme, styled } from '@mui/material/styles'; | ||
2 | import React, { memo, useEffect, useState } from 'react'; | ||
3 | |||
4 | const SHADOW_SIZE = 10; | ||
5 | |||
6 | const EditorAreaDecoration = styled('div')({ | ||
7 | position: 'absolute', | ||
8 | pointerEvents: 'none', | ||
9 | }); | ||
10 | |||
11 | function shadowTheme( | ||
12 | origin: string, | ||
13 | scaleX: boolean, | ||
14 | scaleY: boolean, | ||
15 | ): CSSObject { | ||
16 | function radialGradient(opacity: number, scale: string): string { | ||
17 | return `radial-gradient( | ||
18 | farthest-side at ${origin}, | ||
19 | rgba(0, 0, 0, ${opacity}), | ||
20 | rgba(0, 0, 0, 0) | ||
21 | ) | ||
22 | ${origin} / | ||
23 | ${scaleX ? scale : '100%'} | ||
24 | ${scaleY ? scale : '100%'} | ||
25 | no-repeat`; | ||
26 | } | ||
27 | |||
28 | return { | ||
29 | background: ` | ||
30 | ${radialGradient(0.2, '40%')}, | ||
31 | ${radialGradient(0.14, '50%')}, | ||
32 | ${radialGradient(0.12, '100%')} | ||
33 | `, | ||
34 | }; | ||
35 | } | ||
36 | |||
37 | function animateSize( | ||
38 | theme: Theme, | ||
39 | direction: 'height' | 'width', | ||
40 | opacity: number, | ||
41 | ): CSSObject { | ||
42 | return { | ||
43 | [direction]: opacity * SHADOW_SIZE, | ||
44 | transition: theme.transitions.create(direction, { | ||
45 | duration: theme.transitions.duration.shortest, | ||
46 | easing: theme.transitions.easing.sharp, | ||
47 | }), | ||
48 | }; | ||
49 | } | ||
50 | |||
51 | const TopDecoration = memo( | ||
52 | styled(EditorAreaDecoration, { | ||
53 | shouldForwardProp: (prop) => prop !== 'visible' && prop !== 'opacity', | ||
54 | })<{ | ||
55 | visible: boolean; | ||
56 | opacity: number; | ||
57 | }>(({ theme, visible, opacity }) => ({ | ||
58 | display: visible ? 'block' : 'none', | ||
59 | top: 0, | ||
60 | left: 0, | ||
61 | right: 0, | ||
62 | ...shadowTheme('50% 0', false, true), | ||
63 | ...animateSize(theme, 'height', opacity), | ||
64 | })), | ||
65 | ); | ||
66 | |||
67 | const GutterDecoration = memo( | ||
68 | styled(EditorAreaDecoration, { | ||
69 | shouldForwardProp: (prop) => | ||
70 | prop !== 'top' && | ||
71 | prop !== 'bottom' && | ||
72 | prop !== 'guttersWidth' && | ||
73 | prop !== 'opacity', | ||
74 | })<{ | ||
75 | top: number; | ||
76 | bottom: number; | ||
77 | guttersWidth: number; | ||
78 | opacity: number; | ||
79 | }>(({ theme, top, bottom, guttersWidth, opacity }) => ({ | ||
80 | top, | ||
81 | left: guttersWidth, | ||
82 | bottom, | ||
83 | ...shadowTheme('0 50%', true, false), | ||
84 | ...animateSize(theme, 'width', opacity), | ||
85 | })), | ||
86 | ); | ||
87 | |||
88 | function convertToOpacity(scroll: number): number { | ||
89 | return Math.max(0, Math.min(1, scroll / SHADOW_SIZE)); | ||
90 | } | ||
91 | |||
92 | export default function EditorAreaDecorations({ | ||
93 | parent, | ||
94 | scroller, | ||
95 | }: { | ||
96 | parent: HTMLElement | undefined; | ||
97 | scroller: HTMLElement | undefined; | ||
98 | }): JSX.Element { | ||
99 | const [top, setTop] = useState(0); | ||
100 | const [bottom, setBottom] = useState(0); | ||
101 | const [guttersWidth, setGuttersWidth] = useState(0); | ||
102 | const [topOpacity, setTopOpacity] = useState(0); | ||
103 | const [gutterOpacity, setGutterOpacity] = useState(0); | ||
104 | |||
105 | useEffect(() => { | ||
106 | if (parent === undefined || scroller === undefined) { | ||
107 | return () => {}; | ||
108 | } | ||
109 | const gutters = scroller.querySelector('.cm-gutters'); | ||
110 | |||
111 | const updateBounds = () => { | ||
112 | const parentRect = parent.getBoundingClientRect(); | ||
113 | const rect = scroller.getBoundingClientRect(); | ||
114 | setTop(rect.top - parentRect.top); | ||
115 | setBottom(parentRect.bottom - rect.bottom); | ||
116 | setGuttersWidth(gutters?.clientWidth ?? 0); | ||
117 | }; | ||
118 | updateBounds(); | ||
119 | const resizeObserver = new ResizeObserver(updateBounds); | ||
120 | resizeObserver.observe(scroller); | ||
121 | if (gutters !== null) { | ||
122 | resizeObserver.observe(gutters); | ||
123 | } | ||
124 | |||
125 | const updateScroll = () => { | ||
126 | setTopOpacity(convertToOpacity(scroller.scrollTop)); | ||
127 | setGutterOpacity(convertToOpacity(scroller.scrollLeft)); | ||
128 | }; | ||
129 | updateScroll(); | ||
130 | scroller.addEventListener('scroll', updateScroll); | ||
131 | |||
132 | return () => { | ||
133 | resizeObserver.disconnect(); | ||
134 | scroller.removeEventListener('scroll', updateScroll); | ||
135 | }; | ||
136 | }, [parent, scroller, setTop]); | ||
137 | |||
138 | return ( | ||
139 | <> | ||
140 | <GutterDecoration | ||
141 | top={top} | ||
142 | bottom={bottom} | ||
143 | guttersWidth={guttersWidth} | ||
144 | opacity={gutterOpacity} | ||
145 | /> | ||
146 | <TopDecoration visible={top === 0} opacity={topOpacity} /> | ||
147 | </> | ||
148 | ); | ||
149 | } | ||