Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions Documentation/CommandForm/number-input-field.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# NumberInputField

`NumberInputField` binds the standalone locale-aware [`NumberInput`](../Common/number-input.md) to a non-null number property in an Arc `CommandForm`.

```tsx
import { CommandDialog } from '@cratis/components/CommandDialog';
import { NumberInputField } from '@cratis/components/CommandForm';
import { UpdateSample } from './UpdateSample';

<CommandDialog command={UpdateSample} title='Update sample'>
<NumberInputField<UpdateSample>
value={(command) => command.amount}
title='Amount'
required
min={0}
step={0.01}
minimumFractionDigits={2}
maximumFractionDigits={2}
suffix='kg'
/>
</CommandDialog>;
```

The adapter does not parse or format a second time. It reuses `NumberInput` and adds only CommandForm binding, title/error association, required-state propagation, blur validation, and the non-null command policy:

- the command default is `0`;
- clearing the nullable edit state writes `0` to the command property;
- extracted `null`, `undefined`, or non-finite values become `0`;
- all locale, grouping, fraction, range, step, adornment, callback, part, and token behavior belongs to `NumberInput`.

Use [`NumberField`](number-field.md) instead when the native `input[type=number]` behavior and its clear-to-zero contract are sufficient. The existing native field remains the smaller surface and does not add locale grouping, adornments, or a separate commit callback.
2 changes: 2 additions & 0 deletions Documentation/CommandForm/toc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
href: multi-select-field.md
- name: NumberField
href: number-field.md
- name: NumberInputField
href: number-input-field.md
- name: PasswordField
href: password-field.md
- name: RadioButtonField
Expand Down
5 changes: 5 additions & 0 deletions Documentation/Common/basic-controls.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
Button,
Checkbox,
IconButton,
NumberInput,
Radio,
Surface,
Switch,
Expand All @@ -25,6 +26,7 @@ import {
onChange={(value) => console.log(value)}
/>
<TextArea name='notes' aria-label='Notes' />
<NumberInput value={12.5} onChange={(value) => console.log(value)} aria-label='Amount' />
<Checkbox name='updates' value='email' label='Email updates' />
<Radio name='frequency' value='daily' label='Daily' />
<Switch name='notifications' value='enabled' label='Enable notifications' />
Expand All @@ -40,6 +42,7 @@ import {
| `Button` | `button` / `HTMLButtonElement` | Native click and form action | `root`, `spinner`, `icon`, `label` | `disabled`, `loading`, `variant`, `tone`, `shape`, `size` on `root` |
| `IconButton` | `button` / `HTMLButtonElement` | Uses `Button` click semantics | `root`, `spinner`, `icon`, `label` | `disabled`, `loading` on `root` |
| `TextInput` | `input` / `HTMLInputElement` | `string` | `root` | `disabled`, `invalid`, `readonly` |
| `NumberInput` | owned spinbutton composition | `number \| null` | `root`, `input`, `prefix`, `suffix`, `step`, `description`, `error` | `disabled`, `invalid`, `readonly`, `focused` where documented |
| `TextArea` | `textarea` / `HTMLTextAreaElement` | `string` | `root` | `disabled`, `invalid`, `readonly` |
| `Checkbox` | `input[type=checkbox]` / `HTMLInputElement` | `boolean` | `root`, `input`, `box`, `indicator`, `label` | `selected`, `disabled`, `invalid`, `readonly` |
| `Radio` | one `input[type=radio]` / `HTMLInputElement` | `boolean` when checked | `root`, `input`, `box`, `indicator`, `label` | `selected`, `disabled`, `invalid`, `readonly` |
Expand All @@ -48,6 +51,8 @@ import {

`IconButton` requires `aria-label` and delegates to `Button`; it does not add another button or interaction layer. It accepts the same semantic `variant`, `tone`, `shape`, `size`, `loading`, and `disabled` props as `Button`.

[`NumberInput`](number-input.md) keeps locale-aware numeric edit text behind a controlled `number | null` boundary. Its public contract does not expose a formatter, renderer, or native event type.

`TextInput` accepts the native text-like types `text`, `email`, `password`, `search`, `tel`, and `url`. `TextInput` and `TextArea` preserve native controlled (`value`) and uncontrolled (`defaultValue`) behavior.

`Radio` represents exactly one native option. Give related options the same `name`; the browser owns grouping. Components does not add radio-group state or keyboard orchestration.
Expand Down
2 changes: 2 additions & 0 deletions Documentation/Common/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ The Common module provides reusable UI components and the styling setup primitiv

- **CratisComponentsProvider**: Locale, Components-owned labels, and optional app-wide toaster.
- **TextInput / TextArea**: Native text controls with semantic string changes and real element refs.
- **NumberInput**: Controlled locale-aware numeric entry with nullable values, fractions, bounds, adornments, and explicit commits.
- **Checkbox / Radio / Switch**: Native form choices with semantic boolean changes and browser-owned submission and reset behavior.
- **Button / IconButton**: Native actions with semantic variants, tones, loading, and disabled behavior.
- **Surface**: A bounded `div`, `section`, or `article` container with no invented interaction state.
Expand All @@ -17,6 +18,7 @@ The Common module provides reusable UI components and the styling setup primitiv
## See Also

- [Basic controls](basic-controls.md) — native form, ref, change, part, and state contracts
- [Locale-aware number input](number-input.md) — locale, nullable edit, commit, adornment, range, part, and token contracts
- [CratisComponentsProvider](cratis-components-provider.md) — locale, labels, and toaster configuration
- [Icon](icon.md) - Icon type and IconDisplay component
- [Page](page.md) - Page layout component
Expand Down
120 changes: 120 additions & 0 deletions Documentation/Common/number-input.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
---
title: Locale-aware number input
description: Enter controlled nullable numbers with locale formatting, explicit commits, adornments, bounds, and stable parts.
---

`NumberInput` is the standalone locale-aware numeric control. It keeps transient text separate from the controlled `number | null` value, so clearing or typing an incomplete number never fabricates `0` or exposes `NaN`.

Use the native command [`NumberField`](../CommandForm/number-field.md) when browser-native formatting and a non-null `0` default are sufficient. Use `NumberInput` or [`NumberInputField`](../CommandForm/number-input-field.md) when the interaction needs locale grouping and decimal separators, fraction policy, adornments, nullable edit state, or explicit commit timing.

## Controlled usage

```tsx
import { useState } from 'react';
import { NumberInput } from '@cratis/components/Common';

export const SampleQuantity = () => {
const [quantity, setQuantity] = useState<number | null>(null);

return (
<>
<label id='sample-quantity-label' htmlFor='sample-quantity'>
Quantity
</label>
<NumberInput
id='sample-quantity'
aria-labelledby='sample-quantity-label'
name='quantity'
value={quantity}
onChange={setQuantity}
min={0}
max={100}
step={0.5}
suffix='kg'
minimumFractionDigits={1}
maximumFractionDigits={2}
description='Enter a value from zero to one hundred.'
/>
</>
);
};
```

The nearest `CratisComponentsProvider` supplies the BCP 47 locale. The optional `locale` prop overrides it for one control. An invalid override falls back to the provider locale.

```tsx
<CratisComponentsProvider value={{ locale: 'nb-NO' }}>
<NumberInput value={1234.5} onChange={setValue} aria-label='Amount' />
<NumberInput
value={1234.5}
onChange={setValue}
aria-label='American amount'
locale='en-US'
/>
</CratisComponentsProvider>
```

`useGrouping` defaults to `true`. When fraction props are omitted, decimal formatting uses the locale defaults: zero minimum fraction digits and up to three maximum fraction digits. Set both to the same number for fixed precision.

## Change and commit contract

`NumberInput` deliberately separates editable text from semantic callbacks.

| Interaction | `onChange` | `onCommit` |
| ------------------------------------------- | ---------------------------------------------------------- | ---------------------------------------------- |
| Type or clear without leaving the input | No callback while text is being edited | No callback |
| Press Enter | New finite number or `null`, when it changed | Then `Enter` |
| Press Tab or otherwise blur | New finite number or `null`, when it changed | Then `Blur` |
| Replace the complete input through paste | New finite number or `null`, when it changed | Then `Paste` |
| ArrowUp, ArrowDown, decrement, or increment | Stepped finite number | Then `Step` |
| Commit text that cannot yet form a number | No fabricated change; text returns to the controlled value | Current controlled value and the commit reason |

A commit clamps to `min`/`max`, snaps to `step`, and rounds through the configured fraction policy. The same policy applies when a controlled prop arrives outside those boundaries: `onChange` receives the normalized value once. Until the owner accepts it, the control keeps the original value visible and withholds the named hidden input, so React state, announced content, and native form data cannot silently disagree. When both callbacks run, `onChange` always runs first. The component remains controlled: accept the value in `onChange` to display and submit it as the new value.

## Adornments and semantics

`prefix` and `suffix` render beside the editable text. They never enter the parse buffer, semantic number, or hidden form value. Each rendered adornment receives a stable id and is appended to the input's `aria-describedby` relationship, together with consumer descriptions and an active error message.

Provide an accessible name through `aria-label` or `aria-labelledby`. When a visible external label is used, give it an id, keep `htmlFor` pointed at the input id, and pass that label id through `aria-labelledby`; this also ties the localized increment and decrement action names to the field. `description` and an invalid `errorMessage` are rendered and associated automatically. The editable text control retains React Aria's number-field role description rather than reinstating the spinbutton attributes that its accessibility implementation intentionally removes for focus compatibility. Step buttons are excluded from sequential tab order; ArrowUp and ArrowDown provide the same operation from the input.

`disabled` removes the control from editing and form submission. `readOnly` keeps the value focusable and submittable while disabling edits and steps.

## Props

| Prop | Type | Default | Behavior |
| ----------------------------------- | --------------------------------- | --------------- | ---------------------------------------------------------------------------- |
| `value` | `number \| null` | Required | Controlled finite value; non-finite runtime values render empty. |
| `onChange` | `(value: number \| null) => void` | Required | Receives accepted semantic changes only. |
| `onCommit` | `(value, reason) => void` | — | Receives `Blur`, `Enter`, `Paste`, or `Step` after the change callback. |
| `locale` | `string` | Provider locale | BCP 47 locale override. |
| `useGrouping` | `boolean` | `true` | Enables the locale grouping separator. |
| `minimumFractionDigits` | `number` | Locale default | Minimum displayed fraction digits. |
| `maximumFractionDigits` | `number` | Locale default | Maximum fraction digits retained on commit and shown in the formatted value. |
| `required` | `boolean` | `false` | Requires a non-empty value using native form and accessibility semantics. |
| `min` / `max` | `number` | Unbounded | Commit boundaries and number-field range. |
| `step` | `number` | `1` | Step and commit-snap interval. |
| `prefix` / `suffix` | `ReactNode` | — | Associated presentation outside the numeric value. |
| `placeholder` | `string` | — | Empty edit hint. |
| `disabled` / `readOnly` / `invalid` | `boolean` | `false` | Semantic and visual state. |
| `id` / `name` | `string` | Generated / — | Label association and native form field name. |
| `description` / `errorMessage` | `ReactNode` | — | Associated help and invalid-state content. |
| `pt` | `NumberInputParts` | — | Renderer-independent part classes, styles, titles, and data attributes. |

## Stable parts and tokens

| Typed `pt` key / DOM part | Meaning | Canonical states |
| ------------------------- | ------------------------------------------------------------------- | -------------------------------------------- |
| `root` | Complete field | `disabled`, `invalid`, `readonly` |
| `input` | Editable localized text | `disabled`, `invalid`, `readonly`, `focused` |
| `prefix` / `suffix` | Present adornment | `disabled`, `invalid`, `readonly` |
| `step` | Both step buttons; inspect `data-step='decrement'` or `'increment'` | `disabled`, `invalid`, `readonly` |
| `description` | Supporting text | none |
| `error` | Active validation message | `invalid` |

The component uses the shared control, surface, text, focus, disabled, and error tokens plus these aliases:

- `--cratis-number-input-adornment-color`
- `--cratis-number-input-step-background`
- `--cratis-number-input-step-background-hover`

Use parts and tokens rather than internal element order or implementation-library selectors.
2 changes: 2 additions & 0 deletions Documentation/Common/toc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
href: cratis-components-provider.md
- name: Basic controls
href: basic-controls.md
- name: Locale-aware number input
href: number-input.md
- name: Icon
href: icon.md
- name: Page
Expand Down
40 changes: 29 additions & 11 deletions Documentation/Migration/3-to-4.md
Original file line number Diff line number Diff line change
Expand Up @@ -528,23 +528,41 @@ import './product-components.css';

Because these assignments reference product tokens, existing dark, enhanced-contrast, control-size, status, and accessibility selectors flow through without duplicating the mapping. The product continues to own typography, spacing, motion, elevation, and component-specific treatments.

If one product area still uses Prime's locale-aware `InputNumber`, keep it as an explicitly bounded Prime island. Mount the Prime provider independently around that remaining surface and retain its installed-version theme/license requirements; do not put renderer keys back into `CratisComponentsProvider`:
Replace a retained Prime locale-aware `InputNumber` with the Components-owned standalone control after accepting the product's exact locale, nullable, fraction, range, adornment, and commit behavior:

```tsx
import { PrimeReactProvider } from '@primereact/core';

<CratisComponentsProvider value={{ locale, messages }}>
<PrimeReactProvider license={primeUiLicense}>
<LocaleAwareNumberInput />
</PrimeReactProvider>
</CratisComponentsProvider>;
import { NumberInput } from '@cratis/components/Common';

<NumberInput
value={amount}
onChange={setAmount}
min={0}
step={0.01}
minimumFractionDigits={2}
maximumFractionDigits={2}
suffix='kg'
aria-label='Amount'
/>;
```

PrimeReact 11 receives `license` directly as a provider prop; it does **not** use the `value={{ license }}` shape of `CratisComponentsProvider`. Add the installed Prime theme/provider options beside `license` when that island needs them.
`NumberInput` reads the `CratisComponentsProvider` locale and keeps `prefix`/`suffix` outside the parsed and submitted value. It emits `number | null`; typing stays local until Enter or blur, while full-field paste, arrow keys, and step buttons commit immediately. Use `onCommit` when product behavior depends on that boundary.

For a non-null numeric command property, replace the custom Prime adapter with `NumberInputField`:

```tsx
import { NumberInputField } from '@cratis/components/CommandForm';

<NumberInputField<UpdateSample>
value={(command) => command.amount}
title='Amount'
minimumFractionDigits={2}
maximumFractionDigits={2}
/>;
```

The provider boundary scopes Prime runtime configuration and context, but a JavaScript-imported Prime theme stylesheet is still a **document-global side effect**. Put the import in the smallest host entry point that contains the island and inventory any `.p-*` selectors that intentionally depend on it; wrapping a subtree does not isolate that CSS. Every retained island should have an owner, a reason it remains, its licensing/theme dependencies, and an explicit removal condition or tracking issue.
The CommandForm adapter reuses the standalone control and maps an empty or non-finite edit to the explicit command default `0`. The existing native `NumberField` remains available for simple browser-native numeric entry.

Other areas can remove Prime as soon as they have no direct Prime imports. Remove the separate Prime provider only when number grouping, decimal handling, fraction digits, prefix/suffix, min/max, and command binding have an accepted renderer-independent replacement.
Remove the separate Prime provider, theme, license configuration, and packages only after every direct Prime component use has moved or has an explicit retained owner. A JavaScript-imported Prime theme stylesheet is a document-global side effect; wrapping the old component in a provider never isolated that CSS.

This preserves product token and theme ownership while removing the circular product → Prime preset → Prime variables → Cratis translation.

Expand Down
Loading
Loading