-
Notifications
You must be signed in to change notification settings - Fork 331
/
Copy pathrating.tsx
144 lines (134 loc) · 4.37 KB
/
rating.tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
import React, { useEffect, useMemo, useState } from 'react'
import { GeistUIThemesPalette } from '../themes'
import { NormalTypes, tupleNumber } from '../utils/prop-types'
import RatingIcon from './rating-icon'
import useTheme from '../use-theme'
import useScale, { withScale } from '../use-scale'
import useClasses from '../use-classes'
export type RatingTypes = NormalTypes
const ratingCountTuple = tupleNumber(2, 3, 4, 5, 6, 7, 8, 9, 10)
const ratingValueTuple = tupleNumber(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
export type RatingValue = typeof ratingValueTuple[number]
export type RatingCount = typeof ratingCountTuple[number]
interface Props {
type?: RatingTypes
className?: string
icon?: JSX.Element
count?: RatingCount | number
value?: RatingValue | number
initialValue?: RatingValue
onValueChange?: (value: number) => void
locked?: boolean
onLockedChange?: (locked: boolean) => void
}
const defaultProps = {
type: 'default' as RatingTypes,
className: '',
icon: (<RatingIcon />) as JSX.Element,
count: 5 as RatingCount,
initialValue: 1 as RatingValue,
locked: false,
}
type NativeAttrs = Omit<React.HTMLAttributes<any>, keyof Props>
export type RatingProps = Props & NativeAttrs
const getColor = (type: RatingTypes, palette: GeistUIThemesPalette): string => {
const colors: { [key in RatingTypes]?: string } = {
default: palette.foreground,
success: palette.success,
warning: palette.warning,
error: palette.error,
}
return colors[type] || (colors.default as string)
}
const RatingComponent: React.FC<RatingProps> = ({
type,
className,
icon,
count,
value: customValue,
initialValue,
onValueChange,
locked,
onLockedChange,
...props
}: React.PropsWithChildren<RatingProps> & typeof defaultProps) => {
const theme = useTheme()
const { SCALES } = useScale()
const color = useMemo(() => getColor(type, theme.palette), [type, theme.palette])
const [value, setValue] = useState<number>(initialValue)
const [isLocked, setIsLocked] = useState<boolean>(locked)
const lockedChangeHandler = (next: boolean) => {
setIsLocked(next)
onLockedChange && onLockedChange(next)
}
const valueChangeHandler = (next: number) => {
setValue(next)
const emitValue = next > count ? count : next
onValueChange && onValueChange(emitValue)
}
const clickHandler = (index: number) => {
if (isLocked) return lockedChangeHandler(false)
valueChangeHandler(index)
lockedChangeHandler(true)
}
const mouseEnterHandler = (index: number) => {
if (isLocked) return
valueChangeHandler(index)
}
useEffect(() => {
if (typeof customValue === 'undefined') return
setValue(customValue < 0 ? 0 : customValue)
}, [customValue])
return (
<div className={useClasses('rating', className)} {...props}>
{[...Array(count)].map((_, index) => (
<div
className={useClasses('icon-box', {
hovered: index + 1 <= value,
})}
key={index}
onMouseEnter={() => mouseEnterHandler(index + 1)}
onClick={() => clickHandler(index + 1)}
>
{icon}
</div>
))}
<style jsx>{`
.rating {
box-sizing: border-box;
display: inline-flex;
align-items: center;
--rating-font-size: ${SCALES.font(1)};
font-size: var(--rating-font-size);
width: ${SCALES.width(1, 'auto')};
height: ${SCALES.height(1, 'auto')};
padding: ${SCALES.pt(0)} ${SCALES.pr(0)} ${SCALES.pb(0)} ${SCALES.pl(0)};
margin: ${SCALES.mt(0)} ${SCALES.mr(0)} ${SCALES.mb(0)} ${SCALES.ml(0)};
}
.icon-box {
box-sizing: border-box;
color: ${color};
width: calc(var(--rating-font-size) * 1.5);
height: calc(var(--rating-font-size) * 1.5);
margin-right: calc(var(--rating-font-size) * 1 / 5);
cursor: ${isLocked ? 'default' : 'pointer'};
}
.icon-box :global(svg) {
width: 100%;
height: 100%;
fill: transparent;
transform: scale(1);
transition: transform, color, fill 30ms linear;
}
.hovered :global(svg) {
fill: ${color};
transform: scale(0.9);
}
`}</style>
</div>
)
}
RatingComponent.defaultProps = defaultProps
RatingComponent.displayName = 'GeistRating'
const Rating = withScale(RatingComponent)
export default Rating