Skip to content

Commit

Permalink
feat(rating): rating review changes
Browse files Browse the repository at this point in the history
  • Loading branch information
jorekai committed Jun 6, 2021
1 parent dacd538 commit d44edd4
Show file tree
Hide file tree
Showing 3 changed files with 102 additions and 119 deletions.
93 changes: 47 additions & 46 deletions components/rating/__tests__/index.test.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React, { useState } from 'react'
import { mount } from 'enzyme'

import { Rating } from 'components'
import { mount } from 'enzyme'

describe('Rating', () => {
it('should render correctly', () => {
Expand All @@ -25,10 +26,10 @@ describe('Rating', () => {
it('should show different initialization values', () => {
const wrapper = mount(
<div>
<Rating count={10} value={5}/>
<Rating count={2} value={1}/>
<Rating count={10} value={10}/>
<Rating count={2} value={2}/>
<Rating count={10} value={5} />
<Rating count={2} value={1} />
<Rating count={10} value={10} />
<Rating count={2} value={2} />
</div>,
)
expect(wrapper.html()).toMatchSnapshot()
Expand All @@ -37,69 +38,69 @@ describe('Rating', () => {

it('should initialize state and lock value', () => {
const WrapperRating = () => {
const [value, setValue] = useState<number>(0)
const [lock, setLock] = useState<boolean>(false)
const [value, setValue] = useState<number>(0)
const [lock, setLock] = useState<boolean>(false)

return(
<div>
<Rating type="success" lockCallback={setLock} valueCallback={setValue}/>
<div id="valueDiv">{value}</div>
<div id="lockDiv">{lock ? "true": "false"}</div>
</div>
)
return (
<div>
<Rating type="success" onLockedChange={setLock} onValueChange={setValue} />
<div id="valueDiv">{value}</div>
<div id="lockDiv">{lock ? 'true' : 'false'}</div>
</div>
)
}
const wrapper = mount(<WrapperRating />)
expect(wrapper.find('svg').children())
expect(wrapper.find("#valueDiv").text()).toContain("1")
expect(wrapper.find("#lockDiv").text()).toContain("false")
expect(wrapper.find('#valueDiv').text()).toContain('1')
expect(wrapper.find('#lockDiv').text()).toContain('false')
})

it('should update state and lock value on click', () => {
const WrapperRating = () => {
const [value, setValue] = useState<number>(0)
const [lock, setLock] = useState<boolean>(false)
const [value, setValue] = useState<number>(0)
const [lock, setLock] = useState<boolean>(false)

return(
<div>
<Rating type="success" lockCallback={setLock} valueCallback={setValue}/>
<div id="valueDiv">{value}</div>
<div id="lockDiv">{lock ? "true": "false"}</div>
</div>
)
return (
<div>
<Rating type="success" onLockedChange={setLock} onValueChange={setValue} />
<div id="valueDiv">{value}</div>
<div id="lockDiv">{lock ? 'true' : 'false'}</div>
</div>
)
}
const wrapper = mount(<WrapperRating />)
expect(wrapper.find('svg').last().simulate("mousedown"))
expect(wrapper.find('svg').last().simulate("mouseup"))
expect(wrapper.find("#valueDiv").text()).toContain("5")
expect(wrapper.find("#lockDiv").text()).toContain("true")
expect(wrapper.find('svg').last().simulate('mousedown'))
expect(wrapper.find('svg').last().simulate('mouseup'))
expect(wrapper.find('#valueDiv').text()).toContain('5')
expect(wrapper.find('#lockDiv').text()).toContain('true')
// unlock again
expect(wrapper.find('svg').last().simulate("mousedown"))
expect(wrapper.find('svg').last().simulate("mouseup"))
expect(wrapper.find("#valueDiv").text()).toContain("5")
expect(wrapper.find("#lockDiv").text()).toContain("false")
expect(wrapper.find('svg').last().simulate('mousedown'))
expect(wrapper.find('svg').last().simulate('mouseup'))
expect(wrapper.find('#valueDiv').text()).toContain('5')
expect(wrapper.find('#lockDiv').text()).toContain('false')
})

it('should update snapshot on mouse enter', () => {
const WrapperRating = () => {
const [value, setValue] = useState<number>(0)
const [lock, setLock] = useState<boolean>(false)
const [value, setValue] = useState<number>(0)
const [lock, setLock] = useState<boolean>(false)

return(
<div>
<Rating type="success" lockCallback={setLock} valueCallback={setValue}/>
<div id="valueDiv">{value}</div>
<div id="lockDiv">{lock ? "true": "false"}</div>
</div>
)
return (
<div>
<Rating type="success" onLockedChange={setLock} onValueChange={setValue} />
<div id="valueDiv">{value}</div>
<div id="lockDiv">{lock ? 'true' : 'false'}</div>
</div>
)
}
const wrapper = mount(<WrapperRating />)
const lastStar = wrapper.find('svg').last()
const firstStar = wrapper.find('svg').first()
expect(lastStar.simulate("mouseenter"))
expect(lastStar.simulate('mouseenter'))
expect(wrapper.html()).toMatchSnapshot()
expect(lastStar.simulate("mousedown"))
expect(lastStar.simulate("mouseup"))
expect(firstStar.simulate("mouseenter"))
expect(lastStar.simulate('mousedown'))
expect(lastStar.simulate('mouseup'))
expect(firstStar.simulate('mouseenter'))
expect(wrapper.html()).toMatchSnapshot()
})
})
86 changes: 40 additions & 46 deletions components/rating/rating.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { Icon, Props as IconProps } from '@geist-ui/react-icons'
import React, { useEffect, useMemo, useState } from 'react'

import { GeistUIThemes } from '../themes/presets'
Expand All @@ -10,25 +9,23 @@ import withDefaults from '../utils/with-defaults'
interface Props {
type?: NormalTypes
className?: string
icon?: (props: IconProps) => React.ReactElement<Icon>
icon?: JSX.Element
count?: 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10
value?: 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9
valueCallback?: React.Dispatch<React.SetStateAction<number>>
lock?: boolean
lockCallback?: React.Dispatch<React.SetStateAction<boolean>>
onValueChange?: (value: number) => void
locked?: boolean
onLockedChange?: (locked: boolean) => void
onClick?: React.MouseEventHandler<SVGElement>
onMouseEnter?: React.MouseEventHandler<SVGElement>
}

const defaultProps = {
type: 'default' as NormalTypes,
className: '',
icon: (props: any): React.ReactElement<Icon> => <Star {...props} />,
icon: (props: any): JSX.Element => <Star {...props} />,
count: 5,
value: 0,
valueCallback: () => {},
lock: false,
lockCallback: () => {},
locked: false,
onClick: () => {},
onMouseEnter: () => {},
}
Expand All @@ -46,77 +43,74 @@ const getColor = (type: NormalTypes, theme: GeistUIThemes): string => {
return colors[type] || (colors.default as string)
}

const Rating: React.FC = ({
const Rating: React.FC<React.PropsWithChildren<RatingProps>> = ({
type,
children,
className,
icon,
count,
value,
valueCallback,
lock,
lockCallback,
onValueChange,
locked,
onLockedChange,
onClick,
onMouseEnter,
...props
}: RatingProps) => {
}) => {
const theme = useTheme()
const color = useMemo(() => getColor(type, theme), [type, theme])
const [hoverState, setHoverState] = useState<number>(value) // state is from 0 to count - 1
const [isLocked, setIsLocked] = useState<boolean>(lock)
const [hoverState, setHoverState] = useState<number>(value)
const [isLocked, setIsLocked] = useState<boolean>(locked)

useEffect(() => {
valueCallback(hoverState + 1) // state + 1
if (!onValueChange) return
onValueChange(hoverState + 1)
}, [hoverState])

useEffect(() => {
lockCallback(isLocked)
if (!onLockedChange) return
onLockedChange(isLocked)
}, [isLocked])

const handleMouseUp = (event: React.MouseEvent<SVGElement>, index: number) => {
if (isLocked) {
setIsLocked(false) // unlock
setIsLocked(false)
onClick && onClick(event)
return
}
setHoverState(index)
setIsLocked(true) // lock
setIsLocked(true)
onClick && onClick(event)
}

const handleMouseEnter = (event: React.MouseEvent<SVGElement>, index: number) => {
if (isLocked) return // leave on lock
if (isLocked) return
setHoverState(index)
onMouseEnter && onMouseEnter(event)
}

const Icon = icon

const render = () => {
return (
<>
{[...Array(count)].map((_e, i) => (
<Icon
key={i}
color={color}
className={`${className}`}
fill={i <= hoverState ? color : 'transparent'}
stroke={i <= hoverState ? '#fff' : color}
transform={i <= hoverState ? 'scale(1.1)' : 'scale(1)'}
onMouseEnter={(e: React.MouseEvent<SVGElement, MouseEvent>) =>
handleMouseEnter(e, i)
}
onMouseUp={(e: React.MouseEvent<SVGElement, MouseEvent>) =>
handleMouseUp(e, i)
}
{...props}>
{children}
</Icon>
))}
</>
)
}
return render()
return (
<>
{[...Array(count)].map((_e, i) => (
<Icon
key={i}
color={color}
className={`${className}`}
fill={i <= hoverState ? color : 'transparent'}
stroke={i <= hoverState ? '#fff' : color}
transform={i <= hoverState ? 'scale(1.1)' : 'scale(1)'}
onMouseEnter={(e: React.MouseEvent<SVGElement, MouseEvent>) =>
handleMouseEnter(e, i)
}
onMouseUp={(e: React.MouseEvent<SVGElement, MouseEvent>) => handleMouseUp(e, i)}
{...props}>
{children}
</Icon>
))}
</>
)
}

const MemoRating = React.memo(Rating)
Expand Down
42 changes: 15 additions & 27 deletions pages/en-us/components/rating.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ Display an indicator of rankings with stars.

<Playground
title="Default Rating"
desc="A default Rating component with initializers and callbacks."
desc="A default rating component with initializers and callbacks."
scope={{ Rating, Grid, useState }}
code={`
() => {
Expand All @@ -24,10 +24,10 @@ Display an indicator of rankings with stars.
<>
<Grid.Container gap={2} justify="center">
<Grid xs={12} md={6}>
<Rating lockCallback={setLock} valueCallback={setCount}/>
<Rating onLockedChange={setLock} onValueChange={setCount}/>
</Grid>
<Grid xs={12} md={6}>Selection: {count}</Grid>
<Grid xs={12} md={6}>Locked: {lock ? "true" : "false"}</Grid>
<Grid xs={12} md={6}>selection: {count}</Grid>
<Grid xs={12} md={6}>locked: {lock ? "true" : "false"}</Grid>
</Grid.Container>
</>
)
Expand Down Expand Up @@ -115,29 +115,17 @@ Display an indicator of rankings with stars.
<Attributes edit="/pages/en-us/components/Rating.mdx">
<Attributes.Title>Rating.Props</Attributes.Title>

| Attribute | Description | Type | Accepted values | Default |
| --------- | ------------ | ---------------- | ------------------------------------------------------- | --------- |
| **type** | Rating type | `NormalTypes` | `'default', 'secondary', 'success', 'warning', 'error'` | `default` |
| **count** | Rating star count | `number` | `2, 3, 4, 5, 6, 7, 8, 9, 10` | `5` |
| **value** | Initial star values | `number` | `0, 1, 2, 3, 4, 5, 6, 7, 8, 9` | `0` |
| **valueCallback** | Callback values | `vCType` | [vCType](#vCType) | `void` |
| **lock** | Initial lock state | `boolean` | `'false', 'true'` | `0` |
| **lockCallback** | Callback lock | `lCType` | [lCType](#lCType) | `void` |
| **onClick** | Native event handler | `onEventType` | [onEventType](#onEventType) | `void` |
| **onMouseEnter** | Native event handler | `onEventType` | [onEventType](#onEventType) | `void` |
| ... | native props | `HTMLAttributes` | - | - |

<Attributes.Title>vCType</Attributes.Title>

```ts
type vCType = React.Dispatch<React.SetStateAction<number>>
```
<Attributes.Title>lCType</Attributes.Title>
```ts
type lCType = React.Dispatch<React.SetStateAction<boolean>>
```
| Attribute | Description | Type | Accepted values | Default |
| --------- | ------------ | ---------------- | ------------------------------------------------------- | --------- |
| **type** | Rating type | `NormalTypes` | `'default', 'secondary', 'success', 'warning', 'error'` | `default` |
| **count** | Rating star count | `number` | `2, 3, 4, 5, 6, 7, 8, 9, 10` | `5` |
| **value** | Initial star values | `number` | `0, 1, 2, 3, 4, 5, 6, 7, 8, 9` | `0` |
| **onValueChange** | Callback values | `(value?: any) => void` | - | `void` |
| **locked** | Initial lock state | `boolean` | `'false', 'true'` | `0` |
| **onLockedChange**| Callback lock | `(value?: any) => void` | - | `void` |
| **onClick** | Native event handler | `onEventType` | [onEventType](#onEventType) | `void` |
| **onMouseEnter** | Native event handler | `onEventType` | [onEventType](#onEventType) | `void` |
| ... | native props | `HTMLAttributes` | - | - |

<Attributes.Title>onEventType</Attributes.Title>

Expand Down

0 comments on commit d44edd4

Please sign in to comment.