Skip to content

Commit ee548ba

Browse files
dmytrokirpaclaude
andauthored
feat(react-spinbutton): add useSpinButtonBase_unstable hook (#35907)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 99713fd commit ee548ba

8 files changed

Lines changed: 69 additions & 25 deletions

File tree

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"type": "minor",
3+
"comment": "feat(react-spinbutton): add useSpinButtonBase_unstable hook",
4+
"packageName": "@fluentui/react-spinbutton",
5+
"email": "dmytrokirpa@microsoft.com",
6+
"dependentChangeType": "patch"
7+
}

packages/react-components/react-spinbutton/library/etc/react-spinbutton.api.md

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,18 +6,25 @@
66

77
import type { ComponentProps } from '@fluentui/react-utilities';
88
import type { ComponentState } from '@fluentui/react-utilities';
9+
import type { DistributiveOmit } from '@fluentui/react-utilities';
910
import type { ForwardRefComponent } from '@fluentui/react-utilities';
1011
import type { JSXElement } from '@fluentui/react-utilities';
1112
import * as React_2 from 'react';
1213
import type { Slot } from '@fluentui/react-utilities';
1314
import { SlotClassNames } from '@fluentui/react-utilities';
1415

1516
// @public
16-
export const renderSpinButton_unstable: (state: SpinButtonState) => JSXElement;
17+
export const renderSpinButton_unstable: (state: SpinButtonBaseState) => JSXElement;
1718

1819
// @public
1920
export const SpinButton: ForwardRefComponent<SpinButtonProps>;
2021

22+
// @public (undocumented)
23+
export type SpinButtonBaseProps = DistributiveOmit<SpinButtonProps, 'appearance' | 'size'>;
24+
25+
// @public (undocumented)
26+
export type SpinButtonBaseState = DistributiveOmit<SpinButtonState, 'appearance' | 'size'>;
27+
2128
// @public (undocumented)
2229
export type SpinButtonBounds = 'none' | 'min' | 'max' | 'both';
2330

@@ -68,6 +75,9 @@ export type SpinButtonState = ComponentState<SpinButtonSlots> & Required<Pick<Sp
6875
// @public
6976
export const useSpinButton_unstable: (props: SpinButtonProps, ref: React_2.Ref<HTMLInputElement>) => SpinButtonState;
7077

78+
// @public
79+
export const useSpinButtonBase_unstable: (props: SpinButtonBaseProps, ref: React_2.Ref<HTMLInputElement>) => SpinButtonBaseState;
80+
7181
// @public
7282
export const useSpinButtonStyles_unstable: (state: SpinButtonState) => SpinButtonState;
7383

packages/react-components/react-spinbutton/library/src/SpinButton.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
export type {
2+
SpinButtonBaseProps,
3+
SpinButtonBaseState,
24
SpinButtonBounds,
35
SpinButtonChangeEvent,
46
SpinButtonOnChangeData,
@@ -12,5 +14,6 @@ export {
1214
renderSpinButton_unstable,
1315
spinButtonClassNames,
1416
useSpinButtonStyles_unstable,
17+
useSpinButtonBase_unstable,
1518
useSpinButton_unstable,
1619
} from './components/SpinButton/index';

packages/react-components/react-spinbutton/library/src/components/SpinButton/SpinButton.types.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { ComponentProps, ComponentState, Slot } from '@fluentui/react-utilities';
1+
import type { ComponentProps, ComponentState, DistributiveOmit, Slot } from '@fluentui/react-utilities';
22
import * as React from 'react';
33

44
export type SpinButtonSlots = {
@@ -157,3 +157,6 @@ export type SpinButtonOnChangeData = {
157157

158158
export type SpinButtonSpinState = 'rest' | 'up' | 'down';
159159
export type SpinButtonBounds = 'none' | 'min' | 'max' | 'both';
160+
161+
export type SpinButtonBaseProps = DistributiveOmit<SpinButtonProps, 'appearance' | 'size'>;
162+
export type SpinButtonBaseState = DistributiveOmit<SpinButtonState, 'appearance' | 'size'>;

packages/react-components/react-spinbutton/library/src/components/SpinButton/index.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
export { SpinButton } from './SpinButton';
22
export type {
3+
SpinButtonBaseProps,
4+
SpinButtonBaseState,
35
SpinButtonBounds,
46
SpinButtonChangeEvent,
57
SpinButtonOnChangeData,
@@ -9,5 +11,5 @@ export type {
911
SpinButtonState,
1012
} from './SpinButton.types';
1113
export { renderSpinButton_unstable } from './renderSpinButton';
12-
export { useSpinButton_unstable } from './useSpinButton';
14+
export { useSpinButtonBase_unstable, useSpinButton_unstable } from './useSpinButton';
1315
export { spinButtonClassNames, useSpinButtonStyles_unstable } from './useSpinButtonStyles.styles';

packages/react-components/react-spinbutton/library/src/components/SpinButton/renderSpinButton.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,12 @@
33

44
import { assertSlots } from '@fluentui/react-utilities';
55
import type { JSXElement } from '@fluentui/react-utilities';
6-
import type { SpinButtonState, SpinButtonSlots } from './SpinButton.types';
6+
import type { SpinButtonBaseState, SpinButtonSlots } from './SpinButton.types';
77

88
/**
99
* Render the final JSX of SpinButton
1010
*/
11-
export const renderSpinButton_unstable = (state: SpinButtonState): JSXElement => {
11+
export const renderSpinButton_unstable = (state: SpinButtonBaseState): JSXElement => {
1212
assertSlots<SpinButtonSlots>(state);
1313

1414
return (

packages/react-components/react-spinbutton/library/src/components/SpinButton/useSpinButton.tsx

Lines changed: 36 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ import {
1212
} from '@fluentui/react-utilities';
1313
import { ArrowUp, ArrowDown, End, Enter, Escape, Home, PageDown, PageUp } from '@fluentui/keyboard-keys';
1414
import {
15+
SpinButtonBaseProps,
16+
SpinButtonBaseState,
1517
SpinButtonProps,
1618
SpinButtonState,
1719
SpinButtonSpinState,
@@ -41,26 +43,21 @@ const MAX_SPIN_TIME_MS = 1000;
4143
const lerp = (start: number, end: number, percent: number): number => start + (end - start) * percent;
4244

4345
/**
44-
* Create the state required to render SpinButton.
45-
*
46-
* The returned state can be modified with hooks such as useSpinButtonStyles_unstable,
47-
* before being passed to renderSpinButton_unstable.
46+
* Create the base state required to render SpinButton without design-specific props.
4847
*
49-
* @param props - props from this instance of SpinButton
48+
* @param props - props from this instance of SpinButton (without appearance/size)
5049
* @param ref - reference to root HTMLElement of SpinButton
5150
*/
52-
export const useSpinButton_unstable = (props: SpinButtonProps, ref: React.Ref<HTMLInputElement>): SpinButtonState => {
53-
// Merge props from surrounding <Field>, if any
54-
props = useFieldControlProps_unstable(props, { supportsLabelFor: true, supportsRequired: true });
55-
51+
export const useSpinButtonBase_unstable = (
52+
props: SpinButtonBaseProps,
53+
ref: React.Ref<HTMLInputElement>,
54+
): SpinButtonBaseState => {
5655
const nativeProps = getPartitionedNativeProps({
5756
props,
5857
primarySlotTagName: 'input',
59-
excludedPropNames: ['defaultValue', 'max', 'min', 'onChange', 'size', 'value'],
58+
excludedPropNames: ['defaultValue', 'max', 'min', 'onChange', 'value'],
6059
});
6160

62-
const overrides = useOverrides();
63-
6461
const {
6562
value,
6663
displayValue,
@@ -71,8 +68,6 @@ export const useSpinButton_unstable = (props: SpinButtonProps, ref: React.Ref<HT
7168
stepPage = 1,
7269
precision: precisionFromProps,
7370
onChange,
74-
size = 'medium',
75-
appearance = overrides.inputDefaultAppearance ?? 'outline',
7671
root,
7772
input,
7873
incrementButton,
@@ -159,6 +154,7 @@ export const useSpinButton_unstable = (props: SpinButtonProps, ref: React.Ref<HT
159154
if (inputRef.current) {
160155
// we need to set this here using the IDL attribute directly, because otherwise the timing of the ARIA value update
161156
// is not in sync with the user-entered native input value, and some screen readers end up reading the wrong value.
157+
// eslint-disable-next-line react-compiler/react-compiler
162158
inputRef.current.ariaValueNow = newValue;
163159
}
164160
};
@@ -281,9 +277,7 @@ export const useSpinButton_unstable = (props: SpinButtonProps, ref: React.Ref<HT
281277
}
282278
}
283279

284-
const state: SpinButtonState = {
285-
size,
286-
appearance,
280+
const state: SpinButtonBaseState = {
287281
spinState: keyboardSpinState,
288282
atBound: internalState.current.atBound,
289283

@@ -301,7 +295,6 @@ export const useSpinButton_unstable = (props: SpinButtonProps, ref: React.Ref<HT
301295
defaultProps: {
302296
autoComplete: 'off',
303297
role: 'spinbutton',
304-
appearance,
305298
type: 'text',
306299
...nativeProps.primary,
307300
},
@@ -310,7 +303,6 @@ export const useSpinButton_unstable = (props: SpinButtonProps, ref: React.Ref<HT
310303
incrementButton: slot.always(incrementButton, {
311304
defaultProps: {
312305
tabIndex: -1,
313-
children: <ChevronUp16Regular />,
314306
disabled:
315307
nativeProps.primary.readOnly ||
316308
nativeProps.primary.disabled ||
@@ -324,7 +316,6 @@ export const useSpinButton_unstable = (props: SpinButtonProps, ref: React.Ref<HT
324316
decrementButton: slot.always(decrementButton, {
325317
defaultProps: {
326318
tabIndex: -1,
327-
children: <ChevronDown16Regular />,
328319
disabled:
329320
nativeProps.primary.readOnly ||
330321
nativeProps.primary.disabled ||
@@ -359,3 +350,28 @@ export const useSpinButton_unstable = (props: SpinButtonProps, ref: React.Ref<HT
359350

360351
return state;
361352
};
353+
354+
/**
355+
* Create the state required to render SpinButton.
356+
*
357+
* The returned state can be modified with hooks such as useSpinButtonStyles_unstable,
358+
* before being passed to renderSpinButton_unstable.
359+
*
360+
* @param props - props from this instance of SpinButton
361+
* @param ref - reference to root HTMLElement of SpinButton
362+
*/
363+
export const useSpinButton_unstable = (props: SpinButtonProps, ref: React.Ref<HTMLInputElement>): SpinButtonState => {
364+
// Merge props from surrounding <Field>, if any
365+
props = useFieldControlProps_unstable(props, { supportsLabelFor: true, supportsRequired: true });
366+
367+
const overrides = useOverrides();
368+
369+
const { appearance = overrides.inputDefaultAppearance ?? 'outline', size = 'medium', ...baseProps } = props;
370+
371+
const state = useSpinButtonBase_unstable(baseProps, ref);
372+
373+
state.incrementButton.children ??= <ChevronUp16Regular />;
374+
state.decrementButton.children ??= <ChevronDown16Regular />;
375+
376+
return { ...state, appearance, size };
377+
};

packages/react-components/react-spinbutton/library/src/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,12 @@ export {
33
renderSpinButton_unstable,
44
spinButtonClassNames,
55
useSpinButtonStyles_unstable,
6+
useSpinButtonBase_unstable,
67
useSpinButton_unstable,
78
} from './SpinButton';
89
export type {
10+
SpinButtonBaseProps,
11+
SpinButtonBaseState,
912
SpinButtonOnChangeData,
1013
SpinButtonChangeEvent,
1114
SpinButtonProps,

0 commit comments

Comments
 (0)