Skip to content

Commit 30b11ec

Browse files
authored
feat(evo-marko): evo-star-rating (#633)
1 parent 79efd8b commit 30b11ec

9 files changed

Lines changed: 171 additions & 0 deletions

File tree

.changeset/young-snakes-bet.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@evo-web/marko": patch
3+
---
4+
5+
Add evo-star-rating component
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<h1 style='display: flex; justify-content: space-between; align-items: center;'>
2+
<span>evo-star-rating</span>
3+
<span style='font-weight: normal; font-size: medium; margin-bottom: -15px;'>DS v1.0.0</span>
4+
</h1>
5+
6+
Displays a read-only star rating from 0 to 5, with half-star support.
7+
8+
## Examples and Documentation
9+
10+
- [Storybook](https://ebay.github.io/evo-web/ebayui-core/?path=/story/graphics-icons-evo-star-rating)
11+
- [Storybook Docs](https://ebay.github.io/evo-web/ebayui-core/?path=/docs/graphics-icons-evo-star-rating)
12+
- [Code Examples](https://github.com/eBay/evo-web/tree/main/packages/evo-marko/src/tags/evo-star-rating/examples)
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<style>
2+
.star-rating-examples {
3+
display: flex;
4+
row-gap: 10px;
5+
flex-direction: column;
6+
align-items: flex-start;
7+
}
8+
</style>
9+
10+
<div class="star-rating-examples">
11+
<evo-star-rating value=0 a11yText="0 stars"/>
12+
<evo-star-rating value=0.5 a11yText="0.5 stars"/>
13+
<evo-star-rating value=1 a11yText="1 star"/>
14+
<evo-star-rating value=1.5 a11yText="1.5 stars"/>
15+
<evo-star-rating value=2 a11yText="2 stars"/>
16+
<evo-star-rating value=2.5 a11yText="2.5 stars"/>
17+
<evo-star-rating value=3 a11yText="3 stars"/>
18+
<evo-star-rating value=3.5 a11yText="3.5 stars"/>
19+
<evo-star-rating value=4 a11yText="4 stars"/>
20+
<evo-star-rating value=4.5 a11yText="4.5 stars"/>
21+
<evo-star-rating value=5 a11yText="5 stars"/>
22+
</div>
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import type { Input as StarRatingInput } from "<evo-star-rating>";
2+
export interface Input extends StarRatingInput {}
3+
<evo-star-rating ...input/>
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
static function toDataStars(value: number): string {
2+
const clamped = Math.max(0, Math.min(5, value));
3+
const rounded = Math.round(clamped * 2) / 2;
4+
const whole = Math.floor(rounded);
5+
const half = rounded % 1 !== 0;
6+
return half ? `${whole}-5` : `${whole}`;
7+
}
8+
9+
export interface Input extends Omit<Marko.HTML.Div, "aria-label" | "role"> {
10+
value?: number;
11+
/**
12+
* Accessible label for the star rating.
13+
*
14+
* English default to be overridden is `"Rating: ${value} out of 5"`.
15+
*/
16+
a11yText: string;
17+
}
18+
19+
<const/{ class: inputClass, value = 0, a11yText = `Rating: ${value} out of 5`, ...htmlInput }=input>
20+
21+
<div
22+
...htmlInput
23+
role="img"
24+
aria-label=a11yText
25+
class=["star-rating", inputClass]
26+
data-stars=toDataStars(value)>
27+
<for until=5>
28+
<evo-icon-star-dynamic class="star-rating__icon"/>
29+
</for>
30+
</div>
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import { buildExtensionTemplate } from "../../common/storybook/utils";
2+
import { type Meta } from "@storybook/marko";
3+
import Readme from "./README.md";
4+
import Component, { type Input } from "./index.marko";
5+
import DefaultTemplate from "./examples/default.marko";
6+
import DefaultCode from "./examples/default.marko?raw";
7+
import AllValuesTemplate from "./examples/all-values.marko";
8+
import AllValuesCode from "./examples/all-values.marko?raw";
9+
10+
export default {
11+
title: "graphics & icons/evo-star-rating",
12+
component: Component,
13+
parameters: {
14+
docs: {
15+
description: { component: Readme },
16+
},
17+
},
18+
argTypes: {
19+
value: {
20+
type: "number",
21+
control: { type: "range", min: 0, max: 5, step: 0.5 },
22+
description:
23+
"The star rating value from 0 to 5. Supports half values (e.g. 2.5).",
24+
},
25+
a11yText: {
26+
type: { name: "string", required: true },
27+
control: "text",
28+
description:
29+
'Accessible label for the star rating. English default to be overridden is `"Rating: ${value} out of 5"`.',
30+
},
31+
["<div> attributes" as any]: {
32+
description:
33+
"All attributes and event handlers from [the native HTML `<div>` tag](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/div) will be passed through",
34+
},
35+
},
36+
} satisfies Meta<Input>;
37+
38+
export const Default = buildExtensionTemplate(DefaultTemplate, DefaultCode);
39+
Default.args = {
40+
value: 3.5,
41+
};
42+
43+
export const AllValues = buildExtensionTemplate(
44+
AllValuesTemplate,
45+
AllValuesCode,
46+
);
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
import "@ebay/skin/star-rating";
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import { describe, it, expect, beforeEach, afterEach } from "vitest";
2+
import { render, cleanup } from "@marko/testing-library";
3+
import { composeStories } from "@storybook/marko";
4+
import * as stories from "../star-rating.stories";
5+
6+
const { Default } = composeStories(stories);
7+
8+
afterEach(cleanup);
9+
10+
describe("evo-star-rating", () => {
11+
let component: Awaited<ReturnType<typeof render>>;
12+
13+
beforeEach(async () => {
14+
component = await render(Default);
15+
});
16+
17+
it("should render with role img", () => {
18+
expect(component.getByRole("img")).toBeTruthy();
19+
});
20+
21+
it("should have correct data-stars attribute", () => {
22+
const el = component.getByRole("img");
23+
expect(el.getAttribute("data-stars")).toBe("3-5");
24+
});
25+
26+
it("should have default aria-label", () => {
27+
const el = component.getByRole("img");
28+
expect(el.getAttribute("aria-label")).toBe("3.5 out of 5 stars");
29+
});
30+
31+
it("should render 5 star icons", () => {
32+
const el = component.getByRole("img");
33+
const svgs = el.querySelectorAll("svg");
34+
expect(svgs.length).toBe(5);
35+
});
36+
});
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { describe, it } from "vitest";
2+
import { composeStories } from "@storybook/marko";
3+
import { snapshotHTML } from "../../../common/test-utils/snapshots";
4+
import * as stories from "../star-rating.stories";
5+
6+
const { Default, AllValues } = composeStories(stories);
7+
8+
describe("evo-star-rating SSR", () => {
9+
it("renders default", async () => {
10+
await snapshotHTML(Default);
11+
});
12+
13+
it("renders all values", async () => {
14+
await snapshotHTML(AllValues);
15+
});
16+
});

0 commit comments

Comments
 (0)