improve scrollbar styling

This commit is contained in:
Rene Haas
2022-07-27 11:48:34 +02:00
parent 93b45cef5a
commit f686c4513b
10 changed files with 72 additions and 62 deletions
+5 -2
View File
@@ -27,10 +27,13 @@ export const classNameTrinsicObserver = 'os-trinsic-observer';
export const classNameScrollbar = 'os-scrollbar'; export const classNameScrollbar = 'os-scrollbar';
export const classNameScrollbarHorizontal = `${classNameScrollbar}-horizontal`; export const classNameScrollbarHorizontal = `${classNameScrollbar}-horizontal`;
export const classNameScrollbarVertical = `${classNameScrollbar}-vertical`; export const classNameScrollbarVertical = `${classNameScrollbar}-vertical`;
export const classNameScrollbarTrack = 'os-scrollbar-track'; export const classNameScrollbarTrack = `${classNameScrollbar}-track`;
export const classNameScrollbarHandle = 'os-scrollbar-handle'; export const classNameScrollbarHandle = `${classNameScrollbar}-handle`;
export const classNamesScrollbarVisible = `${classNameScrollbar}-visible`; export const classNamesScrollbarVisible = `${classNameScrollbar}-visible`;
export const classNamesScrollbarCornerless = `${classNameScrollbar}-cornerless`; export const classNamesScrollbarCornerless = `${classNameScrollbar}-cornerless`;
export const classNamesScrollbarTransitionless = `${classNameScrollbar}-transitionless`; export const classNamesScrollbarTransitionless = `${classNameScrollbar}-transitionless`;
export const classNamesScrollbarInteraction = `${classNameScrollbar}-interaction`; export const classNamesScrollbarInteraction = `${classNameScrollbar}-interaction`;
export const classNamesScrollbarUnusable = `${classNameScrollbar}-unusable`;
export const classNamesScrollbarAutoHidden = `${classNameScrollbar}-auto-hidden`; export const classNamesScrollbarAutoHidden = `${classNameScrollbar}-auto-hidden`;
export const classNamesScrollbarTrackInteractive = `${classNameScrollbarTrack}-interactive`;
export const classNamesScrollbarHandleInteractive = `${classNameScrollbarHandle}-interactive`;
+4 -11
View File
@@ -1,5 +1,5 @@
import { assignDeep, each, isObject, keys, isArray, hasOwnProperty, isFunction } from 'support'; import { assignDeep, each, isObject, keys, isArray, hasOwnProperty, isFunction } from 'support';
import { DeepPartial, ReadonlyOptions } from 'typings'; import { DeepPartial, DeepReadonly } from 'typings';
const opsStringify = (value: any) => const opsStringify = (value: any) =>
JSON.stringify(value, (_, val) => { JSON.stringify(value, (_, val) => {
@@ -58,11 +58,11 @@ export interface Options {
autoHideDelay: number; autoHideDelay: number;
dragScroll: boolean; dragScroll: boolean;
clickScroll: boolean; clickScroll: boolean;
touch: boolean; pointers: string[] | null;
}; };
} }
export type ReadonlyOSOptions = ReadonlyOptions<Options>; export type ReadonlyOptions = DeepReadonly<Options>;
export interface OverflowChangedArgs { export interface OverflowChangedArgs {
x: boolean; x: boolean;
@@ -112,15 +112,8 @@ export const defaultOptions: Options = {
autoHideDelay: 800, // number autoHideDelay: 800, // number
dragScroll: true, // true || false dragScroll: true, // true || false
clickScroll: false, // true || false clickScroll: false, // true || false
touch: true, // true || false pointers: ['mouse', 'touch', 'pen'], // null || array of supported pointers: https://developer.mozilla.org/en-US/docs/Web/API/PointerEvent/pointerType
}, },
/*
textarea: {
dynWidth: false, // true || false
dynHeight: false, // true || false
inheritedAttrs: ['style', 'class'], // string || array || null
},
*/
}; };
export const getOptionsDiff = <T>(currOptions: T, newOptions: DeepPartial<T>): DeepPartial<T> => { export const getOptionsDiff = <T>(currOptions: T, newOptions: DeepPartial<T>): DeepPartial<T> => {
@@ -10,7 +10,7 @@ import {
createEventListenerHub, createEventListenerHub,
} from 'support'; } from 'support';
import { createStructureSetup, createScrollbarsSetup } from 'setups'; import { createStructureSetup, createScrollbarsSetup } from 'setups';
import { getOptionsDiff, Options, ReadonlyOSOptions } from 'options'; import { getOptionsDiff, Options, ReadonlyOptions } from 'options';
import { getEnvironment } from 'environment'; import { getEnvironment } from 'environment';
import { import {
getPlugins, getPlugins,
@@ -156,7 +156,7 @@ export const OverlayScrollbars: OverlayScrollbarsStatic = (
const validate = optionsValidationPlugin && optionsValidationPlugin._; const validate = optionsValidationPlugin && optionsValidationPlugin._;
return validate ? validate(opts, true) : opts; return validate ? validate(opts, true) : opts;
}; };
const currentOptions: ReadonlyOSOptions = assignDeep( const currentOptions: ReadonlyOptions = assignDeep(
{}, {},
_getDefaultOptions(), _getDefaultOptions(),
validateOptions(options) validateOptions(options)
@@ -44,7 +44,7 @@ const optionsTemplate: OptionsTemplate<Options> = {
autoHideDelay: numberAllowedValues, // number autoHideDelay: numberAllowedValues, // number
dragScroll: booleanAllowedValues, // true || false dragScroll: booleanAllowedValues, // true || false
clickScroll: booleanAllowedValues, // true || false clickScroll: booleanAllowedValues, // true || false
touch: booleanAllowedValues, // true || false pointers: [oTypes.array, oTypes.null], // string array
}, },
/* /*
textarea: { textarea: {
@@ -9,6 +9,7 @@ import {
} from 'support'; } from 'support';
import { classNamesScrollbarInteraction } from 'classnames'; import { classNamesScrollbarInteraction } from 'classnames';
import { getScrollbarHandleLengthRatio } from 'setups/scrollbarsSetup/scrollbarsSetup.calculations'; import { getScrollbarHandleLengthRatio } from 'setups/scrollbarsSetup/scrollbarsSetup.calculations';
import type { ReadonlyOptions } from 'options';
import type { StructureSetupState } from 'setups'; import type { StructureSetupState } from 'setups';
import type { import type {
ScrollbarsSetupElementsObj, ScrollbarsSetupElementsObj,
@@ -35,6 +36,12 @@ const getScale = (element: HTMLElement): XY<number> => {
y: Math.round(height) / h || 1, y: Math.round(height) / h || 1,
}; };
}; };
const continuePointerDown = (event: PointerEvent, options: ReadonlyOptions, scrollType: 'dragScroll' | 'clickScroll') => {
const scrollbarOptions = options.scrollbars;
const { button, isPrimary, pointerType } = event;
const { pointers } = scrollbarOptions;
return button === 0 && isPrimary && scrollbarOptions[scrollType] && (pointers || []).includes(pointerType);
}
const createRootClickStopPropagationEvents = (scrollbar: HTMLElement, documentElm: Document) => const createRootClickStopPropagationEvents = (scrollbar: HTMLElement, documentElm: Document) =>
on( on(
scrollbar, scrollbar,
@@ -43,6 +50,7 @@ const createRootClickStopPropagationEvents = (scrollbar: HTMLElement, documentEl
{ _capture: true } { _capture: true }
); );
const createDragScrollingEvents = ( const createDragScrollingEvents = (
options: ReadonlyOptions,
doc: Document, doc: Document,
scrollbarHandle: HTMLElement, scrollbarHandle: HTMLElement,
scrollOffsetElement: HTMLElement, scrollOffsetElement: HTMLElement,
@@ -62,9 +70,7 @@ const createDragScrollingEvents = (
}; };
return on(scrollbarHandle, 'pointerdown', (pointerDownEvent: PointerEvent) => { return on(scrollbarHandle, 'pointerdown', (pointerDownEvent: PointerEvent) => {
const { button, isPrimary, pointerId } = pointerDownEvent; if (continuePointerDown(pointerDownEvent, options, 'dragScroll')) {
if (button === 0 && isPrimary) {
const offSelectStart = on(doc, 'selectstart', (event: Event) => preventDefault(event), { const offSelectStart = on(doc, 'selectstart', (event: Event) => preventDefault(event), {
_passive: false, _passive: false,
}); });
@@ -88,13 +94,13 @@ const createDragScrollingEvents = (
}, },
{ _once: true } { _once: true }
); );
scrollbarHandle.setPointerCapture(pointerId); scrollbarHandle.setPointerCapture(pointerDownEvent.pointerId);
} }
}); });
}; };
export const createScrollbarsSetupEvents = export const createScrollbarsSetupEvents =
(structureSetupState: () => StructureSetupState): ScrollbarsSetupEvents => (options: ReadonlyOptions, structureSetupState: () => StructureSetupState): ScrollbarsSetupEvents =>
(scrollbarStructure, scrollbarsAddRemoveClass, documentElm, scrollOffsetElm, isHorizontal) => { (scrollbarStructure, scrollbarsAddRemoveClass, documentElm, scrollOffsetElm, isHorizontal) => {
const { _scrollbar, _handle } = scrollbarStructure; const { _scrollbar, _handle } = scrollbarStructure;
@@ -107,6 +113,7 @@ export const createScrollbarsSetupEvents =
}), }),
createRootClickStopPropagationEvents(_scrollbar, documentElm), createRootClickStopPropagationEvents(_scrollbar, documentElm),
createDragScrollingEvents( createDragScrollingEvents(
options,
documentElm, documentElm,
_handle, _handle,
scrollOffsetElm, scrollOffsetElm,
@@ -24,12 +24,15 @@ import {
} from 'setups/scrollbarsSetup/scrollbarsSetup.elements'; } from 'setups/scrollbarsSetup/scrollbarsSetup.elements';
import { import {
classNamesScrollbarVisible, classNamesScrollbarVisible,
classNamesScrollbarUnusable,
classNamesScrollbarCornerless, classNamesScrollbarCornerless,
classNamesScrollbarAutoHidden, classNamesScrollbarAutoHidden,
classNamesScrollbarHandleInteractive,
classNamesScrollbarTrackInteractive,
} from 'classnames'; } from 'classnames';
import type { StructureSetupUpdateHints } from 'setups/structureSetup/structureSetup.update'; import type { StructureSetupUpdateHints } from 'setups/structureSetup/structureSetup.update';
import type { import type {
ReadonlyOSOptions, ReadonlyOptions,
ScrollbarVisibilityBehavior, ScrollbarVisibilityBehavior,
ScrollbarAutoHideBehavior, ScrollbarAutoHideBehavior,
} from 'options'; } from 'options';
@@ -100,7 +103,7 @@ const refreshScrollbarHandleOffset = (
export const createScrollbarsSetup = ( export const createScrollbarsSetup = (
target: InitializationTarget, target: InitializationTarget,
options: ReadonlyOSOptions, options: ReadonlyOptions,
structureSetupState: (() => StructureSetupState) & StructureSetupStaticState structureSetupState: (() => StructureSetupState) & StructureSetupStaticState
): Setup<ScrollbarsSetupState, ScrollbarsSetupStaticState, [StructureSetupUpdateHints]> => { ): Setup<ScrollbarsSetupState, ScrollbarsSetupStaticState, [StructureSetupUpdateHints]> => {
let autoHideIsMove: boolean; let autoHideIsMove: boolean;
@@ -120,7 +123,7 @@ export const createScrollbarsSetup = (
const [elements, appendElements, destroyElements] = createScrollbarsSetupElements( const [elements, appendElements, destroyElements] = createScrollbarsSetupElements(
target, target,
structureSetupState._elements, structureSetupState._elements,
createScrollbarsSetupEvents(structureSetupState) createScrollbarsSetupEvents(options, structureSetupState)
); );
const { const {
_host, _host,
@@ -221,10 +224,8 @@ export const createScrollbarsSetup = (
const [autoHide, autoHideChanged] = const [autoHide, autoHideChanged] =
checkOption<ScrollbarAutoHideBehavior>('scrollbars.autoHide'); checkOption<ScrollbarAutoHideBehavior>('scrollbars.autoHide');
const [autoHideDelay] = checkOption<number>('scrollbars.autoHideDelay'); const [autoHideDelay] = checkOption<number>('scrollbars.autoHideDelay');
const [dragScrolling, dragScrollingChanged] = checkOption<boolean>( const [dragScroll, dragScrollChanged] = checkOption<boolean>('scrollbars.dragScroll');
'scrollbars.dragScrolling' const [clickScroll, clickScrollChanged] = checkOption<boolean>('scrollbars.clickScroll');
);
const [touchSupport, touchSupportChanged] = checkOption<boolean>('scrollbars.touchSupport');
const updateHandle = _overflowEdgeChanged || _overflowAmountChanged; const updateHandle = _overflowEdgeChanged || _overflowAmountChanged;
const updateVisibility = _overflowStyleChanged || visibilityChanged; const updateVisibility = _overflowStyleChanged || visibilityChanged;
@@ -238,15 +239,6 @@ export const createScrollbarsSetup = (
globalAutoHideDelay = autoHideDelay; globalAutoHideDelay = autoHideDelay;
if (updateVisibility) {
const { _overflowStyle } = currStructureSetupState;
const xVisible = setScrollbarVisibility(_overflowStyle.x, true);
const yVisible = setScrollbarVisibility(_overflowStyle.y, false);
const hasCorner = xVisible && yVisible;
scrollbarsAddRemoveClass(classNamesScrollbarCornerless, !hasCorner);
}
if (themeChanged) { if (themeChanged) {
scrollbarsAddRemoveClass(prevTheme); scrollbarsAddRemoveClass(prevTheme);
scrollbarsAddRemoveClass(theme, true); scrollbarsAddRemoveClass(theme, true);
@@ -259,7 +251,23 @@ export const createScrollbarsSetup = (
autoHideNotNever = autoHide !== 'never'; autoHideNotNever = autoHide !== 'never';
manageScrollbarsAutoHide(!autoHideNotNever, true); manageScrollbarsAutoHide(!autoHideNotNever, true);
} }
if (dragScrollChanged) {
scrollbarsAddRemoveClass(classNamesScrollbarHandleInteractive, dragScroll);
}
if (clickScrollChanged) {
scrollbarsAddRemoveClass(classNamesScrollbarTrackInteractive, clickScroll);
}
if (updateVisibility) {
const { _overflowStyle } = currStructureSetupState;
const xVisible = setScrollbarVisibility(_overflowStyle.x, true);
const yVisible = setScrollbarVisibility(_overflowStyle.y, false);
const hasCorner = xVisible && yVisible;
scrollbarsAddRemoveClass(classNamesScrollbarCornerless, !hasCorner);
}
if (updateHandle) { if (updateHandle) {
const { _overflowAmount } = currStructureSetupState;
refreshScrollbarHandleLength(styleHorizontal, currStructureSetupState, true); refreshScrollbarHandleLength(styleHorizontal, currStructureSetupState, true);
refreshScrollbarHandleLength(styleVertical, currStructureSetupState); refreshScrollbarHandleLength(styleVertical, currStructureSetupState);
@@ -270,6 +278,9 @@ export const createScrollbarsSetup = (
true true
); );
refreshScrollbarHandleOffset(styleVertical, currStructureSetupState, _scrollOffsetElement); refreshScrollbarHandleOffset(styleVertical, currStructureSetupState, _scrollOffsetElement);
scrollbarsAddRemoveClass(classNamesScrollbarUnusable, !_overflowAmount.x, true);
scrollbarsAddRemoveClass(classNamesScrollbarUnusable, !_overflowAmount.y, false);
} }
}, },
scrollbarsSetupState, scrollbarsSetupState,
@@ -1,5 +1,5 @@
import { assignDeep, hasOwnProperty } from 'support'; import { assignDeep, hasOwnProperty } from 'support';
import type { Options, ReadonlyOSOptions } from 'options'; import type { Options, ReadonlyOptions } from 'options';
import type { DeepPartial } from 'typings'; import type { DeepPartial } from 'typings';
export type SetupElements<T extends Record<string, any>> = [elements: T, destroy: () => void]; export type SetupElements<T extends Record<string, any>> = [elements: T, destroy: () => void];
@@ -36,7 +36,7 @@ const getPropByPath = <T>(obj: any, path: string): T =>
export const createOptionCheck = export const createOptionCheck =
( (
options: ReadonlyOSOptions, options: ReadonlyOptions,
changedOptions: DeepPartial<Options>, changedOptions: DeepPartial<Options>,
force?: boolean force?: boolean
): SetupUpdateCheckOption => ): SetupUpdateCheckOption =>
@@ -6,7 +6,7 @@ import { createStructureSetupObservers } from 'setups/structureSetup/structureSe
import type { StructureSetupUpdateHints } from 'setups/structureSetup/structureSetup.update'; import type { StructureSetupUpdateHints } from 'setups/structureSetup/structureSetup.update';
import type { StructureSetupElementsObj } from 'setups/structureSetup/structureSetup.elements'; import type { StructureSetupElementsObj } from 'setups/structureSetup/structureSetup.elements';
import type { TRBL, XY, EventListener } from 'support'; import type { TRBL, XY, EventListener } from 'support';
import type { Options, ReadonlyOSOptions } from 'options'; import type { Options, ReadonlyOptions } from 'options';
import type { Setup } from 'setups'; import type { Setup } from 'setups';
import type { InitializationTarget } from 'initialization'; import type { InitializationTarget } from 'initialization';
import type { DeepPartial, StyleObject, OverflowStyle } from 'typings'; import type { DeepPartial, StyleObject, OverflowStyle } from 'typings';
@@ -67,7 +67,7 @@ const initialStructureSetupUpdateState: StructureSetupState = {
export const createStructureSetup = ( export const createStructureSetup = (
target: InitializationTarget, target: InitializationTarget,
options: ReadonlyOSOptions options: ReadonlyOptions
): Setup<StructureSetupState, StructureSetupStaticState> => { ): Setup<StructureSetupState, StructureSetupStaticState> => {
const checkOptionsFallback = createOptionCheck(options, {}); const checkOptionsFallback = createOptionCheck(options, {});
const state = createState(initialStructureSetupUpdateState); const state = createState(initialStructureSetupUpdateState);
@@ -6,7 +6,7 @@
transition: opacity 0.3s, visibility 0.3s, top 0.3s, right 0.3s, bottom 0.3s, left 0.3s; transition: opacity 0.3s, visibility 0.3s, top 0.3s, right 0.3s, bottom 0.3s, left 0.3s;
pointer-events: none; pointer-events: none;
position: absolute; position: absolute;
z-index: 0; z-index: 99999;
opacity: 0; opacity: 0;
visibility: hidden; visibility: hidden;
} }
@@ -17,32 +17,24 @@ body > .os-scrollbar {
transition: none; transition: none;
} }
.os-scrollbar-track { .os-scrollbar-track {
pointer-events: none;
position: relative; position: relative;
height: 100%;
width: 100%;
padding: 0 !important; padding: 0 !important;
border: none !important; border: none !important;
} }
.os-scrollbar-handle { .os-scrollbar-handle {
pointer-events: none;
position: absolute; position: absolute;
}
.os-scrollbar-track,
.os-scrollbar-handle {
pointer-events: none;
width: 100%; width: 100%;
height: 100%; height: 100%;
} }
.os-scrollbar-handle-interactive, .os-scrollbar.os-scrollbar-track-interactive .os-scrollbar-track,
.os-scrollbar-track-interactive { .os-scrollbar.os-scrollbar-handle-interactive .os-scrollbar-handle {
pointer-events: auto; pointer-events: auto;
touch-action: none; touch-action: none;
} }
.os-scrollbar-unusable,
.os-scrollbar-unusable * {
pointer-events: none !important;
}
.os-scrollbar-unusable .os-scrollbar-handle {
opacity: 0 !important;
visibility: hidden Im !important;
}
.os-scrollbar-horizontal { .os-scrollbar-horizontal {
bottom: 0; bottom: 0;
left: 0; left: 0;
@@ -58,7 +50,8 @@ body > .os-scrollbar {
right: auto; right: auto;
left: 0; left: 0;
} }
.os-scrollbar-visible { .os-scrollbar-visible,
.os-scrollbar-interaction.os-scrollbar-visible {
opacity: 1; opacity: 1;
visibility: visible; visibility: visible;
} }
@@ -66,9 +59,12 @@ body > .os-scrollbar {
opacity: 0; opacity: 0;
visibility: hidden; visibility: hidden;
} }
.os-scrollbar-interaction.os-scrollbar-visible { .os-scrollbar-unusable,
opacity: 1; .os-scrollbar-unusable * {
visibility: visible; pointer-events: none !important;
}
.os-scrollbar-unusable .os-scrollbar-handle {
opacity: 0 !important;
} }
.os-scrollbar.os-scrollbar-horizontal.os-scrollbar-cornerless { .os-scrollbar.os-scrollbar-horizontal.os-scrollbar-cornerless {
left: 0; left: 0;
+2 -2
View File
@@ -2,8 +2,8 @@ export type DeepPartial<T> = {
[P in keyof T]?: T[P] extends Record<string, unknown> ? DeepPartial<T[P]> : T[P]; [P in keyof T]?: T[P] extends Record<string, unknown> ? DeepPartial<T[P]> : T[P];
}; };
export type ReadonlyOptions<T> = { export type DeepReadonly<T> = {
readonly [P in keyof T]: T[P] extends Record<string, unknown> ? ReadonlyOptions<T[P]> : T[P]; readonly [P in keyof T]: T[P] extends Record<string, unknown> ? DeepReadonly<T[P]> : T[P];
}; };
export type PlainObject<T = any> = { [name: string]: T }; export type PlainObject<T = any> = { [name: string]: T };