Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add styleOptions.stylesRoot prop #137

Merged
merged 14 commits into from
May 30, 2024
3 changes: 3 additions & 0 deletions .eslintrc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ parserOptions:
ecmaVersion: 12
sourceType: module

env:
es6: true

plugins:
- prettier

Expand Down
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.

## [Unreleased]

### Changed

- Added support for passing additional options to internal Emotion instance via `emotionOptions` property, by [@OEvgeny](https://github.com/OEvgeny) in PR [#137](https://github.com/compulim/react-scroll-to-bottom/pull/137)

## [4.2.0] - 2021-10-14

### Changed
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ export default props => (
| `className` | `string` | | Set the class name for the root element |
| `debounce` | `number` | `17` | Set the debounce for tracking the `onScroll` event |
| `debug` | `bool` | `NODE_ENV === 'development'` | Show debug information in console |
| `emotionOptions` | `object` | `undefined` | Additional options for the internal Emotion instance |
| `followButtonClassName` | `string` | | Set the class name for the follow button |
| `initialScrollBehavior` | `string` | `smooth` | Set the initial scroll behavior, either `"auto"` (discrete scrolling) or `"smooth"` |
| `mode` | `string` | `"bottom"` | Set it to `"bottom"` for scroll-to-bottom, `"top"` for scroll-to-top |
Expand Down
52 changes: 52 additions & 0 deletions __tests__/should-respect-options.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title></title>
<link href="/assets/index.css" rel="stylesheet" type="text/css" />
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<script src="/react-scroll-to-bottom.development.js"></script>
<script src="/test-harness.js"></script>
<script src="/assets/page-object-model.js"></script>
</head>
<body>
<div id="styles"></div>
<div id="app"></div>
</body>
<script type="text/babel" data-presets="react">
'use strict';

const additionalOptions = {
container: document.getElementById('styles')
};

run(async function () {
await new Promise(resolve =>
ReactDOM.render(
<ReactScrollToBottom.default
className="react-scroll-to-bottom"
followButtonClassName="follow"
scrollViewClassName="scrollable"
emotionOptions={additionalOptions}
>
{pageObjects.paragraphs.map(paragraph => (
<p key={paragraph}>{paragraph}</p>
))}
</ReactScrollToBottom.default>,
document.getElementById('app'),
resolve
)
);

await pageObjects.scrollStabilizedAtBottom();

expect(document.getElementsByClassName('follow')[0]).toBeFalsy();

await pageObjects.mouseWheel(-100);

await pageObjects.scrollStabilized();

expect(document.getElementsByClassName('follow')[0]).toBeTruthy();
expect(document.getElementById('styles').childElementCount).toBeGreaterThan(0);
});
</script>
</html>
3 changes: 3 additions & 0 deletions __tests__/should-respect-options.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
/** @jest-environment ./packages/test-harness/JestEnvironment */

test('should-respect-options.html', () => runHTML('should-respect-options.html'));
4 changes: 4 additions & 0 deletions packages/component/src/BasicScrollToBottom.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ const BasicScrollToBottom = ({
className,
debounce,
debug,
emotionOptions,
followButtonClassName,
initialScrollBehavior,
mode,
Expand All @@ -53,6 +54,7 @@ const BasicScrollToBottom = ({
checkInterval={checkInterval}
debounce={debounce}
debug={debug}
emotionOptions={emotionOptions}
initialScrollBehavior={initialScrollBehavior}
mode={mode}
nonce={nonce}
Expand All @@ -74,6 +76,7 @@ BasicScrollToBottom.defaultProps = {
className: undefined,
debounce: undefined,
debug: undefined,
emotionOptions: undefined,
followButtonClassName: undefined,
initialScrollBehavior: 'smooth',
mode: undefined,
Expand All @@ -88,6 +91,7 @@ BasicScrollToBottom.propTypes = {
className: PropTypes.string,
debounce: PropTypes.number,
debug: PropTypes.bool,
emotionOptions: PropTypes.any,
followButtonClassName: PropTypes.string,
initialScrollBehavior: PropTypes.oneOf(['auto', 'smooth']),
mode: PropTypes.oneOf(['bottom', 'top']),
Expand Down
19 changes: 6 additions & 13 deletions packages/component/src/ScrollToBottom/Composer.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import createEmotion from '@emotion/css/create-instance';
import PropTypes from 'prop-types';
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';

import createCSSKey from '../createCSSKey';
import createDebug from '../utils/debug';
import EventSpy from '../EventSpy';
import FunctionContext from './FunctionContext';
Expand All @@ -12,6 +10,7 @@ import State1Context from './State1Context';
import State2Context from './State2Context';
import StateContext from './StateContext';
import styleConsole from '../utils/styleConsole';
import useEmotion from '../hooks/internal/useEmotion';
import useStateRef from '../hooks/internal/useStateRef';

const DEFAULT_SCROLLER = () => Infinity;
Expand All @@ -21,10 +20,6 @@ const MODE_TOP = 'top';
const NEAR_END_THRESHOLD = 1;
const SCROLL_DECISION_DURATION = 34; // 2 frames

// We pool the emotion object by nonce.
// This is to make sure we don't generate too many unneeded <style> tags.
const emotionPool = {};

function setImmediateInterval(fn, ms) {
fn();

Expand Down Expand Up @@ -55,6 +50,7 @@ const Composer = ({
children,
debounce,
debug: debugFromProp,
emotionOptions,
initialScrollBehavior,
mode,
nonce,
Expand Down Expand Up @@ -501,13 +497,8 @@ const Composer = ({
}
}, [animateToRef, checkInterval, debug, mode, scrollToSticky, setSticky, stickyRef, target, targetRef]);

const styleToClassName = useMemo(() => {
const emotion =
emotionPool[nonce] ||
(emotionPool[nonce] = createEmotion({ key: 'react-scroll-to-bottom--css-' + createCSSKey(), nonce }));

return style => emotion.css(style) + '';
}, [nonce]);
const emotion = useEmotion(nonce, emotionOptions);
const styleToClassName = useCallback(style => emotion.css(style) + '', [emotion]);

const internalContext = useMemo(
() => ({
Expand Down Expand Up @@ -623,6 +614,7 @@ Composer.defaultProps = {
children: undefined,
debounce: 17,
debug: undefined,
emotionOptions: undefined,
initialScrollBehavior: 'smooth',
mode: undefined,
nonce: undefined,
Expand All @@ -634,6 +626,7 @@ Composer.propTypes = {
children: PropTypes.any,
debounce: PropTypes.number,
debug: PropTypes.bool,
emotionOptions: PropTypes.any,
initialScrollBehavior: PropTypes.oneOf(['auto', 'smooth']),
mode: PropTypes.oneOf(['bottom', 'top']),
nonce: PropTypes.string,
Expand Down
58 changes: 58 additions & 0 deletions packages/component/src/hooks/internal/useEmotion.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import createEmotion from '@emotion/css/create-instance';
import { useEffect, useMemo, useRef } from 'react';

import createCSSKey from '../../createCSSKey';

const sharedEmotionByNonce = new Map();
const sharedEmotionUsedTimes = new WeakMap();

export default function useEmotion(nonce, emotionOptions) {
const emotion = useMemo(() => {
const defaultOptions = {
key: 'react-scroll-to-bottom--css-' + createCSSKey(),
nonce
};
if (emotionOptions) {
return createEmotion({
...defaultOptions,
...emotionOptions
});
}
const emotion = sharedEmotionByNonce.get(nonce) ?? createEmotion(defaultOptions);
sharedEmotionByNonce.set(nonce, emotion);
sharedEmotionUsedTimes.set(emotion, sharedEmotionUsedTimes.get(emotion) ?? 0 + 1);
return emotion;
}, [nonce, emotionOptions]);

const nonceRef = useRef();
nonceRef.current = nonce;

useEffect(
() =>
emotion &&
(() => {
if (sharedEmotionUsedTimes.has(emotion)) {
sharedEmotionUsedTimes.set(emotion, sharedEmotionUsedTimes.get(emotion) ?? 0 - 1);
if (sharedEmotionUsedTimes.get(emotion) > 0) {
return;
}
}

sharedEmotionUsedTimes.delete(emotion);

if (sharedEmotionByNonce.get(nonceRef.current) === emotion) {
sharedEmotionByNonce.delete(nonceRef.current);
}

const {
sheet: { container, tags }
} = emotion;
for (const child of tags) {
container.removeChild(child);
}
}),
[emotion]
);

return emotion;
}