@radix-ng/primitives 0.50.0 → 1.0.0-beta.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.
Files changed (207) hide show
  1. package/collection/README.md +1 -0
  2. package/fesm2022/radix-ng-primitives-accordion.mjs +134 -66
  3. package/fesm2022/radix-ng-primitives-accordion.mjs.map +1 -1
  4. package/fesm2022/radix-ng-primitives-alert-dialog.mjs +224 -132
  5. package/fesm2022/radix-ng-primitives-alert-dialog.mjs.map +1 -1
  6. package/fesm2022/radix-ng-primitives-arrow.mjs +26 -10
  7. package/fesm2022/radix-ng-primitives-arrow.mjs.map +1 -1
  8. package/fesm2022/radix-ng-primitives-aspect-ratio.mjs +6 -6
  9. package/fesm2022/radix-ng-primitives-aspect-ratio.mjs.map +1 -1
  10. package/fesm2022/radix-ng-primitives-avatar.mjs +68 -75
  11. package/fesm2022/radix-ng-primitives-avatar.mjs.map +1 -1
  12. package/fesm2022/radix-ng-primitives-button.mjs +123 -0
  13. package/fesm2022/radix-ng-primitives-button.mjs.map +1 -0
  14. package/fesm2022/radix-ng-primitives-calendar.mjs +104 -103
  15. package/fesm2022/radix-ng-primitives-calendar.mjs.map +1 -1
  16. package/fesm2022/radix-ng-primitives-checkbox.mjs +414 -80
  17. package/fesm2022/radix-ng-primitives-checkbox.mjs.map +1 -1
  18. package/fesm2022/radix-ng-primitives-collapsible.mjs +193 -92
  19. package/fesm2022/radix-ng-primitives-collapsible.mjs.map +1 -1
  20. package/fesm2022/radix-ng-primitives-collection.mjs +72 -0
  21. package/fesm2022/radix-ng-primitives-collection.mjs.map +1 -0
  22. package/fesm2022/radix-ng-primitives-config.mjs +5 -5
  23. package/fesm2022/radix-ng-primitives-config.mjs.map +1 -1
  24. package/fesm2022/radix-ng-primitives-context-menu.mjs +143 -427
  25. package/fesm2022/radix-ng-primitives-context-menu.mjs.map +1 -1
  26. package/fesm2022/radix-ng-primitives-core.mjs +757 -757
  27. package/fesm2022/radix-ng-primitives-core.mjs.map +1 -1
  28. package/fesm2022/radix-ng-primitives-cropper.mjs +55 -53
  29. package/fesm2022/radix-ng-primitives-cropper.mjs.map +1 -1
  30. package/fesm2022/radix-ng-primitives-date-field.mjs +93 -86
  31. package/fesm2022/radix-ng-primitives-date-field.mjs.map +1 -1
  32. package/fesm2022/radix-ng-primitives-dialog.mjs +658 -330
  33. package/fesm2022/radix-ng-primitives-dialog.mjs.map +1 -1
  34. package/fesm2022/radix-ng-primitives-dismissable-layer.mjs +98 -76
  35. package/fesm2022/radix-ng-primitives-dismissable-layer.mjs.map +1 -1
  36. package/fesm2022/radix-ng-primitives-drawer.mjs +1059 -0
  37. package/fesm2022/radix-ng-primitives-drawer.mjs.map +1 -0
  38. package/fesm2022/radix-ng-primitives-editable.mjs +20 -20
  39. package/fesm2022/radix-ng-primitives-editable.mjs.map +1 -1
  40. package/fesm2022/radix-ng-primitives-field.mjs +363 -0
  41. package/fesm2022/radix-ng-primitives-field.mjs.map +1 -0
  42. package/fesm2022/radix-ng-primitives-fieldset.mjs +79 -0
  43. package/fesm2022/radix-ng-primitives-fieldset.mjs.map +1 -0
  44. package/fesm2022/radix-ng-primitives-focus-guards.mjs +3 -3
  45. package/fesm2022/radix-ng-primitives-focus-guards.mjs.map +1 -1
  46. package/fesm2022/radix-ng-primitives-focus-scope.mjs +29 -14
  47. package/fesm2022/radix-ng-primitives-focus-scope.mjs.map +1 -1
  48. package/fesm2022/radix-ng-primitives-input.mjs +172 -0
  49. package/fesm2022/radix-ng-primitives-input.mjs.map +1 -0
  50. package/fesm2022/radix-ng-primitives-label.mjs +11 -11
  51. package/fesm2022/radix-ng-primitives-label.mjs.map +1 -1
  52. package/fesm2022/radix-ng-primitives-menu.mjs +1484 -353
  53. package/fesm2022/radix-ng-primitives-menu.mjs.map +1 -1
  54. package/fesm2022/radix-ng-primitives-menubar.mjs +290 -162
  55. package/fesm2022/radix-ng-primitives-menubar.mjs.map +1 -1
  56. package/fesm2022/radix-ng-primitives-meter.mjs +271 -0
  57. package/fesm2022/radix-ng-primitives-meter.mjs.map +1 -0
  58. package/fesm2022/radix-ng-primitives-navigation-menu.mjs +1060 -1553
  59. package/fesm2022/radix-ng-primitives-navigation-menu.mjs.map +1 -1
  60. package/fesm2022/radix-ng-primitives-number-field.mjs +1102 -366
  61. package/fesm2022/radix-ng-primitives-number-field.mjs.map +1 -1
  62. package/fesm2022/radix-ng-primitives-pagination.mjs +51 -51
  63. package/fesm2022/radix-ng-primitives-pagination.mjs.map +1 -1
  64. package/fesm2022/radix-ng-primitives-popover.mjs +980 -995
  65. package/fesm2022/radix-ng-primitives-popover.mjs.map +1 -1
  66. package/fesm2022/radix-ng-primitives-popper.mjs +137 -82
  67. package/fesm2022/radix-ng-primitives-popper.mjs.map +1 -1
  68. package/fesm2022/radix-ng-primitives-portal.mjs +40 -16
  69. package/fesm2022/radix-ng-primitives-portal.mjs.map +1 -1
  70. package/fesm2022/radix-ng-primitives-presence.mjs +134 -246
  71. package/fesm2022/radix-ng-primitives-presence.mjs.map +1 -1
  72. package/fesm2022/radix-ng-primitives-preview-card.mjs +997 -0
  73. package/fesm2022/radix-ng-primitives-preview-card.mjs.map +1 -0
  74. package/fesm2022/radix-ng-primitives-progress.mjs +231 -92
  75. package/fesm2022/radix-ng-primitives-progress.mjs.map +1 -1
  76. package/fesm2022/radix-ng-primitives-radio.mjs +211 -70
  77. package/fesm2022/radix-ng-primitives-radio.mjs.map +1 -1
  78. package/fesm2022/radix-ng-primitives-roving-focus.mjs +127 -77
  79. package/fesm2022/radix-ng-primitives-roving-focus.mjs.map +1 -1
  80. package/fesm2022/radix-ng-primitives-select.mjs +791 -511
  81. package/fesm2022/radix-ng-primitives-select.mjs.map +1 -1
  82. package/fesm2022/radix-ng-primitives-separator.mjs +16 -45
  83. package/fesm2022/radix-ng-primitives-separator.mjs.map +1 -1
  84. package/fesm2022/radix-ng-primitives-slider.mjs +976 -720
  85. package/fesm2022/radix-ng-primitives-slider.mjs.map +1 -1
  86. package/fesm2022/radix-ng-primitives-stepper.mjs +69 -71
  87. package/fesm2022/radix-ng-primitives-stepper.mjs.map +1 -1
  88. package/fesm2022/radix-ng-primitives-switch.mjs +128 -124
  89. package/fesm2022/radix-ng-primitives-switch.mjs.map +1 -1
  90. package/fesm2022/radix-ng-primitives-tabs.mjs +388 -115
  91. package/fesm2022/radix-ng-primitives-tabs.mjs.map +1 -1
  92. package/fesm2022/radix-ng-primitives-time-field.mjs +111 -117
  93. package/fesm2022/radix-ng-primitives-time-field.mjs.map +1 -1
  94. package/fesm2022/radix-ng-primitives-toggle-group.mjs +122 -248
  95. package/fesm2022/radix-ng-primitives-toggle-group.mjs.map +1 -1
  96. package/fesm2022/radix-ng-primitives-toggle.mjs +99 -62
  97. package/fesm2022/radix-ng-primitives-toggle.mjs.map +1 -1
  98. package/fesm2022/radix-ng-primitives-toolbar.mjs +307 -94
  99. package/fesm2022/radix-ng-primitives-toolbar.mjs.map +1 -1
  100. package/fesm2022/radix-ng-primitives-tooltip.mjs +690 -1079
  101. package/fesm2022/radix-ng-primitives-tooltip.mjs.map +1 -1
  102. package/fesm2022/radix-ng-primitives-visually-hidden.mjs +46 -87
  103. package/fesm2022/radix-ng-primitives-visually-hidden.mjs.map +1 -1
  104. package/fesm2022/radix-ng-primitives.mjs.map +1 -1
  105. package/meter/README.md +3 -0
  106. package/navigation-menu/README.md +2 -1
  107. package/package.json +85 -63
  108. package/portal/README.md +2 -0
  109. package/preview-card/README.md +3 -0
  110. package/schematics/collection.json +1 -0
  111. package/schematics/ng-add/index.d.ts +3 -2
  112. package/schematics/ng-add/index.js +62 -31
  113. package/schematics/ng-add/index.js.map +1 -1
  114. package/schematics/ng-add/package-config.d.ts +4 -2
  115. package/schematics/ng-add/package-config.js +10 -2
  116. package/schematics/ng-add/package-config.js.map +1 -1
  117. package/schematics/ng-add/schema.d.ts +3 -0
  118. package/schematics/ng-add/schema.js +3 -0
  119. package/schematics/ng-add/schema.js.map +1 -0
  120. package/schematics/ng-add/schema.json +14 -0
  121. package/select/README.md +2 -0
  122. package/{accordion/index.d.ts → types/radix-ng-primitives-accordion.d.ts} +102 -67
  123. package/types/radix-ng-primitives-alert-dialog.d.ts +114 -0
  124. package/{arrow/index.d.ts → types/radix-ng-primitives-arrow.d.ts} +1 -1
  125. package/{aspect-ratio/index.d.ts → types/radix-ng-primitives-aspect-ratio.d.ts} +1 -1
  126. package/{avatar/index.d.ts → types/radix-ng-primitives-avatar.d.ts} +7 -11
  127. package/types/radix-ng-primitives-button.d.ts +73 -0
  128. package/{calendar/index.d.ts → types/radix-ng-primitives-calendar.d.ts} +2 -3
  129. package/types/radix-ng-primitives-checkbox.d.ts +337 -0
  130. package/types/radix-ng-primitives-collapsible.d.ts +159 -0
  131. package/types/radix-ng-primitives-collection.d.ts +44 -0
  132. package/{config/index.d.ts → types/radix-ng-primitives-config.d.ts} +1 -1
  133. package/types/radix-ng-primitives-context-menu.d.ts +73 -0
  134. package/{core/index.d.ts → types/radix-ng-primitives-core.d.ts} +311 -236
  135. package/{cropper/index.d.ts → types/radix-ng-primitives-cropper.d.ts} +6 -5
  136. package/{date-field/index.d.ts → types/radix-ng-primitives-date-field.d.ts} +42 -27
  137. package/types/radix-ng-primitives-dialog.d.ts +323 -0
  138. package/{dismissable-layer/index.d.ts → types/radix-ng-primitives-dismissable-layer.d.ts} +15 -7
  139. package/types/radix-ng-primitives-drawer.d.ts +448 -0
  140. package/{editable/index.d.ts → types/radix-ng-primitives-editable.d.ts} +1 -1
  141. package/types/radix-ng-primitives-field.d.ts +373 -0
  142. package/types/radix-ng-primitives-fieldset.d.ts +48 -0
  143. package/{focus-scope/index.d.ts → types/radix-ng-primitives-focus-scope.d.ts} +13 -5
  144. package/types/radix-ng-primitives-input.d.ts +87 -0
  145. package/{label/index.d.ts → types/radix-ng-primitives-label.d.ts} +0 -1
  146. package/types/radix-ng-primitives-menu.d.ts +612 -0
  147. package/types/radix-ng-primitives-menubar.d.ts +66 -0
  148. package/types/radix-ng-primitives-meter.d.ts +193 -0
  149. package/types/radix-ng-primitives-navigation-menu.d.ts +488 -0
  150. package/types/radix-ng-primitives-number-field.d.ts +464 -0
  151. package/{pagination/index.d.ts → types/radix-ng-primitives-pagination.d.ts} +2 -2
  152. package/types/radix-ng-primitives-popover.d.ts +416 -0
  153. package/{popper/index.d.ts → types/radix-ng-primitives-popper.d.ts} +50 -9
  154. package/types/radix-ng-primitives-portal.d.ts +30 -0
  155. package/types/radix-ng-primitives-presence.d.ts +55 -0
  156. package/types/radix-ng-primitives-preview-card.d.ts +359 -0
  157. package/types/radix-ng-primitives-progress.d.ts +206 -0
  158. package/{radio/index.d.ts → types/radix-ng-primitives-radio.d.ts} +56 -26
  159. package/{roving-focus/index.d.ts → types/radix-ng-primitives-roving-focus.d.ts} +38 -27
  160. package/types/radix-ng-primitives-select.d.ts +512 -0
  161. package/types/radix-ng-primitives-separator.d.ts +38 -0
  162. package/types/radix-ng-primitives-slider.d.ts +377 -0
  163. package/{stepper/index.d.ts → types/radix-ng-primitives-stepper.d.ts} +21 -22
  164. package/types/radix-ng-primitives-switch.d.ts +121 -0
  165. package/types/radix-ng-primitives-tabs.d.ts +247 -0
  166. package/{time-field/index.d.ts → types/radix-ng-primitives-time-field.d.ts} +46 -31
  167. package/types/radix-ng-primitives-toggle-group.d.ts +116 -0
  168. package/types/radix-ng-primitives-toggle.d.ts +65 -0
  169. package/types/radix-ng-primitives-toolbar.d.ts +180 -0
  170. package/types/radix-ng-primitives-tooltip.d.ts +395 -0
  171. package/{visually-hidden/index.d.ts → types/radix-ng-primitives-visually-hidden.d.ts} +19 -19
  172. package/alert-dialog/index.d.ts +0 -57
  173. package/checkbox/index.d.ts +0 -164
  174. package/collapsible/index.d.ts +0 -85
  175. package/context-menu/index.d.ts +0 -129
  176. package/dialog/index.d.ts +0 -205
  177. package/dropdown-menu/README.md +0 -1
  178. package/dropdown-menu/index.d.ts +0 -171
  179. package/fesm2022/radix-ng-primitives-dropdown-menu.mjs +0 -583
  180. package/fesm2022/radix-ng-primitives-dropdown-menu.mjs.map +0 -1
  181. package/fesm2022/radix-ng-primitives-hover-card.mjs +0 -1246
  182. package/fesm2022/radix-ng-primitives-hover-card.mjs.map +0 -1
  183. package/fesm2022/radix-ng-primitives-tooltip2.mjs +0 -740
  184. package/fesm2022/radix-ng-primitives-tooltip2.mjs.map +0 -1
  185. package/hover-card/README.md +0 -3
  186. package/hover-card/index.d.ts +0 -472
  187. package/menu/index.d.ts +0 -139
  188. package/menubar/index.d.ts +0 -56
  189. package/navigation-menu/index.d.ts +0 -405
  190. package/number-field/index.d.ts +0 -203
  191. package/popover/index.d.ts +0 -403
  192. package/portal/index.d.ts +0 -22
  193. package/presence/index.d.ts +0 -103
  194. package/progress/index.d.ts +0 -79
  195. package/select/index.d.ts +0 -214
  196. package/separator/index.d.ts +0 -63
  197. package/slider/index.d.ts +0 -263
  198. package/switch/index.d.ts +0 -105
  199. package/tabs/index.d.ts +0 -112
  200. package/toggle/index.d.ts +0 -75
  201. package/toggle-group/index.d.ts +0 -194
  202. package/toolbar/index.d.ts +0 -55
  203. package/tooltip/index.d.ts +0 -433
  204. package/tooltip2/README.md +0 -3
  205. package/tooltip2/index.d.ts +0 -325
  206. /package/{focus-guards/index.d.ts → types/radix-ng-primitives-focus-guards.d.ts} +0 -0
  207. /package/{index.d.ts → types/radix-ng-primitives.d.ts} +0 -0
@@ -1,505 +1,1241 @@
1
1
  import * as i0 from '@angular/core';
2
- import { InjectionToken, inject, DestroyRef, signal, effect, Injectable, computed, ElementRef, input, booleanAttribute, Directive, model, numberAttribute, NgModule } from '@angular/core';
2
+ import { numberAttribute, computed, signal, inject, DestroyRef, input, booleanAttribute, Directive, ElementRef, model, output, effect, untracked, NgModule } from '@angular/core';
3
+ import * as i1 from '@radix-ng/primitives/core';
4
+ import { createContext, clamp, getActiveElement, injectControlValueAccessor, injectId, RdxControlValueAccessor } from '@radix-ng/primitives/core';
3
5
  import { NumberFormatter, NumberParser } from '@internationalized/number';
4
- import { Subject, fromEvent } from 'rxjs';
5
- import { takeUntil } from 'rxjs/operators';
6
- import { getActiveElement, isNullish, clamp, snapValueToStep, provideToken } from '@radix-ng/primitives/core';
6
+ import * as i1$1 from '@radix-ng/primitives/portal';
7
+ import { RdxPortal } from '@radix-ng/primitives/portal';
7
8
 
8
- const NUMBER_FIELD_ROOT_CONTEXT = new InjectionToken('NUMBER_FIELD_ROOT_CONTEXT');
9
- function injectNumberFieldRootContext() {
10
- return inject(NUMBER_FIELD_ROOT_CONTEXT);
11
- }
9
+ /**
10
+ * The Number Field context exposes the root directive instance to every child part.
11
+ * The root owns all state, parsing/formatting and value-change logic; parts read
12
+ * signals and call methods off it.
13
+ *
14
+ * @see https://base-ui.com/react/components/number-field
15
+ */
16
+ const [injectNumberFieldRootContext, provideNumberFieldRootContext] = createContext('RdxNumberFieldRootContext');
12
17
 
13
- class PressedHoldService {
14
- constructor() {
15
- this.destroyRef = inject(DestroyRef);
16
- }
17
- create(options) {
18
- const timeout = signal(undefined, ...(ngDevMode ? [{ debugName: "timeout" }] : []));
19
- const triggerHook = new Subject();
20
- const isPressed = signal(false, ...(ngDevMode ? [{ debugName: "isPressed" }] : []));
21
- const resetTimeout = () => {
22
- const timer = timeout();
23
- if (timer !== undefined) {
24
- window.clearTimeout(timer);
25
- timeout.set(undefined);
26
- }
27
- };
28
- const onIncrementPressStart = (delay) => {
29
- resetTimeout();
30
- if (options.disabled())
31
- return;
32
- triggerHook.next();
33
- timeout.set(window.setTimeout(() => {
34
- onIncrementPressStart(60);
35
- }, delay));
36
- };
37
- const onPressStart = (event) => {
38
- if (event.button !== 0 || isPressed())
39
- return;
40
- event.preventDefault();
41
- isPressed.set(true);
42
- onIncrementPressStart(400);
43
- };
44
- const onPressRelease = () => {
45
- isPressed.set(false);
46
- resetTimeout();
47
- };
48
- effect(() => {
49
- // Skip SSR environments
50
- if (typeof window === 'undefined')
51
- return;
52
- const targetElement = options.target?.() || window;
53
- const destroy$ = new Subject();
54
- const pointerDownSub = fromEvent(targetElement, 'pointerdown')
55
- .pipe(takeUntil(destroy$))
56
- .subscribe((e) => onPressStart(e));
57
- const pointerUpSub = fromEvent(window, 'pointerup').pipe(takeUntil(destroy$)).subscribe(onPressRelease);
58
- const pointerCancelSub = fromEvent(window, 'pointercancel')
59
- .pipe(takeUntil(destroy$))
60
- .subscribe(onPressRelease);
61
- this.destroyRef.onDestroy(() => {
62
- destroy$.next();
63
- destroy$.complete();
64
- pointerDownSub.unsubscribe();
65
- pointerUpSub.unsubscribe();
66
- pointerCancelSub.unsubscribe();
67
- });
68
- });
69
- return {
70
- isPressed: isPressed.asReadonly(),
71
- onTrigger: (fn) => {
72
- const sub = triggerHook.subscribe(fn);
73
- this.destroyRef.onDestroy(() => sub.unsubscribe());
74
- }
75
- };
18
+ /**
19
+ * Coerces an optional numeric input, returning `undefined` for nullish/empty/non-numeric values.
20
+ * Unlike `numberAttribute`, `numberOrUndefined(undefined)` is `undefined` (not `NaN`), so an unset
21
+ * `[min]`/`[max]` does not poison clamping.
22
+ */
23
+ function numberOrUndefined(value) {
24
+ if (value == null || value === '') {
25
+ return undefined;
76
26
  }
77
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.3", ngImport: i0, type: PressedHoldService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
78
- static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.3", ngImport: i0, type: PressedHoldService }); }
27
+ const parsed = numberAttribute(value);
28
+ return Number.isNaN(parsed) ? undefined : parsed;
79
29
  }
80
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.3", ngImport: i0, type: PressedHoldService, decorators: [{
81
- type: Injectable
82
- }] });
30
+ /** Default step used when no `step` is provided. */
31
+ const DEFAULT_STEP = 1;
32
+ /** Delay between auto-repeat ticks while holding an increment/decrement button. */
33
+ const CHANGE_VALUE_TICK_DELAY = 60;
34
+ /** Delay before auto-repeat starts while holding an increment/decrement button. */
35
+ const START_AUTO_CHANGE_DELAY = 400;
36
+ /** Pointer travel (px) that cancels a hold, treating the gesture as a scroll. */
37
+ const SCROLLING_POINTER_MOVE_DISTANCE = 8;
38
+ const STEP_EPSILON_FACTOR = 1e-10;
39
+ // Matches Intl.NumberFormat's decimal maximumFractionDigits default.
40
+ const DEFAULT_DIGITS = 3;
41
+ /** Reactive `Intl`-backed formatter built from the current locale and format options. */
83
42
  function useNumberFormatter(locale, options = signal({})) {
84
- return computed(() => new NumberFormatter(locale(), options()));
43
+ return computed(() => new NumberFormatter(locale(), options() ?? {}));
85
44
  }
45
+ /** Reactive `Intl`-backed parser built from the current locale and format options. */
86
46
  function useNumberParser(locale, options = signal({})) {
87
- return computed(() => new NumberParser(locale(), options()));
47
+ return computed(() => new NumberParser(locale(), options() ?? {}));
48
+ }
49
+ /** Whether the format options would cause `Intl.NumberFormat` to round the value. */
50
+ function hasNumberFormatRoundingOptions(format) {
51
+ const opts = format;
52
+ return !!(opts &&
53
+ (opts.maximumFractionDigits != null ||
54
+ opts.minimumFractionDigits != null ||
55
+ opts.maximumSignificantDigits != null ||
56
+ opts.minimumSignificantDigits != null ||
57
+ opts['roundingIncrement'] != null ||
58
+ opts['roundingMode'] != null ||
59
+ opts['roundingPriority'] != null));
60
+ }
61
+ /**
62
+ * Strips floating-point representation errors (e.g. `0.1 + 0.2`) using the format's
63
+ * rounding options when present, otherwise rounding to {@link DEFAULT_DIGITS} digits.
64
+ */
65
+ function removeFloatingPointErrors(value, locale, format) {
66
+ if (!Number.isFinite(value)) {
67
+ return value;
68
+ }
69
+ if (!hasNumberFormatRoundingOptions(format)) {
70
+ return Number(value.toFixed(DEFAULT_DIGITS));
71
+ }
72
+ const formatter = new NumberFormatter('en-US', {
73
+ ...format,
74
+ // These options alter only display decoration, not numeric rounding.
75
+ signDisplay: 'auto',
76
+ currencySign: 'standard',
77
+ notation: format.notation === 'compact' ? 'standard' : format.notation,
78
+ useGrouping: false
79
+ });
80
+ const roundedText = formatter.format(value);
81
+ const roundedValue = new NumberParser('en-US', format ?? {}).parse(roundedText);
82
+ if (Number.isNaN(roundedValue)) {
83
+ return value;
84
+ }
85
+ return formatter.format(roundedValue) === roundedText ? roundedValue : value;
86
+ }
87
+ function snapToStep(value, base, step, mode = 'directional') {
88
+ const stepSize = Math.abs(step);
89
+ const direction = Math.sign(step);
90
+ const tolerance = stepSize * STEP_EPSILON_FACTOR * direction;
91
+ const rawSteps = value - base + tolerance;
92
+ if (mode === 'nearest') {
93
+ return base + Math.round(rawSteps / step) * step;
94
+ }
95
+ const snappedSteps = direction > 0 ? Math.floor(rawSteps / stepSize) : Math.ceil(rawSteps / stepSize);
96
+ return base + snappedSteps * stepSize;
97
+ }
98
+ /**
99
+ * Snaps (optionally), clamps (optionally) and rounds an unvalidated value, mirroring
100
+ * Base UI's `toValidatedNumber`. Snapping happens before clamping so non-step-aligned
101
+ * boundaries stay reachable.
102
+ */
103
+ function toValidatedNumber(value, options) {
104
+ if (value === null) {
105
+ return value;
106
+ }
107
+ const { step, minWithDefault, maxWithDefault, minWithZeroDefault, format, locale, snapOnStep, small } = options;
108
+ let nextValue = value;
109
+ if (step != null && snapOnStep && step !== 0) {
110
+ const base = small || minWithDefault === Number.MIN_SAFE_INTEGER ? minWithZeroDefault : minWithDefault;
111
+ nextValue = snapToStep(nextValue, base, step, small ? 'nearest' : 'directional');
112
+ }
113
+ if (options.clamp) {
114
+ nextValue = clamp(nextValue, minWithDefault, maxWithDefault);
115
+ }
116
+ const roundedValue = removeFloatingPointErrors(nextValue, locale, format);
117
+ return options.clamp ? clamp(roundedValue, minWithDefault, maxWithDefault) : roundedValue;
88
118
  }
89
- function handleDecimalOperation(operator, value1, value2) {
90
- let result = operator === '+' ? value1 + value2 : value1 - value2;
91
- // Check if we have decimals
92
- if (value1 % 1 !== 0 || value2 % 1 !== 0) {
93
- const value1Decimal = value1.toString().split('.');
94
- const value2Decimal = value2.toString().split('.');
95
- const value1DecimalLength = (value1Decimal[1] && value1Decimal[1].length) || 0;
96
- const value2DecimalLength = (value2Decimal[1] && value2Decimal[1].length) || 0;
97
- const multiplier = 10 ** Math.max(value1DecimalLength, value2DecimalLength);
98
- // Transform the decimals to integers based on the precision
99
- value1 = Math.round(value1 * multiplier);
100
- value2 = Math.round(value2 * multiplier);
101
- // Perform the operation on integers values to make sure we don't get a fancy decimal value
102
- result = operator === '+' ? value1 + value2 : value1 - value2;
103
- // Transform the integer result back to decimal
104
- result /= multiplier;
105
- }
106
- return result;
119
+ /**
120
+ * Pointer press-and-hold with auto-repeat, modelled on Base UI's `usePressAndHold`.
121
+ * Must be called in an injection context — it registers cleanup via `DestroyRef`.
122
+ */
123
+ function createPressAndHold(options) {
124
+ const destroyRef = inject(DestroyRef);
125
+ const isPressing = signal(false, ...(ngDevMode ? [{ debugName: "isPressing" }] : /* istanbul ignore next */ []));
126
+ let startEvent = null;
127
+ let startX = 0;
128
+ let startY = 0;
129
+ let didTick = false;
130
+ let tickTimer;
131
+ const clearTick = () => {
132
+ if (tickTimer !== undefined) {
133
+ clearTimeout(tickTimer);
134
+ tickTimer = undefined;
135
+ }
136
+ };
137
+ const scheduleTick = (delay) => {
138
+ tickTimer = setTimeout(() => {
139
+ if (options.disabled() || !startEvent) {
140
+ stop(startEvent);
141
+ return;
142
+ }
143
+ didTick = true;
144
+ const shouldContinue = options.tick(startEvent);
145
+ if (shouldContinue === false) {
146
+ clearTick();
147
+ return;
148
+ }
149
+ scheduleTick(options.tickDelay);
150
+ }, delay);
151
+ };
152
+ const onPointerMove = (event) => {
153
+ // Treat a moving touch/pen gesture as a scroll and cancel the hold.
154
+ if (startEvent && startEvent.pointerType !== 'mouse') {
155
+ const dx = event.clientX - startX;
156
+ const dy = event.clientY - startY;
157
+ if (Math.hypot(dx, dy) > options.scrollDistance) {
158
+ stop(event);
159
+ }
160
+ }
161
+ };
162
+ const stop = (event) => {
163
+ if (!isPressing()) {
164
+ return;
165
+ }
166
+ clearTick();
167
+ window.removeEventListener('pointerup', stop, true);
168
+ window.removeEventListener('pointercancel', stop, true);
169
+ window.removeEventListener('pointermove', onPointerMove, true);
170
+ isPressing.set(false);
171
+ if (event) {
172
+ options.onStop?.(event);
173
+ }
174
+ };
175
+ destroyRef.onDestroy(() => stop(null));
176
+ return {
177
+ isPressing: isPressing.asReadonly(),
178
+ shouldSkipClick: () => didTick,
179
+ onPointerDown(event) {
180
+ if (options.disabled() || isPressing()) {
181
+ return;
182
+ }
183
+ startEvent = event;
184
+ startX = event.clientX;
185
+ startY = event.clientY;
186
+ didTick = false;
187
+ isPressing.set(true);
188
+ window.addEventListener('pointerup', stop, true);
189
+ window.addEventListener('pointercancel', stop, true);
190
+ window.addEventListener('pointermove', onPointerMove, true);
191
+ scheduleTick(options.startDelay);
192
+ }
193
+ };
107
194
  }
108
195
 
109
- class RdxNumberFieldDecrementDirective {
196
+ // Treat pen as touch-like to avoid forcing the software keyboard on stylus taps.
197
+ function isTouchLikePointerType(pointerType) {
198
+ return pointerType === 'touch' || pointerType === 'pen';
199
+ }
200
+ /**
201
+ * Shared behaviour for the increment and decrement stepper buttons: single-press,
202
+ * press-and-hold auto-repeat, and committing a dirty (typed-but-not-blurred) input
203
+ * value before stepping.
204
+ *
205
+ * @see https://base-ui.com/react/components/number-field
206
+ */
207
+ class RdxNumberFieldButton {
208
+ get direction() {
209
+ return this.isIncrement ? 1 : -1;
210
+ }
211
+ get pressReason() {
212
+ return this.isIncrement ? 'increment-press' : 'decrement-press';
213
+ }
110
214
  constructor() {
111
- this.elementRef = inject((ElementRef));
112
- this.pressedHold = inject(PressedHoldService);
113
215
  this.rootContext = injectNumberFieldRootContext();
114
- this.disabled = input(false, ...(ngDevMode ? [{ debugName: "disabled", transform: booleanAttribute }] : [{ transform: booleanAttribute }]));
115
- /**
116
- * @ignore
117
- */
118
- this.isDisabled = computed(() => this.rootContext.disabled() ||
119
- this.rootContext.readonly() ||
120
- this.disabled() ||
121
- this.rootContext.isDecreaseDisabled(), ...(ngDevMode ? [{ debugName: "isDisabled" }] : []));
122
216
  /**
123
- * @ignore
217
+ * When `true`, the button is disabled in addition to inheriting the root's disabled state.
218
+ * @default false
124
219
  */
125
- this.useHold = this.pressedHold.create({
126
- target: signal(this.elementRef.nativeElement),
127
- disabled: this.disabled
220
+ this.disabled = input(false, { ...(ngDevMode ? { debugName: "disabled" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
221
+ /** @ignore Disabled for display/focus purposes (own state, root disabled, or bound reached). */
222
+ this.buttonDisabled = computed(() => this.disabled() ||
223
+ this.rootContext.isDisabled() ||
224
+ (this.isIncrement ? this.rootContext.isIncrementDisabled() : this.rootContext.isDecrementDisabled()), ...(ngDevMode ? [{ debugName: "buttonDisabled" }] : /* istanbul ignore next */ []));
225
+ /** @ignore Disabled for interaction purposes (also blocked while read-only). */
226
+ this.interactionDisabled = computed(() => this.buttonDisabled() || this.rootContext.readonly(), ...(ngDevMode ? [{ debugName: "interactionDisabled" }] : /* istanbul ignore next */ []));
227
+ const root = this.rootContext;
228
+ this.press = createPressAndHold({
229
+ disabled: () => this.interactionDisabled(),
230
+ startDelay: START_AUTO_CHANGE_DELAY,
231
+ tickDelay: CHANGE_VALUE_TICK_DELAY,
232
+ scrollDistance: SCROLLING_POINTER_MOVE_DISTANCE,
233
+ tick: (event) => {
234
+ const amount = root.getStepAmount(event);
235
+ return root.incrementValue(amount, {
236
+ direction: this.direction,
237
+ event,
238
+ reason: this.pressReason
239
+ });
240
+ },
241
+ onStop: () => root.commitValue(root.lastChangedValue ?? root.currentValue())
128
242
  });
129
243
  }
130
- ngOnInit() {
131
- this.useHold.onTrigger(() => this.rootContext.handleDecrease());
244
+ onClick(event) {
245
+ if (event.defaultPrevented || this.interactionDisabled() || this.press.shouldSkipClick()) {
246
+ return;
247
+ }
248
+ this.commitDirtyValue(event);
249
+ const amount = this.rootContext.getStepAmount(event);
250
+ const prev = this.rootContext.currentValue();
251
+ this.rootContext.incrementValue(amount, {
252
+ direction: this.direction,
253
+ event,
254
+ reason: this.pressReason
255
+ });
256
+ const committed = this.rootContext.lastChangedValue ?? this.rootContext.currentValue();
257
+ if (committed !== prev) {
258
+ this.rootContext.commitValue(committed);
259
+ }
260
+ }
261
+ onPointerDown(event) {
262
+ const isMainButton = !event.button || event.button === 0;
263
+ if (event.defaultPrevented || this.rootContext.readonly() || !isMainButton || this.buttonDisabled()) {
264
+ return;
265
+ }
266
+ // Sync the dirty input value before starting the hold sequence.
267
+ this.commitDirtyValue(event);
268
+ if (!isTouchLikePointerType(event.pointerType)) {
269
+ // Focus the input so the user can continue with the keyboard.
270
+ this.rootContext.inputEl()?.focus();
271
+ }
272
+ this.press.onPointerDown(event);
273
+ }
274
+ /** Commits a typed-but-not-yet-blurred input value so stepping starts from it. */
275
+ commitDirtyValue(event) {
276
+ const root = this.rootContext;
277
+ root.allowInputSync = true;
278
+ const parsed = root.parseNumber(root.inputValue());
279
+ if (parsed !== null) {
280
+ root.setValue(parsed, this.pressReason, event, this.direction);
281
+ }
132
282
  }
133
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.3", ngImport: i0, type: RdxNumberFieldDecrementDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
134
- static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "20.3.3", type: RdxNumberFieldDecrementDirective, isStandalone: true, selector: "button[rdxNumberFieldDecrement]", inputs: { disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null } }, host: { attributes: { "tabindex": "-1", "type": "\"button\"" }, listeners: { "contextmenu": "$event.preventDefault()" }, properties: { "attr.aria-label": "\"Decrease\"", "attr.disabled": "isDisabled() ? \"\" : undefined", "attr.data-disabled": "isDisabled() ? \"\" : undefined", "attr.data-pressed": "useHold.isPressed() ? \"true\" : undefined", "style.user-select": "useHold.isPressed() ? \"none\" : null" } }, providers: [PressedHoldService], ngImport: i0 }); }
283
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxNumberFieldButton, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
284
+ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.2.9", type: RdxNumberFieldButton, isStandalone: true, inputs: { disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null } }, host: { attributes: { "type": "button", "tabindex": "-1" }, listeners: { "click": "onClick($event)", "pointerdown": "onPointerDown($event)", "contextmenu": "$event.preventDefault()" }, properties: { "attr.aria-controls": "rootContext.id()", "attr.aria-readonly": "rootContext.readonly() ? \"true\" : undefined", "attr.disabled": "buttonDisabled() ? \"\" : undefined", "attr.data-disabled": "buttonDisabled() ? \"\" : undefined", "attr.data-pressed": "press.isPressing() ? \"\" : undefined", "style.user-select": "\"none\"", "style.-webkit-user-select": "\"none\"" } }, ngImport: i0 }); }
135
285
  }
136
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.3", ngImport: i0, type: RdxNumberFieldDecrementDirective, decorators: [{
286
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxNumberFieldButton, decorators: [{
137
287
  type: Directive,
138
288
  args: [{
139
- selector: 'button[rdxNumberFieldDecrement]',
140
- providers: [PressedHoldService],
141
289
  host: {
290
+ type: 'button',
142
291
  tabindex: '-1',
143
- type: '"button"',
144
- '[attr.aria-label]': '"Decrease"',
145
- '[attr.disabled]': 'isDisabled() ? "" : undefined',
146
- '[attr.data-disabled]': 'isDisabled() ? "" : undefined',
147
- '[attr.data-pressed]': 'useHold.isPressed() ? "true" : undefined',
148
- '[style.user-select]': 'useHold.isPressed() ? "none" : null',
292
+ '[attr.aria-controls]': 'rootContext.id()',
293
+ '[attr.aria-readonly]': 'rootContext.readonly() ? "true" : undefined',
294
+ '[attr.disabled]': 'buttonDisabled() ? "" : undefined',
295
+ '[attr.data-disabled]': 'buttonDisabled() ? "" : undefined',
296
+ '[attr.data-pressed]': 'press.isPressing() ? "" : undefined',
297
+ '[style.user-select]': '"none"',
298
+ '[style.-webkit-user-select]': '"none"',
299
+ '(click)': 'onClick($event)',
300
+ '(pointerdown)': 'onPointerDown($event)',
149
301
  '(contextmenu)': '$event.preventDefault()'
150
302
  }
151
303
  }]
304
+ }], ctorParameters: () => [], propDecorators: { disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }] } });
305
+
306
+ /**
307
+ * A stepper button that decreases the field value when clicked or held.
308
+ *
309
+ * @see https://base-ui.com/react/components/number-field
310
+ */
311
+ class RdxNumberFieldDecrement extends RdxNumberFieldButton {
312
+ constructor() {
313
+ super(...arguments);
314
+ this.isIncrement = false;
315
+ }
316
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxNumberFieldDecrement, deps: null, target: i0.ɵɵFactoryTarget.Directive }); }
317
+ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.9", type: RdxNumberFieldDecrement, isStandalone: true, selector: "button[rdxNumberFieldDecrement]", host: { attributes: { "aria-label": "Decrease" } }, exportAs: ["rdxNumberFieldDecrement"], usesInheritance: true, ngImport: i0 }); }
318
+ }
319
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxNumberFieldDecrement, decorators: [{
320
+ type: Directive,
321
+ args: [{
322
+ selector: 'button[rdxNumberFieldDecrement]',
323
+ exportAs: 'rdxNumberFieldDecrement',
324
+ host: {
325
+ 'aria-label': 'Decrease'
326
+ }
327
+ }]
152
328
  }] });
153
329
 
154
- class RdxNumberFieldIncrementDirective {
330
+ /**
331
+ * Groups the input with the increment and decrement buttons.
332
+ *
333
+ * @see https://base-ui.com/react/components/number-field
334
+ */
335
+ class RdxNumberFieldGroup {
155
336
  constructor() {
156
- this.elementRef = inject((ElementRef));
157
- this.pressedHold = inject(PressedHoldService);
158
337
  this.rootContext = injectNumberFieldRootContext();
159
- this.disabled = input(false, ...(ngDevMode ? [{ debugName: "disabled", transform: booleanAttribute }] : [{ transform: booleanAttribute }]));
160
- /**
161
- * @ignore
162
- */
163
- this.isDisabled = computed(() => this.rootContext.disabled() ||
164
- this.rootContext.readonly() ||
165
- this.disabled() ||
166
- this.rootContext.isIncreaseDisabled(), ...(ngDevMode ? [{ debugName: "isDisabled" }] : []));
167
- /**
168
- * @ignore
169
- */
170
- this.useHold = this.pressedHold.create({
171
- target: signal(this.elementRef.nativeElement),
172
- disabled: this.disabled
173
- });
174
338
  }
175
- ngOnInit() {
176
- this.useHold.onTrigger(() => this.rootContext.handleIncrease());
339
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxNumberFieldGroup, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
340
+ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.9", type: RdxNumberFieldGroup, isStandalone: true, selector: "div[rdxNumberFieldGroup]", host: { attributes: { "role": "group" }, properties: { "attr.data-disabled": "rootContext.isDisabled() ? \"\" : undefined", "attr.data-readonly": "rootContext.readonly() ? \"\" : undefined", "attr.data-required": "rootContext.required() ? \"\" : undefined", "attr.data-scrubbing": "rootContext.isScrubbing() ? \"\" : undefined" } }, exportAs: ["rdxNumberFieldGroup"], ngImport: i0 }); }
341
+ }
342
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxNumberFieldGroup, decorators: [{
343
+ type: Directive,
344
+ args: [{
345
+ selector: 'div[rdxNumberFieldGroup]',
346
+ exportAs: 'rdxNumberFieldGroup',
347
+ host: {
348
+ role: 'group',
349
+ '[attr.data-disabled]': 'rootContext.isDisabled() ? "" : undefined',
350
+ '[attr.data-readonly]': 'rootContext.readonly() ? "" : undefined',
351
+ '[attr.data-required]': 'rootContext.required() ? "" : undefined',
352
+ '[attr.data-scrubbing]': 'rootContext.isScrubbing() ? "" : undefined'
353
+ }
354
+ }]
355
+ }] });
356
+
357
+ /**
358
+ * The hidden native `input[type=number]` that mirrors the field value for native form submission
359
+ * and browser constraint validation (min/max/step/required). Place it inside the root, alongside
360
+ * the visible `[rdxNumberFieldInput]`.
361
+ *
362
+ * @see https://base-ui.com/react/components/number-field
363
+ */
364
+ class RdxNumberFieldHiddenInput {
365
+ constructor() {
366
+ this.rootContext = injectNumberFieldRootContext();
367
+ }
368
+ /** Move focus to the visible input when the hidden one is focused (e.g. via form validation). */
369
+ onFocus() {
370
+ this.rootContext.inputEl()?.focus();
177
371
  }
178
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.3", ngImport: i0, type: RdxNumberFieldIncrementDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
179
- static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "20.3.3", type: RdxNumberFieldIncrementDirective, isStandalone: true, selector: "button[rdxNumberFieldIncrement]", inputs: { disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null } }, host: { attributes: { "tabindex": "-1", "type": "\"button\"" }, listeners: { "contextmenu": "$event.preventDefault()" }, properties: { "attr.aria-label": "\"Increase\"", "attr.disabled": "isDisabled() ? \"\" : undefined", "attr.data-disabled": "isDisabled() ? \"\" : undefined", "attr.data-pressed": "useHold.isPressed() ? \"true\" : undefined", "style.user-select": "useHold.isPressed() ? \"none\" : null" } }, providers: [PressedHoldService], ngImport: i0 }); }
372
+ /** Handle browser autofill, which writes directly to the hidden numeric input. */
373
+ onChange(event) {
374
+ const root = this.rootContext;
375
+ if (root.isDisabled() || root.readonly()) {
376
+ return;
377
+ }
378
+ const nextValue = event.target.valueAsNumber;
379
+ const parsedValue = Number.isNaN(nextValue) ? null : nextValue;
380
+ root.allowInputSync = true;
381
+ root.setValue(parsedValue, 'none', event);
382
+ root.commitValue(root.lastChangedValue ?? root.currentValue());
383
+ }
384
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxNumberFieldHiddenInput, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
385
+ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.9", type: RdxNumberFieldHiddenInput, isStandalone: true, selector: "input[rdxNumberFieldHiddenInput]", host: { attributes: { "type": "number", "tabindex": "-1", "aria-hidden": "true" }, listeners: { "focus": "onFocus()", "change": "onChange($event)" }, properties: { "attr.name": "rootContext.name()", "attr.form": "rootContext.form()", "value": "rootContext.currentValue() ?? \"\"", "attr.min": "rootContext.min()", "attr.max": "rootContext.max()", "attr.step": "rootContext.step()", "disabled": "rootContext.isDisabled()", "attr.required": "rootContext.required() ? \"\" : undefined" }, styleAttribute: "transform: translateX(-100%); position: absolute; overflow: hidden; pointer-events: none; opacity: 0; margin: 0;" }, exportAs: ["rdxNumberFieldHiddenInput"], ngImport: i0 }); }
180
386
  }
181
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.3", ngImport: i0, type: RdxNumberFieldIncrementDirective, decorators: [{
387
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxNumberFieldHiddenInput, decorators: [{
182
388
  type: Directive,
183
389
  args: [{
184
- selector: 'button[rdxNumberFieldIncrement]',
185
- providers: [PressedHoldService],
390
+ selector: 'input[rdxNumberFieldHiddenInput]',
391
+ exportAs: 'rdxNumberFieldHiddenInput',
186
392
  host: {
393
+ type: 'number',
187
394
  tabindex: '-1',
188
- type: '"button"',
189
- '[attr.aria-label]': '"Increase"',
190
- '[attr.disabled]': 'isDisabled() ? "" : undefined',
191
- '[attr.data-disabled]': 'isDisabled() ? "" : undefined',
192
- '[attr.data-pressed]': 'useHold.isPressed() ? "true" : undefined',
193
- '[style.user-select]': 'useHold.isPressed() ? "none" : null',
194
- '(contextmenu)': '$event.preventDefault()'
395
+ 'aria-hidden': 'true',
396
+ '[attr.name]': 'rootContext.name()',
397
+ '[attr.form]': 'rootContext.form()',
398
+ '[value]': 'rootContext.currentValue() ?? ""',
399
+ '[attr.min]': 'rootContext.min()',
400
+ '[attr.max]': 'rootContext.max()',
401
+ '[attr.step]': 'rootContext.step()',
402
+ '[disabled]': 'rootContext.isDisabled()',
403
+ '[attr.required]': 'rootContext.required() ? "" : undefined',
404
+ style: 'transform: translateX(-100%); position: absolute; overflow: hidden; pointer-events: none; opacity: 0; margin: 0;',
405
+ '(focus)': 'onFocus()',
406
+ '(change)': 'onChange($event)'
195
407
  }
196
408
  }]
197
409
  }] });
198
410
 
199
- class RdxNumberFieldInputDirective {
411
+ /**
412
+ * A stepper button that increases the field value when clicked or held.
413
+ *
414
+ * @see https://base-ui.com/react/components/number-field
415
+ */
416
+ class RdxNumberFieldIncrement extends RdxNumberFieldButton {
417
+ constructor() {
418
+ super(...arguments);
419
+ this.isIncrement = true;
420
+ }
421
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxNumberFieldIncrement, deps: null, target: i0.ɵɵFactoryTarget.Directive }); }
422
+ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.9", type: RdxNumberFieldIncrement, isStandalone: true, selector: "button[rdxNumberFieldIncrement]", host: { attributes: { "aria-label": "Increase" } }, exportAs: ["rdxNumberFieldIncrement"], usesInheritance: true, ngImport: i0 }); }
423
+ }
424
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxNumberFieldIncrement, decorators: [{
425
+ type: Directive,
426
+ args: [{
427
+ selector: 'button[rdxNumberFieldIncrement]',
428
+ exportAs: 'rdxNumberFieldIncrement',
429
+ host: {
430
+ 'aria-label': 'Increase'
431
+ }
432
+ }]
433
+ }] });
434
+
435
+ /**
436
+ * The native text input that displays the formatted value and accepts typed input.
437
+ *
438
+ * @see https://base-ui.com/react/components/number-field
439
+ */
440
+ class RdxNumberFieldInput {
200
441
  constructor() {
201
- this.elementRef = inject((ElementRef));
442
+ this.elementRef = inject(ElementRef);
202
443
  this.rootContext = injectNumberFieldRootContext();
203
- this.inputValue = signal(this.rootContext.textValue(), ...(ngDevMode ? [{ debugName: "inputValue" }] : []));
204
- effect(() => {
205
- this.inputValue.set(this.rootContext.textValue());
206
- this.rootContext.setInputValue(this.inputValue());
207
- });
444
+ /** Browsers place the caret at the start; we move it to the end on the first focus. */
445
+ this.hasTouchedInput = false;
446
+ this.rootContext.registerInput(this.elementRef.nativeElement);
208
447
  }
209
- ngOnInit() {
210
- this.rootContext.onInputElement(this.elementRef.nativeElement);
448
+ onFocus(event) {
449
+ const root = this.rootContext;
450
+ if (root.readonly() || root.isDisabled() || this.hasTouchedInput) {
451
+ return;
452
+ }
453
+ this.hasTouchedInput = true;
454
+ const target = event.target;
455
+ const length = target.value.length;
456
+ target.setSelectionRange(length, length);
211
457
  }
212
458
  onBeforeInput(event) {
459
+ // Only validate insertions; deletions and composition have `data === null`.
460
+ if (event.data == null) {
461
+ return;
462
+ }
213
463
  const target = event.target;
214
464
  const nextValue = target.value.slice(0, target.selectionStart ?? undefined) +
215
- (event.data ?? '') +
465
+ event.data +
216
466
  target.value.slice(target.selectionEnd ?? undefined);
217
- if (!this.rootContext.validate(nextValue)) {
467
+ if (!this.rootContext.isValidPartial(nextValue)) {
218
468
  event.preventDefault();
219
469
  }
220
470
  }
221
- onWheelEvent(event) {
222
- if (this.rootContext.disableWheelChange()) {
471
+ onInput(event) {
472
+ const root = this.rootContext;
473
+ const targetValue = event.target.value;
474
+ root.allowInputSync = false;
475
+ if (targetValue.trim() === '') {
476
+ root.setInputValue(targetValue);
477
+ root.setValue(null, 'input-clear', event);
223
478
  return;
224
479
  }
225
- // only handle when in focus
226
- if (event.target !== getActiveElement())
227
- return;
228
- // if on a trackpad, users can scroll in both X and Y at once, check the magnitude of the change
229
- // if it's mostly in the X direction, then just return, the user probably doesn't mean to inc/dec
230
- // this isn't perfect, events come in fast with small deltas and a part of the scroll may give a false indication
231
- // especially if the user is scrolling near 45deg
232
- if (Math.abs(event.deltaY) <= Math.abs(event.deltaX)) {
480
+ const parsedValue = root.parseNumber(targetValue);
481
+ root.setInputValue(targetValue);
482
+ if (parsedValue !== null) {
483
+ root.setValue(parsedValue, 'input-change', event);
484
+ }
485
+ }
486
+ onKeydown(event) {
487
+ const root = this.rootContext;
488
+ if (root.readonly() || root.isDisabled()) {
233
489
  return;
234
490
  }
235
- event.preventDefault();
236
- if (event.deltaY > 0) {
237
- this.rootContext.handleIncrease();
491
+ root.allowInputSync = true;
492
+ const key = event.key;
493
+ if (key === 'ArrowUp' || key === 'ArrowDown') {
494
+ event.preventDefault();
495
+ const parsed = root.parseNumber(root.inputValue());
496
+ const amount = root.getStepAmount(event);
497
+ root.incrementValue(amount, {
498
+ direction: key === 'ArrowUp' ? 1 : -1,
499
+ currentValue: parsed,
500
+ event,
501
+ reason: 'keyboard'
502
+ });
503
+ root.commitValue(root.lastChangedValue ?? root.currentValue());
238
504
  }
239
- else if (event.deltaY < 0) {
240
- this.rootContext.handleDecrease();
505
+ else if (key === 'Home' && root.min() != null) {
506
+ event.preventDefault();
507
+ root.setValue(root.min(), 'keyboard', event);
508
+ root.commitValue(root.lastChangedValue ?? root.currentValue());
509
+ }
510
+ else if (key === 'End' && root.max() != null) {
511
+ event.preventDefault();
512
+ root.setValue(root.max(), 'keyboard', event);
513
+ root.commitValue(root.lastChangedValue ?? root.currentValue());
241
514
  }
242
515
  }
243
- onKeydownPageUp(event) {
244
- event.preventDefault();
245
- this.rootContext.handleIncrease(10);
246
- }
247
- onKeydownPageDown(event) {
248
- event.preventDefault();
249
- this.rootContext.handleDecrease(10);
250
- }
251
- onKeydownHome(event) {
252
- event.preventDefault();
253
- this.rootContext.handleMinMaxValue('min');
254
- }
255
- onKeydownEnd(event) {
516
+ onPaste(event) {
517
+ const root = this.rootContext;
518
+ if (root.readonly() || root.isDisabled()) {
519
+ return;
520
+ }
521
+ const pastedData = event.clipboardData?.getData('text/plain') ?? '';
522
+ // Prevent the subsequent `input` event from also handling the paste.
256
523
  event.preventDefault();
257
- this.rootContext.handleMinMaxValue('max');
258
- }
259
- onInput(event) {
260
- const target = event.target;
261
- this.rootContext.applyInputValue(target.value);
262
- }
263
- onChange() {
264
- this.inputValue.set(this.rootContext.textValue());
265
- }
266
- onKeydownEnter(event) {
267
- const target = event.target;
268
- this.rootContext.applyInputValue(target.value);
524
+ const parsedValue = root.parseNumber(pastedData);
525
+ if (parsedValue !== null) {
526
+ root.allowInputSync = false;
527
+ root.setValue(parsedValue, 'input-paste', event);
528
+ root.setInputValue(pastedData);
529
+ }
269
530
  }
270
- onKeydownUp(event) {
271
- event.preventDefault();
272
- this.rootContext.handleIncrease();
531
+ onBlur(event) {
532
+ const root = this.rootContext;
533
+ if (root.readonly() || root.isDisabled()) {
534
+ return;
535
+ }
536
+ root.markAsTouched();
537
+ const hadManualInput = !root.allowInputSync;
538
+ const hadPendingCommit = root.hasPendingCommit;
539
+ root.allowInputSync = true;
540
+ const text = root.inputValue();
541
+ if (text.trim() === '') {
542
+ root.setValue(null, 'input-clear', event);
543
+ root.commitValue(null);
544
+ return;
545
+ }
546
+ const parsedValue = root.parseNumber(text);
547
+ if (parsedValue === null) {
548
+ return;
549
+ }
550
+ const value = root.currentValue();
551
+ const shouldUpdate = value !== parsedValue;
552
+ const shouldCommit = hadManualInput || shouldUpdate || hadPendingCommit;
553
+ let committedValue = parsedValue;
554
+ if (shouldUpdate) {
555
+ root.setValue(parsedValue, 'input-blur', event);
556
+ committedValue = root.lastChangedValue ?? parsedValue;
557
+ }
558
+ if (shouldCommit) {
559
+ root.commitValue(committedValue);
560
+ }
561
+ const canonicalText = root.formatNumber(committedValue);
562
+ if (root.inputValue() !== canonicalText) {
563
+ root.setInputValue(canonicalText);
564
+ }
273
565
  }
274
- onKeydownDown(event) {
566
+ onWheel(event) {
567
+ const root = this.rootContext;
568
+ if (!root.allowWheelScrub() || root.isDisabled() || root.readonly()) {
569
+ return;
570
+ }
571
+ // Allow pinch-zoom and only scrub while focused.
572
+ if (event.ctrlKey || getActiveElement() !== this.elementRef.nativeElement) {
573
+ return;
574
+ }
275
575
  event.preventDefault();
276
- this.rootContext.handleDecrease();
576
+ root.allowInputSync = true;
577
+ const amount = root.getStepAmount(event);
578
+ root.incrementValue(amount, {
579
+ direction: event.deltaY > 0 ? -1 : 1,
580
+ event,
581
+ reason: 'wheel'
582
+ });
277
583
  }
278
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.3", ngImport: i0, type: RdxNumberFieldInputDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
279
- static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "20.3.3", type: RdxNumberFieldInputDirective, isStandalone: true, selector: "input[rdxNumberFieldInput]", host: { attributes: { "role": "spinbutton", "type": "text", "tabindex": "0", "autocomplete": "off", "autocorrect": "off", "spellcheck": "false", "aria-roledescription": "Number field" }, listeners: { "change": "onChange()", "input": "onInput($event)", "blur": "onKeydownEnter($event)", "beforeinput": "onBeforeInput($event)", "keydown.enter": "onKeydownEnter($event)", "keydown.arrowUp": "onKeydownUp($event)", "keydown.arrowDown": "onKeydownDown($event)", "keydown.home": "onKeydownHome($event)", "keydown.end": "onKeydownEnd($event)", "keydown.pageUp": "onKeydownPageUp($event)", "keydown.pageDown": "onKeydownPageDown($event)", "wheel": "onWheelEvent($event)" }, properties: { "attr.aria-valuenow": "rootContext.value()", "attr.aria-valuemin": "rootContext.min()", "attr.aria-valuemax": "rootContext.max()", "attr.inputmode": "rootContext.inputMode()", "attr.disabled": "rootContext.disabled() ? \"\" : undefined", "attr.data-disabled": "rootContext.disabled() ? \"\" : undefined", "attr.readonly": "rootContext.readonly() ? \"\" : undefined", "attr.data-readonly": "rootContext.readonly() ? \"\" : undefined", "attr.value": "inputValue()" } }, ngImport: i0 }); }
584
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxNumberFieldInput, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
585
+ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.9", type: RdxNumberFieldInput, isStandalone: true, selector: "input[rdxNumberFieldInput]", host: { attributes: { "type": "text", "autocomplete": "off", "autocorrect": "off", "spellcheck": "false", "aria-roledescription": "Number field" }, listeners: { "focus": "onFocus($event)", "blur": "onBlur($event)", "input": "onInput($event)", "beforeinput": "onBeforeInput($event)", "keydown": "onKeydown($event)", "paste": "onPaste($event)", "wheel": "onWheel($event)" }, properties: { "id": "rootContext.id()", "value": "rootContext.inputValue()", "attr.inputmode": "rootContext.inputMode()", "attr.aria-invalid": "rootContext.required() && rootContext.currentValue() === null ? \"true\" : undefined", "disabled": "rootContext.isDisabled()", "attr.readonly": "rootContext.readonly() ? \"\" : undefined", "required": "rootContext.required()", "attr.data-disabled": "rootContext.isDisabled() ? \"\" : undefined", "attr.data-readonly": "rootContext.readonly() ? \"\" : undefined", "attr.data-required": "rootContext.required() ? \"\" : undefined" } }, exportAs: ["rdxNumberFieldInput"], ngImport: i0 }); }
280
586
  }
281
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.3", ngImport: i0, type: RdxNumberFieldInputDirective, decorators: [{
587
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxNumberFieldInput, decorators: [{
282
588
  type: Directive,
283
589
  args: [{
284
590
  selector: 'input[rdxNumberFieldInput]',
591
+ exportAs: 'rdxNumberFieldInput',
285
592
  host: {
286
- role: 'spinbutton',
287
593
  type: 'text',
288
- tabindex: '0',
289
594
  autocomplete: 'off',
290
595
  autocorrect: 'off',
291
596
  spellcheck: 'false',
292
597
  'aria-roledescription': 'Number field',
293
- '[attr.aria-valuenow]': 'rootContext.value()',
294
- '[attr.aria-valuemin]': 'rootContext.min()',
295
- '[attr.aria-valuemax]': 'rootContext.max()',
598
+ '[id]': 'rootContext.id()',
599
+ '[value]': 'rootContext.inputValue()',
296
600
  '[attr.inputmode]': 'rootContext.inputMode()',
297
- '[attr.disabled]': 'rootContext.disabled() ? "" : undefined',
298
- '[attr.data-disabled]': 'rootContext.disabled() ? "" : undefined',
601
+ '[attr.aria-invalid]': 'rootContext.required() && rootContext.currentValue() === null ? "true" : undefined',
602
+ '[disabled]': 'rootContext.isDisabled()',
299
603
  '[attr.readonly]': 'rootContext.readonly() ? "" : undefined',
604
+ '[required]': 'rootContext.required()',
605
+ '[attr.data-disabled]': 'rootContext.isDisabled() ? "" : undefined',
300
606
  '[attr.data-readonly]': 'rootContext.readonly() ? "" : undefined',
301
- '[attr.value]': 'inputValue()',
302
- '(change)': 'onChange()',
607
+ '[attr.data-required]': 'rootContext.required() ? "" : undefined',
608
+ '(focus)': 'onFocus($event)',
609
+ '(blur)': 'onBlur($event)',
303
610
  '(input)': 'onInput($event)',
304
- '(blur)': 'onKeydownEnter($event)',
305
611
  '(beforeinput)': 'onBeforeInput($event)',
306
- '(keydown.enter)': 'onKeydownEnter($event)',
307
- '(keydown.arrowUp)': 'onKeydownUp($event)',
308
- '(keydown.arrowDown)': 'onKeydownDown($event)',
309
- '(keydown.home)': 'onKeydownHome($event)',
310
- '(keydown.end)': 'onKeydownEnd($event)',
311
- '(keydown.pageUp)': 'onKeydownPageUp($event)',
312
- '(keydown.pageDown)': 'onKeydownPageDown($event)',
313
- '(wheel)': 'onWheelEvent($event)'
612
+ '(keydown)': 'onKeydown($event)',
613
+ '(paste)': 'onPaste($event)',
614
+ '(wheel)': 'onWheel($event)'
314
615
  }
315
616
  }]
316
617
  }], ctorParameters: () => [] });
317
618
 
318
- class RdxNumberFieldRootDirective {
619
+ const REASONS = {
620
+ inputChange: 'input-change',
621
+ inputClear: 'input-clear',
622
+ inputBlur: 'input-blur',
623
+ inputPaste: 'input-paste',
624
+ keyboard: 'keyboard',
625
+ incrementPress: 'increment-press',
626
+ decrementPress: 'decrement-press',
627
+ wheel: 'wheel',
628
+ scrub: 'scrub',
629
+ none: 'none'
630
+ };
631
+
632
+ const INPUT_REASONS = [
633
+ REASONS.inputChange,
634
+ REASONS.inputClear,
635
+ REASONS.inputBlur,
636
+ REASONS.inputPaste,
637
+ REASONS.none
638
+ ];
639
+ /**
640
+ * Groups all parts of the number field and manages its state, parsing/formatting
641
+ * and value-change logic. A single directive drives the whole family — parts read
642
+ * signals and call methods off it through the root context.
643
+ *
644
+ * @see https://base-ui.com/react/components/number-field
645
+ */
646
+ class RdxNumberFieldRoot {
319
647
  constructor() {
320
- this.value = model(...(ngDevMode ? [undefined, { debugName: "value" }] : []));
321
- this.min = input(undefined, ...(ngDevMode ? [{ debugName: "min", transform: numberAttribute }] : [{ transform: numberAttribute }]));
322
- this.max = input(undefined, ...(ngDevMode ? [{ debugName: "max", transform: numberAttribute }] : [{ transform: numberAttribute }]));
323
- this.step = input(1, ...(ngDevMode ? [{ debugName: "step", transform: numberAttribute }] : [{ transform: numberAttribute }]));
324
- this.stepSnapping = input(false, ...(ngDevMode ? [{ debugName: "stepSnapping", transform: booleanAttribute }] : [{ transform: booleanAttribute }]));
325
- this.disableWheelChange = input(false, ...(ngDevMode ? [{ debugName: "disableWheelChange", transform: booleanAttribute }] : [{ transform: booleanAttribute }]));
326
- this.locale = input('en', ...(ngDevMode ? [{ debugName: "locale" }] : []));
327
- this.disabled = input(false, ...(ngDevMode ? [{ debugName: "disabled", transform: booleanAttribute }] : [{ transform: booleanAttribute }]));
328
- this.formatOptions = input(...(ngDevMode ? [undefined, { debugName: "formatOptions" }] : []));
648
+ /** @ignore */
649
+ this.cva = injectControlValueAccessor();
650
+ /** The id of the input element. */
651
+ this.id = input(injectId('rdx-number-field-'), ...(ngDevMode ? [{ debugName: "id" }] : /* istanbul ignore next */ []));
652
+ /** The minimum value of the field. */
653
+ this.min = input(undefined, { ...(ngDevMode ? { debugName: "min" } : /* istanbul ignore next */ {}), transform: numberOrUndefined });
654
+ /** The maximum value of the field. */
655
+ this.max = input(undefined, { ...(ngDevMode ? { debugName: "max" } : /* istanbul ignore next */ {}), transform: numberOrUndefined });
329
656
  /**
330
- * When <code>true</code>, the Number Field is read-only.
657
+ * Amount to increment and decrement with the buttons, arrow keys and scrub area.
658
+ * @default 1
331
659
  */
332
- this.readonly = input(false, ...(ngDevMode ? [{ debugName: "readonly", transform: booleanAttribute }] : [{ transform: booleanAttribute }]));
660
+ this.step = input(1, { ...(ngDevMode ? { debugName: "step" } : /* istanbul ignore next */ {}), transform: numberAttribute });
333
661
  /**
334
- * @ignore
662
+ * The step used when incrementing while the Alt key is held. Snaps to multiples of this value.
663
+ * @default 0.1
335
664
  */
336
- this.inputEl = signal(undefined, ...(ngDevMode ? [{ debugName: "inputEl" }] : []));
665
+ this.smallStep = input(0.1, { ...(ngDevMode ? { debugName: "smallStep" } : /* istanbul ignore next */ {}), transform: numberAttribute });
337
666
  /**
338
- * @ignore
667
+ * The step used when incrementing while the Shift key is held. Snaps to multiples of this value.
668
+ * @default 10
339
669
  */
340
- this.isDecreaseDisabled = computed(() => !isNullish(this.value()) &&
341
- (this.clampInputValue(this.value()) === this.min() || (this.min() && !isNaN(this.value()))
342
- ? handleDecimalOperation('-', this.value(), this.step()) < this.min()
343
- : false), ...(ngDevMode ? [{ debugName: "isDecreaseDisabled" }] : []));
670
+ this.largeStep = input(10, { ...(ngDevMode ? { debugName: "largeStep" } : /* istanbul ignore next */ {}), transform: numberAttribute });
344
671
  /**
345
- * @ignore
672
+ * Whether the value should snap to the nearest step when incrementing or decrementing.
673
+ * @default false
346
674
  */
347
- this.isIncreaseDisabled = computed(() => !isNullish(this.value()) &&
348
- (this.clampInputValue(this.value()) === this.max() || (this.min() && !isNaN(this.value()))
349
- ? handleDecimalOperation('+', this.value(), this.step()) > this.max()
350
- : false), ...(ngDevMode ? [{ debugName: "isIncreaseDisabled" }] : []));
675
+ this.snapOnStep = input(false, { ...(ngDevMode ? { debugName: "snapOnStep" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
351
676
  /**
352
- * @ignore
677
+ * When `true`, direct text entry may go outside the `min`/`max` range without clamping.
678
+ * Step interactions (arrow keys, buttons, wheel, scrub) still clamp.
679
+ * @default false
353
680
  */
354
- this.inputMode = computed(() => {
355
- // The inputMode attribute influences the software keyboard that is shown on touch devices.
356
- // Browsers and operating systems are quite inconsistent about what keys are available, however.
357
- // We choose between numeric and decimal based on whether we allow negative and fractional numbers,
358
- // and based on testing on various devices to determine what keys are available in each inputMode.
359
- const hasDecimals = this.numberFormatter().resolvedOptions().maximumFractionDigits > 0;
360
- return hasDecimals ? 'decimal' : 'numeric';
361
- }, ...(ngDevMode ? [{ debugName: "inputMode" }] : []));
681
+ this.allowOutOfRange = input(false, { ...(ngDevMode ? { debugName: "allowOutOfRange" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
682
+ /**
683
+ * Whether the value can be changed with the mouse wheel while the input is focused.
684
+ * @default false
685
+ */
686
+ this.allowWheelScrub = input(false, { ...(ngDevMode ? { debugName: "allowWheelScrub" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
687
+ /** Options used to format the input value (forwarded to `Intl.NumberFormat`). */
688
+ this.format = input(...(ngDevMode ? [undefined, { debugName: "format" }] : /* istanbul ignore next */ []));
689
+ /** The locale used to parse and format the value. */
690
+ this.locale = input('en', ...(ngDevMode ? [{ debugName: "locale" }] : /* istanbul ignore next */ []));
691
+ /**
692
+ * When `true`, the user cannot interact with the field.
693
+ * @default false
694
+ */
695
+ this.disabled = input(false, { ...(ngDevMode ? { debugName: "disabled" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
696
+ /**
697
+ * When `true`, the field is focusable but its value cannot be changed.
698
+ * @default false
699
+ */
700
+ this.readonly = input(false, { ...(ngDevMode ? { debugName: "readonly" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
701
+ /**
702
+ * When `true`, the user must enter a value before the owning form can be submitted.
703
+ * @default false
704
+ */
705
+ this.required = input(false, { ...(ngDevMode ? { debugName: "required" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
706
+ /** Name of the hidden input rendered by `[rdxNumberFieldHiddenInput]`, for form submission. */
707
+ this.name = input(...(ngDevMode ? [undefined, { debugName: "name" }] : /* istanbul ignore next */ []));
708
+ /** Id of the form the hidden input belongs to. Useful when it is rendered outside the form. */
709
+ this.form = input(...(ngDevMode ? [undefined, { debugName: "form" }] : /* istanbul ignore next */ []));
710
+ /** The uncontrolled value of the field when it is initially rendered. */
711
+ this.defaultValue = input(...(ngDevMode ? [undefined, { debugName: "defaultValue" }] : /* istanbul ignore next */ []));
712
+ /** The controlled value of the field. Use with `(onValueChange)` or two-way `[(value)]`. */
713
+ this.value = model(null, ...(ngDevMode ? [{ debugName: "value" }] : /* istanbul ignore next */ []));
714
+ /** Emitted when the value changes (during interaction or programmatically). */
715
+ this.onValueChange = output();
362
716
  /**
363
- * Replace negative textValue formatted using currencySign: 'accounting'
364
- * with a textValue that can be announced using a minus sign.
365
- * @ignore
717
+ * Emitted when the value is committed: on blur after typing, or when a pointer is released
718
+ * after scrubbing or pressing a button. Fires together with `onValueChange` for keyboard input.
366
719
  */
367
- this.textValue = computed(() => isNullish(this.value()) || isNaN(this.value()) ? '' : this.textValueFormatter().format(this.value()), ...(ngDevMode ? [{ debugName: "textValue" }] : []));
720
+ this.onValueCommitted = output();
721
+ /** @ignore The formatted text shown in the input element. */
722
+ this.inputValue = signal('', ...(ngDevMode ? [{ debugName: "inputValue" }] : /* istanbul ignore next */ []));
723
+ /** @ignore Whether a scrub gesture is in progress. */
724
+ this.isScrubbing = signal(false, ...(ngDevMode ? [{ debugName: "isScrubbing" }] : /* istanbul ignore next */ []));
725
+ /** @ignore The native input element, registered by `[rdxNumberFieldInput]`. */
726
+ this.inputEl = signal(undefined, ...(ngDevMode ? [{ debugName: "inputEl" }] : /* istanbul ignore next */ []));
368
727
  /**
369
- * @ignore
728
+ * @ignore Gate that prevents the formatted value from overwriting in-progress typing.
729
+ * Plain field (not a signal): it is toggled imperatively inside event handlers.
370
730
  */
371
- this.onInputElement = (el) => this.inputEl.set(el);
731
+ this.allowInputSync = true;
732
+ /** @ignore Last value produced by `setValue`, used to report the committed value. */
733
+ this.lastChangedValue = null;
734
+ /** @ignore Whether a programmatic change is awaiting a commit. */
735
+ this.hasPendingCommit = false;
736
+ /** @ignore */
737
+ this.isDisabled = computed(() => !!this.cva.disabled(), ...(ngDevMode ? [{ debugName: "isDisabled" }] : /* istanbul ignore next */ []));
738
+ this.formatter = useNumberFormatter(this.locale, this.format);
739
+ this.parser = useNumberParser(this.locale, this.format);
740
+ /** @ignore The current numeric value (`null` when empty). */
741
+ this.currentValue = computed(() => this.cva.value() ?? null, ...(ngDevMode ? [{ debugName: "currentValue" }] : /* istanbul ignore next */ []));
742
+ /** @ignore */
743
+ this.minWithDefault = computed(() => this.min() ?? Number.MIN_SAFE_INTEGER, ...(ngDevMode ? [{ debugName: "minWithDefault" }] : /* istanbul ignore next */ []));
744
+ /** @ignore */
745
+ this.maxWithDefault = computed(() => this.max() ?? Number.MAX_SAFE_INTEGER, ...(ngDevMode ? [{ debugName: "maxWithDefault" }] : /* istanbul ignore next */ []));
746
+ /** @ignore */
747
+ this.minWithZeroDefault = computed(() => this.min() ?? 0, ...(ngDevMode ? [{ debugName: "minWithZeroDefault" }] : /* istanbul ignore next */ []));
748
+ /** @ignore Whether incrementing further is a no-op (value already at max). */
749
+ this.isIncrementDisabled = computed(() => {
750
+ const value = this.currentValue();
751
+ return value != null && value >= this.maxWithDefault();
752
+ }, ...(ngDevMode ? [{ debugName: "isIncrementDisabled" }] : /* istanbul ignore next */ []));
753
+ /** @ignore Whether decrementing further is a no-op (value already at min). */
754
+ this.isDecrementDisabled = computed(() => {
755
+ const value = this.currentValue();
756
+ return value != null && value <= this.minWithDefault();
757
+ }, ...(ngDevMode ? [{ debugName: "isDecrementDisabled" }] : /* istanbul ignore next */ []));
758
+ /** @ignore Software-keyboard hint based on whether the format allows fractional digits. */
759
+ this.inputMode = computed(() => {
760
+ const hasDecimals = (this.formatter().resolvedOptions().maximumFractionDigits ?? 0) > 0;
761
+ return hasDecimals ? 'decimal' : 'numeric';
762
+ }, ...(ngDevMode ? [{ debugName: "inputMode" }] : /* istanbul ignore next */ []));
763
+ // Apply the uncontrolled initial value once it is provided.
764
+ effect(() => {
765
+ const dv = this.defaultValue();
766
+ if (dv !== undefined) {
767
+ untracked(() => {
768
+ this.value.set(dv);
769
+ this.cva.setValue(dv);
770
+ });
771
+ }
772
+ });
773
+ // Keep the visible input in sync with the value whenever it changes externally or the
774
+ // formatting options change. Skipped while the user is typing (`allowInputSync === false`).
775
+ effect(() => {
776
+ const formatted = this.formatNumber(this.currentValue());
777
+ untracked(() => {
778
+ if (this.allowInputSync && this.inputValue() !== formatted) {
779
+ this.inputValue.set(formatted);
780
+ }
781
+ });
782
+ });
372
783
  }
373
- /**
374
- * @ignore
375
- */
376
- clampInputValue(val) {
377
- // Clamp to min and max, round to the nearest step, and round to the specified number of digits
378
- let clampedValue;
379
- if (this.step() === undefined || isNaN(this.step()) || !this.stepSnapping()) {
380
- clampedValue = clamp(val, this.min(), this.max());
381
- }
382
- else {
383
- clampedValue = snapValueToStep(val, this.min(), this.max(), this.step());
784
+ /** @ignore Formats a numeric value to its display string (empty for `null`/`NaN`). */
785
+ formatNumber(value) {
786
+ if (value === null || Number.isNaN(value)) {
787
+ return '';
384
788
  }
385
- clampedValue = this.numberParser().parse(this.numberFormatter().format(clampedValue));
386
- return clampedValue;
789
+ return this.formatter().format(value);
387
790
  }
388
- ngOnInit() {
389
- this.numberParser = useNumberParser(this.locale, this.formatOptions);
390
- this.numberFormatter = useNumberFormatter(this.locale, this.formatOptions);
391
- this.textValueFormatter = useNumberFormatter(this.locale, this.formatOptions);
791
+ /** @ignore Parses a display string to a number, returning `null` when not parseable. */
792
+ parseNumber(text) {
793
+ const parsed = this.parser().parse(text);
794
+ return Number.isNaN(parsed) ? null : parsed;
392
795
  }
393
- /**
394
- * @ignore
395
- */
396
- handleMinMaxValue(type) {
397
- if (type === 'min' && this.min() !== undefined) {
398
- this.value.set(this.clampInputValue(this.min()));
796
+ /** @ignore Whether `text` is a valid partial number for the current locale and bounds. */
797
+ isValidPartial(text) {
798
+ return this.parser().isValidPartialNumber(text, this.min(), this.max());
799
+ }
800
+ /** @ignore The step magnitude for an interaction, honouring Alt (small) and Shift (large). */
801
+ getStepAmount(event) {
802
+ if (event?.altKey) {
803
+ return this.smallStep();
399
804
  }
400
- else if (type === 'max' && this.max() !== undefined) {
401
- this.value.set(this.clampInputValue(this.max()));
805
+ if (event?.shiftKey) {
806
+ return this.largeStep();
402
807
  }
808
+ return this.step();
403
809
  }
404
- /**
405
- * @ignore
406
- */
407
- handleDecrease(multiplier = 1) {
408
- this.handleChangingValue('decrease', multiplier);
810
+ /** @ignore Registers the native input element. */
811
+ registerInput(el) {
812
+ this.inputEl.set(el);
409
813
  }
410
- /**
411
- * @ignore
412
- */
413
- handleIncrease(multiplier = 1) {
414
- this.handleChangingValue('increase', multiplier);
814
+ /** @ignore Sets the displayed text without changing the numeric value. */
815
+ setInputValue(text) {
816
+ this.inputValue.set(text);
817
+ }
818
+ /** @ignore */
819
+ markAsTouched() {
820
+ this.cva.markAsTouched();
415
821
  }
416
822
  /**
417
823
  * @ignore
824
+ * Validates and applies a candidate value, emitting `onValueChange` when it changes.
825
+ * Returns whether a change was fired.
418
826
  */
419
- applyInputValue(val) {
420
- const parsedValue = this.numberParser().parse(val);
421
- this.value.set(isNaN(parsedValue) ? undefined : this.clampInputValue(parsedValue));
422
- // Set to empty state if input value is empty
423
- if (!val.length) {
424
- return this.setInputValue(val);
827
+ setValue(unvalidatedValue, reason, event, direction) {
828
+ const keyState = event;
829
+ // Step interactions always clamp; direct text entry may go out of range when allowed.
830
+ const shouldClamp = !this.allowOutOfRange() || !INPUT_REASONS.includes(reason);
831
+ const validatedValue = toValidatedNumber(unvalidatedValue, {
832
+ step: direction ? this.getStepAmount(keyState) * direction : undefined,
833
+ minWithDefault: this.minWithDefault(),
834
+ maxWithDefault: this.maxWithDefault(),
835
+ minWithZeroDefault: this.minWithZeroDefault(),
836
+ format: this.format(),
837
+ locale: this.locale(),
838
+ snapOnStep: this.snapOnStep(),
839
+ small: keyState?.altKey ?? false,
840
+ clamp: shouldClamp
841
+ });
842
+ const value = this.currentValue();
843
+ const isInputReason = INPUT_REASONS.includes(reason);
844
+ const shouldFireChange = validatedValue !== value ||
845
+ (isInputReason && (unvalidatedValue !== value || this.allowInputSync === false));
846
+ if (shouldFireChange) {
847
+ this.applyValue(validatedValue);
848
+ this.onValueChange.emit(validatedValue);
849
+ this.hasPendingCommit = true;
425
850
  }
426
- // if it failed to parse, then reset input to formatted version of current number
427
- if (isNaN(parsedValue)) {
428
- return this.setInputValue(this.textValue());
851
+ this.lastChangedValue = validatedValue;
852
+ if (this.allowInputSync) {
853
+ this.setInputValue(this.formatNumber(validatedValue));
429
854
  }
430
- return this.setInputValue(this.textValue());
855
+ return shouldFireChange;
431
856
  }
432
857
  /**
433
858
  * @ignore
859
+ * Increments (or decrements) the value by `amount * direction`, starting from `currentValue`
860
+ * when supplied (the live, possibly-dirty input value) or the committed value otherwise.
434
861
  */
435
- setInputValue(val) {
436
- if (this.inputEl()) {
437
- this.inputEl.update((el) => {
438
- if (el)
439
- el.value = val;
440
- return el;
441
- });
442
- }
862
+ incrementValue(amount, params) {
863
+ const prev = params.currentValue == null ? this.currentValue() : params.currentValue;
864
+ const nextValue = typeof prev === 'number' ? prev + amount * params.direction : Math.max(0, this.min() ?? 0);
865
+ return this.setValue(nextValue, params.reason, params.event, params.direction);
443
866
  }
444
- /**
445
- * @ignore
446
- */
447
- validate(val) {
448
- return this.numberParser().isValidPartialNumber(val, this.min(), this.max());
867
+ /** @ignore Emits the committed value at the end of an interaction. */
868
+ commitValue(value) {
869
+ this.hasPendingCommit = false;
870
+ this.onValueCommitted.emit(value);
871
+ }
872
+ applyValue(value) {
873
+ this.value.set(value);
874
+ this.cva.setValue(value);
449
875
  }
450
- handleChangingValue(type, multiplier = 1) {
451
- this.inputEl()?.focus();
452
- if (this.disabled() || this.readonly()) {
876
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxNumberFieldRoot, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
877
+ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.2.9", type: RdxNumberFieldRoot, isStandalone: true, selector: "div[rdxNumberFieldRoot]", inputs: { id: { classPropertyName: "id", publicName: "id", isSignal: true, isRequired: false, transformFunction: null }, min: { classPropertyName: "min", publicName: "min", isSignal: true, isRequired: false, transformFunction: null }, max: { classPropertyName: "max", publicName: "max", isSignal: true, isRequired: false, transformFunction: null }, step: { classPropertyName: "step", publicName: "step", isSignal: true, isRequired: false, transformFunction: null }, smallStep: { classPropertyName: "smallStep", publicName: "smallStep", isSignal: true, isRequired: false, transformFunction: null }, largeStep: { classPropertyName: "largeStep", publicName: "largeStep", isSignal: true, isRequired: false, transformFunction: null }, snapOnStep: { classPropertyName: "snapOnStep", publicName: "snapOnStep", isSignal: true, isRequired: false, transformFunction: null }, allowOutOfRange: { classPropertyName: "allowOutOfRange", publicName: "allowOutOfRange", isSignal: true, isRequired: false, transformFunction: null }, allowWheelScrub: { classPropertyName: "allowWheelScrub", publicName: "allowWheelScrub", isSignal: true, isRequired: false, transformFunction: null }, format: { classPropertyName: "format", publicName: "format", isSignal: true, isRequired: false, transformFunction: null }, locale: { classPropertyName: "locale", publicName: "locale", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, readonly: { classPropertyName: "readonly", publicName: "readonly", isSignal: true, isRequired: false, transformFunction: null }, required: { classPropertyName: "required", publicName: "required", isSignal: true, isRequired: false, transformFunction: null }, name: { classPropertyName: "name", publicName: "name", isSignal: true, isRequired: false, transformFunction: null }, form: { classPropertyName: "form", publicName: "form", isSignal: true, isRequired: false, transformFunction: null }, defaultValue: { classPropertyName: "defaultValue", publicName: "defaultValue", isSignal: true, isRequired: false, transformFunction: null }, value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { value: "valueChange", onValueChange: "onValueChange", onValueCommitted: "onValueCommitted" }, host: { attributes: { "role": "group" }, properties: { "attr.data-disabled": "isDisabled() ? \"\" : undefined", "attr.data-readonly": "readonly() ? \"\" : undefined", "attr.data-required": "required() ? \"\" : undefined", "attr.data-scrubbing": "isScrubbing() ? \"\" : undefined" } }, providers: [provideNumberFieldRootContext(() => inject(RdxNumberFieldRoot))], exportAs: ["rdxNumberFieldRoot"], hostDirectives: [{ directive: i1.RdxControlValueAccessor, inputs: ["value", "value", "disabled", "disabled"] }], ngImport: i0 }); }
878
+ }
879
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxNumberFieldRoot, decorators: [{
880
+ type: Directive,
881
+ args: [{
882
+ selector: 'div[rdxNumberFieldRoot]',
883
+ exportAs: 'rdxNumberFieldRoot',
884
+ providers: [provideNumberFieldRootContext(() => inject(RdxNumberFieldRoot))],
885
+ hostDirectives: [
886
+ {
887
+ directive: RdxControlValueAccessor,
888
+ inputs: ['value: value', 'disabled']
889
+ }
890
+ ],
891
+ host: {
892
+ role: 'group',
893
+ '[attr.data-disabled]': 'isDisabled() ? "" : undefined',
894
+ '[attr.data-readonly]': 'readonly() ? "" : undefined',
895
+ '[attr.data-required]': 'required() ? "" : undefined',
896
+ '[attr.data-scrubbing]': 'isScrubbing() ? "" : undefined'
897
+ }
898
+ }]
899
+ }], ctorParameters: () => [], propDecorators: { id: [{ type: i0.Input, args: [{ isSignal: true, alias: "id", required: false }] }], min: [{ type: i0.Input, args: [{ isSignal: true, alias: "min", required: false }] }], max: [{ type: i0.Input, args: [{ isSignal: true, alias: "max", required: false }] }], step: [{ type: i0.Input, args: [{ isSignal: true, alias: "step", required: false }] }], smallStep: [{ type: i0.Input, args: [{ isSignal: true, alias: "smallStep", required: false }] }], largeStep: [{ type: i0.Input, args: [{ isSignal: true, alias: "largeStep", required: false }] }], snapOnStep: [{ type: i0.Input, args: [{ isSignal: true, alias: "snapOnStep", required: false }] }], allowOutOfRange: [{ type: i0.Input, args: [{ isSignal: true, alias: "allowOutOfRange", required: false }] }], allowWheelScrub: [{ type: i0.Input, args: [{ isSignal: true, alias: "allowWheelScrub", required: false }] }], format: [{ type: i0.Input, args: [{ isSignal: true, alias: "format", required: false }] }], locale: [{ type: i0.Input, args: [{ isSignal: true, alias: "locale", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], readonly: [{ type: i0.Input, args: [{ isSignal: true, alias: "readonly", required: false }] }], required: [{ type: i0.Input, args: [{ isSignal: true, alias: "required", required: false }] }], name: [{ type: i0.Input, args: [{ isSignal: true, alias: "name", required: false }] }], form: [{ type: i0.Input, args: [{ isSignal: true, alias: "form", required: false }] }], defaultValue: [{ type: i0.Input, args: [{ isSignal: true, alias: "defaultValue", required: false }] }], value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }, { type: i0.Output, args: ["valueChange"] }], onValueChange: [{ type: i0.Output, args: ["onValueChange"] }], onValueCommitted: [{ type: i0.Output, args: ["onValueCommitted"] }] } });
900
+
901
+ /**
902
+ * Context shared from the scrub area to its virtual cursor child.
903
+ *
904
+ * @see https://base-ui.com/react/components/number-field
905
+ */
906
+ const [injectNumberFieldScrubAreaContext, provideNumberFieldScrubAreaContext] = createContext('RdxNumberFieldScrubAreaContext');
907
+
908
+ function isWebKitBrowser$1() {
909
+ return (typeof navigator !== 'undefined' &&
910
+ /AppleWebKit/.test(navigator.userAgent) &&
911
+ !/Chrome/.test(navigator.userAgent));
912
+ }
913
+ function isFirefoxBrowser() {
914
+ return typeof navigator !== 'undefined' && /firefox/i.test(navigator.userAgent);
915
+ }
916
+ /** Calculates the viewport rect the virtual cursor loops within. */
917
+ function getViewportRect(teleportDistance, scrubAreaEl) {
918
+ const win = scrubAreaEl.ownerDocument.defaultView ?? window;
919
+ const rect = scrubAreaEl.getBoundingClientRect();
920
+ if (rect && teleportDistance != null) {
921
+ return {
922
+ x: rect.left - teleportDistance / 2,
923
+ y: rect.top - teleportDistance / 2,
924
+ width: rect.right + teleportDistance / 2,
925
+ height: rect.bottom + teleportDistance / 2
926
+ };
927
+ }
928
+ const vV = win.visualViewport;
929
+ if (vV) {
930
+ return { x: vV.offsetLeft, y: vV.offsetTop, width: vV.offsetLeft + vV.width, height: vV.offsetTop + vV.height };
931
+ }
932
+ return {
933
+ x: 0,
934
+ y: 0,
935
+ width: win.document.documentElement.clientWidth,
936
+ height: win.document.documentElement.clientHeight
937
+ };
938
+ }
939
+ /**
940
+ * An interactive area where the user can click and drag to change the field value.
941
+ * Uses the Pointer Lock API for continuous dragging (disabled in Safari to avoid layout shift).
942
+ *
943
+ * @see https://base-ui.com/react/components/number-field
944
+ */
945
+ class RdxNumberFieldScrubArea {
946
+ constructor() {
947
+ this.rootContext = injectNumberFieldRootContext();
948
+ /**
949
+ * The direction the cursor must move to change the value.
950
+ * @default 'horizontal'
951
+ */
952
+ this.direction = input('horizontal', ...(ngDevMode ? [{ debugName: "direction" }] : /* istanbul ignore next */ []));
953
+ /**
954
+ * How many pixels the cursor must move before the value changes. Higher is less sensitive.
955
+ * @default 2
956
+ */
957
+ this.pixelSensitivity = input(2, { ...(ngDevMode ? { debugName: "pixelSensitivity" } : /* istanbul ignore next */ {}), transform: numberAttribute });
958
+ /**
959
+ * If set, the distance the cursor may move from the scrub area center before it loops back.
960
+ */
961
+ this.teleportDistance = input(undefined, { ...(ngDevMode ? { debugName: "teleportDistance" } : /* istanbul ignore next */ {}), transform: numberOrUndefined });
962
+ this.isScrubbing = signal(false, ...(ngDevMode ? [{ debugName: "isScrubbing" }] : /* istanbul ignore next */ []));
963
+ this.isTouchInput = signal(false, ...(ngDevMode ? [{ debugName: "isTouchInput" }] : /* istanbul ignore next */ []));
964
+ this.isPointerLockDenied = signal(false, ...(ngDevMode ? [{ debugName: "isPointerLockDenied" }] : /* istanbul ignore next */ []));
965
+ this.cursorEl = signal(null, ...(ngDevMode ? [{ debugName: "cursorEl" }] : /* istanbul ignore next */ []));
966
+ this.scrubAreaEl = inject(ElementRef).nativeElement;
967
+ this.isScrubbingRef = false;
968
+ this.didMove = false;
969
+ this.pointerDownTarget = null;
970
+ this.virtualCursorCoords = { x: 0, y: 0 };
971
+ this.visualScale = 1;
972
+ /** @ignore Exposed to the scrub-area context provider. */
973
+ this.context = {
974
+ isScrubbing: this.isScrubbing.asReadonly(),
975
+ isTouchInput: this.isTouchInput.asReadonly(),
976
+ isPointerLockDenied: this.isPointerLockDenied.asReadonly(),
977
+ registerCursor: (el) => this.cursorEl.set(el)
978
+ };
979
+ this.canScrub = computed(() => !this.rootContext.isDisabled() && !this.rootContext.readonly(), ...(ngDevMode ? [{ debugName: "canScrub" }] : /* istanbul ignore next */ []));
980
+ const root = this.rootContext;
981
+ // Register global listeners only while actively scrubbing.
982
+ effect((onCleanup) => {
983
+ if (!root.inputEl() || !this.canScrub() || !this.isScrubbing()) {
984
+ return;
985
+ }
986
+ let cumulativeDelta = 0;
987
+ const handlePointerMove = (event) => {
988
+ if (!this.isScrubbingRef) {
989
+ return;
990
+ }
991
+ event.preventDefault();
992
+ this.onScrub(event);
993
+ const { movementX, movementY } = event;
994
+ cumulativeDelta += this.direction() === 'vertical' ? movementY : movementX;
995
+ if (Math.abs(cumulativeDelta) >= this.pixelSensitivity()) {
996
+ cumulativeDelta = 0;
997
+ this.didMove = true;
998
+ const dValue = this.direction() === 'vertical' ? -movementY : movementX;
999
+ const stepAmount = root.getStepAmount(event);
1000
+ const rawAmount = dValue * stepAmount;
1001
+ if (rawAmount !== 0) {
1002
+ root.allowInputSync = true;
1003
+ root.incrementValue(Math.abs(rawAmount), {
1004
+ direction: rawAmount >= 0 ? 1 : -1,
1005
+ event,
1006
+ reason: 'scrub'
1007
+ });
1008
+ }
1009
+ }
1010
+ };
1011
+ const handlePointerUp = (event) => {
1012
+ const finish = () => {
1013
+ try {
1014
+ this.scrubAreaEl.ownerDocument.exitPointerLock();
1015
+ }
1016
+ catch {
1017
+ // Ignore — pointer lock may not be active.
1018
+ }
1019
+ this.isScrubbingRef = false;
1020
+ this.onScrubbingChange(false, event);
1021
+ root.commitValue(root.lastChangedValue ?? root.currentValue());
1022
+ // Manually dispatch a click when no movement happened, since preventDefault on
1023
+ // pointerdown suppresses the browser's synthetic click.
1024
+ if (!this.didMove && this.pointerDownTarget && root.inputEl()) {
1025
+ this.pointerDownTarget.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true }));
1026
+ }
1027
+ this.didMove = false;
1028
+ this.pointerDownTarget = null;
1029
+ };
1030
+ if (isFirefoxBrowser()) {
1031
+ // Firefox needs a small delay or pointer lock won't release on a soft click.
1032
+ setTimeout(finish, 20);
1033
+ }
1034
+ else {
1035
+ finish();
1036
+ }
1037
+ };
1038
+ const win = this.scrubAreaEl.ownerDocument.defaultView ?? window;
1039
+ const vV = win.visualViewport;
1040
+ const handleVisualResize = () => {
1041
+ if (vV) {
1042
+ this.visualScale = vV.scale;
1043
+ }
1044
+ };
1045
+ handleVisualResize();
1046
+ win.addEventListener('pointermove', handlePointerMove, true);
1047
+ win.addEventListener('pointerup', handlePointerUp, true);
1048
+ win.addEventListener('pointercancel', handlePointerUp, true);
1049
+ vV?.addEventListener('resize', handleVisualResize);
1050
+ onCleanup(() => {
1051
+ win.removeEventListener('pointermove', handlePointerMove, true);
1052
+ win.removeEventListener('pointerup', handlePointerUp, true);
1053
+ win.removeEventListener('pointercancel', handlePointerUp, true);
1054
+ vV?.removeEventListener('resize', handleVisualResize);
1055
+ });
1056
+ });
1057
+ }
1058
+ async onPointerDown(event) {
1059
+ const isMainButton = !event.button || event.button === 0;
1060
+ if (event.defaultPrevented || !isMainButton || !this.canScrub()) {
453
1061
  return;
454
1062
  }
455
- const currentInputValue = this.numberParser().parse(this.inputEl()?.value ?? '');
456
- if (isNaN(currentInputValue)) {
457
- this.value.set(this.min() ?? 0);
1063
+ const isTouch = event.pointerType === 'touch';
1064
+ this.isTouchInput.set(isTouch);
1065
+ if (event.pointerType === 'mouse') {
1066
+ event.preventDefault();
1067
+ this.rootContext.inputEl()?.focus();
458
1068
  }
459
- else {
460
- if (type === 'increase') {
461
- this.value.set(this.clampInputValue(currentInputValue + (this.step() ?? 1) * multiplier));
1069
+ this.isScrubbingRef = true;
1070
+ this.didMove = false;
1071
+ this.pointerDownTarget = event.target;
1072
+ this.onScrubbingChange(true, event);
1073
+ // WebKit causes significant layout shift with the native pointer-lock message.
1074
+ if (!isTouch && !isWebKitBrowser$1()) {
1075
+ try {
1076
+ await this.scrubAreaEl.ownerDocument.body.requestPointerLock();
1077
+ this.isPointerLockDenied.set(false);
462
1078
  }
463
- else {
464
- this.value.set(this.clampInputValue(currentInputValue - (this.step() ?? 1) * multiplier));
1079
+ catch {
1080
+ this.isPointerLockDenied.set(true);
465
1081
  }
1082
+ finally {
1083
+ if (this.isScrubbingRef) {
1084
+ this.onScrubbingChange(true, event);
1085
+ }
1086
+ }
1087
+ }
1088
+ }
1089
+ onTouchStart(event) {
1090
+ if (!this.canScrub()) {
1091
+ return;
1092
+ }
1093
+ // Prevent scrolling using touch input when scrubbing.
1094
+ if (event.touches.length === 1) {
1095
+ event.preventDefault();
1096
+ }
1097
+ }
1098
+ onScrub(event) {
1099
+ const cursor = this.cursorEl();
1100
+ if (!cursor) {
1101
+ return;
1102
+ }
1103
+ const rect = getViewportRect(this.teleportDistance(), this.scrubAreaEl);
1104
+ const coords = this.virtualCursorCoords;
1105
+ const newCoords = { x: Math.round(coords.x + event.movementX), y: Math.round(coords.y + event.movementY) };
1106
+ const cursorWidth = cursor.offsetWidth;
1107
+ const cursorHeight = cursor.offsetHeight;
1108
+ if (newCoords.x + cursorWidth / 2 < rect.x) {
1109
+ newCoords.x = rect.width - cursorWidth / 2;
1110
+ }
1111
+ else if (newCoords.x + cursorWidth / 2 > rect.width) {
1112
+ newCoords.x = rect.x - cursorWidth / 2;
1113
+ }
1114
+ if (newCoords.y + cursorHeight / 2 < rect.y) {
1115
+ newCoords.y = rect.height - cursorHeight / 2;
1116
+ }
1117
+ else if (newCoords.y + cursorHeight / 2 > rect.height) {
1118
+ newCoords.y = rect.y - cursorHeight / 2;
1119
+ }
1120
+ this.virtualCursorCoords = newCoords;
1121
+ this.updateCursorTransform(newCoords.x, newCoords.y);
1122
+ }
1123
+ onScrubbingChange(scrubbing, event) {
1124
+ this.isScrubbing.set(scrubbing);
1125
+ this.rootContext.isScrubbing.set(scrubbing);
1126
+ const cursor = this.cursorEl();
1127
+ if (!cursor || !scrubbing) {
1128
+ return;
1129
+ }
1130
+ const initialCoords = {
1131
+ x: event.clientX - cursor.offsetWidth / 2,
1132
+ y: event.clientY - cursor.offsetHeight / 2
1133
+ };
1134
+ this.virtualCursorCoords = initialCoords;
1135
+ this.updateCursorTransform(initialCoords.x, initialCoords.y);
1136
+ }
1137
+ updateCursorTransform(x, y) {
1138
+ const cursor = this.cursorEl();
1139
+ if (cursor) {
1140
+ cursor.style.transform = `translate3d(${x}px,${y}px,0) scale(${1 / this.visualScale})`;
466
1141
  }
467
1142
  }
468
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.3", ngImport: i0, type: RdxNumberFieldRootDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
469
- static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "20.3.3", type: RdxNumberFieldRootDirective, isStandalone: true, selector: "[rdxNumberFieldRoot]", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, min: { classPropertyName: "min", publicName: "min", isSignal: true, isRequired: false, transformFunction: null }, max: { classPropertyName: "max", publicName: "max", isSignal: true, isRequired: false, transformFunction: null }, step: { classPropertyName: "step", publicName: "step", isSignal: true, isRequired: false, transformFunction: null }, stepSnapping: { classPropertyName: "stepSnapping", publicName: "stepSnapping", isSignal: true, isRequired: false, transformFunction: null }, disableWheelChange: { classPropertyName: "disableWheelChange", publicName: "disableWheelChange", isSignal: true, isRequired: false, transformFunction: null }, locale: { classPropertyName: "locale", publicName: "locale", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, formatOptions: { classPropertyName: "formatOptions", publicName: "formatOptions", isSignal: true, isRequired: false, transformFunction: null }, readonly: { classPropertyName: "readonly", publicName: "readonly", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { value: "valueChange" }, host: { attributes: { "role": "group" }, properties: { "attr.data-disabled": "disabled() ? \"\" : undefined", "attr.data-readonly": "readonly() ? \"\" : undefined" } }, providers: [provideToken(NUMBER_FIELD_ROOT_CONTEXT, RdxNumberFieldRootDirective)], exportAs: ["rdxNumberFieldRoot"], ngImport: i0 }); }
1143
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxNumberFieldScrubArea, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
1144
+ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.2.9", type: RdxNumberFieldScrubArea, isStandalone: true, selector: "[rdxNumberFieldScrubArea]", inputs: { direction: { classPropertyName: "direction", publicName: "direction", isSignal: true, isRequired: false, transformFunction: null }, pixelSensitivity: { classPropertyName: "pixelSensitivity", publicName: "pixelSensitivity", isSignal: true, isRequired: false, transformFunction: null }, teleportDistance: { classPropertyName: "teleportDistance", publicName: "teleportDistance", isSignal: true, isRequired: false, transformFunction: null } }, host: { attributes: { "role": "presentation" }, listeners: { "pointerdown": "onPointerDown($event)", "touchstart": "onTouchStart($event)" }, properties: { "style.touch-action": "\"none\"", "style.user-select": "\"none\"", "style.-webkit-user-select": "\"none\"", "attr.data-disabled": "rootContext.isDisabled() ? \"\" : undefined", "attr.data-readonly": "rootContext.readonly() ? \"\" : undefined", "attr.data-scrubbing": "isScrubbing() ? \"\" : undefined" } }, providers: [provideNumberFieldScrubAreaContext(() => inject(RdxNumberFieldScrubArea).context)], exportAs: ["rdxNumberFieldScrubArea"], ngImport: i0 }); }
470
1145
  }
471
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.3", ngImport: i0, type: RdxNumberFieldRootDirective, decorators: [{
1146
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxNumberFieldScrubArea, decorators: [{
472
1147
  type: Directive,
473
1148
  args: [{
474
- selector: '[rdxNumberFieldRoot]',
475
- exportAs: 'rdxNumberFieldRoot',
476
- providers: [provideToken(NUMBER_FIELD_ROOT_CONTEXT, RdxNumberFieldRootDirective)],
1149
+ selector: '[rdxNumberFieldScrubArea]',
1150
+ exportAs: 'rdxNumberFieldScrubArea',
1151
+ providers: [provideNumberFieldScrubAreaContext(() => inject(RdxNumberFieldScrubArea).context)],
477
1152
  host: {
478
- role: 'group',
479
- '[attr.data-disabled]': 'disabled() ? "" : undefined',
480
- '[attr.data-readonly]': 'readonly() ? "" : undefined'
1153
+ role: 'presentation',
1154
+ '[style.touch-action]': '"none"',
1155
+ '[style.user-select]': '"none"',
1156
+ '[style.-webkit-user-select]': '"none"',
1157
+ '[attr.data-disabled]': 'rootContext.isDisabled() ? "" : undefined',
1158
+ '[attr.data-readonly]': 'rootContext.readonly() ? "" : undefined',
1159
+ '[attr.data-scrubbing]': 'isScrubbing() ? "" : undefined',
1160
+ '(pointerdown)': 'onPointerDown($event)',
1161
+ '(touchstart)': 'onTouchStart($event)'
481
1162
  }
482
1163
  }]
483
- }] });
1164
+ }], ctorParameters: () => [], propDecorators: { direction: [{ type: i0.Input, args: [{ isSignal: true, alias: "direction", required: false }] }], pixelSensitivity: [{ type: i0.Input, args: [{ isSignal: true, alias: "pixelSensitivity", required: false }] }], teleportDistance: [{ type: i0.Input, args: [{ isSignal: true, alias: "teleportDistance", required: false }] }] } });
1165
+
1166
+ function isWebKitBrowser() {
1167
+ return (typeof navigator !== 'undefined' &&
1168
+ /AppleWebKit/.test(navigator.userAgent) &&
1169
+ !/Chrome/.test(navigator.userAgent));
1170
+ }
1171
+ /**
1172
+ * A custom element shown instead of the native cursor while scrubbing. It is portaled to the
1173
+ * document body and positioned with the Pointer Lock API. Hidden in Safari (which would shift
1174
+ * layout with the native pointer-lock notification) and for touch input.
1175
+ *
1176
+ * @see https://base-ui.com/react/components/number-field
1177
+ */
1178
+ class RdxNumberFieldScrubAreaCursor {
1179
+ constructor() {
1180
+ this.elementRef = inject(ElementRef);
1181
+ this.scrubContext = injectNumberFieldScrubAreaContext();
1182
+ this.shouldRender = computed(() => this.scrubContext.isScrubbing() &&
1183
+ !isWebKitBrowser() &&
1184
+ !this.scrubContext.isTouchInput() &&
1185
+ !this.scrubContext.isPointerLockDenied(), ...(ngDevMode ? [{ debugName: "shouldRender" }] : /* istanbul ignore next */ []));
1186
+ this.scrubContext.registerCursor(this.elementRef.nativeElement);
1187
+ inject(DestroyRef).onDestroy(() => this.scrubContext.registerCursor(null));
1188
+ }
1189
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxNumberFieldScrubAreaCursor, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
1190
+ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.9", type: RdxNumberFieldScrubAreaCursor, isStandalone: true, selector: "[rdxNumberFieldScrubAreaCursor]", host: { attributes: { "role": "presentation" }, properties: { "style.position": "\"fixed\"", "style.top.px": "0", "style.left.px": "0", "style.pointer-events": "\"none\"", "style.opacity": "shouldRender() ? \"1\" : \"0\"" } }, exportAs: ["rdxNumberFieldScrubAreaCursor"], hostDirectives: [{ directive: i1$1.RdxPortal }], ngImport: i0 }); }
1191
+ }
1192
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxNumberFieldScrubAreaCursor, decorators: [{
1193
+ type: Directive,
1194
+ args: [{
1195
+ selector: '[rdxNumberFieldScrubAreaCursor]',
1196
+ exportAs: 'rdxNumberFieldScrubAreaCursor',
1197
+ hostDirectives: [RdxPortal],
1198
+ host: {
1199
+ role: 'presentation',
1200
+ '[style.position]': '"fixed"',
1201
+ '[style.top.px]': '0',
1202
+ '[style.left.px]': '0',
1203
+ '[style.pointer-events]': '"none"',
1204
+ '[style.opacity]': 'shouldRender() ? "1" : "0"'
1205
+ }
1206
+ }]
1207
+ }], ctorParameters: () => [] });
484
1208
 
485
1209
  const _imports = [
486
- RdxNumberFieldRootDirective,
487
- RdxNumberFieldInputDirective,
488
- RdxNumberFieldIncrementDirective,
489
- RdxNumberFieldDecrementDirective
1210
+ RdxNumberFieldRoot,
1211
+ RdxNumberFieldGroup,
1212
+ RdxNumberFieldInput,
1213
+ RdxNumberFieldHiddenInput,
1214
+ RdxNumberFieldIncrement,
1215
+ RdxNumberFieldDecrement,
1216
+ RdxNumberFieldScrubArea,
1217
+ RdxNumberFieldScrubAreaCursor
490
1218
  ];
491
1219
  class RdxNumberFieldModule {
492
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.3", ngImport: i0, type: RdxNumberFieldModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); }
493
- static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "20.3.3", ngImport: i0, type: RdxNumberFieldModule, imports: [RdxNumberFieldRootDirective,
494
- RdxNumberFieldInputDirective,
495
- RdxNumberFieldIncrementDirective,
496
- RdxNumberFieldDecrementDirective], exports: [RdxNumberFieldRootDirective,
497
- RdxNumberFieldInputDirective,
498
- RdxNumberFieldIncrementDirective,
499
- RdxNumberFieldDecrementDirective] }); }
500
- static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "20.3.3", ngImport: i0, type: RdxNumberFieldModule }); }
1220
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxNumberFieldModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); }
1221
+ static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "21.2.9", ngImport: i0, type: RdxNumberFieldModule, imports: [RdxNumberFieldRoot,
1222
+ RdxNumberFieldGroup,
1223
+ RdxNumberFieldInput,
1224
+ RdxNumberFieldHiddenInput,
1225
+ RdxNumberFieldIncrement,
1226
+ RdxNumberFieldDecrement,
1227
+ RdxNumberFieldScrubArea,
1228
+ RdxNumberFieldScrubAreaCursor], exports: [RdxNumberFieldRoot,
1229
+ RdxNumberFieldGroup,
1230
+ RdxNumberFieldInput,
1231
+ RdxNumberFieldHiddenInput,
1232
+ RdxNumberFieldIncrement,
1233
+ RdxNumberFieldDecrement,
1234
+ RdxNumberFieldScrubArea,
1235
+ RdxNumberFieldScrubAreaCursor] }); }
1236
+ static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxNumberFieldModule }); }
501
1237
  }
502
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.3", ngImport: i0, type: RdxNumberFieldModule, decorators: [{
1238
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxNumberFieldModule, decorators: [{
503
1239
  type: NgModule,
504
1240
  args: [{
505
1241
  imports: [..._imports],
@@ -511,5 +1247,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.3", ngImpor
511
1247
  * Generated bundle index. Do not edit.
512
1248
  */
513
1249
 
514
- export { NUMBER_FIELD_ROOT_CONTEXT, PressedHoldService, RdxNumberFieldDecrementDirective, RdxNumberFieldIncrementDirective, RdxNumberFieldInputDirective, RdxNumberFieldModule, RdxNumberFieldRootDirective, handleDecimalOperation, injectNumberFieldRootContext, useNumberFormatter, useNumberParser };
1250
+ export { CHANGE_VALUE_TICK_DELAY, DEFAULT_STEP, REASONS, RdxNumberFieldButton, RdxNumberFieldDecrement, RdxNumberFieldGroup, RdxNumberFieldHiddenInput, RdxNumberFieldIncrement, RdxNumberFieldInput, RdxNumberFieldModule, RdxNumberFieldRoot, RdxNumberFieldScrubArea, RdxNumberFieldScrubAreaCursor, SCROLLING_POINTER_MOVE_DISTANCE, START_AUTO_CHANGE_DELAY, createPressAndHold, hasNumberFormatRoundingOptions, injectNumberFieldRootContext, injectNumberFieldScrubAreaContext, numberOrUndefined, provideNumberFieldRootContext, provideNumberFieldScrubAreaContext, removeFloatingPointErrors, toValidatedNumber, useNumberFormatter, useNumberParser };
515
1251
  //# sourceMappingURL=radix-ng-primitives-number-field.mjs.map