Skip to content

Commit 023f169

Browse files
brojorjd-solanki
andauthored
feat(rating): Add Rating component (#60)
Co-authored-by: JD Solanki <jdsolanki0001@gmail.com>
1 parent 2eb7394 commit 023f169

File tree

16 files changed

+999
-835
lines changed

16 files changed

+999
-835
lines changed

packages/anu-vue/src/components/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ export { AList } from './list'
1515
export { AListItem } from './list-item'
1616
export { AMenu } from './menu'
1717
export { ARadio } from './radio'
18+
export { ARating } from './rating'
1819
export { ASelect } from './select'
1920
export { ASwitch } from './switch'
2021
export { ATable } from './table'
Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
import { computed, defineComponent, ref, toRef } from 'vue'
2+
import { useLayer, useProps as useLayerProps } from '@/composables/useLayer'
3+
import { disabled, readonly } from '@/composables/useProps'
4+
5+
export const ARating = defineComponent({
6+
name: 'ARating',
7+
props: {
8+
// color: { default: 'primary' },
9+
...useLayerProps({
10+
color: {
11+
default: 'warning',
12+
},
13+
}),
14+
15+
/**
16+
* Bind v-model value to rating
17+
*/
18+
modelValue: {
19+
type: Number,
20+
default: undefined,
21+
},
22+
23+
/**
24+
* Sets amount of rating items
25+
*/
26+
length: {
27+
type: [Number, String],
28+
default: 5,
29+
},
30+
31+
/**
32+
* Allows the award of half a point
33+
*/
34+
halve: {
35+
type: Boolean,
36+
default: false,
37+
},
38+
39+
/**
40+
* Sets empty icon
41+
*/
42+
emptyIcon: {
43+
type: String,
44+
default: 'i-bx:star',
45+
},
46+
47+
/**
48+
* Sets half-filled icon
49+
*/
50+
halfIcon: {
51+
type: String,
52+
default: 'i-bx:bxs-star-half',
53+
},
54+
55+
/**
56+
* Sets filled icon
57+
*/
58+
fullIcon: {
59+
type: String,
60+
default: 'i-bx:bxs-star',
61+
},
62+
63+
/**
64+
* Allows to see visual changes of value on hover
65+
*/
66+
noHoverHint: {
67+
type: Boolean,
68+
default: false,
69+
},
70+
71+
/**
72+
* Animate icon on hover
73+
*/
74+
animate: {
75+
type: Boolean,
76+
default: false,
77+
},
78+
79+
/**
80+
* Make rating component readonly
81+
*/
82+
readonly,
83+
84+
/**
85+
* Disable rating selection
86+
*/
87+
disabled,
88+
},
89+
emits: ['update:modelValue'],
90+
setup(props, { emit }) {
91+
const { getLayerClasses } = useLayer()
92+
93+
const { styles, classes } = getLayerClasses(
94+
toRef(props, 'color'),
95+
ref(''),
96+
ref(false),
97+
)
98+
99+
const rating = ref(0)
100+
const isHovered = ref(false)
101+
102+
const visibleRating = computed(() =>
103+
!props.noHoverHint
104+
&& !props.readonly
105+
&& !props.disabled
106+
&& isHovered.value
107+
? rating.value
108+
: props.modelValue ?? 0,
109+
)
110+
111+
const items = computed(() =>
112+
Array.from({ length: Number(props.length) }, (_, i) => i + 1).map(item =>
113+
item <= visibleRating.value
114+
? props.fullIcon
115+
: item - visibleRating.value === 0.5
116+
? props.halfIcon
117+
: props.emptyIcon,
118+
),
119+
)
120+
121+
const handleClick = () => {
122+
emit('update:modelValue', rating.value)
123+
}
124+
125+
const onMouseEnter = (e: MouseEvent, index: number) => {
126+
isHovered.value = true
127+
128+
const { offsetX, target } = e
129+
if (target instanceof HTMLElement) {
130+
const widthPercentage = (offsetX * 100) / target.clientWidth
131+
props.halve
132+
? (rating.value = widthPercentage < 50 ? index + 0.5 : index + 1)
133+
: (rating.value = index + 1)
134+
}
135+
}
136+
137+
const onMouseLeave = () => {
138+
isHovered.value = false
139+
}
140+
141+
return () => (
142+
<div
143+
class={[
144+
'a-rating flex',
145+
(props.animate && !props.readonly && !props.disabled) && 'a-rating-animated',
146+
props.readonly && 'a-rating-readonly pointer-events-none',
147+
props.disabled && 'a-rating-disabled pointer-events-none',
148+
...classes.value,
149+
]}
150+
style={[...styles.value]}
151+
>
152+
153+
{items.value.map((icon, i) => {
154+
return <i class={['cursor-pointer', icon]}
155+
onClick={handleClick}
156+
onMouseenter={(event => onMouseEnter(event, i))}
157+
onMouseleave={onMouseLeave}/>
158+
})}
159+
</div>
160+
)
161+
},
162+
})
163+
164+
export type ARating = InstanceType<typeof ARating>
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export { ARating } from './ARating'
2+

packages/anu-vue/src/presets/theme-default/index.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,11 @@ const themeShortcuts: Exclude<Preset['shortcuts'], undefined> = [
122122
// 👉 Menu
123123
'a-menu': 'z-[51] shadow-xl [--a-transition-slide-up-transform:10px]',
124124

125+
// 👉 Rating
126+
'a-rating': 'uno-layer-base-text-xl',
127+
'a-rating-animated': 'i:(transition-transform ease-in-out duration-250) hover:i:scale-125',
128+
'a-rating-disabled': 'opacity-50',
129+
125130
// 👉 Radio
126131
'a-radio-circle': 'border-solid h-5 w-5 border-(2 a-border) rounded-full mr-2 p-1 after:(duration-250 ease-in-out)', // ℹ️ :after is inner dot
127132
'a-radio-disabled': 'opacity-50',

packages/documentation/docs/.vitepress/config.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ export default defineConfig({
6161
{ text: 'List', link: '/guide/components/list' },
6262
{ text: 'Menu', link: '/guide/components/menu' },
6363
{ text: 'Radio', link: '/guide/components/radio' },
64+
{ text: 'Rating', link: '/guide/components/rating' },
6465
{ text: 'Select', link: '/guide/components/select' },
6566
{ text: 'Switch', link: '/guide/components/switch' },
6667
{ text: 'Table', link: '/guide/components/table' },
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<script setup lang="ts">
2+
import { ref } from 'vue'
3+
4+
const rating = ref(0)
5+
</script>
6+
7+
<template>
8+
<ARating
9+
v-model="rating"
10+
animate
11+
empty-icon="i-bx:heart"
12+
full-icon="i-bx:bxs-heart"
13+
color="danger"
14+
/>
15+
</template>
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<script setup lang="ts">
2+
import { ref } from 'vue'
3+
4+
const rating = ref(0)
5+
</script>
6+
7+
<template>
8+
<ARating v-model="rating" />
9+
</template>
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<script setup lang="ts">
2+
import { ref } from 'vue'
3+
4+
const rating = ref(0)
5+
</script>
6+
7+
<template>
8+
<ARating
9+
v-model="rating"
10+
color="primary"
11+
/>
12+
</template>
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<script setup lang="ts">
2+
import { ref } from 'vue'
3+
4+
const rating = ref(0)
5+
</script>
6+
7+
<template>
8+
<ARating
9+
v-model="rating"
10+
empty-icon="i-bx:heart"
11+
full-icon="i-bx:bxs-heart"
12+
color="danger"
13+
/>
14+
</template>
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<script setup lang="ts">
2+
import { ref } from 'vue'
3+
4+
const rating = ref(0)
5+
</script>
6+
7+
<template>
8+
<ARating
9+
v-model="rating"
10+
halve
11+
class="text-2xl"
12+
/>
13+
</template>

0 commit comments

Comments
 (0)