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

chore: update types #6

Open
wants to merge 16 commits into
base: main
Choose a base branch
from
1 change: 1 addition & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ module.exports = {
'eslint:recommended',
'plugin:prettier/recommended',
'plugin:react/recommended',
'plugin:react-hooks/recommended',
],
plugins: ['prettier', 'react-native'],
env: {
Expand Down
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@ Props extends ViewProps
| headerProps | ViewProps | Props for the header |
| contentProps | ScrollViewProps | Props for the scroll section |
| footerProps | ViewProps | Props for the footer |
| onRead | () => void | Callback on agreement read |
| onReadChange | (read: boolean) => void | Callback on read value change |
| isRead | boolean | Set and reset read value from outside the component |

## Example

Expand Down
105 changes: 60 additions & 45 deletions __tests__/index.test.tsx
Original file line number Diff line number Diff line change
@@ -1,54 +1,69 @@
import React from 'react'
import { Text, Button } from 'react-native'
import { render, fireEvent } from '@testing-library/react-native'

import Agreement from '../src'

const content = `
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed porttitor lacus quis sollicitudin malesuada. Etiam metus ipsum, facilisis et vulputate sit amet, pellentesque a arcu. Praesent sit amet viverra eros, vitae faucibus sapien. In eu nulla diam. Sed consequat mauris ut ultrices bibendum. Vivamus nec velit sem. Ut vestibulum velit eget justo feugiat luctus. Praesent tempor vel justo id euismod. Quisque at scelerisque lorem.

Praesent orci mi, aliquam a hendrerit sit amet, hendrerit vel erat. Duis ligula quam, fermentum et iaculis ut, vestibulum sed diam. Etiam rutrum magna at leo viverra venenatis. Fusce eget aliquet quam. In vestibulum velit neque, nec lacinia massa maximus a. Vivamus quam massa, efficitur vel dictum ac, convallis vel turpis. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Nam blandit consequat lorem eu imperdiet. Morbi nisi magna, accumsan eget consequat ut, egestas ut orci. Vestibulum dignissim accumsan imperdiet. Nulla sed egestas nisi. Maecenas justo nunc, scelerisque in mi ac, convallis aliquet tellus. Sed id risus nibh.

Nullam ipsum tortor, auctor vel consectetur nec, convallis eget diam. Nam quis bibendum felis. Nunc ut nibh a neque egestas condimentum sed id urna. Vestibulum luctus consequat metus, in dapibus nibh aliquet quis. Donec quis eros et diam hendrerit tempor id vitae elit. Nam mollis finibus ligula in euismod. Aenean sed bibendum nulla, rhoncus tincidunt ex.

Duis vulputate enim ut risus rhoncus semper. Morbi mattis enim augue, facilisis auctor tortor malesuada sit amet. Nullam congue vitae sem ut convallis. Pellentesque efficitur turpis eget maximus tempus. Pellentesque posuere placerat rhoncus. Fusce facilisis ligula non ante finibus posuere. Duis sit amet dapibus mi. Cras sit amet velit urna. Praesent congue, est sit amet dictum viverra, ipsum nisi condimentum velit, a gravida est ante posuere felis. Suspendisse consectetur ut tellus ut ultrices. Sed eget varius felis. Sed ligula dui, dictum et facilisis dictum, dictum a nibh.

Mauris sit amet nibh at lacus bibendum euismod. Aenean eget vulputate dui. Cras pulvinar arcu quis eros ornare, in fringilla mauris ultrices. Vivamus id pulvinar risus. Donec lacus purus, tempus id pulvinar sit amet, porttitor vel ipsum. In ut dictum risus. Duis at odio pellentesque, cursus justo sed, ornare justo. In varius quam in enim semper sodales.
`

describe('Agreement', () => {
const eventData = {
nativeEvent: {
contentOffset: {
y: 200,
},
},
}

it('should enable Agree button after scroll content to bottom', () => {
const onPress = jest.fn()

const { getByText, getByTestId, queryByText } = render(
<Agreement
// contentProps={{
// style: {
// height: 500,
// },
// }}
renderContent={() => <Text>{content}</Text>}
renderFooter={(read) => (
<Button title="Agree" disabled={!read} onPress={onPress} />
)}
import { Text } from 'react-native'
import { render } from '@testing-library/react-native'

import NativeAgreement from '../src'

describe('NativeAgreement', () => {
// scroll to bottom
it('should', () => {
const { getByText } = render(
<NativeAgreement
renderHeader={() => <Text>Header Text</Text>}
renderContent={() => <Text>Content Text</Text>}
renderFooter={() => <Text>Footer Text</Text>}
/>
)

expect(getByText('Header Text')).toBeDefined()
expect(getByText('Content Text')).toBeDefined()
expect(getByText('Footer Text')).toBeDefined()
})

// scroll to bottom
it('should', () => {
const { getByText } = render(
<NativeAgreement
headerComponent={<Text>Header Text</Text>}
contentComponent={<Text>Content Text</Text>}
renderFooter={() => null}
/>
)

fireEvent.press(getByText('Agree'))
expect(getByText('Header Text')).toBeDefined()
expect(getByText('Content Text')).toBeDefined()
})

// fireEvent.scroll(queryByText('Lorem ipsum dolor'), eventData)
fireEvent.scroll(getByTestId('scroll-view'), eventData)
// scroll to bottom
it('should', () => {
const { getByText, findByText } = render(
<NativeAgreement
headerComponent={<Text>Header Text</Text>}
renderHeader={() => <Text>Render Header Text</Text>}
contentComponent={<Text>Content Text</Text>}
renderContent={() => <Text>Render Content Text</Text>}
renderFooter={() => null}
/>
)

fireEvent.press(getByText('Agree'))
expect(getByText('Render Header Text')).toBeDefined()
// expect(findByText('Header Text')).not.toBeDefined()
expect(getByText('Render Content Text')).toBeDefined()
// expect(findByText('Content Text')).not.toBeDefined()
})

expect(onPress).toHaveBeenCalledTimes(1)
// whole text visible
it('should', () => {
const { getByText } = render(
<NativeAgreement
headerComponent={<Text>Header Text</Text>}
renderHeader={() => <Text>Render Header Text</Text>}
contentComponent={<Text>Content Text</Text>}
renderContent={() => <Text>Render Content Text</Text>}
renderFooter={() => null}
/>
)
})

// test read change
})
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "native-agreement",
"version": "0.0.0",
"version": "1.0.0-beta.1",
"description": "Make sure the user has read the agreement",
"author": "Jakub Biesiada",
"license": "MIT",
Expand Down Expand Up @@ -47,6 +47,7 @@
"eslint-config-prettier": "^7.1.0",
"eslint-plugin-prettier": "^3.3.1",
"eslint-plugin-react": "^7.22.0",
"eslint-plugin-react-hooks": "^4.2.0",
"eslint-plugin-react-native": "^3.10.0",
"husky": "^4.3.7",
"jest": "^26.6.3",
Expand Down
84 changes: 71 additions & 13 deletions src/index.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,25 @@
import React, { useState } from 'react'
import React, { useCallback, useEffect, useState } from 'react'
import {
ViewProps,
ScrollViewProps,
View,
ScrollView,
NativeScrollEvent,
NativeSyntheticEvent,
LayoutChangeEvent,
} from 'react-native'

import type { HeaderType } from './types/HeaderType'
import type { ContentType } from './types/ContentType'

interface Props extends ViewProps {
renderFooter: (read: boolean) => React.ReactNode
renderFooter?: (read: boolean) => React.ReactNode
readonly headerProps?: ViewProps
readonly contentProps?: ScrollViewProps
readonly footerProps?: ViewProps
readonly isRead?: boolean
onReadChange?: (read: boolean) => void
onRead?: () => void
}

const isBottomReached = ({
Expand All @@ -26,6 +30,10 @@ const isBottomReached = ({
return layoutMeasurement.height + contentOffset.y >= contentSize.height
}

type HandleScrollCallback = (e: NativeSyntheticEvent<NativeScrollEvent>) => void
type HandleLayoutCallback = (e: LayoutChangeEvent) => void
type HandleContentSizeChangeCallback = (w: number, h: number) => void

const Agreement = ({
renderHeader,
renderContent,
Expand All @@ -35,29 +43,79 @@ const Agreement = ({
headerProps = {},
contentProps = {},
footerProps = {},
isRead = false,
onReadChange,
onRead,
...props
}: Props & HeaderType & ContentType) => {
const [read, setRead] = useState(false)
const [read, setRead] = useState(isRead)

useEffect(() => {
setRead(isRead)
}, [isRead])

useEffect(() => {
onReadChange?.(read)
}, [onReadChange, read])

const [wrapperHeight, setWrapperHeight] = useState(0)

const { onScroll, ...contentRest } = contentProps
const {
onScroll,
onLayout,
onContentSizeChange,
...contentRest
} = contentProps

const handleScroll = (e: NativeSyntheticEvent<NativeScrollEvent>) => {
if (isBottomReached(e.nativeEvent)) {
setRead(true)
}
const handleScroll = useCallback<HandleScrollCallback>(
(e) => {
if (!read && isBottomReached(e.nativeEvent)) {
setRead(true)

onRead?.()
}

onScroll?.(e)
},
[onRead, onScroll, read]
)

onScroll?.(e)
}
const handleLayout = useCallback<HandleLayoutCallback>(
(e) => {
const { height } = e.nativeEvent.layout

setWrapperHeight(height)

onLayout?.(e)
},
[onLayout]
)

const handleContentSizeChange = useCallback<HandleContentSizeChangeCallback>(
(w, h) => {
setRead(h < wrapperHeight)

onContentSizeChange?.(w, h)
},
[onContentSizeChange, wrapperHeight]
)

return (
<View {...props}>
{renderHeader && <View {...headerProps}>{renderHeader(read)}</View>}
{(renderHeader || headerComponent) && (
<View {...headerProps}>{renderHeader?.(read) || headerComponent}</View>
)}

<ScrollView onScroll={handleScroll} {...contentRest}>
<ScrollView
onLayout={handleLayout}
onScroll={handleScroll}
onContentSizeChange={handleContentSizeChange}
{...contentRest}
>
{renderContent?.(read) || contentComponent}
</ScrollView>

<View {...footerProps}>{renderFooter(read)}</View>
{renderFooter && <View {...footerProps}>{renderFooter(read)}</View>}
</View>
)
}
Expand Down
4 changes: 2 additions & 2 deletions src/types/ContentType.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@ import React from 'react'

type RenderContentType = {
renderContent: (read: boolean) => React.ReactNode
readonly contentComponent: undefined
readonly contentComponent?: React.ReactNode
}

type ContentComponentType = {
renderContent: undefined
renderContent?: (read: boolean) => React.ReactNode
readonly contentComponent: React.ReactNode
}

Expand Down
4 changes: 2 additions & 2 deletions src/types/HeaderType.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@ import React from 'react'

type RenderHeaderType = {
renderHeader: (read: boolean) => React.ReactNode
readonly headerComponent: undefined
readonly headerComponent?: React.ReactNode
}

type HeaderComponentType = {
renderHeader: undefined
renderHeader?: (read: boolean) => React.ReactNode
readonly headerComponent: React.ReactNode
}

Expand Down
Loading