@openreachtech/furo-vue 1.6.0 → 1.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.ja.md +2 -0
- package/README.md +2 -0
- package/docs/design-tokens.md +419 -0
- package/docs/furo-accordion.md +109 -0
- package/docs/furo-alert-dialog.md +107 -0
- package/docs/furo-attachment.md +133 -0
- package/docs/furo-autocomplete-field.md +719 -0
- package/docs/furo-avatar-group.md +87 -0
- package/docs/furo-avatar.md +101 -0
- package/docs/furo-badge.md +126 -0
- package/docs/furo-breadcrumb.md +89 -0
- package/docs/furo-button.md +263 -0
- package/docs/furo-checkbox.md +384 -0
- package/docs/furo-collapsible.md +104 -0
- package/docs/furo-control-block.md +102 -0
- package/docs/furo-date-picker.md +270 -0
- package/docs/furo-date-time-picker.md +110 -0
- package/docs/furo-dialog.md +88 -0
- package/docs/furo-drawer.md +122 -0
- package/docs/furo-dropdown-menu.md +298 -0
- package/docs/furo-editable-field.md +391 -0
- package/docs/furo-editor.md +304 -0
- package/docs/furo-email-field.md +93 -0
- package/docs/furo-empty-state.md +97 -0
- package/docs/furo-error-state.md +97 -0
- package/docs/furo-file-field.md +163 -0
- package/docs/furo-icon.md +288 -0
- package/docs/furo-number-field.md +397 -0
- package/docs/furo-pagination.md +101 -0
- package/docs/furo-password-field.md +75 -0
- package/docs/furo-popover.md +114 -0
- package/docs/furo-radio.md +125 -0
- package/docs/furo-scroll-area.md +112 -0
- package/docs/furo-search-field.md +94 -0
- package/docs/furo-section-header.md +135 -0
- package/docs/furo-select.md +443 -0
- package/docs/furo-separator.md +87 -0
- package/docs/furo-sidebar.md +178 -0
- package/docs/furo-skeleton.md +92 -0
- package/docs/furo-splitter.md +103 -0
- package/docs/furo-stepper.md +127 -0
- package/docs/furo-switch.md +107 -0
- package/docs/furo-table.md +233 -0
- package/docs/furo-tabs.md +173 -0
- package/docs/furo-text-field.md +249 -0
- package/docs/furo-textarea.md +214 -0
- package/docs/furo-time-field.md +276 -0
- package/docs/furo-toast.md +217 -0
- package/docs/furo-toggle-group.md +123 -0
- package/docs/furo-toggle.md +104 -0
- package/docs/furo-tool-bar.md +88 -0
- package/docs/furo-tooltip.md +107 -0
- package/lib/components/atoms/FuroButton/FuroButtonContext.js +8 -8
- package/lib/components/atoms/FuroCheckbox/FuroCheckboxContext.js +1 -1
- package/lib/components/atoms/FuroSwitch/FuroSwitchContext.js +1 -1
- package/lib/components/atoms/FuroToggle/FuroToggleContext.js +1 -1
- package/lib/components/molecules/FuroAccordion/FuroAccordionContext.js +1 -1
- package/lib/components/molecules/FuroCollapsible/FuroCollapsibleContext.js +1 -1
- package/lib/components/molecules/FuroDropdownMenu/FuroDropdownMenuContext.js +1 -2
- package/lib/components/molecules/FuroPopover/FuroPopoverContext.js +1 -1
- package/lib/components/molecules/FuroSelect/FuroSelect.vue +8 -6
- package/lib/components/molecules/FuroSelect/FuroSelectContext.js +35 -0
- package/lib/components/molecules/FuroSelect/FuroSelectOptionText.vue +40 -0
- package/lib/components/molecules/FuroSelect/FuroSelectOptionTextContext.js +275 -0
- package/lib/components/molecules/FuroSplitter/FuroSplitterContext.js +1 -1
- package/lib/components/molecules/FuroToggleGroup/FuroToggleGroupContext.js +1 -1
- package/lib/components/molecules/FuroTooltip/FuroTooltipContext.js +1 -1
- package/lib/components/organisms/FuroTable/FuroTableContext.js +3 -3
- package/lib/components/organisms/FuroToast/FuroToastContext.js +6 -2
- package/lib/components/organisms/FuroToast/ToastQueue.js +4 -4
- package/nuxt/index.js +9 -9
- package/package.json +19 -17
- package/public/furo-vue/components.json +1 -1
- package/types/generated/components/atoms/FuroButton/FuroButtonContext.d.ts +4 -4
- package/types/generated/components/molecules/FuroSelect/FuroSelect.vue.d.ts +23 -0
- package/types/generated/components/molecules/FuroSelect/FuroSelectContext.d.ts +14 -0
- package/types/generated/components/molecules/FuroSelect/FuroSelectOptionText.vue.d.ts +50 -0
- package/types/generated/components/molecules/FuroSelect/FuroSelectOptionTextContext.d.ts +141 -0
- package/types/generated/components/organisms/FuroTable/FuroTableContext.d.ts +2 -2
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
# `<FuroTextarea>` Interface Specification
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
Read [`<FuroTextField>`](./furo-text-field.md) first. This page describes the **textarea-specific** contract and the few behavioral differences that follow from using a multiline control.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Differences from `<FuroTextField>`
|
|
10
|
+
|
|
11
|
+
| Topic | `<FuroTextField>` | `<FuroTextarea>` |
|
|
12
|
+
|--------|-------------------|------------------|
|
|
13
|
+
| Native element | `<input type="text">` | `<textarea>` |
|
|
14
|
+
| CSS root class | `.unit-form-control.text` | `.unit-form-control.textarea` |
|
|
15
|
+
| `type` fallthrough | Excluded (always `text`) | N/A — `<textarea>` has no `type` |
|
|
16
|
+
| `@commit-value` | `blur` **or** `Enter` | **`blur` only** — `Enter` inserts a newline in a `<textarea>` and must not be treated as “commit” by default |
|
|
17
|
+
| `parcel.value` | `string \| number \| null` | `string \| null` only (multiline text; no numeric shortcut) |
|
|
18
|
+
|
|
19
|
+
### Class and type naming
|
|
20
|
+
|
|
21
|
+
| Role | Name |
|
|
22
|
+
|------|------|
|
|
23
|
+
| Component | `FuroTextarea` |
|
|
24
|
+
| Context | `FuroTextareaContext` |
|
|
25
|
+
| Emit payload | `TextareaEmitPayload` |
|
|
26
|
+
| Parcel typedef | `FuroTextareaParcel` |
|
|
27
|
+
|
|
28
|
+
`TextareaEmitPayload` is defined for `<FuroTextarea>` only. It extends [`BaseEmitPayload`](../lib/emit-payload/BaseEmitPayload.js) and exposes the same helper surface as text-field payloads for consistency (`.value`, `.trimmedValue`, `.hasEmptyText()`, `.hasEmptyTrimmedText()`, `.hasEnteredText()`, `.hasEnteredTrimmedText()`). Trimming applies to the full string, including newlines; parent Context implements line-length or format rules if needed.
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## Props
|
|
33
|
+
|
|
34
|
+
Same **two mechanisms** as `<FuroTextField>`: `parcel` for Furo behavior, fallthrough for native attributes on the `<textarea>`.
|
|
35
|
+
|
|
36
|
+
### Rule: `parcel` vs fallthrough attributes
|
|
37
|
+
|
|
38
|
+
| Mechanism | When to use | Rendered as |
|
|
39
|
+
|-----------|-------------|-------------|
|
|
40
|
+
| `:parcel` | Value, invalid state, and other **component behavior** | Not direct public HTML attr names unless also bound internally |
|
|
41
|
+
| Fallthrough attributes | Native **HTML** attributes for `<textarea>` | On the underlying `<textarea>` |
|
|
42
|
+
|
|
43
|
+
```html
|
|
44
|
+
<!-- ✅ Correct -->
|
|
45
|
+
<FuroTextarea
|
|
46
|
+
:parcel="context.descriptionParcel"
|
|
47
|
+
:disabled="context.descriptionParcel.disabled"
|
|
48
|
+
rows="5"
|
|
49
|
+
name="description"
|
|
50
|
+
/>
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
> **Why the same split as `<FuroTextField>`:** One reviewable `parcel` object for behavior; visible HTML attributes stay in the template where structure is clear.
|
|
54
|
+
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
### `parcel` shape
|
|
58
|
+
|
|
59
|
+
| Key | Type | Required | Default | Description |
|
|
60
|
+
|-----|------|----------|---------|-------------|
|
|
61
|
+
| `value` | `string \| null` | No | `null` | Controlled value; synced to internal ref. |
|
|
62
|
+
| `invalid` | `boolean` | No | `false` | Invalid / error visual state. |
|
|
63
|
+
|
|
64
|
+
> All values must be **JSON-serializable** (no functions, no class instances).
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
### Fallthrough attributes (recommended usage)
|
|
69
|
+
|
|
70
|
+
Vue forwards non-prop attributes to the underlying `<textarea>` (with `inheritAttrs: false`, the Context binds them explicitly — same pattern as other Furo atoms).
|
|
71
|
+
|
|
72
|
+
Commonly used native attributes:
|
|
73
|
+
|
|
74
|
+
| Attribute | Type | Description |
|
|
75
|
+
|-----------|------|-------------|
|
|
76
|
+
| `disabled` | `boolean` | Disables editing. |
|
|
77
|
+
| `readonly` | `boolean` | Read-only display. |
|
|
78
|
+
| `required` | `boolean` | Required for constraint validation. |
|
|
79
|
+
| `placeholder` | `string` | Hint text (not a substitute for a visible label). |
|
|
80
|
+
| `name` | `string` | Form field name. |
|
|
81
|
+
| `rows` | `number` | Visible row hint (HTML `rows`). |
|
|
82
|
+
| `cols` | `number` | Visible column hint (HTML `cols`). |
|
|
83
|
+
| `maxlength` | `number` | Maximum character length. |
|
|
84
|
+
| `minlength` | `number` | Minimum character length. |
|
|
85
|
+
| `wrap` | `'soft' \| 'hard'` | Line wrapping behavior when submitting forms. |
|
|
86
|
+
| `spellcheck` | `boolean \| 'true' \| 'false'` | Native spellcheck hint. |
|
|
87
|
+
| `autocomplete` | `string` | Rare on `<textarea>` but valid when applicable (e.g. street address sections per HTML guidelines). |
|
|
88
|
+
|
|
89
|
+
Attributes that are **out of scope for documentation examples** but remain valid when passed as fallthrough include `id`, `class`, `style`, `autofocus`, and form-associated attributes your project standardizes elsewhere.
|
|
90
|
+
|
|
91
|
+
Do **not** rely on fallthrough for “resize handle” behavior — prefer scoped CSS on `.unit-form-control.textarea` (e.g. `resize: vertical`) so UX stays consistent across apps.
|
|
92
|
+
|
|
93
|
+
---
|
|
94
|
+
|
|
95
|
+
## Emits
|
|
96
|
+
|
|
97
|
+
Same event names as `<FuroTextField>`. Payload instances are **`TextareaEmitPayload`** (not `TextFieldEmitPayload`):
|
|
98
|
+
|
|
99
|
+
| Event | When |
|
|
100
|
+
|-------|------|
|
|
101
|
+
| `@change-value` | Every `input` event (each edit). |
|
|
102
|
+
| `@commit-value` | **`blur` only** on `<textarea>`. |
|
|
103
|
+
| `@update:value` | Alongside the above; raw string for `v-model:value`. |
|
|
104
|
+
|
|
105
|
+
```html
|
|
106
|
+
<FuroTextarea
|
|
107
|
+
:parcel="context.buildTextareaParcel()"
|
|
108
|
+
@change-value="context.onChangeValue({ payload: $event })"
|
|
109
|
+
@commit-value="context.onCommitValue({ payload: $event })"
|
|
110
|
+
/>
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
> **Why no Enter-to-commit:** In a multiline field, `Enter` is content. Reusing `keypress.enter` from `<FuroTextField>` would break expected editing. If a product needs “submit on Ctrl+Enter” (or similar), that belongs in the **parent** Context or a higher-level form pattern, not in the default Atom contract.
|
|
114
|
+
|
|
115
|
+
> **Emitter responsibility:** `<FuroTextarea>` **always emits** for the events above; the parent Context decides validation and side effects. The component does not suppress emits based on value state.
|
|
116
|
+
|
|
117
|
+
---
|
|
118
|
+
|
|
119
|
+
## Event handler style in parent Context
|
|
120
|
+
|
|
121
|
+
Same as [`<FuroTextField>`](./furo-text-field.md#event-handler-style-in-context): **single object parameter** with `{ payload }` destructuring.
|
|
122
|
+
|
|
123
|
+
```js
|
|
124
|
+
onChangeValue ({
|
|
125
|
+
payload,
|
|
126
|
+
}) {
|
|
127
|
+
this.statusReactive.isDescriptionInvalid = !this.validatesDescription({
|
|
128
|
+
payload,
|
|
129
|
+
})
|
|
130
|
+
}
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
---
|
|
134
|
+
|
|
135
|
+
## `TextareaEmitPayload` API
|
|
136
|
+
|
|
137
|
+
`@change-value` and `@commit-value` pass a **`TextareaEmitPayload`** instance as `$event`. Members match the behavioral contract documented for [`TextFieldEmitPayload`](./furo-text-field.md#textfieldemitpayload-api), but the **runtime type** for this component is `TextareaEmitPayload` only.
|
|
138
|
+
|
|
139
|
+
| Member | Type | Description |
|
|
140
|
+
|--------|------|-------------|
|
|
141
|
+
| `.value` | `string \| null` | Current raw string value of the `<textarea>`. |
|
|
142
|
+
| `.trimmedValue` | `string \| null` | Whitespace-trimmed `.value` (multiline strings use normal string trim semantics). |
|
|
143
|
+
| `.hasEmptyText()` | `boolean` | `true` if `.value` is empty or the control is missing. |
|
|
144
|
+
| `.hasEmptyTrimmedText()` | `boolean` | `true` if `.trimmedValue` is empty or the control is missing. |
|
|
145
|
+
| `.hasEnteredText()` | `boolean` | `true` if `.value` has at least one character (whitespace counts). |
|
|
146
|
+
| `.hasEnteredTrimmedText()` | `boolean` | `true` if `.trimmedValue` has at least one non-whitespace character. |
|
|
147
|
+
|
|
148
|
+
Inside `<FuroTextarea>`, emits use `TextareaEmitPayload.create({ rawEvent })` — parallel to `TextFieldEmitPayload` in `<FuroTextField>`, but a separate class so consumers and types stay textarea-specific.
|
|
149
|
+
|
|
150
|
+
---
|
|
151
|
+
|
|
152
|
+
## `v-model` Support
|
|
153
|
+
|
|
154
|
+
Same contract as `<FuroTextField>`: `v-model:value` is sugar over `:parcel` / `@update:value`.
|
|
155
|
+
|
|
156
|
+
```html
|
|
157
|
+
<FuroTextarea v-model:value="context.descriptionRef.value" />
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
---
|
|
161
|
+
|
|
162
|
+
## Controlled vs. uncontrolled state
|
|
163
|
+
|
|
164
|
+
| Mode | How |
|
|
165
|
+
|------|-----|
|
|
166
|
+
| Controlled | `:parcel="{ value: ... }"` or `v-model:value` |
|
|
167
|
+
| Uncontrolled | Omit `value` in `parcel` |
|
|
168
|
+
|
|
169
|
+
Internal watcher order should match `<FuroTextField>`: prefer `parcel.value`, then fall back when documenting parity tests.
|
|
170
|
+
|
|
171
|
+
---
|
|
172
|
+
|
|
173
|
+
## Example
|
|
174
|
+
|
|
175
|
+
```html
|
|
176
|
+
<FuroTextarea
|
|
177
|
+
:parcel="{
|
|
178
|
+
value: context.descriptionRef.value,
|
|
179
|
+
invalid: context.statusReactive.isDescriptionInvalid,
|
|
180
|
+
}"
|
|
181
|
+
:required="true"
|
|
182
|
+
rows="6"
|
|
183
|
+
name="description"
|
|
184
|
+
placeholder="Describe the issue in detail"
|
|
185
|
+
@change-value="context.onChangeDescription({ payload: $event })"
|
|
186
|
+
@commit-value="context.onCommitDescription({ payload: $event })"
|
|
187
|
+
/>
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
```js
|
|
191
|
+
onChangeDescription ({
|
|
192
|
+
payload,
|
|
193
|
+
}) {
|
|
194
|
+
if (payload.hasEmptyTrimmedText()) {
|
|
195
|
+
this.statusReactive.isDescriptionInvalid = true
|
|
196
|
+
|
|
197
|
+
return
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
this.statusReactive.isDescriptionInvalid = !this.validatesDescription({
|
|
201
|
+
payload,
|
|
202
|
+
})
|
|
203
|
+
}
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
---
|
|
207
|
+
|
|
208
|
+
## Implementation notes (non-normative)
|
|
209
|
+
|
|
210
|
+
- **Height:** Use `rows` and/or CSS `min-height` on `.unit-form-control.textarea` so layouts do not collapse when `rows` is omitted.
|
|
211
|
+
- **Accessibility:** Keep an associated `<label>` (or `aria-labelledby`) in the parent; pass `id` through fallthrough when wiring `for` / `id`.
|
|
212
|
+
- **Testing:** Snapshot or e2e tests should assert that Enter **does not** fire `commit-value`, only `change-value` (input), whereas blur fires `commit-value`.
|
|
213
|
+
|
|
214
|
+
---
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
# FuroTimeField
|
|
2
|
+
|
|
3
|
+
Single-time molecule built on a headless segmented time-field primitive. Renders an inline segmented time control (hour / minute, optionally seconds, optionally AM/PM). No popover or portal — segments are edited in place via type or arrow keys.
|
|
4
|
+
|
|
5
|
+
## Summary
|
|
6
|
+
|
|
7
|
+
| Field | Value |
|
|
8
|
+
|---|---|
|
|
9
|
+
| **Layer** | Molecule |
|
|
10
|
+
| **Path** | `lib/components/molecules/FuroTimeField/` |
|
|
11
|
+
| **Base primitive** | Headless segmented time-field (`Root` + `Input`) |
|
|
12
|
+
| **Value type** | `HH:MM:SS` string or `null` |
|
|
13
|
+
| **Wire format** | Always `HH:MM:SS` (zero-padded via `Time#toString()`); accepts `HH:MM` on input |
|
|
14
|
+
| **Granularity** | `minute` (default), `hour`, or `second` via `parcel.granularity` |
|
|
15
|
+
| **Time format** | 12 or 24 via `parcel.timeFormat` (locale default when unset) |
|
|
16
|
+
| **Step** | Step amount for arrow edits and the ▲/▼ buttons via `parcel.step` (default `1`); the unit follows `granularity` (the smallest visible segment) |
|
|
17
|
+
| **Timezone** | Configurable via `parcel.timezone` (IANA); default `null` = naive wall-clock |
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## Public API
|
|
22
|
+
|
|
23
|
+
### Props
|
|
24
|
+
|
|
25
|
+
| Prop | Type | Default | Description |
|
|
26
|
+
|---|---|---|---|
|
|
27
|
+
| `parcel` | `TimeFieldParcel \| null` | `null` | Reactive data object. Furo-only keys are stripped before passthrough to the underlying primitive. |
|
|
28
|
+
|
|
29
|
+
### Parcel keys
|
|
30
|
+
|
|
31
|
+
| Key | Type | Description |
|
|
32
|
+
|---|---|---|
|
|
33
|
+
| `value` | `string \| null` | Current time as `HH:MM:SS` (or `HH:MM`) or `null`. Context also accepts `Date` / `Time` at the boundary. |
|
|
34
|
+
| `invalid` | `boolean` | Forces error chrome and `aria-invalid` (external validation). |
|
|
35
|
+
| `placeholder` | `string \| null` | Placeholder text for the time field. |
|
|
36
|
+
| `disabled` | `boolean` | Disables all interaction. Also respected from fallthrough `disabled`, including the bare `<FuroTimeField disabled>` spelling. |
|
|
37
|
+
| `readonly` | `boolean` | Segments are focusable but not editable. Also respected from fallthrough `readonly`, including the bare `<FuroTimeField readonly>` spelling. |
|
|
38
|
+
| `decrementLabel` | `string` | Accessible name of the decrement button. Default `'Decrement time'`. |
|
|
39
|
+
| `incrementLabel` | `string` | Accessible name of the increment button. Default `'Increment time'`. |
|
|
40
|
+
| `minValue` | `string \| null` | Minimum time (`HH:MM:SS`). Out-of-range values are **flagged** (`aria-invalid` + error chrome), not clamped. |
|
|
41
|
+
| `maxValue` | `string \| null` | Maximum time (`HH:MM:SS`). Out-of-range values are **flagged**, not clamped. |
|
|
42
|
+
| `timeFormat` | `number \| null` | `12` or `24`. When unset, the locale default applies. |
|
|
43
|
+
| `granularity` | `'hour' \| 'minute' \| 'second' \| null` | Controls visible segments. When unset, it defaults to `minute`. |
|
|
44
|
+
| `step` | `number \| null` | Step amount for segment arrow edits and the ▲/▼ buttons. Default `1`. The button step unit follows `granularity` (`hour`→hours, `second`→seconds, otherwise minutes). Forwarded as the native `step`. |
|
|
45
|
+
| `stepSnapping` | `boolean \| null` | Optional `stepSnapping`. Forwarded only when explicitly set. |
|
|
46
|
+
| `timezone` | `string \| null` | IANA timezone identifier (e.g. `'Asia/Tokyo'`). When set, `Date` inputs are normalized to wall-clock parts in that zone. Default `null` = naive (local parts). **Furo-only — never forwarded to the underlying primitive.** |
|
|
47
|
+
|
|
48
|
+
All parcel keys above are **Furo-only** — they are stripped before the root v-bind spread. `step` (and `stepSnapping` when set) are re-applied to the root after stripping; `timezone` is never forwarded. Pass additional native props via `parcel` to forward them.
|
|
49
|
+
|
|
50
|
+
### Emits
|
|
51
|
+
|
|
52
|
+
| Event | Payload | Description |
|
|
53
|
+
|---|---|---|
|
|
54
|
+
| `change-value` | `TimeFieldEmitPayload` | Fired when the user edits a segment. |
|
|
55
|
+
| `commit-value` | `TimeFieldEmitPayload` | Fired on the same edit event (inline single-region UX). |
|
|
56
|
+
| `update:value` | `string \| null` | `HH:MM:SS` string or `null`. Suitable for `v-model:value`. |
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
## Usage
|
|
61
|
+
|
|
62
|
+
### Basic
|
|
63
|
+
|
|
64
|
+
```vue
|
|
65
|
+
<FuroTimeField
|
|
66
|
+
:parcel="{ placeholder: 'Pick a time' }"
|
|
67
|
+
@update:value="onValueChange"
|
|
68
|
+
/>
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### v-model binding
|
|
72
|
+
|
|
73
|
+
```vue
|
|
74
|
+
<FuroTimeField
|
|
75
|
+
v-model:value="selectedTime"
|
|
76
|
+
:parcel="{ placeholder: 'Pick a time' }"
|
|
77
|
+
/>
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### 12-hour cycle
|
|
81
|
+
|
|
82
|
+
```vue
|
|
83
|
+
<FuroTimeField
|
|
84
|
+
:parcel="{
|
|
85
|
+
timeFormat: 12,
|
|
86
|
+
value: '14:05:00',
|
|
87
|
+
}"
|
|
88
|
+
@update:value="onValueChange"
|
|
89
|
+
/>
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
`update:value` is always emitted as a 24-hour `HH:MM:SS` string regardless of `timeFormat`.
|
|
93
|
+
|
|
94
|
+
### Second granularity
|
|
95
|
+
|
|
96
|
+
```vue
|
|
97
|
+
<FuroTimeField
|
|
98
|
+
:parcel="{
|
|
99
|
+
granularity: 'second',
|
|
100
|
+
value: '08:15:42',
|
|
101
|
+
}"
|
|
102
|
+
@update:value="onValueChange"
|
|
103
|
+
/>
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### Min / Max bounds
|
|
107
|
+
|
|
108
|
+
```vue
|
|
109
|
+
<FuroTimeField
|
|
110
|
+
:parcel="{
|
|
111
|
+
minValue: '09:00:00',
|
|
112
|
+
maxValue: '17:00:00',
|
|
113
|
+
}"
|
|
114
|
+
@update:value="onValueChange"
|
|
115
|
+
/>
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Bounds are **flag-only** for segment editing: out-of-range entries set `aria-invalid` and show error chrome. The control does not clamp or block out-of-range segments — combine with `parcel.invalid` for external validation. The increment/decrement buttons, however, **do clamp** their result to `[minValue, maxValue]`.
|
|
119
|
+
|
|
120
|
+
### Customizable step + increment/decrement buttons
|
|
121
|
+
|
|
122
|
+
```vue
|
|
123
|
+
<FuroTimeField
|
|
124
|
+
:parcel="{
|
|
125
|
+
step: 5,
|
|
126
|
+
value: '09:00:00',
|
|
127
|
+
}"
|
|
128
|
+
@update:value="onValueChange"
|
|
129
|
+
/>
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
`parcel.step` (default `1`) controls both the segment arrow-key increment and the two native ▲/▼ stepper buttons rendered inside the field. The step **unit follows `granularity`** — the smallest visible segment: `granularity: 'hour'` steps hours, `'second'` steps seconds, otherwise (default) minutes. The buttons:
|
|
133
|
+
|
|
134
|
+
- Add / subtract `step` of the smallest segment via `@internationalized/date` `Time.add` / `Time.subtract`.
|
|
135
|
+
- **Clamp** the result to `[minValue, maxValue]` when bounds are set (no overflow past a bound).
|
|
136
|
+
- Start from `00:00:00` when the current value is `null`.
|
|
137
|
+
- Route through the same emit path as segment edits (`change-value` → `update:value` → `commit-value`).
|
|
138
|
+
- Are disabled when `parcel.disabled` / `parcel.readonly`, when increment is at/over `maxValue`, or when decrement is at/under `minValue`.
|
|
139
|
+
|
|
140
|
+
### Configurable timezone
|
|
141
|
+
|
|
142
|
+
```vue
|
|
143
|
+
<FuroTimeField
|
|
144
|
+
:parcel="{
|
|
145
|
+
timezone: 'Asia/Tokyo',
|
|
146
|
+
value: someDateObject,
|
|
147
|
+
}"
|
|
148
|
+
@update:value="onValueChange"
|
|
149
|
+
/>
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
When `parcel.timezone` is an IANA identifier, a `Date` input is normalized to wall-clock time parts **in that zone** (via `Intl.DateTimeFormat` with `hourCycle: 'h23'`, each part zero-padded). When `timezone` is `null` (default), `Date` inputs use local `getHours()` / `getMinutes()` / `getSeconds()` (naive). An invalid timezone identifier normalizes to `null`.
|
|
153
|
+
|
|
154
|
+
The library stays **domain-agnostic** — it never hardcodes a default timezone. Consumers set the default: for example, CloudLink passes `timezone: 'Asia/Tokyo'` from its adapter.
|
|
155
|
+
|
|
156
|
+
> **Controlled-value note.** `timezone` only affects how a `Date` **input** is read into wall-clock parts; edits emit a plain `HH:MM:SS` string. If you bind `value` to a **fixed** `Date` (not a `ref` updated by `@update:value`), every edit re-normalizes that same `Date` back and the field appears frozen. Bind a controlled value instead — feed the emitted string back, falling back to the `Date` only initially:
|
|
157
|
+
>
|
|
158
|
+
> ```vue
|
|
159
|
+
> :parcel="{ timezone: 'Asia/Tokyo', value: emittedValueRef ?? initialDate }"
|
|
160
|
+
> @update:value="value => { emittedValueRef = value }"
|
|
161
|
+
> ```
|
|
162
|
+
|
|
163
|
+
### Disabled / read-only
|
|
164
|
+
|
|
165
|
+
```vue
|
|
166
|
+
<FuroTimeField :parcel="{ disabled: true }" />
|
|
167
|
+
<FuroTimeField :parcel="{ value: '11:20:00', readonly: true }" />
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
### Invalid state
|
|
171
|
+
|
|
172
|
+
```vue
|
|
173
|
+
<FuroTimeField
|
|
174
|
+
:parcel="{
|
|
175
|
+
placeholder: 'Required field',
|
|
176
|
+
invalid: isFieldInvalid,
|
|
177
|
+
}"
|
|
178
|
+
/>
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
---
|
|
182
|
+
|
|
183
|
+
## Value contract
|
|
184
|
+
|
|
185
|
+
| Concern | Decision |
|
|
186
|
+
|---|---|
|
|
187
|
+
| Wire format | `HH:MM:SS` string — always serialized via `Time#toString()` (zero-pads). `HH:MM` accepted on input and padded to `HH:MM:SS`. |
|
|
188
|
+
| Timezone | **Configurable** via `parcel.timezone` (IANA). Default `null` = naive wall-clock (`Date` inputs use local `getHours()` / `getMinutes()` / `getSeconds()`). When set, `Date` inputs are normalized to wall-clock parts in that zone. The library never hardcodes a default — consumers set it (e.g. CloudLink → `'Asia/Tokyo'`). |
|
|
189
|
+
| Range | Single time only; time-range support out of scope. |
|
|
190
|
+
| Clear | Pass `null` to `parcel.value`, or clear all segments via keyboard, to reset to `null`. |
|
|
191
|
+
|
|
192
|
+
The context accepts these input shapes at the boundary and normalizes to an internal `@internationalized/date` `Time`:
|
|
193
|
+
|
|
194
|
+
- `HH:MM` / `HH:MM:SS` string → `parseTime`
|
|
195
|
+
- `Date` object (no `timezone`) → `new Time(hours, minutes, seconds)` (local parts)
|
|
196
|
+
- `Date` object (with `timezone`) → wall-clock parts in that IANA zone via `Intl.DateTimeFormat`
|
|
197
|
+
- `Time` instance → passthrough
|
|
198
|
+
- `null` / invalid → `null`
|
|
199
|
+
|
|
200
|
+
### Native mobile fallback
|
|
201
|
+
|
|
202
|
+
This molecule does not render a native `<input type="time">`. On mobile, the segmented control is still keyboard- and tap-editable; a native fallback is documented here as a future consideration but is out of scope for v1.
|
|
203
|
+
|
|
204
|
+
---
|
|
205
|
+
|
|
206
|
+
## CSS classes
|
|
207
|
+
|
|
208
|
+
```text
|
|
209
|
+
.unit-form-control.time-field ← host div
|
|
210
|
+
.unit-form-control.time-field > .field ← root host element
|
|
211
|
+
.unit-form-control.time-field > .field.invalid ← invalid state (parcel.invalid OR out-of-range)
|
|
212
|
+
.unit-form-control.time-field > .field.disabled ← disabled state
|
|
213
|
+
.unit-form-control.time-field > .field > .field-input ← editable segment (hour/minute/second/dayPeriod)
|
|
214
|
+
.unit-form-control.time-field > .field > .field-input[data-placeholder] ← placeholder segment
|
|
215
|
+
.unit-form-control.time-field > .field > .field-input:focus-visible ← keyboard-active segment
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
All CSS is scoped under `@layer furo` and uses semantic tokens only (`--color-*`, `--size-*`, `--font-size-*`).
|
|
219
|
+
|
|
220
|
+
---
|
|
221
|
+
|
|
222
|
+
## Context: `FuroTimeFieldContext`
|
|
223
|
+
|
|
224
|
+
`FuroTimeFieldContext` (extends `BaseFuroContext`) holds all reactive state and computed logic. The Vue component is a thin template wrapper.
|
|
225
|
+
|
|
226
|
+
Key methods:
|
|
227
|
+
|
|
228
|
+
| Method | Description |
|
|
229
|
+
|---|---|
|
|
230
|
+
| `normalizeToTime({ raw })` | Converts `HH:MM` / `HH:MM:SS` / `Date` / `Time` / `null` → `Time \| null`. |
|
|
231
|
+
| `normalizeStringToTime({ raw })` | Parses a time string via `parseTime`; returns `null` on failure. |
|
|
232
|
+
| `normalizeDateToTime({ raw })` | Converts a `Date` → `Time`. Uses local wall-clock parts, or `parcel.timezone` parts when set. |
|
|
233
|
+
| `normalizeDateToTimeInTimezone({ raw, timezone })` | Converts a `Date` → `Time` using wall-clock parts in a specific IANA timezone; `null` on invalid zone. |
|
|
234
|
+
| `extractTimePartsInTimezone({ raw, timezone })` (static) | Pulls zero-padded `hour` / `minute` / `second` strings from a `Date` via `Intl.DateTimeFormat` (`hourCycle: 'h23'`). |
|
|
235
|
+
| `normalizeToIsoString({ time })` | Converts `Time \| null` → `HH:MM:SS` string or `null` via `Time#toString()`. |
|
|
236
|
+
| `normalizeMinValue()` / `normalizeMaxValue()` | Returns `Time` from `parcel.minValue` / `parcel.maxValue`. |
|
|
237
|
+
| `extractRootAttributes()` | Builds the root attribute object (strips Furo-only keys, applies `modelValue`, `disabled`, `readonly`, `step`, and optional `stepSnapping` / `timeFormat` / `granularity` / `minValue` / `maxValue`). |
|
|
238
|
+
| `stripFuroParcelKeys()` | Removes `FURO_TIME_FIELD_PARCEL_KEYS` from parcel before spreading to the underlying primitive. |
|
|
239
|
+
| `onChangeValue({ time })` | Handles the primitive's `update:modelValue`; emits all three events. |
|
|
240
|
+
| `onIncrementValue()` / `onDecrementValue()` | Steps the value by `step` minutes, clamps to bounds, then routes through the emit trio. |
|
|
241
|
+
| `clampTime({ time })` | Clamps a `Time` into `[minValue, maxValue]`. |
|
|
242
|
+
| `isLiteralPart({ part })` | Whether a segment part is a literal separator (e.g. `:`). |
|
|
243
|
+
| `formatSegmentValue({ part, value })` | Zero-pads filled hour/minute/second segments to two digits (`9` → `09`); day period and literals pass through. With `tabular-nums` this keeps the field width constant as the value changes (no layout shift). |
|
|
244
|
+
| `isTwoDigitSegmentPart({ part })` | Whether a segment part displays zero-padded to two digits (hour, minute, second). |
|
|
245
|
+
|
|
246
|
+
---
|
|
247
|
+
|
|
248
|
+
## Files
|
|
249
|
+
|
|
250
|
+
```text
|
|
251
|
+
lib/components/molecules/FuroTimeField/
|
|
252
|
+
├── FuroTimeField.vue ← component
|
|
253
|
+
├── FuroTimeFieldContext.js ← context
|
|
254
|
+
└── TimeFieldEmitPayload.js ← typed emit payload
|
|
255
|
+
|
|
256
|
+
pages/samples/components/playground/FuroTimeFieldPlayground/
|
|
257
|
+
├── index.vue ← playground page
|
|
258
|
+
└── FuroTimeFieldPlaygroundContext.js
|
|
259
|
+
|
|
260
|
+
tests/__tests__/jsdom/lib/components/molecules/FuroTimeField/
|
|
261
|
+
├── FuroTimeFieldContext.js ← context tests
|
|
262
|
+
└── TimeFieldEmitPayload.js ← payload tests
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
---
|
|
266
|
+
|
|
267
|
+
## Out of scope (v1)
|
|
268
|
+
|
|
269
|
+
- Combined datetime molecule (`FuroDateTimePicker` — Story 1.5)
|
|
270
|
+
- Time range (`TimeRangeField*`)
|
|
271
|
+
- Native `<input type="time">` mobile fallback
|
|
272
|
+
- i18n locale packs (prop-only)
|
|
273
|
+
|
|
274
|
+
## Commit semantics note
|
|
275
|
+
|
|
276
|
+
`change-value` and `commit-value` fire together on every segment edit and on every stepper button press — there is no separate blur/enter distinction (inline single-region UX, identical to `FuroDatePicker`). Treat them as equivalent until cross-component commit semantics are unified.
|