Skip to content

Commit

Permalink
feat(rating): adding the focus indicator to the rating component
Browse files Browse the repository at this point in the history
  • Loading branch information
macci001 committed Nov 5, 2024
1 parent 374136d commit 38f17fe
Show file tree
Hide file tree
Showing 3 changed files with 37 additions and 7 deletions.
15 changes: 13 additions & 2 deletions packages/components/rating/src/rating-segment.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import {useMemo} from "react";
import {useMemo, useState} from "react";
import {clsx, dataAttr} from "@nextui-org/shared-utils";
import {useHover} from "@react-aria/interactions";
import {Radio} from "@nextui-org/radio";
import {chain} from "@react-aria/utils";

import {useRatingContext} from "./rating-context";
import {RatingIcon} from "./rating-icon";
Expand All @@ -24,6 +25,7 @@ const RatingSegment = ({index, icon, fillColor}: RatingSegmentProps) => {
classNames,
isSingleSelection,
name,
focusWithin,
onChange,
onBlur,
setRatingValue,
Expand Down Expand Up @@ -59,6 +61,7 @@ const RatingSegment = ({index, icon, fillColor}: RatingSegmentProps) => {

const segmentStyles = slots.iconSegment({class: clsx(classNames?.iconSegment)});
const {isHovered, hoverProps} = useHover({});
const [isKeyPress, setIsKeyPress] = useState(false);

const radioButtons = useMemo(() => {
const numButtons = Math.floor(1 / precision);
Expand Down Expand Up @@ -95,8 +98,13 @@ const RatingSegment = ({index, icon, fillColor}: RatingSegmentProps) => {
data-slot="radio"
name={name}
value={radioButtonValue.toString()}
onBlur={onBlur}
onBlur={chain(onBlur, () => {
setIsKeyPress(false);
})}
onChange={onChange}
onKeyUp={() => {
setIsKeyPress(true);
}}
/>
</div>
);
Expand All @@ -109,6 +117,9 @@ const RatingSegment = ({index, icon, fillColor}: RatingSegmentProps) => {
<div
className={segmentStyles}
data-hovered={dataAttr(isHovered)}
data-selected={dataAttr(
index + 1 == Math.ceil(ratingValue.selectedValue) && isKeyPress && focusWithin,
)}
data-slot="segment"
{...hoverProps}
>
Expand Down
17 changes: 14 additions & 3 deletions packages/components/rating/src/use-rating.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ import {
import {rating} from "@nextui-org/theme";
import {ReactRef, useDOMRef} from "@nextui-org/react-utils";
import {clsx, dataAttr, objectToDeps} from "@nextui-org/shared-utils";
import {ReactNode, useCallback, useMemo, useRef} from "react";
import {useHover} from "@react-aria/interactions";
import {ReactNode, useCallback, useMemo, useRef, useState} from "react";
import {useHover, useFocusWithin} from "@react-aria/interactions";
import {mergeProps} from "@react-aria/utils";
import {useLocale} from "@react-aria/i18n";
import {AriaTextFieldProps} from "@react-types/textfield";
Expand Down Expand Up @@ -92,6 +92,7 @@ interface Props extends HTMLNextUIProps<"div"> {
* input: "input-classes",
* radioButtonsWrapper: "radio-buttons-wrapper-classes",
* radioButtonWrapper: "radio-button-wrapper-classes",
* label: "label-classes",
* description: "description-classes",
* errorMessage: "error-message-classes",
* }} />
Expand Down Expand Up @@ -217,12 +218,19 @@ export function useRating(originalProps: UseRatingProps) {
const shouldConsiderHover = Math.abs(Math.floor(1 / precision) - 1 / precision) < Number.EPSILON;

const baseStyles = clsx(classNames?.base, className);
const [focusWithin, setFocusWithin] = useState(false);
const {focusWithinProps} = useFocusWithin({
onFocusWithinChange: (focusWithin: boolean) => {
setFocusWithin(focusWithin);
},
});

const getBaseProps: PropGetter = useCallback(
(props = {}) => {
return {
ref: baseDomRef,
className: slots.base({class: baseStyles}),
...mergeProps(props),
...mergeProps(props, focusWithinProps),
"data-slot": "base",
"data-disabled": dataAttr(isDisabled),
"data-invalid": dataAttr(isInvalid),
Expand Down Expand Up @@ -253,6 +261,7 @@ export function useRating(originalProps: UseRatingProps) {
...mergeProps(props, hoverProps),
"data-slot": "icon-wrapper",
"data-hover": dataAttr(isIconWrapperHovered),
"data-selected": dataAttr(ratingValue.selectedValue == 0 && focusWithin),
};
},
[iconWrapperRef, slots, hoverProps, ratingValue, setRatingValue],
Expand Down Expand Up @@ -287,6 +296,7 @@ export function useRating(originalProps: UseRatingProps) {
classNames: {
errorMessage: slots.errorMessage({class: classNames?.errorMessage}),
description: slots.description({class: classNames?.description}),
label: slots.label({class: classNames?.label}),
},
validate: originalProps.validate,
label: label,
Expand Down Expand Up @@ -324,6 +334,7 @@ export function useRating(originalProps: UseRatingProps) {
icon,
defaultValue,
value,
focusWithin,
setRatingValue,
getBaseProps,
getMainWrapperProps,
Expand Down
12 changes: 10 additions & 2 deletions packages/core/theme/src/components/rating.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,18 @@ const rating = tv({
slots: {
base: "flex flex-col w-fit cursor-pointer",
mainWrapper: "relative",
iconWrapper: "inline-flex gap-x-0",
iconSegment: ["relative"],
iconWrapper: [
"inline-flex",
"gap-x-0",
"border border-2 border-transparent data-[selected=true]:border-white",
],
iconSegment: [
"relative",
"border border-2 border-transparent data-[selected=true]:border-white",
],
icon: [],
input: [],
label: [],
radioButtonsWrapper: ["absolute inset-0 top-0 flex"],
radioButtonWrapper: ["col-span-1 inset-0 overflow-hidden opacity-0"],
description: ["text-tiny", "text-foreground-400"],
Expand Down

0 comments on commit 38f17fe

Please sign in to comment.