@ehfuse/overlay-scrollbar 1.2.6 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +55 -202
- package/dist/index.d.ts +28 -12
- package/dist/index.esm.js +679 -104
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +678 -103
- package/dist/index.js.map +1 -1
- package/dist/src/OverlayScrollbar.d.ts +28 -12
- package/dist/src/OverlayScrollbar.d.ts.map +1 -1
- package/dist/src/utils/dragScrollUtils.d.ts +40 -0
- package/dist/src/utils/dragScrollUtils.d.ts.map +1 -0
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -5,8 +5,299 @@ Object.defineProperty(exports, '__esModule', { value: true });
|
|
|
5
5
|
var jsxRuntime = require('react/jsx-runtime');
|
|
6
6
|
var react = require('react');
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
/**
|
|
9
|
+
* MIT License
|
|
10
|
+
*
|
|
11
|
+
* Copyright (c) 2025 KIM YOUNG JIN (ehfuse@gmail.com)
|
|
12
|
+
*
|
|
13
|
+
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
14
|
+
* of this software and associated documentation files (the "Software"), to deal
|
|
15
|
+
* in the Software without restriction, including without limitation the rights
|
|
16
|
+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
17
|
+
* copies of the Software, and to permit persons to whom the Software is
|
|
18
|
+
* furnished to do so, subject to the following conditions:
|
|
19
|
+
*
|
|
20
|
+
* The above copyright notice and this permission notice shall be included in all
|
|
21
|
+
* copies or substantial portions of the Software.
|
|
22
|
+
*
|
|
23
|
+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
24
|
+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
25
|
+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
26
|
+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
27
|
+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
28
|
+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
29
|
+
* SOFTWARE.
|
|
30
|
+
*/
|
|
31
|
+
// 드래그 스크롤을 제외할 클래스들 (자신 또는 부모 요소에서 확인)
|
|
32
|
+
const DEFAULT_EXCLUDE_CLASSES = [
|
|
33
|
+
// 기본 입력 요소들
|
|
34
|
+
"editor",
|
|
35
|
+
"textarea",
|
|
36
|
+
"input",
|
|
37
|
+
"select",
|
|
38
|
+
"textfield",
|
|
39
|
+
"form-control",
|
|
40
|
+
"contenteditable",
|
|
41
|
+
// Material-UI 컴포넌트들
|
|
42
|
+
"MuiInputBase-input",
|
|
43
|
+
"MuiSelect-select",
|
|
44
|
+
"MuiOutlinedInput-input",
|
|
45
|
+
"MuiFilledInput-input",
|
|
46
|
+
"MuiInput-input",
|
|
47
|
+
"MuiFormControl-root",
|
|
48
|
+
"MuiTextField-root",
|
|
49
|
+
"MuiSelect-root",
|
|
50
|
+
"MuiOutlinedInput-root",
|
|
51
|
+
"MuiFilledInput-root",
|
|
52
|
+
"MuiInput-root",
|
|
53
|
+
"MuiAutocomplete-input",
|
|
54
|
+
"MuiDatePicker-input",
|
|
55
|
+
"MuiSlider-thumb",
|
|
56
|
+
"MuiSlider-rail",
|
|
57
|
+
"MuiSlider-track",
|
|
58
|
+
"MuiSlider-mark",
|
|
59
|
+
"MuiSlider-markLabel",
|
|
60
|
+
"MuiSlider-root",
|
|
61
|
+
"MuiSlider-colorPrimary",
|
|
62
|
+
"MuiSlider-sizeMedium",
|
|
63
|
+
"MuiIconButton-root",
|
|
64
|
+
"MuiButton-root",
|
|
65
|
+
"MuiButtonBase-root",
|
|
66
|
+
"MuiTouchRipple-root",
|
|
67
|
+
"MuiCheckbox-root",
|
|
68
|
+
"MuiRadio-root",
|
|
69
|
+
"MuiSwitch-root",
|
|
70
|
+
"PrivateSwitchBase-root",
|
|
71
|
+
// Ant Design 컴포넌트들
|
|
72
|
+
"ant-input",
|
|
73
|
+
"ant-input-affix-wrapper",
|
|
74
|
+
"ant-input-group-addon",
|
|
75
|
+
"ant-input-number",
|
|
76
|
+
"ant-input-number-handler",
|
|
77
|
+
"ant-select",
|
|
78
|
+
"ant-select-selector",
|
|
79
|
+
"ant-select-selection-search",
|
|
80
|
+
"ant-select-dropdown",
|
|
81
|
+
"ant-cascader",
|
|
82
|
+
"ant-cascader-input",
|
|
83
|
+
"ant-picker",
|
|
84
|
+
"ant-picker-input",
|
|
85
|
+
"ant-time-picker",
|
|
86
|
+
"ant-calendar-picker",
|
|
87
|
+
"ant-slider",
|
|
88
|
+
"ant-slider-track",
|
|
89
|
+
"ant-slider-handle",
|
|
90
|
+
"ant-switch",
|
|
91
|
+
"ant-checkbox",
|
|
92
|
+
"ant-checkbox-wrapper",
|
|
93
|
+
"ant-radio",
|
|
94
|
+
"ant-radio-wrapper",
|
|
95
|
+
"ant-rate",
|
|
96
|
+
"ant-upload",
|
|
97
|
+
"ant-upload-drag",
|
|
98
|
+
"ant-form-item",
|
|
99
|
+
"ant-form-item-control",
|
|
100
|
+
"ant-btn",
|
|
101
|
+
"ant-dropdown",
|
|
102
|
+
"ant-dropdown-trigger",
|
|
103
|
+
"ant-menu",
|
|
104
|
+
"ant-menu-item",
|
|
105
|
+
"ant-tooltip",
|
|
106
|
+
"ant-popover",
|
|
107
|
+
"ant-modal",
|
|
108
|
+
"ant-drawer",
|
|
109
|
+
"ant-tree-select",
|
|
110
|
+
"ant-auto-complete",
|
|
111
|
+
"ant-mentions",
|
|
112
|
+
"ant-transfer",
|
|
113
|
+
// Shadcn/ui 컴포넌트들
|
|
114
|
+
"ui-input",
|
|
115
|
+
"ui-textarea",
|
|
116
|
+
"ui-select",
|
|
117
|
+
"ui-select-trigger",
|
|
118
|
+
"ui-select-content",
|
|
119
|
+
"ui-select-item",
|
|
120
|
+
"ui-button",
|
|
121
|
+
"ui-checkbox",
|
|
122
|
+
"ui-radio-group",
|
|
123
|
+
"ui-switch",
|
|
124
|
+
"ui-slider",
|
|
125
|
+
"ui-range-slider",
|
|
126
|
+
"ui-calendar",
|
|
127
|
+
"ui-date-picker",
|
|
128
|
+
"ui-combobox",
|
|
129
|
+
"ui-command",
|
|
130
|
+
"ui-command-input",
|
|
131
|
+
"ui-popover",
|
|
132
|
+
"ui-dialog",
|
|
133
|
+
"ui-sheet",
|
|
134
|
+
"ui-dropdown-menu",
|
|
135
|
+
"ui-context-menu",
|
|
136
|
+
"ui-menubar",
|
|
137
|
+
"ui-navigation-menu",
|
|
138
|
+
"ui-form",
|
|
139
|
+
"ui-form-control",
|
|
140
|
+
"ui-form-item",
|
|
141
|
+
"ui-form-field",
|
|
142
|
+
"ui-label",
|
|
143
|
+
// Radix UI 기본 클래스들 (Shadcn 기반)
|
|
144
|
+
"radix-ui",
|
|
145
|
+
"radix-select",
|
|
146
|
+
"radix-dropdown",
|
|
147
|
+
"radix-dialog",
|
|
148
|
+
"radix-popover",
|
|
149
|
+
"radix-accordion",
|
|
150
|
+
"radix-tabs",
|
|
151
|
+
"radix-slider",
|
|
152
|
+
"radix-switch",
|
|
153
|
+
"radix-checkbox",
|
|
154
|
+
"radix-radio",
|
|
155
|
+
// Quill Editor
|
|
156
|
+
"ql-editor",
|
|
157
|
+
"ql-container",
|
|
158
|
+
"ql-toolbar",
|
|
159
|
+
"ql-picker",
|
|
160
|
+
"ql-picker-label",
|
|
161
|
+
"ql-picker-options",
|
|
162
|
+
"ql-formats",
|
|
163
|
+
"ql-snow",
|
|
164
|
+
"ql-bubble",
|
|
165
|
+
"quill",
|
|
166
|
+
"quilleditor",
|
|
167
|
+
// Monaco Editor
|
|
168
|
+
"monaco-editor",
|
|
169
|
+
"monaco-editor-background",
|
|
170
|
+
"view-lines",
|
|
171
|
+
"decorationsOverviewRuler",
|
|
172
|
+
"monaco-scrollable-element",
|
|
173
|
+
// CodeMirror
|
|
174
|
+
"CodeMirror",
|
|
175
|
+
"CodeMirror-code",
|
|
176
|
+
"CodeMirror-lines",
|
|
177
|
+
"CodeMirror-scroll",
|
|
178
|
+
"CodeMirror-sizer",
|
|
179
|
+
"cm-editor",
|
|
180
|
+
"cm-focused",
|
|
181
|
+
"cm-content",
|
|
182
|
+
// TinyMCE
|
|
183
|
+
"tox-editor-container",
|
|
184
|
+
"tox-editor-header",
|
|
185
|
+
"tox-edit-area",
|
|
186
|
+
"tox-tinymce",
|
|
187
|
+
"mce-content-body",
|
|
188
|
+
// CKEditor
|
|
189
|
+
"ck-editor",
|
|
190
|
+
"ck-content",
|
|
191
|
+
"ck-toolbar",
|
|
192
|
+
"ck-editor__editable",
|
|
193
|
+
"ck-widget",
|
|
194
|
+
// Slate.js
|
|
195
|
+
"slate-editor",
|
|
196
|
+
"slate-content",
|
|
197
|
+
// Draft.js
|
|
198
|
+
"DraftEditor-root",
|
|
199
|
+
"DraftEditor-editorContainer",
|
|
200
|
+
"public-DraftEditor-content",
|
|
201
|
+
// EhfuseEditor
|
|
202
|
+
"ehfuse-editor",
|
|
203
|
+
"ehfuse-editor-wrapper",
|
|
204
|
+
"ehfuse-editor-content",
|
|
205
|
+
"ehfuse-toolbar",
|
|
206
|
+
"ehfuse-toolbar-group",
|
|
207
|
+
"ehfuse-cursor",
|
|
208
|
+
// 기타 에디터들
|
|
209
|
+
"text-editor",
|
|
210
|
+
"rich-text-editor",
|
|
211
|
+
"wysiwyg",
|
|
212
|
+
"ace_editor",
|
|
213
|
+
"ace_content",
|
|
214
|
+
];
|
|
215
|
+
/**
|
|
216
|
+
* 드래그 스크롤이 허용되지 않는 요소들인지 확인
|
|
217
|
+
*/
|
|
218
|
+
/**
|
|
219
|
+
* 드래그 스크롤이 허용되지 않는 요소들인지 확인
|
|
220
|
+
*/
|
|
221
|
+
const isTextInputElement = (element, config) => {
|
|
222
|
+
const tagName = element.tagName.toLowerCase();
|
|
223
|
+
const inputTypes = [
|
|
224
|
+
"text",
|
|
225
|
+
"password",
|
|
226
|
+
"email",
|
|
227
|
+
"number",
|
|
228
|
+
"search",
|
|
229
|
+
"tel",
|
|
230
|
+
"url",
|
|
231
|
+
"checkbox",
|
|
232
|
+
"radio",
|
|
233
|
+
];
|
|
234
|
+
// input 태그이면서 텍스트 입력 타입이나 체크박스/라디오인 경우
|
|
235
|
+
if (tagName === "input") {
|
|
236
|
+
const type = element.type;
|
|
237
|
+
return inputTypes.includes(type);
|
|
238
|
+
}
|
|
239
|
+
// textarea, select, 편집 가능한 요소들
|
|
240
|
+
if (["textarea", "select", "button"].includes(tagName)) {
|
|
241
|
+
return true;
|
|
242
|
+
}
|
|
243
|
+
// SVG 요소들 (아이콘들)
|
|
244
|
+
if ([
|
|
245
|
+
"svg",
|
|
246
|
+
"path",
|
|
247
|
+
"circle",
|
|
248
|
+
"rect",
|
|
249
|
+
"line",
|
|
250
|
+
"polygon",
|
|
251
|
+
"polyline",
|
|
252
|
+
].includes(tagName)) {
|
|
253
|
+
return true;
|
|
254
|
+
}
|
|
255
|
+
// contenteditable 속성이 있는 요소
|
|
256
|
+
if (element.getAttribute("contenteditable") === "true") {
|
|
257
|
+
return true;
|
|
258
|
+
}
|
|
259
|
+
// 추가 셀렉터 체크
|
|
260
|
+
if (config === null || config === void 0 ? void 0 : config.excludeSelectors) {
|
|
261
|
+
for (const selector of config.excludeSelectors) {
|
|
262
|
+
if (element.matches(selector)) {
|
|
263
|
+
return true;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
return checkElementAndParents(element, config);
|
|
268
|
+
};
|
|
269
|
+
/**
|
|
270
|
+
* 자신 또는 부모 요소들을 확인하여 드래그 스크롤을 제외할 요소인지 판단
|
|
271
|
+
*/
|
|
272
|
+
const checkElementAndParents = (element, config) => {
|
|
273
|
+
// 모든 제외 클래스들 합치기 (기본 클래스 + 사용자 추가 클래스)
|
|
274
|
+
const allExcludeClasses = [
|
|
275
|
+
...DEFAULT_EXCLUDE_CLASSES,
|
|
276
|
+
...((config === null || config === void 0 ? void 0 : config.excludeClasses) || []),
|
|
277
|
+
];
|
|
278
|
+
let currentElement = element;
|
|
279
|
+
let depth = 0;
|
|
280
|
+
const maxDepth = 5; // 최대 5단계까지 부모 요소 확인
|
|
281
|
+
while (currentElement && depth <= maxDepth) {
|
|
282
|
+
// 현재 요소가 제외 클래스를 가지고 있는지 확인
|
|
283
|
+
if (allExcludeClasses.some((cls) => currentElement.classList.contains(cls))) {
|
|
284
|
+
return true;
|
|
285
|
+
}
|
|
286
|
+
// 다이얼로그 루트에 도달하면 중단
|
|
287
|
+
if (currentElement.classList.contains("MuiDialogContent-root")) {
|
|
288
|
+
break;
|
|
289
|
+
}
|
|
290
|
+
currentElement = currentElement.parentElement;
|
|
291
|
+
depth++;
|
|
292
|
+
}
|
|
293
|
+
return false;
|
|
294
|
+
};
|
|
295
|
+
|
|
296
|
+
const OverlayScrollbar = react.forwardRef(({ className = "", style = {}, children, onScroll, scrollContainer: externalScrollContainer,
|
|
297
|
+
// 그룹화된 설정 객체들
|
|
298
|
+
thumb = {}, track = {}, arrows = {}, dragScroll = {},
|
|
299
|
+
// 기타 설정들
|
|
300
|
+
showScrollbar = true, hideDelay = 1500, hideDelayOnWheel = 700, }, ref) => {
|
|
10
301
|
const containerRef = react.useRef(null);
|
|
11
302
|
const contentRef = react.useRef(null);
|
|
12
303
|
const scrollbarRef = react.useRef(null);
|
|
@@ -17,6 +308,14 @@ thumbRadius, showScrollbar = true, showArrows = false, arrowStep = 50, trackWidt
|
|
|
17
308
|
const [dragStart, setDragStart] = react.useState({ y: 0, scrollTop: 0 });
|
|
18
309
|
const [thumbHeight, setThumbHeight] = react.useState(0);
|
|
19
310
|
const [thumbTop, setThumbTop] = react.useState(0);
|
|
311
|
+
// 드래그 스크롤 상태
|
|
312
|
+
const [isDragScrolling, setIsDragScrolling] = react.useState(false);
|
|
313
|
+
const [dragScrollStart, setDragScrollStart] = react.useState({
|
|
314
|
+
x: 0,
|
|
315
|
+
y: 0,
|
|
316
|
+
scrollTop: 0,
|
|
317
|
+
scrollLeft: 0,
|
|
318
|
+
});
|
|
20
319
|
const [activeArrow, setActiveArrow] = react.useState(null);
|
|
21
320
|
const [hoveredArrow, setHoveredArrow] = react.useState(null);
|
|
22
321
|
// 초기 마운트 시 hover 방지용
|
|
@@ -26,6 +325,48 @@ thumbRadius, showScrollbar = true, showArrows = false, arrowStep = 50, trackWidt
|
|
|
26
325
|
const [isWheelScrolling, setIsWheelScrolling] = react.useState(false);
|
|
27
326
|
// 숨김 타이머
|
|
28
327
|
const hideTimeoutRef = react.useRef(null);
|
|
328
|
+
// 그룹화된 설정 객체들에 기본값 설정
|
|
329
|
+
const finalThumbConfig = react.useMemo(() => {
|
|
330
|
+
var _a, _b, _c, _d, _e, _f;
|
|
331
|
+
return ({
|
|
332
|
+
width: (_a = thumb.width) !== null && _a !== void 0 ? _a : 8,
|
|
333
|
+
minHeight: (_b = thumb.minHeight) !== null && _b !== void 0 ? _b : 50,
|
|
334
|
+
radius: (_c = thumb.radius) !== null && _c !== void 0 ? _c : ((_d = thumb.width) !== null && _d !== void 0 ? _d : 8) / 2,
|
|
335
|
+
color: (_e = thumb.color) !== null && _e !== void 0 ? _e : "rgba(128, 128, 128, 0.6)",
|
|
336
|
+
activeColor: (_f = thumb.activeColor) !== null && _f !== void 0 ? _f : "rgba(128, 128, 128, 0.9)",
|
|
337
|
+
});
|
|
338
|
+
}, [thumb]);
|
|
339
|
+
const finalTrackConfig = react.useMemo(() => {
|
|
340
|
+
var _a, _b, _c;
|
|
341
|
+
return ({
|
|
342
|
+
width: (_a = track.width) !== null && _a !== void 0 ? _a : 16,
|
|
343
|
+
color: (_b = track.color) !== null && _b !== void 0 ? _b : "rgba(128, 128, 128, 0.1)",
|
|
344
|
+
visible: (_c = track.visible) !== null && _c !== void 0 ? _c : true,
|
|
345
|
+
});
|
|
346
|
+
}, [track]);
|
|
347
|
+
const finalArrowsConfig = react.useMemo(() => {
|
|
348
|
+
var _a, _b, _c, _d;
|
|
349
|
+
return ({
|
|
350
|
+
visible: (_a = arrows.visible) !== null && _a !== void 0 ? _a : false,
|
|
351
|
+
step: (_b = arrows.step) !== null && _b !== void 0 ? _b : 50,
|
|
352
|
+
color: (_c = arrows.color) !== null && _c !== void 0 ? _c : "rgba(128, 128, 128, 0.8)",
|
|
353
|
+
activeColor: (_d = arrows.activeColor) !== null && _d !== void 0 ? _d : "rgba(64, 64, 64, 1.0)",
|
|
354
|
+
});
|
|
355
|
+
}, [arrows]);
|
|
356
|
+
const finalDragScrollConfig = react.useMemo(() => {
|
|
357
|
+
var _a, _b, _c;
|
|
358
|
+
return ({
|
|
359
|
+
enabled: (_a = dragScroll.enabled) !== null && _a !== void 0 ? _a : true,
|
|
360
|
+
excludeClasses: (_b = dragScroll.excludeClasses) !== null && _b !== void 0 ? _b : [],
|
|
361
|
+
excludeSelectors: (_c = dragScroll.excludeSelectors) !== null && _c !== void 0 ? _c : [],
|
|
362
|
+
});
|
|
363
|
+
}, [dragScroll]);
|
|
364
|
+
// 호환성을 위한 변수들 (자주 사용되는 변수들만 유지)
|
|
365
|
+
const finalThumbWidth = finalThumbConfig.width;
|
|
366
|
+
const finalTrackWidth = finalTrackConfig.width;
|
|
367
|
+
const thumbMinHeight = finalThumbConfig.minHeight;
|
|
368
|
+
const showArrows = finalArrowsConfig.visible;
|
|
369
|
+
const arrowStep = finalArrowsConfig.step;
|
|
29
370
|
// ref를 통해 외부에서 스크롤 컨테이너에 접근할 수 있도록 함
|
|
30
371
|
react.useImperativeHandle(ref, () => ({
|
|
31
372
|
getScrollContainer: () => containerRef.current,
|
|
@@ -47,13 +388,61 @@ thumbRadius, showScrollbar = true, showArrows = false, arrowStep = 50, trackWidt
|
|
|
47
388
|
return ((_a = containerRef.current) === null || _a === void 0 ? void 0 : _a.clientHeight) || 0;
|
|
48
389
|
},
|
|
49
390
|
}), []);
|
|
391
|
+
// 실제 스크롤 가능한 요소 찾기
|
|
392
|
+
const findScrollableElement = react.useCallback(() => {
|
|
393
|
+
// externalScrollContainer가 있으면 우선 사용
|
|
394
|
+
if (externalScrollContainer) {
|
|
395
|
+
console.log("OverlayScrollbar: checking external container", {
|
|
396
|
+
scrollHeight: externalScrollContainer.scrollHeight,
|
|
397
|
+
clientHeight: externalScrollContainer.clientHeight,
|
|
398
|
+
});
|
|
399
|
+
// virtuoso의 내부 스크롤러를 찾기
|
|
400
|
+
const virtuosoScroller = externalScrollContainer.querySelector('[data-virtuoso-scroller], [style*="overflow"], .virtuoso-scroller');
|
|
401
|
+
if (virtuosoScroller) {
|
|
402
|
+
const element = virtuosoScroller;
|
|
403
|
+
if (element.scrollHeight > element.clientHeight + 2) {
|
|
404
|
+
console.log("OverlayScrollbar: found virtuoso scroller", {
|
|
405
|
+
scrollHeight: element.scrollHeight,
|
|
406
|
+
clientHeight: element.clientHeight,
|
|
407
|
+
element,
|
|
408
|
+
});
|
|
409
|
+
return element;
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
// externalScrollContainer 자체가 스크롤 가능한지 확인
|
|
413
|
+
if (externalScrollContainer.scrollHeight >
|
|
414
|
+
externalScrollContainer.clientHeight + 2) {
|
|
415
|
+
return externalScrollContainer;
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
if (!containerRef.current) {
|
|
419
|
+
return null;
|
|
420
|
+
}
|
|
421
|
+
// 내부 컨테이너의 스크롤 가능 여부 확인
|
|
422
|
+
if (contentRef.current &&
|
|
423
|
+
contentRef.current.scrollHeight >
|
|
424
|
+
containerRef.current.clientHeight + 2) {
|
|
425
|
+
return containerRef.current;
|
|
426
|
+
}
|
|
427
|
+
// children 요소에서 스크롤 가능한 요소 찾기
|
|
428
|
+
const childScrollableElements = containerRef.current.querySelectorAll('[data-virtuoso-scroller], [style*="overflow"], .virtuoso-scroller, [style*="overflow: auto"], [style*="overflow:auto"]');
|
|
429
|
+
for (const child of childScrollableElements) {
|
|
430
|
+
const element = child;
|
|
431
|
+
if (element.scrollHeight > element.clientHeight + 2) {
|
|
432
|
+
console.log("OverlayScrollbar: found scrollable child element", {
|
|
433
|
+
scrollHeight: element.scrollHeight,
|
|
434
|
+
clientHeight: element.clientHeight,
|
|
435
|
+
element,
|
|
436
|
+
});
|
|
437
|
+
return element;
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
return null;
|
|
441
|
+
}, [externalScrollContainer]);
|
|
50
442
|
// 스크롤 가능 여부 체크
|
|
51
443
|
const isScrollable = react.useCallback(() => {
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
return (contentRef.current.scrollHeight >
|
|
55
|
-
containerRef.current.clientHeight + 2);
|
|
56
|
-
}, []);
|
|
444
|
+
return findScrollableElement() !== null;
|
|
445
|
+
}, [findScrollableElement]);
|
|
57
446
|
// 타이머 정리
|
|
58
447
|
const clearHideTimer = react.useCallback(() => {
|
|
59
448
|
if (hideTimeoutRef.current) {
|
|
@@ -71,23 +460,26 @@ thumbRadius, showScrollbar = true, showArrows = false, arrowStep = 50, trackWidt
|
|
|
71
460
|
}, [clearHideTimer, isDragging]);
|
|
72
461
|
// 스크롤바 위치 및 크기 업데이트
|
|
73
462
|
const updateScrollbar = react.useCallback(() => {
|
|
74
|
-
if (!
|
|
75
|
-
!contentRef.current ||
|
|
76
|
-
!scrollbarRef.current)
|
|
463
|
+
if (!scrollbarRef.current)
|
|
77
464
|
return;
|
|
78
|
-
const
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
const contentHeight = content.scrollHeight;
|
|
82
|
-
const scrollTop = container.scrollTop;
|
|
83
|
-
// 스크롤 불가능하면 숨김
|
|
84
|
-
if (contentHeight <= containerHeight + 2) {
|
|
465
|
+
const scrollableElement = findScrollableElement();
|
|
466
|
+
if (!scrollableElement) {
|
|
467
|
+
// 스크롤 불가능하면 숨김
|
|
85
468
|
setScrollbarVisible(false);
|
|
86
469
|
clearHideTimer();
|
|
87
470
|
return;
|
|
88
471
|
}
|
|
472
|
+
const containerHeight = scrollableElement.clientHeight;
|
|
473
|
+
const contentHeight = scrollableElement.scrollHeight;
|
|
474
|
+
const scrollTop = scrollableElement.scrollTop;
|
|
475
|
+
console.log("OverlayScrollbar: updating scrollbar", {
|
|
476
|
+
containerHeight,
|
|
477
|
+
contentHeight,
|
|
478
|
+
scrollTop,
|
|
479
|
+
element: scrollableElement,
|
|
480
|
+
});
|
|
89
481
|
// 화살표와 간격 공간 계산 (화살표 + 위아래여백 4px + 화살표간격 4px씩, 화살표 없어도 위아래 4px씩 여백)
|
|
90
|
-
const arrowSpace = showArrows ?
|
|
482
|
+
const arrowSpace = showArrows ? finalThumbWidth * 2 + 16 : 8;
|
|
91
483
|
// 썸 높이 계산 (사용자 설정 최소 높이 사용, 화살표 공간 제외)
|
|
92
484
|
const availableHeight = containerHeight - arrowSpace;
|
|
93
485
|
const scrollRatio = containerHeight / contentHeight;
|
|
@@ -100,37 +492,58 @@ thumbRadius, showScrollbar = true, showArrows = false, arrowStep = 50, trackWidt
|
|
|
100
492
|
: 0;
|
|
101
493
|
setThumbHeight(calculatedThumbHeight);
|
|
102
494
|
setThumbTop(calculatedThumbTop);
|
|
103
|
-
}, [
|
|
495
|
+
}, [
|
|
496
|
+
findScrollableElement,
|
|
497
|
+
clearHideTimer,
|
|
498
|
+
showArrows,
|
|
499
|
+
finalThumbWidth,
|
|
500
|
+
thumbMinHeight,
|
|
501
|
+
]);
|
|
104
502
|
// 썸 드래그 시작
|
|
105
503
|
const handleThumbMouseDown = react.useCallback((event) => {
|
|
504
|
+
var _a;
|
|
106
505
|
event.preventDefault();
|
|
107
506
|
event.stopPropagation();
|
|
108
|
-
|
|
507
|
+
const actualScrollContainer = findScrollableElement();
|
|
508
|
+
if (!actualScrollContainer) {
|
|
509
|
+
console.log("Thumb drag - no scrollable element found");
|
|
109
510
|
return;
|
|
511
|
+
}
|
|
110
512
|
setIsDragging(true);
|
|
111
513
|
setDragStart({
|
|
112
514
|
y: event.clientY,
|
|
113
|
-
scrollTop:
|
|
515
|
+
scrollTop: actualScrollContainer.scrollTop,
|
|
114
516
|
});
|
|
115
517
|
clearHideTimer();
|
|
116
518
|
setScrollbarVisible(true);
|
|
117
|
-
|
|
519
|
+
// 포커스 유지 (키보드 입력이 계속 작동하도록)
|
|
520
|
+
(_a = containerRef.current) === null || _a === void 0 ? void 0 : _a.focus();
|
|
521
|
+
}, [findScrollableElement, clearHideTimer]);
|
|
118
522
|
// 썸 드래그 중
|
|
119
523
|
const handleMouseMove = react.useCallback((event) => {
|
|
120
|
-
if (!isDragging
|
|
524
|
+
if (!isDragging)
|
|
121
525
|
return;
|
|
122
|
-
const
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
526
|
+
const actualScrollContainer = findScrollableElement();
|
|
527
|
+
if (!actualScrollContainer) {
|
|
528
|
+
console.log("Mouse move - no scrollable element found");
|
|
529
|
+
return;
|
|
530
|
+
}
|
|
531
|
+
const containerHeight = actualScrollContainer.clientHeight;
|
|
532
|
+
const contentHeight = actualScrollContainer.scrollHeight;
|
|
126
533
|
const scrollableHeight = contentHeight - containerHeight;
|
|
127
534
|
const deltaY = event.clientY - dragStart.y;
|
|
128
535
|
const thumbScrollableHeight = containerHeight - thumbHeight;
|
|
129
536
|
const scrollDelta = (deltaY / thumbScrollableHeight) * scrollableHeight;
|
|
130
537
|
const newScrollTop = Math.max(0, Math.min(scrollableHeight, dragStart.scrollTop + scrollDelta));
|
|
131
|
-
|
|
538
|
+
actualScrollContainer.scrollTop = newScrollTop;
|
|
132
539
|
updateScrollbar();
|
|
133
|
-
}, [
|
|
540
|
+
}, [
|
|
541
|
+
isDragging,
|
|
542
|
+
dragStart,
|
|
543
|
+
thumbHeight,
|
|
544
|
+
updateScrollbar,
|
|
545
|
+
findScrollableElement,
|
|
546
|
+
]);
|
|
134
547
|
// 썸 드래그 종료
|
|
135
548
|
const handleMouseUp = react.useCallback(() => {
|
|
136
549
|
setIsDragging(false);
|
|
@@ -140,26 +553,43 @@ thumbRadius, showScrollbar = true, showArrows = false, arrowStep = 50, trackWidt
|
|
|
140
553
|
}, [isScrollable, setHideTimer, hideDelay]);
|
|
141
554
|
// 트랙 클릭으로 스크롤 점프
|
|
142
555
|
const handleTrackClick = react.useCallback((event) => {
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
556
|
+
var _a;
|
|
557
|
+
console.log("handleTrackClick called", event);
|
|
558
|
+
if (!scrollbarRef.current) {
|
|
559
|
+
console.log("Track click - scrollbarRef not available");
|
|
146
560
|
return;
|
|
561
|
+
}
|
|
147
562
|
const scrollbar = scrollbarRef.current;
|
|
148
563
|
const rect = scrollbar.getBoundingClientRect();
|
|
149
564
|
const clickY = event.clientY - rect.top;
|
|
150
|
-
const
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
565
|
+
const actualScrollContainer = findScrollableElement();
|
|
566
|
+
if (!actualScrollContainer) {
|
|
567
|
+
console.log("Track click - no scrollable element found");
|
|
568
|
+
return;
|
|
569
|
+
}
|
|
570
|
+
console.log("Track click - using scrollable element", actualScrollContainer);
|
|
571
|
+
const containerHeight = actualScrollContainer.clientHeight;
|
|
572
|
+
const contentHeight = actualScrollContainer.scrollHeight;
|
|
154
573
|
const scrollRatio = clickY / containerHeight;
|
|
155
574
|
const newScrollTop = scrollRatio * (contentHeight - containerHeight);
|
|
156
|
-
|
|
575
|
+
console.log("Track click scroll calculation", {
|
|
576
|
+
clickY,
|
|
577
|
+
containerHeight,
|
|
578
|
+
contentHeight,
|
|
579
|
+
scrollRatio,
|
|
580
|
+
newScrollTop,
|
|
581
|
+
actualScrollContainer,
|
|
582
|
+
});
|
|
583
|
+
actualScrollContainer.scrollTop = Math.max(0, Math.min(contentHeight - containerHeight, newScrollTop));
|
|
157
584
|
updateScrollbar();
|
|
158
585
|
setScrollbarVisible(true);
|
|
159
586
|
setHideTimer(hideDelay);
|
|
160
|
-
|
|
587
|
+
// 포커스 유지 (키보드 입력이 계속 작동하도록)
|
|
588
|
+
(_a = containerRef.current) === null || _a === void 0 ? void 0 : _a.focus();
|
|
589
|
+
}, [updateScrollbar, setHideTimer, hideDelay, findScrollableElement]);
|
|
161
590
|
// 위쪽 화살표 클릭 핸들러
|
|
162
591
|
const handleUpArrowClick = react.useCallback((event) => {
|
|
592
|
+
var _a;
|
|
163
593
|
event.preventDefault();
|
|
164
594
|
event.stopPropagation();
|
|
165
595
|
if (!containerRef.current)
|
|
@@ -169,9 +599,12 @@ thumbRadius, showScrollbar = true, showArrows = false, arrowStep = 50, trackWidt
|
|
|
169
599
|
updateScrollbar();
|
|
170
600
|
setScrollbarVisible(true);
|
|
171
601
|
setHideTimer(hideDelay);
|
|
602
|
+
// 포커스 유지 (키보드 입력이 계속 작동하도록)
|
|
603
|
+
(_a = containerRef.current) === null || _a === void 0 ? void 0 : _a.focus();
|
|
172
604
|
}, [updateScrollbar, setHideTimer, arrowStep, hideDelay]);
|
|
173
605
|
// 아래쪽 화살표 클릭 핸들러
|
|
174
606
|
const handleDownArrowClick = react.useCallback((event) => {
|
|
607
|
+
var _a;
|
|
175
608
|
event.preventDefault();
|
|
176
609
|
event.stopPropagation();
|
|
177
610
|
if (!containerRef.current || !contentRef.current)
|
|
@@ -184,12 +617,75 @@ thumbRadius, showScrollbar = true, showArrows = false, arrowStep = 50, trackWidt
|
|
|
184
617
|
updateScrollbar();
|
|
185
618
|
setScrollbarVisible(true);
|
|
186
619
|
setHideTimer(hideDelay);
|
|
620
|
+
// 포커스 유지 (키보드 입력이 계속 작동하도록)
|
|
621
|
+
(_a = containerRef.current) === null || _a === void 0 ? void 0 : _a.focus();
|
|
187
622
|
}, [updateScrollbar, setHideTimer, arrowStep, hideDelay]);
|
|
188
|
-
// 스크롤
|
|
189
|
-
react.
|
|
190
|
-
|
|
191
|
-
if (!
|
|
623
|
+
// 드래그 스크롤 시작
|
|
624
|
+
const handleDragScrollStart = react.useCallback((event) => {
|
|
625
|
+
// 드래그 스크롤이 비활성화된 경우
|
|
626
|
+
if (!finalDragScrollConfig.enabled)
|
|
627
|
+
return;
|
|
628
|
+
// 텍스트 입력 요소나 제외 대상이면 드래그 스크롤 하지 않음
|
|
629
|
+
const target = event.target;
|
|
630
|
+
if (isTextInputElement(target, finalDragScrollConfig)) {
|
|
631
|
+
return;
|
|
632
|
+
}
|
|
633
|
+
// 오른쪽 클릭이나 휠 클릭은 제외
|
|
634
|
+
if (event.button !== 0)
|
|
635
|
+
return;
|
|
636
|
+
const scrollableElement = findScrollableElement();
|
|
637
|
+
if (!scrollableElement)
|
|
192
638
|
return;
|
|
639
|
+
// 스크롤 가능한 영역이 아니면 제외
|
|
640
|
+
if (scrollableElement.scrollHeight <=
|
|
641
|
+
scrollableElement.clientHeight)
|
|
642
|
+
return;
|
|
643
|
+
event.preventDefault();
|
|
644
|
+
setIsDragScrolling(true);
|
|
645
|
+
setDragScrollStart({
|
|
646
|
+
x: event.clientX,
|
|
647
|
+
y: event.clientY,
|
|
648
|
+
scrollTop: scrollableElement.scrollTop,
|
|
649
|
+
scrollLeft: scrollableElement.scrollLeft || 0,
|
|
650
|
+
});
|
|
651
|
+
// 스크롤바 표시
|
|
652
|
+
clearHideTimer();
|
|
653
|
+
setScrollbarVisible(true);
|
|
654
|
+
}, [
|
|
655
|
+
finalDragScrollConfig,
|
|
656
|
+
isTextInputElement,
|
|
657
|
+
findScrollableElement,
|
|
658
|
+
clearHideTimer,
|
|
659
|
+
]);
|
|
660
|
+
// 드래그 스크롤 중
|
|
661
|
+
const handleDragScrollMove = react.useCallback((event) => {
|
|
662
|
+
if (!isDragScrolling)
|
|
663
|
+
return;
|
|
664
|
+
const scrollableElement = findScrollableElement();
|
|
665
|
+
if (!scrollableElement)
|
|
666
|
+
return;
|
|
667
|
+
dragScrollStart.x - event.clientX;
|
|
668
|
+
const deltaY = dragScrollStart.y - event.clientY;
|
|
669
|
+
// 세로 스크롤만 처리 (가로 스크롤은 필요시 나중에 추가)
|
|
670
|
+
const newScrollTop = Math.max(0, Math.min(scrollableElement.scrollHeight -
|
|
671
|
+
scrollableElement.clientHeight, dragScrollStart.scrollTop + deltaY));
|
|
672
|
+
scrollableElement.scrollTop = newScrollTop;
|
|
673
|
+
updateScrollbar();
|
|
674
|
+
}, [
|
|
675
|
+
isDragScrolling,
|
|
676
|
+
dragScrollStart,
|
|
677
|
+
findScrollableElement,
|
|
678
|
+
updateScrollbar,
|
|
679
|
+
]);
|
|
680
|
+
// 드래그 스크롤 종료
|
|
681
|
+
const handleDragScrollEnd = react.useCallback(() => {
|
|
682
|
+
setIsDragScrolling(false);
|
|
683
|
+
if (isScrollable()) {
|
|
684
|
+
setHideTimer(hideDelay);
|
|
685
|
+
}
|
|
686
|
+
}, [isScrollable, setHideTimer, hideDelay]);
|
|
687
|
+
// 스크롤 이벤트 리스너 (externalScrollContainer 우선 사용)
|
|
688
|
+
react.useEffect(() => {
|
|
193
689
|
const handleScroll = (event) => {
|
|
194
690
|
updateScrollbar();
|
|
195
691
|
// 스크롤 중에는 스크롤바 표시
|
|
@@ -216,20 +712,44 @@ thumbRadius, showScrollbar = true, showArrows = false, arrowStep = 50, trackWidt
|
|
|
216
712
|
clearHideTimer();
|
|
217
713
|
setScrollbarVisible(true);
|
|
218
714
|
};
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
715
|
+
const elementsToWatch = [];
|
|
716
|
+
// 실제 스크롤 가능한 요소 찾기
|
|
717
|
+
const scrollableElement = findScrollableElement();
|
|
718
|
+
if (scrollableElement) {
|
|
719
|
+
elementsToWatch.push(scrollableElement);
|
|
720
|
+
console.log("OverlayScrollbar: watching scrollable element for events", scrollableElement);
|
|
721
|
+
}
|
|
722
|
+
// fallback: 내부 컨테이너와 children 요소도 감지
|
|
723
|
+
const container = containerRef.current;
|
|
724
|
+
if (container && !scrollableElement) {
|
|
725
|
+
elementsToWatch.push(container);
|
|
726
|
+
// children 요소들의 스크롤도 감지
|
|
727
|
+
const childScrollableElements = container.querySelectorAll('[data-virtuoso-scroller], [style*="overflow"], .virtuoso-scroller, [style*="overflow: auto"], [style*="overflow:auto"]');
|
|
728
|
+
childScrollableElements.forEach((child) => {
|
|
729
|
+
elementsToWatch.push(child);
|
|
730
|
+
});
|
|
731
|
+
}
|
|
732
|
+
// 모든 요소에 이벤트 리스너 등록
|
|
733
|
+
elementsToWatch.forEach((element) => {
|
|
734
|
+
element.addEventListener("scroll", handleScroll, {
|
|
735
|
+
passive: true,
|
|
736
|
+
});
|
|
737
|
+
element.addEventListener("wheel", handleWheel, {
|
|
738
|
+
passive: true,
|
|
739
|
+
});
|
|
224
740
|
});
|
|
225
741
|
return () => {
|
|
226
|
-
|
|
227
|
-
|
|
742
|
+
// 모든 이벤트 리스너 제거
|
|
743
|
+
elementsToWatch.forEach((element) => {
|
|
744
|
+
element.removeEventListener("scroll", handleScroll);
|
|
745
|
+
element.removeEventListener("wheel", handleWheel);
|
|
746
|
+
});
|
|
228
747
|
if (wheelTimeoutRef.current) {
|
|
229
748
|
clearTimeout(wheelTimeoutRef.current);
|
|
230
749
|
}
|
|
231
750
|
};
|
|
232
751
|
}, [
|
|
752
|
+
findScrollableElement,
|
|
233
753
|
updateScrollbar,
|
|
234
754
|
onScroll,
|
|
235
755
|
clearHideTimer,
|
|
@@ -238,6 +758,17 @@ thumbRadius, showScrollbar = true, showArrows = false, arrowStep = 50, trackWidt
|
|
|
238
758
|
hideDelayOnWheel,
|
|
239
759
|
isWheelScrolling,
|
|
240
760
|
]);
|
|
761
|
+
// 드래그 스크롤 전역 마우스 이벤트 리스너
|
|
762
|
+
react.useEffect(() => {
|
|
763
|
+
if (isDragScrolling) {
|
|
764
|
+
document.addEventListener("mousemove", handleDragScrollMove);
|
|
765
|
+
document.addEventListener("mouseup", handleDragScrollEnd);
|
|
766
|
+
return () => {
|
|
767
|
+
document.removeEventListener("mousemove", handleDragScrollMove);
|
|
768
|
+
document.removeEventListener("mouseup", handleDragScrollEnd);
|
|
769
|
+
};
|
|
770
|
+
}
|
|
771
|
+
}, [isDragScrolling, handleDragScrollMove, handleDragScrollEnd]);
|
|
241
772
|
// 전역 마우스 이벤트 리스너
|
|
242
773
|
react.useEffect(() => {
|
|
243
774
|
if (isDragging) {
|
|
@@ -251,44 +782,63 @@ thumbRadius, showScrollbar = true, showArrows = false, arrowStep = 50, trackWidt
|
|
|
251
782
|
}, [isDragging, handleMouseMove, handleMouseUp]);
|
|
252
783
|
// 초기 스크롤바 업데이트
|
|
253
784
|
react.useEffect(() => {
|
|
785
|
+
// 즉시 업데이트
|
|
254
786
|
updateScrollbar();
|
|
787
|
+
// 약간의 지연 후에도 업데이트 (DOM이 완전히 렌더링된 후)
|
|
788
|
+
const timer = setTimeout(() => {
|
|
789
|
+
updateScrollbar();
|
|
790
|
+
}, 100);
|
|
791
|
+
return () => clearTimeout(timer);
|
|
255
792
|
}, [updateScrollbar]);
|
|
793
|
+
// externalScrollContainer가 변경될 때 스크롤바 업데이트
|
|
794
|
+
react.useEffect(() => {
|
|
795
|
+
if (externalScrollContainer) {
|
|
796
|
+
// externalScrollContainer가 설정된 후 스크롤바 업데이트
|
|
797
|
+
const timer = setTimeout(() => {
|
|
798
|
+
updateScrollbar();
|
|
799
|
+
}, 50);
|
|
800
|
+
return () => clearTimeout(timer);
|
|
801
|
+
}
|
|
802
|
+
}, [externalScrollContainer, updateScrollbar]);
|
|
256
803
|
// 컴포넌트 초기화 완료 표시 (hover 이벤트 활성화용)
|
|
257
804
|
react.useEffect(() => {
|
|
258
805
|
const timer = setTimeout(() => {
|
|
259
806
|
setIsInitialized(true);
|
|
260
|
-
|
|
807
|
+
console.log("OverlayScrollbar initialized", {
|
|
808
|
+
containerRef: !!containerRef.current,
|
|
809
|
+
contentRef: !!contentRef.current,
|
|
810
|
+
isScrollable: isScrollable(),
|
|
811
|
+
});
|
|
812
|
+
// 초기화 후 스크롤바 업데이트 (썸 높이 정확하게 계산)
|
|
813
|
+
updateScrollbar();
|
|
814
|
+
}, 100);
|
|
261
815
|
return () => clearTimeout(timer);
|
|
262
|
-
}, []);
|
|
816
|
+
}, [isScrollable]);
|
|
263
817
|
// Resize observer로 크기 변경 감지
|
|
264
818
|
react.useEffect(() => {
|
|
265
|
-
if (!containerRef.current || !contentRef.current)
|
|
266
|
-
return;
|
|
267
819
|
const resizeObserver = new ResizeObserver(() => {
|
|
268
820
|
updateScrollbar();
|
|
269
821
|
});
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
// 계산된 값들을 메모이제이션하여 안정화
|
|
275
|
-
const { finalThumbWidth, finalTrackWidth } = react.useMemo(() => {
|
|
276
|
-
const computedThumbWidth = thumbWidth !== undefined ? thumbWidth : scrollbarWidth;
|
|
277
|
-
let computedTrackWidth = trackWidth !== undefined ? trackWidth : scrollbarWidth * 2;
|
|
278
|
-
// thumbWidth가 trackWidth보다 크거나 같으면 trackWidth를 thumbWidth와 같게 설정
|
|
279
|
-
if (computedThumbWidth >= computedTrackWidth) {
|
|
280
|
-
computedTrackWidth = computedThumbWidth;
|
|
822
|
+
const elementsToObserve = [];
|
|
823
|
+
// externalScrollContainer가 있으면 우선 관찰
|
|
824
|
+
if (externalScrollContainer) {
|
|
825
|
+
elementsToObserve.push(externalScrollContainer);
|
|
281
826
|
}
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
827
|
+
// 내부 컨테이너들도 관찰
|
|
828
|
+
if (containerRef.current) {
|
|
829
|
+
elementsToObserve.push(containerRef.current);
|
|
830
|
+
}
|
|
831
|
+
if (contentRef.current) {
|
|
832
|
+
elementsToObserve.push(contentRef.current);
|
|
833
|
+
}
|
|
834
|
+
// 모든 요소들 관찰 시작
|
|
835
|
+
elementsToObserve.forEach((element) => {
|
|
836
|
+
resizeObserver.observe(element);
|
|
837
|
+
});
|
|
838
|
+
return () => resizeObserver.disconnect();
|
|
839
|
+
}, [updateScrollbar, externalScrollContainer]);
|
|
840
|
+
// trackWidth가 thumbWidth보다 작으면 thumbWidth와 같게 설정
|
|
841
|
+
const adjustedTrackWidth = Math.max(finalTrackWidth, finalThumbWidth);
|
|
292
842
|
// 웹킷 스크롤바 숨기기용 CSS 동적 주입
|
|
293
843
|
react.useEffect(() => {
|
|
294
844
|
const styleId = "overlay-scrollbar-webkit-hide";
|
|
@@ -311,6 +861,14 @@ thumbRadius, showScrollbar = true, showArrows = false, arrowStep = 50, trackWidt
|
|
|
311
861
|
.overlay-scrollbar-container::-webkit-scrollbar-thumb {
|
|
312
862
|
display: none !important;
|
|
313
863
|
}
|
|
864
|
+
.overlay-scrollbar-container:focus {
|
|
865
|
+
outline: 2px solid rgba(0, 123, 255, 0.3);
|
|
866
|
+
outline-offset: -2px;
|
|
867
|
+
}
|
|
868
|
+
.overlay-scrollbar-container:focus-visible {
|
|
869
|
+
outline: 2px solid rgba(0, 123, 255, 0.5);
|
|
870
|
+
outline-offset: -2px;
|
|
871
|
+
}
|
|
314
872
|
`;
|
|
315
873
|
document.head.appendChild(style);
|
|
316
874
|
return () => {
|
|
@@ -320,60 +878,77 @@ thumbRadius, showScrollbar = true, showArrows = false, arrowStep = 50, trackWidt
|
|
|
320
878
|
}
|
|
321
879
|
};
|
|
322
880
|
}, []);
|
|
323
|
-
return (jsxRuntime.jsxs("div", { className: `overlay-scrollbar-wrapper ${className}`, style: Object.assign({ display: "flex", flexDirection: "column", position: "relative", minHeight: 0, height: "100%", flex: "1 1 0%" }, style), children: [jsxRuntime.jsx("div", { ref: containerRef, className: "overlay-scrollbar-container", style: {
|
|
881
|
+
return (jsxRuntime.jsxs("div", { className: `overlay-scrollbar-wrapper ${className}`, style: Object.assign({ display: "flex", flexDirection: "column", position: "relative", minHeight: 0, height: "100%", flex: "1 1 0%" }, style), children: [jsxRuntime.jsx("div", { ref: containerRef, className: "overlay-scrollbar-container", tabIndex: -1, onMouseDown: handleDragScrollStart, style: {
|
|
324
882
|
width: "100%", // 명시적 너비 설정
|
|
325
|
-
height: "100%", //
|
|
883
|
+
height: "100%", // 부모의 전체 높이 사용
|
|
884
|
+
flex: "1 1 auto", // flex item으로 설정하여 높이를 자동으로 계산
|
|
326
885
|
minHeight: 0, // 최소 높이 보장
|
|
327
886
|
overflow: "auto", // 네이티브 스크롤 기능 유지
|
|
328
887
|
// 브라우저 기본 스크롤바만 숨기기
|
|
329
888
|
scrollbarWidth: "none", // Firefox
|
|
330
889
|
msOverflowStyle: "none", // IE/Edge
|
|
890
|
+
// 키보드 포커스 스타일 (접근성)
|
|
891
|
+
outline: "none", // 기본 아웃라인 제거
|
|
892
|
+
userSelect: isDragScrolling ? "none" : "auto", // 드래그 중 텍스트 선택 방지
|
|
331
893
|
}, children: jsxRuntime.jsx("div", { ref: contentRef, className: "overlay-scrollbar-content", style: {
|
|
332
|
-
|
|
894
|
+
height: "100%", // min-height 대신 height 사용
|
|
895
|
+
minHeight: 0, // flex shrink 허용
|
|
896
|
+
display: "flex", // flex 컨테이너로 설정
|
|
897
|
+
flexDirection: "column", // 세로 방향 정렬
|
|
333
898
|
}, children: children }) }), showScrollbar && (jsxRuntime.jsxs("div", { ref: scrollbarRef, className: "overlay-scrollbar-track", onMouseEnter: () => {
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
}
|
|
899
|
+
console.log("Track hover enter", {
|
|
900
|
+
isScrollable: isScrollable(),
|
|
901
|
+
scrollbarVisible,
|
|
902
|
+
});
|
|
903
|
+
clearHideTimer();
|
|
904
|
+
setScrollbarVisible(true);
|
|
338
905
|
}, onMouseLeave: () => {
|
|
339
|
-
|
|
906
|
+
console.log("Track hover leave", { isDragging });
|
|
907
|
+
if (!isDragging) {
|
|
340
908
|
setHideTimer(hideDelay);
|
|
341
909
|
}
|
|
342
910
|
}, style: {
|
|
343
911
|
position: "absolute",
|
|
344
912
|
top: 0,
|
|
345
|
-
right: 0,
|
|
346
|
-
width: `${
|
|
913
|
+
right: 0,
|
|
914
|
+
width: `${adjustedTrackWidth}px`,
|
|
347
915
|
height: "100%",
|
|
348
916
|
opacity: scrollbarVisible ? 1 : 0,
|
|
349
917
|
transition: "opacity 0.2s ease-in-out",
|
|
350
918
|
cursor: "pointer",
|
|
351
919
|
zIndex: 1000,
|
|
352
|
-
pointerEvents: "auto",
|
|
353
|
-
}, children: [jsxRuntime.jsx("div", { className: "overlay-scrollbar-track-background", onClick:
|
|
920
|
+
pointerEvents: "auto",
|
|
921
|
+
}, children: [finalTrackConfig.visible && (jsxRuntime.jsx("div", { className: "overlay-scrollbar-track-background", onClick: (e) => {
|
|
922
|
+
console.log("Track background clicked", e);
|
|
923
|
+
e.preventDefault();
|
|
924
|
+
e.stopPropagation();
|
|
925
|
+
handleTrackClick(e);
|
|
926
|
+
}, style: {
|
|
354
927
|
position: "absolute",
|
|
355
928
|
top: showArrows
|
|
356
|
-
? `${
|
|
929
|
+
? `${finalThumbConfig.width + 8}px`
|
|
357
930
|
: "4px",
|
|
358
|
-
right: `${(
|
|
359
|
-
|
|
931
|
+
right: `${(adjustedTrackWidth -
|
|
932
|
+
finalThumbConfig.width) /
|
|
933
|
+
2}px`, // 트랙 가운데 정렬
|
|
934
|
+
width: `${finalThumbConfig.width}px`,
|
|
360
935
|
height: showArrows
|
|
361
|
-
? `calc(100% - ${
|
|
936
|
+
? `calc(100% - ${finalThumbConfig.width * 2 + 16}px)`
|
|
362
937
|
: "calc(100% - 8px)",
|
|
363
|
-
backgroundColor:
|
|
364
|
-
borderRadius: `${
|
|
938
|
+
backgroundColor: finalTrackConfig.color,
|
|
939
|
+
borderRadius: `${finalThumbConfig.radius}px`,
|
|
365
940
|
cursor: "pointer",
|
|
366
|
-
} }), jsxRuntime.jsx("div", { ref: thumbRef, className: "overlay-scrollbar-thumb", onMouseDown: handleThumbMouseDown, style: {
|
|
941
|
+
} })), jsxRuntime.jsx("div", { ref: thumbRef, className: "overlay-scrollbar-thumb", onMouseDown: handleThumbMouseDown, style: {
|
|
367
942
|
position: "absolute",
|
|
368
943
|
top: `${(showArrows ? finalThumbWidth + 8 : 4) +
|
|
369
944
|
thumbTop}px`,
|
|
370
|
-
right: `${(
|
|
945
|
+
right: `${(adjustedTrackWidth - finalThumbWidth) / 2}px`, // 트랙 가운데 정렬
|
|
371
946
|
width: `${finalThumbWidth}px`,
|
|
372
947
|
height: `${Math.max(thumbHeight, thumbMinHeight)}px`,
|
|
373
948
|
backgroundColor: isDragging
|
|
374
|
-
?
|
|
375
|
-
:
|
|
376
|
-
borderRadius: `${
|
|
949
|
+
? finalThumbConfig.activeColor
|
|
950
|
+
: finalThumbConfig.color,
|
|
951
|
+
borderRadius: `${finalThumbConfig.radius}px`,
|
|
377
952
|
cursor: "pointer",
|
|
378
953
|
transition: isDragging
|
|
379
954
|
? "none"
|
|
@@ -381,7 +956,7 @@ thumbRadius, showScrollbar = true, showArrows = false, arrowStep = 50, trackWidt
|
|
|
381
956
|
} })] })), showScrollbar && showArrows && (jsxRuntime.jsx("div", { className: "overlay-scrollbar-up-arrow", onClick: handleUpArrowClick, onMouseEnter: () => setHoveredArrow("up"), onMouseLeave: () => setHoveredArrow(null), style: {
|
|
382
957
|
position: "absolute",
|
|
383
958
|
top: "4px",
|
|
384
|
-
right: `${(
|
|
959
|
+
right: `${(adjustedTrackWidth - finalThumbWidth) / 2}px`, // 트랙 가운데 정렬
|
|
385
960
|
width: `${finalThumbWidth}px`,
|
|
386
961
|
height: `${finalThumbWidth}px`,
|
|
387
962
|
cursor: "pointer",
|
|
@@ -390,8 +965,8 @@ thumbRadius, showScrollbar = true, showArrows = false, arrowStep = 50, trackWidt
|
|
|
390
965
|
justifyContent: "center",
|
|
391
966
|
fontSize: `${Math.max(finalThumbWidth * 0.75, 8)}px`,
|
|
392
967
|
color: hoveredArrow === "up"
|
|
393
|
-
?
|
|
394
|
-
:
|
|
968
|
+
? finalArrowsConfig.activeColor
|
|
969
|
+
: finalArrowsConfig.color,
|
|
395
970
|
userSelect: "none",
|
|
396
971
|
zIndex: 1001,
|
|
397
972
|
opacity: scrollbarVisible ? 1 : 0,
|
|
@@ -399,7 +974,7 @@ thumbRadius, showScrollbar = true, showArrows = false, arrowStep = 50, trackWidt
|
|
|
399
974
|
}, children: "\u25B2" })), showScrollbar && showArrows && (jsxRuntime.jsx("div", { className: "overlay-scrollbar-down-arrow", onClick: handleDownArrowClick, onMouseEnter: () => setHoveredArrow("down"), onMouseLeave: () => setHoveredArrow(null), style: {
|
|
400
975
|
position: "absolute",
|
|
401
976
|
bottom: "4px",
|
|
402
|
-
right: `${(
|
|
977
|
+
right: `${(adjustedTrackWidth - finalThumbWidth) / 2}px`, // 트랙 가운데 정렬
|
|
403
978
|
width: `${finalThumbWidth}px`,
|
|
404
979
|
height: `${finalThumbWidth}px`,
|
|
405
980
|
cursor: "pointer",
|
|
@@ -408,8 +983,8 @@ thumbRadius, showScrollbar = true, showArrows = false, arrowStep = 50, trackWidt
|
|
|
408
983
|
justifyContent: "center",
|
|
409
984
|
fontSize: `${Math.max(finalThumbWidth * 0.75, 8)}px`,
|
|
410
985
|
color: hoveredArrow === "down"
|
|
411
|
-
?
|
|
412
|
-
:
|
|
986
|
+
? finalArrowsConfig.activeColor
|
|
987
|
+
: finalArrowsConfig.color,
|
|
413
988
|
userSelect: "none",
|
|
414
989
|
zIndex: 1001,
|
|
415
990
|
opacity: scrollbarVisible ? 1 : 0,
|