From ad9b40e82f94a51a72be88bd2c349bcfb4f7cb3c Mon Sep 17 00:00:00 2001 From: "Sinan Sonmez (Chaush)" <sinansonmez@outlook.com> Date: Fri, 22 Nov 2024 20:34:42 +0000 Subject: [PATCH 1/4] initial migration for index.js --- package.json | 3 +- src/{index.js => index.tsx} | 92 ++++++++++++++++++++++++++----------- tsconfig.json | 35 ++++++++++++++ 3 files changed, 101 insertions(+), 29 deletions(-) rename src/{index.js => index.tsx} (82%) create mode 100644 tsconfig.json diff --git a/package.json b/package.json index c716dbb..25c8b38 100644 --- a/package.json +++ b/package.json @@ -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" } } diff --git a/src/index.js b/src/index.tsx similarity index 82% rename from src/index.js rename to src/index.tsx index e4a62f6..624754c 100644 --- a/src/index.js +++ b/src/index.tsx @@ -1,25 +1,58 @@ 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, @@ -27,14 +60,14 @@ const viewportStyle = { overflow: 'auto' }; -const contentStyle = { +const contentStyle: CSSProperties = { display: 'inline-block', position: 'relative', minWidth: '100%', boxSizing: 'border-box' }; -function reducer(state, action) { +function reducer(state, action: PayloadAction<>) { 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,18 +184,18 @@ 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: string, refs: {viewport: MutableRefObject<HTMLDivElement | null>}) => 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, @@ -170,7 +203,9 @@ Overflow.propTypes = { * 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 @@ -188,20 +223,20 @@ 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>(null); + const toleranceRef = useRef<HTMLDivElement | null>(null); const watchRef = tolerance ? toleranceRef : contentRef; - const observersRef = useRef(); + const observersRef = useRef<HTMLDivElement | 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) { @@ -230,10 +265,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,7 +286,7 @@ function OverflowContent({ children, style: styleProp, ...rest }) { const observers = observersRef.current; const watchNode = watchRef.current; - observers.up.observe(watchNode); + observers?.up.observe(watchNode); observers.left.observe(watchNode); observers.right.observe(watchNode); observers.down.observe(watchNode); @@ -304,11 +339,12 @@ 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 }; /** @@ -352,7 +388,7 @@ OverflowContent.propTypes = { * </Overflow> * ``` */ -function OverflowIndicator({ children, direction }) { +function OverflowIndicator({ children, direction } : OverflowIndicator) { const { state, refs } = useOverflow(); const { canScroll } = state; const isActive = direction @@ -372,19 +408,19 @@ 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; diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..a81f956 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,35 @@ +{ + "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/**/*", + "./src/**/*.json" + ], + "exclude": [ + "node_modules" + ] +} From c8a68b63d9976510d06c6d65ecb09b835f519f81 Mon Sep 17 00:00:00 2001 From: "Sinan Sonmez (Chaush)" <sinansonmez@outlook.com> Date: Fri, 22 Nov 2024 21:46:26 +0000 Subject: [PATCH 2/4] finish index.js migration --- src/index.tsx | 105 ++++++++++++++++++++++++++++++-------------------- 1 file changed, 63 insertions(+), 42 deletions(-) diff --git a/src/index.tsx b/src/index.tsx index 624754c..ff94713 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -15,29 +15,29 @@ enum Direction { left = 'left', right = 'right', down = 'down' -}; +} interface CanScroll { - [Direction.up]: boolean - [Direction.left]: boolean - [Direction.right]: boolean - [Direction.down]: boolean + [Direction.up]: boolean; + [Direction.left]: boolean; + [Direction.right]: boolean; + [Direction.down]: boolean; } interface Dispatch { - type: string - direction: keyof typeof Direction - canScroll: boolean + type: string; + direction: keyof typeof Direction; + canScroll: boolean; } interface OverflowContext { - tolerance?: number | string - refs: { viewport: MutableRefObject<HTMLDivElement | null> } - canScroll?: CanScroll + tolerance?: number | string; + refs: { viewport: MutableRefObject<HTMLDivElement | null> }; + canScroll?: CanScroll; state: { - canScroll: CanScroll - } - dispatch: ({type, direction, canScroll}: Dispatch) => void + canScroll: CanScroll; + }; + dispatch?: ({ type, direction, canScroll }: Dispatch) => void; } const Context = React.createContext<OverflowContext>({}); @@ -67,7 +67,7 @@ const contentStyle: CSSProperties = { boxSizing: 'border-box' }; -function reducer(state, action: PayloadAction<>) { +function reducer(state: { canScroll: CanScroll }, action: Dispatch) { switch (action.type) { case 'CHANGE': { const currentValue = state.canScroll[action.direction]; @@ -190,12 +190,15 @@ interface Overflow { * `<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: ReactNode, + 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: (state: string, refs: {viewport: MutableRefObject<HTMLDivElement | null>}) => void, + 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, @@ -203,10 +206,10 @@ interface Overflow { * 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: number | string - style: CSSProperties - hidden: boolean -}; + 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). @@ -223,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 }: OverflowContent) { +function OverflowContent({ + children, + style: styleProp, + ...rest +}: OverflowContent) { const { dispatch, tolerance, refs } = useOverflow(); const { viewport: viewportRef } = refs; - const contentRef = useRef<HTMLDivElement | null>(null); - const toleranceRef = useRef<HTMLDivElement | null>(null); + const contentRef = useRef<HTMLDivElement>(null); + const toleranceRef = useRef<HTMLDivElement>(null); const watchRef = tolerance ? toleranceRef : contentRef; - const observersRef = useRef<HTMLDivElement | null>(null); + 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: Direction, rootMargin: string) => { + const createObserver = (direction: Direction, rootMargin?: string) => { return new IntersectionObserver( ([entry]) => { if (ignore) { @@ -254,7 +266,7 @@ function OverflowContent({ children, style: styleProp, ...rest }: OverflowConten // case. entry.intersectionRatio !== 0 && entry.isIntersecting; - dispatch({ type: 'CHANGE', direction, canScroll }); + dispatch?.({ type: 'CHANGE', direction, canScroll }); }, { root, @@ -286,16 +298,20 @@ function OverflowContent({ children, style: styleProp, ...rest }: OverflowConten 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]); @@ -343,9 +359,9 @@ interface OverflowContent { /** * Content to render inside the scrollable viewport. */ - children: ReactNode - style: CSSProperties -}; + children: ReactNode; + style: CSSProperties; +} /** * A helper component for rendering your custom indicator when the viewport is @@ -388,7 +404,7 @@ interface OverflowContent { * </Overflow> * ``` */ -function OverflowIndicator({ children, direction } : OverflowIndicator) { +function OverflowIndicator({ children, direction }: OverflowIndicator) { const { state, refs } = useOverflow(); const { canScroll } = state; const isActive = direction @@ -415,13 +431,18 @@ interface OverflowIndicator { * 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: ReactElement | ((stateArg: boolean | CanScroll, refs: OverflowContext['refs']) => ReactElement) + 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: keyof typeof Direction -}; + direction: keyof typeof Direction; +} Overflow.Indicator = OverflowIndicator; Overflow.Content = OverflowContent; From f8f5cd91e5e17333a6d61f45ecaf02f79c33a511 Mon Sep 17 00:00:00 2001 From: "Sinan Sonmez (Chaush)" <sinansonmez@outlook.com> Date: Fri, 22 Nov 2024 21:48:49 +0000 Subject: [PATCH 3/4] replace index.js with index.tsx --- rollup.config.js | 2 +- scripts/extract-docs.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/rollup.config.js b/rollup.config.js index d4d38c0..7872f6e 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -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' }, diff --git a/scripts/extract-docs.js b/scripts/extract-docs.js index e9056f3..e4f3421 100755 --- a/scripts/extract-docs.js +++ b/scripts/extract-docs.js @@ -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]); From be597a4d57abc99bcac9e08b05ae2c3501318e41 Mon Sep 17 00:00:00 2001 From: "Sinan Sonmez (Chaush)" <sinansonmez@outlook.com> Date: Fri, 22 Nov 2024 21:51:51 +0000 Subject: [PATCH 4/4] update ts.config --- tsconfig.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tsconfig.json b/tsconfig.json index a81f956..1391cda 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -26,8 +26,7 @@ "incremental": true }, "include": [ - "./src/**/*", - "./src/**/*.json" + "./src/**/*" ], "exclude": [ "node_modules"