@bloomreach/react-banana-ui 1.40.0 → 1.42.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;
@@ -0,0 +1,33 @@
1
+ import { HTMLAttributes, ReactNode } from 'react';
2
+ import { AnimationState } from './modal-utils';
3
+ import { ModalWidth } from './modal.types';
4
+ /** Props accepted by {@link ModalTransition}. */
5
+ export interface ModalTransitionProps extends HTMLAttributes<HTMLDivElement> {
6
+ /** Content to render inside the animated wrapper. */
7
+ children: ReactNode;
8
+ /** Whether the modal is in the open (entered) state. */
9
+ in: boolean;
10
+ /** Called once when the enter transition starts. */
11
+ onEnter?: () => void;
12
+ /** Called once when the exit transition has fully completed. */
13
+ onExited?: () => void;
14
+ /** Called each time the animation phase changes. */
15
+ onStateChange?: (state: AnimationState) => void;
16
+ /** Width variant forwarded as a BEM modifier class on the wrapper element. */
17
+ width: ModalWidth;
18
+ }
19
+ /**
20
+ * Internal wrapper that drives the CSS enter/exit animation for the modal.
21
+ *
22
+ * Applies `data-entering` / `data-entered` / `data-exiting` / `data-exited`
23
+ * attributes and matching BEM modifier classes so that SCSS can target each
24
+ * animation phase independently.
25
+ *
26
+ * The exit transition is also guarded by a fallback `setTimeout` so that
27
+ * `onExited` fires reliably even when the CSS `transitionend` event is
28
+ * suppressed — for example under `prefers-reduced-motion` or in jsdom tests.
29
+ *
30
+ * @internal
31
+ */
32
+ declare const ModalTransition: import('react').ForwardRefExoticComponent<ModalTransitionProps & import('react').RefAttributes<HTMLDivElement>>;
33
+ export default ModalTransition;
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Fallback buffer added to the exit timeout to account for timing imprecision
3
+ * when the CSS `transitionend` event does not fire (e.g. reduced-motion, test
4
+ * environments).
5
+ */
6
+ export declare const EXIT_FALLBACK_BUFFER_MS = 20;
7
+ /**
8
+ * Default animation duration used when the CSS transition duration cannot be
9
+ * determined from the computed style of the element.
10
+ */
11
+ export declare const DEFAULT_ANIMATION_DURATION_MS = 200;
12
+ /**
13
+ * Represents the four discrete phases of a CSS enter/exit animation cycle.
14
+ *
15
+ * - `entering` — transition has been triggered but has not yet started (first rAF)
16
+ * - `entered` — element is fully visible
17
+ * - `exiting` — exit transition is in progress
18
+ * - `exited` — element is fully hidden
19
+ */
20
+ export type AnimationState = 'entered' | 'entering' | 'exited' | 'exiting';
21
+ /**
22
+ * Parses a CSS time value string into milliseconds.
23
+ *
24
+ * Handles both `ms` (e.g. `"200ms"`) and `s` (e.g. `"0.2s"`) units.
25
+ * Returns `0` for empty or unrecognised values.
26
+ *
27
+ * @param value - Raw CSS time string from `getComputedStyle`.
28
+ * @returns Duration in milliseconds.
29
+ */
30
+ export declare const parseCssTimeToMs: (value: string) => number;
31
+ /**
32
+ * Returns the maximum total transition time (duration + delay) across all CSS
33
+ * transitions applied to `element`, in milliseconds.
34
+ *
35
+ * When no positive transition time is found the function falls back to the
36
+ * `--rbui-modal-animation-duration` CSS custom property, and then to
37
+ * {@link DEFAULT_ANIMATION_DURATION_MS}.
38
+ *
39
+ * @param element - The DOM element whose computed style is inspected.
40
+ * @returns Maximum transition time in milliseconds.
41
+ */
42
+ export declare const getMaxTransitionTimeMs: (element: HTMLDivElement | null) => number;
43
+ /**
44
+ * Maps an {@link AnimationState} to a set of `data-*` attributes suitable for
45
+ * spreading onto a DOM element.
46
+ *
47
+ * Only the attribute corresponding to the current `animationState` is set to
48
+ * `true`; the remaining attributes are `undefined` so they are omitted from
49
+ * the rendered HTML.
50
+ *
51
+ * @param animationState - The current animation phase.
52
+ * @returns An object of `data-*` attribute key/value pairs.
53
+ */
54
+ export declare const getAnimationStateAttributes: (animationState: AnimationState) => Record<string, true | undefined>;
55
+ /**
56
+ * Returns a ref whose `.current` is always synchronised to the latest value of
57
+ * `value`.
58
+ *
59
+ * Useful for reading the most-recent version of a prop or callback inside a
60
+ * stable `useEffect` or `useCallback` without listing it as a dependency,
61
+ * avoiding stale-closure bugs.
62
+ *
63
+ * @param value - The value to keep up-to-date in the ref.
64
+ * @returns A mutable ref object whose `.current` mirrors `value`.
65
+ */
66
+ export declare const useLatestRef: <T>(value: T) => {
67
+ current: T;
68
+ };
@@ -1,35 +1,30 @@
1
1
  import { ModalProps } from './modal.types';
2
2
  /**
3
- * Modal component lets you create dialogs that force the user to take action before continuing
3
+ * Modal component lets you create dialogs that force the user to take action
4
+ * before continuing.
5
+ *
6
+ * The modal traps focus while open, locks the page scroll, and renders its
7
+ * content inside a {@link Portal} so it sits above all other content in the
8
+ * stacking context.
4
9
  *
5
10
  * ## Usage
6
11
  *
7
12
  * ```tsx
8
- * <Modal open>
9
- * <ModalHeader>
10
- * <ModalHeaderTitle>
11
- * Modal header
12
- * </ModalHeaderTitle>
13
- * </ModalHeader>
14
- * <ModalBody>
15
- * Design systems bridge the gap between creativity and consistency, ensuring a harmonious user experience
16
- * across platforms. By establishing a set of guidelines and components, they empower designers to craft
17
- * cohesive digital environments.
18
- * </ModalBody>
19
- * <ModalFooter>
20
- * <ModalFooterActions>
21
- * <ModalCloseButton>
22
- * Cancel
23
- * </ModalCloseButton>
24
- * <BrButton
25
- * onClick={() => {}}
26
- * type="primary"
27
- * >
28
- * Primary
29
- * </BrButton>
30
- * </ModalFooterActions>
31
- * </ModalFooter>
32
- * </Modal>
13
+ * <Modal open>
14
+ * <ModalHeader>
15
+ * <ModalHeaderTitle>Modal header</ModalHeaderTitle>
16
+ * </ModalHeader>
17
+ * <ModalBody>
18
+ * Design systems bridge the gap between creativity and consistency,
19
+ * ensuring a harmonious user experience across platforms.
20
+ * </ModalBody>
21
+ * <ModalFooter>
22
+ * <ModalFooterActions>
23
+ * <ModalCloseButton>Cancel</ModalCloseButton>
24
+ * <BrButton onClick={() => {}} type="primary">Primary</BrButton>
25
+ * </ModalFooterActions>
26
+ * </ModalFooter>
27
+ * </Modal>
33
28
  * ```
34
29
  */
35
30
  declare const Modal: import('react').ForwardRefExoticComponent<ModalProps & import('react').RefAttributes<HTMLDivElement>>;
@@ -0,0 +1,32 @@
1
+ import { AnimationState } from './modal-utils';
2
+ /** Options accepted by {@link useModalPresence}. */
3
+ interface UseModalPresenceOptions {
4
+ /** Whether the modal's DOM tree should be kept in the document after closing. */
5
+ keepMounted: boolean;
6
+ /** Whether the modal is currently open. */
7
+ open: boolean;
8
+ }
9
+ /** Return value of {@link useModalPresence}. */
10
+ interface UseModalPresenceResult {
11
+ /** Current animation phase of the modal. */
12
+ animationState: AnimationState;
13
+ /** Callback to advance the animation state; also drives the mount/unmount lifecycle. */
14
+ handleAnimationStateChange: (state: AnimationState) => void;
15
+ /** Whether the modal's subtree should be rendered into the DOM. */
16
+ isMounted: boolean;
17
+ }
18
+ /**
19
+ * Manages the mounted/unmounted lifecycle and animation state of a modal.
20
+ *
21
+ * The modal is kept in the DOM while it is open or while `keepMounted` is
22
+ * `true`. Once the exit animation completes (i.e. `animationState` reaches
23
+ * `"exited"`) and `keepMounted` is `false`, the modal is unmounted.
24
+ *
25
+ * @param options - Configuration for keep-mounted behaviour and open state.
26
+ * @param options.keepMounted - Whether the modal DOM tree should be retained after closing.
27
+ * @param options.open - Whether the modal is currently open.
28
+ * @returns The current animation state, a state-change handler, and a flag
29
+ * indicating whether the modal subtree should be mounted.
30
+ */
31
+ export declare const useModalPresence: ({ keepMounted, open }: UseModalPresenceOptions) => UseModalPresenceResult;
32
+ export {};
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.42.0",
5
5
  "private": false,
6
6
  "repository": {
7
7
  "type": "git",