Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Meter component #500

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 4 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
38 changes: 38 additions & 0 deletions apps/docs/src/examples/meter.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
.meter {
display: flex;
flex-direction: column;
gap: 2px;
width: 300px;
}

.meter__label-container {
display: flex;
justify-content: space-between;
}

.meter__label,
.meter__value-label {
color: hsl(240 4% 16%);
font-size: 14px;
}

.meter__track {
height: 10px;
background-color: hsl(240 6% 90%);
}

.meter__fill {
background-color: hsl(200 98% 39%);
height: 100%;
width: var(--kb-meter-fill-width);
transition: width 250ms linear;
}

[data-kb-theme="dark"] .meter__label,
[data-kb-theme="dark"] .meter__value-label {
color: hsl(0 100% 100% / 0.9);
}

[data-kb-theme="dark"] .meter__track {
background-color: hsl(240 5% 26%);
}
51 changes: 51 additions & 0 deletions apps/docs/src/examples/meter.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { Meter } from "@kobalte/core/meter";

import style from "./meter.module.css";

export function BasicExample() {
return (
<Meter value={80} class={style.meter}>
<div class={style["meter__label-container"]}>
<Meter.Label class={style.meter__label}>Batter Level:</Meter.Label>
<Meter.ValueLabel class={style["meter__value-label"]} />
</div>
<Meter.Track class={style.meter__track}>
<Meter.Fill class={style.meter__fill} />
</Meter.Track>
</Meter>
);
}

export function CustomValueScaleExample() {
return (
<Meter value={100} minValue={0} maxValue={250} class={style.meter}>
<div class={style["meter__label-container"]}>
<Meter.Label class={style.meter__label}>Disk Space Usage:</Meter.Label>
<Meter.ValueLabel class={style["meter__value-label"]} />
</div>
<Meter.Track class={style.meter__track}>
<Meter.Fill class={style.meter__fill} />
</Meter.Track>
</Meter>
);
}

export function CustomValueLabelExample() {
return (
<Meter
value={3}
minValue={0}
maxValue={10}
getValueLabel={({ value, max }) => `${value} of ${max} tasks completed`}
class={style.meter}
>
<div class={style["meter__label-container"]}>
<Meter.Label class={style.meter__label}>Processing...</Meter.Label>
<Meter.ValueLabel class={style["meter__value-label"]} />
</div>
<Meter.Track class={style.meter__track}>
<Meter.Fill class={style.meter__fill} />
</Meter.Track>
</Meter>
);
}
5 changes: 5 additions & 0 deletions apps/docs/src/routes/docs/core.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,11 @@ const CORE_NAV_SECTIONS: NavSection[] = [
title: "Menubar",
href: "/docs/core/components/menubar",
},
{
title: "Meter",
href: "/docs/core/components/meter",
status: "new",
},
{
title: "Navigation Menu",
href: "/docs/core/components/navigation-menu",
Expand Down
197 changes: 197 additions & 0 deletions apps/docs/src/routes/docs/core/components/meter.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
import { Preview, TabsSnippets } from "../../../../components";
import {
BasicExample,
CustomValueLabelExample,
CustomValueScaleExample,
} from "../../../../examples/meter";

# Meter

Displays numeric value that varies within a defined range

## Import

```ts
import { Meter } from "@kobalte/core/meter";
// or
import { Root, Label, ... } from "@kobalte/core/meter";
// or (deprecated)
import { Meter } from "@kobalte/core";
```

## Features

- Exposed to assistive technology as a meter via ARIA.
- Labeling support for accessibility.
- Internationalized number formatting as a percentage or value.

## Anatomy

The meter consists of:

- **Meter:** The root container for a meter.
- **Meter.Label:** An accessible label that gives the user information on the meter.
- **Meter.ValueLabel:** The accessible label text representing the current value in a human-readable format.
- **Meter.Track:** The component that visually represents the meter track.
- **Meter.Fill:** The component that visually represents the meter value.

```tsx
<Meter>
<Meter.Label />
<Meter.ValueLabel />
<Meter.Track>
<Meter.Fill />
</Meter.Track>
</Meter>
```

## Example

<Preview>
<BasicExample />
</Preview>

<TabsSnippets>
<TabsSnippets.List>
<TabsSnippets.Trigger value="index.tsx">index.tsx</TabsSnippets.Trigger>
<TabsSnippets.Trigger value="style.css">style.css</TabsSnippets.Trigger>
</TabsSnippets.List>
{/* <!-- prettier-ignore-start -->*/}
<TabsSnippets.Content value="index.tsx">
```tsx
import { Meter } from "@kobalte/core/meter";
import "./style.css";

function App() {
return (
<Meter value={80} class="meter">
<div class="meter__label-container">
<Meter.Label class="meter__label">Battery Level:</Meter.Label>
<Meter.ValueLabel class="meter__value-label" />
</div>
<Meter.Track class="meter__track">
<Meter.Fill class="meter__fill" />
</Meter.Track>
</Meter>
);
}
```

</TabsSnippets.Content>
<TabsSnippets.Content value="style.css">
```css
.meter {
display: flex;
flex-direction: column;
gap: 2px;
width: 300px;
}

.meter__label-container {
display: flex;
justify-content: space-between;
}

.meter__label,
.meter__value-label {
color: hsl(240 4% 16%);
font-size: 14px;
}

.meter__track {
height: 10px;
background-color: hsl(240 6% 90%);
}

.meter__fill {
background-color: hsl(200 98% 39%);
height: 100%;
width: var(--kb-meter-fill-width);
transition: width 250ms linear;
}

```

</TabsSnippets.Content>
{/* <!-- prettier-ignore-end -->*/}
</TabsSnippets>

## Usage

### Custom value scale

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.

<Preview>
<CustomValueScaleExample />
</Preview>

```tsx {0}
<Meter value={100} minValue={0} maxValue={250} class="meter">
<div class="meter__label-container">
<Meter.Label class="meter__label">Disk Space Usage:</Meter.Label>
<Meter.ValueLabel class="meter__value-label" />
</div>
<Meter.Track class="meter__track">
<Meter.Fill class="meter__fill" />
</Meter.Track>
</Meter>
```

### Custom value label

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.

<Preview>
<CustomValueLabelExample />
</Preview>

```tsx {4}
<Meter
value={3}
minValue={0}
maxValue={10}
getValueLabel={({ value, max }) => `${value} of ${max} tasks completed`}
class="meter"
>
<div class="meter__label-container">
<Meter.Label class="meter__label">Processing...</Meter.Label>
<Meter.ValueLabel class="meter__value-label" />
</div>
<Meter.Track class="meter__track">
<Meter.Fill class="meter__fill" />
</Meter.Track>
</Meter>
```

### Meter fill width

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.

## API Reference

### Meter

`Meter` is equivalent to the `Root` import from `@kobalte/core/meter` (and deprecated `Meter.Root`).

| Prop | Description |
| :------------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| value | `number` <br/> The meter value. |
| minValue | `number` <br/> The minimum meter value. |
| maxValue | `number` <br/> The maximum meter value. |
| 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. |

| Data attribute | Description |
| :------------- | :---------- |

`Meter.Label`, `Meter.ValueLabel`, `Meter.Track` and `Meter.Fill` shares the same data-attributes.

## Rendered elements

| Component | Default rendered element |
| :----------------- | :----------------------- |
| `Meter` | `div` |
| `Meter.Label` | `span` |
| `Meter.ValueLabel` | `div` |
| `Meter.Track` | `div` |
| `Meter.Fill` | `div` |
1 change: 1 addition & 0 deletions packages/core/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export * as Image from "./image";
export * as Link from "./link";
export * as Listbox from "./listbox";
export * as Menubar from "./menubar";
export * as Meter from "./meter";
export * as NumberField from "./number-field";
export * as Pagination from "./pagination";
export * as Popover from "./popover";
Expand Down
66 changes: 66 additions & 0 deletions packages/core/src/meter/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import {
MeterFill as Fill,
type MeterFillCommonProps,
type MeterFillOptions,
type MeterFillProps,
type MeterFillRenderProps,
} from "./meter-fill";
import {
MeterLabel as Label,
type MeterLabelCommonProps,
type MeterLabelOptions,
type MeterLabelProps,
type MeterLabelRenderProps,
} from "./meter-label";
import {
type MeterRootCommonProps,
type MeterRootOptions,
type MeterRootProps,
type MeterRootRenderProps,
MeterRoot as Root,
} from "./meter-root";
import {
type MeterTrackCommonProps,
type MeterTrackOptions,
type MeterTrackProps,
type MeterTrackRenderProps,
MeterTrack as Track,
} from "./meter-track";
import {
type MeterValueLabelCommonProps,
type MeterValueLabelOptions,
type MeterValueLabelProps,
type MeterValueLabelRenderProps,
MeterValueLabel as ValueLabel,
} from "./meter-value-label";

export type {
MeterFillOptions,
MeterFillCommonProps,
MeterFillRenderProps,
MeterFillProps,
MeterLabelOptions,
MeterLabelCommonProps,
MeterLabelRenderProps,
MeterLabelProps,
MeterRootOptions,
MeterRootCommonProps,
MeterRootRenderProps,
MeterRootProps,
MeterTrackOptions,
MeterTrackCommonProps,
MeterTrackRenderProps,
MeterTrackProps,
MeterValueLabelOptions,
MeterValueLabelCommonProps,
MeterValueLabelRenderProps,
MeterValueLabelProps,
};
export { Fill, Label, Root, Track, ValueLabel };

export const Meter = Object.assign(Root, {
Fill,
Label,
Track,
ValueLabel,
});
Loading
Loading