Skip to content

Commit

Permalink
refactor: jsdoc typing, eslint, function components (#822)
Browse files Browse the repository at this point in the history
* typecheck works

* typing

* more typing

* eslint

* convert date picker android

* github typecheck + lint

* prettier

* prettier rule

* useModal

* typing

* typing

* typing

* fix linting

* node 18

* push

* cleanup

* cleanup

* cleanp

* rm push

* prop fixes

* platform select fix

* cleanup

* DatePickerIOS

* tighten typings
  • Loading branch information
henninghall authored May 24, 2024
1 parent 93e3c66 commit d541a72
Show file tree
Hide file tree
Showing 21 changed files with 5,024 additions and 248 deletions.
9 changes: 9 additions & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/** @type {import('eslint').Linter.Config} */
module.exports = {
root: true,
extends: '@react-native',
rules: {
semi: [2, 'never'],
curly: [2, 'multi-line'],
},
}
23 changes: 23 additions & 0 deletions .github/workflows/lint.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
name: Lint

on:
workflow_call:
workflow_dispatch:

jobs:
lint-js:
name: Lint
runs-on: ubuntu-latest

steps:
- name: Checkout
uses: actions/checkout@v4

- name: Node
uses: actions/setup-node@v4

- name: Install npm dependencies
run: yarn install --frozen-lockfile

- name: Lint
run: yarn lint
8 changes: 8 additions & 0 deletions .github/workflows/pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,11 @@ jobs:
test-android-unit:
name: Test
uses: ./.github/workflows/test-android-unit.yml

lint:
name: Check
uses: ./.github/workflows/lint.yml

typecheck:
name: Check
uses: ./.github/workflows/typecheck.yml
23 changes: 23 additions & 0 deletions .github/workflows/typecheck.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
name: Types

on:
workflow_call:
workflow_dispatch:

jobs:
lint-js:
name: Types
runs-on: ubuntu-latest

steps:
- name: Checkout
uses: actions/checkout@v4

- name: Node
uses: actions/setup-node@v4

- name: Install npm dependencies
run: yarn install --frozen-lockfile

- name: Lint
run: yarn typecheck
5 changes: 3 additions & 2 deletions prettier.config.js → .prettierrc.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
module.exports = {
trailingComma: 'es5',
semi: false,
bracketSameLine: true,
bracketSpacing: true,
singleQuote: true,
semi: false,
}
11 changes: 1 addition & 10 deletions babel.config.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,3 @@
module.exports = {
presets: [
[
'@babel/preset-env',
{
targets: {
node: 'current',
},
},
],
],
presets: ['module:@react-native/babel-preset'],
}
13 changes: 11 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@
"number-picker:update-patch": "bash ./scripts/update-patch.sh",
"emulator": "bash ./scripts/start-android-emulator.sh",
"maestro:ios": "maestro test --include-tags=ios .maestro/",
"maestro:android": "maestro test --include-tags=android .maestro/"
"maestro:android": "maestro test --include-tags=android .maestro/",
"typecheck": "yarn tsc",
"lint": "yarn eslint ./src"
},
"repository": {
"type": "git",
Expand Down Expand Up @@ -47,9 +49,16 @@
"devDependencies": {
"@babel/core": "^7.12.10",
"@babel/preset-env": "^7.12.11",
"@react-native/eslint-config": "0.73.2",
"@types/jest": "^29.5.12",
"@types/react": "18.3.3",
"babel-jest": "^26.6.3",
"eslint": "^8.19.0",
"eslint-plugin-ft-flow": "^3.0.9",
"jest": "^26.6.3",
"prettier": "^2.2.1"
"prettier": "2.8.8",
"react-native": "0.74.1",
"typescript": "5.4.5"
},
"codegenConfig": {
"name": "RNDatePickerSpecs",
Expand Down
195 changes: 85 additions & 110 deletions src/DatePickerAndroid.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React from 'react'
import React, { useCallback, useEffect, useRef } from 'react'
import { NativeEventEmitter } from 'react-native'
import { shouldCloseModal, shouldOpenModal } from './modal'
import { useModal } from './modal'
import { getNativeComponent, getNativeModule } from './modules'

const NativeComponent = getNativeComponent()
Expand All @@ -10,123 +10,98 @@ const height = 180
const timeModeWidth = 240
const defaultWidth = 310

class DatePickerAndroid extends React.PureComponent {
render() {
const props = this.getProps()

if (shouldOpenModal(props, this.previousProps)) {
this.isClosing = false
NativeModule.openPicker(props)
}
if (shouldCloseModal(props, this.previousProps, this.isClosing)) {
this.closing = true
NativeModule.closePicker()
}

this.previousProps = props

if (props.modal) return null

return (
<NativeComponent
{...props}
onChange={this._onChange}
onStateChange={this._onSpinnerStateChanged}
/>
)
}

getId = () => {
if (!this.id) {
this.id = Math.random().toString()
}
return this.id
}

componentDidMount = () => {
this.eventEmitter = new NativeEventEmitter(NativeModule)
this.eventEmitter.addListener('dateChange', this._onChange)
this.eventEmitter.addListener(
'spinnerStateChange',
this._onSpinnerStateChanged
)
this.eventEmitter.addListener('onConfirm', this._onConfirm)
this.eventEmitter.addListener('onCancel', this._onCancel)
}

componentWillUnmount = () => {
this.eventEmitter.removeAllListeners('spinnerStateChange')
this.eventEmitter.removeAllListeners('dateChange')
this.eventEmitter.removeAllListeners('onConfirm')
this.eventEmitter.removeAllListeners('onCancel')
}

getProps = () => ({
...this.props,
date: this._date(),
id: this.getId(),
minimumDate: this._minimumDate(),
maximumDate: this._maximumDate(),
timezoneOffsetInMinutes: this._getTimezoneOffsetInMinutes(),
style: this._getStyle(),
})

_getTimezoneOffsetInMinutes = () => {
if (this.props.timeZoneOffsetInMinutes == undefined) return undefined
return this.props.timeZoneOffsetInMinutes
}

_getStyle = () => {
const width = this.props.mode === 'time' ? timeModeWidth : defaultWidth
return [{ width, height }, this.props.style]
}

_onChange = (e) => {
const { date, id, dateString } = e.nativeEvent ?? e
const newArch = id !== null
if (newArch && id !== this.id) return
const jsDate = this._fromIsoWithTimeZoneOffset(date)
this.props.onDateChange && this.props.onDateChange(jsDate)
if (this.props.onDateStringChange) {
this.props.onDateStringChange(dateString)
/** @type {React.FC<PlatformPickerProps>} */
export const DatePickerAndroid = React.memo((props) => {
const thisId = useRef(Math.random().toString()).current

const onChange = useCallback(
/**
* @typedef {{date: string, id: string, dateString: string}} Data
* @param {{ nativeEvent: Data } | Data & { nativeEvent: undefined }} e
*/
(e) => {
const { date, id, dateString } = e.nativeEvent ?? e
const newArch = id !== null
if (newArch && id !== thisId) return
const jsDate = fromIsoWithTimeZoneOffset(date)
if (props.onDateChange) props.onDateChange(jsDate)
if (props.onDateStringChange) props.onDateStringChange(dateString)
},
[props, thisId]
)

const onSpinnerStateChanged = useCallback(
/**
* @typedef {{ spinnerState: "spinning" | "idle", id: string }} SpinnerStateData
* @param {{ nativeEvent: SpinnerStateData } | SpinnerStateData & { nativeEvent: undefined }} e
*/
(e) => {
const { spinnerState, id } = e.nativeEvent ?? e
const newArch = id !== null
if (newArch && id !== thisId) return
props.onStateChange && props.onStateChange(spinnerState)
},
[props, thisId]
)

useEffect(() => {
const eventEmitter = new NativeEventEmitter(NativeModule)
eventEmitter.addListener('dateChange', onChange)
eventEmitter.addListener('spinnerStateChange', onSpinnerStateChanged)
return () => {
eventEmitter.removeAllListeners('dateChange')
eventEmitter.removeAllListeners('spinnerStateChange')
}
}
_onSpinnerStateChanged = (e) => {
const { spinnerState, id } = e.nativeEvent ?? e
const newArch = id !== null
if (newArch && id !== this.id) return
this.props.onStateChange && this.props.onStateChange(spinnerState)
}, [onChange, onSpinnerStateChanged])

/** @type {NativeProps} */
const modifiedProps = {
...props,
date: toIsoWithTimeZoneOffset(props.date),
id: thisId,
minimumDate: toIsoWithTimeZoneOffset(props.minimumDate),
maximumDate: toIsoWithTimeZoneOffset(props.maximumDate),
timezoneOffsetInMinutes: getTimezoneOffsetInMinutes(props),
style: getStyle(props),
onChange,
onStateChange: onSpinnerStateChanged,
}

_maximumDate = () =>
this.props.maximumDate &&
this._toIsoWithTimeZoneOffset(this.props.maximumDate)
useModal({ props: modifiedProps, id: thisId })

_minimumDate = () =>
this.props.minimumDate &&
this._toIsoWithTimeZoneOffset(this.props.minimumDate)
if (props.modal) return null

_date = () => this._toIsoWithTimeZoneOffset(this.props.date)
return <NativeComponent {...modifiedProps} />
})

_fromIsoWithTimeZoneOffset = (timestamp) => {
return new Date(timestamp)
}
/** @param {PlatformPickerProps} props */
const getStyle = (props) => {
const width = props.mode === 'time' ? timeModeWidth : defaultWidth
return [{ width, height }, props.style]
}

_toIsoWithTimeZoneOffset = (date) => {
return date.toISOString()
}
/** @param {PlatformPickerProps} props */
const getTimezoneOffsetInMinutes = (props) => {
// eslint-disable-next-line eqeqeq
if (props.timeZoneOffsetInMinutes == undefined) return undefined
return props.timeZoneOffsetInMinutes
}

_onConfirm = ({ date, id }) => {
if (id !== this.id) return
this.isClosing = true
this.props.onConfirm(this._fromIsoWithTimeZoneOffset(date))
}
/**
* @template {Date | undefined} T
* @param {T} date
* @returns {T extends Date ? string : undefined}
* */
const toIsoWithTimeZoneOffset = (date) => {
/** @ts-ignore */
if (!date) return undefined
/** @ts-ignore */
return date.toISOString()
}

_onCancel = ({ id }) => {
if (id !== this.id) return
this.isClosing = true
this.props.onCancel()
}
/** @param {string} timestamp */
const fromIsoWithTimeZoneOffset = (timestamp) => {
return new Date(timestamp)
}

export default DatePickerAndroid
Loading

0 comments on commit d541a72

Please sign in to comment.