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
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
@@ -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",
@@ -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
@@ -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' },
2 changes: 1 addition & 1 deletion scripts/extract-docs.js
Original file line number Diff line number Diff line change
@@ -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]);
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];
@@ -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(
() => ({
@@ -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).
@@ -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) {
@@ -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,
@@ -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;
@@ -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]);

@@ -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
@@ -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
@@ -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"
]
}