Skip to content

Commit ed8e868

Browse files
authored
feat: new Meter component (#500)
1 parent 7631960 commit ed8e868

19 files changed

+964
-113
lines changed
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
.meter {
2+
display: flex;
3+
flex-direction: column;
4+
gap: 2px;
5+
width: 300px;
6+
}
7+
8+
.meter__label-container {
9+
display: flex;
10+
justify-content: space-between;
11+
}
12+
13+
.meter__label,
14+
.meter__value-label {
15+
color: hsl(240 4% 16%);
16+
font-size: 14px;
17+
}
18+
19+
.meter__track {
20+
height: 10px;
21+
background-color: hsl(240 6% 90%);
22+
}
23+
24+
.meter__fill {
25+
background-color: hsl(200 98% 39%);
26+
height: 100%;
27+
width: var(--kb-meter-fill-width);
28+
transition: width 250ms linear;
29+
}
30+
31+
[data-kb-theme="dark"] .meter__label,
32+
[data-kb-theme="dark"] .meter__value-label {
33+
color: hsl(0 100% 100% / 0.9);
34+
}
35+
36+
[data-kb-theme="dark"] .meter__track {
37+
background-color: hsl(240 5% 26%);
38+
}

apps/docs/src/examples/meter.tsx

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import { Meter } from "@kobalte/core/meter";
2+
3+
import style from "./meter.module.css";
4+
5+
export function BasicExample() {
6+
return (
7+
<Meter value={80} class={style.meter}>
8+
<div class={style["meter__label-container"]}>
9+
<Meter.Label class={style.meter__label}>Batter Level:</Meter.Label>
10+
<Meter.ValueLabel class={style["meter__value-label"]} />
11+
</div>
12+
<Meter.Track class={style.meter__track}>
13+
<Meter.Fill class={style.meter__fill} />
14+
</Meter.Track>
15+
</Meter>
16+
);
17+
}
18+
19+
export function CustomValueScaleExample() {
20+
return (
21+
<Meter value={100} minValue={0} maxValue={250} class={style.meter}>
22+
<div class={style["meter__label-container"]}>
23+
<Meter.Label class={style.meter__label}>Disk Space Usage:</Meter.Label>
24+
<Meter.ValueLabel class={style["meter__value-label"]} />
25+
</div>
26+
<Meter.Track class={style.meter__track}>
27+
<Meter.Fill class={style.meter__fill} />
28+
</Meter.Track>
29+
</Meter>
30+
);
31+
}
32+
33+
export function CustomValueLabelExample() {
34+
return (
35+
<Meter
36+
value={3}
37+
minValue={0}
38+
maxValue={10}
39+
getValueLabel={({ value, max }) => `${value} of ${max} tasks completed`}
40+
class={style.meter}
41+
>
42+
<div class={style["meter__label-container"]}>
43+
<Meter.Label class={style.meter__label}>Processing...</Meter.Label>
44+
<Meter.ValueLabel class={style["meter__value-label"]} />
45+
</div>
46+
<Meter.Track class={style.meter__track}>
47+
<Meter.Fill class={style.meter__fill} />
48+
</Meter.Track>
49+
</Meter>
50+
);
51+
}

apps/docs/src/routes/docs/core.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,11 @@ const CORE_NAV_SECTIONS: NavSection[] = [
9797
title: "Menubar",
9898
href: "/docs/core/components/menubar",
9999
},
100+
{
101+
title: "Meter",
102+
href: "/docs/core/components/meter",
103+
status: "new",
104+
},
100105
{
101106
title: "Navigation Menu",
102107
href: "/docs/core/components/navigation-menu",
Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
import { Preview, TabsSnippets } from "../../../../components";
2+
import {
3+
BasicExample,
4+
CustomValueLabelExample,
5+
CustomValueScaleExample,
6+
} from "../../../../examples/meter";
7+
8+
# Meter
9+
10+
Displays numeric value that varies within a defined range
11+
12+
## Import
13+
14+
```ts
15+
import { Meter } from "@kobalte/core/meter";
16+
// or
17+
import { Root, Label, ... } from "@kobalte/core/meter";
18+
// or (deprecated)
19+
import { Meter } from "@kobalte/core";
20+
```
21+
22+
## Features
23+
24+
- Exposed to assistive technology as a meter via ARIA.
25+
- Labeling support for accessibility.
26+
- Internationalized number formatting as a percentage or value.
27+
28+
## Anatomy
29+
30+
The meter consists of:
31+
32+
- **Meter:** The root container for a meter.
33+
- **Meter.Label:** An accessible label that gives the user information on the meter.
34+
- **Meter.ValueLabel:** The accessible label text representing the current value in a human-readable format.
35+
- **Meter.Track:** The component that visually represents the meter track.
36+
- **Meter.Fill:** The component that visually represents the meter value.
37+
38+
```tsx
39+
<Meter>
40+
<Meter.Label />
41+
<Meter.ValueLabel />
42+
<Meter.Track>
43+
<Meter.Fill />
44+
</Meter.Track>
45+
</Meter>
46+
```
47+
48+
## Example
49+
50+
<Preview>
51+
<BasicExample />
52+
</Preview>
53+
54+
<TabsSnippets>
55+
<TabsSnippets.List>
56+
<TabsSnippets.Trigger value="index.tsx">index.tsx</TabsSnippets.Trigger>
57+
<TabsSnippets.Trigger value="style.css">style.css</TabsSnippets.Trigger>
58+
</TabsSnippets.List>
59+
{/* <!-- prettier-ignore-start -->*/}
60+
<TabsSnippets.Content value="index.tsx">
61+
```tsx
62+
import { Meter } from "@kobalte/core/meter";
63+
import "./style.css";
64+
65+
function App() {
66+
return (
67+
<Meter value={80} class="meter">
68+
<div class="meter__label-container">
69+
<Meter.Label class="meter__label">Battery Level:</Meter.Label>
70+
<Meter.ValueLabel class="meter__value-label" />
71+
</div>
72+
<Meter.Track class="meter__track">
73+
<Meter.Fill class="meter__fill" />
74+
</Meter.Track>
75+
</Meter>
76+
);
77+
}
78+
```
79+
80+
</TabsSnippets.Content>
81+
<TabsSnippets.Content value="style.css">
82+
```css
83+
.meter {
84+
display: flex;
85+
flex-direction: column;
86+
gap: 2px;
87+
width: 300px;
88+
}
89+
90+
.meter__label-container {
91+
display: flex;
92+
justify-content: space-between;
93+
}
94+
95+
.meter__label,
96+
.meter__value-label {
97+
color: hsl(240 4% 16%);
98+
font-size: 14px;
99+
}
100+
101+
.meter__track {
102+
height: 10px;
103+
background-color: hsl(240 6% 90%);
104+
}
105+
106+
.meter__fill {
107+
background-color: hsl(200 98% 39%);
108+
height: 100%;
109+
width: var(--kb-meter-fill-width);
110+
transition: width 250ms linear;
111+
}
112+
113+
```
114+
115+
</TabsSnippets.Content>
116+
{/* <!-- prettier-ignore-end -->*/}
117+
</TabsSnippets>
118+
119+
## Usage
120+
121+
### Custom value scale
122+
123+
By default, the `value` prop represents the current value of meter, as the minimum and maximum values default to 0 and 100, respectively. Alternatively, a different scale can be used by setting the `minValue` and `maxValue` props.
124+
125+
<Preview>
126+
<CustomValueScaleExample />
127+
</Preview>
128+
129+
```tsx {0}
130+
<Meter value={100} minValue={0} maxValue={250} class="meter">
131+
<div class="meter__label-container">
132+
<Meter.Label class="meter__label">Disk Space Usage:</Meter.Label>
133+
<Meter.ValueLabel class="meter__value-label" />
134+
</div>
135+
<Meter.Track class="meter__track">
136+
<Meter.Fill class="meter__fill" />
137+
</Meter.Track>
138+
</Meter>
139+
```
140+
141+
### Custom value label
142+
143+
The `getValueLabel` prop allows the formatted value used in `Meter.ValueLabel` and ARIA to be replaced with a custom string. It receives the current value, min and max values as parameters.
144+
145+
<Preview>
146+
<CustomValueLabelExample />
147+
</Preview>
148+
149+
```tsx {4}
150+
<Meter
151+
value={3}
152+
minValue={0}
153+
maxValue={10}
154+
getValueLabel={({ value, max }) => `${value} of ${max} tasks completed`}
155+
class="meter"
156+
>
157+
<div class="meter__label-container">
158+
<Meter.Label class="meter__label">Processing...</Meter.Label>
159+
<Meter.ValueLabel class="meter__value-label" />
160+
</div>
161+
<Meter.Track class="meter__track">
162+
<Meter.Fill class="meter__fill" />
163+
</Meter.Track>
164+
</Meter>
165+
```
166+
167+
### Meter fill width
168+
169+
We expose a CSS custom property `--kb-meter-fill-width` which corresponds to the percentage of meterion (ex: 80%). If you are building a linear meter, you can use it to set the width of the `Meter.Fill` component in CSS.
170+
171+
## API Reference
172+
173+
### Meter
174+
175+
`Meter` is equivalent to the `Root` import from `@kobalte/core/meter` (and deprecated `Meter.Root`).
176+
177+
| Prop | Description |
178+
| :------------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
179+
| value | `number` <br/> The meter value. |
180+
| minValue | `number` <br/> The minimum meter value. |
181+
| maxValue | `number` <br/> The maximum meter value. |
182+
| getValueLabel | `(params: { value: number; min: number; max: number }) => string` <br/> A function to get the accessible label text representing the current value in a human-readable format. If not provided, the value label will be read as a percentage of the max value. |
183+
184+
| Data attribute | Description |
185+
| :------------- | :---------- |
186+
187+
`Meter.Label`, `Meter.ValueLabel`, `Meter.Track` and `Meter.Fill` shares the same data-attributes.
188+
189+
## Rendered elements
190+
191+
| Component | Default rendered element |
192+
| :----------------- | :----------------------- |
193+
| `Meter` | `div` |
194+
| `Meter.Label` | `span` |
195+
| `Meter.ValueLabel` | `div` |
196+
| `Meter.Track` | `div` |
197+
| `Meter.Fill` | `div` |

packages/core/src/index.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ export * as Image from "./image";
2828
export * as Link from "./link";
2929
export * as Listbox from "./listbox";
3030
export * as Menubar from "./menubar";
31+
export * as Meter from "./meter";
3132
export * as NumberField from "./number-field";
3233
export * as Pagination from "./pagination";
3334
export * as Popover from "./popover";

packages/core/src/meter/index.tsx

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import {
2+
MeterFill as Fill,
3+
type MeterFillCommonProps,
4+
type MeterFillOptions,
5+
type MeterFillProps,
6+
type MeterFillRenderProps,
7+
} from "./meter-fill";
8+
import {
9+
MeterLabel as Label,
10+
type MeterLabelCommonProps,
11+
type MeterLabelOptions,
12+
type MeterLabelProps,
13+
type MeterLabelRenderProps,
14+
} from "./meter-label";
15+
import {
16+
type MeterRootCommonProps,
17+
type MeterRootOptions,
18+
type MeterRootProps,
19+
type MeterRootRenderProps,
20+
MeterRoot as Root,
21+
} from "./meter-root";
22+
import {
23+
type MeterTrackCommonProps,
24+
type MeterTrackOptions,
25+
type MeterTrackProps,
26+
type MeterTrackRenderProps,
27+
MeterTrack as Track,
28+
} from "./meter-track";
29+
import {
30+
type MeterValueLabelCommonProps,
31+
type MeterValueLabelOptions,
32+
type MeterValueLabelProps,
33+
type MeterValueLabelRenderProps,
34+
MeterValueLabel as ValueLabel,
35+
} from "./meter-value-label";
36+
37+
export type {
38+
MeterFillOptions,
39+
MeterFillCommonProps,
40+
MeterFillRenderProps,
41+
MeterFillProps,
42+
MeterLabelOptions,
43+
MeterLabelCommonProps,
44+
MeterLabelRenderProps,
45+
MeterLabelProps,
46+
MeterRootOptions,
47+
MeterRootCommonProps,
48+
MeterRootRenderProps,
49+
MeterRootProps,
50+
MeterTrackOptions,
51+
MeterTrackCommonProps,
52+
MeterTrackRenderProps,
53+
MeterTrackProps,
54+
MeterValueLabelOptions,
55+
MeterValueLabelCommonProps,
56+
MeterValueLabelRenderProps,
57+
MeterValueLabelProps,
58+
};
59+
export { Fill, Label, Root, Track, ValueLabel };
60+
61+
export const Meter = Object.assign(Root, {
62+
Fill,
63+
Label,
64+
Track,
65+
ValueLabel,
66+
});

0 commit comments

Comments
 (0)