Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions packages/react-spinbutton/etc/react-spinbutton.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ export type SpinButtonCommons = {
appearance: 'outline' | 'underline' | 'filledDarker' | 'filledLighter';
size: 'small' | 'medium';
inputType: 'all' | 'spinners-only';
strings?: SpinButtonStrings;
};

// @public (undocumented)
Expand Down Expand Up @@ -68,6 +69,12 @@ export type SpinButtonState = ComponentState<SpinButtonSlots> & Partial<SpinButt
atBound: SpinButtonBounds;
};

// @public (undocumented)
export type SpinButtonStrings = {
incrementButtonLabel: string;
decrementButtonLabel: string;
};

// @public
export const useSpinButton_unstable: (props: SpinButtonProps, ref: React_2.Ref<HTMLInputElement>) => SpinButtonState;

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import type { SpinButtonStrings } from './SpinButton.types';

export const spinButtonDefaultStrings: SpinButtonStrings = {
incrementButtonLabel: 'Increment by {step}',
decrementButtonLabel: 'Decrement by {step}',
};
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import userEvent from '@testing-library/user-event';
import { SpinButton } from './SpinButton';
import { isConformant } from '../../common/isConformant';
import * as Keys from '@fluentui/keyboard-keys';
import { SpinButtonStrings } from './SpinButton.types';

const getSpinButtonInput = (): HTMLInputElement => {
return screen.getByRole('spinbutton') as HTMLInputElement;
Expand All @@ -18,25 +19,33 @@ describe('SpinButton', () => {
});

it('renders a default uncontrolled state', () => {
render(<SpinButton defaultValue={10} />);
const { getAllByRole } = render(<SpinButton defaultValue={10} />);

const spinButton = getSpinButtonInput();
expect(spinButton.value).toEqual('10');
expect(spinButton.getAttribute('aria-valuenow')).toEqual('10');
expect(spinButton.getAttribute('aria-valuetext')).toBeNull();
expect(spinButton.getAttribute('aria-valuemin')).toBeNull();
expect(spinButton.getAttribute('aria-valuemax')).toBeNull();

const [incrementButton, decrementButton] = getAllByRole('button');
expect(incrementButton.getAttribute('aria-label')).toEqual('Increment by 1');
expect(decrementButton.getAttribute('aria-label')).toEqual('Decrement by 1');
});

it('renders a default controlled state', () => {
render(<SpinButton value={1} onChange={jest.fn()} />);
const { getAllByRole } = render(<SpinButton value={1} onChange={jest.fn()} />);

const spinButton = getSpinButtonInput();
expect(spinButton.value).toEqual('1');
expect(spinButton.getAttribute('aria-valuenow')).toEqual('1');
expect(spinButton.getAttribute('aria-valuetext')).toBeNull();
expect(spinButton.getAttribute('aria-valuemin')).toBeNull();
expect(spinButton.getAttribute('aria-valuemax')).toBeNull();

const [incrementButton, decrementButton] = getAllByRole('button');
expect(incrementButton.getAttribute('aria-label')).toEqual('Increment by 1');
expect(decrementButton.getAttribute('aria-label')).toEqual('Decrement by 1');
});

it('does not render `displayValue` when uncontrolled', () => {
Expand Down Expand Up @@ -501,4 +510,32 @@ describe('SpinButton', () => {
userEvent.type(spinButton, '23');
expect(onChange).toHaveBeenCalledTimes(6); // no change should fire
});

it('applies custom strings', () => {
const strings: SpinButtonStrings = {
incrementButtonLabel: `Increment SpinButton by {step}`,
decrementButtonLabel: `Decrement It`,
};

const { getAllByRole } = render(<SpinButton strings={strings} defaultValue={0} />);

const [incrementButton, decrementButton] = getAllByRole('button');
expect(incrementButton.getAttribute('aria-label')).toEqual('Increment SpinButton by 1');
expect(decrementButton.getAttribute('aria-label')).toEqual('Decrement It');
});

it('overrides custom strings with slot props', () => {
const strings: SpinButtonStrings = {
incrementButtonLabel: `Increment SpinButton by {step}`,
decrementButtonLabel: `Decrement It`,
};

const { getAllByRole } = render(
<SpinButton strings={strings} defaultValue={0} incrementButton={{ 'aria-label': 'Increment Override' }} />,
);

const [incrementButton, decrementButton] = getAllByRole('button');
expect(incrementButton.getAttribute('aria-label')).toEqual('Increment Override');
expect(decrementButton.getAttribute('aria-label')).toEqual('Decrement It');
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,11 @@ export type SpinButtonCommons = {
* @default all
*/
inputType: 'all' | 'spinners-only';

/**
* Strings for localizing text in the control.
*/
strings?: SpinButtonStrings;
};

/**
Expand Down Expand Up @@ -161,3 +166,17 @@ export type SpinButtonOnChangeData = {

export type SpinButtonSpinState = 'rest' | 'up' | 'down';
export type SpinButtonBounds = 'none' | 'min' | 'max' | 'both';

export type SpinButtonStrings = {
/**
* Label applied to the increment button.
* Can include the token "\{step\}" which will be replaced with the value of the `step` prop.
*/
incrementButtonLabel: string;

/**
* Label applied to the decrement button.
* Can include the token "\{step\}" which will be replaced with the value of the `step` prop.
*/
decrementButtonLabel: string;
};
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
SpinButtonChangeEvent,
SpinButtonBounds,
} from './SpinButton.types';
import { spinButtonDefaultStrings } from './SpinButton.strings';
import { calculatePrecision, precisionRound, getBound, clampWhenInRange } from '../../utils/index';
import { ChevronUp16Regular, ChevronDown16Regular } from '@fluentui/react-icons';

Expand Down Expand Up @@ -47,7 +48,7 @@ export const useSpinButton_unstable = (props: SpinButtonProps, ref: React.Ref<HT
const nativeProps = getPartitionedNativeProps({
props,
primarySlotTagName: 'input',
excludedPropNames: ['onChange', 'size'],
excludedPropNames: ['onChange', 'size', 'min', 'max'],
});

const {
Expand All @@ -67,6 +68,7 @@ export const useSpinButton_unstable = (props: SpinButtonProps, ref: React.Ref<HT
incrementButton,
decrementButton,
inputType = 'all',
strings = spinButtonDefaultStrings,
} = props;

const precision = React.useMemo(() => {
Expand Down Expand Up @@ -114,6 +116,7 @@ export const useSpinButton_unstable = (props: SpinButtonProps, ref: React.Ref<HT
autoComplete: 'off',
role: 'spinbutton',
appearance: appearance,
type: 'text',
...nativeProps.primary,
},
}),
Expand All @@ -123,6 +126,7 @@ export const useSpinButton_unstable = (props: SpinButtonProps, ref: React.Ref<HT
tabIndex: -1,
children: <ChevronUp16Regular />,
disabled: nativeProps.primary.disabled,
'aria-label': strings.incrementButtonLabel.replace('{step}', step.toString()),
},
}),
decrementButton: resolveShorthand(decrementButton, {
Expand All @@ -131,6 +135,7 @@ export const useSpinButton_unstable = (props: SpinButtonProps, ref: React.Ref<HT
tabIndex: -1,
children: <ChevronDown16Regular />,
disabled: nativeProps.primary.disabled,
'aria-label': strings.decrementButtonLabel.replace('{step}', step.toString()),
},
}),
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,20 @@ const useButtonStyles = makeStyles({
':disabled': {
color: tokens.colorNeutralForegroundDisabled,
},
'@media (forced-colors: active)': {
color: 'ButtonText',
':enabled': {
':hover': {
color: 'ButtonText',
},
':active': {
color: 'ButtonText',
},
[`&.${spinButtonExtraClassNames.buttonActive}`]: {
color: 'ButtonText',
},
},
},
},

// These designs are not yet finalized so this is copy-paste for the "outline"
Expand Down Expand Up @@ -324,6 +338,20 @@ const useButtonDisabledStyles = makeStyles({
backgroundColor: 'transparent',
},
},
'@media (forced-colors: active)': {
color: 'GrayText',
':enabled': {
':hover': {
color: 'GrayText',
},
':active': {
color: 'GrayText',
},
[`&.${spinButtonExtraClassNames.buttonActive}`]: {
color: 'GrayText',
},
},
},
},

underline: {
Expand Down
1 change: 1 addition & 0 deletions packages/react-spinbutton/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,5 @@ export type {
SpinButtonState,
SpinButtonSpinState,
SpinButtonBounds,
SpinButtonStrings,
} from './SpinButton';
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ export { Appearance } from './SpinButtonAppearance.stories';
export { RTL } from './SpinButtonRTL.stories';
export { Disabled } from './SpinButtonDisabled.stories';
export { InputType } from './SpinButtonInputType.stories';
export { Strings } from './SpinButtonStrings.stories';

import { makeStyles, mergeClasses, shorthands } from '@griffel/react';

const useDecoratorStyles = makeStyles({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,9 @@ export const Bounds = () => {

return (
<>
<Label htmlFor={id}>Bounded SpinButton (min: 0, max: 20)</Label>
<Label htmlFor={id}>Bounded SpinButton</Label>
<SpinButton value={spinButtonValue} min={0} max={20} onChange={onSpinButtonChange} id={id} />
<p>min: 0, max: 20</p>
</>
);
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ type ParserFn = (formattedValue: string) => number;

export const DisplayValue = () => {
const formatter: FormatterFn = value => {
return `${value}"`;
return `$${value}`;
};

const parser: ParserFn = formattedValue => {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import * as React from 'react';
import { SpinButton } from '../index';
import { Label } from '@fluentui/react-label';
import { useId } from '@fluentui/react-utilities';
import type { SpinButtonStrings } from '../SpinButton';

export const Strings = () => {
const id = useId();

const strings: SpinButtonStrings = {
// Uses the `{step}` token which will be replaced by the `step` prop.
incrementButtonLabel: 'Increment the SpinButton by {step}',
// Omits the `{step}` token.
decrementButtonLabel: 'Decrement',
};

return (
<>
<Label htmlFor={id}>Custom Strings</Label>
<SpinButton strings={strings} defaultValue={0} id={id} />
<p>
Inspect the <code>aria-label</code> attributes for the increment and decrement buttons in dev tools, or use a
tool like a screen reader to hear the labels announced.
</p>
</>
);
};

Strings.parameters = {
docs: {
description: {
story: `SpinButton increment and decrement button \`aria-label\`s can be customized with the \`strings\` prop.
This feature allows labels to be localized.`,
},
},
};