@bspk/ui 1.3.9 → 1.3.11

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 (147) hide show
  1. package/dist/components/BannerAlert/BannerAlert.d.ts +5 -5
  2. package/dist/components/BannerAlert/BannerAlert.js +5 -5
  3. package/dist/components/Breadcrumb/BreadcrumbDropdown.d.ts +6 -0
  4. package/dist/components/Breadcrumb/BreadcrumbDropdown.js +6 -0
  5. package/dist/components/Breadcrumb/BreadcrumbDropdown.js.map +1 -1
  6. package/dist/components/CheckboxGroup/CheckboxGroupExample.js +1 -0
  7. package/dist/components/CheckboxGroup/CheckboxGroupExample.js.map +1 -1
  8. package/dist/components/ChipGroup/ChipGroup.d.ts +15 -28
  9. package/dist/components/ChipGroup/ChipGroup.js +12 -22
  10. package/dist/components/ChipGroup/ChipGroup.js.map +1 -1
  11. package/dist/components/ChipGroup/ChipGroupExample.js +61 -6
  12. package/dist/components/ChipGroup/ChipGroupExample.js.map +1 -1
  13. package/dist/components/ChipGroup/chip-group.css +5 -3
  14. package/dist/components/ChipGroup/chip-group.css.js +5 -3
  15. package/dist/components/Drawer/Drawer.js.map +1 -1
  16. package/dist/components/Field/FieldDescription.d.ts +7 -5
  17. package/dist/components/Field/FieldDescription.js +7 -3
  18. package/dist/components/Field/FieldDescription.js.map +1 -1
  19. package/dist/components/Field/FieldError.d.ts +6 -0
  20. package/dist/components/Field/FieldError.js +6 -0
  21. package/dist/components/Field/FieldError.js.map +1 -1
  22. package/dist/components/Field/FieldLabel.d.ts +6 -0
  23. package/dist/components/Field/FieldLabel.js +6 -0
  24. package/dist/components/Field/FieldLabel.js.map +1 -1
  25. package/dist/components/Field/utils.d.ts +5 -0
  26. package/dist/components/Field/utils.js +5 -0
  27. package/dist/components/Field/utils.js.map +1 -1
  28. package/dist/components/InlineAlert/SvgWarningTwoTone.d.ts +6 -0
  29. package/dist/components/InlineAlert/SvgWarningTwoTone.js +6 -0
  30. package/dist/components/InlineAlert/SvgWarningTwoTone.js.map +1 -1
  31. package/dist/components/InputNumber/IncrementButton.d.ts +13 -3
  32. package/dist/components/InputNumber/IncrementButton.js +11 -4
  33. package/dist/components/InputNumber/IncrementButton.js.map +1 -1
  34. package/dist/components/InputNumber/InputNumber.js +26 -10
  35. package/dist/components/InputNumber/InputNumber.js.map +1 -1
  36. package/dist/components/InputNumber/InputNumberExample.js +1 -0
  37. package/dist/components/InputNumber/InputNumberExample.js.map +1 -1
  38. package/dist/components/InputNumber/input-number.css +6 -0
  39. package/dist/components/InputNumber/input-number.css.js +6 -0
  40. package/dist/components/Link/Link.d.ts +1 -1
  41. package/dist/components/Link/Link.js +1 -1
  42. package/dist/components/OTPInput/OTPInput.d.ts +13 -3
  43. package/dist/components/OTPInput/OTPInput.js +11 -39
  44. package/dist/components/OTPInput/OTPInput.js.map +1 -1
  45. package/dist/components/OTPInput/OTPInputExample.js +6 -1
  46. package/dist/components/OTPInput/OTPInputExample.js.map +1 -1
  47. package/dist/components/OTPInput/otp-input.css +18 -17
  48. package/dist/components/OTPInput/otp-input.css.js +18 -17
  49. package/dist/components/Pagination/PageList.d.ts +6 -0
  50. package/dist/components/Pagination/PageList.js +6 -0
  51. package/dist/components/Pagination/PageList.js.map +1 -1
  52. package/dist/components/Scrim/Scrim.d.ts +0 -1
  53. package/dist/components/Scrim/Scrim.js +0 -1
  54. package/dist/components/Scrim/Scrim.js.map +1 -1
  55. package/dist/components/Select/Select.d.ts +11 -11
  56. package/dist/components/Select/Select.js +11 -11
  57. package/dist/components/Skeleton/Circular.d.ts +6 -0
  58. package/dist/components/Skeleton/Circular.js +6 -0
  59. package/dist/components/Skeleton/Circular.js.map +1 -1
  60. package/dist/components/Skeleton/Photo.d.ts +6 -0
  61. package/dist/components/Skeleton/Photo.js +6 -0
  62. package/dist/components/Skeleton/Photo.js.map +1 -1
  63. package/dist/components/Skeleton/Profile.d.ts +6 -0
  64. package/dist/components/Skeleton/Profile.js +6 -0
  65. package/dist/components/Skeleton/Profile.js.map +1 -1
  66. package/dist/components/Skeleton/Rectangular.d.ts +6 -0
  67. package/dist/components/Skeleton/Rectangular.js +6 -0
  68. package/dist/components/Skeleton/Rectangular.js.map +1 -1
  69. package/dist/components/Skeleton/Thumbnail.d.ts +6 -0
  70. package/dist/components/Skeleton/Thumbnail.js +6 -0
  71. package/dist/components/Skeleton/Thumbnail.js.map +1 -1
  72. package/dist/components/Slider/SliderIntervalDots.d.ts +6 -0
  73. package/dist/components/Slider/SliderIntervalDots.js +6 -0
  74. package/dist/components/Slider/SliderIntervalDots.js.map +1 -1
  75. package/dist/components/Snackbar/Manager.d.ts +0 -1
  76. package/dist/components/Snackbar/Manager.js +0 -1
  77. package/dist/components/Snackbar/Manager.js.map +1 -1
  78. package/dist/components/Snackbar/Snackbar.d.ts +0 -1
  79. package/dist/components/Snackbar/Snackbar.js +0 -1
  80. package/dist/components/Snackbar/Snackbar.js.map +1 -1
  81. package/dist/components/TabList/TabList.js +1 -2
  82. package/dist/components/TabList/TabList.js.map +1 -1
  83. package/dist/components/Table/Footer.d.ts +6 -0
  84. package/dist/components/Table/Footer.js +6 -0
  85. package/dist/components/Table/Footer.js.map +1 -1
  86. package/dist/components/TimePicker/Listbox.d.ts +6 -0
  87. package/dist/components/TimePicker/Listbox.js +6 -0
  88. package/dist/components/TimePicker/Listbox.js.map +1 -1
  89. package/dist/components/TimePicker/Segment.d.ts +6 -0
  90. package/dist/components/TimePicker/Segment.js +6 -0
  91. package/dist/components/TimePicker/Segment.js.map +1 -1
  92. package/dist/components/Truncated/Truncated.d.ts +0 -1
  93. package/dist/components/Truncated/Truncated.js +1 -2
  94. package/dist/components/Truncated/Truncated.js.map +1 -1
  95. package/dist/components/UIProvider/UIProvider.d.ts +0 -1
  96. package/dist/components/UIProvider/UIProvider.js +0 -1
  97. package/dist/components/UIProvider/UIProvider.js.map +1 -1
  98. package/dist/hooks/useLongPress.d.ts +30 -15
  99. package/dist/hooks/useLongPress.js +26 -42
  100. package/dist/hooks/useLongPress.js.map +1 -1
  101. package/dist/styles/base.css +9 -0
  102. package/dist/styles/base.css.js +9 -0
  103. package/package.json +1 -1
  104. package/src/components/BannerAlert/BannerAlert.tsx +5 -5
  105. package/src/components/Breadcrumb/BreadcrumbDropdown.tsx +6 -0
  106. package/src/components/CheckboxGroup/CheckboxGroupExample.tsx +1 -0
  107. package/src/components/ChipGroup/ChipGroup.rtl.test.tsx +16 -11
  108. package/src/components/ChipGroup/ChipGroup.tsx +18 -36
  109. package/src/components/ChipGroup/ChipGroupExample.tsx +64 -36
  110. package/src/components/ChipGroup/chip-group.scss +5 -3
  111. package/src/components/Drawer/Drawer.tsx +0 -1
  112. package/src/components/Field/FieldDescription.tsx +7 -5
  113. package/src/components/Field/FieldError.tsx +6 -0
  114. package/src/components/Field/FieldLabel.tsx +6 -0
  115. package/src/components/Field/utils.ts +5 -0
  116. package/src/components/InlineAlert/SvgWarningTwoTone.tsx +6 -0
  117. package/src/components/InputNumber/IncrementButton.tsx +21 -11
  118. package/src/components/InputNumber/InputNumber.tsx +33 -31
  119. package/src/components/InputNumber/InputNumberExample.tsx +1 -0
  120. package/src/components/InputNumber/input-number.scss +10 -0
  121. package/src/components/Link/Link.tsx +1 -1
  122. package/src/components/OTPInput/OTPInput.rtl.test.tsx +4 -2
  123. package/src/components/OTPInput/OTPInput.tsx +34 -63
  124. package/src/components/OTPInput/OTPInputExample.tsx +6 -1
  125. package/src/components/OTPInput/otp-input.scss +50 -45
  126. package/src/components/Pagination/PageList.tsx +6 -0
  127. package/src/components/Scrim/Scrim.tsx +0 -1
  128. package/src/components/Select/Select.tsx +11 -11
  129. package/src/components/Skeleton/Circular.tsx +6 -0
  130. package/src/components/Skeleton/Photo.tsx +6 -0
  131. package/src/components/Skeleton/Profile.tsx +6 -0
  132. package/src/components/Skeleton/Rectangular.tsx +6 -0
  133. package/src/components/Skeleton/Thumbnail.tsx +6 -0
  134. package/src/components/Slider/SliderIntervalDots.tsx +6 -0
  135. package/src/components/Snackbar/Manager.tsx +0 -1
  136. package/src/components/Snackbar/Snackbar.tsx +0 -1
  137. package/src/components/TabList/TabList.tsx +1 -2
  138. package/src/components/Table/Footer.tsx +6 -0
  139. package/src/components/TimePicker/Listbox.tsx +6 -0
  140. package/src/components/TimePicker/Segment.tsx +6 -0
  141. package/src/components/Truncated/Truncated.tsx +1 -2
  142. package/src/components/UIProvider/UIProvider.tsx +0 -1
  143. package/src/hooks/useLongPress.ts +58 -48
  144. package/src/styles/base.scss +9 -0
  145. package/dist/components/Truncated/truncated.css +0 -8
  146. package/dist/components/Truncated/truncated.css.js +0 -13
  147. package/src/components/Truncated/truncated.scss +0 -8
@@ -1,6 +1,5 @@
1
1
  import { ChipGroup } from './ChipGroup';
2
2
  import { presets } from './ChipGroupExample';
3
- import { Chip } from '-/components/Chip';
4
3
  import { hasNoBasicA11yIssues } from '-/rtl/hasNoBasicA11yIssues';
5
4
  import { render } from '-/rtl/util';
6
5
 
@@ -9,22 +8,28 @@ describe('ChipGroup (RTL)', () => {
9
8
  it(
10
9
  `has no basic a11y issues - ${preset.label}`,
11
10
  hasNoBasicA11yIssues(
12
- <ChipGroup {...preset.propState}>
13
- <Chip label="suggestion 1" onClick={() => {}} />
14
- <Chip label="suggestion 2" onClick={() => {}} />
15
- <Chip label="suggestion 3" onClick={() => {}} />
16
- </ChipGroup>,
11
+ <ChipGroup
12
+ {...preset.propState}
13
+ items={[
14
+ { label: 'suggestion 1', onClick: () => {} },
15
+ { label: 'suggestion 2', onClick: () => {} },
16
+ { label: 'suggestion 3', onClick: () => {} },
17
+ ]}
18
+ />,
17
19
  ),
18
20
  );
19
21
  });
20
22
 
21
23
  it('renders', () => {
22
24
  const { getByText } = render(
23
- <ChipGroup {...presets[1].propState}>
24
- <Chip label="suggestion 1" onClick={() => {}} />
25
- <Chip label="suggestion 2" onClick={() => {}} />
26
- <Chip label="suggestion 3" onClick={() => {}} />
27
- </ChipGroup>,
25
+ <ChipGroup
26
+ {...presets[1].propState}
27
+ items={[
28
+ { label: 'suggestion 1', onClick: () => {} },
29
+ { label: 'suggestion 2', onClick: () => {} },
30
+ { label: 'suggestion 3', onClick: () => {} },
31
+ ]}
32
+ />,
28
33
  );
29
34
 
30
35
  expect(getByText('suggestion 1')).toBeInTheDocument();
@@ -1,23 +1,17 @@
1
1
  import './chip-group.scss';
2
2
 
3
- import { ReactNode } from 'react';
4
-
5
- import { ChipProps } from '-/components/Chip';
6
-
7
- export type ChipGroupItem = Pick<
8
- ChipProps,
9
- 'disabled' | 'flat' | 'label' | 'leadingIcon' | 'onClick' | 'selected' | 'trailingBadge' | 'trailingIcon'
10
- >;
3
+ import { Chip, ChipProps } from '-/components/Chip';
11
4
 
12
5
  export type ChipGroupProps = {
13
6
  /**
14
- * To allow chips to wrap. If set to false chips will scroll.
7
+ * Controls the overflow behavior of the chip group. If set to `scroll`, the chip group will be scrollable
8
+ * horizontally. If set to `wrap`, the chip group will wrap to multiple lines as needed.
15
9
  *
16
- * @default true
10
+ * @default wrap
17
11
  */
18
- wrap?: boolean;
19
- /** Only Chip components should be used as children. */
20
- children?: ReactNode;
12
+ overflow?: 'scroll' | 'wrap';
13
+ /** Only Chip components should be used as items. */
14
+ items?: ChipProps[];
21
15
  };
22
16
  /**
23
17
  * A component that manages the layout of a group of chips.
@@ -26,34 +20,22 @@ export type ChipGroupProps = {
26
20
  * import { Chip } from '@bspk/ui/Chip';
27
21
  * import { ChipGroup } from '@bspk/ui/ChipGroup';
28
22
  *
29
- * <ChipGroup wrap={false}>
30
- * <Chip
31
- * label="chip 1"
32
- * leadingIcon={<SvgLightbulb />}
33
- * onClick={() => action('Chip clicked!')}
34
- * trailingIcon={<SvgChevronRight />}
35
- * />
36
- * <Chip
37
- * label="chip 2"
38
- * leadingIcon={<SvgIcecream />}
39
- * onClick={() => action('Chip clicked!')}
40
- * trailingIcon={<SvgChevronRight />}
41
- * />
42
- * <Chip
43
- * label="chip 3"
44
- * leadingIcon={<SvgSignLanguage />}
45
- * onClick={() => action('Chip clicked!')}
46
- * trailingIcon={<SvgClose />}
47
- * />
48
- * </ChipGroup>;
23
+ * <ChipGroup
24
+ * overflow="scroll"
25
+ * items={[
26
+ * { label: 'chip 1', leadingIcon: <SvgLightbulb />, onClick: () => {}, trailingIcon: <SvgChevronRight /> },
27
+ * { label: 'chip 2', leadingIcon: <SvgIcecream />, onClick: () => {}, trailingIcon: <SvgChevronRight /> },
28
+ * { label: 'chip 3', leadingIcon: <SvgSignLanguage />, onClick: () => {}, trailingIcon: <SvgClose /> },
29
+ * ]}
30
+ * />;
49
31
  *
50
32
  * @name ChipGroup
51
33
  * @phase UXReview
52
34
  */
53
- export function ChipGroup({ children, wrap = true }: ChipGroupProps) {
35
+ export function ChipGroup({ overflow = 'wrap', items }: ChipGroupProps) {
54
36
  return (
55
- <div data-bspk="chip-group" data-wrap={wrap || undefined}>
56
- {children}
37
+ <div data-bspk="chip-group" data-scroll={overflow === 'scroll' || undefined}>
38
+ {items?.length ? items.map((item, idx) => <Chip {...item} key={item.label ?? idx} />) : null}
57
39
  </div>
58
40
  );
59
41
  }
@@ -7,53 +7,81 @@ import { SvgLightbulb } from '@bspk/icons/Lightbulb';
7
7
  import { SvgOpportunities } from '@bspk/icons/Opportunities';
8
8
  import { SvgSignLanguage } from '@bspk/icons/SignLanguage';
9
9
 
10
- import { Chip } from '-/components/Chip';
10
+ // import { Chip } from '-/components/Chip';
11
11
  import { ChipGroupProps } from '-/components/ChipGroup';
12
12
  import { ComponentExampleFn, Preset } from '-/utils/demo';
13
13
 
14
14
  export const presets: Preset<ChipGroupProps>[] = [
15
- { label: 'scroll', propState: { wrap: false } },
16
- { label: 'wrap', propState: { wrap: true } },
15
+ {
16
+ label: 'Scroll',
17
+ propState: {
18
+ overflow: 'scroll',
19
+ items: [
20
+ { label: 'chip 1', leadingIcon: <SvgLightbulb />, trailingIcon: <SvgChevronRight /> },
21
+ { label: 'chip 2', leadingIcon: <SvgIcecream />, trailingIcon: <SvgChevronRight /> },
22
+ { label: 'chip 3', leadingIcon: <SvgSignLanguage />, trailingIcon: <SvgClose /> },
23
+ { label: 'chip 4', leadingIcon: <SvgOpportunities />, trailingIcon: <SvgClose /> },
24
+ { label: 'chip 5', leadingIcon: <SvgCloud />, trailingIcon: <SvgKeyboardArrowDown /> },
25
+ ],
26
+ },
27
+ },
28
+ {
29
+ label: 'Scroll: Flat chips',
30
+ propState: {
31
+ overflow: 'scroll',
32
+ items: [
33
+ {
34
+ flat: true,
35
+ label: 'chip 1',
36
+ leadingIcon: <SvgLightbulb />,
37
+ trailingBadge: { count: 9, size: 'x-small' },
38
+ },
39
+ {
40
+ flat: true,
41
+ label: 'chip 2',
42
+ leadingIcon: <SvgIcecream />,
43
+ trailingBadge: { count: 2, size: 'x-small' },
44
+ },
45
+ { flat: true, label: 'chip 3', leadingIcon: <SvgSignLanguage />, trailingIcon: <SvgClose /> },
46
+ {
47
+ flat: true,
48
+ label: 'chip 4',
49
+ leadingIcon: <SvgOpportunities />,
50
+ trailingBadge: { count: 5, size: 'x-small' },
51
+ },
52
+ {
53
+ flat: true,
54
+ label: 'chip 5',
55
+ leadingIcon: <SvgCloud />,
56
+ trailingBadge: { count: 3, size: 'x-small' },
57
+ },
58
+ ],
59
+ },
60
+ },
17
61
  ];
18
62
 
19
63
  export const ChipGroupExample: ComponentExampleFn<ChipGroupProps> = ({ action }) => ({
20
64
  containerStyle: { width: '600px' },
21
65
  presets,
66
+ defaultState: {
67
+ overflow: 'wrap',
68
+ items: [
69
+ { label: 'chip 1', leadingIcon: <SvgLightbulb />, trailingIcon: <SvgChevronRight /> },
70
+ { label: 'chip 2', leadingIcon: <SvgIcecream />, trailingIcon: <SvgChevronRight /> },
71
+ { label: 'chip 3', leadingIcon: <SvgSignLanguage />, trailingIcon: <SvgClose /> },
72
+ { label: 'chip 4', leadingIcon: <SvgOpportunities />, trailingIcon: <SvgClose /> },
73
+ { label: 'chip 5', leadingIcon: <SvgCloud />, trailingIcon: <SvgKeyboardArrowDown /> },
74
+ ],
75
+ },
22
76
  render: ({ props, Component }) => {
23
- const handleChipInputClick = () => action('Chip clicked!');
24
77
  return (
25
- <Component {...props}>
26
- <Chip
27
- label="chip 1"
28
- leadingIcon={<SvgLightbulb />}
29
- onClick={handleChipInputClick}
30
- trailingIcon={<SvgChevronRight />}
31
- />
32
- <Chip
33
- label="chip 2"
34
- leadingIcon={<SvgIcecream />}
35
- onClick={handleChipInputClick}
36
- trailingIcon={<SvgChevronRight />}
37
- />
38
- <Chip
39
- label="chip 3"
40
- leadingIcon={<SvgSignLanguage />}
41
- onClick={handleChipInputClick}
42
- trailingIcon={<SvgClose />}
43
- />
44
- <Chip
45
- label="chip 4"
46
- leadingIcon={<SvgOpportunities />}
47
- onClick={handleChipInputClick}
48
- trailingIcon={<SvgClose />}
49
- />
50
- <Chip
51
- label="chip 5"
52
- leadingIcon={<SvgCloud />}
53
- onClick={handleChipInputClick}
54
- trailingIcon={<SvgKeyboardArrowDown />}
55
- />
56
- </Component>
78
+ <Component
79
+ {...props}
80
+ items={props.items?.map((item) => ({
81
+ ...item,
82
+ onClick: () => action('Chip clicked!'),
83
+ }))}
84
+ />
57
85
  );
58
86
  },
59
87
  });
@@ -2,10 +2,12 @@
2
2
  display: flex;
3
3
  gap: var(--spacing-sizing-02);
4
4
  width: 100%;
5
- overflow: auto;
5
+ flex-flow: row wrap;
6
+ padding-bottom: var(--spacing-sizing-01);
6
7
 
7
- &[data-wrap] {
8
- flex-flow: row wrap;
8
+ &[data-scroll] {
9
+ overflow: auto;
10
+ flex-wrap: nowrap;
9
11
  }
10
12
  }
11
13
 
@@ -76,7 +76,6 @@ export type DrawerProps = Pick<DialogProps, 'container' | 'disableFocusTrap' | '
76
76
  * @name Drawer
77
77
  * @phase UXReview
78
78
  */
79
-
80
79
  export function Drawer({
81
80
  header,
82
81
  children,
@@ -1,6 +1,12 @@
1
1
  import { useFieldContext, describedById } from './utils';
2
2
 
3
- function FieldDescription({ children }: { children?: string }) {
3
+ /**
4
+ * FieldDescription component displays a description associated with a form field.
5
+ *
6
+ * @name FieldDescription
7
+ * @parent Field
8
+ */
9
+ export function FieldDescription({ children }: { children?: string }) {
4
10
  const { id } = useFieldContext();
5
11
 
6
12
  return children ? (
@@ -9,7 +15,3 @@ function FieldDescription({ children }: { children?: string }) {
9
15
  </p>
10
16
  ) : null;
11
17
  }
12
-
13
- FieldDescription.displayName = 'FieldDescription';
14
-
15
- export { FieldDescription };
@@ -6,6 +6,12 @@ export type FieldErrorProps = {
6
6
  children?: string;
7
7
  };
8
8
 
9
+ /**
10
+ * FieldError component displays an error message associated with a form field.
11
+ *
12
+ * @name FieldError
13
+ * @parent Field
14
+ */
9
15
  export function FieldError({ children }: FieldErrorProps) {
10
16
  const { id } = useFieldContext();
11
17
 
@@ -14,6 +14,12 @@ export type FieldLabelProps<As extends ElementType = ElementType> = Pick<FieldCo
14
14
  as?: As;
15
15
  };
16
16
 
17
+ /**
18
+ * FieldLabel component displays a label associated with a form field.
19
+ *
20
+ * @name FieldLabel
21
+ * @parent Field
22
+ */
17
23
  export function FieldLabel<As extends ElementType = ElementType>({
18
24
  children,
19
25
  labelTrailing,
@@ -28,6 +28,11 @@ export type FieldContext = FieldContextProps & {
28
28
 
29
29
  export const fieldContext = createContext<FieldContext | null>(null);
30
30
 
31
+ /**
32
+ * Retrieves the current Field context.
33
+ *
34
+ * Will return a default context if used outside of a Field component.
35
+ */
31
36
  export function useFieldContext(): FieldContext {
32
37
  return (
33
38
  useContext(fieldContext) || {
@@ -1,3 +1,9 @@
1
+ /**
2
+ * SvgWarningTwoTone component renders a two-tone warning SVG icon.
3
+ *
4
+ * @name SvgWarningTwoTone
5
+ * @parent InlineAlert
6
+ */
1
7
  export function SvgWarningTwoTone() {
2
8
  return (
3
9
  <svg fill="none" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
@@ -3,29 +3,39 @@ import { SvgRemove } from '@bspk/icons/Remove';
3
3
  import { useLongPress } from '-/hooks/useLongPress';
4
4
 
5
5
  export type IncrementButtonProps = {
6
+ /** Whether the button is disabled. */
6
7
  disabled: boolean;
7
- increment: -1 | 1;
8
- onIncrement: (increment: -1 | 1) => void;
8
+ /** The kind of increment button, either 'add' or 'remove'. */
9
+ kind: 'add' | 'remove';
10
+ /** The ID of the associated input element. */
9
11
  inputId: string;
12
+ /** Function to trigger the increment action. */
13
+ triggerIncrement: (kind: 'add' | 'remove') => boolean;
10
14
  };
11
15
 
12
- export function IncrementButton({ increment, disabled, onIncrement, inputId }: IncrementButtonProps) {
13
- const add = increment === 1;
14
-
15
- const { setTriggerElement, ...handlers } = useLongPress(() => onIncrement(increment), disabled);
16
+ /**
17
+ * A button component for incrementing or decrementing the InputNumber.
18
+ *
19
+ * @name IncrementButton
20
+ * @parent InputNumber
21
+ */
22
+ export function IncrementButton({ inputId, kind, disabled, triggerIncrement }: IncrementButtonProps) {
23
+ const { ...pressHandlers } = useLongPress({
24
+ callback: () => triggerIncrement(kind),
25
+ });
16
26
 
17
27
  return (
18
28
  <button
19
- {...handlers}
29
+ {...pressHandlers}
20
30
  aria-controls={inputId}
21
- aria-hidden="true"
22
- data-increment={increment}
31
+ aria-label={kind === 'add' ? 'Increase value' : 'Decrease value'}
32
+ data-bspk="input-number--increment-button"
33
+ data-kind={kind}
23
34
  disabled={disabled}
24
- ref={setTriggerElement}
25
35
  tabIndex={-1}
26
36
  type="button"
27
37
  >
28
- {add ? <SvgAdd /> : <SvgRemove />}
38
+ {kind === 'add' ? <SvgAdd aria-hidden /> : <SvgRemove aria-hidden />}
29
39
  </button>
30
40
  );
31
41
  }
@@ -1,14 +1,16 @@
1
1
  import './input-number.scss';
2
- import { useMemo } from 'react';
2
+ import { useEffect, useRef } from 'react';
3
3
  import { IncrementButton } from './IncrementButton';
4
4
  import { useFieldInit } from '-/components/Field';
5
5
  import { useId } from '-/hooks/useId';
6
6
  import { CommonProps, FieldControlProps } from '-/types/common';
7
7
 
8
- function isNumber(value: unknown, fallbackValue: number | undefined = undefined): number | undefined {
8
+ function isNumber(value: unknown): number | undefined;
9
+ function isNumber(value: unknown, fallbackValue: number): number;
10
+ function isNumber(value: unknown, fallbackValue?: number): number | undefined {
9
11
  if (typeof value === 'number') return value;
10
12
  if (typeof value !== 'string') return fallbackValue;
11
- const num = Number(value);
13
+ const num = parseFloat(value);
12
14
  return isNaN(num) ? fallbackValue : num;
13
15
  }
14
16
 
@@ -108,14 +110,25 @@ export function InputNumber({
108
110
  readOnly,
109
111
  invalidProp,
110
112
  });
111
-
112
113
  const max = typeof maxProp === 'number' && maxProp >= min ? maxProp : Number.MAX_SAFE_INTEGER;
113
114
  const centered = align !== 'left';
114
115
  const inputId = useId(id);
115
- const value = useMemo(() => isNumber(valueProp) || 0, [valueProp]);
116
+ const value = isNumber(valueProp, min);
117
+ const removeDisabled = disabled || value + step * -1 < min;
118
+ const addDisabled = disabled || value + step > max;
119
+
120
+ const valueRef = useRef(value);
116
121
 
117
- const handleIncrement = (increment: -1 | 1) => {
118
- onChange(value + increment * step);
122
+ useEffect(() => {
123
+ valueRef.current = value;
124
+ }, [value]);
125
+
126
+ const incrementHandler = (kind: 'add' | 'remove') => {
127
+ const increment = kind === 'add' ? step : step * -1;
128
+ const next = valueRef.current + increment;
129
+ if (next < min || next > max) return false;
130
+ onChange(next);
131
+ return true;
119
132
  };
120
133
 
121
134
  return (
@@ -128,14 +141,6 @@ export function InputNumber({
128
141
  data-size={size}
129
142
  data-stepper-input
130
143
  >
131
- {!!centered && (
132
- <IncrementButton
133
- disabled={disabled ? true : value + -1 < min}
134
- increment={-1}
135
- inputId={inputId}
136
- onIncrement={handleIncrement}
137
- />
138
- )}
139
144
  <input
140
145
  {...inputElementProps}
141
146
  aria-describedby={ariaDescribedBy || undefined}
@@ -151,32 +156,29 @@ export function InputNumber({
151
156
  max={max}
152
157
  min={min}
153
158
  name={name}
159
+ onBlur={(e) => {
160
+ const next = isNumber(e.target.value, min);
161
+ e.target.value = next?.toString() || '';
162
+ onChange(next);
163
+ }}
154
164
  onChange={(e) => {
155
- onChange(isNumber(e.target.value));
165
+ const next = isNumber(e.target.value, min);
166
+ onChange(next);
156
167
  }}
157
168
  readOnly={readOnly}
158
169
  required={required}
159
170
  step={step}
160
171
  type="number"
161
- value={value}
172
+ value={value !== undefined ? value : ''}
162
173
  />
163
- {!centered && (
164
- <>
165
- <div aria-hidden data-divider />
166
- <IncrementButton
167
- disabled={!!disabled || value + -1 < min}
168
- increment={-1}
169
- inputId={inputId}
170
- onIncrement={handleIncrement}
171
- />
172
- </>
173
- )}
174
+ <div aria-hidden data-divider />
174
175
  <IncrementButton
175
- disabled={!!disabled || value + 1 > max}
176
- increment={1}
176
+ disabled={removeDisabled}
177
177
  inputId={inputId}
178
- onIncrement={handleIncrement}
178
+ kind="remove"
179
+ triggerIncrement={incrementHandler}
179
180
  />
181
+ <IncrementButton disabled={addDisabled} inputId={inputId} kind="add" triggerIncrement={incrementHandler} />
180
182
  </div>
181
183
  );
182
184
  }
@@ -6,4 +6,5 @@ export const InputNumberExample: ComponentExample<InputNumberProps> = {
6
6
  'aria-label': 'input number aria-label',
7
7
  },
8
8
  render: ({ props, Component }) => <Component {...props} />,
9
+ variants: false,
9
10
  };
@@ -129,6 +129,16 @@
129
129
  --height: var(--spacing-sizing-12);
130
130
  --svg-width: var(--spacing-sizing-06);
131
131
  }
132
+
133
+ &[data-centered] {
134
+ button:first-of-type {
135
+ order: -1;
136
+ }
137
+
138
+ [data-divider] {
139
+ display: none;
140
+ }
141
+ }
132
142
  }
133
143
 
134
144
  /** Copyright 2025 Anywhere Real Estate - CC BY 4.0 */
@@ -46,7 +46,7 @@ export type LinkProps = Pick<CommonPropsLibrary, 'disabled'> & {
46
46
  * @example
47
47
  * import { Link } from '@bspk/ui/Link';
48
48
  *
49
- * <Link href="https://bspk.dev" label="Example label" trailingIcon="external" />;
49
+ * <Link href="https://anywhere.re" label="Example label" trailingIcon="external" />;
50
50
  *
51
51
  * @name Link
52
52
  * @phase UXReview
@@ -12,8 +12,10 @@ describe('OTPInput (RTL)', () => {
12
12
  });
13
13
 
14
14
  it('renders', () => {
15
- const { getByLabelText } = render(<OTPInput onChange={() => {}} {...presets[0].propState} />);
15
+ const { getByLabelText } = render(
16
+ <OTPInput aria-label="OTP input" onChange={() => {}} {...presets[0].propState} />,
17
+ );
16
18
 
17
- expect(getByLabelText('OTP digit 1')).toBeInTheDocument();
19
+ expect(getByLabelText('OTP input')).toBeInTheDocument();
18
20
  });
19
21
  });