Skip to content
Open
Show file tree
Hide file tree
Changes from all 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

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import {
<Text>250 ms</Text>
<Text>Dateioperationen</Text>
</BigNumber>
<Rating value={4} />
<Rating aria-label="Bewertung" value={4} isReadOnly />
<Text>
<small>Geringer Optimierungsbedarf</small>
</Text>
Expand All @@ -26,7 +26,7 @@ import {
<Text>100 ms</Text>
<Text>Serveroperationen</Text>
</BigNumber>
<Rating value={2} />
<Rating aria-label="Bewertung" value={2} isReadOnly />
<Text>
<small>Optimierungsbedarf</small>
</Text>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,5 @@ import {
<Text>80%</Text>
<Text>Performance</Text>
</BigNumber>
<Rating value={4} />
<Rating aria-label="Performance" value={4} isReadOnly />
</Flex>;
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import {
Label,
Rating,
} from "@mittwald/flow-react-components";

<Rating>
<Label>Bewertung</Label>
</Rating>;
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
import { Rating } from "@mittwald/flow-react-components";

<Rating value={2} />;
<Rating value={2} isReadOnly aria-label="Bewertung" />;
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { Rating } from "@mittwald/flow-react-components";

<Column>
<Rating defaultValue={2} aria-label="Bewertung" />
<Rating
size="s"
defaultValue={2}
aria-label="Bewertung"
/>
</Column>;
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
# Playground

Verwende `<Rating />`, um ein Rating darzustellen. Um die Barrierefreiheit zu
Verwende `<Rating />`, um ein Rating anzubieten. Um die Barrierefreiheit zu
gewährleisten, wird in der Rating Component automatisch ein entsprechendes
ARIA-Label generiert.
ARIA-Label für die einzelnen Rating-Stufen generiert.

<LiveCodeEditor />

Expand All @@ -16,20 +16,28 @@ Es stehen die Größen **Medium** oder **Small** zur Verfügung.

---

# Readonly

Verwende `isReadOnly`, wenn du die Rating-Komponente nur zur Darstellung
verwenden möchtest.

<LiveCodeEditor example="read-only" editorCollapsed />

---

# Kombiniere mit ...

## BigNumber

Das Rating kann mit einer
[BigNumber](/03-components/data-visualisation/big-number/overview)
kombiniert werden, um den Wert der BigNumber visuell besser einordnen zu können.
[BigNumber](/03-components/data-visualisation/big-number/overview) kombiniert
werden, um den Wert der BigNumber visuell besser einordnen zu können.

<LiveCodeEditor example="big-number" editorCollapsed />

## AccentBox

Zur visuellen Hervorhebung kann das Rating innerhalb einer
[AccentBox](/03-components/structure/accent-box/overview)
angezeigt werden.
[AccentBox](/03-components/structure/accent-box/overview) angezeigt werden.

<LiveCodeEditor example="accent-box" editorCollapsed />
99 changes: 92 additions & 7 deletions packages/components/src/components/Rating/Rating.module.scss
Original file line number Diff line number Diff line change
@@ -1,16 +1,101 @@
@use "@/styles/mixins/focus";

.rating {
display: flex;
.ratingSegments {
display: flex;
width: fit-content;
order: 2;
}

.ratingSegment {
display: grid;
grid-template-areas: "star";
border-radius: var(--corner-radius--default);

@include focus.focus;

& {
outline-offset: calc(var(--size-px--xxs) * -1);
}

&[data-focus-visible] {
transform: scale(1.2);
}

.star,
.starFilled {
grid-area: star;
transition-property: opacity, transform;
transition-duration: var(--transition--duration--fast);
}

.star {
opacity: 0;
color: var(--rating--star-color);
}
.starFilled {
opacity: 1;
color: var(--rating--star-filled-color);
}
}

.star {
color: var(--rating--star-color);
&:not(:has(.ratingSegments:hover)),
&[data-readonly] {
.ratingSegment {
&.current {
~ .ratingSegment {
.star {
opacity: 1;
}
.starFilled {
opacity: 0;
}
}
}
}
&:not(:has(.current)) {
.ratingSegment {
.star {
opacity: 1;
}
.starFilled {
opacity: 0;
}
}
}
}
.starFilled {
color: var(--rating--star-filled-color);

&:not([data-readonly]) {
&.ratingSegments:hover {
.ratingSegment {
.star {
opacity: 0;
}

.starFilled {
opacity: 1;
}
}
}
.ratingSegment:hover {
transform: scale(1.2);
transform-origin: center;

~ .ratingSegment {
.star {
opacity: 1;
}

.starFilled {
opacity: 0;
}
}
}
}

@mixin size($size) {
&.size-#{$size} {
column-gap: var(--rating--spacing--#{$size});
&.size-#{$size} .ratingSegment {
padding: var(--rating--spacing--#{$size});
}
}

Expand Down
103 changes: 73 additions & 30 deletions packages/components/src/components/Rating/Rating.tsx
Original file line number Diff line number Diff line change
@@ -1,49 +1,92 @@
import React, { type FC } from "react";
import { IconStar, IconStarFilled } from "@/components/Icon/components/icons";
import React, { type PropsWithChildren } from "react";
import styles from "./Rating.module.scss";
import type { PropsWithClassName } from "@/lib/types/props";
import clsx from "clsx";
import { useLocalizedStringFormatter } from "react-aria";
import locales from "./locales/*.locale.json";
import * as Aria from "react-aria-components";
import { RatingSegment } from "@/components/Rating/components/RatingSegment";
import {
flowComponent,
type FlowComponentProps,
} from "@/lib/componentFactory/flowComponent";
import { PropsContextProvider } from "@/lib/propsContext";
import { useFieldComponent } from "@/lib/hooks/useFieldComponent";
import { useObjectRef } from "@react-aria/utils";
import { useMakeFocusable } from "@/lib/hooks/dom/useMakeFocusable";

export interface RatingProps extends PropsWithClassName {
export interface RatingProps
extends FlowComponentProps,
PropsWithChildren,
Omit<Aria.RadioGroupProps, "children" | "value" | "defaultValue"> {
/** The value sets the amount of filled stars. @default: 0 */
value?: 0 | 1 | 2 | 3 | 4 | 5;
value?: number;
/** The defaultValue sets the amount of default filled stars. @default: 0 */
defaultValue?: number;
/** The size of the component. @default: "m" */
size?: "s" | "m";
}

/** @flr-generate all */
export const Rating: FC<RatingProps> = (props) => {
const { value = 0, size = "m" } = props;
export const Rating = flowComponent("Rating", (props) => {
const {
value,
defaultValue = 0,
size = "m",
className,
children,
ref,
...rest
} = props;

const rootClassName = clsx(styles.rating, styles[`size-${size}`]);
const {
FieldErrorView,
FieldErrorResetContext,
fieldProps,
fieldPropsContext,
} = useFieldComponent(props);

const stringFormatter = useLocalizedStringFormatter(locales);
const rootClassName = clsx(
styles.rating,
styles[`size-${size}`],
fieldProps.className,
className,
);

const stars = Array(5)
.fill("")
.map((_, index) =>
index < value ? (
<IconStarFilled
key={index}
aria-hidden
size={size}
className={styles.starFilled}
/>
) : (
<IconStar key={index} aria-hidden size={size} className={styles.star} />
),
);
const localRef = useObjectRef(ref);
useMakeFocusable(localRef);

return (
<div
aria-label={stringFormatter.format(`rating.${value}`)}
<Aria.RadioGroup
{...rest}
className={rootClassName}
defaultValue={defaultValue.toString()}
value={value || value === 0 ? value.toString() : undefined}
ref={localRef}
>
{stars}
</div>
{(renderProps) => (
<>
<FieldErrorResetContext>
<PropsContextProvider props={fieldPropsContext}>
{children}
<div className={styles.ratingSegments}>
{Array(5)
.fill("")
.map((_, index) => (
<RatingSegment
key={index}
index={index}
selectedValue={parseInt(
renderProps.state.selectedValue ?? "0",
)}
size={size}
/>
))}
</div>
</PropsContextProvider>
</FieldErrorResetContext>
<FieldErrorView />
</>
)}
</Aria.RadioGroup>
);
};
});

export default Rating;
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import React, { type FC } from "react";
import clsx from "clsx";
import styles from "@/components/Rating/Rating.module.scss";
import { IconStar, IconStarFilled } from "@/components/Icon/components/icons";
import * as Aria from "react-aria-components";
import type { RatingProps } from "@/components/Rating";
import { useLocalizedStringFormatter } from "react-aria";
import locales from "../../locales/*.locale.json";

interface Props {
index: number;
selectedValue: number;
size: RatingProps["size"];
}
export const RatingSegment: FC<Props> = (props) => {
const { index, selectedValue, size } = props;

const value = index + 1;

const stringFormatter = useLocalizedStringFormatter(locales);

return (
<Aria.Radio
aria-label={stringFormatter.format(`rating.${value}`)}
value={value.toString()}
className={clsx(
styles.ratingSegment,
value === selectedValue && styles.current,
)}
>
<IconStarFilled aria-hidden size={size} className={styles.starFilled} />
<IconStar aria-hidden size={size} className={styles.star} />
</Aria.Radio>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { RatingSegment } from "./RatingSegment";
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
{
"rating.0": "Bewertung: 0 von 5",
"rating.1": "Bewertung: 1 von 5",
"rating.2": "Bewertung: 2 von 5",
"rating.3": "Bewertung: 3 von 5",
"rating.4": "Bewertung: 4 von 5",
"rating.5": "Bewertung: 5 von 5"
"rating.0": "0 von 5",
"rating.1": "1 von 5",
"rating.2": "2 von 5",
"rating.3": "3 von 5",
"rating.4": "4 von 5",
"rating.5": "5 von 5"
}
Loading