Skip to content

Commit 7102995

Browse files
authored
refactor: replace bounds logic with clamp and add tests (#19118)
1 parent 8382893 commit 7102995

File tree

7 files changed

+81
-23
lines changed

7 files changed

+81
-23
lines changed
Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
11
/**
2-
* Copyright IBM Corp. 2023
2+
* Copyright IBM Corp. 2023, 2025
33
*
44
* This source code is licensed under the Apache-2.0 license found in the
55
* LICENSE file in the root directory of this source tree.
66
*/
77

88
export const levels = ['one', 'two', 'three'] as const;
99

10-
export const MIN_LEVEL = 0;
11-
export const MAX_LEVEL = levels.length - 1;
12-
1310
export const LayerLevels = [0, 1, 2] as const;
1411

12+
export const MIN_LEVEL = LayerLevels[0];
13+
export const MAX_LEVEL = LayerLevels[LayerLevels.length - 1];
14+
1515
export type LayerLevel = (typeof LayerLevels)[number];

packages/react/src/components/Layer/index.tsx

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/**
2-
* Copyright IBM Corp. 2016, 2023
2+
* Copyright IBM Corp. 2016, 2025
33
*
44
* This source code is licensed under the Apache-2.0 license found in the
55
* LICENSE file in the root directory of this source tree.
@@ -15,6 +15,7 @@ import {
1515
PolymorphicComponentPropWithRef,
1616
PolymorphicRef,
1717
} from '../../internal/PolymorphicProps';
18+
import { clamp } from '../../internal/clamp';
1819

1920
/**
2021
* A custom hook that will return information about the current layer. A common
@@ -64,10 +65,7 @@ const Layer = React.forwardRef<
6465
const prefix = usePrefix();
6566
const className = cx(`${prefix}--layer-${levels[level]}`, customClassName);
6667
// The level should be between MIN_LEVEL and MAX_LEVEL
67-
const value = Math.max(
68-
MIN_LEVEL,
69-
Math.min(level + 1, MAX_LEVEL)
70-
) as LayerLevel;
68+
const value = clamp(level + 1, MIN_LEVEL, MAX_LEVEL);
7169

7270
const BaseComponent = as || 'div';
7371

packages/react/src/components/NumberInput/NumberInput.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -373,7 +373,7 @@ const NumberInput = React.forwardRef<HTMLInputElement, NumberInputProps>(
373373
getDecimalPlaces(step)
374374
);
375375
const floatValue = parseFloat(rawValue.toFixed(precision));
376-
const newValue = clamp(floatValue, min, max);
376+
const newValue = clamp(floatValue, min ?? -Infinity, max ?? Infinity);
377377

378378
const state = {
379379
value:

packages/react/src/components/PaginationNav/PaginationNav.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/**
2-
* Copyright IBM Corp. 2020
2+
* Copyright IBM Corp. 2020, 2025
33
*
44
* This source code is licensed under the Apache-2.0 license found in the
55
* LICENSE file in the root directory of this source tree.
@@ -18,6 +18,7 @@ import { usePrefix } from '../../internal/usePrefix';
1818
import { TranslateWithId } from '../../types/common';
1919
import { breakpoints } from '@carbon/layout';
2020
import { useMatchMedia } from '../../internal/useMatchMedia';
21+
import { clamp } from '../../internal/clamp';
2122

2223
const translationIds = {
2324
'carbon.pagination-nav.next': 'Next',
@@ -350,7 +351,7 @@ const PaginationNav = React.forwardRef<HTMLElement, PaginationNavProps>(
350351
numberOfPages = itemsShown === 4 ? itemsShown : 5;
351352
break;
352353
case 'sm':
353-
numberOfPages = Math.max(4, Math.min(itemsShown, 7));
354+
numberOfPages = clamp(itemsShown, 4, 7);
354355
break;
355356

356357
default:

packages/react/src/components/Slider/Slider.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import {
3030
UpperHandleFocus,
3131
} from './SliderHandles';
3232
import { TranslateWithId } from '../../types/common';
33+
import { clamp } from '../../internal/clamp';
3334

3435
const ThumbWrapper = ({
3536
hasTooltip = false,
@@ -1069,7 +1070,7 @@ class Slider extends PureComponent<SliderProps> {
10691070
range,
10701071
});
10711072
/** `leftPercentRaw` clamped between 0 and 1. */
1072-
const leftPercent = Math.min(1, Math.max(0, leftPercentRaw));
1073+
const leftPercent = clamp(leftPercentRaw, 0, 1);
10731074

10741075
if (useRawValue) {
10751076
return {
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
/**
2+
* Copyright IBM Corp. 2025
3+
*
4+
* This source code is licensed under the Apache-2.0 license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
import { clamp } from '../clamp';
9+
10+
describe('clamp', () => {
11+
it('should return the value unmodified when it is within the bounds', () => {
12+
expect(clamp(5, 1, 10)).toBe(5);
13+
expect(clamp(1, 1, 10)).toBe(1);
14+
expect(clamp(10, 1, 10)).toBe(10);
15+
});
16+
17+
it('should clamp the value to the lower bound when below min', () => {
18+
expect(clamp(-5, 0, 10)).toBe(0);
19+
expect(clamp(5, 10, 20)).toBe(10);
20+
});
21+
22+
it('should clamp the value to the upper bound when above max', () => {
23+
expect(clamp(15, 0, 10)).toBe(10);
24+
expect(clamp(5, 0, 2)).toBe(2);
25+
});
26+
27+
it('should return `NaN` if any argument is not a number', () => {
28+
expect(clamp('a', 0, 10)).toBeNaN();
29+
expect(clamp(5, 'a', 10)).toBeNaN();
30+
expect(clamp(5, 0, 'a')).toBeNaN();
31+
});
32+
33+
it('should handle cases where the lower bound is greater than the upper bound', () => {
34+
expect(clamp(5, 10, 1)).toBe(1);
35+
expect(clamp(15, 10, 1)).toBe(1);
36+
expect(clamp(-5, 10, 1)).toBe(1);
37+
});
38+
39+
it('should return the bound when lower and upper bounds are equal', () => {
40+
expect(clamp(-5, 1, 1)).toBe(1);
41+
expect(clamp(0, 1, 1)).toBe(1);
42+
expect(clamp(5, 1, 1)).toBe(1);
43+
});
44+
45+
it('should handle explicit `Infinity` and `-Infinity` bounds', () => {
46+
expect(clamp(Infinity, 0, 10)).toBe(10);
47+
expect(clamp(-Infinity, 0, 10)).toBe(0);
48+
expect(clamp(5, 0, Infinity)).toBe(5);
49+
expect(clamp(5, -Infinity, 10)).toBe(5);
50+
expect(clamp(5, -Infinity, Infinity)).toBe(5);
51+
});
52+
53+
it('should return `NaN` when bounds are `NaN`', () => {
54+
expect(clamp(5, NaN, 10)).toBeNaN();
55+
expect(clamp(5, 0, NaN)).toBeNaN();
56+
expect(clamp(5, NaN, NaN)).toBeNaN();
57+
});
58+
59+
it('should work with decimal values', () => {
60+
expect(clamp(5.5, 1.2, 10.8)).toBe(5.5);
61+
expect(clamp(0.5, 1.2, 10.8)).toBe(1.2);
62+
expect(clamp(11.5, 1.2, 10.8)).toBe(10.8);
63+
});
64+
});

packages/react/src/internal/clamp.ts

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,12 @@
11
/**
2-
* Copyright IBM Corp. 2016, 2025
2+
* Copyright IBM Corp. 2025
33
*
44
* This source code is licensed under the Apache-2.0 license found in the
55
* LICENSE file in the root directory of this source tree.
66
*/
77

88
/**
9-
* Synonymous to ECMA2017+ `Math.clamp`.
10-
*
11-
* @param {number} val
12-
* @param {number} min
13-
* @param {number} max
14-
*
15-
* @returns `val` if `max>=val>=min`; `min` if `val<min`; `max` if `val>max`.
9+
* Clamps a number between a minimum and maximum value (inclusive).
1610
*/
17-
export const clamp = (value: number, min?: number, max?: number) =>
18-
Math.min(max ?? Infinity, Math.max(min ?? -Infinity, value));
11+
export const clamp = <T extends number>(num: number, min: T, max: T): T =>
12+
Math.min(max, Math.max(min, num)) as T;

0 commit comments

Comments
 (0)