@bloomreach/react-banana-ui 1.40.0 → 1.41.0

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.
@@ -7,6 +7,7 @@ export * from './input-field';
7
7
  export * from './radio';
8
8
  export * from './radio-field';
9
9
  export * from './radio-group';
10
+ export * from './segmented-control';
10
11
  export * from './select-field';
11
12
  export * from './select-option';
12
13
  export * from './slider';
@@ -0,0 +1,2 @@
1
+ export { default as SegmentedControl } from './segmented-control';
2
+ export type { SegmentedControlItem, SegmentedControlProps } from './segmented-control.types';
@@ -0,0 +1,26 @@
1
+ import { SegmentedControlProps } from './segmented-control.types';
2
+ /**
3
+ * A radio-group-based control that presents a set of mutually exclusive choices
4
+ * as a row of equal-width segments with an animated thumb indicator that slides
5
+ * to the selected item.
6
+ *
7
+ * Supports controlled and uncontrolled usage, keyboard navigation (arrow keys,
8
+ * Home/End, Space/Enter), full-width layout, two sizes (`md` and `sm`),
9
+ * read-only and disabled states, icon-only segments, and native form
10
+ * participation via hidden radio inputs.
11
+ *
12
+ * @example
13
+ * ```tsx
14
+ * <SegmentedControl
15
+ * items={[
16
+ * { value: 'day', label: 'Day' },
17
+ * { value: 'week', label: 'Week' },
18
+ * { value: 'month', label: 'Month' },
19
+ * ]}
20
+ * defaultValue="day"
21
+ * onChange={(value) => console.log(value)}
22
+ * />
23
+ * ```
24
+ */
25
+ declare const SegmentedControl: import('react').ForwardRefExoticComponent<SegmentedControlProps & import('react').RefAttributes<HTMLDivElement>>;
26
+ export default SegmentedControl;
@@ -0,0 +1,18 @@
1
+ import { Args, Meta } from '@storybook/react-vite';
2
+ import { Story } from './segmented-control.stories';
3
+ import { SegmentedControlProps } from './segmented-control.types';
4
+ type StoryArgs = Args & SegmentedControlProps;
5
+ declare const meta: Meta<StoryArgs>;
6
+ export default meta;
7
+ export declare const DefaultQA: Story;
8
+ export declare const ControlledQA: Story;
9
+ export declare const WithIconsQA: Story;
10
+ export declare const IconOnlyQA: Story;
11
+ export declare const SmallSizeQA: Story;
12
+ export declare const FullWidthQA: Story;
13
+ export declare const DisabledGroupQA: Story;
14
+ export declare const DisabledItemsQA: Story;
15
+ export declare const TwoItemsQA: Story;
16
+ export declare const FiveItemsQA: Story;
17
+ export declare const ReadOnlyQA: Story;
18
+ export declare const CombinedStories: Story;
@@ -0,0 +1,18 @@
1
+ import { default as SegmentedControl } from './segmented-control';
2
+ import { Meta, StoryObj } from '@storybook/react-vite';
3
+ declare const meta: Meta<typeof SegmentedControl>;
4
+ export default meta;
5
+ export type Story = StoryObj<typeof SegmentedControl>;
6
+ export declare const Default: Story;
7
+ export declare const Controlled: Story;
8
+ export declare const Uncontrolled: Story;
9
+ export declare const WithIcons: Story;
10
+ export declare const IconOnly: Story;
11
+ export declare const SmallSize: Story;
12
+ export declare const FullWidth: Story;
13
+ export declare const DisabledGroup: Story;
14
+ export declare const DisabledItems: Story;
15
+ export declare const TwoItems: Story;
16
+ export declare const FiveItems: Story;
17
+ export declare const ReadOnly: Story;
18
+ export declare const FormIntegration: Story;
@@ -0,0 +1,93 @@
1
+ import { FocusEventHandler, HTMLAttributes, ReactNode } from 'react';
2
+ export interface SegmentedControlItem {
3
+ /**
4
+ * Accessible name for icon-only segments. Required when `label` is omitted.
5
+ */
6
+ ariaLabel?: string;
7
+ /**
8
+ * Whether this individual segment is disabled.
9
+ */
10
+ disabled?: boolean;
11
+ /**
12
+ * Icon element displayed before the label (or alone for icon-only segments).
13
+ */
14
+ icon?: ReactNode;
15
+ /**
16
+ * Text label displayed in the segment.
17
+ */
18
+ label?: string;
19
+ /**
20
+ * Unique value identifying this segment.
21
+ */
22
+ value: string;
23
+ }
24
+ export interface SegmentedControlProps extends Omit<HTMLAttributes<HTMLDivElement>, 'color' | 'onBlur' | 'onChange' | 'onFocus' | 'role' | 'tabIndex'> {
25
+ /**
26
+ * Accessible label for the radio group.
27
+ */
28
+ 'aria-label'?: string;
29
+ /**
30
+ * ID of an element that labels the radio group.
31
+ */
32
+ 'aria-labelledby'?: string;
33
+ /**
34
+ * Additional CSS class applied to the root element.
35
+ */
36
+ className?: string;
37
+ /**
38
+ * The initially selected value (uncontrolled mode).
39
+ */
40
+ defaultValue?: string;
41
+ /**
42
+ * Disables the entire control and all segments.
43
+ * @default false
44
+ */
45
+ disabled?: boolean;
46
+ /**
47
+ * Associates the hidden radio inputs with a `<form>` by its `id`.
48
+ */
49
+ form?: string;
50
+ /**
51
+ * When true, the control stretches to fill its container and distributes
52
+ * segments equally.
53
+ * @default false
54
+ */
55
+ fullWidth?: boolean;
56
+ /**
57
+ * The segments to render. Each item must have a unique `value`.
58
+ */
59
+ items: SegmentedControlItem[];
60
+ /**
61
+ * Name attribute for the hidden radio inputs, enabling form participation.
62
+ * Auto-generated if omitted.
63
+ */
64
+ name?: string;
65
+ /**
66
+ * Callback fired when a segment loses focus.
67
+ * The event bubbles to the control root.
68
+ */
69
+ onBlur?: FocusEventHandler<HTMLDivElement>;
70
+ /**
71
+ * Callback fired when the selected value changes.
72
+ */
73
+ onChange?: (value: string) => void;
74
+ /**
75
+ * Callback fired when a segment receives focus.
76
+ * The event bubbles to the control root.
77
+ */
78
+ onFocus?: FocusEventHandler<HTMLDivElement>;
79
+ /**
80
+ * When true, the control displays its value but does not allow interaction.
81
+ * @default false
82
+ */
83
+ readOnly?: boolean;
84
+ /**
85
+ * Size of the control.
86
+ * @default 'md'
87
+ */
88
+ size?: 'md' | 'sm';
89
+ /**
90
+ * The currently selected value (controlled mode).
91
+ */
92
+ value?: string;
93
+ }
@@ -0,0 +1,69 @@
1
+ import { FocusEvent, ForwardedRef, KeyboardEvent, RefCallback } from 'react';
2
+ import { SegmentedControlItem } from './segmented-control.types';
3
+ /**
4
+ * CSS transform and width values used to position the sliding thumb or focus
5
+ * indicator absolutely over a specific segment element.
6
+ */
7
+ interface IndicatorStyle {
8
+ /** CSS `translateX` value derived from the segment's `offsetLeft`. */
9
+ transform: string;
10
+ /** Pixel width derived from the segment's `offsetWidth`. */
11
+ width: string;
12
+ }
13
+ /** Parameters accepted by the `useSegmentedControl` hook. */
14
+ interface UseSegmentedControlParameters {
15
+ /** Initial selected value for uncontrolled usage. */
16
+ defaultValue?: string;
17
+ /** When `true`, all interaction is suppressed. */
18
+ disabled: boolean;
19
+ /** Ordered list of segment descriptors. */
20
+ items: SegmentedControlItem[];
21
+ /** Base name for the hidden radio inputs; auto-generated when omitted. */
22
+ name?: string;
23
+ /** Callback fired when the selected value changes. */
24
+ onChange?: (value: string) => void;
25
+ /** When `true`, the selected value is displayed but cannot be changed. */
26
+ readOnly: boolean;
27
+ /** Forwarded ref from the `SegmentedControl` component. */
28
+ ref: ForwardedRef<HTMLDivElement>;
29
+ /** Currently selected value for controlled usage. */
30
+ value?: string;
31
+ }
32
+ /** Values returned by `useSegmentedControl` for consumption by `SegmentedControl`. */
33
+ interface UseSegmentedControlReturn {
34
+ /** Style for the focus-ring indicator, or `null` when no segment is focused. */
35
+ focusIndicatorStyle: IndicatorStyle | null;
36
+ /** Capture-phase blur handler that clears the focused index when focus leaves the control. */
37
+ handleBlur: (event: FocusEvent<HTMLDivElement>) => void;
38
+ /** Capture-phase focus handler that tracks which segment item currently has focus. */
39
+ handleFocus: (event: FocusEvent<HTMLDivElement>) => void;
40
+ /** Keyboard handler supporting arrow navigation, Home/End, and Space/Enter activation. */
41
+ handleKeyDown: (event: KeyboardEvent<HTMLDivElement>) => void;
42
+ /** Selects a segment by value, guarded by disabled and read-only checks. */
43
+ handleSelect: (value: string, isItemDisabled: boolean) => void;
44
+ /** When `true`, suppresses the thumb CSS transition to prevent an unwanted initial animation. */
45
+ isThumbTransitionDisabled: boolean;
46
+ /** Merged ref combining the forwarded ref and the internal root ref. */
47
+ mergedRef: null | RefCallback<HTMLDivElement>;
48
+ /** Resolved `name` attribute shared by all hidden radio inputs in the group. */
49
+ resolvedName: string | undefined;
50
+ /** The currently selected segment value. */
51
+ selectedValue: string | undefined;
52
+ /** Stable callback to register or clear a segment element ref by its array index. */
53
+ setItemRef: (index: number, element: HTMLDivElement | null) => void;
54
+ /** Index of the segment that should receive `tabIndex={0}` for roving-tabindex navigation. */
55
+ tabbableIndex: number;
56
+ /** Style for the selected-item thumb indicator, or `null` when nothing is selected. */
57
+ thumbStyle: IndicatorStyle | null;
58
+ }
59
+ /**
60
+ * Encapsulates all state and event-handling logic for the `SegmentedControl`
61
+ * component, including:
62
+ * - Controlled/uncontrolled value management via `useControlledValue`
63
+ * - Roving-tabindex keyboard navigation
64
+ * - Animated thumb and focus-ring indicator positioning via synchronous DOM
65
+ * measurements in a layout effect
66
+ * - `ResizeObserver`-based indicator recalculation on container resize
67
+ */
68
+ declare const useSegmentedControl: (parameters: UseSegmentedControlParameters) => UseSegmentedControlReturn;
69
+ export default useSegmentedControl;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@bloomreach/react-banana-ui",
3
3
  "type": "module",
4
- "version": "1.40.0",
4
+ "version": "1.41.0",
5
5
  "private": false,
6
6
  "repository": {
7
7
  "type": "git",