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

feat: rating button #2820

Merged
merged 9 commits into from
Jun 15, 2023
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 1 addition & 2 deletions apps/docs/components/components/rating.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ hideBreadcrumbs: true

### Value handling

The `value` prop allows you to set the current rating and max number of stars via `max` prop.
The `value` prop allows you to set the current rating and max number of stars via `max` prop.
dkacper marked this conversation as resolved.
Show resolved Hide resolved

If you need to support partial stars, you can show half-filled star icons by passing the `half-increment` prop.

Expand Down Expand Up @@ -47,7 +47,6 @@ If you need to support partial stars, you can show half-filled star icons by pas

Rating by default uses a `warning-500` for filled and partially filled stars and `disabled-500` as an empty icon color. You can change these values in your [Tailwind configuration](https://tailwindcss.com/docs/configuration#theme) or override them for a single element using [`important modifier`](https://tailwindcss.com/docs/configuration#important-modifier).


<Showcase showcase-name="Rating/RatingColors">

<!-- vue -->
Expand Down
165 changes: 165 additions & 0 deletions apps/docs/components/components/ratingbutton.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
---
layout: AtomLayout
hideBreadcrumbs: true
---

# RatingButton

The RatingButton component is an interactive input element specifically designed for evaluations on product pages. It provides users with the ability to select a rating by choosing an icon using either the cursor or keyboard input. The number of icons displayed and the actual icon used can be customized based on your specific requirements. The rating scale within the component operates from left to right.

::: slot usage

## Examples

### Basic usage

To make the RatingButton work you just have to provide a state.

<Showcase showcase-name="RatingButton/Basic" style="min-height:250px">
<!-- vue -->
<<<../../preview/nuxt/pages/showcases/RatingButton/Basic.vue
<!-- end vue -->
<!-- react -->
<<<../../preview/next/pages/showcases/RatingButton/Basic.tsx#source
<!-- end react -->
</Showcase>

### Sizes

RatingButton comes with three different sizes which are compatible with StorefrontUI icons. Size of the RatingButton maps to the icon size as follows:

- `SfRatingButtonSize.sm` -> `SfIconSize.base`
- `SfRatingButtonSize.base` -> `SfIconSize.lg`
- `SfRatingButtonSize.lg` -> `SfIconSize.xl`

<Showcase showcase-name="RatingButton/Sizes" style="min-height:250px">
<!-- vue -->
<<<../../preview/nuxt/pages/showcases/RatingButton/Sizes.vue
<!-- end vue -->
<!-- react -->
<<<../../preview/next/pages/showcases/RatingButton/Sizes.tsx#source
<!-- end react -->
</Showcase>

### Adjust maximum rating score value

Set any number of stars using `max` prop. For example, if you increase the maximum rating score to 10, the component will dynamically generate ten buttons, giving users a wider range of options to rate their experience.

<Showcase showcase-name="RatingButton/MaxNumber" style="min-height:250px">
<!-- vue -->
<<<../../preview/nuxt/pages/showcases/RatingButton/MaxNumber.vue
<!-- end vue -->
<!-- react -->
<<<../../preview/next/pages/showcases/RatingButton/MaxNumber.tsx#source
<!-- end react -->
</Showcase>

### Custom icon

You can customize an icon of the RatingButton using
<!-- react -->
render function.
<!-- end react -->
<!-- vue -->
scoped slot.
<!-- end vue -->
It passes some usefull props that you can use to manage your custom icons.

<Showcase showcase-name="RatingButton/CustomIcon" style="min-height:250px">
<!-- vue -->
<<<../../preview/nuxt/pages/showcases/RatingButton/CustomIcon.vue
<!-- end vue -->
<!-- react -->
<<<../../preview/next/pages/showcases/RatingButton/CustomIcon.tsx#source
<!-- end react -->
</Showcase>

## Accessibility notes

RatingButton follows [WAI-ARIA guidelines](https://www.w3.org/WAI/ARIA/apg/patterns/radio/examples/radio-rating/) for rating radio groups.

The outer wrapper element is `<fieldset>`. You can set a corrsponding `<legend>` using `label` prop.

The RatingButton uses hidden radio input elements to satisfy all keyboard functional requirements. Each input element has corresponding label that reflects selected value. By default it says "stars", so make sure you change it when using custom icons. You can change each icon label using `getLabelText` prop.

## Playground

<Generate style="height:400px" />

:::

::: slot api

## Props

| Prop name | Type | Default value | Possible values |
| ---------------- | ------------------------------------------------- | ------------------ | ------------------ |
<!-- react -->
| `value` | `number` | `0` | |
| `onChange` | `(value: number) => void` | | |
<!-- end react -->
| `name` | `string` | `sf-rating-button` | |
| `max` | `number` | `5` | |
| `size` | `SfRatingButtonSize` | `base` | `sm`, `base`, `lg` |
| `disabled` | `boolean` | `false` | |
| `label` | `string` | `Rating` | |
| `labelClassName` | `string` | `Rating` | |
| `getLabelText` | `(value: number) => string` | | |
<!-- react -->
| `children` | `(state: SfRatingButtonRenderProps) => ReactNode` | | |
<!-- end react -->

<!-- react -->

## Render function props

SfRatingButtonRenderProps

| Prop name | Type | Default value | Possible values |
| ---------- | -------------------- | ------------- | ------------------ |
| `isFilled` | `boolean` | | |
| `max` | `number` | | |
| `iconSize` | `SfIconSize` | | `base`, `lg`, `xl` |

<!-- end react -->

<!-- vue -->

## Slots

| Slot name | Description |
| --------- | ------------------------------------------------------------------------ |
| `default` | Scoped slot of a single icon to be rendered when iteraing all the icons. |

### Slot props

| Prop name | Type | Default value | Possible values |
| ---------- | -------------------- | ------------- | ------------------ |
| `isFilled` | `boolean` | | |
| `max` | `number` | | |
| `iconSize` | `SfIconSize` | | `base`, `lg`, `xl` |

## Events

| Event name | Trigger |
| ------------------- | ----------------------------- |
| `update:modelValue` | triggers v-model update event |

<!-- end vue -->

:::

## Source code

::: slot source

<SourceCode>
<!-- vue -->
<<<../../../packages/sfui/frameworks/vue/components/SfRatingButton/SfRatingButton.vue
<!-- end vue -->
<!-- react -->
<<<../../../packages/sfui/frameworks/react/components/SfRatingButton/SfRatingButton.tsx
<!-- end react -->
</SourceCode>

:::
75 changes: 75 additions & 0 deletions apps/preview/next/pages/examples/SfRatingButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { useEffect, useState } from 'react';
import { SfRatingButton, SfRatingButtonSize } from '@storefront-ui/react';
import { ExamplePageLayout } from '../examples';
import { prepareControls } from '../../components/utils/Controls';
import ComponentExample from '../../components/utils/ComponentExample';

function Example() {
const [max, setMax] = useState(5);
const { state, controls } = prepareControls(
[
{
type: 'range',
modelName: 'value',
propDefaultValue: 0,
propType: 'number',
options: [
{
bind: {
min: 0,
max,
step: 1,
},
},
],
},
{
type: 'range',
modelName: 'max',
propDefaultValue: 5,
propType: 'number',
options: [
{
bind: {
min: 1,
step: 1,
max: 10,
},
},
],
},
{
type: 'select',
modelName: 'size',
propType: 'SfRatingButtonSize',
propDefaultValue: SfRatingButtonSize.base,
options: Object.values(SfRatingButtonSize),
},
{
type: 'boolean',
modelName: 'disabled',
propType: 'boolean',
propDefaultValue: false,
},
],
{
value: 0,
max,
disabled: false,
size: SfRatingButtonSize.base,
},
);

useEffect(() => {
setMax(state.get.max || 5);
}, [state.get.max]);

return (
<ComponentExample controls={{ state, controls }}>
<SfRatingButton {...state.get} onChange={(value) => state.set({ value })} />
</ComponentExample>
);
}

Example.getLayout = ExamplePageLayout;
export default Example;
13 changes: 13 additions & 0 deletions apps/preview/next/pages/showcases/RatingButton/Basic.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { ShowcasePageLayout } from '../../showcases';
// #region source
import { useState } from 'react';
import { SfRatingButton } from '@storefront-ui/react';

export default function RatingButtonBasic() {
const [rating, setRating] = useState(0);

return <SfRatingButton value={rating} onChange={setRating} />;
}

// #endregion source
RatingButtonBasic.getLayout = ShowcasePageLayout;
31 changes: 31 additions & 0 deletions apps/preview/next/pages/showcases/RatingButton/CustomIcon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { ShowcasePageLayout } from '../../showcases';
// #region source
import { useState } from 'react';
import { SfIconFavorite, SfIconFavoriteFilled, SfRatingButton } from '@storefront-ui/react';

export default function RatingButtonCustomIcon() {
const [rating, setRating] = useState(0);

return (
<SfRatingButton value={rating} onChange={setRating}>
{({ isFilled, iconSize }) =>
isFilled ? (
<SfIconFavoriteFilled
role="none"
size={iconSize}
className="text-red-700 cursor-pointer peer-disabled:cursor-default peer-disabled:text-neutral-500 peer-focus-visible:outline"
/>
) : (
<SfIconFavorite
role="none"
size={iconSize}
className="text-neutral-500 cursor-pointer peer-disabled:cursor-default peer-disabled:text-neutral-400 peer-focus-visible:outline"
/>
)
}
</SfRatingButton>
);
}

// #endregion source
RatingButtonCustomIcon.getLayout = ShowcasePageLayout;
13 changes: 13 additions & 0 deletions apps/preview/next/pages/showcases/RatingButton/MaxNumber.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { ShowcasePageLayout } from '../../showcases';
// #region source
import { useState } from 'react';
import { SfRatingButton } from '@storefront-ui/react';

export default function RatingButtonMax() {
const [rating, setRating] = useState(5);

return <SfRatingButton max={10} value={rating} onChange={setRating} />;
}

// #endregion source
RatingButtonMax.getLayout = ShowcasePageLayout;
21 changes: 21 additions & 0 deletions apps/preview/next/pages/showcases/RatingButton/Sizes.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { ShowcasePageLayout } from '../../showcases';
// #region source
import { useState } from 'react';
import { SfRatingButton } from '@storefront-ui/react';

export default function RatingButtonSizes() {
const [rating1, setRating1] = useState(0);
const [rating2, setRating2] = useState(0);
const [rating3, setRating3] = useState(0);

return (
<div>
<SfRatingButton value={rating1} onChange={setRating1} size="sm" />
<SfRatingButton value={rating2} onChange={setRating2} size="base" />
<SfRatingButton value={rating3} onChange={setRating3} size="lg" />
</div>
);
}

// #endregion source
RatingButtonSizes.getLayout = ShowcasePageLayout;
Loading
Loading