@utilitywarehouse/hearth-react-native 0.25.0 → 0.27.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/.turbo/turbo-build.log +1 -1
- package/.turbo/turbo-lint.log +15 -15
- package/CHANGELOG.md +71 -0
- package/build/components/Banner/Banner.js +12 -1
- package/build/components/Modal/Modal.d.ts +1 -1
- package/build/components/Modal/Modal.js +30 -7
- package/build/components/Modal/Modal.props.d.ts +4 -2
- package/build/components/PillGroup/Pill.js +0 -1
- package/build/components/PillGroup/PillGroup.js +4 -1
- package/build/components/SegmentedControl/SegmentedControl.context.d.ts +14 -0
- package/build/components/SegmentedControl/SegmentedControl.context.js +9 -0
- package/build/components/SegmentedControl/SegmentedControl.d.ts +6 -0
- package/build/components/SegmentedControl/SegmentedControl.js +196 -0
- package/build/components/SegmentedControl/SegmentedControl.props.d.ts +18 -0
- package/build/components/SegmentedControl/SegmentedControl.props.js +1 -0
- package/build/components/SegmentedControl/SegmentedControlOption.d.ts +18 -0
- package/build/components/SegmentedControl/SegmentedControlOption.js +122 -0
- package/build/components/SegmentedControl/SegmentedControlOption.props.d.ts +12 -0
- package/build/components/SegmentedControl/SegmentedControlOption.props.js +1 -0
- package/build/components/SegmentedControl/index.d.ts +4 -0
- package/build/components/SegmentedControl/index.js +2 -0
- package/build/components/index.d.ts +1 -0
- package/build/components/index.js +1 -0
- package/docs/changelog.mdx +136 -0
- package/docs/components/AllComponents.web.tsx +14 -0
- package/package.json +3 -3
- package/src/components/Banner/Banner.tsx +12 -1
- package/src/components/Modal/Modal.docs.mdx +9 -3
- package/src/components/Modal/Modal.props.ts +4 -2
- package/src/components/Modal/Modal.tsx +44 -7
- package/src/components/PillGroup/Pill.tsx +0 -1
- package/src/components/PillGroup/PillGroup.tsx +4 -0
- package/src/components/SegmentedControl/SegmentedControl.context.ts +22 -0
- package/src/components/SegmentedControl/SegmentedControl.docs.mdx +90 -0
- package/src/components/SegmentedControl/SegmentedControl.figma.tsx +40 -0
- package/src/components/SegmentedControl/SegmentedControl.props.ts +20 -0
- package/src/components/SegmentedControl/SegmentedControl.stories.tsx +77 -0
- package/src/components/SegmentedControl/SegmentedControl.tsx +257 -0
- package/src/components/SegmentedControl/SegmentedControlOption.props.ts +14 -0
- package/src/components/SegmentedControl/SegmentedControlOption.tsx +213 -0
- package/src/components/SegmentedControl/index.ts +4 -0
- package/src/components/index.ts +1 -0
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { Canvas, Controls, Meta } from '@storybook/addon-docs/blocks';
|
|
2
|
+
import { SegmentedControl, SegmentedControlOption } from '../../';
|
|
3
|
+
import { BackToTopButton, UsageWrap, ViewFigmaButton } from '../../../docs/components';
|
|
4
|
+
import * as Stories from './SegmentedControl.stories';
|
|
5
|
+
|
|
6
|
+
<Meta title="Components / Segmented Control" />
|
|
7
|
+
|
|
8
|
+
<BackToTopButton />
|
|
9
|
+
|
|
10
|
+
<ViewFigmaButton url="https://www.figma.com/design/6NKZXZhFSExXrcbBgc6zTR/Hearth-Components---Tokens?node-id=6185-1021&t=c7xg5X0N2EL0t87h-4" />
|
|
11
|
+
|
|
12
|
+
# Segmented Control
|
|
13
|
+
|
|
14
|
+
Segmented Control lets users switch between a small set of related options.
|
|
15
|
+
Each option is presented as an equal-priority segment in a single horizontal group.
|
|
16
|
+
|
|
17
|
+
- [Playground](#playground)
|
|
18
|
+
- [Usage](#usage)
|
|
19
|
+
- [Sizes](#sizes)
|
|
20
|
+
- [Props](#props)
|
|
21
|
+
- [Accessibility](#accessibility)
|
|
22
|
+
|
|
23
|
+
## Playground
|
|
24
|
+
|
|
25
|
+
<Canvas of={Stories.Playground} />
|
|
26
|
+
|
|
27
|
+
<Controls of={Stories.Playground} />
|
|
28
|
+
|
|
29
|
+
## Usage
|
|
30
|
+
|
|
31
|
+
<UsageWrap>
|
|
32
|
+
<SegmentedControl defaultValue="day">
|
|
33
|
+
<SegmentedControlOption value="day">Day</SegmentedControlOption>
|
|
34
|
+
<SegmentedControlOption value="week">Week</SegmentedControlOption>
|
|
35
|
+
<SegmentedControlOption value="month">Month</SegmentedControlOption>
|
|
36
|
+
</SegmentedControl>
|
|
37
|
+
</UsageWrap>
|
|
38
|
+
|
|
39
|
+
```tsx
|
|
40
|
+
import { SegmentedControl, SegmentedControlOption } from '@utilitywarehouse/hearth-react-native';
|
|
41
|
+
|
|
42
|
+
const Example = () => {
|
|
43
|
+
return (
|
|
44
|
+
<SegmentedControl defaultValue="day">
|
|
45
|
+
<SegmentedControlOption value="day">Day</SegmentedControlOption>
|
|
46
|
+
<SegmentedControlOption value="week">Week</SegmentedControlOption>
|
|
47
|
+
<SegmentedControlOption value="month">Month</SegmentedControlOption>
|
|
48
|
+
</SegmentedControl>
|
|
49
|
+
);
|
|
50
|
+
};
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Sizes
|
|
54
|
+
|
|
55
|
+
Figma defines two size variants for Segmented Control:
|
|
56
|
+
|
|
57
|
+
- `sm` maps to `SM-32`
|
|
58
|
+
- `md` maps to `MD-48`
|
|
59
|
+
|
|
60
|
+
<Canvas of={Stories.Sizes} />
|
|
61
|
+
|
|
62
|
+
## Props
|
|
63
|
+
|
|
64
|
+
### SegmentedControl
|
|
65
|
+
|
|
66
|
+
| Property | Type | Description | Default |
|
|
67
|
+
| --------------- | ------------------------- | -------------------------------------- | ------------ |
|
|
68
|
+
| `value` | `string` | Controlled selected option value. | - |
|
|
69
|
+
| `defaultValue` | `string` | Initial selected value (uncontrolled). | first option |
|
|
70
|
+
| `onValueChange` | `(value: string) => void` | Called when selected option changes. | - |
|
|
71
|
+
| `size` | `'sm' \| 'md'` | Size variant (`SM-32` / `MD-48`). | `'sm'` |
|
|
72
|
+
| `disabled` | `boolean` | Disables all options in the group. | `false` |
|
|
73
|
+
| `children` | `ReactNode` | `SegmentedControlOption` children. | required |
|
|
74
|
+
|
|
75
|
+
### SegmentedControlOption
|
|
76
|
+
|
|
77
|
+
| Property | Type | Description | Default |
|
|
78
|
+
| ---------- | ----------- | ------------------------------ | -------- |
|
|
79
|
+
| `value` | `string` | Unique value for this segment. | required |
|
|
80
|
+
| `children` | `ReactNode` | Option label/content. | required |
|
|
81
|
+
| `disabled` | `boolean` | Disables only this option. | `false` |
|
|
82
|
+
|
|
83
|
+
## Accessibility
|
|
84
|
+
|
|
85
|
+
SegmentedControl uses radio semantics so assistive technologies can understand it as a single-choice group.
|
|
86
|
+
|
|
87
|
+
- SegmentedControl root uses `accessibilityRole="radiogroup"`.
|
|
88
|
+
- Each SegmentedControlOption uses `accessibilityRole="radio"`.
|
|
89
|
+
- Option state is communicated with `accessibilityState={{ checked, disabled }}`.
|
|
90
|
+
- If `accessibilityLabel` is not provided on an option, the component falls back to the option text content and then to the option `value`.
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import figma from '@figma/code-connect';
|
|
2
|
+
import { SegmentedControl, SegmentedControlOption } from '../';
|
|
3
|
+
|
|
4
|
+
figma.connect(
|
|
5
|
+
SegmentedControl,
|
|
6
|
+
'https://www.figma.com/design/6NKZXZhFSExXrcbBgc6zTR/Hearth-Components---Tokens?node-id=6185-1021&t=c7xg5X0N2EL0t87h-4',
|
|
7
|
+
{
|
|
8
|
+
props: {
|
|
9
|
+
size: figma.enum('Size', {
|
|
10
|
+
'SM-32': 'sm',
|
|
11
|
+
'MD-48': 'md',
|
|
12
|
+
}),
|
|
13
|
+
disabled: figma.enum('State', {
|
|
14
|
+
Disabled: true,
|
|
15
|
+
}),
|
|
16
|
+
options: figma.children('Option'),
|
|
17
|
+
},
|
|
18
|
+
example: props => (
|
|
19
|
+
<SegmentedControl defaultValue="option-1" size={props.size} disabled={props.disabled}>
|
|
20
|
+
{props.options}
|
|
21
|
+
</SegmentedControl>
|
|
22
|
+
),
|
|
23
|
+
}
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
figma.connect(
|
|
27
|
+
SegmentedControlOption,
|
|
28
|
+
'https://www.figma.com/design/6NKZXZhFSExXrcbBgc6zTR/Hearth-Components---Tokens?node-id=4340-1252&t=c7xg5X0N2EL0t87h-4',
|
|
29
|
+
{
|
|
30
|
+
props: {
|
|
31
|
+
label: figma.string('Label'),
|
|
32
|
+
value: figma.string('Value'),
|
|
33
|
+
},
|
|
34
|
+
example: props => (
|
|
35
|
+
<SegmentedControlOption value={props.value ?? 'option'}>
|
|
36
|
+
{props.label ?? 'Option'}
|
|
37
|
+
</SegmentedControlOption>
|
|
38
|
+
),
|
|
39
|
+
}
|
|
40
|
+
);
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
import type { ViewProps } from 'react-native';
|
|
3
|
+
import type { FlexLayoutProps } from '../../types';
|
|
4
|
+
|
|
5
|
+
export interface SegmentedControlProps extends ViewProps, FlexLayoutProps {
|
|
6
|
+
/** Controlled selected option value. */
|
|
7
|
+
value?: string;
|
|
8
|
+
/** Initial selected option value for uncontrolled mode. */
|
|
9
|
+
defaultValue?: string;
|
|
10
|
+
/** Called when selected option changes. */
|
|
11
|
+
onValueChange?: (value: string) => void;
|
|
12
|
+
/** Size variant. */
|
|
13
|
+
size?: 'sm' | 'md';
|
|
14
|
+
/** Disables all options in the control. */
|
|
15
|
+
disabled?: boolean;
|
|
16
|
+
/** SegmentedControlOption children. */
|
|
17
|
+
children: ReactNode;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export default SegmentedControlProps;
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
import { BodyText, Flex, SegmentedControl, SegmentedControlOption } from '../';
|
|
3
|
+
|
|
4
|
+
const meta = {
|
|
5
|
+
title: 'Stories / Segmented Control',
|
|
6
|
+
component: SegmentedControl,
|
|
7
|
+
parameters: {
|
|
8
|
+
layout: 'centered',
|
|
9
|
+
},
|
|
10
|
+
args: {
|
|
11
|
+
size: 'sm',
|
|
12
|
+
disabled: false,
|
|
13
|
+
},
|
|
14
|
+
argTypes: {
|
|
15
|
+
size: {
|
|
16
|
+
control: 'radio',
|
|
17
|
+
options: ['sm', 'md'],
|
|
18
|
+
},
|
|
19
|
+
disabled: {
|
|
20
|
+
control: 'boolean',
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export default meta;
|
|
26
|
+
|
|
27
|
+
export const Playground = {
|
|
28
|
+
render: (args: { size?: 'sm' | 'md'; disabled?: boolean }) => (
|
|
29
|
+
<SegmentedControl defaultValue="day" {...args}>
|
|
30
|
+
<SegmentedControlOption value="day">Day</SegmentedControlOption>
|
|
31
|
+
<SegmentedControlOption value="week">Week</SegmentedControlOption>
|
|
32
|
+
<SegmentedControlOption value="month">Month</SegmentedControlOption>
|
|
33
|
+
</SegmentedControl>
|
|
34
|
+
),
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export const Sizes = {
|
|
38
|
+
render: () => (
|
|
39
|
+
<Flex spacing="sm" align="center">
|
|
40
|
+
<SegmentedControl defaultValue="one" size="sm">
|
|
41
|
+
<SegmentedControlOption value="one">Label</SegmentedControlOption>
|
|
42
|
+
<SegmentedControlOption value="two">Label</SegmentedControlOption>
|
|
43
|
+
<SegmentedControlOption value="three">Label</SegmentedControlOption>
|
|
44
|
+
</SegmentedControl>
|
|
45
|
+
<SegmentedControl defaultValue="one" size="md">
|
|
46
|
+
<SegmentedControlOption value="one">Label</SegmentedControlOption>
|
|
47
|
+
<SegmentedControlOption value="two">Label</SegmentedControlOption>
|
|
48
|
+
<SegmentedControlOption value="three">Label</SegmentedControlOption>
|
|
49
|
+
</SegmentedControl>
|
|
50
|
+
</Flex>
|
|
51
|
+
),
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
export const Controlled = {
|
|
55
|
+
render: () => {
|
|
56
|
+
const [value, setValue] = useState('annual');
|
|
57
|
+
return (
|
|
58
|
+
<Flex spacing="sm" align="center">
|
|
59
|
+
<SegmentedControl value={value} onValueChange={setValue}>
|
|
60
|
+
<SegmentedControlOption value="monthly">Monthly</SegmentedControlOption>
|
|
61
|
+
<SegmentedControlOption value="annual">Annual</SegmentedControlOption>
|
|
62
|
+
</SegmentedControl>
|
|
63
|
+
<BodyText size="sm">Selected: {value}</BodyText>
|
|
64
|
+
</Flex>
|
|
65
|
+
);
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
export const Disabled = {
|
|
70
|
+
render: () => (
|
|
71
|
+
<SegmentedControl defaultValue="left" disabled>
|
|
72
|
+
<SegmentedControlOption value="left">Left</SegmentedControlOption>
|
|
73
|
+
<SegmentedControlOption value="center">Center</SegmentedControlOption>
|
|
74
|
+
<SegmentedControlOption value="right">Right</SegmentedControlOption>
|
|
75
|
+
</SegmentedControl>
|
|
76
|
+
),
|
|
77
|
+
};
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
import { Children, isValidElement, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
2
|
+
import { View } from 'react-native';
|
|
3
|
+
import Animated, {
|
|
4
|
+
Easing,
|
|
5
|
+
useAnimatedStyle,
|
|
6
|
+
useReducedMotion,
|
|
7
|
+
useSharedValue,
|
|
8
|
+
withTiming,
|
|
9
|
+
} from 'react-native-reanimated';
|
|
10
|
+
import { StyleSheet } from 'react-native-unistyles';
|
|
11
|
+
import { useStyleProps } from '../../hooks';
|
|
12
|
+
import { SegmentedControlContext } from './SegmentedControl.context';
|
|
13
|
+
import type SegmentedControlProps from './SegmentedControl.props';
|
|
14
|
+
|
|
15
|
+
const Indicator = Animated.createAnimatedComponent(View);
|
|
16
|
+
const GROUP_BORDER_WIDTH = 1;
|
|
17
|
+
|
|
18
|
+
const SegmentedControl = ({
|
|
19
|
+
value: controlledValue,
|
|
20
|
+
defaultValue,
|
|
21
|
+
onValueChange,
|
|
22
|
+
size = 'sm',
|
|
23
|
+
disabled = false,
|
|
24
|
+
children,
|
|
25
|
+
style,
|
|
26
|
+
...props
|
|
27
|
+
}: SegmentedControlProps) => {
|
|
28
|
+
const { computedStyles, remainingProps } = useStyleProps(props);
|
|
29
|
+
const isReducedMotion = useReducedMotion();
|
|
30
|
+
const indicatorPositionOffset = GROUP_BORDER_WIDTH;
|
|
31
|
+
|
|
32
|
+
const optionValues = useMemo(() => {
|
|
33
|
+
const values: string[] = [];
|
|
34
|
+
|
|
35
|
+
const walk = (node: any) => {
|
|
36
|
+
Children.forEach(node, child => {
|
|
37
|
+
if (!isValidElement(child)) return;
|
|
38
|
+
|
|
39
|
+
const childType: any = child.type;
|
|
40
|
+
const childProps: any = child.props;
|
|
41
|
+
|
|
42
|
+
if (
|
|
43
|
+
childType?.displayName === 'SegmentedControlOption' &&
|
|
44
|
+
typeof childProps?.value === 'string'
|
|
45
|
+
) {
|
|
46
|
+
values.push(childProps.value);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (childProps?.children) {
|
|
50
|
+
walk(childProps.children);
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
walk(children);
|
|
56
|
+
return values;
|
|
57
|
+
}, [children]);
|
|
58
|
+
const optionValuesKey = useMemo(() => optionValues.join('|'), [optionValues]);
|
|
59
|
+
const optionValuesRef = useRef<string[]>(optionValues);
|
|
60
|
+
|
|
61
|
+
useEffect(() => {
|
|
62
|
+
optionValuesRef.current = optionValues;
|
|
63
|
+
}, [optionValues]);
|
|
64
|
+
|
|
65
|
+
const getInitialValue = () => {
|
|
66
|
+
if (controlledValue !== undefined) return controlledValue;
|
|
67
|
+
if (defaultValue !== undefined) return defaultValue;
|
|
68
|
+
return optionValues[0];
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const [uncontrolledValue, setUncontrolledValue] = useState<string | undefined>(getInitialValue);
|
|
72
|
+
|
|
73
|
+
useEffect(() => {
|
|
74
|
+
if (controlledValue !== undefined) {
|
|
75
|
+
setUncontrolledValue(controlledValue);
|
|
76
|
+
}
|
|
77
|
+
}, [controlledValue]);
|
|
78
|
+
|
|
79
|
+
useEffect(() => {
|
|
80
|
+
const currentOptionValues = optionValuesRef.current;
|
|
81
|
+
setUncontrolledValue(prev => {
|
|
82
|
+
if (!prev) return currentOptionValues[0];
|
|
83
|
+
if (!currentOptionValues.includes(prev)) return currentOptionValues[0];
|
|
84
|
+
return prev;
|
|
85
|
+
});
|
|
86
|
+
}, [optionValuesKey]);
|
|
87
|
+
|
|
88
|
+
const currentValue = controlledValue !== undefined ? controlledValue : uncontrolledValue;
|
|
89
|
+
|
|
90
|
+
const indicatorX = useSharedValue(0);
|
|
91
|
+
const indicatorWidth = useSharedValue(0);
|
|
92
|
+
const indicatorY = useSharedValue(0);
|
|
93
|
+
const indicatorHeight = useSharedValue(0);
|
|
94
|
+
const [hasIndicator, setHasIndicator] = useState(false);
|
|
95
|
+
const layoutsRef = useRef<Map<string, { x: number; y: number; width: number; height: number }>>(
|
|
96
|
+
new Map()
|
|
97
|
+
);
|
|
98
|
+
const prevValueRef = useRef<string | undefined>(undefined);
|
|
99
|
+
const initialisedRef = useRef(false);
|
|
100
|
+
|
|
101
|
+
const select = useCallback(
|
|
102
|
+
(nextValue: string) => {
|
|
103
|
+
if (disabled) return;
|
|
104
|
+
if (controlledValue === undefined) {
|
|
105
|
+
setUncontrolledValue(nextValue);
|
|
106
|
+
}
|
|
107
|
+
onValueChange?.(nextValue);
|
|
108
|
+
},
|
|
109
|
+
[controlledValue, disabled, onValueChange]
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
const registerOptionLayout = useCallback(
|
|
113
|
+
(value: string, layout: { x: number; y: number; width: number; height: number }) => {
|
|
114
|
+
layoutsRef.current.set(value, layout);
|
|
115
|
+
const activeValue = controlledValue !== undefined ? controlledValue : uncontrolledValue;
|
|
116
|
+
if (!activeValue || activeValue !== value) return;
|
|
117
|
+
|
|
118
|
+
if (!initialisedRef.current) {
|
|
119
|
+
indicatorX.value = Math.max(0, layout.x - indicatorPositionOffset);
|
|
120
|
+
indicatorWidth.value = layout.width;
|
|
121
|
+
indicatorY.value = Math.max(0, layout.y - indicatorPositionOffset);
|
|
122
|
+
indicatorHeight.value = layout.height;
|
|
123
|
+
prevValueRef.current = activeValue;
|
|
124
|
+
initialisedRef.current = true;
|
|
125
|
+
setHasIndicator(true);
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (prevValueRef.current === activeValue) return;
|
|
130
|
+
|
|
131
|
+
const config = {
|
|
132
|
+
delay: 200,
|
|
133
|
+
duration: isReducedMotion ? 0 : 220,
|
|
134
|
+
easing: Easing.out(Easing.cubic),
|
|
135
|
+
} as const;
|
|
136
|
+
|
|
137
|
+
indicatorX.value = withTiming(Math.max(0, layout.x - indicatorPositionOffset), config);
|
|
138
|
+
indicatorWidth.value = withTiming(layout.width, config);
|
|
139
|
+
indicatorY.value = withTiming(Math.max(0, layout.y - indicatorPositionOffset), config);
|
|
140
|
+
indicatorHeight.value = withTiming(layout.height, config);
|
|
141
|
+
prevValueRef.current = activeValue;
|
|
142
|
+
},
|
|
143
|
+
[
|
|
144
|
+
controlledValue,
|
|
145
|
+
indicatorHeight,
|
|
146
|
+
indicatorWidth,
|
|
147
|
+
indicatorX,
|
|
148
|
+
indicatorY,
|
|
149
|
+
indicatorPositionOffset,
|
|
150
|
+
isReducedMotion,
|
|
151
|
+
uncontrolledValue,
|
|
152
|
+
]
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
useEffect(() => {
|
|
156
|
+
if (!currentValue || !initialisedRef.current) return;
|
|
157
|
+
if (prevValueRef.current === undefined || prevValueRef.current === currentValue) return;
|
|
158
|
+
const layout = layoutsRef.current.get(currentValue);
|
|
159
|
+
if (!layout) return;
|
|
160
|
+
const config = {
|
|
161
|
+
duration: isReducedMotion ? 0 : 220,
|
|
162
|
+
easing: Easing.out(Easing.cubic),
|
|
163
|
+
} as const;
|
|
164
|
+
|
|
165
|
+
indicatorX.value = withTiming(Math.max(0, layout.x - indicatorPositionOffset), config);
|
|
166
|
+
indicatorWidth.value = withTiming(layout.width, config);
|
|
167
|
+
indicatorY.value = withTiming(Math.max(0, layout.y - indicatorPositionOffset), config);
|
|
168
|
+
indicatorHeight.value = withTiming(layout.height, config);
|
|
169
|
+
prevValueRef.current = currentValue;
|
|
170
|
+
}, [
|
|
171
|
+
currentValue,
|
|
172
|
+
indicatorHeight,
|
|
173
|
+
indicatorWidth,
|
|
174
|
+
indicatorX,
|
|
175
|
+
indicatorY,
|
|
176
|
+
indicatorPositionOffset,
|
|
177
|
+
isReducedMotion,
|
|
178
|
+
optionValuesKey,
|
|
179
|
+
]);
|
|
180
|
+
|
|
181
|
+
const indicatorStyle = useAnimatedStyle(() => ({
|
|
182
|
+
transform: [{ translateX: indicatorX.value }, { translateY: indicatorY.value }],
|
|
183
|
+
width: indicatorWidth.value,
|
|
184
|
+
height: indicatorHeight.value,
|
|
185
|
+
}));
|
|
186
|
+
|
|
187
|
+
styles.useVariants({ disabled, size });
|
|
188
|
+
|
|
189
|
+
const contextValue = useMemo(
|
|
190
|
+
() => ({
|
|
191
|
+
value: currentValue,
|
|
192
|
+
select,
|
|
193
|
+
disabled,
|
|
194
|
+
size,
|
|
195
|
+
registerOptionLayout,
|
|
196
|
+
}),
|
|
197
|
+
[currentValue, select, disabled, size, registerOptionLayout]
|
|
198
|
+
);
|
|
199
|
+
|
|
200
|
+
return (
|
|
201
|
+
<SegmentedControlContext.Provider value={contextValue}>
|
|
202
|
+
<View
|
|
203
|
+
accessibilityRole="radiogroup"
|
|
204
|
+
accessibilityState={{ disabled }}
|
|
205
|
+
style={[styles.container, computedStyles, style]}
|
|
206
|
+
{...remainingProps}
|
|
207
|
+
>
|
|
208
|
+
{hasIndicator ? (
|
|
209
|
+
<Indicator pointerEvents="none" style={[styles.indicator, indicatorStyle]} />
|
|
210
|
+
) : null}
|
|
211
|
+
{children}
|
|
212
|
+
</View>
|
|
213
|
+
</SegmentedControlContext.Provider>
|
|
214
|
+
);
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
SegmentedControl.displayName = 'SegmentedControl';
|
|
218
|
+
|
|
219
|
+
const styles = StyleSheet.create(theme => ({
|
|
220
|
+
container: {
|
|
221
|
+
flexDirection: 'row',
|
|
222
|
+
alignItems: 'center',
|
|
223
|
+
alignSelf: 'flex-start',
|
|
224
|
+
gap: theme.components.segmentedControl.group.gap,
|
|
225
|
+
height: theme.components.segmentedControl.group.height,
|
|
226
|
+
borderRadius: theme.components.segmentedControl.group.borderRadius,
|
|
227
|
+
borderWidth: theme.components.segmentedControl.group.borderWidth,
|
|
228
|
+
backgroundColor: theme.color.surface.neutral.subtle,
|
|
229
|
+
borderColor: theme.color.border.strong,
|
|
230
|
+
variants: {
|
|
231
|
+
size: {
|
|
232
|
+
sm: {
|
|
233
|
+
height: 32,
|
|
234
|
+
padding: 2,
|
|
235
|
+
},
|
|
236
|
+
md: {
|
|
237
|
+
height: theme.components.segmentedControl.group.height,
|
|
238
|
+
padding: 2,
|
|
239
|
+
},
|
|
240
|
+
},
|
|
241
|
+
disabled: {
|
|
242
|
+
true: {
|
|
243
|
+
opacity: theme.opacity.disabled,
|
|
244
|
+
},
|
|
245
|
+
},
|
|
246
|
+
},
|
|
247
|
+
},
|
|
248
|
+
indicator: {
|
|
249
|
+
position: 'absolute',
|
|
250
|
+
left: 0,
|
|
251
|
+
top: 0,
|
|
252
|
+
borderRadius: theme.components.segmentedControl.borderRadius,
|
|
253
|
+
backgroundColor: theme.color.interactive.brand.surface.strong.default,
|
|
254
|
+
},
|
|
255
|
+
}));
|
|
256
|
+
|
|
257
|
+
export default SegmentedControl;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
import type { PressableProps, ViewProps } from 'react-native';
|
|
3
|
+
|
|
4
|
+
export interface SegmentedControlOptionProps extends Omit<PressableProps, 'children'> {
|
|
5
|
+
/** Unique option value. */
|
|
6
|
+
value: string;
|
|
7
|
+
/** Option label/content. */
|
|
8
|
+
children: ReactNode;
|
|
9
|
+
/** Disables only this option. */
|
|
10
|
+
disabled?: boolean;
|
|
11
|
+
style?: ViewProps['style'];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export default SegmentedControlOptionProps;
|