diff --git a/babel.config.js b/babel.config.js index 0e7ae84..4a5470b 100644 --- a/babel.config.js +++ b/babel.config.js @@ -11,7 +11,8 @@ module.exports = { } } ], - '@babel/preset-react' + '@babel/preset-react', + '@babel/typescript', ], comments: false, env: { diff --git a/jest.config.js b/jest.config.js index 1a7ddc4..ab53cc9 100644 --- a/jest.config.js +++ b/jest.config.js @@ -17,7 +17,7 @@ module.exports = { setupFiles: [require.resolve('core-js')], - moduleFileExtensions: ['jsx', 'js'], + moduleFileExtensions: ['ts', 'tsx', 'jsx', 'js'], collectCoverageFrom: ['/src/**/*.{js,jsx}'], @@ -30,19 +30,19 @@ module.exports = { branches: 100, function: 100, lines: 100, - statements: 100 - } + statements: 100, + }, }, // A map from regular expressions to paths to transformers transform: { - '^.+\\.(js|jsx)$': require.resolve('./test-harness/preprocessor'), - '^(?!.*\\.(js|jsx|css|json)$)': require.resolve('./test-harness/fileTransform') + '^.+\\.(ts|tsx|js|jsx)$': require.resolve('./test-harness/preprocessor'), + '^(?!.*\\.(ts|tsx|js|jsx|css|json)$)': require.resolve('./test-harness/fileTransform'), }, verbose: true, testURL: 'http://localhost', // The test environment that will be used for testing - testEnvironment: 'jest-environment-jsdom-global' + testEnvironment: 'jest-environment-jsdom-global', }; diff --git a/package.json b/package.json index a881488..13f7b30 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ ], "main": "dist/index.js", "scripts": { - "build": "rimraf dist && NODE_ENV=production babel --config-file=./babel.config.js src -d dist", + "build": "rimraf dist && NODE_ENV=production babel --config-file=./babel.config.js src -d dist --extensions '.js,.jsx,.ts,.tsx' && tsc", "lint": "eslint --config .eslintrc --ext .jsx --ext .js .", "lint:fix": "eslint --config .eslintrc --ext .jsx --ext .js . --fix", "test": "jest --config=./jest.config.js", @@ -40,10 +40,13 @@ "@babel/plugin-transform-runtime": "~7.9.6", "@babel/preset-env": "~7.9.6", "@babel/preset-react": "~7.9.4", + "@babel/preset-typescript": "^7.10.1", "@babel/register": "~7.9.0", "@babel/runtime": "~7.9.6", "@testing-library/jest-dom": "~5.7.0", "@testing-library/react": "~10.0.4", + "@types/jest": "^26.0.0", + "@types/react": "^16.9.36", "babel-eslint": "~10.1.0", "babel-jest": "~26.0.1", "babel-loader": "~8.1.0", @@ -72,6 +75,8 @@ "react-dom": "~16.13.1", "rimraf": "~3.0.2", "snyk": "~1.320.1", + "ts-jest": "^26.1.0", + "typescript": "^3.9.5", "uuid": "~8.0.0" }, "peerDependencies": { diff --git a/src/Hide.jsx b/src/Hide.tsx similarity index 76% rename from src/Hide.jsx rename to src/Hide.tsx index 074695a..98225d2 100644 --- a/src/Hide.jsx +++ b/src/Hide.tsx @@ -1,8 +1,14 @@ import React, { Fragment } from 'react'; import PropTypes from 'prop-types'; import useThreshold from './useThreshold'; +import { Threshold } from './ThresholdMap'; -export const Hide = props => { +interface HideThresholdProps { + thresholds?: Array; + children: React.ReactNode; +} + +export const Hide = (props: HideThresholdProps) => { const { children, thresholds } = props; const breakpoints = Array.isArray(thresholds) ? thresholds : [thresholds]; const threshold = useThreshold(); @@ -15,7 +21,7 @@ Hide.propTypes = { /** @ignore */ children: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.node), PropTypes.node]).isRequired, /** A single value or an array of values to hide this containers content */ - thresholds: PropTypes.oneOfType([PropTypes.string, PropTypes.array]).isRequired + thresholds: PropTypes.oneOfType([PropTypes.string, PropTypes.array]).isRequired, }; export default Hide; diff --git a/src/ResponsiveContext.jsx b/src/ResponsiveContext.js similarity index 100% rename from src/ResponsiveContext.jsx rename to src/ResponsiveContext.js diff --git a/src/ResponsiveProvider.jsx b/src/ResponsiveProvider.tsx similarity index 72% rename from src/ResponsiveProvider.jsx rename to src/ResponsiveProvider.tsx index 8c082a1..e1a1005 100644 --- a/src/ResponsiveProvider.jsx +++ b/src/ResponsiveProvider.tsx @@ -2,9 +2,15 @@ import React from 'react'; import PropTypes from 'prop-types'; import ResponsiveContext from './ResponsiveContext'; +import { ThresholdMap } from './ThresholdMap'; import defaultThresholdMap from './defaultThresholdMap'; -export const ResponsiveProvider = props => { +interface WithThresholdProps { + thresholdMap?: ThresholdMap; + children: React.ReactNode; +} + +export const ResponsiveProvider = (props: WithThresholdProps) => { const { thresholdMap, children } = props; const getThresholdMap = () => { @@ -17,11 +23,11 @@ ResponsiveProvider.propTypes = { /** The names and values of the responsive breakpoints */ thresholdMap: PropTypes.object, /** @ignore */ - children: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.node), PropTypes.node]).isRequired + children: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.node), PropTypes.node]).isRequired, }; ResponsiveProvider.defaultProps = { - thresholdMap: defaultThresholdMap + thresholdMap: defaultThresholdMap, }; export default ResponsiveProvider; diff --git a/src/Show.jsx b/src/Show.tsx similarity index 76% rename from src/Show.jsx rename to src/Show.tsx index 8c63574..8927be9 100644 --- a/src/Show.jsx +++ b/src/Show.tsx @@ -1,8 +1,14 @@ import React, { Fragment } from 'react'; import PropTypes from 'prop-types'; import useThreshold from './useThreshold'; +import { Threshold } from './ThresholdMap'; -export const Show = props => { +interface ShowThresholdProps { + thresholds?: Array; + children: React.ReactNode; +} + +export const Show = (props: ShowThresholdProps) => { const { children, thresholds } = props; const breakpoints = Array.isArray(thresholds) ? thresholds : [thresholds]; const threshold = useThreshold(); @@ -15,7 +21,7 @@ Show.propTypes = { /** @ignore */ children: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.node), PropTypes.node]).isRequired, /** A single value or an array of values to show this containers content */ - thresholds: PropTypes.oneOfType([PropTypes.string, PropTypes.array]).isRequired + thresholds: PropTypes.oneOfType([PropTypes.string, PropTypes.array]).isRequired, }; export default Show; diff --git a/src/ThresholdMap.ts b/src/ThresholdMap.ts new file mode 100644 index 0000000..1fea3e5 --- /dev/null +++ b/src/ThresholdMap.ts @@ -0,0 +1,4 @@ +export type Threshold = 'xs' | 'sm' | 'md' | 'lg' | 'xl'; +export type ThresholdMap = { + [key in Threshold]?: number; +}; diff --git a/src/WithResponsiveProps.jsx b/src/WithResponsiveProps.tsx similarity index 83% rename from src/WithResponsiveProps.jsx rename to src/WithResponsiveProps.tsx index 14b713e..e2a5b94 100644 --- a/src/WithResponsiveProps.jsx +++ b/src/WithResponsiveProps.tsx @@ -4,6 +4,10 @@ import ResponsiveContext from './ResponsiveContext'; import useThreshold from './useThreshold'; import defaultThresholdMap from './defaultThresholdMap'; +export interface ResponsivePropsConfig { + propKeys: Array; +} + // This component takes a given property, like size, and based on the current threshold replaces that value // for example on a mobile phone the value for size would be replaced with the value from xs // e.g. size={{xs: 'small', 'md': 'large'}} @@ -11,8 +15,8 @@ import defaultThresholdMap from './defaultThresholdMap'; // propKeys: [ 'size' ], // } -const WithResponsiveProps = configuration => WrappedComponent => { - const Component = props => { +const WithResponsiveProps = (configuration: ResponsivePropsConfig) => (WrappedComponent: React.ComponentType) => { + const Component = (props: any) => { const threshold = useThreshold(); const responsiveContext = useContext(ResponsiveContext); diff --git a/src/WithThreshold.jsx b/src/WithThreshold.tsx similarity index 70% rename from src/WithThreshold.jsx rename to src/WithThreshold.tsx index 0b3c531..eea6693 100644 --- a/src/WithThreshold.jsx +++ b/src/WithThreshold.tsx @@ -1,17 +1,22 @@ -/* eslint-disable react/prop-types */ -/* eslint-disable react/destructuring-assignment */ import React from 'react'; import getThreshold from './getThreshold'; +import { Threshold } from './ThresholdMap'; import ResponsiveContext from './ResponsiveContext'; import defaultThresholdMap from './defaultThresholdMap'; -const withThreshold = () => Component => { - class WithThreshold extends React.Component { - map; +interface WithThresholdProps { + threshold?: Threshold; +} - timeout = 0; +function withThreshold

(Component: React.ComponentType

) { + return class extends React.Component { + static contextType = ResponsiveContext; + static displayName = Component.displayName || `WithThreshold${Component.name}`; - constructor(props) { + private map: ThresholdMap = defaultThresholdMap; + private timeout = 0; + + constructor(props: P) { super(props); this.state = { @@ -63,10 +68,8 @@ const withThreshold = () => Component => { return ; } - } - WithThreshold.contextType = ResponsiveContext; - - return WithThreshold; -}; + }; +} -export default withThreshold; +const WithThreshold = () => withThreshold; +export default WithThreshold; diff --git a/src/defaultThresholdMap.js b/src/defaultThresholdMap.ts similarity index 55% rename from src/defaultThresholdMap.js rename to src/defaultThresholdMap.ts index c23c368..66f5923 100644 --- a/src/defaultThresholdMap.js +++ b/src/defaultThresholdMap.ts @@ -1,4 +1,6 @@ -const defaultThresholdMap = () => { +import { ThresholdMap } from './ThresholdMap'; + +const defaultThresholdMap = (): ThresholdMap => { return { xs: 0, sm: 480, diff --git a/src/getThreshold.js b/src/getThreshold.ts similarity index 74% rename from src/getThreshold.js rename to src/getThreshold.ts index 15dfadc..1691797 100644 --- a/src/getThreshold.js +++ b/src/getThreshold.ts @@ -1,5 +1,7 @@ -const getThreshold = (width = 0, breakpoints = { xs: 0 }) => { - const breakpointKeys = Object.keys(breakpoints); +import { Threshold, ThresholdMap } from './ThresholdMap'; + +const getThreshold = (width: number = 0, breakpoints: ThresholdMap = { xs: 0 }) => { + const breakpointKeys = Object.keys(breakpoints) as Array; let result = breakpointKeys[0]; diff --git a/src/responsivePropBuilder.js b/src/responsivePropBuilder.ts similarity index 73% rename from src/responsivePropBuilder.js rename to src/responsivePropBuilder.ts index 11c2533..3430ff0 100644 --- a/src/responsivePropBuilder.js +++ b/src/responsivePropBuilder.ts @@ -1,8 +1,17 @@ +import { Threshold } from './ThresholdMap'; import defaultThresholdMap from './defaultThresholdMap'; -const getCurrentValue = value => (value !== undefined ? value : null); +const getCurrentValue = (value: any) => (value !== undefined ? value : null); -const responsivePropBuilder = (currentThreshold, props, configuration, thresholdMap = defaultThresholdMap) => { +export interface GenericProps { + [key: string]: any; +} + +export interface ResponsivePropsConfig { + propKeys: Array; +} + +const responsivePropBuilder = (currentThreshold: Threshold, props: GenericProps, configuration: ResponsivePropsConfig, thresholdMap = defaultThresholdMap) => { // get the keys from the map, e.g. ['xs', 'sm', 'md', 'lg', 'xl'] const thresholdKeys = Object.keys(thresholdMap); @@ -16,10 +25,10 @@ const responsivePropBuilder = (currentThreshold, props, configuration, threshold // only an object can contain responsive values, null is an object also but that's not valid // e.g. size={{xs: 'h4', md: 'h3'}} - const propKeys = configuration.propKeys.filter(propKey => typeof props[propKey] === 'object' && props[propKey] !== null); + const propKeys = configuration.propKeys.filter((propKey: string) => typeof props[propKey] === 'object' && props[propKey] !== null); // loop through the props that have been found as being responsive and extract an object of name/value pairs - const translatedValues = propKeys.reduce((acc, propKey) => { + const translatedValues = propKeys.reduce((acc: any, propKey: string) => { let result = null; // find the first threshold with a value. That is our value because we reversed them above starting at the current threshold and moving to smaller thresholds for (let i = 0; i < thresholds.length; i++) { diff --git a/src/useThreshold.js b/src/useThreshold.ts similarity index 89% rename from src/useThreshold.js rename to src/useThreshold.ts index 832d28c..732e9df 100644 --- a/src/useThreshold.js +++ b/src/useThreshold.ts @@ -1,10 +1,11 @@ /* eslint-disable import/no-named-as-default */ import { useState, useEffect, useContext } from 'react'; import getThreshold from './getThreshold'; +import { ThresholdMap } from './ThresholdMap'; import ResponsiveContext from './ResponsiveContext'; import defaultThresholdMap from './defaultThresholdMap'; -const getCurrentThreshold = map => getThreshold(window.innerWidth, map); +const getCurrentThreshold = (map: ThresholdMap) => getThreshold(window.innerWidth, map); function useThreshold() { const responsiveContext = useContext(ResponsiveContext); diff --git a/tests/requestAnimationFrame.spec.jsx b/tests/requestAnimationFrame.spec.tsx similarity index 99% rename from tests/requestAnimationFrame.spec.jsx rename to tests/requestAnimationFrame.spec.tsx index 2bb729f..eaf110f 100644 --- a/tests/requestAnimationFrame.spec.jsx +++ b/tests/requestAnimationFrame.spec.tsx @@ -8,7 +8,7 @@ const breakpoints = { sm: 480, md: 768, lg: 960, - xl: 1280 + xl: 1280, }; beforeEach(() => { diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..3f076d9 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,28 @@ +{ + "compilerOptions": { + "outDir": "lib", + "target": "es5", + "module": "commonjs", + "jsx": "react", + "allowJs": true, + "declaration": true, + "emitDeclarationOnly": true, + "sourceMap": true, + "importHelpers": true, + "forceConsistentCasingInFileNames": true, + "moduleResolution": "Node", + "noImplicitThis": true, + "noImplicitReturns": true, + "noImplicitAny": true, + "skipLibCheck": true, + "lib": ["es5", "dom"], + "types": ["node", "jest"], + "noUnusedLocals": true, + "resolveJsonModule": true, + "esModuleInterop": true, + "preserveSymlinks": true, + "baseUrl": ".", + "outDir": "./dist" + }, + "include": ["src"], +}