Merge pull request #33112 from AmbrishRamachandiran/range-slider-component

BUI - Add new range slider component
This commit is contained in:
Charles de Dreuille
2026-03-26 14:37:00 +00:00
committed by GitHub
18 changed files with 918 additions and 3 deletions
+7
View File
@@ -0,0 +1,7 @@
---
'@backstage/ui': patch
---
Added RangeSlider component for selecting numeric ranges.
**Affected components:** RangeSlider
@@ -0,0 +1,85 @@
'use client';
import { Slider } from '../../../../../packages/ui/src/components/Slider';
export const SingleValue = () => {
return (
<Slider label="Volume" minValue={0} maxValue={100} defaultValue={50} />
);
};
export const Default = () => {
return (
<Slider
label="Price Range"
minValue={0}
maxValue={1000}
defaultValue={[200, 800]}
/>
);
};
export const WithCustomRange = () => {
return (
<Slider
label="Temperature (°C)"
minValue={-20}
maxValue={40}
defaultValue={[0, 20]}
step={5}
/>
);
};
export const WithFormattedValues = () => {
return (
<Slider
label="Budget"
minValue={0}
maxValue={10000}
defaultValue={[2000, 8000]}
step={100}
formatOptions={{
style: 'currency',
currency: 'USD',
maximumFractionDigits: 0,
}}
/>
);
};
export const WithDescription = () => {
return (
<Slider
label="Age Range"
description="Select the age range for your target audience"
minValue={0}
maxValue={100}
defaultValue={[18, 65]}
/>
);
};
export const Required = () => {
return (
<Slider
label="Score Range"
minValue={0}
maxValue={100}
defaultValue={[20, 80]}
isRequired
/>
);
};
export const Disabled = () => {
return (
<Slider
label="Disabled Range"
minValue={0}
maxValue={100}
defaultValue={[30, 70]}
isDisabled
/>
);
};
+109
View File
@@ -0,0 +1,109 @@
import { PropsTable } from '@/components/PropsTable';
import { Snippet } from '@/components/Snippet';
import { CodeBlock } from '@/components/CodeBlock';
import { ReactAriaLink } from '@/components/ReactAriaLink';
import { sliderPropDefs } from './props-definition';
import {
snippetUsage,
singleValueSnippet,
defaultSnippet,
withCustomRangeSnippet,
withFormattedValuesSnippet,
withDescriptionSnippet,
requiredSnippet,
disabledSnippet,
} from './snippets';
import {
SingleValue,
Default,
WithCustomRange,
WithFormattedValues,
WithDescription,
Required,
Disabled,
} from './components';
import { PageTitle } from '@/components/PageTitle';
import { Theming } from '@/components/Theming';
import { ChangelogComponent } from '@/components/ChangelogComponent';
import { SliderDefinition } from '../../../utils/definitions';
export const reactAriaUrls = {
slider: 'https://react-spectrum.adobe.com/react-aria/Slider.html',
};
<PageTitle
title="Slider"
description="A slider for selecting numeric values, supporting both single values and ranges with customizable formatting and validation."
/>
<Snippet align="center" py={4} preview={<Default />} code={defaultSnippet} />
## Usage
<CodeBlock code={snippetUsage} />
## API reference
<PropsTable data={sliderPropDefs} />
<ReactAriaLink component="Slider" href={reactAriaUrls.slider} />
## Examples
### Single value
Use a single number as the default value to create a single-thumb slider.
<Snippet
align="center"
py={4}
preview={<SingleValue />}
code={singleValueSnippet}
/>
### Custom range
Define custom minimum, maximum, and step values for specific use cases.
<Snippet
align="center"
py={4}
preview={<WithCustomRange />}
code={withCustomRangeSnippet}
/>
### Formatted values
Use the `formatOptions` prop with standard Intl.NumberFormat options to customize how values are displayed.
<Snippet
align="center"
py={4}
preview={<WithFormattedValues />}
code={withFormattedValuesSnippet}
/>
### With description
Add helpful context with a description below the label.
<Snippet
align="center"
py={4}
preview={<WithDescription />}
code={withDescriptionSnippet}
/>
### Required
Mark a field as required to show a "Required" indicator in the label.
<Snippet align="center" py={4} preview={<Required />} code={requiredSnippet} />
### Disabled
<Snippet align="center" py={4} preview={<Disabled />} code={disabledSnippet} />
<Theming definition={SliderDefinition} />
<ChangelogComponent component="slider" />
@@ -0,0 +1,92 @@
import { classNamePropDefs, stylePropDefs } from '@/utils/propDefs';
import type { PropDef } from '@/utils/propDefs';
export const sliderPropDefs: Record<string, PropDef> = {
label: {
type: 'string',
description: 'The label text for the slider.',
},
description: {
type: 'string',
description: 'Additional description text displayed below the label.',
},
secondaryLabel: {
type: 'string',
description:
'Optional secondary label displayed next to the main label (e.g., "Optional").',
},
isRequired: {
type: 'boolean',
description:
'Whether the field is required. Displays "Required" in the label if true.',
},
minValue: {
type: 'number',
description: 'The minimum value of the slider.',
default: '0',
},
maxValue: {
type: 'number',
description: 'The maximum value of the slider.',
default: '100',
},
step: {
type: 'number',
description: 'The step increment for slider values.',
default: '1',
},
value: {
type: 'enum',
values: ['number', '[number, number]'],
description:
'Controlled value. Use a single number for a single-thumb slider, or an array [min, max] for a range slider. Use with onChange for controlled behavior.',
},
defaultValue: {
type: 'enum',
values: ['number', '[number, number]'],
description:
'Initial value for uncontrolled usage. Use a single number for a single-thumb slider, or an array [min, max] for a range slider.',
default: 'minValue or [minValue, maxValue]',
},
onChange: {
type: 'enum',
values: ['(value: number | [number, number]) => void'],
description: 'Called when the slider value changes.',
},
onChangeEnd: {
type: 'enum',
values: ['(value: number | [number, number]) => void'],
description:
'Called when the user stops dragging, useful for triggering actions only on final values.',
},
formatOptions: {
type: 'object',
description:
'Intl.NumberFormat options for formatting the displayed value (e.g., { style: "currency", currency: "USD" }).',
},
isDisabled: {
type: 'boolean',
description: 'Prevents user interaction when true.',
},
orientation: {
type: 'enum',
values: ['horizontal', 'vertical'],
description: 'The orientation of the slider.',
default: 'horizontal',
},
name: {
type: 'string',
description: 'Form field name for form submission.',
},
'aria-label': {
type: 'string',
description:
'Accessible label for screen readers when no visible label is provided.',
},
'aria-labelledby': {
type: 'string',
description: 'ID of an element that labels the slider for accessibility.',
},
...classNamePropDefs,
...stylePropDefs,
};
@@ -0,0 +1,67 @@
export const snippetUsage = `import { Slider } from '@backstage/ui';
<Slider
label="My Range"
minValue={0}
maxValue={100}
defaultValue={[25, 75]}
/>`;
export const singleValueSnippet = `<Slider
label="Volume"
minValue={0}
maxValue={100}
defaultValue={50}
/>`;
export const defaultSnippet = `<Slider
label="Price Range"
minValue={0}
maxValue={1000}
defaultValue={[200, 800]}
/>`;
export const withCustomRangeSnippet = `<Slider
label="Temperature (°C)"
minValue={-20}
maxValue={40}
defaultValue={[0, 20]}
step={5}
/>`;
export const withFormattedValuesSnippet = `<Slider
label="Budget"
minValue={0}
maxValue={10000}
defaultValue={[2000, 8000]}
step={100}
formatOptions={{
style: 'currency',
currency: 'USD',
maximumFractionDigits: 0,
}}
/>`;
export const withDescriptionSnippet = `<Slider
label="Age Range"
description="Select the age range for your target audience"
minValue={0}
maxValue={100}
defaultValue={[18, 65]}
/>`;
export const requiredSnippet = `<Slider
label="Score Range"
minValue={0}
maxValue={100}
defaultValue={[20, 80]}
isRequired
/>`;
export const disabledSnippet = `<Slider
label="Disabled Range"
minValue={0}
maxValue={100}
defaultValue={[30, 70]}
isDisabled
/>`;
+12
View File
@@ -105,6 +105,18 @@ export const components: Page[] = [
title: 'Skeleton',
slug: 'skeleton',
},
{
title: 'Slider',
slug: 'slider',
},
{
title: 'Select',
slug: 'select',
},
{
title: 'Skeleton',
slug: 'skeleton',
},
{
title: 'Switch',
slug: 'switch',
+53
View File
@@ -42,6 +42,7 @@ import { RowProps as RowProps_2 } from 'react-aria-components';
import type { SearchFieldProps as SearchFieldProps_2 } from 'react-aria-components';
import type { SelectProps as SelectProps_2 } from 'react-aria-components';
import type { SeparatorProps } from 'react-aria-components';
import type { SliderProps as SliderProps_2 } from 'react-aria-components';
import type { SortDescriptor as SortDescriptor_2 } from 'react-stately';
import type { SubmenuTriggerProps as SubmenuTriggerProps_2 } from 'react-aria-components';
import type { SwitchProps as SwitchProps_2 } from 'react-aria-components';
@@ -1154,6 +1155,7 @@ export const FieldLabelDefinition: {
readonly description: {};
readonly htmlFor: {};
readonly id: {};
readonly descriptionId: {};
readonly className: {};
};
};
@@ -1165,6 +1167,7 @@ export type FieldLabelOwnProps = {
description?: string | null;
htmlFor?: string;
id?: string;
descriptionId?: string;
className?: string;
};
@@ -2459,6 +2462,56 @@ export interface SkeletonProps
extends Omit<ComponentProps<'div'>, 'children' | 'className' | 'style'>,
SkeletonOwnProps {}
// @public (undocumented)
export const Slider: (<T extends number | number[]>(
props: SliderProps<T> & {
ref?: React.ForwardedRef<HTMLDivElement>;
},
) => JSX.Element) & {
displayName: string;
};
// @public
export const SliderDefinition: {
readonly styles: {
readonly [key: string]: string;
};
readonly classNames: {
readonly root: 'bui-Slider';
readonly header: 'bui-SliderHeader';
readonly track: 'bui-SliderTrack';
readonly trackFill: 'bui-SliderTrackFill';
readonly thumb: 'bui-SliderThumb';
readonly output: 'bui-SliderOutput';
};
readonly propDefs: {
readonly className: {};
readonly label: {};
readonly secondaryLabel: {};
readonly description: {};
readonly isRequired: {};
};
};
// @public (undocumented)
export interface SliderOwnProps {
// (undocumented)
className?: string;
// (undocumented)
description?: FieldLabelProps['description'];
// (undocumented)
isRequired?: boolean;
// (undocumented)
label?: FieldLabelProps['label'];
// (undocumented)
secondaryLabel?: FieldLabelProps['secondaryLabel'];
}
// @public (undocumented)
export interface SliderProps<T extends number | number[]>
extends Omit<SliderProps_2<T>, 'children' | 'className'>,
SliderOwnProps {}
// @public (undocumented)
export type SortDescriptor = SortDescriptor_2;
@@ -23,8 +23,15 @@ import { FieldLabelDefinition } from './definition';
export const FieldLabel = forwardRef<HTMLDivElement, FieldLabelProps>(
(props: FieldLabelProps, ref) => {
const { ownProps, restProps } = useDefinition(FieldLabelDefinition, props);
const { classes, label, secondaryLabel, description, htmlFor, id } =
ownProps;
const {
classes,
label,
secondaryLabel,
description,
htmlFor,
id,
descriptionId,
} = ownProps;
if (!label) return null;
@@ -41,7 +48,9 @@ export const FieldLabel = forwardRef<HTMLDivElement, FieldLabelProps>(
</Label>
)}
{description && (
<div className={classes.description}>{description}</div>
<div className={classes.description} id={descriptionId}>
{description}
</div>
)}
</div>
);
@@ -36,6 +36,7 @@ export const FieldLabelDefinition = defineComponent<FieldLabelOwnProps>()({
description: {},
htmlFor: {},
id: {},
descriptionId: {},
className: {},
},
});
@@ -41,6 +41,11 @@ export type FieldLabelOwnProps = {
*/
id?: string;
/**
* The id to apply to the description element for aria-describedby
*/
descriptionId?: string;
className?: string;
};
@@ -0,0 +1,135 @@
/*
* Copyright 2026 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
@layer tokens, base, components, utilities;
@layer components {
.bui-Slider {
display: flex;
flex-direction: column;
gap: var(--bui-space-2);
width: 100%;
color: var(--bui-fg-primary);
&[data-orientation='vertical'] {
height: 200px;
width: auto;
}
}
.bui-SliderHeader {
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: var(--bui-space-3);
}
.bui-SliderOutput {
font-size: var(--bui-font-size-2);
font-weight: var(--bui-font-weight-medium);
color: var(--bui-fg-secondary);
white-space: nowrap;
}
.bui-SliderTrack {
position: relative;
height: 4px;
width: 100%;
background: var(--bui-bg-neutral-3);
border-radius: var(--bui-radius-sm);
cursor: pointer;
&[data-disabled] {
cursor: not-allowed;
}
/* Vertical orientation */
[data-orientation='vertical'] & {
width: 4px;
height: 100%;
}
}
.bui-SliderTrackFill {
position: absolute;
top: 0;
height: 100%;
background: var(--bui-bg-solid);
border-radius: var(--bui-radius-sm);
pointer-events: none;
/* Vertical orientation */
[data-orientation='vertical'] & {
width: 100%;
height: auto;
left: 0;
right: 0;
top: auto;
}
}
.bui-SliderThumb {
width: 20px;
height: 20px;
border-radius: 50%;
background: var(--bui-bg-solid);
border: 2px solid var(--bui-bg-solid);
box-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);
cursor: grab;
transition: transform 200ms;
/* Fix: Ensure thumb is vertically centered on track */
top: 50%;
transform: translateY(-50%);
&[data-focus-visible] {
outline: 2px solid var(--bui-ring);
outline-offset: 2px;
}
&[data-dragging] {
cursor: grabbing;
box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1),
0 2px 4px -2px rgb(0 0 0 / 0.1);
transform: translateY(-50%) scale(1.1);
}
/* Hover effect */
&:hover:not([data-disabled]) {
transform: translateY(-50%) scale(1.1);
}
}
/* Improved disabled state */
.bui-Slider[data-disabled] {
opacity: 0.6;
cursor: not-allowed;
& .bui-SliderTrack {
background: var(--bui-bg-neutral-2);
}
& .bui-SliderTrackFill {
background: var(--bui-bg-neutral-4);
}
& .bui-SliderThumb {
cursor: not-allowed;
background: var(--bui-bg-neutral-4);
border-color: var(--bui-border-neutral);
}
}
}
@@ -0,0 +1,122 @@
/*
* Copyright 2026 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import preview from '../../../../../.storybook/preview';
import { Slider } from './Slider';
const meta = preview.meta({
title: 'Backstage UI/Slider',
component: Slider,
});
export const SingleThumb = meta.story({
args: {
label: 'Volume',
defaultValue: 50,
},
});
export const SingleThumbWithRange = meta.story({
args: {
label: 'Brightness',
minValue: 0,
maxValue: 100,
defaultValue: 75,
step: 5,
},
});
export const RangeSlider = meta.story({
args: {
label: 'Price Range',
defaultValue: [25, 75],
},
});
export const WithCustomRange = meta.story({
args: {
label: 'Temperature (°C)',
minValue: -20,
maxValue: 40,
defaultValue: [0, 20],
step: 5,
},
});
export const WithFormattedValues = meta.story({
args: {
label: 'Budget',
minValue: 0,
maxValue: 10000,
defaultValue: [2000, 8000],
step: 100,
formatOptions: {
style: 'currency',
currency: 'USD',
maximumFractionDigits: 0,
},
},
});
export const WithDescription = meta.story({
args: {
label: 'Age Range',
description: 'Select the age range for your target audience',
minValue: 0,
maxValue: 100,
defaultValue: [18, 65],
},
});
export const Required = meta.story({
args: {
label: 'Score Range',
defaultValue: [20, 80],
isRequired: true,
},
});
export const Disabled = meta.story({
args: {
label: 'Disabled Range',
defaultValue: [30, 70],
isDisabled: true,
},
});
export const WithSteps = meta.story({
args: {
label: 'Rating',
minValue: 0,
maxValue: 5,
step: 0.5,
defaultValue: 3.5,
},
});
export const Percentage = meta.story({
args: {
label: 'Completion',
minValue: 0,
maxValue: 1,
step: 0.01,
defaultValue: 0.65,
formatOptions: {
style: 'percent',
minimumFractionDigits: 0,
maximumFractionDigits: 0,
},
},
});
@@ -0,0 +1,123 @@
/*
* Copyright 2026 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { forwardRef, useEffect, useId } from 'react';
import {
Slider as AriaSlider,
SliderTrack,
SliderThumb,
SliderOutput,
} from 'react-aria-components';
import clsx from 'clsx';
import { FieldLabel } from '../FieldLabel';
import { FieldError } from '../FieldError';
import type { SliderProps } from './types';
import { useDefinition } from '../../hooks/useDefinition';
import { SliderDefinition } from './definition';
function SliderImpl<T extends number | number[]>(
props: SliderProps<T>,
ref: React.ForwardedRef<HTMLDivElement>,
) {
const { ownProps, restProps } = useDefinition(SliderDefinition, props);
const { classes, className, label, secondaryLabel, description, isRequired } =
ownProps;
const labelId = useId();
const descriptionId = useId();
useEffect(() => {
if (!label && !restProps['aria-label'] && !restProps['aria-labelledby']) {
console.warn(
'Slider requires either a visible label, aria-label, or aria-labelledby for accessibility',
);
}
}, [label, restProps]);
const secondaryLabelText = secondaryLabel || (isRequired ? 'Required' : null);
return (
<AriaSlider
className={clsx(classes.root, className)}
aria-labelledby={label ? labelId : undefined}
aria-describedby={label && description ? descriptionId : undefined}
{...restProps}
ref={ref}
>
{label && (
<div className={classes.header}>
<FieldLabel
id={labelId}
label={label}
secondaryLabel={secondaryLabelText}
description={description}
descriptionId={description ? descriptionId : undefined}
/>
<SliderOutput className={classes.output}>
{({ state }) =>
state.values
.map((_, i) => state.getThumbValueLabel(i))
.join(' ')
}
</SliderOutput>
</div>
)}
<SliderTrack className={classes.track}>
{({ state }) => {
const numThumbs = state.values.length;
// Calculate track fill
let trackFillStyle: React.CSSProperties;
if (numThumbs === 1) {
// Single thumb: fill from start to thumb
const percent = state.getThumbPercent(0);
const isVertical = state.orientation === 'vertical';
trackFillStyle = isVertical
? { bottom: 0, height: `${percent * 100}%` }
: { left: 0, width: `${percent * 100}%` };
} else {
// Range: fill between thumbs
const start = state.getThumbPercent(0);
const end = state.getThumbPercent(1);
const rangePercent = (end - start) * 100;
const isVertical = state.orientation === 'vertical';
trackFillStyle = isVertical
? { bottom: `${start * 100}%`, height: `${rangePercent}%` }
: { left: `${start * 100}%`, width: `${rangePercent}%` };
}
return (
<>
<div className={classes.trackFill} style={trackFillStyle} />
<SliderThumb index={0} className={classes.thumb} />
{numThumbs > 1 && (
<SliderThumb index={1} className={classes.thumb} />
)}
</>
);
}}
</SliderTrack>
<FieldError />
</AriaSlider>
);
}
/** @public */
export const Slider = forwardRef(SliderImpl) as (<T extends number | number[]>(
props: SliderProps<T> & { ref?: React.ForwardedRef<HTMLDivElement> },
) => JSX.Element) & { displayName: string };
Slider.displayName = 'Slider';
@@ -0,0 +1,42 @@
/*
* Copyright 2026 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { defineComponent } from '../../hooks/useDefinition';
import type { SliderOwnProps } from './types';
import styles from './Slider.module.css';
/**
* Component definition for Slider
* @public
*/
export const SliderDefinition = defineComponent<SliderOwnProps>()({
styles,
classNames: {
root: 'bui-Slider',
header: 'bui-SliderHeader',
track: 'bui-SliderTrack',
trackFill: 'bui-SliderTrackFill',
thumb: 'bui-SliderThumb',
output: 'bui-SliderOutput',
},
propDefs: {
className: {},
label: {},
secondaryLabel: {},
description: {},
isRequired: {},
},
});
@@ -0,0 +1,19 @@
/*
* Copyright 2026 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export { Slider } from './Slider';
export * from './types';
export { SliderDefinition } from './definition';
@@ -0,0 +1,32 @@
/*
* Copyright 2026 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type { SliderProps as AriaSliderProps } from 'react-aria-components';
import type { FieldLabelProps } from '../FieldLabel/types';
/** @public */
export interface SliderOwnProps {
className?: string;
label?: FieldLabelProps['label'];
secondaryLabel?: FieldLabelProps['secondaryLabel'];
description?: FieldLabelProps['description'];
isRequired?: boolean;
}
/** @public */
export interface SliderProps<T extends number | number[]>
extends Omit<AriaSliderProps<T>, 'children' | 'className'>,
SliderOwnProps {}
+1
View File
@@ -63,6 +63,7 @@ export {
export { SearchFieldDefinition } from './components/SearchField/definition';
export { SelectDefinition } from './components/Select/definition';
export { SkeletonDefinition } from './components/Skeleton/definition';
export { SliderDefinition } from './components/Slider/definition';
export { SwitchDefinition } from './components/Switch/definition';
export { ToggleButtonDefinition } from './components/ToggleButton/definition';
export { ToggleButtonGroupDefinition } from './components/ToggleButtonGroup/definition';
+1
View File
@@ -41,6 +41,7 @@ export * from './components/ButtonIcon';
export * from './components/ButtonLink';
export * from './components/Checkbox';
export * from './components/RadioGroup';
export * from './components/Slider';
export * from './components/Table';
export * from './components/TablePagination';
export * from './components/Tabs';