expo-interface 0.1.0 → 0.2.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/README.md +282 -180
- package/package.json +39 -13
- package/src/alert/alert.css +49 -0
- package/src/alert/index.android.tsx +68 -0
- package/src/alert/index.ios.tsx +47 -0
- package/src/alert/index.tsx +60 -0
- package/src/alert/shared.ts +10 -0
- package/src/alert/types.ts +58 -0
- package/src/button/button.css +6 -0
- package/src/button/index.android.tsx +9 -2
- package/src/button/index.ios.tsx +8 -3
- package/src/button/index.tsx +19 -1
- package/src/button/types.ts +7 -0
- package/src/checkbox/checkbox.css +29 -0
- package/src/checkbox/index.android.tsx +54 -0
- package/src/checkbox/index.ios.tsx +50 -0
- package/src/checkbox/index.tsx +50 -0
- package/src/checkbox/types.ts +27 -0
- package/src/collapsible/collapsible.css +32 -0
- package/src/collapsible/index.android.tsx +34 -0
- package/src/collapsible/index.ios.tsx +31 -0
- package/src/collapsible/index.tsx +41 -0
- package/src/collapsible/shared.ts +21 -0
- package/src/collapsible/types.ts +24 -0
- package/src/color-picker/color-picker.css +66 -0
- package/src/color-picker/index.android.tsx +117 -0
- package/src/color-picker/index.ios.tsx +33 -0
- package/src/color-picker/index.tsx +62 -0
- package/src/color-picker/shared.ts +204 -0
- package/src/color-picker/sheet.tsx +432 -0
- package/src/color-picker/types.ts +34 -0
- package/src/context-menu/index.android.tsx +27 -0
- package/src/context-menu/index.ios.tsx +22 -0
- package/src/context-menu/index.tsx +56 -0
- package/src/date-time/index.android.tsx +3 -2
- package/src/divider/divider.css +16 -0
- package/src/divider/index.android.tsx +26 -0
- package/src/divider/index.ios.tsx +18 -0
- package/src/divider/index.tsx +21 -0
- package/src/divider/types.ts +20 -0
- package/src/field-group/field-group.css +23 -7
- package/src/field-group/index.android.tsx +22 -22
- package/src/field-group/index.web.tsx +2 -2
- package/src/gauge/gauge.css +200 -0
- package/src/gauge/index.android.tsx +212 -0
- package/src/gauge/index.ios.tsx +40 -0
- package/src/gauge/index.tsx +165 -0
- package/src/gauge/shared.ts +79 -0
- package/src/gauge/types.ts +64 -0
- package/src/index.ts +24 -3
- package/src/menu/index.android.tsx +66 -0
- package/src/menu/index.ios.tsx +72 -0
- package/src/menu/index.tsx +30 -0
- package/src/menu/list.tsx +98 -0
- package/src/menu/menu.css +83 -0
- package/src/menu/types.ts +60 -0
- package/src/picker/index.android.tsx +3 -2
- package/src/progress/index.android.tsx +23 -9
- package/src/progress/index.ios.tsx +7 -6
- package/src/progress/index.tsx +43 -6
- package/src/progress/progress.css +33 -0
- package/src/progress/types.ts +23 -6
- package/src/screen/header.tsx +1 -0
- package/src/segmented/index.android.tsx +67 -0
- package/src/segmented/index.ios.tsx +47 -0
- package/src/segmented/index.tsx +58 -0
- package/src/segmented/segmented.css +55 -0
- package/src/segmented/types.ts +31 -0
- package/src/slider/index.android.tsx +87 -0
- package/src/slider/index.ios.tsx +64 -0
- package/src/slider/index.tsx +55 -0
- package/src/slider/slider.css +30 -0
- package/src/slider/types.ts +39 -0
- package/src/stepper/index.android.tsx +58 -0
- package/src/stepper/index.ios.tsx +55 -0
- package/src/stepper/index.tsx +47 -0
- package/src/stepper/shared.ts +17 -0
- package/src/stepper/stepper.css +61 -0
- package/src/stepper/types.ts +35 -0
- package/src/switch/index.tsx +2 -0
- package/src/text-field/shared.ts +2 -0
- package/src/tooltip/index.android.tsx +23 -0
- package/src/tooltip/index.ios.tsx +17 -0
- package/src/tooltip/index.tsx +41 -0
- package/src/tooltip/tooltip.css +40 -0
- package/src/tooltip/types.ts +25 -0
- package/src/link.ts +0 -36
- package/src/qr/index.tsx +0 -26
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* The popup is a native `[popover="auto"]` element: the browser puts it in the
|
|
3
|
+
* top layer, closes it on outside click / Escape (light dismiss), and toggles
|
|
4
|
+
* it from the trigger's `popovertarget` — no JS open/close state. Placement is
|
|
5
|
+
* CSS anchor positioning relative to the trigger's `anchor-name`, flipping to
|
|
6
|
+
* stay on screen. Context menus instead set `left`/`top` at the pointer.
|
|
7
|
+
*/
|
|
8
|
+
.ui-menu {
|
|
9
|
+
display: inline-flex;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
.ui-menu__list {
|
|
13
|
+
box-sizing: border-box;
|
|
14
|
+
min-width: 200px;
|
|
15
|
+
inset: auto;
|
|
16
|
+
margin: 0;
|
|
17
|
+
padding: 6px;
|
|
18
|
+
border: 1px solid var(--color-separator);
|
|
19
|
+
border-radius: 12px;
|
|
20
|
+
background: var(--color-background-element);
|
|
21
|
+
color: var(--color-label);
|
|
22
|
+
box-shadow: 0 8px 28px rgba(0, 0, 0, 0.18);
|
|
23
|
+
overflow: visible;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
.ui-menu__list--anchored {
|
|
27
|
+
position-area: bottom span-left;
|
|
28
|
+
position-try-fallbacks: flip-block, flip-inline, flip-block flip-inline;
|
|
29
|
+
margin-block: 4px;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
.ui-menu__list::backdrop {
|
|
33
|
+
background: transparent;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
.ui-menu__item {
|
|
37
|
+
-webkit-appearance: none;
|
|
38
|
+
appearance: none;
|
|
39
|
+
display: flex;
|
|
40
|
+
flex-direction: row;
|
|
41
|
+
align-items: center;
|
|
42
|
+
gap: 10px;
|
|
43
|
+
box-sizing: border-box;
|
|
44
|
+
width: 100%;
|
|
45
|
+
margin: 0;
|
|
46
|
+
padding: 8px 10px;
|
|
47
|
+
border: none;
|
|
48
|
+
border-radius: 8px;
|
|
49
|
+
background: transparent;
|
|
50
|
+
color: var(--color-label);
|
|
51
|
+
font-family: var(--font-display);
|
|
52
|
+
font-size: 14px;
|
|
53
|
+
font-weight: 500;
|
|
54
|
+
line-height: 18px;
|
|
55
|
+
text-align: left;
|
|
56
|
+
white-space: nowrap;
|
|
57
|
+
cursor: pointer;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
.ui-menu__item:hover:not(:disabled),
|
|
61
|
+
.ui-menu__item:focus-visible {
|
|
62
|
+
background: color-mix(in srgb, var(--color-label) 8%, transparent);
|
|
63
|
+
outline: none;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
.ui-menu__item:disabled {
|
|
67
|
+
opacity: 0.4;
|
|
68
|
+
cursor: default;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
.ui-menu__item--destructive {
|
|
72
|
+
color: var(--color-destructive);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
.ui-menu__separator {
|
|
76
|
+
height: 1px;
|
|
77
|
+
margin: 6px 4px;
|
|
78
|
+
background: var(--color-separator);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
.ui-context-menu {
|
|
82
|
+
display: contents;
|
|
83
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type {ReactNode} from 'react';
|
|
2
|
+
import type {IconToken} from '../icons';
|
|
3
|
+
import type {ButtonProps} from '../button/types';
|
|
4
|
+
|
|
5
|
+
/** One entry of a `Menu` / `ContextMenu`. */
|
|
6
|
+
export interface MenuItem {
|
|
7
|
+
/** Item text. */
|
|
8
|
+
label: string;
|
|
9
|
+
/** Leading icon. */
|
|
10
|
+
icon?: IconToken;
|
|
11
|
+
/**
|
|
12
|
+
* `destructive` renders the item in the danger color.
|
|
13
|
+
* @default 'default'
|
|
14
|
+
*/
|
|
15
|
+
role?: 'default' | 'destructive';
|
|
16
|
+
/** Greys the item out and ignores presses. */
|
|
17
|
+
disabled?: boolean;
|
|
18
|
+
/** Draw a separator above this item. */
|
|
19
|
+
separator?: boolean;
|
|
20
|
+
/** Called when the item is selected; the menu then closes. */
|
|
21
|
+
onPress?: () => void;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Cross-platform dropdown menu opened from a button.
|
|
26
|
+
*
|
|
27
|
+
* Bridges the SwiftUI `Menu` on iOS, the Jetpack Compose Material 3
|
|
28
|
+
* `DropdownMenu` on Android, and a `role="menu"` popup on web. The trigger
|
|
29
|
+
* looks like the kit's `Button` and takes the same styling props.
|
|
30
|
+
*/
|
|
31
|
+
export interface MenuProps extends Pick<ButtonProps, 'variant' | 'size' | 'shape' | 'color' | 'hideLabel' | 'disabled'> {
|
|
32
|
+
/** Trigger text (kept for accessibility when `hideLabel` is set). */
|
|
33
|
+
label: string;
|
|
34
|
+
/** Trigger icon. */
|
|
35
|
+
icon?: IconToken;
|
|
36
|
+
/** Entries shown when the menu opens. */
|
|
37
|
+
items: MenuItem[];
|
|
38
|
+
/** Identifier used to locate the trigger in end-to-end tests. */
|
|
39
|
+
testID?: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Cross-platform context menu attached to arbitrary content.
|
|
44
|
+
*
|
|
45
|
+
* Opens on long-press (iOS `contextMenu`, Android `DropdownMenu` anchored by
|
|
46
|
+
* `combinedClickable`) or right-click / long-press on web. `children` must
|
|
47
|
+
* be native (`@expo/ui`) content on iOS/Android.
|
|
48
|
+
*/
|
|
49
|
+
export interface ContextMenuProps {
|
|
50
|
+
/** Entries shown when the menu opens. */
|
|
51
|
+
items: MenuItem[];
|
|
52
|
+
/** Content that triggers the menu. */
|
|
53
|
+
children: ReactNode;
|
|
54
|
+
/** Called on a plain tap of the content (Android/web; iOS taps pass through). */
|
|
55
|
+
onPress?: () => void;
|
|
56
|
+
/** Disables the menu. */
|
|
57
|
+
disabled?: boolean;
|
|
58
|
+
/** Identifier used to locate the trigger in end-to-end tests. */
|
|
59
|
+
testID?: string;
|
|
60
|
+
}
|
|
@@ -3,7 +3,7 @@ import type {PickerProps, PickerValue} from './types';
|
|
|
3
3
|
import {useState} from 'react';
|
|
4
4
|
import unfoldMore from '@expo/material-symbols/unfold_more.xml';
|
|
5
5
|
import {useMaterialColors, DropdownMenu, DropdownMenuItem, Column, Row, Text, Icon} from '@expo/ui/jetpack-compose';
|
|
6
|
-
import {background, clickable, clip, fillMaxWidth, padding, Shapes} from '@expo/ui/jetpack-compose/modifiers';
|
|
6
|
+
import {background, clickable, clip, fillMaxWidth, padding, Shapes, testID as testIDModifier} from '@expo/ui/jetpack-compose/modifiers';
|
|
7
7
|
import {extractItems, labelFor, PickerItem, useSelectedValue} from './shared';
|
|
8
8
|
|
|
9
9
|
/**
|
|
@@ -20,6 +20,7 @@ function PickerComponent<T extends PickerValue>({
|
|
|
20
20
|
accentColor,
|
|
21
21
|
selectedValue,
|
|
22
22
|
onValueChange,
|
|
23
|
+
testID,
|
|
23
24
|
}: PickerProps<T>) {
|
|
24
25
|
const colors = useMaterialColors();
|
|
25
26
|
const items = extractItems<T>(children);
|
|
@@ -30,7 +31,7 @@ function PickerComponent<T extends PickerValue>({
|
|
|
30
31
|
// a Form, and the web pill uses `secondaryLabel` to match.
|
|
31
32
|
const colorValue = accentColor && !disabled ? accentColor : colors.onSurfaceVariant;
|
|
32
33
|
return (
|
|
33
|
-
<Column modifiers={[fillMaxWidth()]}>
|
|
34
|
+
<Column modifiers={[fillMaxWidth(), ...(testID ? [testIDModifier(testID)] : [])]}>
|
|
34
35
|
<Row
|
|
35
36
|
verticalAlignment="center"
|
|
36
37
|
horizontalArrangement="spaceBetween"
|
|
@@ -1,24 +1,38 @@
|
|
|
1
1
|
import type {ProgressProps} from './types';
|
|
2
|
-
import {LinearProgressIndicator} from '@expo/ui/jetpack-compose';
|
|
3
|
-
import {fillMaxWidth, testID as testIDModifier} from '@expo/ui/jetpack-compose/modifiers';
|
|
2
|
+
import {CircularProgressIndicator, LinearProgressIndicator} from '@expo/ui/jetpack-compose';
|
|
3
|
+
import {fillMaxWidth, size as sizeMod, testID as testIDModifier} from '@expo/ui/jetpack-compose/modifiers';
|
|
4
4
|
import {useColor} from '../theme';
|
|
5
5
|
|
|
6
|
+
const STROKE = 3;
|
|
7
|
+
|
|
6
8
|
/**
|
|
7
|
-
* Android renders the Material 3 `LinearProgressIndicator
|
|
8
|
-
* width
|
|
9
|
+
* Android renders the Material 3 `LinearProgressIndicator` (filling the row
|
|
10
|
+
* width to match the iOS and web bars) or `CircularProgressIndicator` sized
|
|
11
|
+
* to `size`.
|
|
9
12
|
*/
|
|
10
|
-
export function Progress({value, color, trackColor, testID}: ProgressProps) {
|
|
13
|
+
export function Progress({value, variant = 'linear', size = 24, color, trackColor, testID}: ProgressProps) {
|
|
11
14
|
const accent = useColor('tint');
|
|
12
15
|
const track = useColor('backgroundSelected');
|
|
16
|
+
const testMods = testID ? [testIDModifier(testID)] : [];
|
|
17
|
+
|
|
18
|
+
if (variant === 'circular') {
|
|
19
|
+
return (
|
|
20
|
+
<CircularProgressIndicator
|
|
21
|
+
progress={value ?? null}
|
|
22
|
+
color={color ?? accent}
|
|
23
|
+
trackColor={trackColor ?? track}
|
|
24
|
+
strokeWidth={STROKE}
|
|
25
|
+
modifiers={[sizeMod(size, size), ...testMods]}
|
|
26
|
+
/>
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
13
30
|
return (
|
|
14
31
|
<LinearProgressIndicator
|
|
15
32
|
progress={value ?? null}
|
|
16
33
|
color={color ?? accent}
|
|
17
34
|
trackColor={trackColor ?? track}
|
|
18
|
-
modifiers={[
|
|
19
|
-
fillMaxWidth(),
|
|
20
|
-
...(testID ? [testIDModifier(testID)] : []),
|
|
21
|
-
]}
|
|
35
|
+
modifiers={[fillMaxWidth(), ...testMods]}
|
|
22
36
|
/>
|
|
23
37
|
);
|
|
24
38
|
}
|
|
@@ -1,19 +1,20 @@
|
|
|
1
1
|
import type {ProgressProps} from './types';
|
|
2
2
|
import {ProgressView} from '@expo/ui/swift-ui';
|
|
3
|
-
import {tint} from '@expo/ui/swift-ui/modifiers';
|
|
3
|
+
import {progressViewStyle, tint} from '@expo/ui/swift-ui/modifiers';
|
|
4
4
|
import {useColor} from '../theme';
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
|
-
* iOS renders SwiftUI's `ProgressView` in its linear style (
|
|
8
|
-
*
|
|
9
|
-
*
|
|
7
|
+
* iOS renders SwiftUI's `ProgressView` in its linear style (a flexible-width
|
|
8
|
+
* bar that fills its parent) or circular style (the system ring / activity
|
|
9
|
+
* spinner, at its native size). `trackColor` has no SwiftUI equivalent and is
|
|
10
|
+
* ignored.
|
|
10
11
|
*/
|
|
11
|
-
export function Progress({value, color, testID}: ProgressProps) {
|
|
12
|
+
export function Progress({value, variant = 'linear', color, testID}: ProgressProps) {
|
|
12
13
|
const accent = useColor('tint');
|
|
13
14
|
return (
|
|
14
15
|
<ProgressView
|
|
15
16
|
value={value ?? null}
|
|
16
|
-
modifiers={[tint(color ?? accent)]}
|
|
17
|
+
modifiers={[progressViewStyle(variant), tint(color ?? accent)]}
|
|
17
18
|
testID={testID}
|
|
18
19
|
/>
|
|
19
20
|
);
|
package/src/progress/index.tsx
CHANGED
|
@@ -3,20 +3,57 @@ import type {CSSProperties} from 'react';
|
|
|
3
3
|
import type {ProgressProps} from './types';
|
|
4
4
|
import {useColor} from '../theme';
|
|
5
5
|
|
|
6
|
+
const STROKE = 3;
|
|
7
|
+
|
|
6
8
|
/**
|
|
7
|
-
* On web the bar is a native `<meter>` element
|
|
8
|
-
* by the `--ui-progress-*` custom properties
|
|
9
|
-
*
|
|
10
|
-
*
|
|
9
|
+
* On web the linear bar is a native `<meter>` element whose bar/track colors
|
|
10
|
+
* are driven by the `--ui-progress-*` custom properties (meter pseudo-elements
|
|
11
|
+
* can't be styled inline). Indeterminate meters don't exist on the web
|
|
12
|
+
* platform, so an undefined `value` renders an empty bar. The circular
|
|
13
|
+
* variant is an SVG ring; without a `value` it spins as an activity indicator.
|
|
11
14
|
*/
|
|
12
|
-
export function Progress({value, color, trackColor, testID}: ProgressProps) {
|
|
15
|
+
export function Progress({value, variant = 'linear', size = 24, color, trackColor, testID}: ProgressProps) {
|
|
13
16
|
const tint = useColor('tint');
|
|
14
17
|
const track = useColor('backgroundSelected');
|
|
15
|
-
const clamped = Math.max(0, Math.min(1, value ?? 0));
|
|
16
18
|
const vars = {
|
|
17
19
|
'--ui-progress-bar': color ?? tint,
|
|
18
20
|
'--ui-progress-track': trackColor ?? track,
|
|
19
21
|
} as CSSProperties;
|
|
22
|
+
|
|
23
|
+
if (variant === 'circular') {
|
|
24
|
+
const indeterminate = value == null;
|
|
25
|
+
const clamped = indeterminate ? 0.25 : Math.max(0, Math.min(1, value));
|
|
26
|
+
const radius = (size - STROKE) / 2;
|
|
27
|
+
const circumference = 2 * Math.PI * radius;
|
|
28
|
+
return (
|
|
29
|
+
<svg
|
|
30
|
+
className={['ui-progress-ring', indeterminate && 'ui-progress-ring--indeterminate'].filter(Boolean).join(' ')}
|
|
31
|
+
width={size}
|
|
32
|
+
height={size}
|
|
33
|
+
viewBox={`0 0 ${size} ${size}`}
|
|
34
|
+
role="progressbar"
|
|
35
|
+
aria-valuemin={0}
|
|
36
|
+
aria-valuemax={1}
|
|
37
|
+
aria-valuenow={indeterminate ? undefined : clamped}
|
|
38
|
+
data-testid={testID}
|
|
39
|
+
style={vars}>
|
|
40
|
+
<circle className="ui-progress-ring__track" cx={size / 2} cy={size / 2} r={radius} fill="none" strokeWidth={STROKE}/>
|
|
41
|
+
<circle
|
|
42
|
+
className="ui-progress-ring__bar"
|
|
43
|
+
cx={size / 2}
|
|
44
|
+
cy={size / 2}
|
|
45
|
+
r={radius}
|
|
46
|
+
fill="none"
|
|
47
|
+
strokeWidth={STROKE}
|
|
48
|
+
strokeLinecap="round"
|
|
49
|
+
strokeDasharray={circumference}
|
|
50
|
+
strokeDashoffset={circumference * (1 - clamped)}
|
|
51
|
+
/>
|
|
52
|
+
</svg>
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const clamped = Math.max(0, Math.min(1, value ?? 0));
|
|
20
57
|
return (
|
|
21
58
|
<meter
|
|
22
59
|
className="ui-progress"
|
|
@@ -8,6 +8,15 @@
|
|
|
8
8
|
-webkit-appearance: none;
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
+
/*
|
|
12
|
+
* With `appearance: none` Chromium lays the bar out as an inline-block on a
|
|
13
|
+
* line box, so the value only paints about half the track height. Flex
|
|
14
|
+
* stretches the bar (and its value) to the full 6px.
|
|
15
|
+
*/
|
|
16
|
+
.ui-progress::-webkit-meter-inner-element {
|
|
17
|
+
display: flex;
|
|
18
|
+
}
|
|
19
|
+
|
|
11
20
|
.ui-progress::-webkit-meter-bar {
|
|
12
21
|
border: none;
|
|
13
22
|
border-radius: 999px;
|
|
@@ -25,3 +34,27 @@
|
|
|
25
34
|
border-radius: 999px;
|
|
26
35
|
background: var(--ui-progress-bar);
|
|
27
36
|
}
|
|
37
|
+
|
|
38
|
+
.ui-progress-ring {
|
|
39
|
+
flex-shrink: 0;
|
|
40
|
+
display: inline-block;
|
|
41
|
+
transform: rotate(-90deg);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
.ui-progress-ring__track {
|
|
45
|
+
stroke: var(--ui-progress-track);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
.ui-progress-ring__bar {
|
|
49
|
+
stroke: var(--ui-progress-bar);
|
|
50
|
+
transition: stroke-dashoffset 0.2s ease;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
.ui-progress-ring--indeterminate {
|
|
54
|
+
animation: ui-progress-spin 0.9s linear infinite;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
@keyframes ui-progress-spin {
|
|
58
|
+
from { transform: rotate(-90deg); }
|
|
59
|
+
to { transform: rotate(270deg); }
|
|
60
|
+
}
|
package/src/progress/types.ts
CHANGED
|
@@ -1,18 +1,35 @@
|
|
|
1
|
+
/** Shape of the indicator. */
|
|
2
|
+
export type ProgressVariant = 'linear' | 'circular';
|
|
3
|
+
|
|
1
4
|
/**
|
|
2
|
-
* Cross-platform
|
|
5
|
+
* Cross-platform progress indicator.
|
|
3
6
|
*
|
|
4
|
-
* Bridges the SwiftUI `ProgressView` on iOS, the
|
|
5
|
-
* `LinearProgressIndicator`
|
|
7
|
+
* Bridges the SwiftUI `ProgressView` (linear or circular style) on iOS, the
|
|
8
|
+
* Jetpack Compose `LinearProgressIndicator` / `CircularProgressIndicator` on
|
|
9
|
+
* Android, and the HTML `<meter>` element (linear) or an SVG ring (circular)
|
|
10
|
+
* on web.
|
|
6
11
|
*/
|
|
7
12
|
export interface ProgressProps {
|
|
8
13
|
/**
|
|
9
14
|
* Progress value between `0` and `1`.
|
|
10
|
-
* Omit for an indeterminate indicator (
|
|
15
|
+
* Omit for an indeterminate indicator (a spinner when `circular`; the linear
|
|
16
|
+
* web bar has no indeterminate state and renders empty).
|
|
11
17
|
*/
|
|
12
18
|
value?: number;
|
|
13
|
-
/**
|
|
19
|
+
/**
|
|
20
|
+
* Bar or ring.
|
|
21
|
+
* @default 'linear'
|
|
22
|
+
*/
|
|
23
|
+
variant?: ProgressVariant;
|
|
24
|
+
/**
|
|
25
|
+
* Diameter of the circular indicator in points/dp. Ignored for `linear`,
|
|
26
|
+
* and on iOS where the system spinner keeps its native size.
|
|
27
|
+
* @default 24
|
|
28
|
+
*/
|
|
29
|
+
size?: number;
|
|
30
|
+
/** Color of the filled portion. Defaults to the theme tint. */
|
|
14
31
|
color?: string;
|
|
15
|
-
/** Color of the unfilled track behind the bar. */
|
|
32
|
+
/** Color of the unfilled track behind the bar/ring. */
|
|
16
33
|
trackColor?: string;
|
|
17
34
|
/** Identifier used to locate the component in end-to-end tests. */
|
|
18
35
|
testID?: string;
|
package/src/screen/header.tsx
CHANGED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type {PickerValue} from '../picker/types';
|
|
2
|
+
import type {SegmentedControlProps} from './types';
|
|
3
|
+
|
|
4
|
+
import {Row, SegmentedButton, SingleChoiceSegmentedButtonRow, Text, useMaterialColors} from '@expo/ui/jetpack-compose';
|
|
5
|
+
import {fillMaxWidth, testID as testIDModifier} from '@expo/ui/jetpack-compose/modifiers';
|
|
6
|
+
import {onAccent} from '../accent';
|
|
7
|
+
import {useColor} from '../theme';
|
|
8
|
+
import {extractItems, PickerItem, useSelectedValue} from '../picker/shared';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Android renders the Material 3 `SingleChoiceSegmentedButtonRow`. The
|
|
12
|
+
* selected segment is filled with the live accent seed (matching the iOS
|
|
13
|
+
* tint cascade and the web accent) and the row pins the control to the
|
|
14
|
+
* trailing edge, mirroring the iOS Form row.
|
|
15
|
+
*/
|
|
16
|
+
function SegmentedControlComponent<T extends PickerValue>({
|
|
17
|
+
label,
|
|
18
|
+
children,
|
|
19
|
+
selectedValue,
|
|
20
|
+
onValueChange,
|
|
21
|
+
disabled,
|
|
22
|
+
accentColor,
|
|
23
|
+
testID,
|
|
24
|
+
}: SegmentedControlProps<T>) {
|
|
25
|
+
const colors = useMaterialColors();
|
|
26
|
+
const tint = useColor('tint');
|
|
27
|
+
const accent = accentColor ?? tint;
|
|
28
|
+
const items = extractItems<T>(children);
|
|
29
|
+
const [current, setValue] = useSelectedValue(selectedValue, onValueChange, items[0]?.value);
|
|
30
|
+
const segmentColors = {
|
|
31
|
+
activeContainerColor: accent,
|
|
32
|
+
activeContentColor: onAccent(accent),
|
|
33
|
+
activeBorderColor: colors.outline,
|
|
34
|
+
inactiveContainerColor: '#00000000',
|
|
35
|
+
inactiveContentColor: colors.onSurface,
|
|
36
|
+
inactiveBorderColor: colors.outline,
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
return (
|
|
40
|
+
<Row
|
|
41
|
+
verticalAlignment="center"
|
|
42
|
+
horizontalArrangement="spaceBetween"
|
|
43
|
+
modifiers={[fillMaxWidth(), ...(testID ? [testIDModifier(testID)] : [])]}>
|
|
44
|
+
{label != null ? (
|
|
45
|
+
<Text color={disabled ? colors.onSurfaceVariant : colors.onSurface}>{label}</Text>
|
|
46
|
+
) : null}
|
|
47
|
+
<SingleChoiceSegmentedButtonRow>
|
|
48
|
+
{items.map(item => (
|
|
49
|
+
<SegmentedButton
|
|
50
|
+
key={String(item.value)}
|
|
51
|
+
selected={item.value === current}
|
|
52
|
+
enabled={!disabled}
|
|
53
|
+
colors={segmentColors}
|
|
54
|
+
onClick={disabled ? undefined : () => setValue(item.value)}>
|
|
55
|
+
<SegmentedButton.Label>
|
|
56
|
+
<Text>{item.label}</Text>
|
|
57
|
+
</SegmentedButton.Label>
|
|
58
|
+
</SegmentedButton>
|
|
59
|
+
))}
|
|
60
|
+
</SingleChoiceSegmentedButtonRow>
|
|
61
|
+
</Row>
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
SegmentedControlComponent.Item = PickerItem;
|
|
66
|
+
|
|
67
|
+
export {SegmentedControlComponent as SegmentedControl};
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type {PickerValue} from '../picker/types';
|
|
2
|
+
import type {SegmentedControlProps} from './types';
|
|
3
|
+
import type {ViewModifier} from '@expo/ui/swift-ui/modifiers';
|
|
4
|
+
|
|
5
|
+
import {Picker as SwiftUIPicker, Text} from '@expo/ui/swift-ui';
|
|
6
|
+
import {pickerStyle, tag, tint, disabled as disabledMod} from '@expo/ui/swift-ui/modifiers';
|
|
7
|
+
import {extractItems, PickerItem, useSelectedValue} from '../picker/shared';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* iOS renders SwiftUI's `Picker` in the `segmented` style — the system
|
|
11
|
+
* `UISegmentedControl` — with the label leading inside a Form row. Drop it
|
|
12
|
+
* straight into a `FieldGroup.Section`.
|
|
13
|
+
*/
|
|
14
|
+
function SegmentedControlComponent<T extends PickerValue>({
|
|
15
|
+
label,
|
|
16
|
+
children,
|
|
17
|
+
selectedValue,
|
|
18
|
+
onValueChange,
|
|
19
|
+
disabled,
|
|
20
|
+
accentColor,
|
|
21
|
+
testID,
|
|
22
|
+
}: SegmentedControlProps<T>) {
|
|
23
|
+
const items = extractItems<T>(children);
|
|
24
|
+
const [current, setValue] = useSelectedValue(selectedValue, onValueChange, items[0]?.value);
|
|
25
|
+
const modifiers: ViewModifier[] = [pickerStyle('segmented')];
|
|
26
|
+
if (accentColor) modifiers.push(tint(accentColor));
|
|
27
|
+
if (disabled) modifiers.push(disabledMod(true));
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
<SwiftUIPicker
|
|
31
|
+
label={label}
|
|
32
|
+
selection={current}
|
|
33
|
+
onSelectionChange={value => setValue(value as T)}
|
|
34
|
+
modifiers={modifiers}
|
|
35
|
+
testID={testID}>
|
|
36
|
+
{items.map(item => (
|
|
37
|
+
<Text key={String(item.value)} modifiers={[tag(item.value)]}>
|
|
38
|
+
{item.label}
|
|
39
|
+
</Text>
|
|
40
|
+
))}
|
|
41
|
+
</SwiftUIPicker>
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
SegmentedControlComponent.Item = PickerItem;
|
|
46
|
+
|
|
47
|
+
export {SegmentedControlComponent as SegmentedControl};
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import './segmented.css';
|
|
2
|
+
import type {CSSProperties} from 'react';
|
|
3
|
+
import type {PickerValue} from '../picker/types';
|
|
4
|
+
import type {SegmentedControlProps} from './types';
|
|
5
|
+
import {StyleSheet, type TextStyle} from 'react-native';
|
|
6
|
+
import {Label} from '../typography';
|
|
7
|
+
import {flatten} from '../theme';
|
|
8
|
+
import {extractItems, PickerItem, useSelectedValue} from '../picker/shared';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* On web the control is a `radiogroup` of native `<button>`s styled after the
|
|
12
|
+
* iOS segmented control (grey pill, raised selected segment). The row mirrors
|
|
13
|
+
* the native layout: label on the leading edge, control on the trailing edge.
|
|
14
|
+
*/
|
|
15
|
+
function SegmentedControlComponent<T extends PickerValue>({
|
|
16
|
+
label,
|
|
17
|
+
children,
|
|
18
|
+
selectedValue,
|
|
19
|
+
onValueChange,
|
|
20
|
+
disabled,
|
|
21
|
+
accentColor,
|
|
22
|
+
testID,
|
|
23
|
+
style,
|
|
24
|
+
}: SegmentedControlProps<T>) {
|
|
25
|
+
const items = extractItems<T>(children);
|
|
26
|
+
const [current, setValue] = useSelectedValue(selectedValue, onValueChange, items[0]?.value);
|
|
27
|
+
const vars = flatten(StyleSheet.flatten(style) as TextStyle) as CSSProperties;
|
|
28
|
+
return (
|
|
29
|
+
<div
|
|
30
|
+
className={['ui-segmented', disabled && 'ui-segmented--disabled'].filter(Boolean).join(' ')}
|
|
31
|
+
style={vars}
|
|
32
|
+
data-testid={testID}>
|
|
33
|
+
{label != null ? <Label color="label" style={{flexShrink: 1}}>{label}</Label> : null}
|
|
34
|
+
<div className="ui-segmented__group" role="radiogroup" aria-label={label}>
|
|
35
|
+
{items.map(item => {
|
|
36
|
+
const selected = item.value === current;
|
|
37
|
+
return (
|
|
38
|
+
<button
|
|
39
|
+
key={String(item.value)}
|
|
40
|
+
type="button"
|
|
41
|
+
role="radio"
|
|
42
|
+
className="ui-segmented__item"
|
|
43
|
+
aria-checked={selected}
|
|
44
|
+
disabled={disabled}
|
|
45
|
+
style={selected && accentColor ? {color: accentColor} : undefined}
|
|
46
|
+
onClick={() => setValue(item.value)}>
|
|
47
|
+
{item.label}
|
|
48
|
+
</button>
|
|
49
|
+
);
|
|
50
|
+
})}
|
|
51
|
+
</div>
|
|
52
|
+
</div>
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
SegmentedControlComponent.Item = PickerItem;
|
|
57
|
+
|
|
58
|
+
export {SegmentedControlComponent as SegmentedControl};
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
.ui-segmented {
|
|
2
|
+
display: flex;
|
|
3
|
+
flex-direction: row;
|
|
4
|
+
align-items: center;
|
|
5
|
+
justify-content: space-between;
|
|
6
|
+
gap: 12px;
|
|
7
|
+
width: 100%;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
.ui-segmented__group {
|
|
11
|
+
display: inline-flex;
|
|
12
|
+
flex-shrink: 0;
|
|
13
|
+
flex-direction: row;
|
|
14
|
+
padding: 2px;
|
|
15
|
+
border-radius: 9px;
|
|
16
|
+
background: var(--color-pill-background);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
.ui-segmented__item {
|
|
20
|
+
-webkit-appearance: none;
|
|
21
|
+
appearance: none;
|
|
22
|
+
min-width: 48px;
|
|
23
|
+
margin: 0;
|
|
24
|
+
padding: 4px 12px;
|
|
25
|
+
border: none;
|
|
26
|
+
border-radius: 7px;
|
|
27
|
+
background: transparent;
|
|
28
|
+
color: var(--color-label);
|
|
29
|
+
font-family: var(--font-display);
|
|
30
|
+
font-size: 13px;
|
|
31
|
+
font-weight: 500;
|
|
32
|
+
line-height: 18px;
|
|
33
|
+
white-space: nowrap;
|
|
34
|
+
cursor: pointer;
|
|
35
|
+
transition: background-color 0.15s ease, box-shadow 0.15s ease;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/*
|
|
39
|
+
* Raised segment (iOS: white light / systemGray2-ish dark). `light-dark()`
|
|
40
|
+
* follows the element's `color-scheme`, which the theme CSS sets from the OS
|
|
41
|
+
* and which a host can force (Storybook's scheme toolbar does).
|
|
42
|
+
*/
|
|
43
|
+
.ui-segmented__item[aria-checked='true'] {
|
|
44
|
+
background: light-dark(#ffffff, #636366);
|
|
45
|
+
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12), 0 0 0 0.5px rgba(0, 0, 0, 0.04);
|
|
46
|
+
font-weight: 600;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
.ui-segmented__item:disabled {
|
|
50
|
+
cursor: default;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
.ui-segmented--disabled {
|
|
54
|
+
opacity: 0.4;
|
|
55
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type {ReactNode} from 'react';
|
|
2
|
+
import type {StyleProp, ViewStyle} from 'react-native';
|
|
3
|
+
import type {PickerValue} from '../picker/types';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Cross-platform segmented control: a single-choice row of segments.
|
|
7
|
+
*
|
|
8
|
+
* Bridges the SwiftUI `Picker` in its `segmented` style on iOS, the Jetpack
|
|
9
|
+
* Compose Material 3 `SingleChoiceSegmentedButtonRow` on Android, and a
|
|
10
|
+
* `radiogroup` of native `<button>`s on web. Options are declared with
|
|
11
|
+
* `SegmentedControl.Item` children, exactly like `Picker`. May be used
|
|
12
|
+
* controlled (`selectedValue` + `onValueChange`) or uncontrolled.
|
|
13
|
+
*/
|
|
14
|
+
export interface SegmentedControlProps<T extends PickerValue = PickerValue> {
|
|
15
|
+
/** Label rendered at the leading edge of the row, mirroring an iOS Form row. */
|
|
16
|
+
label?: string;
|
|
17
|
+
/** `SegmentedControl.Item` children that define the segments. */
|
|
18
|
+
children?: ReactNode;
|
|
19
|
+
/** Current value (controlled). When omitted the component keeps its own state. */
|
|
20
|
+
selectedValue?: T;
|
|
21
|
+
/** Called whenever the user selects a segment. */
|
|
22
|
+
onValueChange?: (value: T) => void;
|
|
23
|
+
/** Disables interaction. */
|
|
24
|
+
disabled?: boolean;
|
|
25
|
+
/** Tint of the selected segment (Android/web) or the picker (iOS). */
|
|
26
|
+
accentColor?: string;
|
|
27
|
+
/** Identifier used to locate the component in end-to-end tests. */
|
|
28
|
+
testID?: string;
|
|
29
|
+
/** Style applied to the row container (web only). */
|
|
30
|
+
style?: StyleProp<ViewStyle>;
|
|
31
|
+
}
|