@bacon-pos/ui 0.15.1 → 0.16.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.
- package/lib/commonjs/components/SegmentedButton/SegmentedButton.js +217 -0
- package/lib/commonjs/components/SegmentedButton/SegmentedButton.js.map +1 -0
- package/lib/commonjs/components/SegmentedButton/SegmentedButton.stories.js +215 -0
- package/lib/commonjs/components/SegmentedButton/SegmentedButton.stories.js.map +1 -0
- package/lib/commonjs/components/SegmentedButton/SegmentedButton.test.js +97 -0
- package/lib/commonjs/components/SegmentedButton/SegmentedButton.test.js.map +1 -0
- package/lib/commonjs/components/SegmentedButton/index.js +13 -0
- package/lib/commonjs/components/SegmentedButton/index.js.map +1 -0
- package/lib/commonjs/components/index.js +21 -10
- package/lib/commonjs/components/index.js.map +1 -1
- package/lib/module/components/SegmentedButton/SegmentedButton.js +213 -0
- package/lib/module/components/SegmentedButton/SegmentedButton.js.map +1 -0
- package/lib/module/components/SegmentedButton/SegmentedButton.stories.js +211 -0
- package/lib/module/components/SegmentedButton/SegmentedButton.stories.js.map +1 -0
- package/lib/module/components/SegmentedButton/SegmentedButton.test.js +96 -0
- package/lib/module/components/SegmentedButton/SegmentedButton.test.js.map +1 -0
- package/lib/module/components/SegmentedButton/index.js +4 -0
- package/lib/module/components/SegmentedButton/index.js.map +1 -0
- package/lib/module/components/index.js +1 -0
- package/lib/module/components/index.js.map +1 -1
- package/lib/typescript/commonjs/components/SegmentedButton/SegmentedButton.d.ts +68 -0
- package/lib/typescript/commonjs/components/SegmentedButton/SegmentedButton.d.ts.map +1 -0
- package/lib/typescript/commonjs/components/SegmentedButton/index.d.ts +2 -0
- package/lib/typescript/commonjs/components/SegmentedButton/index.d.ts.map +1 -0
- package/lib/typescript/commonjs/components/index.d.ts +1 -0
- package/lib/typescript/commonjs/components/index.d.ts.map +1 -1
- package/lib/typescript/module/components/SegmentedButton/SegmentedButton.d.ts +68 -0
- package/lib/typescript/module/components/SegmentedButton/SegmentedButton.d.ts.map +1 -0
- package/lib/typescript/module/components/SegmentedButton/index.d.ts +2 -0
- package/lib/typescript/module/components/SegmentedButton/index.d.ts.map +1 -0
- package/lib/typescript/module/components/index.d.ts +1 -0
- package/lib/typescript/module/components/index.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/components/SegmentedButton/SegmentedButton.tsx +269 -0
- package/src/components/SegmentedButton/index.ts +7 -0
- package/src/components/index.ts +1 -0
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
import React from 'react';
|
|
4
|
+
import { render, screen, fireEvent } from '@testing-library/react-native';
|
|
5
|
+
import { SegmentedButton } from "./SegmentedButton.js";
|
|
6
|
+
import { ThemeProvider } from "../../theme/index.js";
|
|
7
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
8
|
+
function renderWithTheme(ui) {
|
|
9
|
+
return render(/*#__PURE__*/_jsx(ThemeProvider, {
|
|
10
|
+
children: ui
|
|
11
|
+
}));
|
|
12
|
+
}
|
|
13
|
+
const OPTS = [{
|
|
14
|
+
label: 'One',
|
|
15
|
+
value: '1'
|
|
16
|
+
}, {
|
|
17
|
+
label: 'Two',
|
|
18
|
+
value: '2'
|
|
19
|
+
}, {
|
|
20
|
+
label: 'Three',
|
|
21
|
+
value: '3'
|
|
22
|
+
}];
|
|
23
|
+
describe('SegmentedButton', () => {
|
|
24
|
+
it('renders one segment per option', () => {
|
|
25
|
+
renderWithTheme(/*#__PURE__*/_jsx(SegmentedButton, {
|
|
26
|
+
options: OPTS
|
|
27
|
+
}));
|
|
28
|
+
expect(screen.getByText('One')).toBeTruthy();
|
|
29
|
+
expect(screen.getByText('Two')).toBeTruthy();
|
|
30
|
+
expect(screen.getByText('Three')).toBeTruthy();
|
|
31
|
+
});
|
|
32
|
+
it('selects the first option by default (uncontrolled) and reports changes', () => {
|
|
33
|
+
const onChange = jest.fn();
|
|
34
|
+
renderWithTheme(/*#__PURE__*/_jsx(SegmentedButton, {
|
|
35
|
+
options: OPTS,
|
|
36
|
+
onChange: onChange
|
|
37
|
+
}));
|
|
38
|
+
expect(screen.getByTestId('segment-1').props.accessibilityState.selected).toBe(true);
|
|
39
|
+
fireEvent.press(screen.getByText('Two'));
|
|
40
|
+
expect(onChange).toHaveBeenCalledWith('2');
|
|
41
|
+
expect(screen.getByTestId('segment-2').props.accessibilityState.selected).toBe(true);
|
|
42
|
+
});
|
|
43
|
+
it('honors defaultValue', () => {
|
|
44
|
+
renderWithTheme(/*#__PURE__*/_jsx(SegmentedButton, {
|
|
45
|
+
options: OPTS,
|
|
46
|
+
defaultValue: "3"
|
|
47
|
+
}));
|
|
48
|
+
expect(screen.getByTestId('segment-3').props.accessibilityState.selected).toBe(true);
|
|
49
|
+
});
|
|
50
|
+
it('respects a controlled value and does not self-update', () => {
|
|
51
|
+
const onChange = jest.fn();
|
|
52
|
+
renderWithTheme(/*#__PURE__*/_jsx(SegmentedButton, {
|
|
53
|
+
options: OPTS,
|
|
54
|
+
value: "2",
|
|
55
|
+
onChange: onChange
|
|
56
|
+
}));
|
|
57
|
+
expect(screen.getByTestId('segment-2').props.accessibilityState.selected).toBe(true);
|
|
58
|
+
fireEvent.press(screen.getByText('Three'));
|
|
59
|
+
expect(onChange).toHaveBeenCalledWith('3');
|
|
60
|
+
// value prop still "2" → stays selected
|
|
61
|
+
expect(screen.getByTestId('segment-2').props.accessibilityState.selected).toBe(true);
|
|
62
|
+
});
|
|
63
|
+
it('does not fire when the whole control is disabled', () => {
|
|
64
|
+
const onChange = jest.fn();
|
|
65
|
+
renderWithTheme(/*#__PURE__*/_jsx(SegmentedButton, {
|
|
66
|
+
options: OPTS,
|
|
67
|
+
disabled: true,
|
|
68
|
+
onChange: onChange
|
|
69
|
+
}));
|
|
70
|
+
fireEvent.press(screen.getByText('Two'));
|
|
71
|
+
expect(onChange).not.toHaveBeenCalled();
|
|
72
|
+
});
|
|
73
|
+
it('does not fire for a disabled segment but works for others', () => {
|
|
74
|
+
const onChange = jest.fn();
|
|
75
|
+
const opts = [{
|
|
76
|
+
label: 'A',
|
|
77
|
+
value: 'a'
|
|
78
|
+
}, {
|
|
79
|
+
label: 'B',
|
|
80
|
+
value: 'b',
|
|
81
|
+
disabled: true
|
|
82
|
+
}, {
|
|
83
|
+
label: 'C',
|
|
84
|
+
value: 'c'
|
|
85
|
+
}];
|
|
86
|
+
renderWithTheme(/*#__PURE__*/_jsx(SegmentedButton, {
|
|
87
|
+
options: opts,
|
|
88
|
+
onChange: onChange
|
|
89
|
+
}));
|
|
90
|
+
fireEvent.press(screen.getByText('B'));
|
|
91
|
+
expect(onChange).not.toHaveBeenCalled();
|
|
92
|
+
fireEvent.press(screen.getByText('C'));
|
|
93
|
+
expect(onChange).toHaveBeenCalledWith('c');
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
//# sourceMappingURL=SegmentedButton.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"names":["React","render","screen","fireEvent","SegmentedButton","ThemeProvider","jsx","_jsx","renderWithTheme","ui","children","OPTS","label","value","describe","it","options","expect","getByText","toBeTruthy","onChange","jest","fn","getByTestId","props","accessibilityState","selected","toBe","press","toHaveBeenCalledWith","defaultValue","disabled","not","toHaveBeenCalled","opts"],"sourceRoot":"..\\..\\..\\..\\src","sources":["components/SegmentedButton/SegmentedButton.test.tsx"],"mappings":"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","ignoreList":[]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"names":["SegmentedButton"],"sourceRoot":"..\\..\\..\\..\\src","sources":["components/SegmentedButton/index.ts"],"mappings":";;AAAA,SACEA,eAAe,QAKV,sBAAmB","ignoreList":[]}
|
|
@@ -24,6 +24,7 @@ export * from "./ProductCard/index.js";
|
|
|
24
24
|
export * from "./ProgressBar/index.js";
|
|
25
25
|
export * from "./QuantityStepper/index.js";
|
|
26
26
|
export * from "./Radio/index.js";
|
|
27
|
+
export * from "./SegmentedButton/index.js";
|
|
27
28
|
export * from "./Select/index.js";
|
|
28
29
|
export * from "./Sidebar/index.js";
|
|
29
30
|
export * from "./Spinner/index.js";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"names":[],"sourceRoot":"..\\..\\..\\src","sources":["components/index.ts"],"mappings":";;AAAA,cAAc,kBAAS;AACvB,cAAc,mBAAU;AACxB,cAAc,kBAAS;AACvB,cAAc,kBAAS;AACvB,cAAc,mBAAU;AACxB,cAAc,iBAAQ;AACtB,cAAc,qBAAY;AAC1B,cAAc,iBAAQ;AACtB,cAAc,sBAAa;AAC3B,cAAc,sBAAa;AAC3B,cAAc,mBAAU;AACxB,cAAc,qBAAY;AAC1B,cAAc,uBAAc;AAC5B,cAAc,iBAAQ;AACtB,cAAc,uBAAc;AAC5B,cAAc,kBAAS;AACvB,cAAc,mBAAU;AACxB,cAAc,uBAAc;AAC5B,cAAc,iBAAQ;AACtB,cAAc,iBAAQ;AACtB,cAAc,wBAAe;AAC7B,cAAc,wBAAe;AAC7B,cAAc,4BAAmB;AACjC,cAAc,kBAAS;AACvB,cAAc,mBAAU;AACxB,cAAc,oBAAW;AACzB,cAAc,oBAAW;AACzB,cAAc,qBAAY;AAC1B,cAAc,oBAAW;AACzB,cAAc,mBAAU;AACxB,cAAc,kBAAS;AACvB,cAAc,iBAAQ;AACtB,cAAc,kBAAS;AACvB,cAAc,qBAAY","ignoreList":[]}
|
|
1
|
+
{"version":3,"names":[],"sourceRoot":"..\\..\\..\\src","sources":["components/index.ts"],"mappings":";;AAAA,cAAc,kBAAS;AACvB,cAAc,mBAAU;AACxB,cAAc,kBAAS;AACvB,cAAc,kBAAS;AACvB,cAAc,mBAAU;AACxB,cAAc,iBAAQ;AACtB,cAAc,qBAAY;AAC1B,cAAc,iBAAQ;AACtB,cAAc,sBAAa;AAC3B,cAAc,sBAAa;AAC3B,cAAc,mBAAU;AACxB,cAAc,qBAAY;AAC1B,cAAc,uBAAc;AAC5B,cAAc,iBAAQ;AACtB,cAAc,uBAAc;AAC5B,cAAc,kBAAS;AACvB,cAAc,mBAAU;AACxB,cAAc,uBAAc;AAC5B,cAAc,iBAAQ;AACtB,cAAc,iBAAQ;AACtB,cAAc,wBAAe;AAC7B,cAAc,wBAAe;AAC7B,cAAc,4BAAmB;AACjC,cAAc,kBAAS;AACvB,cAAc,4BAAmB;AACjC,cAAc,mBAAU;AACxB,cAAc,oBAAW;AACzB,cAAc,oBAAW;AACzB,cAAc,qBAAY;AAC1B,cAAc,oBAAW;AACzB,cAAc,mBAAU;AACxB,cAAc,kBAAS;AACvB,cAAc,iBAAQ;AACtB,cAAc,kBAAS;AACvB,cAAc,qBAAY","ignoreList":[]}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { type StyleProp, type ViewStyle } from 'react-native';
|
|
2
|
+
import type { IconName } from '../Icon/icons.generated';
|
|
3
|
+
export type SegmentedButtonSize = 'sm' | 'md';
|
|
4
|
+
/** `default` = soft-rounded rectangle; `round` = fully rounded (pill). */
|
|
5
|
+
export type SegmentedButtonShape = 'default' | 'round';
|
|
6
|
+
export interface SegmentedOption {
|
|
7
|
+
/** Visible label. */
|
|
8
|
+
label: string;
|
|
9
|
+
/** Unique value reported on change. */
|
|
10
|
+
value: string;
|
|
11
|
+
/** Optional icon shown next to the label. */
|
|
12
|
+
icon?: IconName;
|
|
13
|
+
/** Icon side. Defaults to `left`. */
|
|
14
|
+
iconPosition?: 'left' | 'right';
|
|
15
|
+
/** Disable just this segment. */
|
|
16
|
+
disabled?: boolean;
|
|
17
|
+
}
|
|
18
|
+
export interface SegmentedButtonProps {
|
|
19
|
+
/** The segments. Two or more recommended; any count is supported. */
|
|
20
|
+
options: SegmentedOption[];
|
|
21
|
+
/** Selected value (controlled). */
|
|
22
|
+
value?: string;
|
|
23
|
+
/** Initial selected value (uncontrolled). Defaults to the first option. */
|
|
24
|
+
defaultValue?: string;
|
|
25
|
+
/** Change handler. */
|
|
26
|
+
onChange?: (value: string) => void;
|
|
27
|
+
/** Size: `sm` (16px text) or `md` (18px text, default). */
|
|
28
|
+
size?: SegmentedButtonSize;
|
|
29
|
+
/** Corner style: `default` (rounded rect) or `round` (pill). Defaults to `default`. */
|
|
30
|
+
shape?: SegmentedButtonShape;
|
|
31
|
+
/** Override the track corner radius in px (segments follow). Wins over `shape`. */
|
|
32
|
+
radius?: number;
|
|
33
|
+
/** Stretch to the container; segments share the width equally. */
|
|
34
|
+
fullWidth?: boolean;
|
|
35
|
+
/** Fixed track width in px (overrides `fullWidth`). */
|
|
36
|
+
width?: number;
|
|
37
|
+
/** Disable the whole control. */
|
|
38
|
+
disabled?: boolean;
|
|
39
|
+
/** Show the track border. Defaults to true. */
|
|
40
|
+
bordered?: boolean;
|
|
41
|
+
/** Track background color. Defaults to `background.muted`. */
|
|
42
|
+
trackColor?: string;
|
|
43
|
+
/** Selected segment background. Defaults to `action.inverse` (dark). */
|
|
44
|
+
selectedColor?: string;
|
|
45
|
+
/** Selected segment text/icon color. Defaults to `action.inverseForeground`. */
|
|
46
|
+
selectedTextColor?: string;
|
|
47
|
+
/** Unselected text/icon color. Defaults to `text.secondary`. */
|
|
48
|
+
textColor?: string;
|
|
49
|
+
/** Icon size override. Defaults to the size preset. */
|
|
50
|
+
iconSize?: number;
|
|
51
|
+
/** Slide the selected highlight between segments. Defaults to true. */
|
|
52
|
+
animated?: boolean;
|
|
53
|
+
/** Override the track container style. */
|
|
54
|
+
style?: StyleProp<ViewStyle>;
|
|
55
|
+
/** Accessibility label for the group. */
|
|
56
|
+
accessibilityLabel?: string;
|
|
57
|
+
testID?: string;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* SegmentedButton — faithful to the Bacon POS Figma "Segmented button". A single
|
|
61
|
+
* connected control of 2+ options where exactly one is selected: the active
|
|
62
|
+
* segment fills dark with inverse text; the rest are transparent with secondary
|
|
63
|
+
* text, all inside a rounded track. Supports `sm`/`md` sizes, `default`/`round`
|
|
64
|
+
* shapes (or a custom `radius`), hug / `fullWidth` / fixed `width` layouts, icons,
|
|
65
|
+
* per-segment disabling, and color overrides. Controlled or uncontrolled.
|
|
66
|
+
*/
|
|
67
|
+
export declare function SegmentedButton({ options, value, defaultValue, onChange, size, shape, radius, fullWidth, width, disabled, bordered, trackColor, selectedColor, selectedTextColor, textColor, iconSize, animated, style, accessibilityLabel, testID, }: SegmentedButtonProps): import("react/jsx-runtime").JSX.Element;
|
|
68
|
+
//# sourceMappingURL=SegmentedButton.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SegmentedButton.d.ts","sourceRoot":"","sources":["../../../../../src/components/SegmentedButton/SegmentedButton.tsx"],"names":[],"mappings":"AACA,OAAO,EAOL,KAAK,SAAS,EACd,KAAK,SAAS,EACf,MAAM,cAAc,CAAC;AAKtB,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,yBAAyB,CAAC;AAGxD,MAAM,MAAM,mBAAmB,GAAG,IAAI,GAAG,IAAI,CAAC;AAC9C,0EAA0E;AAC1E,MAAM,MAAM,oBAAoB,GAAG,SAAS,GAAG,OAAO,CAAC;AAEvD,MAAM,WAAW,eAAe;IAC9B,qBAAqB;IACrB,KAAK,EAAE,MAAM,CAAC;IACd,uCAAuC;IACvC,KAAK,EAAE,MAAM,CAAC;IACd,6CAA6C;IAC7C,IAAI,CAAC,EAAE,QAAQ,CAAC;IAChB,qCAAqC;IACrC,YAAY,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;IAChC,iCAAiC;IACjC,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED,MAAM,WAAW,oBAAoB;IACnC,qEAAqE;IACrE,OAAO,EAAE,eAAe,EAAE,CAAC;IAC3B,mCAAmC;IACnC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,2EAA2E;IAC3E,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,sBAAsB;IACtB,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACnC,2DAA2D;IAC3D,IAAI,CAAC,EAAE,mBAAmB,CAAC;IAC3B,uFAAuF;IACvF,KAAK,CAAC,EAAE,oBAAoB,CAAC;IAC7B,mFAAmF;IACnF,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,kEAAkE;IAClE,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,uDAAuD;IACvD,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,iCAAiC;IACjC,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,+CAA+C;IAC/C,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,8DAA8D;IAC9D,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,wEAAwE;IACxE,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,gFAAgF;IAChF,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,gEAAgE;IAChE,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,uDAAuD;IACvD,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,uEAAuE;IACvE,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,0CAA0C;IAC1C,KAAK,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IAC7B,yCAAyC;IACzC,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAkBD;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAAC,EAC9B,OAAO,EACP,KAAK,EACL,YAAY,EACZ,QAAQ,EACR,IAAW,EACX,KAAiB,EACjB,MAAM,EACN,SAAiB,EACjB,KAAK,EACL,QAAgB,EAChB,QAAe,EACf,UAAU,EACV,aAAa,EACb,iBAAiB,EACjB,SAAS,EACT,QAAQ,EACR,QAAe,EACf,KAAK,EACL,kBAAkB,EAClB,MAAM,GACP,EAAE,oBAAoB,2CAkJtB"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../src/components/SegmentedButton/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,eAAe,EACf,KAAK,oBAAoB,EACzB,KAAK,eAAe,EACpB,KAAK,mBAAmB,EACxB,KAAK,oBAAoB,GAC1B,MAAM,mBAAmB,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/components/index.ts"],"names":[],"mappings":"AAAA,cAAc,SAAS,CAAC;AACxB,cAAc,UAAU,CAAC;AACzB,cAAc,SAAS,CAAC;AACxB,cAAc,SAAS,CAAC;AACxB,cAAc,UAAU,CAAC;AACzB,cAAc,QAAQ,CAAC;AACvB,cAAc,YAAY,CAAC;AAC3B,cAAc,QAAQ,CAAC;AACvB,cAAc,aAAa,CAAC;AAC5B,cAAc,aAAa,CAAC;AAC5B,cAAc,UAAU,CAAC;AACzB,cAAc,YAAY,CAAC;AAC3B,cAAc,cAAc,CAAC;AAC7B,cAAc,QAAQ,CAAC;AACvB,cAAc,cAAc,CAAC;AAC7B,cAAc,SAAS,CAAC;AACxB,cAAc,UAAU,CAAC;AACzB,cAAc,cAAc,CAAC;AAC7B,cAAc,QAAQ,CAAC;AACvB,cAAc,QAAQ,CAAC;AACvB,cAAc,eAAe,CAAC;AAC9B,cAAc,eAAe,CAAC;AAC9B,cAAc,mBAAmB,CAAC;AAClC,cAAc,SAAS,CAAC;AACxB,cAAc,UAAU,CAAC;AACzB,cAAc,WAAW,CAAC;AAC1B,cAAc,WAAW,CAAC;AAC1B,cAAc,YAAY,CAAC;AAC3B,cAAc,WAAW,CAAC;AAC1B,cAAc,UAAU,CAAC;AACzB,cAAc,SAAS,CAAC;AACxB,cAAc,QAAQ,CAAC;AACvB,cAAc,SAAS,CAAC;AACxB,cAAc,YAAY,CAAC;AAE3B,YAAY,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/components/index.ts"],"names":[],"mappings":"AAAA,cAAc,SAAS,CAAC;AACxB,cAAc,UAAU,CAAC;AACzB,cAAc,SAAS,CAAC;AACxB,cAAc,SAAS,CAAC;AACxB,cAAc,UAAU,CAAC;AACzB,cAAc,QAAQ,CAAC;AACvB,cAAc,YAAY,CAAC;AAC3B,cAAc,QAAQ,CAAC;AACvB,cAAc,aAAa,CAAC;AAC5B,cAAc,aAAa,CAAC;AAC5B,cAAc,UAAU,CAAC;AACzB,cAAc,YAAY,CAAC;AAC3B,cAAc,cAAc,CAAC;AAC7B,cAAc,QAAQ,CAAC;AACvB,cAAc,cAAc,CAAC;AAC7B,cAAc,SAAS,CAAC;AACxB,cAAc,UAAU,CAAC;AACzB,cAAc,cAAc,CAAC;AAC7B,cAAc,QAAQ,CAAC;AACvB,cAAc,QAAQ,CAAC;AACvB,cAAc,eAAe,CAAC;AAC9B,cAAc,eAAe,CAAC;AAC9B,cAAc,mBAAmB,CAAC;AAClC,cAAc,SAAS,CAAC;AACxB,cAAc,mBAAmB,CAAC;AAClC,cAAc,UAAU,CAAC;AACzB,cAAc,WAAW,CAAC;AAC1B,cAAc,WAAW,CAAC;AAC1B,cAAc,YAAY,CAAC;AAC3B,cAAc,WAAW,CAAC;AAC1B,cAAc,UAAU,CAAC;AACzB,cAAc,SAAS,CAAC;AACxB,cAAc,QAAQ,CAAC;AACvB,cAAc,SAAS,CAAC;AACxB,cAAc,YAAY,CAAC;AAE3B,YAAY,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC"}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { type StyleProp, type ViewStyle } from 'react-native';
|
|
2
|
+
import type { IconName } from '../Icon/icons.generated';
|
|
3
|
+
export type SegmentedButtonSize = 'sm' | 'md';
|
|
4
|
+
/** `default` = soft-rounded rectangle; `round` = fully rounded (pill). */
|
|
5
|
+
export type SegmentedButtonShape = 'default' | 'round';
|
|
6
|
+
export interface SegmentedOption {
|
|
7
|
+
/** Visible label. */
|
|
8
|
+
label: string;
|
|
9
|
+
/** Unique value reported on change. */
|
|
10
|
+
value: string;
|
|
11
|
+
/** Optional icon shown next to the label. */
|
|
12
|
+
icon?: IconName;
|
|
13
|
+
/** Icon side. Defaults to `left`. */
|
|
14
|
+
iconPosition?: 'left' | 'right';
|
|
15
|
+
/** Disable just this segment. */
|
|
16
|
+
disabled?: boolean;
|
|
17
|
+
}
|
|
18
|
+
export interface SegmentedButtonProps {
|
|
19
|
+
/** The segments. Two or more recommended; any count is supported. */
|
|
20
|
+
options: SegmentedOption[];
|
|
21
|
+
/** Selected value (controlled). */
|
|
22
|
+
value?: string;
|
|
23
|
+
/** Initial selected value (uncontrolled). Defaults to the first option. */
|
|
24
|
+
defaultValue?: string;
|
|
25
|
+
/** Change handler. */
|
|
26
|
+
onChange?: (value: string) => void;
|
|
27
|
+
/** Size: `sm` (16px text) or `md` (18px text, default). */
|
|
28
|
+
size?: SegmentedButtonSize;
|
|
29
|
+
/** Corner style: `default` (rounded rect) or `round` (pill). Defaults to `default`. */
|
|
30
|
+
shape?: SegmentedButtonShape;
|
|
31
|
+
/** Override the track corner radius in px (segments follow). Wins over `shape`. */
|
|
32
|
+
radius?: number;
|
|
33
|
+
/** Stretch to the container; segments share the width equally. */
|
|
34
|
+
fullWidth?: boolean;
|
|
35
|
+
/** Fixed track width in px (overrides `fullWidth`). */
|
|
36
|
+
width?: number;
|
|
37
|
+
/** Disable the whole control. */
|
|
38
|
+
disabled?: boolean;
|
|
39
|
+
/** Show the track border. Defaults to true. */
|
|
40
|
+
bordered?: boolean;
|
|
41
|
+
/** Track background color. Defaults to `background.muted`. */
|
|
42
|
+
trackColor?: string;
|
|
43
|
+
/** Selected segment background. Defaults to `action.inverse` (dark). */
|
|
44
|
+
selectedColor?: string;
|
|
45
|
+
/** Selected segment text/icon color. Defaults to `action.inverseForeground`. */
|
|
46
|
+
selectedTextColor?: string;
|
|
47
|
+
/** Unselected text/icon color. Defaults to `text.secondary`. */
|
|
48
|
+
textColor?: string;
|
|
49
|
+
/** Icon size override. Defaults to the size preset. */
|
|
50
|
+
iconSize?: number;
|
|
51
|
+
/** Slide the selected highlight between segments. Defaults to true. */
|
|
52
|
+
animated?: boolean;
|
|
53
|
+
/** Override the track container style. */
|
|
54
|
+
style?: StyleProp<ViewStyle>;
|
|
55
|
+
/** Accessibility label for the group. */
|
|
56
|
+
accessibilityLabel?: string;
|
|
57
|
+
testID?: string;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* SegmentedButton — faithful to the Bacon POS Figma "Segmented button". A single
|
|
61
|
+
* connected control of 2+ options where exactly one is selected: the active
|
|
62
|
+
* segment fills dark with inverse text; the rest are transparent with secondary
|
|
63
|
+
* text, all inside a rounded track. Supports `sm`/`md` sizes, `default`/`round`
|
|
64
|
+
* shapes (or a custom `radius`), hug / `fullWidth` / fixed `width` layouts, icons,
|
|
65
|
+
* per-segment disabling, and color overrides. Controlled or uncontrolled.
|
|
66
|
+
*/
|
|
67
|
+
export declare function SegmentedButton({ options, value, defaultValue, onChange, size, shape, radius, fullWidth, width, disabled, bordered, trackColor, selectedColor, selectedTextColor, textColor, iconSize, animated, style, accessibilityLabel, testID, }: SegmentedButtonProps): import("react/jsx-runtime").JSX.Element;
|
|
68
|
+
//# sourceMappingURL=SegmentedButton.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SegmentedButton.d.ts","sourceRoot":"","sources":["../../../../../src/components/SegmentedButton/SegmentedButton.tsx"],"names":[],"mappings":"AACA,OAAO,EAOL,KAAK,SAAS,EACd,KAAK,SAAS,EACf,MAAM,cAAc,CAAC;AAKtB,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,yBAAyB,CAAC;AAGxD,MAAM,MAAM,mBAAmB,GAAG,IAAI,GAAG,IAAI,CAAC;AAC9C,0EAA0E;AAC1E,MAAM,MAAM,oBAAoB,GAAG,SAAS,GAAG,OAAO,CAAC;AAEvD,MAAM,WAAW,eAAe;IAC9B,qBAAqB;IACrB,KAAK,EAAE,MAAM,CAAC;IACd,uCAAuC;IACvC,KAAK,EAAE,MAAM,CAAC;IACd,6CAA6C;IAC7C,IAAI,CAAC,EAAE,QAAQ,CAAC;IAChB,qCAAqC;IACrC,YAAY,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;IAChC,iCAAiC;IACjC,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED,MAAM,WAAW,oBAAoB;IACnC,qEAAqE;IACrE,OAAO,EAAE,eAAe,EAAE,CAAC;IAC3B,mCAAmC;IACnC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,2EAA2E;IAC3E,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,sBAAsB;IACtB,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACnC,2DAA2D;IAC3D,IAAI,CAAC,EAAE,mBAAmB,CAAC;IAC3B,uFAAuF;IACvF,KAAK,CAAC,EAAE,oBAAoB,CAAC;IAC7B,mFAAmF;IACnF,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,kEAAkE;IAClE,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,uDAAuD;IACvD,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,iCAAiC;IACjC,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,+CAA+C;IAC/C,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,8DAA8D;IAC9D,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,wEAAwE;IACxE,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,gFAAgF;IAChF,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,gEAAgE;IAChE,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,uDAAuD;IACvD,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,uEAAuE;IACvE,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,0CAA0C;IAC1C,KAAK,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IAC7B,yCAAyC;IACzC,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAkBD;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAAC,EAC9B,OAAO,EACP,KAAK,EACL,YAAY,EACZ,QAAQ,EACR,IAAW,EACX,KAAiB,EACjB,MAAM,EACN,SAAiB,EACjB,KAAK,EACL,QAAgB,EAChB,QAAe,EACf,UAAU,EACV,aAAa,EACb,iBAAiB,EACjB,SAAS,EACT,QAAQ,EACR,QAAe,EACf,KAAK,EACL,kBAAkB,EAClB,MAAM,GACP,EAAE,oBAAoB,2CAkJtB"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../src/components/SegmentedButton/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,eAAe,EACf,KAAK,oBAAoB,EACzB,KAAK,eAAe,EACpB,KAAK,mBAAmB,EACxB,KAAK,oBAAoB,GAC1B,MAAM,mBAAmB,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/components/index.ts"],"names":[],"mappings":"AAAA,cAAc,SAAS,CAAC;AACxB,cAAc,UAAU,CAAC;AACzB,cAAc,SAAS,CAAC;AACxB,cAAc,SAAS,CAAC;AACxB,cAAc,UAAU,CAAC;AACzB,cAAc,QAAQ,CAAC;AACvB,cAAc,YAAY,CAAC;AAC3B,cAAc,QAAQ,CAAC;AACvB,cAAc,aAAa,CAAC;AAC5B,cAAc,aAAa,CAAC;AAC5B,cAAc,UAAU,CAAC;AACzB,cAAc,YAAY,CAAC;AAC3B,cAAc,cAAc,CAAC;AAC7B,cAAc,QAAQ,CAAC;AACvB,cAAc,cAAc,CAAC;AAC7B,cAAc,SAAS,CAAC;AACxB,cAAc,UAAU,CAAC;AACzB,cAAc,cAAc,CAAC;AAC7B,cAAc,QAAQ,CAAC;AACvB,cAAc,QAAQ,CAAC;AACvB,cAAc,eAAe,CAAC;AAC9B,cAAc,eAAe,CAAC;AAC9B,cAAc,mBAAmB,CAAC;AAClC,cAAc,SAAS,CAAC;AACxB,cAAc,UAAU,CAAC;AACzB,cAAc,WAAW,CAAC;AAC1B,cAAc,WAAW,CAAC;AAC1B,cAAc,YAAY,CAAC;AAC3B,cAAc,WAAW,CAAC;AAC1B,cAAc,UAAU,CAAC;AACzB,cAAc,SAAS,CAAC;AACxB,cAAc,QAAQ,CAAC;AACvB,cAAc,SAAS,CAAC;AACxB,cAAc,YAAY,CAAC;AAE3B,YAAY,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/components/index.ts"],"names":[],"mappings":"AAAA,cAAc,SAAS,CAAC;AACxB,cAAc,UAAU,CAAC;AACzB,cAAc,SAAS,CAAC;AACxB,cAAc,SAAS,CAAC;AACxB,cAAc,UAAU,CAAC;AACzB,cAAc,QAAQ,CAAC;AACvB,cAAc,YAAY,CAAC;AAC3B,cAAc,QAAQ,CAAC;AACvB,cAAc,aAAa,CAAC;AAC5B,cAAc,aAAa,CAAC;AAC5B,cAAc,UAAU,CAAC;AACzB,cAAc,YAAY,CAAC;AAC3B,cAAc,cAAc,CAAC;AAC7B,cAAc,QAAQ,CAAC;AACvB,cAAc,cAAc,CAAC;AAC7B,cAAc,SAAS,CAAC;AACxB,cAAc,UAAU,CAAC;AACzB,cAAc,cAAc,CAAC;AAC7B,cAAc,QAAQ,CAAC;AACvB,cAAc,QAAQ,CAAC;AACvB,cAAc,eAAe,CAAC;AAC9B,cAAc,eAAe,CAAC;AAC9B,cAAc,mBAAmB,CAAC;AAClC,cAAc,SAAS,CAAC;AACxB,cAAc,mBAAmB,CAAC;AAClC,cAAc,UAAU,CAAC;AACzB,cAAc,WAAW,CAAC;AAC1B,cAAc,WAAW,CAAC;AAC1B,cAAc,YAAY,CAAC;AAC3B,cAAc,WAAW,CAAC;AAC1B,cAAc,UAAU,CAAC;AACzB,cAAc,SAAS,CAAC;AACxB,cAAc,QAAQ,CAAC;AACvB,cAAc,SAAS,CAAC;AACxB,cAAc,YAAY,CAAC;AAE3B,YAAY,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC"}
|
package/package.json
CHANGED
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
import { useEffect, useRef, useState } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
Animated,
|
|
4
|
+
Easing,
|
|
5
|
+
Pressable,
|
|
6
|
+
View,
|
|
7
|
+
type LayoutChangeEvent,
|
|
8
|
+
type PressableStateCallbackType,
|
|
9
|
+
type StyleProp,
|
|
10
|
+
type ViewStyle,
|
|
11
|
+
} from 'react-native';
|
|
12
|
+
import { useTheme } from '../../theme';
|
|
13
|
+
import type { RadiusToken } from '../../theme/spacing';
|
|
14
|
+
import type { TypographyVariant } from '../../theme/typography';
|
|
15
|
+
import { Icon } from '../Icon/Icon';
|
|
16
|
+
import type { IconName } from '../Icon/icons.generated';
|
|
17
|
+
import { Text } from '../Text/Text';
|
|
18
|
+
|
|
19
|
+
export type SegmentedButtonSize = 'sm' | 'md';
|
|
20
|
+
/** `default` = soft-rounded rectangle; `round` = fully rounded (pill). */
|
|
21
|
+
export type SegmentedButtonShape = 'default' | 'round';
|
|
22
|
+
|
|
23
|
+
export interface SegmentedOption {
|
|
24
|
+
/** Visible label. */
|
|
25
|
+
label: string;
|
|
26
|
+
/** Unique value reported on change. */
|
|
27
|
+
value: string;
|
|
28
|
+
/** Optional icon shown next to the label. */
|
|
29
|
+
icon?: IconName;
|
|
30
|
+
/** Icon side. Defaults to `left`. */
|
|
31
|
+
iconPosition?: 'left' | 'right';
|
|
32
|
+
/** Disable just this segment. */
|
|
33
|
+
disabled?: boolean;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface SegmentedButtonProps {
|
|
37
|
+
/** The segments. Two or more recommended; any count is supported. */
|
|
38
|
+
options: SegmentedOption[];
|
|
39
|
+
/** Selected value (controlled). */
|
|
40
|
+
value?: string;
|
|
41
|
+
/** Initial selected value (uncontrolled). Defaults to the first option. */
|
|
42
|
+
defaultValue?: string;
|
|
43
|
+
/** Change handler. */
|
|
44
|
+
onChange?: (value: string) => void;
|
|
45
|
+
/** Size: `sm` (16px text) or `md` (18px text, default). */
|
|
46
|
+
size?: SegmentedButtonSize;
|
|
47
|
+
/** Corner style: `default` (rounded rect) or `round` (pill). Defaults to `default`. */
|
|
48
|
+
shape?: SegmentedButtonShape;
|
|
49
|
+
/** Override the track corner radius in px (segments follow). Wins over `shape`. */
|
|
50
|
+
radius?: number;
|
|
51
|
+
/** Stretch to the container; segments share the width equally. */
|
|
52
|
+
fullWidth?: boolean;
|
|
53
|
+
/** Fixed track width in px (overrides `fullWidth`). */
|
|
54
|
+
width?: number;
|
|
55
|
+
/** Disable the whole control. */
|
|
56
|
+
disabled?: boolean;
|
|
57
|
+
/** Show the track border. Defaults to true. */
|
|
58
|
+
bordered?: boolean;
|
|
59
|
+
/** Track background color. Defaults to `background.muted`. */
|
|
60
|
+
trackColor?: string;
|
|
61
|
+
/** Selected segment background. Defaults to `action.inverse` (dark). */
|
|
62
|
+
selectedColor?: string;
|
|
63
|
+
/** Selected segment text/icon color. Defaults to `action.inverseForeground`. */
|
|
64
|
+
selectedTextColor?: string;
|
|
65
|
+
/** Unselected text/icon color. Defaults to `text.secondary`. */
|
|
66
|
+
textColor?: string;
|
|
67
|
+
/** Icon size override. Defaults to the size preset. */
|
|
68
|
+
iconSize?: number;
|
|
69
|
+
/** Slide the selected highlight between segments. Defaults to true. */
|
|
70
|
+
animated?: boolean;
|
|
71
|
+
/** Override the track container style. */
|
|
72
|
+
style?: StyleProp<ViewStyle>;
|
|
73
|
+
/** Accessibility label for the group. */
|
|
74
|
+
accessibilityLabel?: string;
|
|
75
|
+
testID?: string;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const SIZES: Record<
|
|
79
|
+
SegmentedButtonSize,
|
|
80
|
+
{
|
|
81
|
+
paddingH: number;
|
|
82
|
+
paddingV: number;
|
|
83
|
+
gap: number;
|
|
84
|
+
trackPadding: number;
|
|
85
|
+
text: TypographyVariant;
|
|
86
|
+
icon: number;
|
|
87
|
+
radius: RadiusToken;
|
|
88
|
+
}
|
|
89
|
+
> = {
|
|
90
|
+
sm: { paddingH: 12, paddingV: 10, gap: 6, trackPadding: 4, text: 'bodyMedium', icon: 18, radius: 'md' },
|
|
91
|
+
md: { paddingH: 16, paddingV: 12, gap: 8, trackPadding: 5, text: 'bodyLarge', icon: 20, radius: 'lg' },
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* SegmentedButton — faithful to the Bacon POS Figma "Segmented button". A single
|
|
96
|
+
* connected control of 2+ options where exactly one is selected: the active
|
|
97
|
+
* segment fills dark with inverse text; the rest are transparent with secondary
|
|
98
|
+
* text, all inside a rounded track. Supports `sm`/`md` sizes, `default`/`round`
|
|
99
|
+
* shapes (or a custom `radius`), hug / `fullWidth` / fixed `width` layouts, icons,
|
|
100
|
+
* per-segment disabling, and color overrides. Controlled or uncontrolled.
|
|
101
|
+
*/
|
|
102
|
+
export function SegmentedButton({
|
|
103
|
+
options,
|
|
104
|
+
value,
|
|
105
|
+
defaultValue,
|
|
106
|
+
onChange,
|
|
107
|
+
size = 'md',
|
|
108
|
+
shape = 'default',
|
|
109
|
+
radius,
|
|
110
|
+
fullWidth = false,
|
|
111
|
+
width,
|
|
112
|
+
disabled = false,
|
|
113
|
+
bordered = true,
|
|
114
|
+
trackColor,
|
|
115
|
+
selectedColor,
|
|
116
|
+
selectedTextColor,
|
|
117
|
+
textColor,
|
|
118
|
+
iconSize,
|
|
119
|
+
animated = true,
|
|
120
|
+
style,
|
|
121
|
+
accessibilityLabel,
|
|
122
|
+
testID,
|
|
123
|
+
}: SegmentedButtonProps) {
|
|
124
|
+
const theme = useTheme();
|
|
125
|
+
const cfg = SIZES[size];
|
|
126
|
+
|
|
127
|
+
const isControlled = value !== undefined;
|
|
128
|
+
const [uncontrolled, setUncontrolled] = useState(defaultValue ?? options[0]?.value ?? '');
|
|
129
|
+
const selected = isControlled ? value : uncontrolled;
|
|
130
|
+
|
|
131
|
+
const select = (next: string) => {
|
|
132
|
+
if (!isControlled) setUncontrolled(next);
|
|
133
|
+
onChange?.(next);
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
// Sliding highlight (thumb): we measure each segment's position/size and glide
|
|
137
|
+
// an absolutely-positioned fill to the selected one.
|
|
138
|
+
const layouts = useRef<Record<string, { x: number; width: number }>>({});
|
|
139
|
+
const thumbX = useRef(new Animated.Value(0)).current;
|
|
140
|
+
const thumbW = useRef(new Animated.Value(0)).current;
|
|
141
|
+
const placed = useRef(false);
|
|
142
|
+
const [measured, setMeasured] = useState(false);
|
|
143
|
+
|
|
144
|
+
const placeThumb = (x: number, width: number, withAnim: boolean) => {
|
|
145
|
+
if (withAnim && animated) {
|
|
146
|
+
Animated.parallel([
|
|
147
|
+
Animated.timing(thumbX, { toValue: x, duration: 200, easing: Easing.out(Easing.cubic), useNativeDriver: false }),
|
|
148
|
+
Animated.timing(thumbW, { toValue: width, duration: 200, easing: Easing.out(Easing.cubic), useNativeDriver: false }),
|
|
149
|
+
]).start();
|
|
150
|
+
} else {
|
|
151
|
+
thumbX.setValue(x);
|
|
152
|
+
thumbW.setValue(width);
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
// Animate to the selected segment when it changes (once layouts are known).
|
|
157
|
+
useEffect(() => {
|
|
158
|
+
const l = layouts.current[selected];
|
|
159
|
+
if (!l) return;
|
|
160
|
+
placeThumb(l.x, l.width, placed.current);
|
|
161
|
+
if (!placed.current) {
|
|
162
|
+
placed.current = true;
|
|
163
|
+
setMeasured(true);
|
|
164
|
+
}
|
|
165
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
166
|
+
}, [selected, animated]);
|
|
167
|
+
|
|
168
|
+
const onSegmentLayout = (value: string) => (e: LayoutChangeEvent) => {
|
|
169
|
+
const { x, width } = e.nativeEvent.layout;
|
|
170
|
+
layouts.current[value] = { x, width };
|
|
171
|
+
// Place/keep the thumb aligned with the selected segment (initial mount and
|
|
172
|
+
// on relayout/resize) without animating.
|
|
173
|
+
if (value === selected) {
|
|
174
|
+
placeThumb(x, width, false);
|
|
175
|
+
if (!placed.current) {
|
|
176
|
+
placed.current = true;
|
|
177
|
+
setMeasured(true);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
const trackRadius = radius ?? (shape === 'round' ? theme.radii.pill : theme.radii.xl);
|
|
183
|
+
const segmentRadius =
|
|
184
|
+
shape === 'round'
|
|
185
|
+
? theme.radii.pill
|
|
186
|
+
: radius != null
|
|
187
|
+
? Math.max(radius - cfg.trackPadding, 0)
|
|
188
|
+
: theme.radii[cfg.radius];
|
|
189
|
+
|
|
190
|
+
const selectedBg = selectedColor ?? theme.colors.action.inverse;
|
|
191
|
+
const selectedFg = selectedTextColor ?? theme.colors.action.inverseForeground;
|
|
192
|
+
const restFg = textColor ?? theme.colors.text.secondary;
|
|
193
|
+
const resolvedIconSize = iconSize ?? cfg.icon;
|
|
194
|
+
|
|
195
|
+
return (
|
|
196
|
+
<View
|
|
197
|
+
testID={testID}
|
|
198
|
+
accessibilityRole="radiogroup"
|
|
199
|
+
accessibilityLabel={accessibilityLabel}
|
|
200
|
+
style={[
|
|
201
|
+
{
|
|
202
|
+
padding: cfg.trackPadding,
|
|
203
|
+
borderRadius: trackRadius,
|
|
204
|
+
borderWidth: bordered ? 1 : 0,
|
|
205
|
+
borderColor: theme.colors.border.default,
|
|
206
|
+
backgroundColor: trackColor ?? theme.colors.background.muted,
|
|
207
|
+
alignSelf: fullWidth && width == null ? 'stretch' : 'flex-start',
|
|
208
|
+
width: width ?? (fullWidth ? '100%' : undefined),
|
|
209
|
+
opacity: disabled ? 0.5 : 1,
|
|
210
|
+
},
|
|
211
|
+
style,
|
|
212
|
+
]}
|
|
213
|
+
>
|
|
214
|
+
{/* Inner row: shares an unpadded coordinate space with the thumb so the
|
|
215
|
+
measured segment x/width line up exactly. */}
|
|
216
|
+
<View style={{ flexDirection: 'row', alignItems: 'stretch', gap: cfg.gap, position: 'relative' }}>
|
|
217
|
+
<Animated.View
|
|
218
|
+
pointerEvents="none"
|
|
219
|
+
style={{
|
|
220
|
+
position: 'absolute',
|
|
221
|
+
top: 0,
|
|
222
|
+
bottom: 0,
|
|
223
|
+
left: thumbX,
|
|
224
|
+
width: thumbW,
|
|
225
|
+
borderRadius: segmentRadius,
|
|
226
|
+
backgroundColor: selectedBg,
|
|
227
|
+
opacity: measured ? 1 : 0,
|
|
228
|
+
}}
|
|
229
|
+
/>
|
|
230
|
+
{options.map((opt) => {
|
|
231
|
+
const isSelected = opt.value === selected;
|
|
232
|
+
const isDisabled = disabled || opt.disabled;
|
|
233
|
+
const fg = isSelected ? selectedFg : restFg;
|
|
234
|
+
const iconNode = opt.icon ? <Icon name={opt.icon} size={resolvedIconSize} color={fg} /> : null;
|
|
235
|
+
const iconRight = opt.iconPosition === 'right';
|
|
236
|
+
return (
|
|
237
|
+
<Pressable
|
|
238
|
+
key={opt.value}
|
|
239
|
+
onLayout={onSegmentLayout(opt.value)}
|
|
240
|
+
onPress={isDisabled ? undefined : () => select(opt.value)}
|
|
241
|
+
disabled={isDisabled}
|
|
242
|
+
accessibilityRole="button"
|
|
243
|
+
accessibilityState={{ selected: isSelected, disabled: isDisabled }}
|
|
244
|
+
accessibilityLabel={opt.label}
|
|
245
|
+
testID={`segment-${opt.value}`}
|
|
246
|
+
style={({ pressed }: PressableStateCallbackType): StyleProp<ViewStyle> => ({
|
|
247
|
+
flex: fullWidth || width != null ? 1 : undefined,
|
|
248
|
+
flexDirection: 'row',
|
|
249
|
+
alignItems: 'center',
|
|
250
|
+
justifyContent: 'center',
|
|
251
|
+
gap: cfg.gap,
|
|
252
|
+
paddingHorizontal: cfg.paddingH,
|
|
253
|
+
paddingVertical: cfg.paddingV,
|
|
254
|
+
borderRadius: segmentRadius,
|
|
255
|
+
opacity: opt.disabled ? 0.5 : pressed && !isSelected ? 0.6 : 1,
|
|
256
|
+
})}
|
|
257
|
+
>
|
|
258
|
+
{iconNode && !iconRight ? iconNode : null}
|
|
259
|
+
<Text variant={cfg.text} weight="medium" color={fg} numberOfLines={1}>
|
|
260
|
+
{opt.label}
|
|
261
|
+
</Text>
|
|
262
|
+
{iconNode && iconRight ? iconNode : null}
|
|
263
|
+
</Pressable>
|
|
264
|
+
);
|
|
265
|
+
})}
|
|
266
|
+
</View>
|
|
267
|
+
</View>
|
|
268
|
+
);
|
|
269
|
+
}
|
package/src/components/index.ts
CHANGED