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

DRAFT: Typescript migration #34

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@
"@babel/preset-env": "^7.7.1",
"@babel/preset-react": "^7.7.0",
"@rollup/plugin-replace": "^2.2.1",
"@types/react": "^18.3.12",
"bundlesize": "^0.18.0",
"escape-html": "^1.0.3",
"eslint": "^6.6.0",
Expand All @@ -76,6 +77,6 @@
"rollup-plugin-terser": "^5.1.2"
},
"dependencies": {
"prop-types": "^15.7.2"
"typescript": "^5.7.2"
}
}
2 changes: 1 addition & 1 deletion rollup.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { terser } from 'rollup-plugin-terser';
import pkg from './package.json';

const baseConfig = {
input: 'src/index.js',
input: 'src/index.tsx',
external: ['react', 'react-dom', 'prop-types'],
output: [
{ file: pkg.main, format: 'cjs' },
Expand Down
2 changes: 1 addition & 1 deletion scripts/extract-docs.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ const fs = require('fs');
const reactDocs = require('react-docgen');
const displayNameHandler = require('react-docgen-displayname-handler').default;

const files = ['src/index.js'];
const files = ['src/index.tsx'];

const resolver = reactDocs.resolver.findAllComponentDefinitions;
const handlers = reactDocs.defaultHandlers.concat([displayNameHandler]);
Expand Down
135 changes: 96 additions & 39 deletions src/index.js → src/index.tsx
Original file line number Diff line number Diff line change
@@ -1,40 +1,73 @@
import React, {
CSSProperties,
MutableRefObject,
ReactElement,
ReactNode,
useContext,
useEffect,
useMemo,
useReducer,
useRef
} from 'react';
import PropTypes from 'prop-types';

const Context = React.createContext();
enum Direction {
up = 'up',
left = 'left',
right = 'right',
down = 'down'
}

interface CanScroll {
[Direction.up]: boolean;
[Direction.left]: boolean;
[Direction.right]: boolean;
[Direction.down]: boolean;
}

interface Dispatch {
type: string;
direction: keyof typeof Direction;
canScroll: boolean;
}

interface OverflowContext {
tolerance?: number | string;
refs: { viewport: MutableRefObject<HTMLDivElement | null> };
canScroll?: CanScroll;
state: {
canScroll: CanScroll;
};
dispatch?: ({ type, direction, canScroll }: Dispatch) => void;
}

const Context = React.createContext<OverflowContext>({});

export function useOverflow() {
return useContext(Context);
}

const containerStyle = {
const containerStyle: CSSProperties = {
display: 'flex',
flexDirection: 'column',
position: 'relative'
};

const viewportStyle = {
const viewportStyle: CSSProperties = {
position: 'relative',
flexBasis: '100%',
flexShrink: 1,
flexGrow: 0,
overflow: 'auto'
};

const contentStyle = {
const contentStyle: CSSProperties = {
display: 'inline-block',
position: 'relative',
minWidth: '100%',
boxSizing: 'border-box'
};

function reducer(state, action) {
function reducer(state: { canScroll: CanScroll }, action: Dispatch) {
switch (action.type) {
case 'CHANGE': {
const currentValue = state.canScroll[action.direction];
Expand Down Expand Up @@ -104,10 +137,10 @@ export default function Overflow({
style: styleProp,
tolerance = 0,
...rest
}) {
}: Overflow) {
const [state, dispatch] = useReducer(reducer, null, getInitialState);
const hidden = rest.hidden;
const viewportRef = useRef();
const viewportRef = useRef<HTMLDivElement>(null);

const style = useMemo(
() => ({
Expand Down Expand Up @@ -151,27 +184,32 @@ export default function Overflow({
);
}

Overflow.propTypes = {
interface Overflow {
/**
* Elements to render inside the outer container. This should include an
* `<Overflow.Content>` element at a minimum, but should also include your
* scroll indicators if you’d like to overlay them on the scrollable viewport.
*/
children: PropTypes.node,
children: ReactNode;
/**
* Callback that receives the latest overflow state and an object of refs, if
* you’d like to react to overflow in a custom way.
*/
onStateChange: PropTypes.func,
onStateChange: (
state: OverflowContext['state'],
refs: OverflowContext['refs']
) => void;
/**
* Distance (number of pixels or CSS length unit like `1em`) to the edge of
* the content at which to consider the viewport fully scrolled. For example,
* if set to 10, then it will consider scrolling to have reached the end as
* long as it’s within 10 pixels of the border. You can use this when your
* content has padding and scrolling close to the edge should be good enough.
*/
tolerance: PropTypes.oneOfType([PropTypes.number, PropTypes.string])
};
tolerance: number | string;
style: CSSProperties;
hidden: boolean;
}

// For Firefox, update on a threshold of 0 in addition to any intersection at
// all (represented by a tiny tiny threshold).
Expand All @@ -188,20 +226,29 @@ const threshold = [0, 1e-12];
* own element inside `<Overflow.Content>` instead – otherwise you risk
* interfering with the styles this component needs to function.
*/
function OverflowContent({ children, style: styleProp, ...rest }) {
function OverflowContent({
children,
style: styleProp,
...rest
}: OverflowContent) {
const { dispatch, tolerance, refs } = useOverflow();
const { viewport: viewportRef } = refs;
const contentRef = useRef();
const toleranceRef = useRef();
const contentRef = useRef<HTMLDivElement>(null);
const toleranceRef = useRef<HTMLDivElement>(null);
const watchRef = tolerance ? toleranceRef : contentRef;
const observersRef = useRef();
const observersRef = useRef<{
[Direction.up]: IntersectionObserver;
[Direction.left]: IntersectionObserver;
[Direction.down]: IntersectionObserver;
[Direction.right]: IntersectionObserver;
} | null>(null);

useEffect(() => {
let ignore = false;

const root = viewportRef.current;

const createObserver = (direction, rootMargin) => {
const createObserver = (direction: Direction, rootMargin?: string) => {
return new IntersectionObserver(
([entry]) => {
if (ignore) {
Expand All @@ -219,7 +266,7 @@ function OverflowContent({ children, style: styleProp, ...rest }) {
// case.
entry.intersectionRatio !== 0 &&
entry.isIntersecting;
dispatch({ type: 'CHANGE', direction, canScroll });
dispatch?.({ type: 'CHANGE', direction, canScroll });
},
{
root,
Expand All @@ -230,10 +277,10 @@ function OverflowContent({ children, style: styleProp, ...rest }) {
};

const observers = {
up: createObserver('up', '100% 0px -100% 0px'),
left: createObserver('left', '0px -100% 0px 100%'),
right: createObserver('right', '0px 100% 0px -100%'),
down: createObserver('down', '-100% 0px 100% 0px')
up: createObserver(Direction.up, '100% 0px -100% 0px'),
left: createObserver(Direction.left, '0px -100% 0px 100%'),
right: createObserver(Direction.right, '0px 100% 0px -100%'),
down: createObserver(Direction.down, '-100% 0px 100% 0px')
};

observersRef.current = observers;
Expand All @@ -251,16 +298,20 @@ function OverflowContent({ children, style: styleProp, ...rest }) {
const observers = observersRef.current;
const watchNode = watchRef.current;

observers.up.observe(watchNode);
observers.left.observe(watchNode);
observers.right.observe(watchNode);
observers.down.observe(watchNode);
if (watchNode) {
observers?.up.observe(watchNode);
observers?.left.observe(watchNode);
observers?.right.observe(watchNode);
observers?.down.observe(watchNode);
}

return () => {
observers.up.unobserve(watchNode);
observers.left.unobserve(watchNode);
observers.right.unobserve(watchNode);
observers.down.unobserve(watchNode);
if (watchNode) {
observers?.up.unobserve(watchNode);
observers?.left.unobserve(watchNode);
observers?.right.unobserve(watchNode);
observers?.down.unobserve(watchNode);
}
};
}, [watchRef]);

Expand Down Expand Up @@ -304,12 +355,13 @@ function OverflowContent({ children, style: styleProp, ...rest }) {

OverflowContent.displayName = 'Overflow.Content';

OverflowContent.propTypes = {
interface OverflowContent {
/**
* Content to render inside the scrollable viewport.
*/
children: PropTypes.node
};
children: ReactNode;
style: CSSProperties;
}

/**
* A helper component for rendering your custom indicator when the viewport is
Expand Down Expand Up @@ -352,7 +404,7 @@ OverflowContent.propTypes = {
* </Overflow>
* ```
*/
function OverflowIndicator({ children, direction }) {
function OverflowIndicator({ children, direction }: OverflowIndicator) {
const { state, refs } = useOverflow();
const { canScroll } = state;
const isActive = direction
Expand All @@ -372,20 +424,25 @@ function OverflowIndicator({ children, direction }) {

OverflowIndicator.displayName = 'Overflow.Indicator';

OverflowIndicator.propTypes = {
interface OverflowIndicator {
/**
* Indicator to render when scrolling is allowed in the requested direction.
* If given a function, it will be passed the overflow state and an object
* containing the `viewport` ref. You can use this `refs` parameter to render
* an indicator that is also a button that scrolls the viewport (for example).
*/
children: PropTypes.oneOfType([PropTypes.node, PropTypes.func]),
children:
| ReactElement
| ((
stateArg: boolean | CanScroll,
refs: OverflowContext['refs']
) => ReactElement);
/**
* The scrollabe direction to watch for. If not supplied, the indicator will
* be active when scrolling is allowed in any direction.
*/
direction: PropTypes.oneOf(['up', 'down', 'left', 'right'])
};
direction: keyof typeof Direction;
}

Overflow.Indicator = OverflowIndicator;
Overflow.Content = OverflowContent;
34 changes: 34 additions & 0 deletions tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
{
"compilerOptions": {
"target": "esnext",
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"allowJs": true,
"skipLibCheck": true,
"esModuleInterop": true,
"isolatedModules": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"strictNullChecks": true,
"forceConsistentCasingInFileNames": true,
"module": "es2020",
"moduleResolution": "node",
"resolveJsonModule": true,
"noEmit": true,
"experimentalDecorators": true,
"jsx": "preserve",
"baseUrl": "./src",
"useUnknownInCatchVariables": false,
"composite": true,
"incremental": true
},
"include": [
"./src/**/*"
],
"exclude": [
"node_modules"
]
}