diff --git a/apps/docs/src/examples/meter.module.css b/apps/docs/src/examples/meter.module.css new file mode 100644 index 00000000..6910f097 --- /dev/null +++ b/apps/docs/src/examples/meter.module.css @@ -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%); +} diff --git a/apps/docs/src/examples/meter.tsx b/apps/docs/src/examples/meter.tsx new file mode 100644 index 00000000..60dd74b9 --- /dev/null +++ b/apps/docs/src/examples/meter.tsx @@ -0,0 +1,51 @@ +import { Meter } from "@kobalte/core/meter"; + +import style from "./meter.module.css"; + +export function BasicExample() { + return ( + +
+ Batter Level: + +
+ + + +
+ ); +} + +export function CustomValueScaleExample() { + return ( + +
+ Disk Space Usage: + +
+ + + +
+ ); +} + +export function CustomValueLabelExample() { + return ( + `${value} of ${max} tasks completed`} + class={style.meter} + > +
+ Processing... + +
+ + + +
+ ); +} diff --git a/apps/docs/src/routes/docs/core.tsx b/apps/docs/src/routes/docs/core.tsx index d4b1efb2..1a30f001 100644 --- a/apps/docs/src/routes/docs/core.tsx +++ b/apps/docs/src/routes/docs/core.tsx @@ -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", diff --git a/apps/docs/src/routes/docs/core/components/meter.mdx b/apps/docs/src/routes/docs/core/components/meter.mdx new file mode 100644 index 00000000..b5e9310a --- /dev/null +++ b/apps/docs/src/routes/docs/core/components/meter.mdx @@ -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 + + + + + + + +``` + +## Example + + + + + + + + index.tsx + style.css + + {/* */} + + ```tsx + import { Meter } from "@kobalte/core/meter"; + import "./style.css"; + + function App() { + return ( + +
+ Battery Level: + +
+ + + +
+ ); + } + ``` + +
+ + ```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; + } + + ``` + + + {/* */} +
+ +## 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. + + + + + +```tsx {0} + +
+ Disk Space Usage: + +
+ + + +
+``` + +### 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. + + + + + +```tsx {4} + `${value} of ${max} tasks completed`} + class="meter" +> +
+ Processing... + +
+ + + +
+``` + +### 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`
The meter value. | +| minValue | `number`
The minimum meter value. | +| maxValue | `number`
The maximum meter value. | +| getValueLabel | `(params: { value: number; min: number; max: number }) => string`
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` | diff --git a/packages/core/src/index.tsx b/packages/core/src/index.tsx index 4a72a633..e3f43f03 100644 --- a/packages/core/src/index.tsx +++ b/packages/core/src/index.tsx @@ -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"; diff --git a/packages/core/src/meter/index.tsx b/packages/core/src/meter/index.tsx new file mode 100644 index 00000000..64037c15 --- /dev/null +++ b/packages/core/src/meter/index.tsx @@ -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, +}); diff --git a/packages/core/src/meter/meter-context.tsx b/packages/core/src/meter/meter-context.tsx new file mode 100644 index 00000000..794eccfe --- /dev/null +++ b/packages/core/src/meter/meter-context.tsx @@ -0,0 +1,28 @@ +import { type Accessor, createContext, useContext } from "solid-js"; + +export interface MeterDataSet {} + +export interface MeterContextValue { + dataset: Accessor; + value: Accessor; + valuePercent: Accessor; + valueLabel: Accessor; + meterFillWidth: Accessor; + labelId: Accessor; + generateId: (part: string) => string; + registerLabelId: (id: string) => () => void; +} + +export const MeterContext = createContext(); + +export function useMeterContext() { + const context = useContext(MeterContext); + + if (context === undefined) { + throw new Error( + "[kobalte]: `useMeterContext` must be used within a `Meter.Root` component", + ); + } + + return context; +} diff --git a/packages/core/src/meter/meter-fill.tsx b/packages/core/src/meter/meter-fill.tsx new file mode 100644 index 00000000..e69f74bf --- /dev/null +++ b/packages/core/src/meter/meter-fill.tsx @@ -0,0 +1,49 @@ +import { type JSX, type ValidComponent, splitProps } from "solid-js"; + +import { combineStyle } from "@solid-primitives/props"; +import { + type ElementOf, + Polymorphic, + type PolymorphicProps, +} from "../polymorphic"; +import { type MeterDataSet, useMeterContext } from "./meter-context"; + +export interface MeterFillOptions {} + +export interface MeterFillCommonProps { + style?: JSX.CSSProperties | string; +} + +export interface MeterFillRenderProps + extends MeterFillCommonProps, + MeterDataSet {} + +export type MeterFillProps< + T extends ValidComponent | HTMLElement = HTMLElement, +> = MeterFillOptions & Partial>>; + +/** + * The component that visually represents the meter value. + * Used to visually show the fill of `Meter.Track`. + */ +export function MeterFill( + props: PolymorphicProps>, +) { + const context = useMeterContext(); + + const [local, others] = splitProps(props as MeterFillProps, ["style"]); + + return ( + + as="div" + style={combineStyle( + { + "--kb-meter-fill-width": context.meterFillWidth(), + }, + local.style, + )} + {...context.dataset()} + {...others} + /> + ); +} diff --git a/packages/core/src/meter/meter-label.tsx b/packages/core/src/meter/meter-label.tsx new file mode 100644 index 00000000..5cd951ee --- /dev/null +++ b/packages/core/src/meter/meter-label.tsx @@ -0,0 +1,57 @@ +import { mergeDefaultProps } from "@kobalte/utils"; +import { + type ValidComponent, + createEffect, + onCleanup, + splitProps, +} from "solid-js"; + +import { + type ElementOf, + Polymorphic, + type PolymorphicProps, +} from "../polymorphic"; +import { type MeterDataSet, useMeterContext } from "./meter-context"; + +export interface MeterLabelOptions {} + +export interface MeterLabelCommonProps { + id: string; +} + +export interface MeterLabelRenderProps + extends MeterLabelCommonProps, + MeterDataSet {} + +export type MeterLabelProps< + T extends ValidComponent | HTMLElement = HTMLElement, +> = MeterLabelOptions & Partial>>; + +/** + * An accessible label that gives the user information on the meter. + */ +export function MeterLabel( + props: PolymorphicProps>, +) { + const context = useMeterContext(); + + const mergedProps = mergeDefaultProps( + { + id: context.generateId("label"), + }, + props as MeterLabelProps, + ); + + const [local, others] = splitProps(mergedProps, ["id"]); + + createEffect(() => onCleanup(context.registerLabelId(local.id))); + + return ( + + as="span" + id={local.id} + {...context.dataset()} + {...others} + /> + ); +} diff --git a/packages/core/src/meter/meter-root.tsx b/packages/core/src/meter/meter-root.tsx new file mode 100644 index 00000000..8b64d7d8 --- /dev/null +++ b/packages/core/src/meter/meter-root.tsx @@ -0,0 +1,175 @@ +/* + * Portions of this file are based on code from react-spectrum. + * Apache License Version 2.0, Copyright 2020 Adobe. + * + * Credits to the React Spectrum team: + * https://github.com/adobe/react-spectrum/blob/main/packages/%40react-aria/meter/src/useMeter.ts + */ + +import { clamp, createGenerateId, mergeDefaultProps } from "@kobalte/utils"; +import { + type Accessor, + type ValidComponent, + createMemo, + createSignal, + createUniqueId, + splitProps, +} from "solid-js"; + +import { createNumberFormatter } from "../i18n"; +import { + type ElementOf, + Polymorphic, + type PolymorphicProps, +} from "../polymorphic"; +import { createRegisterId } from "../primitives"; +import { + MeterContext, + type MeterContextValue, + type MeterDataSet, +} from "./meter-context"; + +interface GetValueLabelParams { + value: number; + min: number; + max: number; +} +export interface MeterRootOptions { + /** + * The meter value. + * @default 0 + */ + value?: number; + + /** + * The minimum meter value. + * @default 0 + */ + minValue?: number; + + /** + * The maximum meter value. + * @default 100 + */ + maxValue?: number; + + /** + * 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. + */ + getValueLabel?: (params: GetValueLabelParams) => string; +} + +export interface MeterRootCommonProps { + id: string; + role: string; + "aria-valuenow": number | undefined; + "aria-valuemin": number; + "aria-valuemax": number; + "aria-valuetext": string | undefined; + "aria-labelledby": string | undefined; +} + +export interface MeterRootRenderProps + extends MeterRootCommonProps, + MeterDataSet {} + +export type MeterRootProps< + T extends ValidComponent | HTMLElement = HTMLElement, +> = MeterRootOptions & Partial>>; + +/** + * Meter displays numeric value that varies within a defined range. + */ +export function MeterRoot( + props: PolymorphicProps>, +) { + const defaultId = `meter-${createUniqueId()}`; + + const mergedProps = mergeDefaultProps( + { + id: defaultId, + value: 0, + minValue: 0, + maxValue: 100, + role: "meter", + indeterminate: false, + }, + props as MeterRootProps, + ); + + const [local, others] = splitProps(mergedProps, [ + "value", + "minValue", + "maxValue", + "getValueLabel", + "role", + "aria-valuetext", + "aria-labelledby", + "aria-valuemax", + "aria-valuemin", + "aria-valuenow", + "indeterminate", + ]); + + const [labelId, setLabelId] = createSignal(); + + const defaultFormatter = createNumberFormatter(() => ({ style: "percent" })); + + const value = () => { + return clamp(local.value!, local.minValue!, local.maxValue!); + }; + + const valuePercent = () => { + return (value() - local.minValue!) / (local.maxValue! - local.minValue!); + }; + + const valueLabel = () => { + if (local.indeterminate) { + return undefined; + } + if (local.getValueLabel) { + return local.getValueLabel({ + value: value(), + min: local.minValue!, + max: local.maxValue!, + }); + } + + return defaultFormatter().format(valuePercent()); + }; + + const meterFillWidth = () => { + return `${Math.round(valuePercent() * 100)}%`; + }; + + const dataset: Accessor = createMemo(() => { + return {}; + }); + const context: MeterContextValue = { + dataset, + value, + valuePercent, + valueLabel, + labelId, + meterFillWidth, + generateId: createGenerateId(() => others.id!), + registerLabelId: createRegisterId(setLabelId), + }; + + return ( + + + as="div" + role={local.role || "meter"} + aria-valuenow={local.indeterminate ? undefined : value()} + aria-valuemin={local.minValue} + aria-valuemax={local.maxValue} + aria-valuetext={valueLabel()} + aria-labelledby={labelId()} + {...dataset()} + {...others} + /> + + ); +} diff --git a/packages/core/src/meter/meter-track.tsx b/packages/core/src/meter/meter-track.tsx new file mode 100644 index 00000000..fd3a4c4a --- /dev/null +++ b/packages/core/src/meter/meter-track.tsx @@ -0,0 +1,37 @@ +import type { ValidComponent } from "solid-js"; +import { + type ElementOf, + Polymorphic, + type PolymorphicProps, +} from "../polymorphic"; +import { type MeterDataSet, useMeterContext } from "./meter-context"; + +export interface MeterTrackOptions {} + +export interface MeterTrackCommonProps {} + +export interface MeterTrackRenderProps + extends MeterTrackCommonProps, + MeterDataSet {} + +export type MeterTrackProps< + T extends ValidComponent | HTMLElement = HTMLElement, +> = MeterTrackOptions & Partial>>; + +/** + * The component that visually represents the meter track. + * Act as a container for `Meter.Fill`. + */ +export function MeterTrack( + props: PolymorphicProps>, +) { + const context = useMeterContext(); + + return ( + + as="div" + {...context.dataset()} + {...(props as MeterTrackProps)} + /> + ); +} diff --git a/packages/core/src/meter/meter-value-label.tsx b/packages/core/src/meter/meter-value-label.tsx new file mode 100644 index 00000000..84e639b2 --- /dev/null +++ b/packages/core/src/meter/meter-value-label.tsx @@ -0,0 +1,43 @@ +import type { JSX, ValidComponent } from "solid-js"; + +import { + type ElementOf, + Polymorphic, + type PolymorphicProps, +} from "../polymorphic"; +import { type MeterDataSet, useMeterContext } from "./meter-context"; + +export interface MeterValueLabelOptions {} + +export interface MeterValueLabelCommonProps< + T extends HTMLElement = HTMLElement, +> {} + +export interface MeterValueLabelRenderProps + extends MeterValueLabelCommonProps, + MeterDataSet { + children: JSX.Element; +} + +export type MeterValueLabelProps< + T extends ValidComponent | HTMLElement = HTMLElement, +> = MeterValueLabelOptions & Partial>>; + +/** + * The accessible label text representing the current value in a human-readable format. + */ +export function MeterValueLabel( + props: PolymorphicProps>, +) { + const context = useMeterContext(); + + return ( + + as="div" + {...context.dataset()} + {...(props as MeterValueLabelProps)} + > + {context.valueLabel()} + + ); +} diff --git a/packages/core/src/meter/meter.test.tsx b/packages/core/src/meter/meter.test.tsx new file mode 100644 index 00000000..ee70896b --- /dev/null +++ b/packages/core/src/meter/meter.test.tsx @@ -0,0 +1,132 @@ +/* + * Portions of this file are based on code from react-spectrum. + * Apache License Version 2.0, Copyright 2020 Adobe. + * + * Credits to the React Spectrum team: + * https://github.com/adobe/react-spectrum/blob/main/packages/%40react-aria/meter/src/useMeter.ts + * https://github.com/adobe/react-spectrum/blob/main/packages/%40react-spectrum/meter/test/Meter.test.js + +*/ + +import { render } from "@solidjs/testing-library"; + +import * as Meter from "."; + +describe("Meter", () => { + it("handles defaults", () => { + const { getByRole, getByTestId } = render(() => ( + + Meter + + + + + + )); + + const meter = getByRole("meter"); + expect(meter).toHaveAttribute("aria-valuemin", "0"); + expect(meter).toHaveAttribute("aria-valuemax", "100"); + expect(meter).toHaveAttribute("aria-valuenow", "0"); + expect(meter).toHaveAttribute("aria-valuetext", "0%"); + + const valueLabel = getByTestId("value-label"); + expect(valueLabel).toHaveTextContent("0%"); + + const labelId = meter.getAttribute("aria-labelledby"); + expect(labelId).toBeDefined(); + + const label = document.getElementById(labelId!); + expect(label).toHaveTextContent("Meter"); + }); + + it("supports custom value label", () => { + const { getByRole, getByTestId } = render(() => ( + `${value} of ${max} completed`} + > + Meter + + + + + + )); + + const meter = getByRole("meter"); + expect(meter).toHaveAttribute("aria-valuetext", "3 of 10 completed"); + + const valueLabel = getByTestId("value-label"); + expect(valueLabel).toHaveTextContent("3 of 10 completed"); + }); + + it("should update all fields by value", () => { + const { getByRole } = render(() => ( + + Meter + + + + + + )); + + const meter = getByRole("meter"); + + expect(meter).toHaveAttribute("aria-valuemin", "0"); + expect(meter).toHaveAttribute("aria-valuemax", "100"); + expect(meter).toHaveAttribute("aria-valuenow", "30"); + expect(meter).toHaveAttribute("aria-valuetext", "30%"); + }); + + it("should clamps values to 'minValue'", () => { + const { getByRole } = render(() => ( + + Meter + + + + + + )); + + const meter = getByRole("meter"); + expect(meter).toHaveAttribute("aria-valuenow", "0"); + expect(meter).toHaveAttribute("aria-valuetext", "0%"); + }); + + it("should clamps values to 'maxValue'", () => { + const { getByRole } = render(() => ( + + Meter + + + + + + )); + + const meter = getByRole("meter"); + expect(meter).toHaveAttribute("aria-valuenow", "100"); + expect(meter).toHaveAttribute("aria-valuetext", "100%"); + }); + + it("supports negative values", () => { + const { getByRole } = render(() => ( + + Meter + + + + + + )); + + const meter = getByRole("meter"); + expect(meter).toHaveAttribute("aria-valuenow", "0"); + expect(meter).toHaveAttribute("aria-valuetext", "50%"); + }); +}); diff --git a/packages/core/src/progress/progress-context.tsx b/packages/core/src/progress/progress-context.tsx index e9faceda..bbaa168f 100644 --- a/packages/core/src/progress/progress-context.tsx +++ b/packages/core/src/progress/progress-context.tsx @@ -1,19 +1,15 @@ import { type Accessor, createContext, useContext } from "solid-js"; +import type { MeterContextValue, MeterDataSet } from "../meter/meter-context"; -export interface ProgressDataSet { +export interface ProgressDataSet extends MeterDataSet { "data-progress": "loading" | "complete" | undefined; "data-indeterminate": string | undefined; } -export interface ProgressContextValue { +export interface ProgressContextValue + extends Omit { dataset: Accessor; - value: Accessor; - valuePercent: Accessor; - valueLabel: Accessor; progressFillWidth: Accessor; - labelId: Accessor; - generateId: (part: string) => string; - registerLabelId: (id: string) => () => void; } export const ProgressContext = createContext(); diff --git a/packages/core/src/progress/progress-fill.tsx b/packages/core/src/progress/progress-fill.tsx index ca029122..7b0f79d4 100644 --- a/packages/core/src/progress/progress-fill.tsx +++ b/packages/core/src/progress/progress-fill.tsx @@ -1,22 +1,24 @@ -import { type JSX, type ValidComponent, splitProps } from "solid-js"; +import { type Component, type ValidComponent, splitProps } from "solid-js"; import { combineStyle } from "@solid-primitives/props"; import { - type ElementOf, - Polymorphic, - type PolymorphicProps, -} from "../polymorphic"; + Meter, + type MeterFillCommonProps, + type MeterFillOptions, + type MeterFillRenderProps, +} from "../meter"; +import type { ElementOf, PolymorphicProps } from "../polymorphic"; import { type ProgressDataSet, useProgressContext } from "./progress-context"; -export interface ProgressFillOptions {} +export interface ProgressFillOptions extends MeterFillOptions {} -export interface ProgressFillCommonProps { - style?: JSX.CSSProperties | string; -} +export interface ProgressFillCommonProps + extends MeterFillCommonProps {} export interface ProgressFillRenderProps extends ProgressFillCommonProps, - ProgressDataSet {} + ProgressDataSet, + MeterFillRenderProps {} export type ProgressFillProps< T extends ValidComponent | HTMLElement = HTMLElement, @@ -34,8 +36,9 @@ export function ProgressFill( const [local, others] = splitProps(props as ProgressFillProps, ["style"]); return ( - - as="div" + > + > style={combineStyle( { "--kb-progress-fill-width": context.progressFillWidth(), diff --git a/packages/core/src/progress/progress-label.tsx b/packages/core/src/progress/progress-label.tsx index 541cbdee..e6016cc2 100644 --- a/packages/core/src/progress/progress-label.tsx +++ b/packages/core/src/progress/progress-label.tsx @@ -1,5 +1,6 @@ import { mergeDefaultProps } from "@kobalte/utils"; import { + type Component, type ValidComponent, createEffect, onCleanup, @@ -7,20 +8,22 @@ import { } from "solid-js"; import { - type ElementOf, - Polymorphic, - type PolymorphicProps, -} from "../polymorphic"; + Meter, + type MeterLabelCommonProps, + type MeterLabelOptions, + type MeterLabelRenderProps, +} from "../meter"; +import type { ElementOf, PolymorphicProps } from "../polymorphic"; import { type ProgressDataSet, useProgressContext } from "./progress-context"; -export interface ProgressLabelOptions {} +export interface ProgressLabelOptions extends MeterLabelOptions {} -export interface ProgressLabelCommonProps { - id: string; -} +export interface ProgressLabelCommonProps + extends MeterLabelCommonProps {} export interface ProgressLabelRenderProps - extends ProgressLabelCommonProps, + extends MeterLabelRenderProps, + ProgressLabelCommonProps, ProgressDataSet {} export type ProgressLabelProps< @@ -41,14 +44,14 @@ export function ProgressLabel( }, props as ProgressLabelProps, ); - const [local, others] = splitProps(mergedProps, ["id"]); createEffect(() => onCleanup(context.registerLabelId(local.id))); return ( - - as="span" + > + > id={local.id} {...context.dataset()} {...others} diff --git a/packages/core/src/progress/progress-root.tsx b/packages/core/src/progress/progress-root.tsx index b7dfaf62..22ae0a5d 100644 --- a/packages/core/src/progress/progress-root.tsx +++ b/packages/core/src/progress/progress-root.tsx @@ -9,6 +9,7 @@ import { clamp, createGenerateId, mergeDefaultProps } from "@kobalte/utils"; import { type Accessor, + type Component, type ValidComponent, createMemo, createSignal, @@ -18,10 +19,12 @@ import { import { createNumberFormatter } from "../i18n"; import { - type ElementOf, - Polymorphic, - type PolymorphicProps, -} from "../polymorphic"; + Meter, + type MeterRootCommonProps, + type MeterRootOptions, + type MeterRootRenderProps, +} from "../meter"; +import type { ElementOf, PolymorphicProps } from "../polymorphic"; import { createRegisterId } from "../primitives"; import { ProgressContext, @@ -29,56 +32,21 @@ import { type ProgressDataSet, } from "./progress-context"; -interface GetValueLabelParams { - value: number; - min: number; - max: number; -} - -export interface ProgressRootOptions { - /** - * The progress value. - * @default 0 - */ - value?: number; - - /** - * The minimum progress value. - * @default 0 - */ - minValue?: number; - - /** - * The maximum progress value. - * @default 100 - */ - maxValue?: number; - +export interface ProgressRootOptions + extends Omit { /** Whether the progress is in an indeterminate state. */ indeterminate?: boolean; - - /** - * 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. - */ - getValueLabel?: (params: GetValueLabelParams) => string; } -export interface ProgressRootCommonProps { - id: string; -} +export interface ProgressRootCommonProps + extends MeterRootCommonProps {} export interface ProgressRootRenderProps - extends ProgressRootCommonProps, + extends Omit, + ProgressRootCommonProps, ProgressDataSet { role: "progressbar"; - "aria-valuenow": number | undefined; - "aria-valuemin": number; - "aria-valuemax": number; - "aria-valuetext": string | undefined; - "aria-labelledby": string | undefined; } - export type ProgressRootProps< T extends ValidComponent | HTMLElement = HTMLElement, > = ProgressRootOptions & Partial>>; @@ -169,16 +137,13 @@ export function ProgressRoot( return ( - - as="div" + > + > role="progressbar" - aria-valuenow={local.indeterminate ? undefined : value()} - aria-valuemin={local.minValue} - aria-valuemax={local.maxValue} - aria-valuetext={valueLabel()} - aria-labelledby={labelId()} + indeterminate={local.indeterminate || false} {...dataset()} - {...others} + {...mergedProps} /> ); diff --git a/packages/core/src/progress/progress-track.tsx b/packages/core/src/progress/progress-track.tsx index c4cd384f..24ee2f10 100644 --- a/packages/core/src/progress/progress-track.tsx +++ b/packages/core/src/progress/progress-track.tsx @@ -1,19 +1,21 @@ -import type { ValidComponent } from "solid-js"; +import type { Component, ValidComponent } from "solid-js"; import { - type ElementOf, - Polymorphic, - type PolymorphicProps, -} from "../polymorphic"; + Meter, + type MeterTrackCommonProps, + type MeterTrackOptions, + type MeterTrackRenderProps, +} from "../meter"; +import type { ElementOf, PolymorphicProps } from "../polymorphic"; import { type ProgressDataSet, useProgressContext } from "./progress-context"; -export interface ProgressTrackOptions {} +export interface ProgressTrackOptions extends MeterTrackOptions {} -export interface ProgressTrackCommonProps< - T extends HTMLElement = HTMLElement, -> {} +export interface ProgressTrackCommonProps + extends MeterTrackCommonProps {} export interface ProgressTrackRenderProps - extends ProgressTrackCommonProps, + extends MeterTrackRenderProps, + ProgressTrackCommonProps, ProgressDataSet {} export type ProgressTrackProps< @@ -30,8 +32,9 @@ export function ProgressTrack( const context = useProgressContext(); return ( - - as="div" + > + > {...context.dataset()} {...(props as ProgressTrackProps)} /> diff --git a/packages/core/src/progress/progress-value-label.tsx b/packages/core/src/progress/progress-value-label.tsx index f8b3a918..b7a1ab3c 100644 --- a/packages/core/src/progress/progress-value-label.tsx +++ b/packages/core/src/progress/progress-value-label.tsx @@ -1,23 +1,24 @@ -import type { JSX, ValidComponent } from "solid-js"; +import type { Component, ValidComponent } from "solid-js"; import { - type ElementOf, - Polymorphic, - type PolymorphicProps, -} from "../polymorphic"; + Meter, + type MeterValueLabelCommonProps, + type MeterValueLabelOptions, + type MeterValueLabelRenderProps, +} from "../meter"; +import type { ElementOf, PolymorphicProps } from "../polymorphic"; import { type ProgressDataSet, useProgressContext } from "./progress-context"; -export interface ProgressValueLabelOptions {} +export interface ProgressValueLabelOptions extends MeterValueLabelOptions {} export interface ProgressValueLabelCommonProps< T extends HTMLElement = HTMLElement, -> {} +> extends MeterValueLabelCommonProps {} export interface ProgressValueLabelRenderProps - extends ProgressValueLabelCommonProps, - ProgressDataSet { - children: JSX.Element; -} + extends MeterValueLabelRenderProps, + ProgressValueLabelCommonProps, + ProgressDataSet {} export type ProgressValueLabelProps< T extends ValidComponent | HTMLElement = HTMLElement, @@ -33,12 +34,13 @@ export function ProgressValueLabel( const context = useProgressContext(); return ( - - as="div" + + > + > {...context.dataset()} {...(props as ProgressValueLabelProps)} - > - {context.valueLabel()} - + /> ); }