add server tests, improve readme and types

This commit is contained in:
Rene Haas
2022-10-17 11:32:09 +02:00
parent f3bcdfba85
commit 9c3b00a9a9
22 changed files with 5170 additions and 321 deletions
+3 -1
View File
@@ -31,8 +31,9 @@ const defaultRules = {
groups: ['builtin', 'external', 'index', 'internal', 'unknown', 'type'],
pathGroups: [
{
pattern: '**/*.{css,scss,sass}',
pattern: '*.{css,scss,sass}',
group: 'unknown',
patternOptions: { matchBase: true },
position: 'after',
},
],
@@ -46,6 +47,7 @@ const defaultRules = {
},
],
'react/jsx-filename-extension': ['error', { extensions: ['.jsx', '.tsx'] }],
'react/require-default-props': ['off'],
};
const defaultExtends = ['airbnb', 'prettier', 'plugin:react/jsx-runtime'];
const defaultPlugins = ['prettier', 'json', '@typescript-eslint', 'import', 'react'];
+11 -1
View File
@@ -27,12 +27,13 @@
## Why
I've created this plugin because I hate ugly and space consuming scrollbars. Similar plugins haven't met my requirements in terms of features, quality, simplicity, license or browser support.
I created this plugin because I hate ugly and space consuming scrollbars. Similar plugins haven't met my requirements in terms of features, quality, simplicity, license or browser support.
## Goals & Features
- Simple, powerful and good documented API
- High browser compatibility - <b>Firefox</b>, <b>Chrome</b>, <b>Opera</b>, <b>Edge</b>, <b>Safari 10+</b> and <b>IE 11</b>
- Can be run on the server - <b>SSR</b>, <b>SSG</b> and <b>ISR</b> support
- Tested on various devices - <b>Mobile</b>, <b>Desktop</b> and <b>Tablet</b>
- Tested with various (and mixed) inputs - <b>Mouse</b>, <b>touch</b> and <b>pen</b>
- <b>Treeshaking</b> - bundle only what you really need
@@ -85,6 +86,13 @@ You can initialize either directly with an `Element` or with an `Object` where y
const osInstance = OverlayScrollbars(document.querySelector('#myElement'), {});
```
### Bridging initialization flickering
If you initialize OverlayScrollbars it needs a few milliseconds to create and append all the elements to the DOM.
While this period the native scrollbars are still visible and are switched out after the initialization is finished. This is perceived as flickering.
To fix this behavior apply the `data-overlayscrollbars=""` attribute to the target element (and `html` element if the target element is `body`).
<details><summary><h6>Initialization with an Object</h6></summary>
> __Note__: For now please refer to the <b>TypeScript definitions</b> for a more detailed description of all possibilities.
@@ -444,6 +452,7 @@ You can write and publish your own Plugins. This section is a work in progress.
## Sponsors
<table>
<tbody>
<tr>
<td>
<a href="https://www.browserstack.com" target="_blank">
@@ -454,6 +463,7 @@ You can write and publish your own Plugins. This section is a work in progress.
Thanks to <a href="https://www.browserstack.com" target="_blank">BrowserStack</a> for sponsoring open source projects and letting me test OverlayScrollbars for free.
</td>
</tr>
</tbody>
</table>
## Future Plans
+1
View File
@@ -0,0 +1 @@
module.exports = {};
+15 -1
View File
@@ -4,9 +4,16 @@ const resolve = require('./resolve');
// For a detailed explanation regarding each configuration property, visit:
// https://jestjs.io/docs/en/configuration.html
const assetFilesModuleNameMapper = {
'\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$':
path.resolve(__dirname, 'jest.fileMock.js'),
'.*\\.(css|less|scss|sass)$': path.resolve(__dirname, 'jest.fileMock.js'),
};
/** @type {import('jest').Config} */
module.exports = {
coverageDirectory: './.coverage/jest',
moduleNameMapper: assetFilesModuleNameMapper,
projects: [
{
displayName: 'node',
@@ -15,9 +22,12 @@ module.exports = {
clearMocks: true,
moduleDirectories: resolve.directories,
moduleFileExtensions: resolve.extensions.map((ext) => ext.replace(/\./, '')),
moduleNameMapper: {
...assetFilesModuleNameMapper,
...resolve.paths.jest.moduleNameMapper,
},
testPathIgnorePatterns: ['\\\\node_modules\\\\'],
setupFilesAfterEnv: [path.resolve(__dirname, './jest.setup.js')],
...resolve.paths.jest,
},
{
displayName: 'jsdom',
@@ -26,6 +36,10 @@ module.exports = {
clearMocks: true,
moduleDirectories: resolve.directories,
moduleFileExtensions: resolve.extensions.map((ext) => ext.replace(/\./, '')),
moduleNameMapper: {
...assetFilesModuleNameMapper,
...resolve.paths.jest.moduleNameMapper,
},
testPathIgnorePatterns: ['\\\\node_modules\\\\'],
setupFilesAfterEnv: [path.resolve(__dirname, './jest.setup.js')],
...resolve.paths.jest,
+25 -23
View File
@@ -16,32 +16,34 @@ export const esbuildPluginTailwind = ({
build.onEnd(async (result) => {
if (result) {
const { metafile, outputFiles } = result;
const { inputs } = metafile;
const tailwindFile = outputFiles.find(({ path: outputFilePath }) =>
tailwindCssFileRegex.test(outputFilePath)
);
if (tailwindFile) {
const { path: tailwindFilePath, text: tailwindFileCss } = tailwindFile;
const tailwindContentGlobs = (resolvedTailwindConfig?.content || []).filter(
(entry) => typeof entry === 'string'
);
const inputFilePaths = Object.keys(inputs).map((input) => path.resolve(input));
const includedFiles = Array.from(
new Set(
tailwindContentGlobs
.map((glob) => minimatch.match(inputFilePaths, glob, { dot: true }))
.flat()
)
if (metafile && outputFiles) {
const { inputs } = metafile;
const tailwindFile = outputFiles.find(({ path: outputFilePath }) =>
tailwindCssFileRegex.test(outputFilePath)
);
const postcssResult = await postcss([
tailwindcss({ ...(resolvedTailwindConfig || {}), content: includedFiles }),
]).process(tailwindFileCss, {
from: tailwindFilePath,
});
if (tailwindFile) {
const { path: tailwindFilePath, text: tailwindFileCss } = tailwindFile;
const tailwindContentGlobs = (resolvedTailwindConfig?.content || []).filter(
(entry) => typeof entry === 'string'
);
const inputFilePaths = Object.keys(inputs).map((input) => path.resolve(input));
const includedFiles = Array.from(
new Set(
tailwindContentGlobs
.map((glob) => minimatch.match(inputFilePaths, glob, { dot: true }))
.flat()
)
);
tailwindFile.contents = Buffer.from(postcssResult.css);
const postcssResult = await postcss([
tailwindcss({ ...(resolvedTailwindConfig || {}), content: includedFiles }),
]).process(tailwindFileCss, {
from: tailwindFilePath,
});
tailwindFile.contents = Buffer.from(postcssResult.css);
}
}
}
});
+1514
View File
File diff suppressed because it is too large Load Diff
+4 -1
View File
@@ -1,4 +1,7 @@
{
"private": true,
"main": "tailwind.config.js"
"main": "tailwind.config.js",
"devDependencies": {
"@tailwindcss/typography": "^0.5.7"
}
}
+95 -5
View File
@@ -3,13 +3,100 @@ const defaultTheme = require('tailwindcss/defaultTheme');
/** @type {import('tailwindcss').Config} */
module.exports = {
theme: {
extend: {},
extend: {
colors: {
transparent: 'transparent',
current: 'currentColor',
'primary-cyan1': '#33FFFF',
'primary-cyan2': '#87FED1',
'primary-green': '#C0FEB1',
'primary-blue1': '#338EFF',
'primary-blue2': '#4276FF',
'primary-violet': '#5D55FF',
'primary-dark': '#0A376B',
'primary-gray1': '#475774',
'primary-gray2': '#697996',
},
transitionProperty: {
transformColor: 'transform, color',
},
fontFamily: {
sans: ['Noto Sans', ...defaultTheme.fontFamily.sans],
},
typography: ({ theme }) => ({
DEFAULT: {
css: {
b: {
fontWeight: theme('fontWeight.medium'),
},
strong: {
fontWeight: theme('fontWeight.medium'),
},
h5: {
color: theme('colors.primary-dark'),
fontWeight: theme('fontWeight.medium'),
fontSize: theme('fontSize.sm'),
},
h6: {
color: theme('colors.primary-dark'),
fontWeight: theme('fontWeight.medium'),
fontSize: theme('fontSize.sm'),
},
'blockquote > p > strong:first-child': {
color: theme('colors.primary-blue2'),
},
'blockquote p:first-of-type::before': {
content: '',
},
'blockquote p:last-of-type::after': {
content: '',
},
code: {
background: 'var(--tw-prose-pre-bg)',
fontWeight: theme('fontWeight.medium'),
padding: theme('padding[1]'),
borderRadius: theme('borderRadius.md'),
},
'code::before': {
content: '',
},
'code::after': {
content: '',
},
'summary > *:only-child,': {
display: 'inline-block',
},
summary: {
display: 'inline list-item',
cursor: 'pointer',
},
},
},
primary: {
css: {
'--tw-prose-body': theme('colors.primary-gray1'),
'--tw-prose-headings': theme('colors.primary-dark'),
'--tw-prose-lead': theme('colors.primary-gray1'),
'--tw-prose-links': theme('colors.primary-blue2'),
'--tw-prose-bold': theme('colors.primary-dark'),
'--tw-prose-counters': theme('colors.primary-gray1'),
'--tw-prose-bullets': theme('colors.primary-blue2'),
'--tw-prose-hr': theme('colors.slate[200]'),
'--tw-prose-quotes': theme('colors.primary-dark'),
'--tw-prose-quote-borders': theme('colors.slate[200]'),
'--tw-prose-captions': theme('colors.primary-gray1'),
'--tw-prose-code': theme('colors.primary-dark'),
'--tw-prose-pre-code': theme('colors.pink[100]'),
'--tw-prose-pre-bg': theme('colors.slate[100]'),
'--tw-prose-th-borders': theme('colors.slate[200]'),
'--tw-prose-td-borders': theme('colors.slate[200]'),
},
},
}),
},
container: {
center: true,
},
fontFamily: {
sans: ['Noto Sans', ...defaultTheme.fontFamily.sans],
},
fontWeight: {
normal: 400,
medium: 600,
@@ -25,5 +112,8 @@ module.exports = {
xxl: '1536px',
},
},
plugins: [],
plugins: [
// eslint-disable-next-line global-require
require('@tailwindcss/typography'),
],
};
+3373 -243
View File
File diff suppressed because it is too large Load Diff
+1
View File
@@ -13,6 +13,7 @@
"@~local/rollup": "file:./local/rollup",
"@~local/tailwind": "file:./local/tailwind",
"@~local/tsconfig": "file:./local/tsconfig",
"@~package/overlayscrollbars": "file:./packages/overlayscrollbars",
"@babel/core": "^7.18.2",
"@babel/plugin-transform-runtime": "^7.18.2",
"@babel/preset-env": "^7.18.2",
+11 -1
View File
@@ -27,12 +27,13 @@
## Why
I've created this plugin because I hate ugly and space consuming scrollbars. Similar plugins haven't met my requirements in terms of features, quality, simplicity, license or browser support.
I created this plugin because I hate ugly and space consuming scrollbars. Similar plugins haven't met my requirements in terms of features, quality, simplicity, license or browser support.
## Goals & Features
- Simple, powerful and good documented API
- High browser compatibility - <b>Firefox</b>, <b>Chrome</b>, <b>Opera</b>, <b>Edge</b>, <b>Safari 10+</b> and <b>IE 11</b>
- Can be run on the server - <b>SSR</b>, <b>SSG</b> and <b>ISR</b> support
- Tested on various devices - <b>Mobile</b>, <b>Desktop</b> and <b>Tablet</b>
- Tested with various (and mixed) inputs - <b>Mouse</b>, <b>touch</b> and <b>pen</b>
- <b>Treeshaking</b> - bundle only what you really need
@@ -85,6 +86,13 @@ You can initialize either directly with an `Element` or with an `Object` where y
const osInstance = OverlayScrollbars(document.querySelector('#myElement'), {});
```
### Bridging initialization flickering
If you initialize OverlayScrollbars it needs a few milliseconds to create and append all the elements to the DOM.
While this period the native scrollbars are still visible and are switched out after the initialization is finished. This is perceived as flickering.
To fix this behavior apply the `data-overlayscrollbars=""` attribute to the target element (and `html` element if the target element is `body`).
<details><summary><h6>Initialization with an Object</h6></summary>
> __Note__: For now please refer to the <b>TypeScript definitions</b> for a more detailed description of all possibilities.
@@ -444,6 +452,7 @@ You can write and publish your own Plugins. This section is a work in progress.
## Sponsors
<table>
<tbody>
<tr>
<td>
<a href="https://www.browserstack.com" target="_blank">
@@ -454,6 +463,7 @@ You can write and publish your own Plugins. This section is a work in progress.
Thanks to <a href="https://www.browserstack.com" target="_blank">BrowserStack</a> for sponsoring open source projects and letting me test OverlayScrollbars for free.
</td>
</tr>
</tbody>
</table>
## Future Plans
+1 -1
View File
@@ -1,4 +1,4 @@
import '~/index.scss';
import './index.scss';
export { OverlayScrollbars } from '~/overlayscrollbars';
export { ScrollbarsHidingPlugin, SizeObserverPlugin, ClickScrollPlugin } from '~/plugins';
@@ -78,11 +78,6 @@ const unwrap = (elm: HTMLElement | false | null | undefined) => {
removeElements(elm);
};
const addDataAttrHost = (elm: HTMLElement, value: string) => {
attr(elm, dataAttributeHost, value);
return removeAttr.bind(0, elm, dataAttributeHost);
};
export const createStructureSetupElements = (
target: InitializationTarget
): StructureSetupElements => {
@@ -113,14 +108,11 @@ export const createStructureSetupElements = (
const targetElement = targetIsElm ? target : targetStructureInitialization.target;
const isTextarea = is(targetElement, 'textarea');
const ownerDocument = targetElement.ownerDocument;
const docElement = ownerDocument.documentElement;
const isBody = targetElement === ownerDocument.body;
const wnd = ownerDocument.defaultView as Window;
const staticInitializationElement = generalStaticInitializationElement<
[InitializationTargetElement]
>.bind(0, [targetElement]);
const dynamicInitializationElement = generalDynamicInitializationElement<
[InitializationTargetElement]
>.bind(0, [targetElement]);
const staticInitializationElement = generalStaticInitializationElement.bind(0, [targetElement]);
const dynamicInitializationElement = generalDynamicInitializationElement.bind(0, [targetElement]);
const viewportElement = staticInitializationElement(
createNewDiv,
defaultViewportInitialization,
@@ -155,7 +147,7 @@ export const createStructureSetupElements = (
!_nativeScrollbarsHiding &&
createUniqueViewportArrangeElement &&
createUniqueViewportArrangeElement(env),
_scrollOffsetElement: viewportIsTargetBody ? ownerDocument.documentElement : viewportElement,
_scrollOffsetElement: viewportIsTargetBody ? docElement : viewportElement,
_scrollEventElement: viewportIsTargetBody ? ownerDocument : viewportElement,
_windowElm: wnd,
_documentElm: ownerDocument,
@@ -179,7 +171,15 @@ export const createStructureSetupElements = (
const elementIsGenerated = (elm: HTMLElement | false) =>
elm ? indexOf(generatedElements, elm) > -1 : null;
const { _target, _host, _padding, _viewport, _content, _viewportArrange } = evaluatedTargetObj;
const destroyFns: (() => any)[] = [];
const destroyFns: (() => any)[] = [
() => {
// always remove dataAttributeHost from host and from <html> element if target is body
removeAttr(_host, dataAttributeHost);
if (isBody) {
removeAttr(docElement, dataAttributeHost);
}
},
];
const isTextareaHostGenerated = isTextarea && elementIsGenerated(_host);
let targetContents = isTextarea
? _target
@@ -190,7 +190,8 @@ export const createStructureSetupElements = (
);
const contentSlot = _content || _viewport;
const appendElements = () => {
const removeHostDataAttr = addDataAttrHost(_host, viewportIsTarget ? 'viewport' : 'host');
attr(_host, dataAttributeHost, viewportIsTarget ? 'viewport' : 'host');
const removePaddingClass = addClass(_padding, classNamePadding);
const removeViewportClass = addClass(_viewport, !viewportIsTarget && classNameViewport);
const removeContentClass = addClass(_content, classNameContent);
@@ -215,7 +216,6 @@ export const createStructureSetupElements = (
push(destroyFns, () => {
removeHtmlClass();
removeHostDataAttr();
removeAttr(_viewport, dataAttributeHostOverflowX);
removeAttr(_viewport, dataAttributeHostOverflowY);
@@ -416,18 +416,16 @@ export const createOverflowUpdateSegment: CreateStructureUpdateSegment = (
};
const overflowAmountClientSize = {
w: max0(
viewportIsTargetBody
(viewportIsTargetBody
? _windowElm.innerWidth
: arrangedViewportClientSize.w +
max0(viewportclientSize.w - viewportScrollSize.w) +
sizeFraction.w
: arrangedViewportClientSize.w + max0(viewportclientSize.w - viewportScrollSize.w)) +
sizeFraction.w
),
h: max0(
viewportIsTargetBody
? _windowElm.innerHeight
: arrangedViewportClientSize.h +
max0(viewportclientSize.h - viewportScrollSize.h) +
sizeFraction.h
(viewportIsTargetBody
? _windowElm.innerHeight + sizeFraction.h
: arrangedViewportClientSize.h + max0(viewportclientSize.h - viewportScrollSize.h)) +
sizeFraction.h
),
};
@@ -1,3 +1,4 @@
import { isClient } from '~/support/compatibility/server';
import { jsAPI } from '~/support/compatibility/vendors';
export const MutationObserverConstructor = jsAPI<typeof MutationObserver>('MutationObserver');
@@ -6,5 +7,8 @@ export const IntersectionObserverConstructor =
export const ResizeObserverConstructor = jsAPI<typeof ResizeObserver>('ResizeObserver');
export const cAF = jsAPI<typeof cancelAnimationFrame>('cancelAnimationFrame');
export const rAF = jsAPI<typeof requestAnimationFrame>('requestAnimationFrame');
export const setT = window.setTimeout as (handler: TimerHandler, timeout?: number) => number;
export const clearT = window.clearTimeout as (id?: number) => void;
export const setT = (isClient() && window.setTimeout) as (
handler: TimerHandler,
timeout?: number
) => number;
export const clearT = (isClient() && window.clearTimeout) as (id?: number) => void;
@@ -1,2 +1,3 @@
export * from '~/support/compatibility/vendors';
export * from '~/support/compatibility/apis';
export * from '~/support/compatibility/server';
@@ -0,0 +1 @@
export const isClient = () => typeof window !== 'undefined';
@@ -1,4 +1,5 @@
import { each } from '~/support/utils/array';
import { isClient } from '~/support/compatibility/server';
import { hasOwnProperty } from '~/support/utils/object';
import { createDiv } from '~/support/dom/create';
@@ -95,17 +96,19 @@ export const cssPropertyValue = (property: string, values: string, suffix?: stri
* @param name The name of the JS function, object or constructor.
*/
export const jsAPI = <T = any>(name: string): T | undefined => {
let result: any = jsCache[name] || window[name];
if (isClient()) {
let result: any = jsCache[name] || window[name];
if (hasOwnProperty(jsCache, name)) {
if (hasOwnProperty(jsCache, name)) {
return result;
}
each(jsPrefixes, (prefix: string) => {
result = result || window[prefix + firstLetterToUpper(name)];
return !result;
});
jsCache[name] = result;
return result;
}
each(jsPrefixes, (prefix: string) => {
result = result || window[prefix + firstLetterToUpper(name)];
return !result;
});
jsCache[name] = result;
return result;
};
@@ -1,10 +1,11 @@
import { isClient } from '~/support/compatibility/server';
import { isElement } from '~/support/utils/types';
import { push, from } from '~/support/utils/array';
type InputElementType = Node | Element | Node | false | null | undefined;
type OutputElementType = Node | Element | null;
const elmPrototype = Element.prototype;
const getElmPrototype = (isClient() && Element.prototype) as Element; // only Element.prototype wont work on server
/**
* Find all elements with the passed selector, outgoing (and including) the passed element or the document if no element was provided.
@@ -38,8 +39,9 @@ const is = (elm: InputElementType, selector: string): boolean => {
if (isElement(elm)) {
/* istanbul ignore next */
// eslint-disable-next-line
// @ts-ignore
const fn: (...args: any) => boolean = elmPrototype.matches || elmPrototype.msMatchesSelector;
const fn: (...args: any) => boolean =
// @ts-ignore
getElmPrototype.matches || getElmPrototype.msMatchesSelector;
return fn.call(elm, selector);
}
return false;
@@ -76,7 +78,7 @@ const parent = (elm: InputElementType): OutputElementType => (elm ? elm.parentEl
const closest = (elm: InputElementType, selector: string): OutputElementType => {
if (isElement(elm)) {
const closestFn = elmPrototype.closest;
const closestFn = getElmPrototype.closest;
if (closestFn) {
return closestFn.call(elm, selector);
}
@@ -1,6 +1,7 @@
import { isClient } from '~/support/compatibility/server';
import type { PlainObject } from '~/typings';
const ElementNodeType = Node.ELEMENT_NODE;
const ElementNodeType = isClient() && Node.ELEMENT_NODE;
const { toString, hasOwnProperty } = Object.prototype;
export const isUndefined = (obj: any): obj is undefined => obj === undefined;
@@ -130,14 +130,20 @@ const assertCorrectDOMStructure = (targetType: TargetType, viewportIsTarget: boo
const createStructureSetupElementsProxy = (
target: InitializationTarget,
tabindex?: boolean
options: { tabindex?: boolean; autoAppend?: boolean } = {
tabindex: false,
autoAppend: true,
}
): StructureSetupElementsProxy => {
const { tabindex, autoAppend } = options;
const [elements, appendElements, destroy] = createStructureSetupElements(target);
// simulate tabindex inheritance from host via mutation observer
if (tabindex) {
elements._viewport.setAttribute('tabindex', elements._target.getAttribute('tabindex')!);
}
appendElements();
if (autoAppend) {
appendElements();
}
return {
input: target,
elements,
@@ -1121,7 +1127,7 @@ describe('structureSetup.elements', () => {
const target = document.body.firstElementChild as HTMLElement;
target.focus();
const { elements } = createStructureSetupElementsProxy(target, true);
const { elements } = createStructureSetupElementsProxy(target, { tabindex: true });
expect(elements._viewport.getAttribute('tabindex')).toBe('-1');
expect(document.activeElement).toBe(elements._viewport);
@@ -1149,4 +1155,35 @@ describe('structureSetup.elements', () => {
expect(preInitFocus).toBe(document.activeElement);
});
});
describe('data-overlayscrollbars attribute', () => {
test('already set data-overlayscrollbars attribute is removed', () => {
const target = document.body;
target.setAttribute(dataAttributeHost, '');
const { destroy } = createStructureSetupElementsProxy(target);
destroy();
expect(document.body.getAttribute(dataAttributeHost)).toBe(null);
});
test('already set data-overlayscrollbars attribute is removed even if initialization gets canceled', () => {
const target = document.body;
target.setAttribute(dataAttributeHost, '');
const { destroy } = createStructureSetupElementsProxy(target, { autoAppend: false });
destroy();
expect(document.body.getAttribute(dataAttributeHost)).toBe(null);
});
test('already set data-overlayscrollbars attribute on html element is removed if target is body', () => {
document.documentElement.setAttribute(dataAttributeHost, '');
const { destroy } = createStructureSetupElementsProxy(document.body);
destroy();
expect(document.documentElement.getAttribute(dataAttributeHost)).toBe(null);
});
});
});
@@ -0,0 +1,25 @@
describe('usage on server', () => {
test('import', async () => {
await expect(
(async () => {
const module = await import('~/index');
return module;
})()
).resolves.toBeTruthy();
});
test('static functions', async () => {
await expect(
(async () => {
const { OverlayScrollbars } = await import('~/index');
expect(() => {
OverlayScrollbars.valid(false);
}).not.toThrow();
expect(() => {
OverlayScrollbars.plugin({});
}).not.toThrow();
})()
).resolves.not.toThrow();
});
});