@@ -2,15 +2,18 @@ import { css } from '@emotion/react';
2
2
import { Crossword as ReactCrossword } from '@guardian/react-crossword-next' ;
3
3
import type { CrosswordProps } from '@guardian/react-crossword-next' ;
4
4
import {
5
+ between ,
5
6
from ,
6
7
headlineBold17 ,
7
8
space ,
8
9
textSans14 ,
9
10
textSansItalic12 ,
10
11
} from '@guardian/source/foundations' ;
11
12
import { Hide } from '@guardian/source/react-components' ;
13
+ import libDebounce from 'lodash.debounce' ;
12
14
import type { ReactNode } from 'react' ;
13
- import { memo } from 'react' ;
15
+ import { memo , useEffect , useRef , useState } from 'react' ;
16
+ import { removeMediaRulePrefix , useMatchMedia } from '../lib/useMatchMedia' ;
14
17
import { palette } from '../palette' ;
15
18
import { AdSlot } from './AdSlot.web' ;
16
19
@@ -51,13 +54,55 @@ const Layout: CrosswordProps['Layout'] = ({
51
54
gridWidth,
52
55
MobileBannerAd,
53
56
} ) => {
57
+ const cluesRef = useRef < HTMLDivElement > ( null ) ;
58
+ const [ showGradient , setShowGradient ] = useState ( false ) ;
59
+
60
+ const betweenTabletAndLeftCol = useMatchMedia (
61
+ removeMediaRulePrefix ( between . tablet . and . leftCol ) ,
62
+ ) ;
63
+
64
+ const updateGradientVisibility = ( ) => {
65
+ const clueList = cluesRef . current ;
66
+ if ( ! clueList ) return ;
67
+ const scrollPos = clueList . scrollTop ;
68
+ const maxScroll = clueList . scrollHeight - clueList . clientHeight ;
69
+ setShowGradient ( scrollPos < maxScroll - 16 ) ;
70
+ } ;
71
+
72
+ useEffect ( ( ) => {
73
+ const clueList = cluesRef . current ;
74
+ if ( ! clueList ) return ;
75
+
76
+ updateGradientVisibility ( ) ;
77
+
78
+ clueList . addEventListener (
79
+ 'scroll' ,
80
+ libDebounce ( updateGradientVisibility , 100 ) ,
81
+ ) ;
82
+ window . addEventListener (
83
+ 'resize' ,
84
+ libDebounce ( updateGradientVisibility , 100 ) ,
85
+ ) ;
86
+
87
+ return ( ) => {
88
+ clueList . removeEventListener (
89
+ 'scroll' ,
90
+ libDebounce ( updateGradientVisibility , 100 ) ,
91
+ ) ;
92
+ window . removeEventListener (
93
+ 'resize' ,
94
+ libDebounce ( updateGradientVisibility , 100 ) ,
95
+ ) ;
96
+ } ;
97
+ } , [ ] ) ;
98
+
54
99
return (
55
100
< div
56
101
css = { css `
57
102
dis play: flex;
58
103
flex- direction: column;
59
104
gap: ${ space [ 4 ] } px;
60
- ${ from . phablet } {
105
+ ${ from . tablet } {
61
106
flex- direction: row;
62
107
}
63
108
` }
@@ -71,7 +116,7 @@ const Layout: CrosswordProps['Layout'] = ({
71
116
< FocusedClue
72
117
additionalCss = { css `
73
118
max- width: ${ gridWidth } px;
74
- ${ from . phablet } {
119
+ ${ from . tablet } {
75
120
dis play: none;
76
121
}
77
122
` }
@@ -85,7 +130,8 @@ const Layout: CrosswordProps['Layout'] = ({
85
130
>
86
131
< FocusedClue
87
132
additionalCss = { css `
88
- ${ from . phablet } {
133
+ max- width: ${ gridWidth } px;
134
+ ${ from . tablet } {
89
135
dis play: none;
90
136
}
91
137
` }
@@ -106,22 +152,67 @@ const Layout: CrosswordProps['Layout'] = ({
106
152
107
153
< div
108
154
css = { css `
109
- ${ textSans14 } ;
155
+ position : relative ;
110
156
flex: 1;
111
157
dis play: flex;
112
- flex- direction: column;
113
- gap: ${ space [ 4 ] } px;
114
- align- items: flex- start;
115
- ${ from . desktop } {
116
- flex- direction: row;
158
+ ${ from . tablet } {
159
+ max- height: ${ gridWidth } px;
160
+ ::after {
161
+ display : ${ showGradient ? 'block' : 'none' } ;
162
+ position : absolute;
163
+ content : '' ;
164
+ bottom : 0 ;
165
+ left : 0 ;
166
+ width : 100% ;
167
+ height : 64px ;
168
+ background-image : linear-gradient (
169
+ 180deg ,
170
+ transparent,
171
+ ${ palette ( '--article-background' ) }
172
+ );
173
+ }
117
174
}
118
- > * {
119
- flex : 1 ;
175
+ ${ from . leftCol } {
176
+ max- height: none;
177
+ ::after {
178
+ background-image : none;
179
+ }
120
180
}
121
181
` }
122
182
>
123
- < Clues direction = "across" Header = { CluesHeader } />
124
- < Clues direction = "down" Header = { CluesHeader } />
183
+ < div
184
+ ref = { cluesRef }
185
+ css = { css `
186
+ ${ textSans14 } ;
187
+ flex: 1;
188
+ dis play: flex;
189
+ flex- direction: column;
190
+ gap: ${ space [ 4 ] } px;
191
+ ${ from . tablet } {
192
+ overflow- y: scroll;
193
+ }
194
+ ${ from . desktop } {
195
+ flex- direction: row;
196
+ }
197
+ ${ from . leftCol } {
198
+ overflow: vis ible;
199
+ }
200
+ > * {
201
+ flex : 1 ;
202
+ }
203
+ ` }
204
+ >
205
+ < Clues
206
+ direction = "across"
207
+ Header = { CluesHeader }
208
+ scrollToSelected = { betweenTabletAndLeftCol }
209
+ />
210
+ < Clues
211
+ direction = "down"
212
+ Header = { CluesHeader }
213
+ scrollToSelected = { betweenTabletAndLeftCol }
214
+ />
215
+ </ div >
125
216
</ div >
126
217
</ div >
127
218
) ;
0 commit comments