From 3f9efd93b2e6327bae39441f02794c30277bc4d1 Mon Sep 17 00:00:00 2001 From: Maharshi Alpesh Date: Wed, 16 Oct 2024 12:21:59 +0530 Subject: [PATCH] chore(rating): adding suggestions of ryo and wkw --- .changeset/smooth-masks-kiss.md | 8 ++ .../content/components/rating/disabled.ts | 2 +- apps/docs/content/docs/components/rating.mdx | 65 ++++----- .../rating/__tests__/rating.test.tsx | 129 +++++++++++++++--- packages/components/rating/package.json | 1 + .../components/rating/src/rating-segment.tsx | 16 ++- packages/components/rating/src/rating.tsx | 88 ++++++------ packages/components/rating/src/use-rating.ts | 112 +++++++-------- .../rating/stories/rating.stories.tsx | 64 +++++++-- packages/core/theme/src/components/rating.ts | 3 +- .../shared-icons/src/angry-emojicon.tsx | 4 +- .../shared-icons/src/happy-emojicon.tsx | 2 + packages/utilities/shared-icons/src/heart.tsx | 9 +- packages/utilities/shared-icons/src/like.tsx | 9 +- packages/utilities/shared-icons/src/music.tsx | 9 +- .../shared-icons/src/sad-emojicon.tsx | 2 + packages/utilities/shared-icons/src/star.tsx | 4 +- .../shared-icons/src/straight-emojicon.tsx | 2 + pnpm-lock.yaml | 3 + 19 files changed, 352 insertions(+), 180 deletions(-) create mode 100644 .changeset/smooth-masks-kiss.md diff --git a/.changeset/smooth-masks-kiss.md b/.changeset/smooth-masks-kiss.md new file mode 100644 index 0000000000..e13197509d --- /dev/null +++ b/.changeset/smooth-masks-kiss.md @@ -0,0 +1,8 @@ +--- +"@nextui-org/rating": minor +"@nextui-org/react": minor +"@nextui-org/theme": minor +"@nextui-org/shared-icons": minor +--- + +Add rating component(#3807) diff --git a/apps/docs/content/components/rating/disabled.ts b/apps/docs/content/components/rating/disabled.ts index f5567a4595..64a9c718ea 100644 --- a/apps/docs/content/components/rating/disabled.ts +++ b/apps/docs/content/components/rating/disabled.ts @@ -2,7 +2,7 @@ const App = `import {Rating} from "@nextui-org/react"; export default function App() { return (
- +
); }`; diff --git a/apps/docs/content/docs/components/rating.mdx b/apps/docs/content/docs/components/rating.mdx index fa72c0ec0b..7ecbff3751 100644 --- a/apps/docs/content/docs/components/rating.mdx +++ b/apps/docs/content/docs/components/rating.mdx @@ -57,7 +57,7 @@ Passing `isRequired` property will make `rating` required. Size of the `rating` can be changed by `size` property. By default, `size` property is set to `sm`. - + ### Description @@ -69,7 +69,7 @@ Description of the `rating` can be set by `description` property. Error Message of the `rating` can be set by `errorMessage` property. - + ### Controlled @@ -79,20 +79,20 @@ Error Message of the `rating` can be set by `errorMessage` property. The color filled in the rating icons can be changed by `fillColor` property. By default, the `fillColor` is set to `gold`. - + -### Icon +### Custom Icon Custom Icon for the rating component can be used by using `icon` property. - + ### RatingSegment - Rating can be customized with the use of `RatingSegment` as a child of `Rating` component. - RatingSegment has an `icon` prop for using custom icon and `fillColor` prop for the color to be filled in the icon. - + **Note:** As shown in the above example, it is required for `RatingSegment` to have `index` prop. @@ -101,13 +101,15 @@ Custom Icon for the rating component can be used by using `icon` property. ## Slots - **base**: Rating wrapper, it handles alignment, placement, and general appearance. -- **mainWrapper**: Wraps the underlying iconWrapper, helperWrapper and underlying input element. +- **mainWrapper**: Wraps the underlying iconWrapper and underlying input element. - **input**: The underlying input element. - **iconWrapper**: Wraps all the iconSegment elements. - **iconSegment**: The iconSegment element. - **icon**: The icon element. -- **helperWrapper**: Wraps the `description`. +- **radioButtonsWrapper**: Wraps all the hidden radio buttons used in internal of the Rating. +- **radioButtonWrapper**: Each radio button is wrapped by radioButtonWrapper. - **description**: The description of the Rating. +- **errorMessage**: Error message of the Rating to be displayed when Rating is invalid. @@ -140,30 +142,29 @@ Custom Icon for the rating component can be used by using `icon` property. ### Rating Props -| Attribute | Type | Description | Default | -| ------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------| ------------- | -| length | `number` | The length/maxValue of the rating. | `5` | -| precision | `number` in `range(0, 1)` | The precision for the rating value. | `1` | -| children | `ReactNode[]` | The `RatingSegment` which makes up the `Rating`. | `null` | -| icon | `ReactNode` | The `icon` which needs to be desplayed `rating` component. | `StarIcon` | -| fillColor | `string` | The color which needs to be filled in the displayed icon. | `gold` | -| strokeColor | `string` | The strokeColor which needs to be used in the displayed icon. | `filledColor` | -| opacity | `number` in `range(0, 1)` | The opacity for the displayed icon when it is not active. | `0.2` | -| activeOpacity | `number` in `range(0, 1)` | The opacity for the displayed icon when it is active. | `1` | -| isSingleSelection | `boolean` | When true only icon corresponding to rating value will be active. | `false` | -| size | `sm` \| `md` \| `lg` | The size of the rating. | `sm` | -| value | `string` | The current value of the rating (controlled). | - | -| defaultValue | `string` | The default value of the rating (uncontrolled). | - | -| description | `ReactNode` | A description for the rating. Provides a hint such as specific requirements for what to choose. | - | -| errorMessage | `ReactNode` | A errorMessage for the rating. | - | -| isRequired | `boolean` | Whether user rating is required on the input before form submission. | `false` | -| isReadOnly | `boolean` | Whether the rating can be selected but not changed by the user. | | -| isDisabled | `boolean` | Whether the rating is disabled. | `false` | -| isInvalid | `boolean` | Whether the rating is invalid. | `false` | -| baseRef | `RefObject` | The ref to the base element. | - | -| validationState | `valid` \| `invalid` | Whether the rating should display its "valid" or "invalid" visual styling. (**Deprecated**) use **isInvalid** instead. | - | -| disableAnimation | `boolean` | Whether the rating should be animated. | `false` | -| classNames | `Record<"base"| "mainWrapper"| "iconWrapper"| "input"| "iconSegment" | "icon" | "helperWrapper" | "description", string>` | Allows to set custom class names for the Rating slots. | - | +| Attribute | Type | Description | Default | +| ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------| ------------- | +| length | `number` | The length/maxValue of the rating. | `5` | +| precision | `number` in `range (0, 1]` (0 not included, 1 included) | The precision for the rating value. | `1` | +| children | `ReactNode[]` | The `RatingSegment` which makes up the `Rating`. | `null` | +| icon | `ReactNode` | The `icon` which needs to be desplayed `rating` component. | `StarIcon` | +| fillColor | `string` | The color which needs to be filled in the displayed icon. | `gold` | +| strokeColor | `string` | The strokeColor which needs to be used in the displayed icon. | `filledColor` | +| opacity | `number` in `range [0, 1]` (both 0 and 1 are included) | The opacity for the displayed icon when it is not active. | `0.2` | +| activeOpacity | `number` in `range [0, 1]` (both 0 and 1 are included) | The opacity for the displayed icon when it is active. | `1` | +| isSingleSelection | `boolean` | Only icon corresponding to the rating wil be active. | `false` | +| size | `sm` \| `md` \| `lg` | The size of the rating. | `sm` | +| value | `string` | The current value of the rating (controlled). | - | +| defaultValue | `string` | The default value of the rating (uncontrolled). | - | +| description | `ReactNode` | A description for the rating. Provides a hint such as specific requirements for what to choose. | - | +| errorMessage | `ReactNode` | `((v: ValidationResult) => ReactNode)` | A errorMessage for the rating. | - | +| isRequired | `boolean` | Whether user rating is required on the input before form submission. | `false` | +| isReadOnly | `boolean` | Whether the rating can be selected but not changed by the user. | | +| isDisabled | `boolean` | Whether the rating is disabled. | `false` | +| isInvalid | `boolean` | Whether the rating is invalid. | `false` | +| baseRef | `RefObject` | The ref to the base element. | - | +| disableAnimation | `boolean` | Whether the rating should be animated. | `false` | +| classNames | `Record<"base"| "mainWrapper"| "iconWrapper"| "input"| "iconSegment" | "icon" | "radioButtonsWrapper" | "radioButtonWrapper" | "description" | "errorMessage", string>` | Allows to set custom class names for the Rating slots. | - | ### Rating Events diff --git a/packages/components/rating/__tests__/rating.test.tsx b/packages/components/rating/__tests__/rating.test.tsx index d24e549e9a..7377d1a87f 100644 --- a/packages/components/rating/__tests__/rating.test.tsx +++ b/packages/components/rating/__tests__/rating.test.tsx @@ -1,5 +1,6 @@ import * as React from "react"; -import {fireEvent, render, renderHook} from "@testing-library/react"; +import {act, render, renderHook} from "@testing-library/react"; +import {focus} from "@nextui-org/test-utils"; import {useForm} from "react-hook-form"; import userEvent from "@testing-library/user-event"; @@ -36,12 +37,106 @@ describe("Rating", () => { expect(icons.length).toBe(3); }); + + it("should be able to reset the rating value after being selected once", async () => { + render(); + + const input = document.querySelectorAll("[data-slot=input]")[0]; + const radioButtonForValueZero = document.querySelectorAll("[data-slot=radio]")[0]; + const radioButtonForValueOne = document.querySelectorAll("[data-slot=radio]")[1]; + + const user = userEvent.setup(); + + await user.click(radioButtonForValueOne); + expect(input).toHaveValue(1); + + await user.click(radioButtonForValueZero); + expect(input).toHaveValue(0); + }); + + it("should be able to change the rating value on keypress", async () => { + render(); + + const input = document.querySelectorAll("[data-slot=input]")[0]; + const radioButtonForValueOne = document.querySelectorAll("[data-slot=radio]")[1] as HTMLElement; + const radioButtonForValueTwo = document.querySelectorAll("[data-slot=radio]")[2] as HTMLElement; + + const user = userEvent.setup(); + + await user.click(radioButtonForValueOne); + expect(input).toHaveValue(1); + + act(() => { + focus(radioButtonForValueOne); + }); + await user.keyboard("[ArrowRight]"); + expect(input).toHaveValue(2); + + act(() => { + focus(radioButtonForValueTwo); + }); + await user.keyboard("[ArrowLeft]"); + expect(input).toHaveValue(1); + }); +}); + +describe("validation", () => { + let user = userEvent.setup(); + + beforeAll(() => { + user = userEvent.setup(); + }); + + it("should support native validationBehaviour", async () => { + const {getAllByRole, getByTestId} = render( +
+ + , + ); + + const radios = getAllByRole("radio") as HTMLInputElement[]; + + for (let input of radios) { + expect(input).toHaveAttribute("required"); + expect(input).not.toHaveAttribute("aria-required"); + expect(input.validity.valid).toBe(false); + } + + act(() => { + (getByTestId("form") as HTMLFormElement).checkValidity(); + }); + expect(document.activeElement).toBe(radios[0]); + + await user.click(radios[0]); + for (let input of radios) { + expect(input.validity.valid).toBe(true); + } + }); + + it("should support aria validationBehaviour", async () => { + const {getByRole, getAllByRole} = render( +
+ + , + ); + + const group = getByRole("radiogroup"); + + expect(group).toHaveAttribute("aria-required", "true"); + + const radios = getAllByRole("radio") as HTMLInputElement[]; + + for (let input of radios) { + expect(input.validity.valid).toBe(true); + } + }); }); describe("Rating with React Hook Form", () => { let rating1: Element; let rating2: Element; let rating3: Element; + let radioButtonRating3: Element; let submitButton: HTMLButtonElement; let onSubmit: () => void; @@ -61,29 +156,32 @@ describe("Rating with React Hook Form", () => { onSubmit = jest.fn(); render( -
- - - - - , + <> +
+ + + + + + , ); rating1 = document.querySelectorAll("[data-slot=input]")[0]!; rating2 = document.querySelectorAll("[data-slot=input]")[1]!; rating3 = document.querySelectorAll("[data-slot=input]")[2]!; + radioButtonRating3 = document.querySelectorAll("[data-slot=radio]")[13]!; submitButton = document.querySelector("button")!; }); it("should work with defaultValues", () => { expect(rating1).toHaveValue(2); - expect(rating2).toHaveValue(0); - expect(rating3).toHaveValue(0); + expect(rating2).toHaveValue(null); + expect(rating3).toHaveValue(null); }); it("should not submit form when required field is empty", async () => { @@ -95,10 +193,9 @@ describe("Rating with React Hook Form", () => { }); it("should submit form when required field is not empty", async () => { - fireEvent.change(rating3, {target: {value: "2"}}); - const user = userEvent.setup(); + await user.click(radioButtonRating3); await user.click(submitButton); expect(onSubmit).toHaveBeenCalledTimes(1); diff --git a/packages/components/rating/package.json b/packages/components/rating/package.json index 88526c62ba..5a5cb8e16c 100644 --- a/packages/components/rating/package.json +++ b/packages/components/rating/package.json @@ -57,6 +57,7 @@ "devDependencies": { "@nextui-org/theme": "workspace:*", "@nextui-org/system": "workspace:*", + "@nextui-org/test-utils": "workspace:*", "clean-package": "2.2.0", "react": "^18.0.0", "react-dom": "^18.0.0", diff --git a/packages/components/rating/src/rating-segment.tsx b/packages/components/rating/src/rating-segment.tsx index 9375e5c044..bf6ce251de 100644 --- a/packages/components/rating/src/rating-segment.tsx +++ b/packages/components/rating/src/rating-segment.tsx @@ -1,4 +1,3 @@ -import {mergeRefs} from "@nextui-org/react-utils"; import {useMemo, useRef} from "react"; import {clsx, dataAttr} from "@nextui-org/shared-utils"; import {useHover} from "@react-aria/interactions"; @@ -29,7 +28,7 @@ const RatingSegment = ({index, icon, fillColor}: RatingSegmentProps) => { onBlur, } = context; - const iconRef = useRef(null); + const iconRef = useRef(null); let value = ratingValue.selectedValue; @@ -70,13 +69,20 @@ const RatingSegment = ({index, icon, fillColor}: RatingSegmentProps) => { }; return ( -
+
{Array.from(Array(numButtons)).map((_, idx) => { return ( -
+
{ return (
((props, ref) => { Component, children, length, - hasHelper, isInvalid, + isRequired, + ratingValue, + defaultValue, + value, + name, description, errorMessage, + classNames, + slots, + validationBehavior, + icon = , + onChange, + onBlur, setRatingValue, getBaseProps, getMainWrapperProps, getIconWrapperProps, - getHelperWrapperProps, getInputProps, - getDescriptionProps, - getErrorMessageProps, - ratingValue, - value, - name, - onBlur, - onChange, + validate, } = context; const IconList = useMemo(() => { - if (children) { - return
{children}
; - } - return (
{ setRatingValue({selectedValue: Number(e), hoveredValue: Number(e)}); }} > - {Array.from(Array(length)).map((_, idx) => ( - - ))} + {children ?? + Array.from(Array(length)).map((_, idx) => ( + + ))}
); - }, [children, length, getIconWrapperProps, name, onBlur, onChange]); - - const Helper = useMemo(() => { - if (!hasHelper) { - return null; - } - if (isInvalid && !!errorMessage) { - return ( -
-
{errorMessage}
-
- ); - } - - return ( -
-
{description}
-
- ); }, [ - hasHelper, + children, + length, + getIconWrapperProps, + name, + defaultValue, + ratingValue, + setRatingValue, isInvalid, + isRequired, description, errorMessage, - getHelperWrapperProps, - getDescriptionProps, - getErrorMessageProps, + slots, + classNames, + validationBehavior, + onBlur, + onChange, + validate, ]); const Input = useMemo( @@ -111,7 +112,6 @@ const Rating = forwardRef<"div", RatingProps>((props, ref) => {
{IconList} - {Helper} {Input}
diff --git a/packages/components/rating/src/use-rating.ts b/packages/components/rating/src/use-rating.ts index b9acbc63d3..3c5c813697 100644 --- a/packages/components/rating/src/use-rating.ts +++ b/packages/components/rating/src/use-rating.ts @@ -13,7 +13,6 @@ import {ReactNode, useCallback, useMemo, useRef} from "react"; import {useHover} from "@react-aria/interactions"; import {mergeProps} from "@react-aria/utils"; import {useLocale} from "@react-aria/i18n"; -import {StarIcon} from "@nextui-org/shared-icons"; import {AriaTextFieldProps} from "@react-types/textfield"; import {useControlledState} from "@react-stately/utils"; import {useSafeLayoutEffect} from "@nextui-org/use-safe-layout-effect"; @@ -49,15 +48,15 @@ interface Props extends HTMLNextUIProps<"div"> { */ strokeColor?: string; /** - * Opacity when the icon is not active. By default, opacity will be 0.2 + * Opacity when the icon is not active. By default, opacity will be 0.2. Range [0, 1] */ opacity?: number; /** - * Opacity when the icon is active. By default, selectedOpacity will be 1 + * Opacity when the icon is active. By default, selectedOpacity will be 1. Range [0, 1] */ activeOpacity?: number; /** - * Precision fraction round-off for Rating value. + * Precision fraction round-off for Rating value. Range (0, 1] */ precision?: number; /** @@ -76,10 +75,6 @@ interface Props extends HTMLNextUIProps<"div"> { * Boolean to disable animation. */ disableAnimation?: boolean; - /** - * Error message - */ - errorMessage?: React.ReactNode; /** * Classname or List of classes to change the classNames of the element. * if `className` is passed, it will be added to the base slot. @@ -92,7 +87,6 @@ interface Props extends HTMLNextUIProps<"div"> { * iconWrapper: "icon-wrapper-classes", * iconSegement: "icon-segment-classes", * icon: "icon-classes", - * helperWrapper: "helper-wrapper-classes", * description: "description-classes", * errorMessage: "error-message-classes", * }} /> @@ -100,7 +94,7 @@ interface Props extends HTMLNextUIProps<"div"> { */ classNames?: SlotsToClasses; /** - * React aria onChange event. + * onValueChnage method gets called whenever the value of rating changes. */ onValueChange?: (value: number) => void; } @@ -119,20 +113,29 @@ export function useRating(originalProps: UseRatingProps) { length = 5, classNames, strokeColor, - precision = 1, + precision: precisionValue = 1, fillColor = "gold", - opacity = 0.2, - activeOpacity = 1, + opacity: opacityValue = 0.2, + activeOpacity: activeOpacityValue = 1, children = null, isSingleSelection = false, - icon = StarIcon({}), + icon, + defaultValue, + value, onValueChange = () => {}, + validationBehavior, + name, + errorMessage, + description, + onChange, + onBlur, ...otherProps } = props; const { disableAnimation = globalContext?.disableAnimation ?? false, isDisabled = originalProps.isDisabled ?? false, + isRequired = originalProps.isRequired ?? false, } = originalProps; const {direction} = useLocale(); @@ -177,24 +180,38 @@ export function useRating(originalProps: UseRatingProps) { // i.e. setting ref.current.value to something which is uncontrolled // hence, sync the state with `ref.current.value` useSafeLayoutEffect(() => { - if (!domRef.current) return; + if (!domRef.current || !domRef.current.value) return; setRatingValue({ hoveredValue: Number(domRef.current.value), selectedValue: Number(domRef.current.value), }); }, [domRef.current]); - const description = props.description; + let precision = precisionValue; + + if (precisionValue <= 0 || precisionValue > 1) precision = 1; + + let opacity = opacityValue; + + if (opacityValue < 0) opacity = 0; + if (opacityValue > 1) opacity = 1; + + let activeOpacity = activeOpacityValue; + + if (activeOpacityValue < 0) activeOpacity = 0; + if (activeOpacityValue > 1) activeOpacity = 1; + + const validate = (value: string | null) => { + if (!props.validate || !value) return null; + props?.validate(value); + }; const isInvalid = props.isInvalid ?? false; - const errorMessage = props.errorMessage; - const hasHelper = !!description || !!errorMessage; const {hoverProps, isHovered: isIconWrapperHovered} = useHover({isDisabled}); const shouldConsiderHover = Math.abs(Math.floor(1 / precision) - 1 / precision) < Number.EPSILON; const onMouseMoveIconWrapper = (e: React.MouseEvent) => { - if (!iconWrapperRef || !iconWrapperRef.current) return; - if (!shouldConsiderHover) return; + if (!iconWrapperRef || !iconWrapperRef.current || !shouldConsiderHover) return; let precisionValue = precision; @@ -255,24 +272,14 @@ export function useRating(originalProps: UseRatingProps) { [iconWrapperRef, slots, hoverProps, ratingValue, setRatingValue, onMouseMoveIconWrapper], ); - const getHelperWrapperProps: PropGetter = useCallback( - (props = {}) => { - return { - className: slots.helperWrapper({class: clsx(classNames?.helperWrapper)}), - ...props, - "data-slot": "helper-wrapper", - }; - }, - [slots], - ); - const getInputProps: PropGetter = useCallback( (props = {}) => { return { ref: domRef, - value: ratingValue.selectedValue == -1 ? null : ratingValue.selectedValue, + value: ratingValue.selectedValue == -1 ? "" : ratingValue.selectedValue, className: slots.input({class: clsx(classNames?.input)}), type: "number", + readOnly: true, ...mergeProps(props, otherProps), "data-slot": "input", }; @@ -280,28 +287,6 @@ export function useRating(originalProps: UseRatingProps) { [domRef, ratingValue, slots, originalProps, originalProps.value], ); - const getDescriptionProps: PropGetter = useCallback( - (props = {}) => { - return { - className: slots.description({class: clsx(classNames?.description)}), - "data-slot": "description", - ...props, - }; - }, - [slots], - ); - - const getErrorMessageProps: PropGetter = useCallback( - (props = {}) => { - return { - className: slots.errorMessage({class: clsx(classNames?.errorMessage)}), - "data-slot": "error", - ...props, - }; - }, - [slots], - ); - return { Component, domRef, @@ -316,23 +301,26 @@ export function useRating(originalProps: UseRatingProps) { slots, fillColor, strokeColor, - icon, - hasHelper, isInvalid, - description, - errorMessage, + isRequired, shouldConsiderHover, opacity, activeOpacity, + name, + description, + errorMessage, + validationBehavior, + icon, + defaultValue, + value, setRatingValue, getBaseProps, getMainWrapperProps, getIconWrapperProps, - getHelperWrapperProps, getInputProps, - getDescriptionProps, - getErrorMessageProps, - ...otherProps, + onChange, + onBlur, + validate, }; } diff --git a/packages/components/rating/stories/rating.stories.tsx b/packages/components/rating/stories/rating.stories.tsx index f01b3181f4..a4e4e01b87 100644 --- a/packages/components/rating/stories/rating.stories.tsx +++ b/packages/components/rating/stories/rating.stories.tsx @@ -1,3 +1,5 @@ +import type {ValidationResult} from "@react-types/shared"; + import React, {useState} from "react"; import {Meta} from "@storybook/react"; import {button, rating} from "@nextui-org/theme"; @@ -33,6 +35,12 @@ export default { type: "boolean", }, }, + validationBehavior: { + control: { + type: "select", + }, + options: ["aria", "native"], + }, }, } as Meta; @@ -47,11 +55,7 @@ const WithReactHookFormTemplate = (args) => { register, handleSubmit, formState: {errors}, - } = useForm({ - defaultValues: { - rating: 2, - }, - }); + } = useForm(); const onSubmit = (data: any) => { // eslint-disable-next-line no-console @@ -71,6 +75,26 @@ const WithReactHookFormTemplate = (args) => { ); }; +const ErrorTemplate = (args) => { + const {register, handleSubmit} = useForm(); + + const onSubmit = (data: any) => { + // eslint-disable-next-line no-console + alert("Submitted value: " + JSON.stringify(data)); + }; + + return ( +
+
+ + + +
+ ); +}; + const ControlledTemplate = (args) => { const [value, setValue] = React.useState("2"); @@ -111,7 +135,7 @@ const CustomIconTemplate = (args) => { } length={5} onValueChange={setValue1} /> @@ -121,7 +145,7 @@ const CustomIconTemplate = (args) => { } length={5} onValueChange={setValue2} /> @@ -131,7 +155,7 @@ const CustomIconTemplate = (args) => { } length={5} onValueChange={setValue3} /> @@ -147,10 +171,10 @@ const CustomSegmentTemplate = (args) => { return (
- - - - + } index={0} {...args} /> + } index={1} {...args} /> + } index={2} {...args} /> + } index={3} {...args} />
Rating Value: {value}
@@ -219,6 +243,22 @@ export const WithErrorMessage = { }, }; +export const WithErrorMessageFunction = { + render: ErrorTemplate, + + args: { + ...defaultProps, + isRequired: true, + min: 1, + description: "Minimum rating value of 1 is required.", + errorMessage: (value: ValidationResult) => { + if (value.validationDetails.rangeUnderflow) { + return "Please rating value greater than or equal to 1."; + } + }, + }, +}; + export const Controlled = { render: ControlledTemplate, args: { diff --git a/packages/core/theme/src/components/rating.ts b/packages/core/theme/src/components/rating.ts index 6fea74121d..3c4b8c1343 100644 --- a/packages/core/theme/src/components/rating.ts +++ b/packages/core/theme/src/components/rating.ts @@ -10,7 +10,8 @@ const rating = tv({ iconSegment: ["relative"], icon: [], input: [], - helperWrapper: [], + radioButtonsWrapper: ["absolute inset-0 top-0 flex"], + radioButtonWrapper: ["col-span-1 inset-0 overflow-hidden opacity-0"], description: ["text-tiny", "text-foreground-400"], errorMessage: ["text-tiny", "text-red-400"], }, diff --git a/packages/utilities/shared-icons/src/angry-emojicon.tsx b/packages/utilities/shared-icons/src/angry-emojicon.tsx index ba6dd24aad..37b4eff38d 100644 --- a/packages/utilities/shared-icons/src/angry-emojicon.tsx +++ b/packages/utilities/shared-icons/src/angry-emojicon.tsx @@ -3,8 +3,8 @@ import {IconSvgProps} from "./types"; export const AngryEmojicon = ({...props}: IconSvgProps) => { return ( { return ( { return ( - + ); diff --git a/packages/utilities/shared-icons/src/like.tsx b/packages/utilities/shared-icons/src/like.tsx index fba3fdf851..96d23eab04 100644 --- a/packages/utilities/shared-icons/src/like.tsx +++ b/packages/utilities/shared-icons/src/like.tsx @@ -2,7 +2,14 @@ import {IconSvgProps} from "./types"; export const LikeIcon = ({...props}: IconSvgProps) => { return ( - + diff --git a/packages/utilities/shared-icons/src/music.tsx b/packages/utilities/shared-icons/src/music.tsx index 2d0de91e58..7a6f696704 100644 --- a/packages/utilities/shared-icons/src/music.tsx +++ b/packages/utilities/shared-icons/src/music.tsx @@ -2,7 +2,14 @@ import {IconSvgProps} from "./types"; export const MusicIcon = ({...props}: IconSvgProps) => { return ( - + diff --git a/packages/utilities/shared-icons/src/sad-emojicon.tsx b/packages/utilities/shared-icons/src/sad-emojicon.tsx index 6c2602edb5..8ecd002425 100644 --- a/packages/utilities/shared-icons/src/sad-emojicon.tsx +++ b/packages/utilities/shared-icons/src/sad-emojicon.tsx @@ -3,6 +3,8 @@ import {IconSvgProps} from "./types"; export const SadEmojicon = ({...props}: IconSvgProps) => { return ( { return ( - Star ); diff --git a/packages/utilities/shared-icons/src/straight-emojicon.tsx b/packages/utilities/shared-icons/src/straight-emojicon.tsx index 5af395d440..75de361b16 100644 --- a/packages/utilities/shared-icons/src/straight-emojicon.tsx +++ b/packages/utilities/shared-icons/src/straight-emojicon.tsx @@ -3,6 +3,8 @@ import {IconSvgProps} from "./types"; export const StraightEmojicon = ({...props}: IconSvgProps) => { return (