@radix-ng/primitives 0.51.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 (178) hide show
  1. package/fesm2022/radix-ng-primitives-accordion.mjs +105 -38
  2. package/fesm2022/radix-ng-primitives-accordion.mjs.map +1 -1
  3. package/fesm2022/radix-ng-primitives-alert-dialog.mjs +221 -129
  4. package/fesm2022/radix-ng-primitives-alert-dialog.mjs.map +1 -1
  5. package/fesm2022/radix-ng-primitives-arrow.mjs +20 -4
  6. package/fesm2022/radix-ng-primitives-arrow.mjs.map +1 -1
  7. package/fesm2022/radix-ng-primitives-aspect-ratio.mjs.map +1 -1
  8. package/fesm2022/radix-ng-primitives-avatar.mjs +54 -61
  9. package/fesm2022/radix-ng-primitives-avatar.mjs.map +1 -1
  10. package/fesm2022/radix-ng-primitives-button.mjs +123 -0
  11. package/fesm2022/radix-ng-primitives-button.mjs.map +1 -0
  12. package/fesm2022/radix-ng-primitives-calendar.mjs.map +1 -1
  13. package/fesm2022/radix-ng-primitives-checkbox.mjs +378 -54
  14. package/fesm2022/radix-ng-primitives-checkbox.mjs.map +1 -1
  15. package/fesm2022/radix-ng-primitives-collapsible.mjs +182 -81
  16. package/fesm2022/radix-ng-primitives-collapsible.mjs.map +1 -1
  17. package/fesm2022/radix-ng-primitives-collection.mjs +40 -57
  18. package/fesm2022/radix-ng-primitives-collection.mjs.map +1 -1
  19. package/fesm2022/radix-ng-primitives-config.mjs.map +1 -1
  20. package/fesm2022/radix-ng-primitives-context-menu.mjs +140 -424
  21. package/fesm2022/radix-ng-primitives-context-menu.mjs.map +1 -1
  22. package/fesm2022/radix-ng-primitives-core.mjs +735 -744
  23. package/fesm2022/radix-ng-primitives-core.mjs.map +1 -1
  24. package/fesm2022/radix-ng-primitives-cropper.mjs +1 -0
  25. package/fesm2022/radix-ng-primitives-cropper.mjs.map +1 -1
  26. package/fesm2022/radix-ng-primitives-date-field.mjs +51 -45
  27. package/fesm2022/radix-ng-primitives-date-field.mjs.map +1 -1
  28. package/fesm2022/radix-ng-primitives-dialog.mjs +655 -327
  29. package/fesm2022/radix-ng-primitives-dialog.mjs.map +1 -1
  30. package/fesm2022/radix-ng-primitives-dismissable-layer.mjs +70 -46
  31. package/fesm2022/radix-ng-primitives-dismissable-layer.mjs.map +1 -1
  32. package/fesm2022/radix-ng-primitives-drawer.mjs +1059 -0
  33. package/fesm2022/radix-ng-primitives-drawer.mjs.map +1 -0
  34. package/fesm2022/radix-ng-primitives-editable.mjs.map +1 -1
  35. package/fesm2022/radix-ng-primitives-field.mjs +363 -0
  36. package/fesm2022/radix-ng-primitives-field.mjs.map +1 -0
  37. package/fesm2022/radix-ng-primitives-fieldset.mjs +79 -0
  38. package/fesm2022/radix-ng-primitives-fieldset.mjs.map +1 -0
  39. package/fesm2022/radix-ng-primitives-focus-scope.mjs +23 -8
  40. package/fesm2022/radix-ng-primitives-focus-scope.mjs.map +1 -1
  41. package/fesm2022/radix-ng-primitives-input.mjs +172 -0
  42. package/fesm2022/radix-ng-primitives-input.mjs.map +1 -0
  43. package/fesm2022/radix-ng-primitives-label.mjs +6 -6
  44. package/fesm2022/radix-ng-primitives-label.mjs.map +1 -1
  45. package/fesm2022/radix-ng-primitives-menu.mjs +1480 -344
  46. package/fesm2022/radix-ng-primitives-menu.mjs.map +1 -1
  47. package/fesm2022/radix-ng-primitives-menubar.mjs +290 -162
  48. package/fesm2022/radix-ng-primitives-menubar.mjs.map +1 -1
  49. package/fesm2022/radix-ng-primitives-meter.mjs +271 -0
  50. package/fesm2022/radix-ng-primitives-meter.mjs.map +1 -0
  51. package/fesm2022/radix-ng-primitives-navigation-menu.mjs +1052 -1553
  52. package/fesm2022/radix-ng-primitives-navigation-menu.mjs.map +1 -1
  53. package/fesm2022/radix-ng-primitives-number-field.mjs +1102 -367
  54. package/fesm2022/radix-ng-primitives-number-field.mjs.map +1 -1
  55. package/fesm2022/radix-ng-primitives-pagination.mjs.map +1 -1
  56. package/fesm2022/radix-ng-primitives-popover.mjs +978 -989
  57. package/fesm2022/radix-ng-primitives-popover.mjs.map +1 -1
  58. package/fesm2022/radix-ng-primitives-popper.mjs +91 -41
  59. package/fesm2022/radix-ng-primitives-popper.mjs.map +1 -1
  60. package/fesm2022/radix-ng-primitives-portal.mjs +34 -10
  61. package/fesm2022/radix-ng-primitives-portal.mjs.map +1 -1
  62. package/fesm2022/radix-ng-primitives-presence.mjs +134 -246
  63. package/fesm2022/radix-ng-primitives-presence.mjs.map +1 -1
  64. package/fesm2022/radix-ng-primitives-preview-card.mjs +997 -0
  65. package/fesm2022/radix-ng-primitives-preview-card.mjs.map +1 -0
  66. package/fesm2022/radix-ng-primitives-progress.mjs +223 -84
  67. package/fesm2022/radix-ng-primitives-progress.mjs.map +1 -1
  68. package/fesm2022/radix-ng-primitives-radio.mjs +191 -51
  69. package/fesm2022/radix-ng-primitives-radio.mjs.map +1 -1
  70. package/fesm2022/radix-ng-primitives-roving-focus.mjs +96 -50
  71. package/fesm2022/radix-ng-primitives-roving-focus.mjs.map +1 -1
  72. package/fesm2022/radix-ng-primitives-select.mjs +791 -509
  73. package/fesm2022/radix-ng-primitives-select.mjs.map +1 -1
  74. package/fesm2022/radix-ng-primitives-separator.mjs +12 -35
  75. package/fesm2022/radix-ng-primitives-separator.mjs.map +1 -1
  76. package/fesm2022/radix-ng-primitives-slider.mjs +969 -717
  77. package/fesm2022/radix-ng-primitives-slider.mjs.map +1 -1
  78. package/fesm2022/radix-ng-primitives-stepper.mjs +15 -19
  79. package/fesm2022/radix-ng-primitives-stepper.mjs.map +1 -1
  80. package/fesm2022/radix-ng-primitives-switch.mjs +125 -113
  81. package/fesm2022/radix-ng-primitives-switch.mjs.map +1 -1
  82. package/fesm2022/radix-ng-primitives-tabs.mjs +381 -108
  83. package/fesm2022/radix-ng-primitives-tabs.mjs.map +1 -1
  84. package/fesm2022/radix-ng-primitives-time-field.mjs +55 -46
  85. package/fesm2022/radix-ng-primitives-time-field.mjs.map +1 -1
  86. package/fesm2022/radix-ng-primitives-toggle-group.mjs +121 -247
  87. package/fesm2022/radix-ng-primitives-toggle-group.mjs.map +1 -1
  88. package/fesm2022/radix-ng-primitives-toggle.mjs +98 -61
  89. package/fesm2022/radix-ng-primitives-toggle.mjs.map +1 -1
  90. package/fesm2022/radix-ng-primitives-toolbar.mjs +303 -92
  91. package/fesm2022/radix-ng-primitives-toolbar.mjs.map +1 -1
  92. package/fesm2022/radix-ng-primitives-tooltip.mjs +690 -1071
  93. package/fesm2022/radix-ng-primitives-tooltip.mjs.map +1 -1
  94. package/fesm2022/radix-ng-primitives-visually-hidden.mjs +25 -66
  95. package/fesm2022/radix-ng-primitives-visually-hidden.mjs.map +1 -1
  96. package/meter/README.md +3 -0
  97. package/navigation-menu/README.md +2 -1
  98. package/package.json +31 -18
  99. package/portal/README.md +2 -0
  100. package/preview-card/README.md +3 -0
  101. package/schematics/collection.json +1 -0
  102. package/schematics/ng-add/index.d.ts +3 -2
  103. package/schematics/ng-add/index.js +62 -31
  104. package/schematics/ng-add/index.js.map +1 -1
  105. package/schematics/ng-add/package-config.d.ts +4 -2
  106. package/schematics/ng-add/package-config.js +10 -2
  107. package/schematics/ng-add/package-config.js.map +1 -1
  108. package/schematics/ng-add/schema.d.ts +3 -0
  109. package/schematics/ng-add/schema.js +3 -0
  110. package/schematics/ng-add/schema.js.map +1 -0
  111. package/schematics/ng-add/schema.json +14 -0
  112. package/select/README.md +2 -0
  113. package/types/radix-ng-primitives-accordion.d.ts +48 -14
  114. package/types/radix-ng-primitives-alert-dialog.d.ts +95 -38
  115. package/types/radix-ng-primitives-arrow.d.ts +1 -1
  116. package/types/radix-ng-primitives-aspect-ratio.d.ts +1 -1
  117. package/types/radix-ng-primitives-avatar.d.ts +7 -11
  118. package/types/radix-ng-primitives-button.d.ts +73 -0
  119. package/types/radix-ng-primitives-calendar.d.ts +1 -2
  120. package/types/radix-ng-primitives-checkbox.d.ts +201 -32
  121. package/types/radix-ng-primitives-collapsible.d.ts +112 -39
  122. package/types/radix-ng-primitives-collection.d.ts +38 -34
  123. package/types/radix-ng-primitives-config.d.ts +1 -1
  124. package/types/radix-ng-primitives-context-menu.d.ts +60 -116
  125. package/types/radix-ng-primitives-core.d.ts +307 -236
  126. package/types/radix-ng-primitives-cropper.d.ts +2 -2
  127. package/types/radix-ng-primitives-date-field.d.ts +38 -23
  128. package/types/radix-ng-primitives-dialog.d.ts +282 -165
  129. package/types/radix-ng-primitives-dismissable-layer.d.ts +15 -7
  130. package/types/radix-ng-primitives-drawer.d.ts +448 -0
  131. package/types/radix-ng-primitives-editable.d.ts +1 -1
  132. package/types/radix-ng-primitives-field.d.ts +373 -0
  133. package/types/radix-ng-primitives-fieldset.d.ts +48 -0
  134. package/types/radix-ng-primitives-focus-scope.d.ts +13 -5
  135. package/types/radix-ng-primitives-input.d.ts +87 -0
  136. package/types/radix-ng-primitives-label.d.ts +0 -1
  137. package/types/radix-ng-primitives-menu.d.ts +572 -99
  138. package/types/radix-ng-primitives-menubar.d.ts +60 -50
  139. package/types/radix-ng-primitives-meter.d.ts +193 -0
  140. package/types/radix-ng-primitives-navigation-menu.d.ts +422 -340
  141. package/types/radix-ng-primitives-number-field.d.ts +405 -145
  142. package/types/radix-ng-primitives-pagination.d.ts +2 -2
  143. package/types/radix-ng-primitives-popover.d.ts +365 -351
  144. package/types/radix-ng-primitives-popper.d.ts +49 -9
  145. package/types/radix-ng-primitives-portal.d.ts +14 -6
  146. package/types/radix-ng-primitives-presence.d.ts +28 -76
  147. package/types/radix-ng-primitives-preview-card.d.ts +359 -0
  148. package/types/radix-ng-primitives-progress.d.ts +174 -48
  149. package/types/radix-ng-primitives-radio.d.ts +55 -25
  150. package/types/radix-ng-primitives-roving-focus.d.ts +30 -21
  151. package/types/radix-ng-primitives-select.d.ts +475 -177
  152. package/types/radix-ng-primitives-separator.d.ts +7 -32
  153. package/types/radix-ng-primitives-slider.d.ts +315 -201
  154. package/types/radix-ng-primitives-stepper.d.ts +5 -7
  155. package/types/radix-ng-primitives-switch.d.ts +86 -71
  156. package/types/radix-ng-primitives-tabs.d.ts +213 -79
  157. package/types/radix-ng-primitives-time-field.d.ts +42 -27
  158. package/types/radix-ng-primitives-toggle-group.d.ts +85 -164
  159. package/types/radix-ng-primitives-toggle.d.ts +43 -53
  160. package/types/radix-ng-primitives-toolbar.d.ts +163 -38
  161. package/types/radix-ng-primitives-tooltip.d.ts +347 -384
  162. package/types/radix-ng-primitives-visually-hidden.d.ts +19 -19
  163. package/dropdown-menu/README.md +0 -1
  164. package/fesm2022/radix-ng-primitives-dropdown-menu.mjs +0 -581
  165. package/fesm2022/radix-ng-primitives-dropdown-menu.mjs.map +0 -1
  166. package/fesm2022/radix-ng-primitives-hover-card.mjs +0 -1238
  167. package/fesm2022/radix-ng-primitives-hover-card.mjs.map +0 -1
  168. package/fesm2022/radix-ng-primitives-select2.mjs +0 -897
  169. package/fesm2022/radix-ng-primitives-select2.mjs.map +0 -1
  170. package/fesm2022/radix-ng-primitives-tooltip2.mjs +0 -735
  171. package/fesm2022/radix-ng-primitives-tooltip2.mjs.map +0 -1
  172. package/hover-card/README.md +0 -3
  173. package/select2/README.md +0 -3
  174. package/tooltip2/README.md +0 -3
  175. package/types/radix-ng-primitives-dropdown-menu.d.ts +0 -171
  176. package/types/radix-ng-primitives-hover-card.d.ts +0 -471
  177. package/types/radix-ng-primitives-select2.d.ts +0 -511
  178. package/types/radix-ng-primitives-tooltip2.d.ts +0 -325
@@ -1,503 +1,1238 @@
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" }] : /* istanbul ignore next */ []));
19
- const triggerHook = new Subject();
20
- const isPressed = signal(false, ...(ngDevMode ? [{ debugName: "isPressed" }] : /* istanbul ignore next */ []));
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: "21.2.9", ngImport: i0, type: PressedHoldService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
78
- static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.9", 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: "21.2.9", 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;
88
86
  }
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;
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;
118
+ }
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" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
115
216
  /**
116
- * @ignore
217
+ * When `true`, the button is disabled in addition to inheriting the root's disabled state.
218
+ * @default false
117
219
  */
118
- this.isDisabled = computed(() => this.rootContext.disabled() ||
119
- this.rootContext.readonly() ||
120
- this.disabled() ||
121
- this.rootContext.isDecreaseDisabled(), ...(ngDevMode ? [{ debugName: "isDisabled" }] : /* istanbul ignore next */ []));
122
- /**
123
- * @ignore
124
- */
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())
242
+ });
243
+ }
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
128
255
  });
256
+ const committed = this.rootContext.lastChangedValue ?? this.rootContext.currentValue();
257
+ if (committed !== prev) {
258
+ this.rootContext.commitValue(committed);
259
+ }
129
260
  }
130
- ngOnInit() {
131
- this.useHold.onTrigger(() => this.rootContext.handleDecrease());
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);
132
273
  }
133
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxNumberFieldDecrementDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
134
- static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.2.9", 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 }); }
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
+ }
282
+ }
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: "21.2.9", 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
  }]
152
- }], propDecorators: { disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }] } });
304
+ }], ctorParameters: () => [], propDecorators: { disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }] } });
153
305
 
154
- class RdxNumberFieldIncrementDirective {
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
+ }]
328
+ }] });
329
+
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" } : /* istanbul ignore next */ {}), 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" }] : /* istanbul ignore next */ []));
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();
177
367
  }
178
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxNumberFieldIncrementDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
179
- static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.2.9", 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 }); }
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();
371
+ }
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: "21.2.9", 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
- }], propDecorators: { disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }] } });
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 {
200
417
  constructor() {
201
- this.elementRef = inject((ElementRef));
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 {
441
+ constructor() {
442
+ this.elementRef = inject(ElementRef);
202
443
  this.rootContext = injectNumberFieldRootContext();
203
- this.inputValue = signal(this.rootContext.textValue(), ...(ngDevMode ? [{ debugName: "inputValue" }] : /* istanbul ignore next */ []));
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) {
213
- const inputEvent = event;
214
- const target = inputEvent.target;
459
+ // Only validate insertions; deletions and composition have `data === null`.
460
+ if (event.data == null) {
461
+ return;
462
+ }
463
+ const target = event.target;
215
464
  const nextValue = target.value.slice(0, target.selectionStart ?? undefined) +
216
- (inputEvent.data ?? '') +
465
+ event.data +
217
466
  target.value.slice(target.selectionEnd ?? undefined);
218
- if (!this.rootContext.validate(nextValue)) {
219
- inputEvent.preventDefault();
467
+ if (!this.rootContext.isValidPartial(nextValue)) {
468
+ event.preventDefault();
220
469
  }
221
470
  }
222
- onWheelEvent(event) {
223
- 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);
224
478
  return;
225
479
  }
226
- // only handle when in focus
227
- if (event.target !== getActiveElement())
228
- return;
229
- // if on a trackpad, users can scroll in both X and Y at once, check the magnitude of the change
230
- // if it's mostly in the X direction, then just return, the user probably doesn't mean to inc/dec
231
- // this isn't perfect, events come in fast with small deltas and a part of the scroll may give a false indication
232
- // especially if the user is scrolling near 45deg
233
- 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()) {
234
489
  return;
235
490
  }
236
- event.preventDefault();
237
- if (event.deltaY > 0) {
238
- 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());
239
504
  }
240
- else if (event.deltaY < 0) {
241
- 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());
242
514
  }
243
515
  }
244
- onKeydownPageUp(event) {
245
- event.preventDefault();
246
- this.rootContext.handleIncrease(10);
247
- }
248
- onKeydownPageDown(event) {
249
- event.preventDefault();
250
- this.rootContext.handleDecrease(10);
251
- }
252
- onKeydownHome(event) {
253
- event.preventDefault();
254
- this.rootContext.handleMinMaxValue('min');
255
- }
256
- 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.
257
523
  event.preventDefault();
258
- this.rootContext.handleMinMaxValue('max');
259
- }
260
- onInput(event) {
261
- const target = event.target;
262
- this.rootContext.applyInputValue(target.value);
263
- }
264
- onChange() {
265
- this.inputValue.set(this.rootContext.textValue());
266
- }
267
- onKeydownEnter(event) {
268
- const target = event.target;
269
- 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
+ }
270
530
  }
271
- onKeydownUp(event) {
272
- event.preventDefault();
273
- 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
+ }
274
565
  }
275
- 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
+ }
276
575
  event.preventDefault();
277
- 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
+ });
278
583
  }
279
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxNumberFieldInputDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
280
- static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.9", 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 }); }
281
586
  }
282
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxNumberFieldInputDirective, decorators: [{
587
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxNumberFieldInput, decorators: [{
283
588
  type: Directive,
284
589
  args: [{
285
590
  selector: 'input[rdxNumberFieldInput]',
591
+ exportAs: 'rdxNumberFieldInput',
286
592
  host: {
287
- role: 'spinbutton',
288
593
  type: 'text',
289
- tabindex: '0',
290
594
  autocomplete: 'off',
291
595
  autocorrect: 'off',
292
596
  spellcheck: 'false',
293
597
  'aria-roledescription': 'Number field',
294
- '[attr.aria-valuenow]': 'rootContext.value()',
295
- '[attr.aria-valuemin]': 'rootContext.min()',
296
- '[attr.aria-valuemax]': 'rootContext.max()',
598
+ '[id]': 'rootContext.id()',
599
+ '[value]': 'rootContext.inputValue()',
297
600
  '[attr.inputmode]': 'rootContext.inputMode()',
298
- '[attr.disabled]': 'rootContext.disabled() ? "" : undefined',
299
- '[attr.data-disabled]': 'rootContext.disabled() ? "" : undefined',
601
+ '[attr.aria-invalid]': 'rootContext.required() && rootContext.currentValue() === null ? "true" : undefined',
602
+ '[disabled]': 'rootContext.isDisabled()',
300
603
  '[attr.readonly]': 'rootContext.readonly() ? "" : undefined',
604
+ '[required]': 'rootContext.required()',
605
+ '[attr.data-disabled]': 'rootContext.isDisabled() ? "" : undefined',
301
606
  '[attr.data-readonly]': 'rootContext.readonly() ? "" : undefined',
302
- '[attr.value]': 'inputValue()',
303
- '(change)': 'onChange()',
607
+ '[attr.data-required]': 'rootContext.required() ? "" : undefined',
608
+ '(focus)': 'onFocus($event)',
609
+ '(blur)': 'onBlur($event)',
304
610
  '(input)': 'onInput($event)',
305
- '(blur)': 'onKeydownEnter($event)',
306
611
  '(beforeinput)': 'onBeforeInput($event)',
307
- '(keydown.enter)': 'onKeydownEnter($event)',
308
- '(keydown.arrowUp)': 'onKeydownUp($event)',
309
- '(keydown.arrowDown)': 'onKeydownDown($event)',
310
- '(keydown.home)': 'onKeydownHome($event)',
311
- '(keydown.end)': 'onKeydownEnd($event)',
312
- '(keydown.pageUp)': 'onKeydownPageUp($event)',
313
- '(keydown.pageDown)': 'onKeydownPageDown($event)',
314
- '(wheel)': 'onWheelEvent($event)'
612
+ '(keydown)': 'onKeydown($event)',
613
+ '(paste)': 'onPaste($event)',
614
+ '(wheel)': 'onWheel($event)'
315
615
  }
316
616
  }]
317
617
  }], ctorParameters: () => [] });
318
618
 
319
- 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 {
320
647
  constructor() {
321
- this.value = model(...(ngDevMode ? [undefined, { debugName: "value" }] : /* istanbul ignore next */ []));
322
- this.min = input(undefined, { ...(ngDevMode ? { debugName: "min" } : /* istanbul ignore next */ {}), transform: numberAttribute });
323
- this.max = input(undefined, { ...(ngDevMode ? { debugName: "max" } : /* istanbul ignore next */ {}), transform: numberAttribute });
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 });
656
+ /**
657
+ * Amount to increment and decrement with the buttons, arrow keys and scrub area.
658
+ * @default 1
659
+ */
324
660
  this.step = input(1, { ...(ngDevMode ? { debugName: "step" } : /* istanbul ignore next */ {}), transform: numberAttribute });
325
- this.stepSnapping = input(false, { ...(ngDevMode ? { debugName: "stepSnapping" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
326
- this.disableWheelChange = input(false, { ...(ngDevMode ? { debugName: "disableWheelChange" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
327
- this.locale = input('en', ...(ngDevMode ? [{ debugName: "locale" }] : /* istanbul ignore next */ []));
328
- this.disabled = input(false, { ...(ngDevMode ? { debugName: "disabled" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
329
- this.formatOptions = input(...(ngDevMode ? [undefined, { debugName: "formatOptions" }] : /* istanbul ignore next */ []));
330
661
  /**
331
- * When <code>true</code>, the Number Field is read-only.
662
+ * The step used when incrementing while the Alt key is held. Snaps to multiples of this value.
663
+ * @default 0.1
332
664
  */
333
- this.readonly = input(false, { ...(ngDevMode ? { debugName: "readonly" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
665
+ this.smallStep = input(0.1, { ...(ngDevMode ? { debugName: "smallStep" } : /* istanbul ignore next */ {}), transform: numberAttribute });
334
666
  /**
335
- * @ignore
667
+ * The step used when incrementing while the Shift key is held. Snaps to multiples of this value.
668
+ * @default 10
336
669
  */
337
- this.inputEl = signal(undefined, ...(ngDevMode ? [{ debugName: "inputEl" }] : /* istanbul ignore next */ []));
670
+ this.largeStep = input(10, { ...(ngDevMode ? { debugName: "largeStep" } : /* istanbul ignore next */ {}), transform: numberAttribute });
338
671
  /**
339
- * @ignore
672
+ * Whether the value should snap to the nearest step when incrementing or decrementing.
673
+ * @default false
340
674
  */
341
- this.isDecreaseDisabled = computed(() => !isNullish(this.value()) &&
342
- (this.clampInputValue(this.value()) === this.min() || (this.min() && !isNaN(this.value()))
343
- ? handleDecimalOperation('-', this.value(), this.step()) < this.min()
344
- : false), ...(ngDevMode ? [{ debugName: "isDecreaseDisabled" }] : /* istanbul ignore next */ []));
675
+ this.snapOnStep = input(false, { ...(ngDevMode ? { debugName: "snapOnStep" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
345
676
  /**
346
- * @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
347
680
  */
348
- this.isIncreaseDisabled = computed(() => !isNullish(this.value()) &&
349
- (this.clampInputValue(this.value()) === this.max() || (this.min() && !isNaN(this.value()))
350
- ? handleDecimalOperation('+', this.value(), this.step()) > this.max()
351
- : false), ...(ngDevMode ? [{ debugName: "isIncreaseDisabled" }] : /* istanbul ignore next */ []));
681
+ this.allowOutOfRange = input(false, { ...(ngDevMode ? { debugName: "allowOutOfRange" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
352
682
  /**
353
- * @ignore
683
+ * Whether the value can be changed with the mouse wheel while the input is focused.
684
+ * @default false
354
685
  */
355
- this.inputMode = computed(() => {
356
- // The inputMode attribute influences the software keyboard that is shown on touch devices.
357
- // Browsers and operating systems are quite inconsistent about what keys are available, however.
358
- // We choose between numeric and decimal based on whether we allow negative and fractional numbers,
359
- // and based on testing on various devices to determine what keys are available in each inputMode.
360
- const hasDecimals = this.numberFormatter().resolvedOptions().maximumFractionDigits > 0;
361
- return hasDecimals ? 'decimal' : 'numeric';
362
- }, ...(ngDevMode ? [{ debugName: "inputMode" }] : /* istanbul ignore next */ []));
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 */ []));
363
691
  /**
364
- * Replace negative textValue formatted using currencySign: 'accounting'
365
- * with a textValue that can be announced using a minus sign.
366
- * @ignore
692
+ * When `true`, the user cannot interact with the field.
693
+ * @default false
367
694
  */
368
- this.textValue = computed(() => isNullish(this.value()) || isNaN(this.value()) ? '' : this.textValueFormatter().format(this.value()), ...(ngDevMode ? [{ debugName: "textValue" }] : /* istanbul ignore next */ []));
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();
369
716
  /**
370
- * @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.
371
719
  */
372
- this.onInputElement = (el) => this.inputEl.set(el);
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 */ []));
727
+ /**
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.
730
+ */
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
+ });
373
783
  }
374
- /**
375
- * @ignore
376
- */
377
- clampInputValue(val) {
378
- // Clamp to min and max, round to the nearest step, and round to the specified number of digits
379
- let clampedValue;
380
- if (this.step() === undefined || isNaN(this.step()) || !this.stepSnapping()) {
381
- clampedValue = clamp(val, this.min(), this.max());
382
- }
383
- else {
384
- 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 '';
385
788
  }
386
- clampedValue = this.numberParser().parse(this.numberFormatter().format(clampedValue));
387
- return clampedValue;
789
+ return this.formatter().format(value);
388
790
  }
389
- ngOnInit() {
390
- this.numberParser = useNumberParser(this.locale, this.formatOptions);
391
- this.numberFormatter = useNumberFormatter(this.locale, this.formatOptions);
392
- 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;
393
795
  }
394
- /**
395
- * @ignore
396
- */
397
- handleMinMaxValue(type) {
398
- if (type === 'min' && this.min() !== undefined) {
399
- 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();
400
804
  }
401
- else if (type === 'max' && this.max() !== undefined) {
402
- this.value.set(this.clampInputValue(this.max()));
805
+ if (event?.shiftKey) {
806
+ return this.largeStep();
403
807
  }
808
+ return this.step();
404
809
  }
405
- /**
406
- * @ignore
407
- */
408
- handleDecrease(multiplier = 1) {
409
- this.handleChangingValue('decrease', multiplier);
810
+ /** @ignore Registers the native input element. */
811
+ registerInput(el) {
812
+ this.inputEl.set(el);
410
813
  }
411
- /**
412
- * @ignore
413
- */
414
- handleIncrease(multiplier = 1) {
415
- 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();
416
821
  }
417
822
  /**
418
823
  * @ignore
824
+ * Validates and applies a candidate value, emitting `onValueChange` when it changes.
825
+ * Returns whether a change was fired.
419
826
  */
420
- applyInputValue(val) {
421
- const parsedValue = this.numberParser().parse(val);
422
- this.value.set(isNaN(parsedValue) ? undefined : this.clampInputValue(parsedValue));
423
- // Set to empty state if input value is empty
424
- if (!val.length) {
425
- 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;
426
850
  }
427
- // if it failed to parse, then reset input to formatted version of current number
428
- if (isNaN(parsedValue)) {
429
- return this.setInputValue(this.textValue());
851
+ this.lastChangedValue = validatedValue;
852
+ if (this.allowInputSync) {
853
+ this.setInputValue(this.formatNumber(validatedValue));
430
854
  }
431
- return this.setInputValue(this.textValue());
855
+ return shouldFireChange;
432
856
  }
433
857
  /**
434
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.
435
861
  */
436
- setInputValue(val) {
437
- if (this.inputEl()) {
438
- this.inputEl.update((el) => {
439
- if (el)
440
- el.value = val;
441
- return el;
442
- });
443
- }
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);
444
866
  }
445
- /**
446
- * @ignore
447
- */
448
- validate(val) {
449
- 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);
450
871
  }
451
- handleChangingValue(type, multiplier = 1) {
452
- this.inputEl()?.focus();
453
- if (this.disabled() || this.readonly()) {
872
+ applyValue(value) {
873
+ this.value.set(value);
874
+ this.cva.setValue(value);
875
+ }
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()) {
454
1061
  return;
455
1062
  }
456
- const currentInputValue = this.numberParser().parse(this.inputEl()?.value ?? '');
457
- if (isNaN(currentInputValue)) {
458
- 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();
459
1068
  }
460
- else {
461
- if (type === 'increase') {
462
- 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);
463
1078
  }
464
- else {
465
- this.value.set(this.clampInputValue(currentInputValue - (this.step() ?? 1) * multiplier));
1079
+ catch {
1080
+ this.isPointerLockDenied.set(true);
466
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})`;
467
1141
  }
468
1142
  }
469
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxNumberFieldRootDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
470
- static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.2.9", 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 }); }
471
1145
  }
472
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxNumberFieldRootDirective, decorators: [{
1146
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxNumberFieldScrubArea, decorators: [{
473
1147
  type: Directive,
474
1148
  args: [{
475
- selector: '[rdxNumberFieldRoot]',
476
- exportAs: 'rdxNumberFieldRoot',
477
- providers: [provideToken(NUMBER_FIELD_ROOT_CONTEXT, RdxNumberFieldRootDirective)],
1149
+ selector: '[rdxNumberFieldScrubArea]',
1150
+ exportAs: 'rdxNumberFieldScrubArea',
1151
+ providers: [provideNumberFieldScrubAreaContext(() => inject(RdxNumberFieldScrubArea).context)],
478
1152
  host: {
479
- role: 'group',
480
- '[attr.data-disabled]': 'disabled() ? "" : undefined',
481
- '[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)'
482
1162
  }
483
1163
  }]
484
- }], propDecorators: { value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }, { type: i0.Output, args: ["valueChange"] }], 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 }] }], stepSnapping: [{ type: i0.Input, args: [{ isSignal: true, alias: "stepSnapping", required: false }] }], disableWheelChange: [{ type: i0.Input, args: [{ isSignal: true, alias: "disableWheelChange", required: false }] }], locale: [{ type: i0.Input, args: [{ isSignal: true, alias: "locale", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], formatOptions: [{ type: i0.Input, args: [{ isSignal: true, alias: "formatOptions", required: false }] }], readonly: [{ type: i0.Input, args: [{ isSignal: true, alias: "readonly", required: false }] }] } });
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: () => [] });
485
1208
 
486
1209
  const _imports = [
487
- RdxNumberFieldRootDirective,
488
- RdxNumberFieldInputDirective,
489
- RdxNumberFieldIncrementDirective,
490
- RdxNumberFieldDecrementDirective
1210
+ RdxNumberFieldRoot,
1211
+ RdxNumberFieldGroup,
1212
+ RdxNumberFieldInput,
1213
+ RdxNumberFieldHiddenInput,
1214
+ RdxNumberFieldIncrement,
1215
+ RdxNumberFieldDecrement,
1216
+ RdxNumberFieldScrubArea,
1217
+ RdxNumberFieldScrubAreaCursor
491
1218
  ];
492
1219
  class RdxNumberFieldModule {
493
1220
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxNumberFieldModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); }
494
- static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "21.2.9", ngImport: i0, type: RdxNumberFieldModule, imports: [RdxNumberFieldRootDirective,
495
- RdxNumberFieldInputDirective,
496
- RdxNumberFieldIncrementDirective,
497
- RdxNumberFieldDecrementDirective], exports: [RdxNumberFieldRootDirective,
498
- RdxNumberFieldInputDirective,
499
- RdxNumberFieldIncrementDirective,
500
- RdxNumberFieldDecrementDirective] }); }
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] }); }
501
1236
  static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxNumberFieldModule }); }
502
1237
  }
503
1238
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxNumberFieldModule, decorators: [{
@@ -512,5 +1247,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
512
1247
  * Generated bundle index. Do not edit.
513
1248
  */
514
1249
 
515
- 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 };
516
1251
  //# sourceMappingURL=radix-ng-primitives-number-field.mjs.map