From 52021615b8cf52c6d11eb27575c3250f7ab16fe0 Mon Sep 17 00:00:00 2001 From: jer3m01 Date: Sun, 27 Jul 2025 23:42:59 +0200 Subject: [PATCH 01/12] refactor: rename component --- .../docs/src/examples/rating-group.module.css | 62 --- apps/docs/src/examples/rating-group.tsx | 235 ---------- apps/docs/src/routes/docs/core.tsx | 4 +- .../docs/core/components/color-area.mdx | 2 +- .../core/components/color-channel-field.mdx | 2 +- .../docs/core/components/color-slider.mdx | 10 +- .../docs/core/components/color-swatch.mdx | 6 +- .../docs/core/components/color-wheel.mdx | 2 +- .../docs/core/components/rating-group.mdx | 423 ------------------ packages/core/src/index.tsx | 2 - packages/core/src/rating-group/index.tsx | 131 ------ .../rating-group-item-description.tsx | 52 --- .../src/rating-group/rating-group-label.tsx | 29 -- packages/core/src/rating/index.tsx | 131 ++++++ .../rating-context.tsx} | 10 +- .../rating-control.tsx} | 26 +- .../rating-hidden-input.tsx} | 8 +- .../rating-item-context.tsx} | 20 +- .../rating-item-control.tsx} | 44 +- .../src/rating/rating-item-description.tsx | 52 +++ .../rating-item-label.tsx} | 32 +- .../rating-item.tsx} | 90 ++-- packages/core/src/rating/rating-label.tsx | 29 ++ .../rating-root.tsx} | 34 +- .../src/{rating-group => rating}/utils.ts | 0 25 files changed, 357 insertions(+), 1079 deletions(-) delete mode 100644 apps/docs/src/examples/rating-group.module.css delete mode 100644 apps/docs/src/examples/rating-group.tsx delete mode 100644 apps/docs/src/routes/docs/core/components/rating-group.mdx delete mode 100644 packages/core/src/rating-group/index.tsx delete mode 100644 packages/core/src/rating-group/rating-group-item-description.tsx delete mode 100644 packages/core/src/rating-group/rating-group-label.tsx create mode 100644 packages/core/src/rating/index.tsx rename packages/core/src/{rating-group/rating-group-context.tsx => rating/rating-context.tsx} (69%) rename packages/core/src/{rating-group/rating-group-control.tsx => rating/rating-control.tsx} (65%) rename packages/core/src/{rating-group/rating-group-hidden-input.tsx => rating/rating-hidden-input.tsx} (66%) rename packages/core/src/{rating-group/rating-group-item-context.tsx => rating/rating-item-context.tsx} (52%) rename packages/core/src/{rating-group/rating-group-item-control.tsx => rating/rating-item-control.tsx} (52%) create mode 100644 packages/core/src/rating/rating-item-description.tsx rename packages/core/src/{rating-group/rating-group-item-label.tsx => rating/rating-item-label.tsx} (55%) rename packages/core/src/{rating-group/rating-group-item.tsx => rating/rating-item.tsx} (71%) create mode 100644 packages/core/src/rating/rating-label.tsx rename packages/core/src/{rating-group/rating-group-root.tsx => rating/rating-root.tsx} (86%) rename packages/core/src/{rating-group => rating}/utils.ts (100%) diff --git a/apps/docs/src/examples/rating-group.module.css b/apps/docs/src/examples/rating-group.module.css deleted file mode 100644 index 30b84be41..000000000 --- a/apps/docs/src/examples/rating-group.module.css +++ /dev/null @@ -1,62 +0,0 @@ -.rating-group { - display: flex; - flex-direction: column; - gap: 8px; -} - -.rating-group__label { - color: hsl(240 6% 10%); - font-size: 14px; - font-weight: 500; - user-select: none; -} - -.rating-group__description { - color: hsl(240 5% 26%); - font-size: 12px; - user-select: none; -} - -.rating-group__error-message { - color: hsl(0 72% 51%); - font-size: 12px; - user-select: none; -} - -.rating-group__control { - display: flex; - gap: 4px; -} - -.rating-group-item { - cursor: pointer; - fill: hsl(240 6% 90%); -} - -.rating-group-item:focus { - outline: none; -} - -[data-kb-theme="dark"] .rating-group-item { - fill: hsl(240 5% 26%); -} - -.rating-group-item[data-highlighted] { - fill: hsl(200 98% 39%); -} - -.half-star-icon > path + path { - fill: hsl(240 6% 90%); -} - -[data-kb-theme="dark"] .half-star-icon > path + path { - fill: hsl(240 5% 26%); -} - -[data-kb-theme="dark"] .rating-group__label { - color: hsl(240 5% 84%); -} - -[data-kb-theme="dark"] .rating-group__description { - color: hsl(240 5% 65%); -} diff --git a/apps/docs/src/examples/rating-group.tsx b/apps/docs/src/examples/rating-group.tsx deleted file mode 100644 index c7d7189c5..000000000 --- a/apps/docs/src/examples/rating-group.tsx +++ /dev/null @@ -1,235 +0,0 @@ -import { Index, createSignal } from "solid-js"; -import { RatingGroup } from "../../../../packages/core/src/rating-group"; - -import style from "./rating-group.module.css"; - -export function BasicExample() { - return ( - - - Rate Us: - - - - {(_) => ( - - - - - - )} - - - - ); -} - -export function DefaultValueExample() { - return ( - - - - {(_) => ( - - - - - - )} - - - - ); -} - -export function ControlledExample() { - const [value, setValue] = createSignal(0); - - return ( - <> - - - - {(_) => ( - - - - - - )} - - - -

Your rating is: {value()}/5

- - ); -} - -export function HalfRatingsExample() { - return ( - - - - {(_) => ( - - - {(state) => (state.half() ? : )} - - - )} - - - - ); -} - -export function DescriptionExample() { - return ( - - - Rate Us: - - - - {(_) => ( - - - - - - )} - - - - Rate your experience with us. - - - ); -} - -export function ErrorMessageExample() { - const [value, setValue] = createSignal(0); - - return ( - - - Rate Us: - - - - {(_) => ( - - - - - - )} - - - - Please select a rating between 1 and 5. - - - ); -} - -export function HTMLFormExample() { - let formRef: HTMLFormElement | undefined; - - const onSubmit = (e: SubmitEvent) => { - e.preventDefault(); - e.stopPropagation(); - - const formData = new FormData(formRef); - - alert(JSON.stringify(Object.fromEntries(formData), null, 2)); - }; - - return ( -
- - - - {(_) => ( - - - - - - )} - - - - -
- - -
-
- ); -} - -function StarIcon() { - return ( - - Star Icon - - - ); -} - -function StarHalfIcon() { - return ( - - Half Star Icon - - - - ); -} diff --git a/apps/docs/src/routes/docs/core.tsx b/apps/docs/src/routes/docs/core.tsx index ea5d95a55..49f7b5863 100644 --- a/apps/docs/src/routes/docs/core.tsx +++ b/apps/docs/src/routes/docs/core.tsx @@ -157,8 +157,8 @@ const CORE_NAV_SECTIONS: NavSection[] = [ href: "/docs/core/components/radio-group", }, { - title: "Rating Group", - href: "/docs/core/components/rating-group", + title: "Rating", + href: "/docs/core/components/rating", status: "unreleased", }, { diff --git a/apps/docs/src/routes/docs/core/components/color-area.mdx b/apps/docs/src/routes/docs/core/components/color-area.mdx index eb79c5ccc..cada55d75 100644 --- a/apps/docs/src/routes/docs/core/components/color-area.mdx +++ b/apps/docs/src/routes/docs/core/components/color-area.mdx @@ -70,7 +70,7 @@ The color area consists of: ```tsx import { ColorArea } from "@kobalte/core/color-area"; - import { parseColor } from "@kobalte/utils"; + import { parseColor } from "@kobalte/core/colors"; import "./style.css"; function App() { diff --git a/apps/docs/src/routes/docs/core/components/color-channel-field.mdx b/apps/docs/src/routes/docs/core/components/color-channel-field.mdx index d1a61748a..6ee34d1d6 100644 --- a/apps/docs/src/routes/docs/core/components/color-channel-field.mdx +++ b/apps/docs/src/routes/docs/core/components/color-channel-field.mdx @@ -74,7 +74,7 @@ The color channel field consists of: ```tsx import { ColorChannelField } from "@kobalte/core/color-channel-field"; - import { parseColor } from "@kobalte/utils"; + import { parseColor } from "@kobalte/core/colors"; import "./style.css"; function App() { diff --git a/apps/docs/src/routes/docs/core/components/color-slider.mdx b/apps/docs/src/routes/docs/core/components/color-slider.mdx index f63c2899a..b59973271 100644 --- a/apps/docs/src/routes/docs/core/components/color-slider.mdx +++ b/apps/docs/src/routes/docs/core/components/color-slider.mdx @@ -66,7 +66,7 @@ The color slider consists of: ```tsx import { ColorSlider } from "@kobalte/core/color-slider"; - import { parseColor } from "@kobalte/utils"; + import { parseColor } from "@kobalte/core/colors"; import "./style.css"; function App() { @@ -151,7 +151,7 @@ This must be one of the channels included in the color value, for example, for R ```tsx import { ColorSlider } from "@kobalte/core/color-slider"; - import { parseColor } from "@kobalte/utils"; + import { parseColor } from "@kobalte/core/colors"; import "./style.css"; function App() { @@ -232,7 +232,7 @@ This must be one of the channels included in the color value, for example, for R ```tsx import { createSignal } from "solid-js"; import { ColorSlider } from "@kobalte/core/color-slider"; - import { parseColor } from "@kobalte/utils"; + import { parseColor } from "@kobalte/core/colors"; import "./style.css"; function App() { @@ -313,7 +313,7 @@ This must be one of the channels included in the color value, for example, for R ```tsx import { ColorSlider } from "@kobalte/core/color-slider"; - import { parseColor } from "@kobalte/utils"; + import { parseColor } from "@kobalte/core/colors"; import "./style.css"; function App() { @@ -393,7 +393,7 @@ This must be one of the channels included in the color value, for example, for R ```tsx import { ColorSlider } from "@kobalte/core/color-slider"; - import { parseColor } from "@kobalte/utils"; + import { parseColor } from "@kobalte/core/colors"; import "./style.css"; function App() { diff --git a/apps/docs/src/routes/docs/core/components/color-swatch.mdx b/apps/docs/src/routes/docs/core/components/color-swatch.mdx index c5bf1883b..b2e5f8ad1 100644 --- a/apps/docs/src/routes/docs/core/components/color-swatch.mdx +++ b/apps/docs/src/routes/docs/core/components/color-swatch.mdx @@ -46,7 +46,7 @@ The color swatch consists of: ```tsx import { ColorSwatch } from "@kobalte/core/color-swatch"; - import { parseColor } from "@kobalte/utils"; + import { parseColor } from "@kobalte/core/colors"; import "./style.css"; function App() { @@ -89,7 +89,7 @@ ColorSwatch accepts a value via the `value` prop. The value should be `Color` ob ```tsx import { ColorSwatch } from "@kobalte/core/color-swatch"; - import { parseColor } from "@kobalte/utils"; + import { parseColor } from "@kobalte/core/colors"; import "./style.css"; function App() { @@ -128,7 +128,7 @@ ColorSwatch accepts a value via the `value` prop. The value should be `Color` ob ```tsx import { ColorSwatch } from "@kobalte/core/color-swatch"; - import { parseColor } from "@kobalte/utils"; + import { parseColor } from "@kobalte/core/colors"; import "./style.css"; function App() { diff --git a/apps/docs/src/routes/docs/core/components/color-wheel.mdx b/apps/docs/src/routes/docs/core/components/color-wheel.mdx index 681aacf05..c3cb68a7d 100644 --- a/apps/docs/src/routes/docs/core/components/color-wheel.mdx +++ b/apps/docs/src/routes/docs/core/components/color-wheel.mdx @@ -137,7 +137,7 @@ By default, `ColorWheel` is uncontrolled with a default value of red (hue = 0). ```tsx {6} import { ColorWheel } from "@kobalte/core/color-wheel"; -import { parseColor } from "@kobalte/core"; +import { parseColor } from "@kobalte/core/colors"; function App() { return ( diff --git a/apps/docs/src/routes/docs/core/components/rating-group.mdx b/apps/docs/src/routes/docs/core/components/rating-group.mdx deleted file mode 100644 index 7d467a9d5..000000000 --- a/apps/docs/src/routes/docs/core/components/rating-group.mdx +++ /dev/null @@ -1,423 +0,0 @@ -import { Preview, TabsSnippets, Kbd } from "../../../../components"; -import { - BasicExample, - DefaultValueExample, - ControlledExample, - HalfRatingsExample, - DescriptionExample, - ErrorMessageExample, - HTMLFormExample, -} from "../../../../examples/rating-group"; - -# Rating Group - -Allows users to rate items using a set of icons. - -## Import - -```ts -import { RatingGroup } from "@kobalte/core/rating-group"; -// or -import { Root, Label, ... } from "@kobalte/core/rating-group"; -``` - -## Features - -- Precise ratings with half-value increments. -- Syncs with form reset events. -- Group and rating labeling support for assistive technology. -- Can be controlled or uncontrolled. - -## Anatomy - -The rating group consists of: - -- **RatingGroup**: The root container for the rating group. -- **RatingGroup.Control**: The container for the rating items. -- **RatingGroup.Label**: The label that gives the user information on the rating group. -- **RatingGroup.HiddenInput**: The native html input that is visually hidden in the rating group. -- **RatingGroup.Description**: The description that gives the user more information on the rating group. -- **RatingGroup.ErrorMessage**: The error message that gives the user information about how to fix a validation error on the rating group. - -The rating item consists of: - -- **RatingGroup.Item**: The root container for a rating item. -- **RatingGroup.ItemControl**: The element that visually represents a rating item. -- **RatingGroup.ItemLabel**: The label that gives the user information on the rating item. -- **RatingGroup.ItemDescription**: The description that gives the user more information on the rating item. - -```tsx - - - - - - - - - - - - - -``` - -## Example - - - - - - - - index.tsx - style.css - - {/* */} - - ```tsx - import { RatingGroup } from "@kobalte/core/rating-group"; - import "./style.css"; - - function App() { - return ( - - Rate Us: - - - {_ => ( - - - - - - )} - - - - ); - } - ``` - - - -```css -.rating-group { - display: flex; - flex-direction: column; - gap: 8px; -} - -.rating-group\_\_label { -color: hsl(240 6% 10%); -font-size: 14px; -font-weight: 500; -user-select: none; -} - -.rating-group\_\_description { -color: hsl(240 5% 26%); -font-size: 12px; -user-select: none; -} - -.rating-group\_\_error-message { -color: hsl(0 72% 51%); -font-size: 12px; -user-select: none; -} - -.rating-group\_\_control { -display: flex; -gap: 4px; -} - -.rating-group-item { -cursor: pointer; -fill: hsl(240 6% 90%); -transition: fill 200ms cubic-bezier(0.2, 0, 0, 1); -} - -.rating-group-item[data-highlighted] { -fill: hsl(200 98% 39%); -} - -```` - - - {/* */} - - -## Usage - -### Default value - -An initial, uncontrolled value can be provided using the `defaultValue` prop. - - - - - -```tsx {0} - - - - {_ => ( - - - - - - )} - - - -```` - -### Controlled value - -The `value` prop can be used to make the value controlled. The `onChange` event is fired when the user selects a rating, and receives the new value. - - - - - -```tsx {3,7} -import { createSignal } from "solid-js"; - -function ControlledExample() { - const [value, setValue] = createSignal(0); - - return ( - <> - - - - {_ => ( - - - - - - )} - - - -

Your rating is: {value()}/5

- - ); -} -``` - -### Half Ratings - -Allow 0.5 value steps by setting the `allowHalf` prop to true. - - - - - -```tsx{0} - - - - {_ => ( - - - {(state) => (state.half() ? : )} - - - )} - - - -``` - -### Description - -The `RatingGroup.Description` component can be used to associate additional help text with a rating group. - - - - - -```tsx {13} - - Rate Us: - - - {_ => ( - - - - - - )} - - - Rate your experience with us. - -``` - -### Error message - -The `RatingGroup.ErrorMessage` component can be used to help the user fix a validation error. It should be combined with the `validationState` prop to semantically mark the rating group as invalid for assistive technologies. - -By default, it will render only when the `validationState` prop is set to `invalid`, use the `forceMount` prop to always render the error message (ex: for usage with animation libraries). - - - - - -```tsx {9,23-25} -import { createSignal } from "solid-js"; - -function ErrorMessageExample() { - const [value, setValue] = createSignal(0); - - return ( - - Rate Us: - - - {_ => ( - - - - - - )} - - - Please select a rating between 1 and 5. - - ); -} -``` - -### HTML forms - -The `name` prop can be used for integration with HTML forms. - - - - - -```tsx {7,19} -function HTMLFormExample() { - const onSubmit = (e: SubmitEvent) => { - // handle form submission. - }; - - return ( -
- - - - {_ => ( - - - - - - )} - - - - -
- - -
-
- ); -} -``` - -## API Reference - -### RatingGroup - -`RatingGroup` is equivalent to the `Root` import from `@kobalte/core/rating-group`. - -| Prop | Description | -| :-------------- | :------------------------------------------------------------------------------------------------------------------------------------------------- | -| value | `number`
The current rating value. | -| defaultValue | `number`
The initial value of the rating group when it is first rendered. Use when you do not need to control the state of the rating group. | -| onChange | `(value: number) => void`
Event handler called when the value changes. | -| allowHalf | `boolean`
Whether to allow half ratings. | -| orientation | `'horizontal' \| 'vertical'`
The axis the rating group items should align with. | -| name | `string`
The name of the rating group. Submitted with its owning form as part of a name/value pair. | -| validationState | `'valid' \| 'invalid'`
Whether the rating group should display its "valid" or "invalid" visual styling. | -| required | `boolean`
Whether the user must select an item before the owning form can be submitted. | -| disabled | `boolean`
Whether the rating group is disabled. | -| readOnly | `boolean`
Whether the rating group items can be selected but not changed by the user. | - -| Data attribute | Description | -| :------------- | :--------------------------------------------------------------------------------------------- | -| data-valid | Present when the rating group is valid according to the validation rules. | -| data-invalid | Present when the rating group is invalid according to the validation rules. | -| data-required | Present when the user must select a rating group item before the owning form can be submitted. | -| data-disabled | Present when the rating group is disabled. | -| data-readonly | Present when the rating group is read only. | - -`RatingGroup.Label`, `RatingGroup.Description` and `RatingGroup.ErrorMesssage` shares the same data-attributes. - -### RatingGroup.ErrorMessage - -| Prop | Description | -| :--------- | :-------------------------------------------------------------------------------------------------------------------------------------- | -| forceMount | `boolean`
Used to force mounting when more control is needed. Useful when controlling animation with SolidJS animation libraries. | - -### RatingGroup.ItemControl - -| Render Prop | Description | -| :---------- | :---------------------------------------------------------------- | -| half | `Accessor`
Whether the rating item is half. | -| highlighted | `Accessor`
Whether the rating item is highlighted. | - -| Data attribute | Description | -| :--------------- | :--------------------------------------------------------------------------------- | -| data-valid | Present when the parent rating group is valid according to the validation rules. | -| data-invalid | Present when the parent rating group is invalid according to the validation rules. | -| data-required | Present when the parent rating group is required. | -| data-disabled | Present when the parent rating group is disabled. | -| data-readonly | Present when the parent rating group is read only. | -| data-checked | Present when the rating is checked. | -| data-half | Present when the rating is half. | -| data-highlighted | Present when the rating is highlighted. | - -`RatingGroup.ItemLabel` and `RatingGroup.ItemDescription` share the same data-attributes. - -## Rendered elements - -| Component | Default rendered element | -| :---------------------------- | :----------------------- | -| `RatingGroup` | `div` | -| `RatingGroup.Control` | `div` | -| `RatingGroup.Label` | `span` | -| `RatingGroup.HiddenInput` | `input` | -| `RatingGroup.Description` | `div` | -| `RatingGroup.ErrorMessage` | `div` | -| `RatingGroup.Item` | `div` | -| `RatingGroup.ItemControl` | `div` | -| `RatingGroup.ItemLabel` | `label` | -| `RatingGroup.ItemDescription` | `div` | - -## Accessibility - -### Keyboard Interactions - -| Key | Description | -| :-------------------- | :----------------------------------------------------------------------------------------------- | -| ArrowDown | Moves focus to the next item, increasing the rating value based on the `allowHalf` property. | -| ArrowRight | Moves focus to the next item, increasing the rating value based on the `allowHalf` property. | -| ArrowUp | Moves focus to the previous item, decreasing the rating value based on the `allowHalf` property. | -| ArrowLeft | Moves focus to the previous item, decreasing the rating value based on the `allowHalf` property. | -| Space | Selects the focused item in the rating group. | -| Home | Sets the value of the rating group to 1. | -| End | Sets the value of the rating group to the maximum value. | diff --git a/packages/core/src/index.tsx b/packages/core/src/index.tsx index 3a6de86ae..4a72a633e 100644 --- a/packages/core/src/index.tsx +++ b/packages/core/src/index.tsx @@ -33,7 +33,6 @@ export * as Pagination from "./pagination"; export * as Popover from "./popover"; export * as Progress from "./progress"; export * as RadioGroup from "./radio-group"; -export * as RatingGroup from "./rating-group"; export * as Select from "./select"; export * as Separator from "./separator"; export * as Skeleton from "./skeleton"; @@ -41,7 +40,6 @@ export * as Slider from "./slider"; export * as Switch from "./switch"; export * as Tabs from "./tabs"; export * as TextField from "./text-field"; -export * as TimeField from "./time-field"; export * as Toast from "./toast"; export * as ToggleButton from "./toggle-button"; export * as ToggleGroup from "./toggle-group"; diff --git a/packages/core/src/rating-group/index.tsx b/packages/core/src/rating-group/index.tsx deleted file mode 100644 index 35c1cfb26..000000000 --- a/packages/core/src/rating-group/index.tsx +++ /dev/null @@ -1,131 +0,0 @@ -import { - FormControlDescription as Description, - FormControlErrorMessage as ErrorMessage, - type FormControlDescriptionCommonProps as RatingGroupDescriptionCommonProps, - type FormControlDescriptionOptions as RatingGroupDescriptionOptions, - type FormControlDescriptionProps as RatingGroupDescriptionProps, - type FormControlDescriptionRenderProps as RatingGroupDescriptionRenderProps, - type FormControlErrorMessageCommonProps as RatingGroupErrorMessageCommonProps, - type FormControlErrorMessageOptions as RatingGroupErrorMessageOptions, - type FormControlErrorMessageProps as RatingGroupErrorMessageProps, - type FormControlErrorMessageRenderProps as RatingGroupErrorMessageRenderProps, -} from "../form-control"; - -import { - RatingGroupControl as Control, - type RatingGroupControlCommonProps, - type RatingGroupControlOptions, - type RatingGroupControlProps, - type RatingGroupControlRenderProps, -} from "./rating-group-control"; -import { - RatingGroupHiddenInput as HiddenInput, - type RatingGroupHiddenInputProps, -} from "./rating-group-hidden-input"; -import { - RatingGroupItem as Item, - type RatingGroupItemCommonProps, - type RatingGroupItemOptions, - type RatingGroupItemProps, - type RatingGroupItemRenderProps, -} from "./rating-group-item"; -import { - RatingGroupItemControl as ItemControl, - type RatingGroupItemControlCommonProps, - type RatingGroupItemControlOptions, - type RatingGroupItemControlProps, - type RatingGroupItemControlRenderProps, -} from "./rating-group-item-control"; -import { - RatingGroupItemDescription as ItemDescription, - type RatingGroupItemDescriptionCommonProps, - type RatingGroupItemDescriptionOptions, - type RatingGroupItemDescriptionProps, - type RatingGroupItemDescriptionRenderProps, -} from "./rating-group-item-description"; -import { - RatingGroupItemLabel as ItemLabel, - type RatingGroupItemLabelCommonProps, - type RatingGroupItemLabelOptions, - type RatingGroupItemLabelProps, - type RatingGroupItemLabelRenderProps, -} from "./rating-group-item-label"; -import { - RatingGroupLabel as Label, - type RatingGroupLabelCommonProps, - type RatingGroupLabelOptions, - type RatingGroupLabelProps, - type RatingGroupLabelRenderProps, -} from "./rating-group-label"; -import { - type RatingGroupRootCommonProps, - type RatingGroupRootOptions, - type RatingGroupRootProps, - type RatingGroupRootRenderProps, - RatingGroupRoot as Root, -} from "./rating-group-root"; - -export type { - RatingGroupControlCommonProps, - RatingGroupControlOptions, - RatingGroupControlProps, - RatingGroupControlRenderProps, - RatingGroupDescriptionOptions, - RatingGroupDescriptionCommonProps, - RatingGroupDescriptionRenderProps, - RatingGroupDescriptionProps, - RatingGroupErrorMessageOptions, - RatingGroupErrorMessageCommonProps, - RatingGroupErrorMessageRenderProps, - RatingGroupErrorMessageProps, - RatingGroupHiddenInputProps, - RatingGroupItemControlOptions, - RatingGroupItemControlCommonProps, - RatingGroupItemControlRenderProps, - RatingGroupItemControlProps, - RatingGroupItemDescriptionOptions, - RatingGroupItemDescriptionCommonProps, - RatingGroupItemDescriptionRenderProps, - RatingGroupItemDescriptionProps, - RatingGroupItemLabelOptions, - RatingGroupItemLabelCommonProps, - RatingGroupItemLabelRenderProps, - RatingGroupItemLabelProps, - RatingGroupItemOptions, - RatingGroupItemCommonProps, - RatingGroupItemRenderProps, - RatingGroupItemProps, - RatingGroupLabelOptions, - RatingGroupLabelCommonProps, - RatingGroupLabelRenderProps, - RatingGroupLabelProps, - RatingGroupRootOptions, - RatingGroupRootCommonProps, - RatingGroupRootRenderProps, - RatingGroupRootProps, -}; - -export { - Description, - ErrorMessage, - Control, - HiddenInput, - ItemControl, - ItemDescription, - ItemLabel, - Item, - Label, - Root, -}; - -export const RatingGroup = Object.assign(Root, { - Description, - ErrorMessage, - Control, - HiddenInput, - ItemControl, - ItemDescription, - ItemLabel, - Item, - Label, -}); diff --git a/packages/core/src/rating-group/rating-group-item-description.tsx b/packages/core/src/rating-group/rating-group-item-description.tsx deleted file mode 100644 index c94dca90a..000000000 --- a/packages/core/src/rating-group/rating-group-item-description.tsx +++ /dev/null @@ -1,52 +0,0 @@ -import { mergeDefaultProps } from "@kobalte/utils"; -import { type ValidComponent, createEffect, onCleanup } from "solid-js"; - -import { - type ElementOf, - Polymorphic, - type PolymorphicProps, -} from "../polymorphic"; -import { - type RatingGroupItemDataSet, - useRatingGroupItemContext, -} from "./rating-group-item-context"; - -export interface RatingGroupItemDescriptionOptions {} - -export interface RatingGroupItemDescriptionCommonProps< - T extends HTMLElement = HTMLElement, -> { - id: string; -} - -export interface RatingGroupItemDescriptionRenderProps - extends RatingGroupItemDescriptionCommonProps, - RatingGroupItemDataSet {} - -export type RatingGroupItemDescriptionProps< - T extends ValidComponent | HTMLElement = HTMLElement, -> = RatingGroupItemDescriptionOptions & - Partial>>; - -export function RatingGroupItemDescription( - props: PolymorphicProps>, -) { - const context = useRatingGroupItemContext(); - - const mergedProps = mergeDefaultProps( - { - id: context.generateId("description"), - }, - props as RatingGroupItemDescriptionProps, - ); - - createEffect(() => onCleanup(context.registerDescription(mergedProps.id))); - - return ( - - as="div" - {...context.dataset()} - {...mergedProps} - /> - ); -} diff --git a/packages/core/src/rating-group/rating-group-label.tsx b/packages/core/src/rating-group/rating-group-label.tsx deleted file mode 100644 index dd9ae7884..000000000 --- a/packages/core/src/rating-group/rating-group-label.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import type { Component, ValidComponent } from "solid-js"; - -import { FormControlLabel } from "../form-control"; -import type { ElementOf, PolymorphicProps } from "../polymorphic"; - -export interface RatingGroupLabelOptions {} - -export interface RatingGroupLabelCommonProps< - T extends HTMLElement = HTMLElement, -> {} - -export interface RatingGroupLabelRenderProps - extends RatingGroupLabelCommonProps {} - -export type RatingGroupLabelProps< - T extends ValidComponent | HTMLElement = HTMLElement, -> = RatingGroupLabelOptions & - Partial>>; - -export function RatingGroupLabel( - props: PolymorphicProps>, -) { - return ( - > - as="span" - {...(props as RatingGroupLabelProps)} - /> - ); -} diff --git a/packages/core/src/rating/index.tsx b/packages/core/src/rating/index.tsx new file mode 100644 index 000000000..ad8188085 --- /dev/null +++ b/packages/core/src/rating/index.tsx @@ -0,0 +1,131 @@ +import { + FormControlDescription as Description, + FormControlErrorMessage as ErrorMessage, + type FormControlDescriptionCommonProps as RatingDescriptionCommonProps, + type FormControlDescriptionOptions as RatingDescriptionOptions, + type FormControlDescriptionProps as RatingDescriptionProps, + type FormControlDescriptionRenderProps as RatingDescriptionRenderProps, + type FormControlErrorMessageCommonProps as RatingErrorMessageCommonProps, + type FormControlErrorMessageOptions as RatingErrorMessageOptions, + type FormControlErrorMessageProps as RatingErrorMessageProps, + type FormControlErrorMessageRenderProps as RatingErrorMessageRenderProps, +} from "../form-control"; + +import { + RatingControl as Control, + type RatingControlCommonProps, + type RatingControlOptions, + type RatingControlProps, + type RatingControlRenderProps, +} from "./rating-control"; +import { + RatingHiddenInput as HiddenInput, + type RatingHiddenInputProps, +} from "./rating-hidden-input"; +import { + RatingItem as Item, + type RatingItemCommonProps, + type RatingItemOptions, + type RatingItemProps, + type RatingItemRenderProps, +} from "./rating-item"; +import { + RatingItemControl as ItemControl, + type RatingItemControlCommonProps, + type RatingItemControlOptions, + type RatingItemControlProps, + type RatingItemControlRenderProps, +} from "./rating-item-control"; +import { + RatingItemDescription as ItemDescription, + type RatingItemDescriptionCommonProps, + type RatingItemDescriptionOptions, + type RatingItemDescriptionProps, + type RatingItemDescriptionRenderProps, +} from "./rating-item-description"; +import { + RatingItemLabel as ItemLabel, + type RatingItemLabelCommonProps, + type RatingItemLabelOptions, + type RatingItemLabelProps, + type RatingItemLabelRenderProps, +} from "./rating-item-label"; +import { + RatingLabel as Label, + type RatingLabelCommonProps, + type RatingLabelOptions, + type RatingLabelProps, + type RatingLabelRenderProps, +} from "./rating-label"; +import { + type RatingRootCommonProps, + type RatingRootOptions, + type RatingRootProps, + type RatingRootRenderProps, + RatingRoot as Root, +} from "./rating-root"; + +export type { + RatingControlCommonProps, + RatingControlOptions, + RatingControlProps, + RatingControlRenderProps, + RatingDescriptionOptions, + RatingDescriptionCommonProps, + RatingDescriptionRenderProps, + RatingDescriptionProps, + RatingErrorMessageOptions, + RatingErrorMessageCommonProps, + RatingErrorMessageRenderProps, + RatingErrorMessageProps, + RatingHiddenInputProps, + RatingItemControlOptions, + RatingItemControlCommonProps, + RatingItemControlRenderProps, + RatingItemControlProps, + RatingItemDescriptionOptions, + RatingItemDescriptionCommonProps, + RatingItemDescriptionRenderProps, + RatingItemDescriptionProps, + RatingItemLabelOptions, + RatingItemLabelCommonProps, + RatingItemLabelRenderProps, + RatingItemLabelProps, + RatingItemOptions, + RatingItemCommonProps, + RatingItemRenderProps, + RatingItemProps, + RatingLabelOptions, + RatingLabelCommonProps, + RatingLabelRenderProps, + RatingLabelProps, + RatingRootOptions, + RatingRootCommonProps, + RatingRootRenderProps, + RatingRootProps, +}; + +export { + Description, + ErrorMessage, + Control, + HiddenInput, + ItemControl, + ItemDescription, + ItemLabel, + Item, + Label, + Root, +}; + +export const Rating = Object.assign(Root, { + Description, + ErrorMessage, + Control, + HiddenInput, + ItemControl, + ItemDescription, + ItemLabel, + Item, + Label, +}); diff --git a/packages/core/src/rating-group/rating-group-context.tsx b/packages/core/src/rating/rating-context.tsx similarity index 69% rename from packages/core/src/rating-group/rating-group-context.tsx rename to packages/core/src/rating/rating-context.tsx index 5ecdd19fc..b55119177 100644 --- a/packages/core/src/rating-group/rating-group-context.tsx +++ b/packages/core/src/rating/rating-context.tsx @@ -7,7 +7,7 @@ import { } from "solid-js"; import type { CollectionItemWithRef } from "../primitives"; -export interface RatingGroupContextValue { +export interface RatingContextValue { value: Accessor; setValue: (value: number) => void; allowHalf: Accessor; @@ -20,14 +20,14 @@ export interface RatingGroupContextValue { setItems: Setter; } -export const RatingGroupContext = createContext(); +export const RatingContext = createContext(); -export function useRatingGroupContext() { - const context = useContext(RatingGroupContext); +export function useRatingContext() { + const context = useContext(RatingContext); if (context === undefined) { throw new Error( - "[kobalte]: `useRatingGroupContext` must be used within a `RatingGroup` component", + "[kobalte]: `useRatingContext` must be used within a `Rating` component", ); } diff --git a/packages/core/src/rating-group/rating-group-control.tsx b/packages/core/src/rating/rating-control.tsx similarity index 65% rename from packages/core/src/rating-group/rating-group-control.tsx rename to packages/core/src/rating/rating-control.tsx index fe1f097ef..b60b86679 100644 --- a/packages/core/src/rating-group/rating-group-control.tsx +++ b/packages/core/src/rating/rating-control.tsx @@ -6,32 +6,32 @@ import { Polymorphic, type PolymorphicProps, } from "../polymorphic"; -import { useRatingGroupContext } from "./rating-group-context"; +import { useRatingContext } from "./rating-context"; -export interface RatingGroupControlOptions {} +export interface RatingControlOptions {} -export interface RatingGroupControlCommonProps< +export interface RatingControlCommonProps< T extends HTMLElement = HTMLElement, > { id: string; onPointerLeave: JSX.EventHandlerUnion; } -export interface RatingGroupControlRenderProps - extends RatingGroupControlCommonProps { +export interface RatingControlRenderProps + extends RatingControlCommonProps { role: "presentation"; } -export type RatingGroupControlProps< +export type RatingControlProps< T extends ValidComponent | HTMLElement = HTMLElement, -> = RatingGroupControlOptions & - Partial>>; +> = RatingControlOptions & + Partial>>; -export function RatingGroupControl( - props: PolymorphicProps>, +export function RatingControl( + props: PolymorphicProps>, ) { const formControlContext = useFormControlContext(); - const context = useRatingGroupContext(); + const context = useRatingContext(); const defaultId = `${formControlContext.generateId("control")}`; @@ -39,7 +39,7 @@ export function RatingGroupControl( { id: defaultId, }, - props as RatingGroupControlProps, + props as RatingControlProps, ); const [local, others] = splitProps(mergedProps, ["onPointerLeave"]); @@ -60,7 +60,7 @@ export function RatingGroupControl( }; return ( - + as="div" role="presentation" onPointerLeave={onPointerLeave} diff --git a/packages/core/src/rating-group/rating-group-hidden-input.tsx b/packages/core/src/rating/rating-hidden-input.tsx similarity index 66% rename from packages/core/src/rating-group/rating-group-hidden-input.tsx rename to packages/core/src/rating/rating-hidden-input.tsx index 8e1aacce3..488e8a653 100644 --- a/packages/core/src/rating-group/rating-group-hidden-input.tsx +++ b/packages/core/src/rating/rating-hidden-input.tsx @@ -2,13 +2,13 @@ import { visuallyHiddenStyles } from "@kobalte/utils"; import type { ComponentProps } from "solid-js"; import { useFormControlContext } from "../form-control"; -import { useRatingGroupContext } from "./rating-group-context"; +import { useRatingContext } from "./rating-context"; -export interface RatingGroupHiddenInputProps extends ComponentProps<"input"> {} +export interface RatingHiddenInputProps extends ComponentProps<"input"> {} -export function RatingGroupHiddenInput(props: RatingGroupHiddenInputProps) { +export function RatingHiddenInput(props: RatingHiddenInputProps) { const formControlContext = useFormControlContext(); - const context = useRatingGroupContext(); + const context = useRatingContext(); return ( ; highlighted: Accessor; } -export interface RatingGroupItemContextValue { - state: RatingGroupItemState; - dataset: Accessor; +export interface RatingItemContextValue { + state: RatingItemState; + dataset: Accessor; itemId: Accessor; generateId: (part: string) => string; registerLabel: (id: string) => () => void; registerDescription: (id: string) => () => void; } -export const RatingGroupItemContext = - createContext(); +export const RatingItemContext = + createContext(); -export function useRatingGroupItemContext() { - const context = useContext(RatingGroupItemContext); +export function useRatingItemContext() { + const context = useContext(RatingItemContext); if (context === undefined) { throw new Error( - "[kobalte]: `useRatingGroupItemContext` must be used within a `RatingGroup.Item` component", + "[kobalte]: `useRatingItemContext` must be used within a `Rating.Item` component", ); } diff --git a/packages/core/src/rating-group/rating-group-item-control.tsx b/packages/core/src/rating/rating-item-control.tsx similarity index 52% rename from packages/core/src/rating-group/rating-group-item-control.tsx rename to packages/core/src/rating/rating-item-control.tsx index 0ae65545d..b3ffbbdd3 100644 --- a/packages/core/src/rating-group/rating-group-item-control.tsx +++ b/packages/core/src/rating/rating-item-control.tsx @@ -7,39 +7,39 @@ import { type PolymorphicProps, } from "../polymorphic"; import { - type RatingGroupItemState, - useRatingGroupItemContext, -} from "./rating-group-item-context"; + type RatingItemState, + useRatingItemContext, +} from "./rating-item-context"; -export interface RatingGroupItemControlOptions { +export interface RatingItemControlOptions { /** * The children of the rating group item. * Can be a `JSX.Element` or a _render prop_ for having access to the internal state. */ - children?: JSX.Element | ((state: RatingGroupItemState) => JSX.Element); + children?: JSX.Element | ((state: RatingItemState) => JSX.Element); } -export interface RatingGroupItemControlCommonProps< +export interface RatingItemControlCommonProps< T extends HTMLElement = HTMLElement, > { id: string; } -export interface RatingGroupItemControlRenderProps - extends RatingGroupItemControlCommonProps { +export interface RatingItemControlRenderProps + extends RatingItemControlCommonProps { role: "presentation"; children: JSX.Element; } -export type RatingGroupItemControlProps< +export type RatingItemControlProps< T extends ValidComponent | HTMLElement = HTMLElement, -> = RatingGroupItemControlOptions & - Partial>>; +> = RatingItemControlOptions & + Partial>>; -export function RatingGroupItemControl( - props: PolymorphicProps>, +export function RatingItemControl( + props: PolymorphicProps>, ) { - const context = useRatingGroupItemContext(); + const context = useRatingItemContext(); const defaultId = `${context.generateId("control")}`; @@ -47,35 +47,35 @@ export function RatingGroupItemControl( { id: defaultId, }, - props as RatingGroupItemControlProps, + props as RatingItemControlProps, ); const [local, others] = splitProps(mergedProps, ["children"]); return ( - + as="div" role="presentation" {...others} > - {local.children} - + ); } -interface RatingGroupItemControlChildProps - extends Pick { - state: RatingGroupItemState; +interface RatingItemControlChildProps + extends Pick { + state: RatingItemState; } -function RatingGroupItemControlChild(props: RatingGroupItemControlChildProps) { +function RatingItemControlChild(props: RatingItemControlChildProps) { const resolvedChildren = children(() => { const body = props.children; return isFunction(body) ? body(props.state) : body; diff --git a/packages/core/src/rating/rating-item-description.tsx b/packages/core/src/rating/rating-item-description.tsx new file mode 100644 index 000000000..a12ec4420 --- /dev/null +++ b/packages/core/src/rating/rating-item-description.tsx @@ -0,0 +1,52 @@ +import { mergeDefaultProps } from "@kobalte/utils"; +import { type ValidComponent, createEffect, onCleanup } from "solid-js"; + +import { + type ElementOf, + Polymorphic, + type PolymorphicProps, +} from "../polymorphic"; +import { + type RatingItemDataSet, + useRatingItemContext, +} from "./rating-item-context"; + +export interface RatingItemDescriptionOptions {} + +export interface RatingItemDescriptionCommonProps< + T extends HTMLElement = HTMLElement, +> { + id: string; +} + +export interface RatingItemDescriptionRenderProps + extends RatingItemDescriptionCommonProps, + RatingItemDataSet {} + +export type RatingItemDescriptionProps< + T extends ValidComponent | HTMLElement = HTMLElement, +> = RatingItemDescriptionOptions & + Partial>>; + +export function RatingItemDescription( + props: PolymorphicProps>, +) { + const context = useRatingItemContext(); + + const mergedProps = mergeDefaultProps( + { + id: context.generateId("description"), + }, + props as RatingItemDescriptionProps, + ); + + createEffect(() => onCleanup(context.registerDescription(mergedProps.id))); + + return ( + + as="div" + {...context.dataset()} + {...mergedProps} + /> + ); +} diff --git a/packages/core/src/rating-group/rating-group-item-label.tsx b/packages/core/src/rating/rating-item-label.tsx similarity index 55% rename from packages/core/src/rating-group/rating-group-item-label.tsx rename to packages/core/src/rating/rating-item-label.tsx index bb5c19210..3822b0000 100644 --- a/packages/core/src/rating-group/rating-group-item-label.tsx +++ b/packages/core/src/rating/rating-item-label.tsx @@ -13,40 +13,40 @@ import { type PolymorphicProps, } from "../polymorphic"; import { - type RatingGroupItemDataSet, - useRatingGroupItemContext, -} from "./rating-group-item-context"; + type RatingItemDataSet, + useRatingItemContext, +} from "./rating-item-context"; -export interface RatingGroupItemLabelOptions {} +export interface RatingItemLabelOptions {} -export interface RatingGroupItemLabelCommonProps< +export interface RatingItemLabelCommonProps< T extends HTMLElement = HTMLElement, > { id: string; style: JSX.CSSProperties | string; } -export interface RatingGroupItemLabelRenderProps - extends RatingGroupItemLabelCommonProps, - RatingGroupItemDataSet { +export interface RatingItemLabelRenderProps + extends RatingItemLabelCommonProps, + RatingItemDataSet { for: string | undefined; } -export type RatingGroupItemLabelProps< +export type RatingItemLabelProps< T extends ValidComponent | HTMLElement = HTMLElement, -> = RatingGroupItemLabelOptions & - Partial>>; +> = RatingItemLabelOptions & + Partial>>; -export function RatingGroupItemLabel( - props: PolymorphicProps>, +export function RatingItemLabel( + props: PolymorphicProps>, ) { - const context = useRatingGroupItemContext(); + const context = useRatingItemContext(); const mergedProps = mergeDefaultProps( { id: context.generateId("label"), }, - props as RatingGroupItemLabelProps, + props as RatingItemLabelProps, ); const [local, others] = splitProps(mergedProps, ["style"]); @@ -54,7 +54,7 @@ export function RatingGroupItemLabel( createEffect(() => onCleanup(context.registerLabel(others.id!))); return ( - + as="label" for={context.itemId()} style={combineStyle(visuallyHiddenStyles, local.style)} diff --git a/packages/core/src/rating-group/rating-group-item.tsx b/packages/core/src/rating/rating-item.tsx similarity index 71% rename from packages/core/src/rating-group/rating-group-item.tsx rename to packages/core/src/rating/rating-item.tsx index 945e85933..f54d58622 100644 --- a/packages/core/src/rating-group/rating-group-item.tsx +++ b/packages/core/src/rating/rating-item.tsx @@ -25,17 +25,17 @@ import { } from "../polymorphic"; import { type CollectionItemWithRef, createRegisterId } from "../primitives"; import { createDomCollectionItem } from "../primitives/create-dom-collection"; -import { useRatingGroupContext } from "./rating-group-context"; +import { useRatingContext } from "./rating-context"; import { - RatingGroupItemContext, - type RatingGroupItemContextValue, - type RatingGroupItemDataSet, -} from "./rating-group-item-context"; + RatingItemContext, + type RatingItemContextValue, + type RatingItemDataSet, +} from "./rating-item-context"; import { getEventPoint, getRelativePoint } from "./utils"; -export interface RatingGroupItemOptions {} +export interface RatingItemOptions {} -export interface RatingGroupItemCommonProps< +export interface RatingItemCommonProps< T extends HTMLElement = HTMLElement, > { id: string; @@ -48,9 +48,9 @@ export interface RatingGroupItemCommonProps< onPointerMove: JSX.EventHandlerUnion; } -export interface RatingGroupItemRenderProps - extends RatingGroupItemCommonProps, - RatingGroupItemDataSet { +export interface RatingItemRenderProps + extends RatingItemCommonProps, + RatingItemDataSet { role: "radio"; tabIndex: number | undefined; "aria-required": boolean | undefined; @@ -59,17 +59,17 @@ export interface RatingGroupItemRenderProps "aria-checked": boolean; } -export type RatingGroupItemProps< +export type RatingItemProps< T extends ValidComponent | HTMLElement = HTMLElement, -> = RatingGroupItemOptions & Partial>>; +> = RatingItemOptions & Partial>>; -export function RatingGroupItem( - props: PolymorphicProps>, +export function RatingItem( + props: PolymorphicProps>, ) { let ref: HTMLElement | undefined; const formControlContext = useFormControlContext(); - const ratingGroupContext = useRatingGroupContext(); + const RatingContext = useRatingContext(); const defaultId = `${formControlContext.generateId("item")}-${createUniqueId()}`; @@ -77,7 +77,7 @@ export function RatingGroupItem( { id: defaultId, }, - props as RatingGroupItemProps, + props as RatingItemProps, ); const [local, others] = splitProps(mergedProps, [ @@ -118,7 +118,7 @@ export function RatingGroupItem( [ local["aria-describedby"], descriptionId(), - ratingGroupContext.ariaDescribedBy(), + RatingContext.ariaDescribedBy(), ] .filter(Boolean) .join(" ") || undefined @@ -132,12 +132,12 @@ export function RatingGroupItem( const [descriptionId, setDescriptionId] = createSignal(); const index = () => - ref ? ratingGroupContext.items().findIndex((v) => v.ref() === ref) : -1; + ref ? RatingContext.items().findIndex((v) => v.ref() === ref) : -1; const [value, setValue] = createSignal(); const newValue = () => - ratingGroupContext.isHovering() - ? ratingGroupContext.hoveredValue()! - : ratingGroupContext.value()!; + RatingContext.isHovering() + ? RatingContext.hoveredValue()! + : RatingContext.value()!; const equal = () => Math.ceil(newValue()!) === value(); const highlighted = () => value()! <= newValue()! || equal(); const half = () => equal() && Math.abs(newValue()! - value()!) === 0.5; @@ -146,7 +146,7 @@ export function RatingGroupItem( setValue( direction() === "ltr" ? index() + 1 - : ratingGroupContext.items().length - index(), + : RatingContext.items().length - index(), ); }); @@ -158,24 +158,24 @@ export function RatingGroupItem( const focusItem = (index: number) => ( - ratingGroupContext.items()[Math.round(index)].ref() as HTMLElement + RatingContext.items()[Math.round(index)].ref() as HTMLElement ).focus(); const setPrevValue = () => { - const factor = ratingGroupContext.allowHalf() ? 0.5 : 1; - const value = Math.max(0, ratingGroupContext.value()! - factor); - ratingGroupContext.setValue(value); + const factor = RatingContext.allowHalf() ? 0.5 : 1; + const value = Math.max(0, RatingContext.value()! - factor); + RatingContext.setValue(value); focusItem(Math.max(value - 1, 0)); }; const setNextValue = () => { - const factor = ratingGroupContext.allowHalf() ? 0.5 : 1; + const factor = RatingContext.allowHalf() ? 0.5 : 1; const value = Math.min( - ratingGroupContext.items().length, - (ratingGroupContext.value() === -1 ? 0 : ratingGroupContext.value())! + + RatingContext.items().length, + (RatingContext.value() === -1 ? 0 : RatingContext.value())! + factor, ); - ratingGroupContext.setValue(value); + RatingContext.setValue(value); focusItem(value - 1); }; @@ -183,11 +183,11 @@ export function RatingGroupItem( callHandler(e, local.onClick); const value = - ratingGroupContext.hoveredValue() === -1 + RatingContext.hoveredValue() === -1 ? index() + 1 - : ratingGroupContext.hoveredValue(); - ratingGroupContext.setValue(value); - ratingGroupContext.setHoveredValue(-1); + : RatingContext.hoveredValue(); + RatingContext.setValue(value); + RatingContext.setHoveredValue(-1); focusItem(value - 1); }; @@ -199,13 +199,13 @@ export function RatingGroupItem( const point = getEventPoint(e); const relativePoint = getRelativePoint(point, e.currentTarget); const percentX = relativePoint.getPercentValue({ - orientation: ratingGroupContext.orientation(), + orientation: RatingContext.orientation(), dir: direction(), }); const isMidway = percentX < 0.5; - const half = ratingGroupContext.allowHalf() && isMidway; + const half = RatingContext.allowHalf() && isMidway; const factor = half ? 0.5 : 0; - ratingGroupContext.setHoveredValue(value()! - factor); + RatingContext.setHoveredValue(value()! - factor); }; const onKeyDown: JSX.EventHandlerUnion = (e) => { @@ -232,27 +232,27 @@ export function RatingGroupItem( break; case EventKey.Space: e.preventDefault(); - ratingGroupContext.setValue(newValue()!); + RatingContext.setValue(newValue()!); break; case EventKey.Home: e.preventDefault(); - ratingGroupContext.setValue(1); + RatingContext.setValue(1); break; case EventKey.End: e.preventDefault(); - ratingGroupContext.setValue(ratingGroupContext.items().length); + RatingContext.setValue(RatingContext.items().length); break; } }; - const dataset: Accessor = createMemo(() => ({ + const dataset: Accessor = createMemo(() => ({ ...formControlContext.dataset(), "data-checked": equal() ? "" : undefined, "data-half": half() ? "" : undefined, "data-highlighted": highlighted() ? "" : undefined, })); - const context: RatingGroupItemContextValue = { + const context: RatingItemContextValue = { state: { highlighted, half }, dataset, generateId: createGenerateId(() => others.id!), @@ -262,8 +262,8 @@ export function RatingGroupItem( }; return ( - - + + as="div" ref={mergeRefs((el) => (ref = el), local.ref)} role="radio" @@ -280,6 +280,6 @@ export function RatingGroupItem( {...dataset()} {...others} /> - + ); } diff --git a/packages/core/src/rating/rating-label.tsx b/packages/core/src/rating/rating-label.tsx new file mode 100644 index 000000000..798cd90c9 --- /dev/null +++ b/packages/core/src/rating/rating-label.tsx @@ -0,0 +1,29 @@ +import type { Component, ValidComponent } from "solid-js"; + +import { FormControlLabel } from "../form-control"; +import type { ElementOf, PolymorphicProps } from "../polymorphic"; + +export interface RatingLabelOptions {} + +export interface RatingLabelCommonProps< + T extends HTMLElement = HTMLElement, +> {} + +export interface RatingLabelRenderProps + extends RatingLabelCommonProps {} + +export type RatingLabelProps< + T extends ValidComponent | HTMLElement = HTMLElement, +> = RatingLabelOptions & + Partial>>; + +export function RatingLabel( + props: PolymorphicProps>, +) { + return ( + > + as="span" + {...(props as RatingLabelProps)} + /> + ); +} diff --git a/packages/core/src/rating-group/rating-group-root.tsx b/packages/core/src/rating/rating-root.tsx similarity index 86% rename from packages/core/src/rating-group/rating-group-root.tsx rename to packages/core/src/rating/rating-root.tsx index dff4c4d9e..43d39e6e0 100644 --- a/packages/core/src/rating-group/rating-group-root.tsx +++ b/packages/core/src/rating/rating-root.tsx @@ -30,11 +30,11 @@ import { } from "../primitives"; import { createDomCollection } from "../primitives/create-dom-collection"; import { - RatingGroupContext, - type RatingGroupContextValue, -} from "./rating-group-context"; + RatingContext, + type RatingContextValue, +} from "./rating-context"; -export interface RatingGroupRootOptions { +export interface RatingRootOptions { /** The current rating value. */ value?: number; @@ -79,7 +79,7 @@ export interface RatingGroupRootOptions { readOnly?: boolean; } -export interface RatingGroupRootCommonProps< +export interface RatingRootCommonProps< T extends HTMLElement = HTMLElement, > { id: string; @@ -89,8 +89,8 @@ export interface RatingGroupRootCommonProps< "aria-label"?: string; } -export interface RatingGroupRootRenderProps - extends RatingGroupRootCommonProps, +export interface RatingRootRenderProps + extends RatingRootCommonProps, FormControlDataSet { role: "radiogroup"; "aria-invalid": boolean | undefined; @@ -100,23 +100,23 @@ export interface RatingGroupRootRenderProps "aria-orientation": Orientation | undefined; } -export type RatingGroupRootProps< +export type RatingRootProps< T extends ValidComponent | HTMLElement = HTMLElement, -> = RatingGroupRootOptions & Partial>>; +> = RatingRootOptions & Partial>>; -export function RatingGroupRoot( - props: PolymorphicProps>, +export function RatingRoot( + props: PolymorphicProps>, ) { let ref: HTMLElement | undefined; - const defaultId = `ratinggroup-${createUniqueId()}`; + const defaultId = `Rating-${createUniqueId()}`; const mergedProps = mergeDefaultProps( { id: defaultId, orientation: "horizontal", }, - props as RatingGroupRootProps, + props as RatingRootProps, ); const [local, formControlProps, others] = splitProps( @@ -167,7 +167,7 @@ export function RatingGroupRoot( return formControlContext.getAriaDescribedBy(local["aria-describedby"]); }; - const context: RatingGroupContextValue = { + const context: RatingContextValue = { value, setValue: (newValue) => { if (formControlContext.isReadOnly() || formControlContext.isDisabled()) { @@ -189,8 +189,8 @@ export function RatingGroupRoot( return ( - - + + as="div" ref={mergeRefs((el) => (ref = el), local.ref)} role="radiogroup" @@ -207,7 +207,7 @@ export function RatingGroupRoot( {...formControlContext.dataset()} {...others} /> - + ); diff --git a/packages/core/src/rating-group/utils.ts b/packages/core/src/rating/utils.ts similarity index 100% rename from packages/core/src/rating-group/utils.ts rename to packages/core/src/rating/utils.ts From 78d020ba75b7980ec5c2c00322ee25238ebb6205 Mon Sep 17 00:00:00 2001 From: jer3m01 Date: Sun, 27 Jul 2025 23:43:27 +0200 Subject: [PATCH 02/12] docs: fix parseColor import --- apps/docs/src/examples/rating.module.css | 62 +++ apps/docs/src/examples/rating.tsx | 235 ++++++++++ .../routes/docs/core/components/rating.mdx | 423 ++++++++++++++++++ 3 files changed, 720 insertions(+) create mode 100644 apps/docs/src/examples/rating.module.css create mode 100644 apps/docs/src/examples/rating.tsx create mode 100644 apps/docs/src/routes/docs/core/components/rating.mdx diff --git a/apps/docs/src/examples/rating.module.css b/apps/docs/src/examples/rating.module.css new file mode 100644 index 000000000..ef021968f --- /dev/null +++ b/apps/docs/src/examples/rating.module.css @@ -0,0 +1,62 @@ +.rating { + display: flex; + flex-direction: column; + gap: 8px; +} + +.rating__label { + color: hsl(240 6% 10%); + font-size: 14px; + font-weight: 500; + user-select: none; +} + +.rating__description { + color: hsl(240 5% 26%); + font-size: 12px; + user-select: none; +} + +.rating__error-message { + color: hsl(0 72% 51%); + font-size: 12px; + user-select: none; +} + +.rating__control { + display: flex; + gap: 4px; +} + +.rating-item { + cursor: pointer; + fill: hsl(240 6% 90%); +} + +.rating-item:focus { + outline: none; +} + +[data-kb-theme="dark"] .rating-item { + fill: hsl(240 5% 26%); +} + +.rating-item[data-highlighted] { + fill: hsl(200 98% 39%); +} + +.half-star-icon > path + path { + fill: hsl(240 6% 90%); +} + +[data-kb-theme="dark"] .half-star-icon > path + path { + fill: hsl(240 5% 26%); +} + +[data-kb-theme="dark"] .rating__label { + color: hsl(240 5% 84%); +} + +[data-kb-theme="dark"] .rating__description { + color: hsl(240 5% 65%); +} diff --git a/apps/docs/src/examples/rating.tsx b/apps/docs/src/examples/rating.tsx new file mode 100644 index 000000000..f4f30ed2e --- /dev/null +++ b/apps/docs/src/examples/rating.tsx @@ -0,0 +1,235 @@ +import { Index, createSignal } from "solid-js"; +import { Rating } from "../../../../packages/core/src/rating"; + +import style from "./rating.module.css"; + +export function BasicExample() { + return ( + + + Rate Us: + + + + {(_) => ( + + + + + + )} + + + + ); +} + +export function DefaultValueExample() { + return ( + + + + {(_) => ( + + + + + + )} + + + + ); +} + +export function ControlledExample() { + const [value, setValue] = createSignal(0); + + return ( + <> + + + + {(_) => ( + + + + + + )} + + + +

Your rating is: {value()}/5

+ + ); +} + +export function HalfRatingsExample() { + return ( + + + + {(_) => ( + + + {(state) => (state.half() ? : )} + + + )} + + + + ); +} + +export function DescriptionExample() { + return ( + + + Rate Us: + + + + {(_) => ( + + + + + + )} + + + + Rate your experience with us. + + + ); +} + +export function ErrorMessageExample() { + const [value, setValue] = createSignal(0); + + return ( + + + Rate Us: + + + + {(_) => ( + + + + + + )} + + + + Please select a rating between 1 and 5. + + + ); +} + +export function HTMLFormExample() { + let formRef: HTMLFormElement | undefined; + + const onSubmit = (e: SubmitEvent) => { + e.preventDefault(); + e.stopPropagation(); + + const formData = new FormData(formRef); + + alert(JSON.stringify(Object.fromEntries(formData), null, 2)); + }; + + return ( +
+ + + + {(_) => ( + + + + + + )} + + + + +
+ + +
+
+ ); +} + +function StarIcon() { + return ( + + Star Icon + + + ); +} + +function StarHalfIcon() { + return ( + + Half Star Icon + + + + ); +} diff --git a/apps/docs/src/routes/docs/core/components/rating.mdx b/apps/docs/src/routes/docs/core/components/rating.mdx new file mode 100644 index 000000000..96f6f2d5f --- /dev/null +++ b/apps/docs/src/routes/docs/core/components/rating.mdx @@ -0,0 +1,423 @@ +import { Preview, TabsSnippets, Kbd } from "../../../../components"; +import { + BasicExample, + DefaultValueExample, + ControlledExample, + HalfRatingsExample, + DescriptionExample, + ErrorMessageExample, + HTMLFormExample, +} from "../../../../examples/rating"; + +# Rating Group + +Allows users to rate items using a set of icons. + +## Import + +```ts +import { Rating } from "@kobalte/core/rating"; +// or +import { Root, Label, ... } from "@kobalte/core/rating"; +``` + +## Features + +- Precise ratings with half-value increments. +- Syncs with form reset events. +- Group and rating labeling support for assistive technology. +- Can be controlled or uncontrolled. + +## Anatomy + +The rating group consists of: + +- **Rating**: The root container for the rating group. +- **Rating.Control**: The container for the rating items. +- **Rating.Label**: The label that gives the user information on the rating group. +- **Rating.HiddenInput**: The native html input that is visually hidden in the rating group. +- **Rating.Description**: The description that gives the user more information on the rating group. +- **Rating.ErrorMessage**: The error message that gives the user information about how to fix a validation error on the rating group. + +The rating item consists of: + +- **Rating.Item**: The root container for a rating item. +- **Rating.ItemControl**: The element that visually represents a rating item. +- **Rating.ItemLabel**: The label that gives the user information on the rating item. +- **Rating.ItemDescription**: The description that gives the user more information on the rating item. + +```tsx + + + + + + + + + + + + + +``` + +## Example + + + + + + + + index.tsx + style.css + + {/* */} + + ```tsx + import { Rating } from "@kobalte/core/rating"; + import "./style.css"; + + function App() { + return ( + + Rate Us: + + + {_ => ( + + + + + + )} + + + + ); + } + ``` + + + +```css +.rating { + display: flex; + flex-direction: column; + gap: 8px; +} + +.rating\_\_label { +color: hsl(240 6% 10%); +font-size: 14px; +font-weight: 500; +user-select: none; +} + +.rating\_\_description { +color: hsl(240 5% 26%); +font-size: 12px; +user-select: none; +} + +.rating\_\_error-message { +color: hsl(0 72% 51%); +font-size: 12px; +user-select: none; +} + +.rating\_\_control { +display: flex; +gap: 4px; +} + +.rating-item { +cursor: pointer; +fill: hsl(240 6% 90%); +transition: fill 200ms cubic-bezier(0.2, 0, 0, 1); +} + +.rating-item[data-highlighted] { +fill: hsl(200 98% 39%); +} + +```` + + + {/* */} + + +## Usage + +### Default value + +An initial, uncontrolled value can be provided using the `defaultValue` prop. + + + + + +```tsx {0} + + + + {_ => ( + + + + + + )} + + + +```` + +### Controlled value + +The `value` prop can be used to make the value controlled. The `onChange` event is fired when the user selects a rating, and receives the new value. + + + + + +```tsx {3,7} +import { createSignal } from "solid-js"; + +function ControlledExample() { + const [value, setValue] = createSignal(0); + + return ( + <> + + + + {_ => ( + + + + + + )} + + + +

Your rating is: {value()}/5

+ + ); +} +``` + +### Half Ratings + +Allow 0.5 value steps by setting the `allowHalf` prop to true. + + + + + +```tsx{0} + + + + {_ => ( + + + {(state) => (state.half() ? : )} + + + )} + + + +``` + +### Description + +The `Rating.Description` component can be used to associate additional help text with a rating group. + + + + + +```tsx {13} + + Rate Us: + + + {_ => ( + + + + + + )} + + + Rate your experience with us. + +``` + +### Error message + +The `Rating.ErrorMessage` component can be used to help the user fix a validation error. It should be combined with the `validationState` prop to semantically mark the rating group as invalid for assistive technologies. + +By default, it will render only when the `validationState` prop is set to `invalid`, use the `forceMount` prop to always render the error message (ex: for usage with animation libraries). + + + + + +```tsx {9,23-25} +import { createSignal } from "solid-js"; + +function ErrorMessageExample() { + const [value, setValue] = createSignal(0); + + return ( + + Rate Us: + + + {_ => ( + + + + + + )} + + + Please select a rating between 1 and 5. + + ); +} +``` + +### HTML forms + +The `name` prop can be used for integration with HTML forms. + + + + + +```tsx {7,19} +function HTMLFormExample() { + const onSubmit = (e: SubmitEvent) => { + // handle form submission. + }; + + return ( +
+ + + + {_ => ( + + + + + + )} + + + + +
+ + +
+
+ ); +} +``` + +## API Reference + +### Rating + +`Rating` is equivalent to the `Root` import from `@kobalte/core/rating`. + +| Prop | Description | +| :-------------- | :------------------------------------------------------------------------------------------------------------------------------------------------- | +| value | `number`
The current rating value. | +| defaultValue | `number`
The initial value of the rating group when it is first rendered. Use when you do not need to control the state of the rating group. | +| onChange | `(value: number) => void`
Event handler called when the value changes. | +| allowHalf | `boolean`
Whether to allow half ratings. | +| orientation | `'horizontal' \| 'vertical'`
The axis the rating group items should align with. | +| name | `string`
The name of the rating group. Submitted with its owning form as part of a name/value pair. | +| validationState | `'valid' \| 'invalid'`
Whether the rating group should display its "valid" or "invalid" visual styling. | +| required | `boolean`
Whether the user must select an item before the owning form can be submitted. | +| disabled | `boolean`
Whether the rating group is disabled. | +| readOnly | `boolean`
Whether the rating group items can be selected but not changed by the user. | + +| Data attribute | Description | +| :------------- | :--------------------------------------------------------------------------------------------- | +| data-valid | Present when the rating group is valid according to the validation rules. | +| data-invalid | Present when the rating group is invalid according to the validation rules. | +| data-required | Present when the user must select a rating group item before the owning form can be submitted. | +| data-disabled | Present when the rating group is disabled. | +| data-readonly | Present when the rating group is read only. | + +`Rating.Label`, `Rating.Description` and `Rating.ErrorMesssage` shares the same data-attributes. + +### Rating.ErrorMessage + +| Prop | Description | +| :--------- | :-------------------------------------------------------------------------------------------------------------------------------------- | +| forceMount | `boolean`
Used to force mounting when more control is needed. Useful when controlling animation with SolidJS animation libraries. | + +### Rating.ItemControl + +| Render Prop | Description | +| :---------- | :---------------------------------------------------------------- | +| half | `Accessor`
Whether the rating item is half. | +| highlighted | `Accessor`
Whether the rating item is highlighted. | + +| Data attribute | Description | +| :--------------- | :--------------------------------------------------------------------------------- | +| data-valid | Present when the parent rating group is valid according to the validation rules. | +| data-invalid | Present when the parent rating group is invalid according to the validation rules. | +| data-required | Present when the parent rating group is required. | +| data-disabled | Present when the parent rating group is disabled. | +| data-readonly | Present when the parent rating group is read only. | +| data-checked | Present when the rating is checked. | +| data-half | Present when the rating is half. | +| data-highlighted | Present when the rating is highlighted. | + +`Rating.ItemLabel` and `Rating.ItemDescription` share the same data-attributes. + +## Rendered elements + +| Component | Default rendered element | +| :---------------------------- | :----------------------- | +| `Rating` | `div` | +| `Rating.Control` | `div` | +| `Rating.Label` | `span` | +| `Rating.HiddenInput` | `input` | +| `Rating.Description` | `div` | +| `Rating.ErrorMessage` | `div` | +| `Rating.Item` | `div` | +| `Rating.ItemControl` | `div` | +| `Rating.ItemLabel` | `label` | +| `Rating.ItemDescription` | `div` | + +## Accessibility + +### Keyboard Interactions + +| Key | Description | +| :-------------------- | :----------------------------------------------------------------------------------------------- | +| ArrowDown | Moves focus to the next item, increasing the rating value based on the `allowHalf` property. | +| ArrowRight | Moves focus to the next item, increasing the rating value based on the `allowHalf` property. | +| ArrowUp | Moves focus to the previous item, decreasing the rating value based on the `allowHalf` property. | +| ArrowLeft | Moves focus to the previous item, decreasing the rating value based on the `allowHalf` property. | +| Space | Selects the focused item in the rating group. | +| Home | Sets the value of the rating group to 1. | +| End | Sets the value of the rating group to the maximum value. | From 76151c7b0d9e99d6fbec4d4134f8d44e85f74c84 Mon Sep 17 00:00:00 2001 From: jer3m01 Date: Sun, 27 Jul 2025 23:44:22 +0200 Subject: [PATCH 03/12] style: format --- apps/docs/src/examples/rating.tsx | 46 ++++++++----------- .../routes/docs/core/components/rating.mdx | 4 +- packages/core/src/rating/rating-control.tsx | 10 ++-- .../core/src/rating/rating-item-context.tsx | 3 +- .../core/src/rating/rating-item-label.tsx | 3 +- packages/core/src/rating/rating-item.tsx | 11 ++--- packages/core/src/rating/rating-label.tsx | 10 ++-- packages/core/src/rating/rating-root.tsx | 9 +--- 8 files changed, 33 insertions(+), 63 deletions(-) diff --git a/apps/docs/src/examples/rating.tsx b/apps/docs/src/examples/rating.tsx index f4f30ed2e..d02616539 100644 --- a/apps/docs/src/examples/rating.tsx +++ b/apps/docs/src/examples/rating.tsx @@ -5,11 +5,9 @@ import style from "./rating.module.css"; export function BasicExample() { return ( - - - Rate Us: - - + + Rate Us: + {(_) => ( @@ -26,8 +24,8 @@ export function BasicExample() { export function DefaultValueExample() { return ( - - + + {(_) => ( @@ -47,12 +45,8 @@ export function ControlledExample() { return ( <> - - + + {(_) => ( @@ -71,8 +65,8 @@ export function ControlledExample() { export function HalfRatingsExample() { return ( - - + + {(_) => ( @@ -89,11 +83,9 @@ export function HalfRatingsExample() { export function DescriptionExample() { return ( - - - Rate Us: - - + + Rate Us: + {(_) => ( @@ -104,7 +96,7 @@ export function DescriptionExample() { )} - + Rate your experience with us. @@ -116,15 +108,13 @@ export function ErrorMessageExample() { return ( - - Rate Us: - - + Rate Us: + {(_) => ( @@ -160,8 +150,8 @@ export function HTMLFormExample() { onSubmit={onSubmit} class="flex flex-col items-center space-y-6" > - - + + {(_) => ( diff --git a/apps/docs/src/routes/docs/core/components/rating.mdx b/apps/docs/src/routes/docs/core/components/rating.mdx index 96f6f2d5f..83ab2d097 100644 --- a/apps/docs/src/routes/docs/core/components/rating.mdx +++ b/apps/docs/src/routes/docs/core/components/rating.mdx @@ -395,8 +395,8 @@ function HTMLFormExample() { ## Rendered elements -| Component | Default rendered element | -| :---------------------------- | :----------------------- | +| Component | Default rendered element | +| :----------------------- | :----------------------- | | `Rating` | `div` | | `Rating.Control` | `div` | | `Rating.Label` | `span` | diff --git a/packages/core/src/rating/rating-control.tsx b/packages/core/src/rating/rating-control.tsx index b60b86679..b01690c42 100644 --- a/packages/core/src/rating/rating-control.tsx +++ b/packages/core/src/rating/rating-control.tsx @@ -10,22 +10,18 @@ import { useRatingContext } from "./rating-context"; export interface RatingControlOptions {} -export interface RatingControlCommonProps< - T extends HTMLElement = HTMLElement, -> { +export interface RatingControlCommonProps { id: string; onPointerLeave: JSX.EventHandlerUnion; } -export interface RatingControlRenderProps - extends RatingControlCommonProps { +export interface RatingControlRenderProps extends RatingControlCommonProps { role: "presentation"; } export type RatingControlProps< T extends ValidComponent | HTMLElement = HTMLElement, -> = RatingControlOptions & - Partial>>; +> = RatingControlOptions & Partial>>; export function RatingControl( props: PolymorphicProps>, diff --git a/packages/core/src/rating/rating-item-context.tsx b/packages/core/src/rating/rating-item-context.tsx index 8874a7811..83d6711eb 100644 --- a/packages/core/src/rating/rating-item-context.tsx +++ b/packages/core/src/rating/rating-item-context.tsx @@ -21,8 +21,7 @@ export interface RatingItemContextValue { registerDescription: (id: string) => () => void; } -export const RatingItemContext = - createContext(); +export const RatingItemContext = createContext(); export function useRatingItemContext() { const context = useContext(RatingItemContext); diff --git a/packages/core/src/rating/rating-item-label.tsx b/packages/core/src/rating/rating-item-label.tsx index 3822b0000..a8c36d0f8 100644 --- a/packages/core/src/rating/rating-item-label.tsx +++ b/packages/core/src/rating/rating-item-label.tsx @@ -34,8 +34,7 @@ export interface RatingItemLabelRenderProps export type RatingItemLabelProps< T extends ValidComponent | HTMLElement = HTMLElement, -> = RatingItemLabelOptions & - Partial>>; +> = RatingItemLabelOptions & Partial>>; export function RatingItemLabel( props: PolymorphicProps>, diff --git a/packages/core/src/rating/rating-item.tsx b/packages/core/src/rating/rating-item.tsx index f54d58622..bbd04993f 100644 --- a/packages/core/src/rating/rating-item.tsx +++ b/packages/core/src/rating/rating-item.tsx @@ -35,9 +35,7 @@ import { getEventPoint, getRelativePoint } from "./utils"; export interface RatingItemOptions {} -export interface RatingItemCommonProps< - T extends HTMLElement = HTMLElement, -> { +export interface RatingItemCommonProps { id: string; ref: T | ((el: T) => void); "aria-labelledby": string | undefined; @@ -157,9 +155,7 @@ export function RatingItem( }; const focusItem = (index: number) => - ( - RatingContext.items()[Math.round(index)].ref() as HTMLElement - ).focus(); + (RatingContext.items()[Math.round(index)].ref() as HTMLElement).focus(); const setPrevValue = () => { const factor = RatingContext.allowHalf() ? 0.5 : 1; @@ -172,8 +168,7 @@ export function RatingItem( const factor = RatingContext.allowHalf() ? 0.5 : 1; const value = Math.min( RatingContext.items().length, - (RatingContext.value() === -1 ? 0 : RatingContext.value())! + - factor, + (RatingContext.value() === -1 ? 0 : RatingContext.value())! + factor, ); RatingContext.setValue(value); focusItem(value - 1); diff --git a/packages/core/src/rating/rating-label.tsx b/packages/core/src/rating/rating-label.tsx index 798cd90c9..47fd866d5 100644 --- a/packages/core/src/rating/rating-label.tsx +++ b/packages/core/src/rating/rating-label.tsx @@ -5,17 +5,13 @@ import type { ElementOf, PolymorphicProps } from "../polymorphic"; export interface RatingLabelOptions {} -export interface RatingLabelCommonProps< - T extends HTMLElement = HTMLElement, -> {} +export interface RatingLabelCommonProps {} -export interface RatingLabelRenderProps - extends RatingLabelCommonProps {} +export interface RatingLabelRenderProps extends RatingLabelCommonProps {} export type RatingLabelProps< T extends ValidComponent | HTMLElement = HTMLElement, -> = RatingLabelOptions & - Partial>>; +> = RatingLabelOptions & Partial>>; export function RatingLabel( props: PolymorphicProps>, diff --git a/packages/core/src/rating/rating-root.tsx b/packages/core/src/rating/rating-root.tsx index 43d39e6e0..e3b6b3b4c 100644 --- a/packages/core/src/rating/rating-root.tsx +++ b/packages/core/src/rating/rating-root.tsx @@ -29,10 +29,7 @@ import { createFormResetListener, } from "../primitives"; import { createDomCollection } from "../primitives/create-dom-collection"; -import { - RatingContext, - type RatingContextValue, -} from "./rating-context"; +import { RatingContext, type RatingContextValue } from "./rating-context"; export interface RatingRootOptions { /** The current rating value. */ @@ -79,9 +76,7 @@ export interface RatingRootOptions { readOnly?: boolean; } -export interface RatingRootCommonProps< - T extends HTMLElement = HTMLElement, -> { +export interface RatingRootCommonProps { id: string; ref: T | ((el: T) => void); "aria-labelledby": string | undefined; From d746848da3fe7b132a451d322b32530f154c5da2 Mon Sep 17 00:00:00 2001 From: jer3m01 Date: Sun, 27 Jul 2025 23:51:42 +0200 Subject: [PATCH 04/12] fix(breadcrumbs): override separator using children --- packages/core/src/breadcrumbs/breadcrumbs-separator.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/src/breadcrumbs/breadcrumbs-separator.tsx b/packages/core/src/breadcrumbs/breadcrumbs-separator.tsx index d9326061f..536e71161 100644 --- a/packages/core/src/breadcrumbs/breadcrumbs-separator.tsx +++ b/packages/core/src/breadcrumbs/breadcrumbs-separator.tsx @@ -40,7 +40,7 @@ export function BreadcrumbsSeparator( aria-hidden="true" {...(props as BreadcrumbsSeparatorProps)} > - {context.separator()} + {props.children ?? context.separator()} ); } From 8adf3abd4e51a50954867f2a10517ee82dd0b740 Mon Sep 17 00:00:00 2001 From: jer3m01 Date: Mon, 28 Jul 2025 18:13:34 +0200 Subject: [PATCH 05/12] fix(pagination): fixed items --- .../core/src/pagination/pagination-items.tsx | 28 +++++++++++-------- 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/packages/core/src/pagination/pagination-items.tsx b/packages/core/src/pagination/pagination-items.tsx index 63475358b..09c3a4279 100644 --- a/packages/core/src/pagination/pagination-items.tsx +++ b/packages/core/src/pagination/pagination-items.tsx @@ -24,8 +24,11 @@ export function PaginationItems(props: PaginationItemsProps) { const _showFirst = showFirst() && page() - 1 > siblingCount(); const _showLast = showLast() && count() - page() > siblingCount(); - let showFirstEllipsis = page() - (showFirst() ? 2 : 1) > siblingCount(); + let showFirstEllipsis = + fixedItems() !== "no-ellipsis" && + page() - (showFirst() ? 2 : 1) > siblingCount(); let showLastEllipsis = + fixedItems() !== "no-ellipsis" && count() - page() - (showLast() ? 1 : 0) > siblingCount(); let previousSiblingCount = Math.min(page() - 1, siblingCount()); @@ -40,23 +43,24 @@ export function PaginationItems(props: PaginationItemsProps) { previousSiblingCount += Math.max(siblingCount() - nextSiblingCountRef, 0); nextSiblingCount += Math.max(siblingCount() - previousSiblingCountRef, 0); - if (!_showFirst) nextSiblingCount++; - if (!_showLast) previousSiblingCount++; + if (showFirst() && !_showFirst) nextSiblingCount++; + if (showLast() && !_showLast) previousSiblingCount++; // Check specifically if true and not "no-ellipsis" if (fixedItems() === true) { if (!showFirstEllipsis) nextSiblingCount++; if (!showLastEllipsis) previousSiblingCount++; - } - //replace ellipsis if it would replace only one item - if (page() - previousSiblingCount - (showFirst() ? 2 : 1) === 1) { - showFirstEllipsis = false; - previousSiblingCount++; - } - if (count() - page() - nextSiblingCount - (showLast() ? 1 : 0) === 1) { - showLastEllipsis = false; - nextSiblingCount++; + //replace ellipsis if it would replace only one item + if (page() - previousSiblingCount - (showFirst() ? 2 : 1) === 1) { + showFirstEllipsis = false; + previousSiblingCount++; + } + + if (count() - page() - nextSiblingCount - (showLast() ? 1 : 0) === 1) { + showLastEllipsis = false; + nextSiblingCount++; + } } } From fb41819160565322ffca8225172009fcbbefbcf4 Mon Sep 17 00:00:00 2001 From: jer3m01 Date: Wed, 8 Oct 2025 00:32:31 +0200 Subject: [PATCH 06/12] refactor(time-field): remove @internationalized/date --- .../core/src/i18n/create-date-formatter.ts | 11 +- .../src/time-field/time-field-context.tsx | 11 +- .../time-field/time-field-field-context.tsx | 3 +- .../core/src/time-field/time-field-field.tsx | 154 ++++++++---------- .../core/src/time-field/time-field-root.tsx | 51 +++--- packages/core/src/time-field/types.ts | 16 -- packages/core/src/time-field/utils.ts | 55 +++---- 7 files changed, 123 insertions(+), 178 deletions(-) diff --git a/packages/core/src/i18n/create-date-formatter.ts b/packages/core/src/i18n/create-date-formatter.ts index 4d178ac93..6c3aab422 100644 --- a/packages/core/src/i18n/create-date-formatter.ts +++ b/packages/core/src/i18n/create-date-formatter.ts @@ -6,25 +6,20 @@ * https://github.com/adobe/react-spectrum/blob/15e101b74966bd5eb719c6529ce71ce57eaed430/packages/@react-aria/i18n/src/useDateFormatter.ts */ -import { DateFormatter } from "@internationalized/date"; import { type MaybeAccessor, access } from "@kobalte/utils"; import { type Accessor, createMemo } from "solid-js"; import { useLocale } from "./i18n-provider"; -export interface DateFormatterOptions extends Intl.DateTimeFormatOptions { - calendar?: string; -} - /** * Provides localized date formatting for the current locale. Automatically updates when the locale changes, * and handles caching of the date formatter for performance. * @param options - Formatting options. */ export function createDateFormatter( - options: MaybeAccessor, -): Accessor { + options: MaybeAccessor, +): Accessor { const { locale } = useLocale(); - return createMemo(() => new DateFormatter(locale(), access(options))); + return createMemo(() => new Intl.DateTimeFormat(locale(), access(options))); } diff --git a/packages/core/src/time-field/time-field-context.tsx b/packages/core/src/time-field/time-field-context.tsx index 0631fe7ed..94a6df625 100644 --- a/packages/core/src/time-field/time-field-context.tsx +++ b/packages/core/src/time-field/time-field-context.tsx @@ -4,20 +4,19 @@ import type { TimeFieldIntlTranslations } from "./time-field.intl"; import type { TimeFieldGranularity, TimeFieldHourCycle, - TimeValue, } from "./types"; export interface TimeFieldContextValue { translations: Accessor; - value: Accessor; - setValue: (value: TimeValue | undefined) => void; + value: Accessor; + setValue: (value: Date | undefined) => void; hourCycle: Accessor; granularity: Accessor; hideTimeZone: Accessor; shouldForceLeadingZeros: Accessor; - placeholderTime: Accessor; - placeholderValue: Accessor; - defaultTimeZone: Accessor; + placeholderTime: Accessor; + placeholderValue: Accessor; + // defaultTimeZone: Accessor; formattedValue: Accessor; isDisabled: Accessor; focusManager: Accessor; diff --git a/packages/core/src/time-field/time-field-field-context.tsx b/packages/core/src/time-field/time-field-field-context.tsx index 0aa84e698..83956a8d5 100644 --- a/packages/core/src/time-field/time-field-field-context.tsx +++ b/packages/core/src/time-field/time-field-field-context.tsx @@ -1,10 +1,9 @@ -import type { ResolvedDateTimeFormatOptions } from "@internationalized/date"; import { type Accessor, createContext, useContext } from "solid-js"; import type { SegmentType, TimeSegment } from "./types"; export interface TimeFieldFieldContextValue { dateValue: Accessor; - dateFormatterResolvedOptions: Accessor; + dateFormatterResolvedOptions: Accessor; segments: Accessor; ariaLabel: Accessor; ariaLabelledBy: Accessor; diff --git a/packages/core/src/time-field/time-field-field.tsx b/packages/core/src/time-field/time-field-field.tsx index 00828c486..5047f8fc9 100644 --- a/packages/core/src/time-field/time-field-field.tsx +++ b/packages/core/src/time-field/time-field-field.tsx @@ -7,7 +7,6 @@ * https://github.com/adobe/react-spectrum/blob/a98da553e73ca70bb3215a106878fa98fac826f2/packages/%40react-aria/datepicker/src/useDateField.ts */ -import { DateFormatter } from "@internationalized/date"; import { callHandler, createGenerateId, @@ -25,7 +24,7 @@ import { } from "solid-js"; import { useFormControlContext } from "../form-control"; -import { useLocale } from "../i18n"; +import { createDateFormatter, useLocale } from "../i18n"; import { type ElementOf, Polymorphic, @@ -40,11 +39,9 @@ import type { FormatterOptions, SegmentType, TimeSegment, - TimeValue, } from "./types"; import { - convertValue, - createPlaceholderDate, + emptyDateTime, getTimeFieldFormatOptions, } from "./utils"; @@ -76,8 +73,6 @@ export type TimeFieldFieldProps< export function TimeFieldField( props: PolymorphicProps>, ) { - let ref: HTMLElement | undefined; - const formControlContext = useFormControlContext(); const timeFieldContext = useTimeFieldContext(); @@ -97,19 +92,13 @@ export function TimeFieldField( "aria-describedby", ]); - const { locale, direction } = useLocale(); + const { direction } = useLocale(); const val = createMemo( () => timeFieldContext.value() || timeFieldContext.placeholderTime(), ); - const [placeholderDate, setPlaceholderDate] = createSignal( - createPlaceholderDate( - val(), - timeFieldContext.placeholderTime(), - timeFieldContext.defaultTimeZone(), - ), - ); + const [placeholderDate, setPlaceholderDate] = createSignal(emptyDateTime()); const formatOpts: Accessor = createMemo(() => ({ granularity: timeFieldContext.granularity(), @@ -122,7 +111,7 @@ export function TimeFieldField( const opts = createMemo(() => getTimeFieldFormatOptions(formatOpts())); - const dateFormatter = createMemo(() => new DateFormatter(locale(), opts())); + const dateFormatter = createDateFormatter(opts); const resolvedOptions = createMemo(() => dateFormatter().resolvedOptions()); const ariaLabelledBy = createMemo(() => { @@ -168,7 +157,7 @@ export function TimeFieldField( : placeholderDate(); }); - const setValue = (newValue: TimeValue) => { + const setValue = (newValue: Date) => { if (formControlContext.isDisabled() || formControlContext.isReadOnly()) { return; } @@ -177,28 +166,18 @@ export function TimeFieldField( Object.keys(validSegments()).length >= Object.keys(allSegments()).length ) { timeFieldContext.setValue(newValue); - setPlaceholderDate(convertValue(newValue)); - } else { - setPlaceholderDate(convertValue(newValue)); - } + } + setPlaceholderDate(newValue); }; - const dateValue = createMemo(() => - convertValue(displayValue())?.toDate( - timeFieldContext.defaultTimeZone() ?? "UTC", - ), - ); - const segments = createMemo(() => { - const resolvedDateValue = dateValue(); - const resolvedDisplayValue = displayValue(); - if (!resolvedDateValue || !resolvedDisplayValue) { + if (!displayValue()) { return []; } return dateFormatter() - .formatToParts(resolvedDateValue) + .formatToParts(displayValue()) .map((segment) => { const isOriginallyEditable = EDITABLE_SEGMENTS[segment.type as keyof typeof EDITABLE_SEGMENTS]; @@ -221,7 +200,7 @@ export function TimeFieldField( segment.type, text: isPlaceholder ? placeholder : segment.value, ...getSegmentLimits( - resolvedDisplayValue, + displayValue()!, segment.type, resolvedOptions(), ), @@ -259,7 +238,6 @@ export function TimeFieldField( resolvedDisplayValue, type, amount, - resolvedOptions(), ); if (newValue) { @@ -310,33 +288,37 @@ export function TimeFieldField( return newValue; }); - const placeholder = createPlaceholderDate( - val(), - timeFieldContext.placeholderTime(), - timeFieldContext.defaultTimeZone(), - ); + const placeholder = emptyDateTime(); const resolvedDisplayValue = displayValue(); - let value = resolvedDisplayValue; + let value = resolvedDisplayValue || emptyDateTime(); if (resolvedDisplayValue && placeholder) { if (part === "dayPeriod") { - const isPM = resolvedDisplayValue.hour >= 12; - const shouldBePM = placeholder.hour >= 12; + const isPM = resolvedDisplayValue.getHours() >= 12; + const shouldBePM = placeholder.getHours() >= 12; if (isPM && !shouldBePM) { - value = resolvedDisplayValue.set({ - hour: resolvedDisplayValue.hour - 12, - }); + value.setHours( + resolvedDisplayValue.getHours() - 12, + ); } else if (!isPM && shouldBePM) { - value = resolvedDisplayValue.set({ - hour: resolvedDisplayValue.hour + 12, - }); + value.setHours( + resolvedDisplayValue.getHours() + 12, + ); } } else if (part in resolvedDisplayValue) { - value = resolvedDisplayValue.set({ - [part]: placeholder[part as keyof typeof placeholder], - }); + switch (part) { + case "hour": + value.setHours(placeholder.getHours()); + break; + case "minute": + value.setMinutes(placeholder.getMinutes()); + break; + case "second": + value.setSeconds(placeholder.getSeconds()); + break; + } } } timeFieldContext.setValue(undefined); @@ -392,13 +374,13 @@ export function TimeFieldField( const resolvedDisplayValue = displayValue(); if (resolvedDisplayValue) { - setValue(resolvedDisplayValue.copy()); + setValue(resolvedDisplayValue); } } }; const context: TimeFieldFieldContextValue = { - dateValue, + dateValue: displayValue, dateFormatterResolvedOptions: resolvedOptions, ariaLabel: () => others["aria-label"], ariaLabelledBy, @@ -418,10 +400,7 @@ export function TimeFieldField( as="div" role="presentation" - ref={mergeRefs((el) => { - timeFieldContext.setInputRef(el); - ref = el; - }, local.ref)} + ref={mergeRefs(timeFieldContext.setInputRef, local.ref)} aria-labelledby={ariaLabelledBy()} aria-describedby={ariaDescribedBy()} onKeyDown={onKeyDown} @@ -455,41 +434,41 @@ const TYPE_MAPPING = { }; function getSegmentLimits( - date: TimeValue, + date: Date, type: string, options: Intl.ResolvedDateTimeFormatOptions, ) { switch (type) { case "dayPeriod": return { - value: date.hour >= 12 ? 12 : 0, + value: date.getHours() >= 12 ? 12 : 0, minValue: 0, maxValue: 12, }; case "hour": if (options.hour12) { - const isPM = date.hour >= 12; + const isPM = date.getHours() >= 12; return { - value: date.hour, + value: date.getHours(), minValue: isPM ? 12 : 0, maxValue: isPM ? 23 : 11, }; } return { - value: date.hour, + value: date.getHours(), minValue: 0, maxValue: 23, }; case "minute": return { - value: date.minute, + value: date.getMinutes(), minValue: 0, maxValue: 59, }; case "second": return { - value: date.second, + value: date.getSeconds(), minValue: 0, maxValue: 59, }; @@ -499,31 +478,33 @@ function getSegmentLimits( } function addSegment( - value: TimeValue, + value: Date, part: string, amount: number, - options: Intl.ResolvedDateTimeFormatOptions, ) { if ("hour" in value) { switch (part) { case "dayPeriod": { - const hours = value.hour; + const hours = value.getHours(); const isPM = hours >= 12; - return value.set({ hour: isPM ? hours - 12 : hours + 12 }); + value.setHours(isPM ? hours - 12 : hours + 12); + return value; } case "hour": + value.setHours((value.getHours() + amount) % 24); + return value; case "minute": + value.setMinutes((value.getMinutes() + amount) % 60); + return value; case "second": - return value.cycle(part, amount, { - round: part !== "hour", - hourCycle: options.hour12 ? 12 : 24, - }); + value.setSeconds((value.getSeconds() + amount) % 60); + return value; } } } function setSegmentBase( - value: TimeValue, + value: Date, part: string, segmentValue: number, options: Intl.ResolvedDateTimeFormatOptions, @@ -531,18 +512,19 @@ function setSegmentBase( if ("hour" in value) { switch (part) { case "dayPeriod": { - const hours = value.hour; + const hours = value.getHours(); const wasPM = hours >= 12; const isPM = segmentValue >= 12; if (isPM === wasPM) { return value; } - return value.set({ hour: wasPM ? hours - 12 : hours + 12 }); + value.setHours(wasPM ? hours - 12 : hours + 12); + return value; } case "hour": if (options.hour12) { - const hours = value.hour; + const hours = value.getHours(); const wasPM = hours >= 12; if (!wasPM && segmentValue === 12) { // biome-ignore lint/style/noParameterAssign: used in fallthrough @@ -553,10 +535,14 @@ function setSegmentBase( segmentValue += 12; } } - return value.set({ [part]: segmentValue }); + value.setHours(segmentValue); + return value; case "minute": + value.setMinutes(segmentValue); + return value; case "second": - return value.set({ [part]: segmentValue }); + value.setSeconds(segmentValue); + return value; } } } @@ -564,23 +550,23 @@ function setSegmentBase( export function getPlaceholder( field: string, value: string, - placeholderValue?: TimeValue, + placeholderValue?: Date, ) { if (field === "dayPeriod") { return value; } if (placeholderValue) { - if (placeholderValue.hour && field === "hour") { - return String(placeholderValue.hour); + if (placeholderValue.getHours() && field === "hour") { + return String(placeholderValue.getHours()); } - if (placeholderValue.minute && field === "minute") { - return String(placeholderValue.minute); + if (placeholderValue.getMinutes() && field === "minute") { + return String(placeholderValue.getMinutes()); } - if (placeholderValue.second && field === "second") { - return String(placeholderValue.second); + if (placeholderValue.getSeconds() && field === "second") { + return String(placeholderValue.getSeconds()); } } diff --git a/packages/core/src/time-field/time-field-root.tsx b/packages/core/src/time-field/time-field-root.tsx index 05ab64ef2..5c42e45ba 100644 --- a/packages/core/src/time-field/time-field-root.tsx +++ b/packages/core/src/time-field/time-field-root.tsx @@ -1,4 +1,3 @@ -import { DateFormatter, Time } from "@internationalized/date"; import { type ValidationState, access, @@ -43,26 +42,24 @@ import { type TimeFieldIntlTranslations, } from "./time-field.intl"; import type { - MappedTimeValue, TimeFieldGranularity, TimeFieldHourCycle, - TimeValue, } from "./types"; import { - convertValue, createDefaultProps, + emptyDateTime, getTimeFieldFormatOptions, } from "./utils"; export interface TimeFieldRootOptions { /** The current value (controlled). */ - value?: TimeValue; + value?: Date; /** The default value (uncontrolled). */ - defaultValue?: TimeValue; + defaultValue?: Date; /** Handler that is called when the value changes. */ - onChange?: (value: MappedTimeValue) => void; + onChange?: (value: Date) => void; /** * Whether to display the time in 12 or 24-hour format. @@ -89,13 +86,13 @@ export interface TimeFieldRootOptions { * A placeholder time that influences the format of the placeholder shown when no value is selected. * Defaults to 12:00 AM or 00:00 depending on the hour cycle. */ - placeholderValue?: TimeValue; + placeholderValue?: Date; /** The minimum allowed time that a user may select. */ - minValue?: TimeValue; + minValue?: Date; /** The maximum allowed time that a user may select. */ - maxValue?: TimeValue; + maxValue?: Date; /** * A unique identifier for the component. @@ -152,7 +149,7 @@ export type TimeFieldRootProps< export function TimeFieldRoot( props: PolymorphicProps>, ) { - let ref: HTMLElement | undefined; + const [ref, setRef] = createSignal(); const defaultId = `time-field-${createUniqueId()}`; @@ -195,21 +192,21 @@ export function TimeFieldRoot( const focusManager = createFocusManager(inputRef); - const [value, setValue] = createControllableSignal({ + const [value, setValue] = createControllableSignal({ value: () => local.value, defaultValue: () => local.defaultValue, onChange: (value) => local.onChange?.(value!), }); - const { granularity, defaultTimeZone } = createDefaultProps({ + const { granularity } = createDefaultProps({ value: () => value() ?? local.placeholderValue, granularity: () => local.granularity, }); createFormResetListener( - () => ref, + ref, () => { - setValue(local.defaultValue ?? new Time()); + setValue(local.defaultValue ?? emptyDateTime()); }, ); @@ -218,19 +215,17 @@ export function TimeFieldRoot( return local.validationState; } - const v = value() || local.placeholderValue; - const day = v && "day" in v ? v : undefined; - const minDate = convertValue(local.minValue, day); - const maxDate = convertValue(local.maxValue, day); + const minDate = local.minValue; + const maxDate = local.maxValue; const rangeOverflow = value() != null && minDate != null && - value()!.compare(convertValue(local.maxValue)!) > 0; + value()! > (local.maxValue ?? Number.POSITIVE_INFINITY); const rangeUnderflow = value() != null && maxDate != null && - value()!.compare(convertValue(local.minValue)!) < 0; + value()! < (local.minValue ?? Number.NEGATIVE_INFINITY); return rangeOverflow || rangeUnderflow ? "invalid" : undefined; }); @@ -246,19 +241,17 @@ export function TimeFieldRoot( const formattedValue = createMemo(() => { const formatOptions = getTimeFieldFormatOptions({ granularity: granularity(), - timeZone: defaultTimeZone(), + // timeZone: defaultTimeZone(), hideTimeZone: local.hideTimeZone, hourCycle: local.hourCycle, }); const dateFormatter = createMemo( - () => new DateFormatter(locale(), formatOptions), + () => new Intl.DateTimeFormat(locale(), formatOptions), ); if (value()) { - return dateFormatter().format( - convertValue(value())!.toDate(defaultTimeZone() ?? "UTC"), - ); + return dateFormatter().format(value()); } return ""; @@ -291,9 +284,9 @@ export function TimeFieldRoot( granularity, hideTimeZone: () => local.hideTimeZone ?? false, shouldForceLeadingZeros: () => local.shouldForceLeadingZeros ?? false, - placeholderTime: () => value() || (local.placeholderValue ?? new Time()), + placeholderTime: () => value() || (local.placeholderValue ?? emptyDateTime()), placeholderValue: () => local.placeholderValue, - defaultTimeZone, + // defaultTimeZone, formattedValue, focusManager: () => focusManager, isDisabled: () => formControlContext.isDisabled() ?? false, @@ -310,7 +303,7 @@ export function TimeFieldRoot( as="div" - ref={mergeRefs((el) => (ref = el), local.ref)} + ref={mergeRefs(setRef, local.ref)} role="group" id={access(formControlProps.id)!} aria-invalid={ diff --git a/packages/core/src/time-field/types.ts b/packages/core/src/time-field/types.ts index fd35dfa1d..4e8d8c2a1 100644 --- a/packages/core/src/time-field/types.ts +++ b/packages/core/src/time-field/types.ts @@ -1,23 +1,7 @@ -import type { - CalendarDateTime, - Time, - ZonedDateTime, -} from "@internationalized/date"; - export type TimeFieldGranularity = "hour" | "minute" | "second"; export type TimeFieldHourCycle = 12 | 24; -export type TimeValue = Time | CalendarDateTime | ZonedDateTime; - -export type MappedTimeValue = T extends ZonedDateTime - ? ZonedDateTime - : T extends CalendarDateTime - ? CalendarDateTime - : T extends Time - ? Time - : never; - export type SegmentType = | "hour" | "minute" diff --git a/packages/core/src/time-field/utils.ts b/packages/core/src/time-field/utils.ts index 9e0952c99..dbcbccc81 100644 --- a/packages/core/src/time-field/utils.ts +++ b/packages/core/src/time-field/utils.ts @@ -9,14 +9,13 @@ import { type Accessor, createEffect, createMemo } from "solid-js"; import type { FormatterOptions, TimeFieldGranularity, - TimeValue, } from "./types"; export function createDefaultProps(props: { - value: Accessor; + value: Accessor; granularity: Accessor; }) { - let lastValue: TimeValue; + let lastValue: Date; const value = createMemo(() => { const resolvedValue = props.value(); @@ -28,15 +27,15 @@ export function createDefaultProps(props: { return lastValue; }); - const defaultTimeZone = createMemo(() => { - const resolvedValue = value(); + // const defaultTimeZone = createMemo(() => { + // const resolvedValue = value(); - if (resolvedValue && "timeZone" in resolvedValue) { - return resolvedValue.timeZone; - } + // if (resolvedValue && "timeZone" in resolvedValue) { + // return resolvedValue.timeZone; + // } - return undefined; - }); + // return undefined; + // }); const granularity = createMemo(() => { return props.granularity() || "minute"; @@ -53,35 +52,18 @@ export function createDefaultProps(props: { } }); - return { granularity, defaultTimeZone }; -} - -export function convertValue( - value?: TimeValue | null, - date: DateValue = today(getLocalTimeZone()), -) { - if (!value) { - return null; - } - - if ("day" in value) { - return value; - } - - return toCalendarDateTime(date, value); + return { granularity/*, defaultTimeZone */}; } export function createPlaceholderDate( - value?: TimeValue | null, - placeholderValue?: TimeValue | null, + placeholderValue?: Date | null, timeZone?: string, ) { - const valueTimeZone = - value && "timeZone" in value ? value.timeZone : undefined; - return (valueTimeZone || timeZone) && placeholderValue - ? toZoned(convertValue(placeholderValue)!, (valueTimeZone || timeZone)!) - : convertValue(placeholderValue); + return placeholderValue; + // (valueTimeZone || timeZone) && placeholderValue + // ? toZoned(convertValue(placeholderValue)!, (valueTimeZone || timeZone)!) + // : convertValue(placeholderValue); } export function getTimeFieldFormatOptions( @@ -125,3 +107,10 @@ export function getTimeFieldFormatOptions( return opts; } + +/** + * Returns the current date with the time set to 00:00:00. + */ +export function emptyDateTime(): Date { + return new Date(new Date().setHours(0, 0, 0, 0)); +} From 05fef24d20f50669997ced859a2641a520a1ba15 Mon Sep 17 00:00:00 2001 From: jer3m01 Date: Tue, 28 Oct 2025 11:10:17 +0100 Subject: [PATCH 07/12] wip --- packages/core/dev/App.tsx | 8 ++ packages/core/dev/index.css | 91 +++++++++++++++++++ packages/core/dev/index.html | 22 ++--- .../create-collection/create-collection.ts | 3 - .../core/src/time-field/time-field-field.tsx | 23 ++++- .../core/src/time-field/time-field-root.tsx | 10 +- packages/core/src/time-field/utils.ts | 43 --------- 7 files changed, 132 insertions(+), 68 deletions(-) diff --git a/packages/core/dev/App.tsx b/packages/core/dev/App.tsx index 0d8c8d200..3d1e80464 100644 --- a/packages/core/dev/App.tsx +++ b/packages/core/dev/App.tsx @@ -1,6 +1,14 @@ +import { TimeField } from "../src/time-field"; + export default function App() { return ( <> + + Event time + + {segment => } + + ); } diff --git a/packages/core/dev/index.css b/packages/core/dev/index.css index e69de29bb..570d3af68 100644 --- a/packages/core/dev/index.css +++ b/packages/core/dev/index.css @@ -0,0 +1,91 @@ +.time-field { + display: flex; + flex-direction: column; + gap: 4px; +} + +.time-field__label { + color: hsl(240 6% 10%); + font-size: 14px; + font-weight: 500; + user-select: none; +} + +.time-field__field { + display: flex; + padding: 4px 8px; + border-radius: 6px; + width: 150px; + white-space: nowrap; + forced-color-adjust: none; + background-color: #fff; + border: 1px solid hsl(240 6% 90%); + color: hsl(240 4% 16%); + font-size: 14px; +} + +.time-field__field:focus-within { + outline: 2px solid hsl(200 98% 39%); + outline-offset: -1px; +} + +.time-field__segment { + padding: 0 2px; + font-variant-numeric: tabular-nums; + text-align: end; +} + +.time-field__segment[data-type="literal"] { + padding: 0; +} + +.time-field__segment[data-placeholder] { + color: hsl(240 4% 46%); + font-style: italic; +} + +.time-field__segment:focus { + color: #fff !important; + background: hsl(200 98% 39%); + outline: none; + border-radius: 4px; +} + +.time-field__segment::selection { + background-color: transparent; +} + +.time-field__description { + color: hsl(240 5% 26%); + font-size: 12px; + user-select: none; +} + +.time-field__error-message { + color: hsl(0 72% 51%); + font-size: 12px; + user-select: none; +} + +[data-kb-theme="dark"] .time-field__label { + color: hsl(240 5% 84%); +} + +[data-kb-theme="dark"] .time-field__field { + background-color: hsl(240 4% 16%); + border: 1px solid hsl(240 5% 34%); + color: hsl(0 100% 100% / 0.9); +} + +.time-field__field[data-invalid] { + border-color: hsl(0 72% 51%); + outline-color: hsl(0 72% 51%); +} + +[data-kb-theme="dark"] .time-field__segment[data-placeholder] { + color: hsl(0 100% 100% / 0.5); +} + +[data-kb-theme="dark"] .time-field__description { + color: hsl(240 5% 65%); +} diff --git a/packages/core/dev/index.html b/packages/core/dev/index.html index 450a4e5af..727466d58 100644 --- a/packages/core/dev/index.html +++ b/packages/core/dev/index.html @@ -1,14 +1,14 @@ - - - - - Playground | @kobalte/core - - - -
- - + + + + + Playground | @kobalte/core + + + +
+ + diff --git a/packages/core/src/primitives/create-collection/create-collection.ts b/packages/core/src/primitives/create-collection/create-collection.ts index b64bf3caa..d4e75448c 100644 --- a/packages/core/src/primitives/create-collection/create-collection.ts +++ b/packages/core/src/primitives/create-collection/create-collection.ts @@ -9,10 +9,7 @@ import { access } from "@kobalte/utils"; import { type Accessor, - createEffect, createMemo, - createSignal, - on, } from "solid-js"; import type { Collection, CollectionBase, CollectionNode } from "./types"; diff --git a/packages/core/src/time-field/time-field-field.tsx b/packages/core/src/time-field/time-field-field.tsx index 5047f8fc9..7c47263ec 100644 --- a/packages/core/src/time-field/time-field-field.tsx +++ b/packages/core/src/time-field/time-field-field.tsx @@ -103,7 +103,7 @@ export function TimeFieldField( const formatOpts: Accessor = createMemo(() => ({ granularity: timeFieldContext.granularity(), maxGranularity: "hour", - timeZone: timeFieldContext.defaultTimeZone(), + // timeZone: timeFieldContext.defaultTimeZone(), hideTimeZone: timeFieldContext.hideTimeZone(), hourCycle: timeFieldContext.hourCycle(), shouldForceLeadingZeros: timeFieldContext.shouldForceLeadingZeros(), @@ -166,7 +166,7 @@ export function TimeFieldField( Object.keys(validSegments()).length >= Object.keys(allSegments()).length ) { timeFieldContext.setValue(newValue); - } + } setPlaceholderDate(newValue); }; @@ -225,13 +225,30 @@ export function TimeFieldField( ) => { const resolvedDisplayValue = displayValue(); + console.log("ADJUST", type, amount, validSegments(), resolvedDisplayValue, Object.keys(validSegments()).length, Object.keys(allSegments()).length); + if (!(validSegments() as any)[type]) { markValid(type); if ( resolvedDisplayValue && Object.keys(validSegments()).length >= Object.keys(allSegments()).length ) { - setValue(resolvedDisplayValue); + let multiplier = 1000; + + switch (type) { + case "hour": + multiplier *= 60 * 60; + break; + case "minute": + multiplier *= 60; + break; + case "dayPeriod": + multiplier *= 60 * 60 * 12; + break; + } + + console.log(new Date(resolvedDisplayValue.getTime() + amount * multiplier)) + setValue(new Date(resolvedDisplayValue.getTime() + amount * multiplier)) } } else if (resolvedDisplayValue) { const newValue = addSegment( diff --git a/packages/core/src/time-field/time-field-root.tsx b/packages/core/src/time-field/time-field-root.tsx index 5c42e45ba..dbe0507a8 100644 --- a/packages/core/src/time-field/time-field-root.tsx +++ b/packages/core/src/time-field/time-field-root.tsx @@ -46,7 +46,6 @@ import type { TimeFieldHourCycle, } from "./types"; import { - createDefaultProps, emptyDateTime, getTimeFieldFormatOptions, } from "./utils"; @@ -198,11 +197,6 @@ export function TimeFieldRoot( onChange: (value) => local.onChange?.(value!), }); - const { granularity } = createDefaultProps({ - value: () => value() ?? local.placeholderValue, - granularity: () => local.granularity, - }); - createFormResetListener( ref, () => { @@ -240,7 +234,7 @@ export function TimeFieldRoot( const formattedValue = createMemo(() => { const formatOptions = getTimeFieldFormatOptions({ - granularity: granularity(), + granularity: local.granularity!, // timeZone: defaultTimeZone(), hideTimeZone: local.hideTimeZone, hourCycle: local.hourCycle, @@ -281,7 +275,7 @@ export function TimeFieldRoot( value, setValue, hourCycle: () => local.hourCycle, - granularity, + granularity: () => local.granularity!, hideTimeZone: () => local.hideTimeZone ?? false, shouldForceLeadingZeros: () => local.shouldForceLeadingZeros ?? false, placeholderTime: () => value() || (local.placeholderValue ?? emptyDateTime()), diff --git a/packages/core/src/time-field/utils.ts b/packages/core/src/time-field/utils.ts index dbcbccc81..5729e0c72 100644 --- a/packages/core/src/time-field/utils.ts +++ b/packages/core/src/time-field/utils.ts @@ -11,49 +11,6 @@ import type { TimeFieldGranularity, } from "./types"; -export function createDefaultProps(props: { - value: Accessor; - granularity: Accessor; -}) { - let lastValue: Date; - - const value = createMemo(() => { - const resolvedValue = props.value(); - - if (resolvedValue) { - lastValue = resolvedValue; - } - - return lastValue; - }); - - // const defaultTimeZone = createMemo(() => { - // const resolvedValue = value(); - - // if (resolvedValue && "timeZone" in resolvedValue) { - // return resolvedValue.timeZone; - // } - - // return undefined; - // }); - - const granularity = createMemo(() => { - return props.granularity() || "minute"; - }); - - createEffect(() => { - const resolvedValue = value(); - const resolvedGranularity = granularity(); - - if (resolvedValue && !(resolvedGranularity in resolvedValue)) { - throw new Error( - `Invalid granularity ${resolvedGranularity} for value ${resolvedValue.toString()}`, - ); - } - }); - - return { granularity/*, defaultTimeZone */}; -} export function createPlaceholderDate( placeholderValue?: Date | null, From 6d99ae20b6e3de5801bb0b8d30f744b2eead34a2 Mon Sep 17 00:00:00 2001 From: jer3m01 Date: Sun, 2 Nov 2025 17:09:47 +0100 Subject: [PATCH 08/12] wip --- packages/core/dev/index.html | 22 +-- .../create-collection/create-collection.ts | 5 +- .../src/time-field/time-field-context.tsx | 5 +- .../core/src/time-field/time-field-field.tsx | 167 +++++++----------- .../core/src/time-field/time-field-root.tsx | 27 ++- .../src/time-field/time-field-segment.tsx | 15 +- packages/core/src/time-field/utils.ts | 7 +- 7 files changed, 100 insertions(+), 148 deletions(-) diff --git a/packages/core/dev/index.html b/packages/core/dev/index.html index 727466d58..450a4e5af 100644 --- a/packages/core/dev/index.html +++ b/packages/core/dev/index.html @@ -1,14 +1,14 @@ - - - - - Playground | @kobalte/core - - - -
- - + + + + + Playground | @kobalte/core + + + +
+ + diff --git a/packages/core/src/primitives/create-collection/create-collection.ts b/packages/core/src/primitives/create-collection/create-collection.ts index d4e75448c..10cb3f49d 100644 --- a/packages/core/src/primitives/create-collection/create-collection.ts +++ b/packages/core/src/primitives/create-collection/create-collection.ts @@ -7,10 +7,7 @@ */ import { access } from "@kobalte/utils"; -import { - type Accessor, - createMemo, -} from "solid-js"; +import { type Accessor, createMemo } from "solid-js"; import type { Collection, CollectionBase, CollectionNode } from "./types"; import { buildNodes } from "./utils"; diff --git a/packages/core/src/time-field/time-field-context.tsx b/packages/core/src/time-field/time-field-context.tsx index 94a6df625..674a1f9cb 100644 --- a/packages/core/src/time-field/time-field-context.tsx +++ b/packages/core/src/time-field/time-field-context.tsx @@ -1,10 +1,7 @@ import type { FocusManager } from "@kobalte/utils"; import { type Accessor, createContext, useContext } from "solid-js"; import type { TimeFieldIntlTranslations } from "./time-field.intl"; -import type { - TimeFieldGranularity, - TimeFieldHourCycle, -} from "./types"; +import type { TimeFieldGranularity, TimeFieldHourCycle } from "./types"; export interface TimeFieldContextValue { translations: Accessor; diff --git a/packages/core/src/time-field/time-field-field.tsx b/packages/core/src/time-field/time-field-field.tsx index 7c47263ec..c835e0d9e 100644 --- a/packages/core/src/time-field/time-field-field.tsx +++ b/packages/core/src/time-field/time-field-field.tsx @@ -35,15 +35,8 @@ import { TimeFieldFieldContext, type TimeFieldFieldContextValue, } from "./time-field-field-context"; -import type { - FormatterOptions, - SegmentType, - TimeSegment, -} from "./types"; -import { - emptyDateTime, - getTimeFieldFormatOptions, -} from "./utils"; +import type { FormatterOptions, SegmentType, TimeSegment } from "./types"; +import { emptyDateTime, getTimeFieldFormatOptions } from "./utils"; export interface TimeFieldFieldOptions { children?: (segment: Accessor) => JSX.Element; @@ -98,7 +91,10 @@ export function TimeFieldField( () => timeFieldContext.value() || timeFieldContext.placeholderTime(), ); - const [placeholderDate, setPlaceholderDate] = createSignal(emptyDateTime()); + const [placeholderDate, _setPlaceholderDate] = createSignal(emptyDateTime()); + + const setPlaceholderDate = (value: Date) => + _setPlaceholderDate(new Date(value.getTime())); const formatOpts: Accessor = createMemo(() => ({ granularity: timeFieldContext.granularity(), @@ -171,7 +167,6 @@ export function TimeFieldField( }; const segments = createMemo(() => { - if (!displayValue()) { return []; } @@ -199,11 +194,7 @@ export function TimeFieldField( TYPE_MAPPING[segment.type as keyof typeof TYPE_MAPPING] || segment.type, text: isPlaceholder ? placeholder : segment.value, - ...getSegmentLimits( - displayValue()!, - segment.type, - resolvedOptions(), - ), + ...getSegmentLimits(displayValue()!, segment.type, resolvedOptions()), isPlaceholder, placeholder, isEditable, @@ -225,37 +216,9 @@ export function TimeFieldField( ) => { const resolvedDisplayValue = displayValue(); - console.log("ADJUST", type, amount, validSegments(), resolvedDisplayValue, Object.keys(validSegments()).length, Object.keys(allSegments()).length); - - if (!(validSegments() as any)[type]) { + if (resolvedDisplayValue) { markValid(type); - if ( - resolvedDisplayValue && - Object.keys(validSegments()).length >= Object.keys(allSegments()).length - ) { - let multiplier = 1000; - - switch (type) { - case "hour": - multiplier *= 60 * 60; - break; - case "minute": - multiplier *= 60; - break; - case "dayPeriod": - multiplier *= 60 * 60 * 12; - break; - } - - console.log(new Date(resolvedDisplayValue.getTime() + amount * multiplier)) - setValue(new Date(resolvedDisplayValue.getTime() + amount * multiplier)) - } - } else if (resolvedDisplayValue) { - const newValue = addSegment( - resolvedDisplayValue, - type, - amount, - ); + const newValue = addSegment(resolvedDisplayValue, type, amount); if (newValue) { setValue(newValue); @@ -284,6 +247,8 @@ export function TimeFieldField( const resolvedDisplayValue = displayValue(); + console.log(part, value); + if (resolvedDisplayValue) { const newValue = setSegmentBase( resolvedDisplayValue, @@ -292,6 +257,8 @@ export function TimeFieldField( resolvedOptions(), ); + console.log(newValue); + if (newValue) { setValue(newValue); } @@ -308,7 +275,7 @@ export function TimeFieldField( const placeholder = emptyDateTime(); const resolvedDisplayValue = displayValue(); - let value = resolvedDisplayValue || emptyDateTime(); + const value = resolvedDisplayValue || emptyDateTime(); if (resolvedDisplayValue && placeholder) { if (part === "dayPeriod") { @@ -316,13 +283,9 @@ export function TimeFieldField( const shouldBePM = placeholder.getHours() >= 12; if (isPM && !shouldBePM) { - value.setHours( - resolvedDisplayValue.getHours() - 12, - ); + value.setHours(resolvedDisplayValue.getHours() - 12); } else if (!isPM && shouldBePM) { - value.setHours( - resolvedDisplayValue.getHours() + 12, - ); + value.setHours(resolvedDisplayValue.getHours() + 12); } } else if (part in resolvedDisplayValue) { switch (part) { @@ -494,29 +457,23 @@ function getSegmentLimits( return {}; } -function addSegment( - value: Date, - part: string, - amount: number, -) { - if ("hour" in value) { - switch (part) { - case "dayPeriod": { - const hours = value.getHours(); - const isPM = hours >= 12; - value.setHours(isPM ? hours - 12 : hours + 12); - return value; - } - case "hour": - value.setHours((value.getHours() + amount) % 24); - return value; - case "minute": - value.setMinutes((value.getMinutes() + amount) % 60); - return value; - case "second": - value.setSeconds((value.getSeconds() + amount) % 60); - return value; +function addSegment(value: Date, part: string, amount: number) { + switch (part) { + case "dayPeriod": { + const hours = value.getHours(); + const isPM = hours >= 12; + value.setHours(isPM ? hours - 12 : hours + 12); + return value; } + case "hour": + value.setHours((value.getHours() + amount) % 24); + return value; + case "minute": + value.setMinutes((value.getMinutes() + amount) % 60); + return value; + case "second": + value.setSeconds((value.getSeconds() + amount) % 60); + return value; } } @@ -526,41 +483,39 @@ function setSegmentBase( segmentValue: number, options: Intl.ResolvedDateTimeFormatOptions, ) { - if ("hour" in value) { - switch (part) { - case "dayPeriod": { - const hours = value.getHours(); - const wasPM = hours >= 12; - const isPM = segmentValue >= 12; - if (isPM === wasPM) { - return value; - } - value.setHours(wasPM ? hours - 12 : hours + 12); + switch (part) { + case "dayPeriod": { + const hours = value.getHours(); + const wasPM = hours >= 12; + const isPM = segmentValue >= 12; + if (isPM === wasPM) { return value; } + value.setHours(wasPM ? hours - 12 : hours + 12); + return value; + } - case "hour": - if (options.hour12) { - const hours = value.getHours(); - const wasPM = hours >= 12; - if (!wasPM && segmentValue === 12) { - // biome-ignore lint/style/noParameterAssign: used in fallthrough - segmentValue = 0; - } - if (wasPM && segmentValue < 12) { - // biome-ignore lint/style/noParameterAssign: used in fallthrough - segmentValue += 12; - } + case "hour": + if (options.hour12) { + const hours = value.getHours(); + const wasPM = hours >= 12; + if (!wasPM && segmentValue === 12) { + // biome-ignore lint/style/noParameterAssign: used in fallthrough + segmentValue = 0; } - value.setHours(segmentValue); - return value; - case "minute": - value.setMinutes(segmentValue); - return value; - case "second": - value.setSeconds(segmentValue); - return value; - } + if (wasPM && segmentValue < 12) { + // biome-ignore lint/style/noParameterAssign: used in fallthrough + segmentValue += 12; + } + } + value.setHours(segmentValue); + return value; + case "minute": + value.setMinutes(segmentValue); + return value; + case "second": + value.setSeconds(segmentValue); + return value; } } diff --git a/packages/core/src/time-field/time-field-root.tsx b/packages/core/src/time-field/time-field-root.tsx index dbe0507a8..fd7b70e0c 100644 --- a/packages/core/src/time-field/time-field-root.tsx +++ b/packages/core/src/time-field/time-field-root.tsx @@ -41,14 +41,8 @@ import { TIME_FIELD_INTL_MESSAGES, type TimeFieldIntlTranslations, } from "./time-field.intl"; -import type { - TimeFieldGranularity, - TimeFieldHourCycle, -} from "./types"; -import { - emptyDateTime, - getTimeFieldFormatOptions, -} from "./utils"; +import type { TimeFieldGranularity, TimeFieldHourCycle } from "./types"; +import { emptyDateTime, getTimeFieldFormatOptions } from "./utils"; export interface TimeFieldRootOptions { /** The current value (controlled). */ @@ -191,18 +185,18 @@ export function TimeFieldRoot( const focusManager = createFocusManager(inputRef); - const [value, setValue] = createControllableSignal({ + const [value, _setValue] = createControllableSignal({ value: () => local.value, defaultValue: () => local.defaultValue, onChange: (value) => local.onChange?.(value!), }); - createFormResetListener( - ref, - () => { - setValue(local.defaultValue ?? emptyDateTime()); - }, - ); + const setValue = (value: Date | undefined) => + _setValue(value ? new Date(value.getTime()) : undefined); + + createFormResetListener(ref, () => { + setValue(local.defaultValue ?? emptyDateTime()); + }); const validationState = createMemo(() => { if (local.validationState) { @@ -278,7 +272,8 @@ export function TimeFieldRoot( granularity: () => local.granularity!, hideTimeZone: () => local.hideTimeZone ?? false, shouldForceLeadingZeros: () => local.shouldForceLeadingZeros ?? false, - placeholderTime: () => value() || (local.placeholderValue ?? emptyDateTime()), + placeholderTime: () => + value() || (local.placeholderValue ?? emptyDateTime()), placeholderValue: () => local.placeholderValue, // defaultTimeZone, formattedValue, diff --git a/packages/core/src/time-field/time-field-segment.tsx b/packages/core/src/time-field/time-field-segment.tsx index 7cd138217..9db5dcd51 100644 --- a/packages/core/src/time-field/time-field-segment.tsx +++ b/packages/core/src/time-field/time-field-segment.tsx @@ -186,7 +186,6 @@ export function TimeFieldSegment( const hourDateFormatter = createDateFormatter(() => ({ hour: "numeric", hour12: options().hour12, - timeZone: options().timeZone, })); const amPmFormatter = createDateFormatter({ @@ -277,6 +276,8 @@ export function TimeFieldSegment( const newValue = enteredKeys + key; + console.log("AAAAAAAAAAAAAAAAAAAAA", newValue); + switch (local.segment.type) { case "dayPeriod": if (filter.startsWith(am(), key)) { @@ -295,9 +296,14 @@ export function TimeFieldSegment( return; } + console.log("valid"); + let numberValue = numberParser().parse(newValue); let segmentValue = numberValue; let allowsZero = local.segment.minValue === 0; + + console.log(numberValue, segmentValue); + if ( local.segment.type === "hour" && fieldContext.dateFormatterResolvedOptions().hour12 @@ -336,6 +342,8 @@ export function TimeFieldSegment( const shouldSetValue = segmentValue !== 0 || allowsZero; + console.log(shouldSetValue, segmentValue); + if (shouldSetValue) { fieldContext.setSegment(local.segment.type, segmentValue); } @@ -469,6 +477,11 @@ export function TimeFieldSegment( if (resolvedDateValue) { if (local.segment.type === "hour" && !local.segment.isPlaceholder) { + console.log( + "EFFECT", + resolvedDateValue, + hourDateFormatter().format(resolvedDateValue), + ); setTextValue(hourDateFormatter().format(resolvedDateValue)); } else { setTextValue(local.segment.isPlaceholder ? "" : local.segment.text); diff --git a/packages/core/src/time-field/utils.ts b/packages/core/src/time-field/utils.ts index 5729e0c72..aede028ce 100644 --- a/packages/core/src/time-field/utils.ts +++ b/packages/core/src/time-field/utils.ts @@ -6,17 +6,12 @@ import { today, } from "@internationalized/date"; import { type Accessor, createEffect, createMemo } from "solid-js"; -import type { - FormatterOptions, - TimeFieldGranularity, -} from "./types"; - +import type { FormatterOptions, TimeFieldGranularity } from "./types"; export function createPlaceholderDate( placeholderValue?: Date | null, timeZone?: string, ) { - return placeholderValue; // (valueTimeZone || timeZone) && placeholderValue // ? toZoned(convertValue(placeholderValue)!, (valueTimeZone || timeZone)!) From 32c13e31162cec4c67a5c26c9965054815e195e5 Mon Sep 17 00:00:00 2001 From: jer3m01 Date: Sun, 2 Nov 2025 18:56:35 +0100 Subject: [PATCH 09/12] wip --- packages/core/dev/App.tsx | 4 +- packages/core/src/time-field/index.tsx | 24 +- .../src/time-field/time-field-context.tsx | 18 +- .../time-field/time-field-field-context.tsx | 33 -- .../core/src/time-field/time-field-field.tsx | 546 ------------------ .../time-field/time-field-hidden-input.tsx | 14 +- .../core/src/time-field/time-field-input.tsx | 161 ++++++ .../core/src/time-field/time-field-root.tsx | 130 +++-- .../src/time-field/time-field-segment.tsx | 27 +- .../core/src/time-field/time-field.intl.ts | 2 + packages/core/src/time-field/types.ts | 24 +- packages/core/src/time-field/utils.ts | 68 --- 12 files changed, 288 insertions(+), 763 deletions(-) delete mode 100644 packages/core/src/time-field/time-field-field-context.tsx delete mode 100644 packages/core/src/time-field/time-field-field.tsx create mode 100644 packages/core/src/time-field/time-field-input.tsx delete mode 100644 packages/core/src/time-field/utils.ts diff --git a/packages/core/dev/App.tsx b/packages/core/dev/App.tsx index 3d1e80464..b3aa8caf4 100644 --- a/packages/core/dev/App.tsx +++ b/packages/core/dev/App.tsx @@ -5,9 +5,9 @@ export default function App() { <> Event time - + {segment => } - + ); diff --git a/packages/core/src/time-field/index.tsx b/packages/core/src/time-field/index.tsx index 246ebb898..afe0feb1b 100644 --- a/packages/core/src/time-field/index.tsx +++ b/packages/core/src/time-field/index.tsx @@ -11,12 +11,12 @@ import { type FormControlErrorMessageRenderProps as TimeFieldErrorMessageRenderProps, } from "../form-control"; import { - TimeFieldField as Field, - type TimeFieldFieldCommonProps, - type TimeFieldFieldOptions, - type TimeFieldFieldProps, - type TimeFieldFieldRenderProps, -} from "./time-field-field"; + TimeFieldInput as Input, + type TimeFieldInputCommonProps, + type TimeFieldInputOptions, + type TimeFieldInputProps, + type TimeFieldInputRenderProps, +} from "./time-field-input"; import { TimeFieldHiddenInput as HiddenInput, type TimeFieldHiddenInputProps, @@ -57,10 +57,10 @@ export type { TimeFieldSegmentCommonProps, TimeFieldSegmentRenderProps, TimeFieldSegmentProps, - TimeFieldFieldOptions, - TimeFieldFieldCommonProps, - TimeFieldFieldRenderProps, - TimeFieldFieldProps, + TimeFieldInputOptions, + TimeFieldInputCommonProps, + TimeFieldInputRenderProps, + TimeFieldInputProps, TimeFieldLabelOptions, TimeFieldLabelCommonProps, TimeFieldLabelRenderProps, @@ -71,11 +71,11 @@ export type { TimeFieldRootProps, }; -export { Root, Label, Field, Segment, Description, ErrorMessage, HiddenInput }; +export { Root, Label, Input, Segment, Description, ErrorMessage, HiddenInput }; export const TimeField = Object.assign(Root, { Label, - Field, + Input, Segment, Description, ErrorMessage, diff --git a/packages/core/src/time-field/time-field-context.tsx b/packages/core/src/time-field/time-field-context.tsx index 674a1f9cb..6347cfa19 100644 --- a/packages/core/src/time-field/time-field-context.tsx +++ b/packages/core/src/time-field/time-field-context.tsx @@ -1,19 +1,21 @@ import type { FocusManager } from "@kobalte/utils"; import { type Accessor, createContext, useContext } from "solid-js"; import type { TimeFieldIntlTranslations } from "./time-field.intl"; -import type { TimeFieldGranularity, TimeFieldHourCycle } from "./types"; +import type { SegmentType, Time, TimeFieldGranularity, TimeFieldHourCycle } from "./types"; export interface TimeFieldContextValue { translations: Accessor; - value: Accessor; - setValue: (value: Date | undefined) => void; + value: Accessor | undefined>; + setValue: (value: Partial