@box/blueprint-web 17.0.0 → 17.1.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.
@@ -6950,55 +6950,56 @@
6950
6950
  overflow:auto;
6951
6951
  }
6952
6952
 
6953
- .bp_data_table_module_dataTableWrapper--9a3ae{
6953
+ .bp_data_table_module_dataTableWrapper--c7392{
6954
6954
  --border-radius-datatable:0;
6955
6955
  --outline-offset-focus-and-selected-row-datatable:-0.25rem;
6956
6956
  --outline-select-color-datatable:#0000;
6957
6957
  --sticky-element-z-index:2;
6958
6958
  height:100%;
6959
6959
  overflow:auto;
6960
+ padding-block-end:var(--bp-space-020, .5rem);
6960
6961
  scroll-padding:60px 100px;
6961
6962
  width:100%;
6962
6963
  }
6963
- .bp_data_table_module_dataTableWrapper--9a3ae [data-modern=false]{
6964
+ .bp_data_table_module_dataTableWrapper--c7392 [data-modern=false]{
6964
6965
  --border-radius-hover-datatable:var(--radius-4);
6965
6966
  }
6966
- .bp_data_table_module_dataTableWrapper--9a3ae [data-modern=true]{
6967
+ .bp_data_table_module_dataTableWrapper--c7392 [data-modern=true]{
6967
6968
  --border-radius-hover-datatable:var(--bp-radius-08, 1rem);
6968
6969
  }
6969
- .bp_data_table_module_dataTableWrapper--9a3ae table tbody > tr[data-hovered]{
6970
+ .bp_data_table_module_dataTableWrapper--c7392 table tbody > tr[data-hovered]{
6970
6971
  --list-item-border-radius:var(--border-radius-hover-datatable);
6971
6972
  --list-item-row-border-radius:var(--list-item-border-radius);
6972
6973
  --list-item-first-column-item-radius:var(--list-item-border-radius) 0 0 var(--list-item-border-radius);
6973
6974
  --list-item-last-column-item-radius:0 var(--list-item-border-radius) var(--list-item-border-radius) 0;
6974
6975
  }
6975
- .bp_data_table_module_dataTableWrapper--9a3ae.bp_data_table_module_isScrolledX--9a3ae table tbody > tr[data-hovered][data-focus-visible]{
6976
+ .bp_data_table_module_dataTableWrapper--c7392.bp_data_table_module_isScrolledX--c7392 table tbody > tr[data-hovered][data-focus-visible]{
6976
6977
  --list-item-border-radius:0;
6977
6978
  --list-item-row-border-radius:0;
6978
6979
  --list-item-first-column-item-radius:0;
6979
6980
  --list-item-last-column-item-radius:0;
6980
6981
  }
6981
- .bp_data_table_module_dataTableWrapper--9a3ae table > thead > tr{
6982
+ .bp_data_table_module_dataTableWrapper--c7392 table > thead > tr{
6982
6983
  background:var(--gray-white);
6983
6984
  position:sticky;
6984
6985
  top:0;
6985
6986
  z-index:var(--sticky-element-z-index);
6986
6987
  }
6987
- .bp_data_table_module_dataTableWrapper--9a3ae table > thead > tr th:first-child{
6988
+ .bp_data_table_module_dataTableWrapper--c7392 table > thead > tr th:first-child{
6988
6989
  background:inherit;
6989
6990
  height:100%;
6990
6991
  left:0;
6991
6992
  position:sticky;
6992
6993
  z-index:var(--sticky-element-z-index);
6993
6994
  }
6994
- .bp_data_table_module_dataTableWrapper--9a3ae table > thead > tr th:first-child[data-focus-visible]{
6995
+ .bp_data_table_module_dataTableWrapper--c7392 table > thead > tr th:first-child[data-focus-visible]{
6995
6996
  position:sticky;
6996
6997
  z-index:var(--sticky-element-z-index);
6997
6998
  }
6998
- .bp_data_table_module_dataTableWrapper--9a3ae table tbody > tr{
6999
+ .bp_data_table_module_dataTableWrapper--c7392 table tbody > tr{
6999
7000
  --is-row-focused:0;
7000
7001
  }
7001
- .bp_data_table_module_dataTableWrapper--9a3ae table tbody > tr > td [data-actionswrapper]::after{
7002
+ .bp_data_table_module_dataTableWrapper--c7392 table tbody > tr > td [data-actionswrapper]::after{
7002
7003
  background:inherit;
7003
7004
  content:"";
7004
7005
  display:block;
@@ -7008,18 +7009,18 @@
7008
7009
  width:100%;
7009
7010
  z-index:-1;
7010
7011
  }
7011
- .bp_data_table_module_dataTableWrapper--9a3ae table tbody > tr[data-focus-visible]{
7012
+ .bp_data_table_module_dataTableWrapper--c7392 table tbody > tr[data-focus-visible]{
7012
7013
  --outline-offset-focus-cell:var(--outline-offset-focus-row);
7013
7014
  --is-row-focused:1;
7014
7015
  }
7015
- .bp_data_table_module_dataTableWrapper--9a3ae table tbody > tr[data-focus-visible][aria-selected=true]{
7016
+ .bp_data_table_module_dataTableWrapper--c7392 table tbody > tr[data-focus-visible][aria-selected=true]{
7016
7017
  --outline-offset-focus-cell:var(--outline-offset-focus-and-selected-row);
7017
7018
  }
7018
7019
 
7019
- .bp_data_table_module_isScrolledX--9a3ae table > thead > tr > th:first-child{
7020
+ .bp_data_table_module_isScrolledX--c7392 table > thead > tr > th:first-child{
7020
7021
  overflow:visible;
7021
7022
  }
7022
- .bp_data_table_module_isScrolledX--9a3ae table > thead > tr > th:first-child::after{
7023
+ .bp_data_table_module_isScrolledX--c7392 table > thead > tr > th:first-child::after{
7023
7024
  background:inherit;
7024
7025
  border-right:1px solid var(--border-divider-border);
7025
7026
  box-shadow:10px 0 7px -4px var(--item-background);
@@ -7033,7 +7034,7 @@
7033
7034
  z-index:-1;
7034
7035
  }
7035
7036
 
7036
- .bp_data_table_module_hideActionWrapperRight--9a3ae table tbody > tr > td [data-actionswrapper]::after{
7037
+ .bp_data_table_module_hideActionWrapperRight--c7392 table tbody > tr > td [data-actionswrapper]::after{
7037
7038
  display:none;
7038
7039
  }
7039
7040
  .bp_list_checkbox_module_selection--711bf[data-modern=false]{
@@ -1,4 +1,4 @@
1
1
  import '../index.css';
2
- var styles = {"dataTableWrapper":"bp_data_table_module_dataTableWrapper--9a3ae","isScrolledX":"bp_data_table_module_isScrolledX--9a3ae","hideActionWrapperRight":"bp_data_table_module_hideActionWrapperRight--9a3ae"};
2
+ var styles = {"dataTableWrapper":"bp_data_table_module_dataTableWrapper--c7392","isScrolledX":"bp_data_table_module_isScrolledX--c7392","hideActionWrapperRight":"bp_data_table_module_hideActionWrapperRight--c7392"};
3
3
 
4
4
  export { styles as default };
@@ -6950,55 +6950,56 @@
6950
6950
  overflow:auto;
6951
6951
  }
6952
6952
 
6953
- .bp_data_table_module_dataTableWrapper--9a3ae{
6953
+ .bp_data_table_module_dataTableWrapper--c7392{
6954
6954
  --border-radius-datatable:0;
6955
6955
  --outline-offset-focus-and-selected-row-datatable:-0.25rem;
6956
6956
  --outline-select-color-datatable:#0000;
6957
6957
  --sticky-element-z-index:2;
6958
6958
  height:100%;
6959
6959
  overflow:auto;
6960
+ padding-block-end:var(--bp-space-020, .5rem);
6960
6961
  scroll-padding:60px 100px;
6961
6962
  width:100%;
6962
6963
  }
6963
- .bp_data_table_module_dataTableWrapper--9a3ae [data-modern=false]{
6964
+ .bp_data_table_module_dataTableWrapper--c7392 [data-modern=false]{
6964
6965
  --border-radius-hover-datatable:var(--radius-4);
6965
6966
  }
6966
- .bp_data_table_module_dataTableWrapper--9a3ae [data-modern=true]{
6967
+ .bp_data_table_module_dataTableWrapper--c7392 [data-modern=true]{
6967
6968
  --border-radius-hover-datatable:var(--bp-radius-08, 1rem);
6968
6969
  }
6969
- .bp_data_table_module_dataTableWrapper--9a3ae table tbody > tr[data-hovered]{
6970
+ .bp_data_table_module_dataTableWrapper--c7392 table tbody > tr[data-hovered]{
6970
6971
  --list-item-border-radius:var(--border-radius-hover-datatable);
6971
6972
  --list-item-row-border-radius:var(--list-item-border-radius);
6972
6973
  --list-item-first-column-item-radius:var(--list-item-border-radius) 0 0 var(--list-item-border-radius);
6973
6974
  --list-item-last-column-item-radius:0 var(--list-item-border-radius) var(--list-item-border-radius) 0;
6974
6975
  }
6975
- .bp_data_table_module_dataTableWrapper--9a3ae.bp_data_table_module_isScrolledX--9a3ae table tbody > tr[data-hovered][data-focus-visible]{
6976
+ .bp_data_table_module_dataTableWrapper--c7392.bp_data_table_module_isScrolledX--c7392 table tbody > tr[data-hovered][data-focus-visible]{
6976
6977
  --list-item-border-radius:0;
6977
6978
  --list-item-row-border-radius:0;
6978
6979
  --list-item-first-column-item-radius:0;
6979
6980
  --list-item-last-column-item-radius:0;
6980
6981
  }
6981
- .bp_data_table_module_dataTableWrapper--9a3ae table > thead > tr{
6982
+ .bp_data_table_module_dataTableWrapper--c7392 table > thead > tr{
6982
6983
  background:var(--gray-white);
6983
6984
  position:sticky;
6984
6985
  top:0;
6985
6986
  z-index:var(--sticky-element-z-index);
6986
6987
  }
6987
- .bp_data_table_module_dataTableWrapper--9a3ae table > thead > tr th:first-child{
6988
+ .bp_data_table_module_dataTableWrapper--c7392 table > thead > tr th:first-child{
6988
6989
  background:inherit;
6989
6990
  height:100%;
6990
6991
  left:0;
6991
6992
  position:sticky;
6992
6993
  z-index:var(--sticky-element-z-index);
6993
6994
  }
6994
- .bp_data_table_module_dataTableWrapper--9a3ae table > thead > tr th:first-child[data-focus-visible]{
6995
+ .bp_data_table_module_dataTableWrapper--c7392 table > thead > tr th:first-child[data-focus-visible]{
6995
6996
  position:sticky;
6996
6997
  z-index:var(--sticky-element-z-index);
6997
6998
  }
6998
- .bp_data_table_module_dataTableWrapper--9a3ae table tbody > tr{
6999
+ .bp_data_table_module_dataTableWrapper--c7392 table tbody > tr{
6999
7000
  --is-row-focused:0;
7000
7001
  }
7001
- .bp_data_table_module_dataTableWrapper--9a3ae table tbody > tr > td [data-actionswrapper]::after{
7002
+ .bp_data_table_module_dataTableWrapper--c7392 table tbody > tr > td [data-actionswrapper]::after{
7002
7003
  background:inherit;
7003
7004
  content:"";
7004
7005
  display:block;
@@ -7008,18 +7009,18 @@
7008
7009
  width:100%;
7009
7010
  z-index:-1;
7010
7011
  }
7011
- .bp_data_table_module_dataTableWrapper--9a3ae table tbody > tr[data-focus-visible]{
7012
+ .bp_data_table_module_dataTableWrapper--c7392 table tbody > tr[data-focus-visible]{
7012
7013
  --outline-offset-focus-cell:var(--outline-offset-focus-row);
7013
7014
  --is-row-focused:1;
7014
7015
  }
7015
- .bp_data_table_module_dataTableWrapper--9a3ae table tbody > tr[data-focus-visible][aria-selected=true]{
7016
+ .bp_data_table_module_dataTableWrapper--c7392 table tbody > tr[data-focus-visible][aria-selected=true]{
7016
7017
  --outline-offset-focus-cell:var(--outline-offset-focus-and-selected-row);
7017
7018
  }
7018
7019
 
7019
- .bp_data_table_module_isScrolledX--9a3ae table > thead > tr > th:first-child{
7020
+ .bp_data_table_module_isScrolledX--c7392 table > thead > tr > th:first-child{
7020
7021
  overflow:visible;
7021
7022
  }
7022
- .bp_data_table_module_isScrolledX--9a3ae table > thead > tr > th:first-child::after{
7023
+ .bp_data_table_module_isScrolledX--c7392 table > thead > tr > th:first-child::after{
7023
7024
  background:inherit;
7024
7025
  border-right:1px solid var(--border-divider-border);
7025
7026
  box-shadow:10px 0 7px -4px var(--item-background);
@@ -7033,7 +7034,7 @@
7033
7034
  z-index:-1;
7034
7035
  }
7035
7036
 
7036
- .bp_data_table_module_hideActionWrapperRight--9a3ae table tbody > tr > td [data-actionswrapper]::after{
7037
+ .bp_data_table_module_hideActionWrapperRight--c7392 table tbody > tr > td [data-actionswrapper]::after{
7037
7038
  display:none;
7038
7039
  }
7039
7040
  .bp_list_checkbox_module_selection--711bf[data-modern=false]{
@@ -1,5 +1,5 @@
1
- /** Three digits, the width the design spec draws the control at. */
2
- declare const NUMBER_FIELD_DEFAULT_MAX = 999;
1
+ /** How many digits wide the control is when `maxLength` is omitted and the field is unconstrained. */
2
+ declare const NUMBER_FIELD_DEFAULT_DIGITS = 3;
3
3
  /** Non-negative integers only. Also governs what react-aria lets through, keeping the field digits-only. */
4
4
  declare const NUMBER_FIELD_FORMAT_OPTIONS: Intl.NumberFormatOptions;
5
- export { NUMBER_FIELD_DEFAULT_MAX, NUMBER_FIELD_FORMAT_OPTIONS };
5
+ export { NUMBER_FIELD_DEFAULT_DIGITS, NUMBER_FIELD_FORMAT_OPTIONS };
@@ -1,9 +1,9 @@
1
- /** Three digits, the width the design spec draws the control at. */
2
- const NUMBER_FIELD_DEFAULT_MAX = 999;
1
+ /** How many digits wide the control is when `maxLength` is omitted and the field is unconstrained. */
2
+ const NUMBER_FIELD_DEFAULT_DIGITS = 3;
3
3
  /** Non-negative integers only. Also governs what react-aria lets through, keeping the field digits-only. */
4
4
  const NUMBER_FIELD_FORMAT_OPTIONS = {
5
5
  maximumFractionDigits: 0,
6
6
  useGrouping: false
7
7
  };
8
8
 
9
- export { NUMBER_FIELD_DEFAULT_MAX, NUMBER_FIELD_FORMAT_OPTIONS };
9
+ export { NUMBER_FIELD_DEFAULT_DIGITS, NUMBER_FIELD_FORMAT_OPTIONS };
@@ -1,11 +1,12 @@
1
1
  import { type ComponentPropsWithoutRef, type ForwardedRef } from 'react';
2
+ import { type NumberFieldProps } from './types';
2
3
  type NumberFieldControlProps = {
3
- ariaValueMax: number;
4
- ariaValueMin: number;
4
+ ariaValueMax?: number;
5
+ ariaValueMin?: number;
5
6
  decrementAriaLabel: string;
6
7
  hasError: boolean;
7
8
  incrementAriaLabel: string;
8
- inputLength: number;
9
+ inputMaxLength?: number;
9
10
  inputProps: ComponentPropsWithoutRef<'input'>;
10
11
  inputRef: ForwardedRef<HTMLInputElement>;
11
12
  isAnimationEnabled: boolean;
@@ -17,8 +18,10 @@ type NumberFieldControlProps = {
17
18
  loadingAriaLabel?: string;
18
19
  maxReachedTooltipContent?: string;
19
20
  minReachedTooltipContent?: string;
21
+ onBlur?: NumberFieldProps['onBlur'];
22
+ onInputChange?: (rawValue: string) => void;
20
23
  showDecrementTooltip: boolean;
21
24
  showIncrementTooltip: boolean;
22
25
  };
23
- export declare const NumberFieldControl: ({ ariaValueMax, ariaValueMin, decrementAriaLabel, hasError, incrementAriaLabel, inputLength, inputProps, inputRef, isAnimationEnabled, isDecrementDisabled, isDecrementLoading, isIncrementDisabled, isIncrementLoading, labelId, loadingAriaLabel, maxReachedTooltipContent, minReachedTooltipContent, showDecrementTooltip, showIncrementTooltip, }: NumberFieldControlProps) => React.ReactElement;
26
+ export declare const NumberFieldControl: ({ ariaValueMax, ariaValueMin, decrementAriaLabel, hasError, incrementAriaLabel, inputMaxLength, inputProps, inputRef, isAnimationEnabled, isDecrementDisabled, isDecrementLoading, isIncrementDisabled, isIncrementLoading, labelId, loadingAriaLabel, maxReachedTooltipContent, minReachedTooltipContent, onBlur, onInputChange, showDecrementTooltip, showIncrementTooltip, }: NumberFieldControlProps) => React.ReactElement;
24
27
  export {};
@@ -3,6 +3,7 @@ import { Minus, Plus } from '@box/blueprint-web-assets/icons/Medium';
3
3
  import { useContext } from 'react';
4
4
  import { NumberFieldStateContext, useContextProps, InputContext, Group } from 'react-aria-components';
5
5
  import { BaseTextInput } from '../primitives/base-text-input/base-text-input.js';
6
+ import { composeEventHandlers } from '../utils/composeEventHandlers.js';
6
7
  import { StepperButton } from './stepper-button.js';
7
8
  import styles from './number-field.module.js';
8
9
 
@@ -12,7 +13,7 @@ const NumberFieldControl = ({
12
13
  decrementAriaLabel,
13
14
  hasError,
14
15
  incrementAriaLabel,
15
- inputLength,
16
+ inputMaxLength,
16
17
  inputProps,
17
18
  inputRef,
18
19
  isAnimationEnabled,
@@ -24,13 +25,23 @@ const NumberFieldControl = ({
24
25
  loadingAriaLabel,
25
26
  maxReachedTooltipContent,
26
27
  minReachedTooltipContent,
28
+ onBlur,
29
+ onInputChange,
27
30
  showDecrementTooltip,
28
31
  showIncrementTooltip
29
32
  }) => {
30
33
  const state = useContext(NumberFieldStateContext);
31
- const [racInputProps, racInputRef] = useContextProps({
32
- maxLength: inputLength
33
- }, inputRef, InputContext);
34
+ const racInputOverrides = inputMaxLength !== undefined ? {
35
+ maxLength: inputMaxLength
36
+ } : {};
37
+ const [racInputProps, racInputRef] = useContextProps(racInputOverrides, inputRef, InputContext);
38
+ // RAC `style` can be a render function; BaseTextInput only accepts CSSProperties.
39
+ const {
40
+ onBlur: racOnBlur,
41
+ onInput: racOnInput,
42
+ style: _racStyle,
43
+ ...racDomProps
44
+ } = racInputProps;
34
45
  if (!state) {
35
46
  throw new Error('NumberFieldControl must be rendered inside NumberField');
36
47
  }
@@ -42,6 +53,9 @@ const NumberFieldControl = ({
42
53
  loading: true,
43
54
  loadingAriaLabel
44
55
  } : {};
56
+ const handleInput = event => {
57
+ onInputChange?.(event.currentTarget.value);
58
+ };
45
59
  return jsxs(Group, {
46
60
  "aria-labelledby": labelId,
47
61
  className: styles.control,
@@ -56,7 +70,7 @@ const NumberFieldControl = ({
56
70
  ...decrementLoading
57
71
  }), jsx(BaseTextInput, {
58
72
  ...inputProps,
59
- ...racInputProps,
73
+ ...racDomProps,
60
74
  ref: racInputRef,
61
75
  "aria-valuemax": ariaValueMax,
62
76
  "aria-valuemin": ariaValueMin,
@@ -64,6 +78,8 @@ const NumberFieldControl = ({
64
78
  "data-bp-animated": isAnimationEnabled ? 'true' : 'false',
65
79
  invalid: hasError,
66
80
  label: "",
81
+ onBlur: composeEventHandlers(racOnBlur, onBlur),
82
+ onInput: composeEventHandlers(racOnInput, handleInput),
67
83
  // Pinned to the `text` react-aria already sets, since it types this as any input type.
68
84
  type: "text"
69
85
  }), jsx(StepperButton, {
@@ -5,5 +5,10 @@ import { type NumberFieldProps } from './types';
5
5
  * Formats non-negative whole numbers out of the box; `formatOptions` opens that up to decimals, currency
6
6
  * and percent, and doubles as the rule for what may be typed. The control is sized to hold `maxLength`
7
7
  * characters and does not grow or shrink with its container.
8
+ *
9
+ * Unconstrained by default: no clamp, no implied stepper bounds. Pass `clampToRange` with both
10
+ * `min` and `max` for library-owned range behavior (omitting either bound leaves the field
11
+ * unconstrained); or drive steppers yourself with `isIncrementDisabled` / `isDecrementDisabled`
12
+ * and validate via `onChange` / `onBlur` / `onInputChange`.
8
13
  */
9
14
  export declare const NumberField: import("react").ForwardRefExoticComponent<NumberFieldProps & import("react").RefAttributes<HTMLInputElement>>;
@@ -9,7 +9,7 @@ import { InlineError } from '../primitives/inline-error/inline-error.js';
9
9
  import { Text } from '../text/text.js';
10
10
  import { useLabelable } from '../util-components/labelable/useLabelable.js';
11
11
  import { useUniqueId } from '../utils/useUniqueId.js';
12
- import { NUMBER_FIELD_FORMAT_OPTIONS, NUMBER_FIELD_DEFAULT_MAX } from './constants.js';
12
+ import { NUMBER_FIELD_FORMAT_OPTIONS, NUMBER_FIELD_DEFAULT_DIGITS } from './constants.js';
13
13
  import { NumberFieldControl } from './number-field-control.js';
14
14
  import styles from './number-field.module.js';
15
15
 
@@ -19,9 +19,15 @@ import styles from './number-field.module.js';
19
19
  * Formats non-negative whole numbers out of the box; `formatOptions` opens that up to decimals, currency
20
20
  * and percent, and doubles as the rule for what may be typed. The control is sized to hold `maxLength`
21
21
  * characters and does not grow or shrink with its container.
22
+ *
23
+ * Unconstrained by default: no clamp, no implied stepper bounds. Pass `clampToRange` with both
24
+ * `min` and `max` for library-owned range behavior (omitting either bound leaves the field
25
+ * unconstrained); or drive steppers yourself with `isIncrementDisabled` / `isDecrementDisabled`
26
+ * and validate via `onChange` / `onBlur` / `onInputChange`.
22
27
  */
23
28
  const NumberField = /*#__PURE__*/forwardRef((props, forwardedRef) => {
24
29
  const {
30
+ clampToRange = false,
25
31
  className,
26
32
  decrementAriaLabel,
27
33
  disabled = false,
@@ -31,16 +37,20 @@ const NumberField = /*#__PURE__*/forwardRef((props, forwardedRef) => {
31
37
  hideLabel = false,
32
38
  id,
33
39
  incrementAriaLabel,
40
+ isDecrementDisabled = false,
41
+ isIncrementDisabled = false,
34
42
  label,
35
43
  loading = false,
36
44
  loadingAriaLabel,
37
- max = NUMBER_FIELD_DEFAULT_MAX,
45
+ max,
38
46
  maxLength,
39
47
  maxReachedTooltipContent,
40
- min = 0,
48
+ min,
41
49
  minReachedTooltipContent,
42
50
  name,
51
+ onBlur,
43
52
  onChange,
53
+ onInputChange,
44
54
  readOnly = false,
45
55
  required = false,
46
56
  step = 1,
@@ -62,8 +72,15 @@ const NumberField = /*#__PURE__*/forwardRef((props, forwardedRef) => {
62
72
  // InlineError alone would leave an inconsistent a11y state.
63
73
  const hasError = !!error && !disabled && !isAnyLoading;
64
74
  const hasSubtext = !hasError && !!subtext;
65
- // Also sizes the control, handed to the stylesheet as `--number-field-digits` below.
66
- const inputLength = maxLength ?? String(max).length;
75
+ const shouldClamp = clampToRange && min !== undefined && max !== undefined;
76
+ const atMin = shouldClamp && value <= min;
77
+ const atMax = shouldClamp && value >= max;
78
+ const decrementBlocked = isDecrementDisabled || atMin;
79
+ const incrementBlocked = isIncrementDisabled || atMax;
80
+ // Width is always set. Native maxLength is only a real limit: an explicit prop, or the digits of
81
+ // `max` when clamping. Unconstrained fields stay three digits wide without capping typing.
82
+ const inputLength = maxLength ?? (shouldClamp && max !== undefined ? String(max).length : NUMBER_FIELD_DEFAULT_DIGITS);
83
+ const inputMaxLength = maxLength ?? (shouldClamp ? inputLength : undefined);
67
84
  const Label = useLabelable(label, inputId, required);
68
85
  // Mirrors TextInput
69
86
  const {
@@ -85,8 +102,8 @@ const NumberField = /*#__PURE__*/forwardRef((props, forwardedRef) => {
85
102
  isInvalid: hasError,
86
103
  isReadOnly: readOnly,
87
104
  isRequired: required,
88
- maxValue: max,
89
- minValue: min,
105
+ maxValue: shouldClamp ? max : undefined,
106
+ minValue: shouldClamp ? min : undefined,
90
107
  name: name,
91
108
  onChange: onChange,
92
109
  step: step,
@@ -107,20 +124,22 @@ const NumberField = /*#__PURE__*/forwardRef((props, forwardedRef) => {
107
124
  decrementAriaLabel: decrementAriaLabel,
108
125
  hasError: hasError,
109
126
  incrementAriaLabel: incrementAriaLabel,
110
- inputLength: inputLength,
127
+ inputMaxLength: inputMaxLength,
111
128
  inputProps: rest,
112
129
  inputRef: forwardedRef,
113
130
  isAnimationEnabled: isAnimationEnabled,
114
- isDecrementDisabled: !isInteractive || value <= min || isAnyLoading && !isDecrementLoading,
131
+ isDecrementDisabled: !isInteractive || decrementBlocked || isAnyLoading && !isDecrementLoading,
115
132
  isDecrementLoading: isDecrementLoading,
116
- isIncrementDisabled: !isInteractive || value >= max || isAnyLoading && !isIncrementLoading,
133
+ isIncrementDisabled: !isInteractive || incrementBlocked || isAnyLoading && !isIncrementLoading,
117
134
  isIncrementLoading: isIncrementLoading,
118
135
  labelId: labelId,
119
136
  loadingAriaLabel: loadingAriaLabel,
120
137
  maxReachedTooltipContent: maxReachedTooltipContent,
121
138
  minReachedTooltipContent: minReachedTooltipContent,
122
- showDecrementTooltip: isInteractive && !isAnyLoading && value <= min,
123
- showIncrementTooltip: isInteractive && !isAnyLoading && value >= max
139
+ onBlur: onBlur,
140
+ onInputChange: onInputChange,
141
+ showDecrementTooltip: isInteractive && !isAnyLoading && decrementBlocked && !!minReachedTooltipContent,
142
+ showIncrementTooltip: isInteractive && !isAnyLoading && incrementBlocked && !!maxReachedTooltipContent
124
143
  }), hasError ? jsx(InlineError, {
125
144
  className: styles.inlineError,
126
145
  id: inlineErrorId,
@@ -13,9 +13,5 @@ type StepperButtonProps = {
13
13
  showTooltip: boolean;
14
14
  tooltipContent?: string;
15
15
  } & StepperLoadingProps;
16
- /**
17
- * With a boundary tooltip to show, `disabled` gives way to `aria-disabled` so the button stays focusable
18
- * and hoverable enough to surface it.
19
- */
20
16
  export declare const StepperButton: ({ ariaLabel, className, disabled, icon, onClick, showTooltip, tooltipContent, ...loadingProps }: StepperButtonProps) => React.ReactElement;
21
17
  export type { StepperLoadingProps };
@@ -2,10 +2,6 @@ import { jsx } from 'react/jsx-runtime';
2
2
  import { Button } from '../button/button.js';
3
3
  import { Tooltip } from '../tooltip/tooltip.js';
4
4
 
5
- /**
6
- * With a boundary tooltip to show, `disabled` gives way to `aria-disabled` so the button stays focusable
7
- * and hoverable enough to surface it.
8
- */
9
5
  const StepperButton = ({
10
6
  ariaLabel,
11
7
  className,
@@ -16,10 +12,9 @@ const StepperButton = ({
16
12
  tooltipContent,
17
13
  ...loadingProps
18
14
  }) => {
19
- const tooltip = showTooltip ? tooltipContent : undefined;
20
15
  const button = jsx(Button, {
21
16
  ...loadingProps,
22
- accessibleWhenDisabled: !!tooltip,
17
+ accessibleWhenDisabled: showTooltip,
23
18
  "aria-label": ariaLabel,
24
19
  className: className,
25
20
  disabled: disabled,
@@ -29,13 +24,14 @@ const StepperButton = ({
29
24
  type: "button",
30
25
  variant: "secondary"
31
26
  });
32
- if (tooltip) {
33
- return jsx(Tooltip, {
34
- content: tooltip,
35
- children: button
36
- });
27
+ if (!tooltipContent) {
28
+ return button;
37
29
  }
38
- return button;
30
+ return jsx(Tooltip, {
31
+ content: tooltipContent,
32
+ open: showTooltip ? undefined : false,
33
+ children: button
34
+ });
39
35
  };
40
36
 
41
37
  export { StepperButton };
@@ -1,5 +1,6 @@
1
1
  import { type ComponentPropsWithoutRef, type ReactNode } from 'react';
2
2
  import { type RequireAllOrNone } from 'type-fest';
3
+ import { type BaseTextInputProps } from '../primitives/base-text-input';
3
4
  import { type Labelable } from '../util-components/labelable';
4
5
  export interface Loading {
5
6
  /**
@@ -14,12 +15,25 @@ export interface Loading {
14
15
  * Attributes NumberField sets from its own props, so they cannot be forwarded. `size` is included because
15
16
  * the control takes its width from `maxLength`.
16
17
  */
17
- type ReservedInputAttributes = 'defaultValue' | 'disabled' | 'inputMode' | 'max' | 'min' | 'onBlur' | 'onChange' | 'onKeyDown' | 'pattern' | 'readOnly' | 'required' | 'size' | 'step' | 'type' | 'value';
18
+ type ReservedInputAttributes = 'defaultValue' | 'disabled' | 'inputMode' | 'max' | 'min' | 'onBlur' | 'onChange' | 'onInput' | 'onKeyDown' | 'pattern' | 'readOnly' | 'required' | 'size' | 'step' | 'type' | 'value';
18
19
  /**
19
20
  * Remaining props are forwarded to the `<input>`, so the field takes part in native form submission.
20
21
  * `className` styles the wrapper holding the label, the control and the subtext.
21
22
  */
22
23
  interface NumberFieldBaseProps extends Omit<ComponentPropsWithoutRef<'input'>, ReservedInputAttributes>, Labelable {
24
+ /**
25
+ * When true **and both `min` and `max` are set**, typed commits and controlled values are clamped
26
+ * into `[min, max]` via RAC, and steppers are disabled at the bounds.
27
+ *
28
+ * If either bound is omitted, this prop is a no-op — the field stays unconstrained (same as the
29
+ * default). Example: `clampToRange min={1} max={10}`.
30
+ *
31
+ * Without this (the default), the consumer owns validation and stepper bounds via `onChange` /
32
+ * `onBlur` / `isIncrementDisabled` / `isDecrementDisabled`.
33
+ *
34
+ * @default false
35
+ */
36
+ clampToRange?: boolean;
23
37
  /** Accessible label for the decrement button. */
24
38
  decrementAriaLabel: string;
25
39
  /**
@@ -45,33 +59,53 @@ interface NumberFieldBaseProps extends Omit<ComponentPropsWithoutRef<'input'>, R
45
59
  hideLabel?: boolean;
46
60
  /** Accessible label for the increment button. */
47
61
  incrementAriaLabel: string;
62
+ /**
63
+ * When true, disables the decrement stepper. Use this to own stepper bounds when `clampToRange` is
64
+ * off (e.g. `value <= min`). Combined with `clampToRange`, also disables at `min`.
65
+ */
66
+ isDecrementDisabled?: boolean;
67
+ /**
68
+ * When true, disables the increment stepper. Use this to own stepper bounds when `clampToRange` is
69
+ * off (e.g. `value >= max`). Combined with `clampToRange`, also disables at `max`.
70
+ */
71
+ isIncrementDisabled?: boolean;
48
72
  /** The field label rendered above the control. */
49
73
  label: Labelable['label'];
50
74
  /**
51
- * The upper bound. Disables the increment button once reached, shows `maxReachedTooltipContent`, and
52
- * snaps typed commits down to this value.
53
- *
54
- * @default 999
75
+ * Upper bound hint: exposed as `aria-valuemax` and used for `maxReachedTooltipContent`. Only drives
76
+ * RAC clamp / auto-disabled steppers, and the default `maxLength`, when `clampToRange` is true
77
+ * **and `min` is also set**; otherwise it is a hint only.
55
78
  */
56
79
  max?: number;
57
80
  /**
58
- * How many characters the input accepts and is sized to hold. Defaults to the digits of `max`; set it
59
- * when `formatOptions` needs room for a separator or a sign.
81
+ * How many characters the input accepts. Always wins when set, and also sizes the control.
82
+ *
83
+ * When omitted: the digits of `max` if `clampToRange` is on with both bounds (width and typing
84
+ * cap). Unconstrained fields stay three digits wide and do not cap typing.
60
85
  */
61
86
  maxLength?: number;
62
- /** Localized tooltip content shown when the increment button is disabled because the value is at max. */
87
+ /** Localized tooltip content shown when the increment button is disabled. */
63
88
  maxReachedTooltipContent?: string;
64
89
  /**
65
- * The lower bound. Disables the decrement button once reached, shows `minReachedTooltipContent`, and
66
- * snaps typed commits up to this value.
67
- *
68
- * @default 0
90
+ * Lower bound hint: exposed as `aria-valuemin` and used for `minReachedTooltipContent`. Only drives
91
+ * RAC clamp / auto-disabled steppers when `clampToRange` is true **and `max` is also set**;
92
+ * otherwise it is a hint only.
69
93
  */
70
94
  min?: number;
71
- /** Localized tooltip content shown when the decrement button is disabled because the value is at min. */
95
+ /** Localized tooltip content shown when the decrement button is disabled. */
72
96
  minReachedTooltipContent?: string;
97
+ /**
98
+ * Called when the input loses focus. Fires when tabbing or clicking away, including when moving
99
+ * from the input to a stepper.
100
+ */
101
+ onBlur?: BaseTextInputProps['onBlur'];
73
102
  /** Called with the new value when a stepper is used, or when the typed draft is committed on blur or Enter. */
74
103
  onChange: (value: number) => void;
104
+ /**
105
+ * Called with the raw input string as the user types, before the draft is committed via `onChange`.
106
+ * Use this for free-typed intermediate values (e.g. keeping a below-min draft visible).
107
+ */
108
+ onInputChange?: (rawValue: string) => void;
75
109
  /**
76
110
  * When true, the value is displayed but cannot be edited and the stepper buttons are inactive.
77
111
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@box/blueprint-web",
3
- "version": "17.0.0",
3
+ "version": "17.1.0",
4
4
  "type": "module",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "publishConfig": {
@@ -55,7 +55,7 @@
55
55
  "dependencies": {
56
56
  "@ariakit/react": "0.4.21",
57
57
  "@ariakit/react-core": "0.4.21",
58
- "@box/blueprint-web-assets": "^5.7.15",
58
+ "@box/blueprint-web-assets": "^5.7.17",
59
59
  "@internationalized/date": "^3.12.0",
60
60
  "@radix-ui/react-accordion": "1.1.2",
61
61
  "@radix-ui/react-checkbox": "1.0.4",
@@ -86,7 +86,7 @@
86
86
  "devDependencies": {
87
87
  "@box/box-test-client": "^3.2.7",
88
88
  "@box/playwright-utils": "^2.8.5",
89
- "@box/storybook-utils": "^1.2.24",
89
+ "@box/storybook-utils": "^1.2.26",
90
90
  "@figma/code-connect": "1.4.4",
91
91
  "@playwright/test": "1.46.1",
92
92
  "@types/js-yaml": "^4.0.9",