@hzblj/kara-components 0.14.0 → 0.14.1
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/CHANGELOG.md +13 -0
- package/lib/module/atoms/activity-indicator/activity-indicator.android.js +14 -7
- package/lib/module/atoms/activity-indicator/activity-indicator.android.js.map +1 -1
- package/lib/module/atoms/activity-indicator/activity-indicator.ios.js +12 -2
- package/lib/module/atoms/activity-indicator/activity-indicator.ios.js.map +1 -1
- package/lib/module/atoms/date-picker/date-picker.android.js +13 -1
- package/lib/module/atoms/date-picker/date-picker.android.js.map +1 -1
- package/lib/module/atoms/date-picker/date-picker.ios.js +19 -12
- package/lib/module/atoms/date-picker/date-picker.ios.js.map +1 -1
- package/lib/module/atoms/picker/picker.android.js +48 -41
- package/lib/module/atoms/picker/picker.android.js.map +1 -1
- package/lib/module/atoms/picker/picker.ios.js +25 -2
- package/lib/module/atoms/picker/picker.ios.js.map +1 -1
- package/lib/module/atoms/segmented-control/segmented-control.android.js +25 -18
- package/lib/module/atoms/segmented-control/segmented-control.android.js.map +1 -1
- package/lib/module/atoms/segmented-control/segmented-control.ios.js +15 -2
- package/lib/module/atoms/segmented-control/segmented-control.ios.js.map +1 -1
- package/lib/module/host-context.js +8 -0
- package/lib/module/host-context.js.map +1 -1
- package/lib/typescript/atoms/activity-indicator/activity-indicator.ios.d.ts +4 -1
- package/lib/typescript/atoms/activity-indicator/activity-indicator.types.d.ts +2 -1
- package/lib/typescript/atoms/date-picker/date-picker.ios.d.ts +2 -1
- package/lib/typescript/atoms/date-picker/date-picker.types.d.ts +1 -1
- package/lib/typescript/atoms/picker/picker.ios.d.ts +5 -0
- package/lib/typescript/atoms/picker/picker.types.d.ts +6 -2
- package/lib/typescript/atoms/segmented-control/segmented-control.ios.d.ts +2 -1
- package/lib/typescript/atoms/segmented-control/segmented-control.types.d.ts +2 -1
- package/lib/typescript/host-context.d.ts +7 -0
- package/package.json +4 -4
- package/src/atoms/activity-indicator/activity-indicator.android.tsx +18 -5
- package/src/atoms/activity-indicator/activity-indicator.ios.tsx +10 -2
- package/src/atoms/activity-indicator/activity-indicator.types.ts +2 -1
- package/src/atoms/date-picker/date-picker.android.tsx +15 -1
- package/src/atoms/date-picker/date-picker.ios.tsx +20 -9
- package/src/atoms/date-picker/date-picker.types.ts +1 -1
- package/src/atoms/picker/picker.android.tsx +53 -44
- package/src/atoms/picker/picker.ios.tsx +28 -7
- package/src/atoms/picker/picker.types.ts +6 -2
- package/src/atoms/segmented-control/segmented-control.android.tsx +29 -18
- package/src/atoms/segmented-control/segmented-control.ios.tsx +10 -2
- package/src/atoms/segmented-control/segmented-control.types.ts +2 -1
- package/src/host-context.tsx +8 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"names":["SegmentedControl","NativeSegmentedControl","useAppTheme","useCallback","useMemo","jsx","_jsx","disabled","onChange","selectedIndex","testID","values","theme","face","typography","footnoteMedium","options","map","label","index","value","String","onValueChange","Number","activePillColor","color","activeTextColor","fontFamily","fontSize","inactiveTextColor","selectedValue"],"sourceRoot":"../../../../src","sources":["atoms/segmented-control/segmented-control.ios.tsx"],"mappings":";;AAAA,SAAQA,gBAAgB,IAAIC,sBAAsB,QAAO,6BAA6B;AACtF,SAAQC,WAAW,QAAO,mBAAmB;AAC7C,SAAQC,WAAW,EAAEC,OAAO,QAAO,OAAO;AAAA,SAAAC,GAAA,IAAAC,IAAA;
|
|
1
|
+
{"version":3,"names":["RNHostView","SegmentedControl","NativeSegmentedControl","useAppTheme","useCallback","useMemo","useInsideHost","jsx","_jsx","MATCH_HEIGHT_ONLY","horizontal","vertical","disabled","onChange","selectedIndex","testID","values","theme","insideHost","face","typography","footnoteMedium","options","map","label","index","value","String","onValueChange","Number","control","activePillColor","color","activeTextColor","fontFamily","fontSize","inactiveTextColor","selectedValue","matchContents","children"],"sourceRoot":"../../../../src","sources":["atoms/segmented-control/segmented-control.ios.tsx"],"mappings":";;AAAA,SAAQA,UAAU,QAAO,mBAAmB;AAC5C,SAAQC,gBAAgB,IAAIC,sBAAsB,QAAO,6BAA6B;AACtF,SAAQC,WAAW,QAAO,mBAAmB;AAC7C,SAAQC,WAAW,EAAEC,OAAO,QAAO,OAAO;AAE1C,SAAQC,aAAa,QAAO,uBAAoB;AAAA,SAAAC,GAAA,IAAAC,IAAA;AAGhD,MAAMC,iBAAiB,GAAG;EAACC,UAAU,EAAE,KAAK;EAAEC,QAAQ,EAAE;AAAI,CAAC;;AAE7D;AACA;AACA;AACA;AACA;AACA;AACA,OAAO,MAAMV,gBAA2C,GAAGA,CAAC;EAC1DW,QAAQ,GAAG,KAAK;EAChBC,QAAQ;EACRC,aAAa;EACbC,MAAM;EACNC;AACqB,CAAC,KAAK;EAC3B,MAAMC,KAAK,GAAGd,WAAW,CAAC,CAAC;EAC3B,MAAMe,UAAU,GAAGZ,aAAa,CAAC,CAAC;EAClC,MAAMa,IAAI,GAAGF,KAAK,CAACG,UAAU,CAACC,cAAc;;EAE5C;EACA,MAAMC,OAAO,GAAGjB,OAAO,CAAC,MAAMW,MAAM,CAACO,GAAG,CAAC,CAACC,KAAK,EAAEC,KAAK,MAAM;IAACD,KAAK;IAAEE,KAAK,EAAEC,MAAM,CAACF,KAAK;EAAC,CAAC,CAAC,CAAC,EAAE,CAACT,MAAM,CAAC,CAAC;EAEtG,MAAMY,aAAa,GAAGxB,WAAW,CAC9BsB,KAAa,IAAK;IACjB,MAAMD,KAAK,GAAGI,MAAM,CAACH,KAAK,CAAC;IAC3Bb,QAAQ,GAAGY,KAAK,EAAET,MAAM,CAACS,KAAK,CAAC,IAAI,EAAE,CAAC;EACxC,CAAC,EACD,CAACZ,QAAQ,EAAEG,MAAM,CACnB,CAAC;EAED,MAAMc,OAAO,gBACXtB,IAAA,CAACN,sBAAsB;IACrB6B,eAAe,EAAEd,KAAK,CAACe,KAAK,CAAC,qBAAqB,CAAE;IACpDC,eAAe,EAAEhB,KAAK,CAACe,KAAK,CAAC,mBAAmB,CAAE;IAClDpB,QAAQ,EAAEA,QAAS;IACnBsB,UAAU,EAAEf,IAAI,CAACe,UAAW;IAC5BC,QAAQ,EAAEhB,IAAI,CAACgB,QAAS;IACxBC,iBAAiB,EAAEnB,KAAK,CAACe,KAAK,CAAC,iBAAiB,CAAE;IAClDJ,aAAa,EAAEA,aAAc;IAC7BN,OAAO,EAAEA,OAAQ;IACjBe,aAAa,EAAEV,MAAM,CAACb,aAAa,CAAE;IACrCC,MAAM,EAAEA;EAAO,CAChB,CACF;EAED,OAAOG,UAAU,gBAAGV,IAAA,CAACR,UAAU;IAACsC,aAAa,EAAE7B,iBAAkB;IAAA8B,QAAA,EAAET;EAAO,CAAa,CAAC,GAAGA,OAAO;AACpG,CAAC","ignoreList":[]}
|
|
@@ -9,6 +9,14 @@ export const HostBoundary = ({
|
|
|
9
9
|
value: true,
|
|
10
10
|
children: children
|
|
11
11
|
});
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Whether this renders inside a `Screen` or `Island` already. An atom that brings its own host renders straight into
|
|
15
|
+
* that one instead: on Android a host nested in another is a real view beside the outer host's own `ComposeView`, and
|
|
16
|
+
* the outer host (a horizontal `LinearLayout`) measures its `ComposeView` in the width the nested one leaves over,
|
|
17
|
+
* which for a full-width nested host is none.
|
|
18
|
+
*/
|
|
19
|
+
export const useInsideHost = () => useContext(HostContext);
|
|
12
20
|
export const useHostGuard = atom => {
|
|
13
21
|
const insideHost = useContext(HostContext);
|
|
14
22
|
if (__DEV__ && !insideHost) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"names":["createContext","useContext","jsx","_jsx","HostContext","HostBoundary","children","Provider","value","useHostGuard","atom","insideHost","__DEV__","console","warn"],"sourceRoot":"../../src","sources":["host-context.tsx"],"mappings":";;AAAA,SAAQA,aAAa,EAAkBC,UAAU,QAAO,OAAO;AAAA,SAAAC,GAAA,IAAAC,IAAA;AAE/D,MAAMC,WAAW,gBAAGJ,aAAa,CAAC,KAAK,CAAC;AAExC,OAAO,MAAMK,YAAY,GAAGA,CAAC;EAACC;AAA+B,CAAC,kBAC5DH,IAAA,CAACC,WAAW,CAACG,QAAQ;EAACC,KAAK;EAAAF,QAAA,EAAEA;AAAQ,CAAuB,CAC7D;
|
|
1
|
+
{"version":3,"names":["createContext","useContext","jsx","_jsx","HostContext","HostBoundary","children","Provider","value","useInsideHost","useHostGuard","atom","insideHost","__DEV__","console","warn"],"sourceRoot":"../../src","sources":["host-context.tsx"],"mappings":";;AAAA,SAAQA,aAAa,EAAkBC,UAAU,QAAO,OAAO;AAAA,SAAAC,GAAA,IAAAC,IAAA;AAE/D,MAAMC,WAAW,gBAAGJ,aAAa,CAAC,KAAK,CAAC;AAExC,OAAO,MAAMK,YAAY,GAAGA,CAAC;EAACC;AAA+B,CAAC,kBAC5DH,IAAA,CAACC,WAAW,CAACG,QAAQ;EAACC,KAAK;EAAAF,QAAA,EAAEA;AAAQ,CAAuB,CAC7D;;AAED;AACA;AACA;AACA;AACA;AACA;AACA,OAAO,MAAMG,aAAa,GAAGA,CAAA,KAAMR,UAAU,CAACG,WAAW,CAAC;AAE1D,OAAO,MAAMM,YAAY,GAAIC,IAAY,IAAK;EAC5C,MAAMC,UAAU,GAAGX,UAAU,CAACG,WAAW,CAAC;EAE1C,IAAIS,OAAO,IAAI,CAACD,UAAU,EAAE;IAC1BE,OAAO,CAACC,IAAI,CACV,cAAcJ,IAAI,mGACpB,CAAC;EACH;AACF,CAAC","ignoreList":[]}
|
|
@@ -1,3 +1,6 @@
|
|
|
1
1
|
import type { ActivityIndicatorComponent } from './activity-indicator.types';
|
|
2
|
-
/**
|
|
2
|
+
/**
|
|
3
|
+
* React Native's spinner, which on iOS is UIKit's own `UIActivityIndicatorView`. Inside a `Screen` or `Island` it is
|
|
4
|
+
* carried into that host by `RNHostView`, sized to the spinner (see `useInsideHost`).
|
|
5
|
+
*/
|
|
3
6
|
export declare const ActivityIndicator: ActivityIndicatorComponent;
|
|
@@ -4,7 +4,8 @@ export type ActivityIndicatorSize = 'large' | 'small';
|
|
|
4
4
|
/**
|
|
5
5
|
* That something is loading, for as long as it takes: UIKit's spinner on iOS, Material 3's expressive
|
|
6
6
|
* `LoadingIndicator` on Android, its shapes morphing as it turns. A React Native view on iOS and a host of its own on
|
|
7
|
-
* Android, so it renders anywhere
|
|
7
|
+
* Android, so it renders anywhere; inside a `Screen` or `Island` it joins that host, beside other native views. For how
|
|
8
|
+
* far along something is, use `Progress`.
|
|
8
9
|
*/
|
|
9
10
|
export type ActivityIndicatorProps = {
|
|
10
11
|
/** What is loading, for VoiceOver and TalkBack: "Loading photos". Unset, the platform's own word ("In progress"). */
|
|
@@ -3,6 +3,7 @@ import type { DatePickerComponent } from './date-picker.types';
|
|
|
3
3
|
* The system's compact date picker: the value in a pill, and a tap on it opens the calendar popover UIKit owns. Its own
|
|
4
4
|
* host, sized to the pill on both axes — a host that took the width it was given would lay the pill out against the
|
|
5
5
|
* far edge of it, or past its container's — so it drops into a row the way a `Text` does. The label is the caller's,
|
|
6
|
-
* beside it; the calendar is in the accent.
|
|
6
|
+
* beside it; the calendar is in the accent. Inside a `Screen` or `Island` it renders into that host instead (see
|
|
7
|
+
* `useInsideHost`).
|
|
7
8
|
*/
|
|
8
9
|
export declare const DatePicker: DatePickerComponent;
|
|
@@ -3,7 +3,7 @@ export type DatePickerMode = 'date' | 'datetime' | 'time';
|
|
|
3
3
|
/**
|
|
4
4
|
* A date or time picker: SwiftUI's compact `DatePicker` on iOS, a field that opens Material's picker dialog on a tap
|
|
5
5
|
* on Android. The React Native tier: it brings its own host and sizes itself to the value, so it sits in a row beside
|
|
6
|
-
* its label like a `Text`.
|
|
6
|
+
* its label like a `Text`. Inside a `Screen` or `Island` it joins that host instead, beside a native `Text` label.
|
|
7
7
|
*/
|
|
8
8
|
export type DatePickerProps = {
|
|
9
9
|
/** The latest date that can be picked. Android ignores it in `time` mode. */
|
|
@@ -1,2 +1,7 @@
|
|
|
1
1
|
import type { PickerComponent } from './picker.types';
|
|
2
|
+
/**
|
|
3
|
+
* A SwiftUI wheel. On its own it is `@expo/ui`'s community picker, which brings a host of its own. Inside a `Screen` or
|
|
4
|
+
* `Island` it is the same wheel rendered into that host instead (see `useInsideHost`): a host nested in the island's
|
|
5
|
+
* stack is given no height there, and the wheel spills over the views around it.
|
|
6
|
+
*/
|
|
2
7
|
export declare const Picker: PickerComponent;
|
|
@@ -8,7 +8,8 @@ export type PickerOption = {
|
|
|
8
8
|
};
|
|
9
9
|
/**
|
|
10
10
|
* A single choice from a list: a SwiftUI wheel picker on iOS, Material's exposed dropdown on Android. It brings its
|
|
11
|
-
* own host, so it renders outside `Screen` and `Island` as well.
|
|
11
|
+
* own host, so it renders outside `Screen` and `Island` as well. Inside one it renders into that host instead, so it
|
|
12
|
+
* can share a stack with other native views.
|
|
12
13
|
*/
|
|
13
14
|
export type PickerProps = {
|
|
14
15
|
/** Stops the user changing the selection, and dims the picker. */
|
|
@@ -17,7 +18,10 @@ export type PickerProps = {
|
|
|
17
18
|
onValueChange?: (value: string) => void;
|
|
18
19
|
/** The choices, in order. */
|
|
19
20
|
options: PickerOption[];
|
|
20
|
-
/**
|
|
21
|
+
/**
|
|
22
|
+
* Android: the dropdown field's test tag. iOS: the wheel's, inside a `Screen` or `Island` only; on its own,
|
|
23
|
+
* `@expo/ui`'s wheel picker drops it.
|
|
24
|
+
*/
|
|
21
25
|
testID?: string;
|
|
22
26
|
/** The selected option's `value`. Controlled: pass back what `onValueChange` reports. */
|
|
23
27
|
value: string;
|
|
@@ -2,6 +2,7 @@ import type { SegmentedControlComponent } from './segmented-control.types';
|
|
|
2
2
|
/**
|
|
3
3
|
* kara's own `UISegmentedControl`, with the pill in the accent and white text on it — the system control tints only the
|
|
4
4
|
* pill, and SwiftUI's segmented picker not even that. A plain `UIView` in React Native layout: it needs no host, takes
|
|
5
|
-
* the width it is given and UIKit's height.
|
|
5
|
+
* the width it is given and UIKit's height. Inside a `Screen` or `Island` it is carried into that host by `RNHostView`
|
|
6
|
+
* (see `useInsideHost`), the host's width and its own height. UIKit plays the control's own feedback.
|
|
6
7
|
*/
|
|
7
8
|
export declare const SegmentedControl: SegmentedControlComponent;
|
|
@@ -2,7 +2,8 @@ import type { ReactNode } from 'react';
|
|
|
2
2
|
/**
|
|
3
3
|
* A row of mutually exclusive segments, the selected one in the accent: kara's own `UISegmentedControl` on iOS,
|
|
4
4
|
* Material's single-choice segmented buttons on Android. It needs no `Screen` or `Island`: iOS is a plain view, Android
|
|
5
|
-
* brings its own host.
|
|
5
|
+
* brings its own host. Inside one it joins that host, so it can share a stack with other native views. It takes the
|
|
6
|
+
* width it is given; the height is the platform's.
|
|
6
7
|
*/
|
|
7
8
|
export type SegmentedControlProps = {
|
|
8
9
|
/** Stops the user changing the segment, and dims the control. */
|
|
@@ -2,4 +2,11 @@ import { type ReactNode } from 'react';
|
|
|
2
2
|
export declare const HostBoundary: ({ children }: {
|
|
3
3
|
children: ReactNode;
|
|
4
4
|
}) => import("react").JSX.Element;
|
|
5
|
+
/**
|
|
6
|
+
* Whether this renders inside a `Screen` or `Island` already. An atom that brings its own host renders straight into
|
|
7
|
+
* that one instead: on Android a host nested in another is a real view beside the outer host's own `ComposeView`, and
|
|
8
|
+
* the outer host (a horizontal `LinearLayout`) measures its `ComposeView` in the width the nested one leaves over,
|
|
9
|
+
* which for a full-width nested host is none.
|
|
10
|
+
*/
|
|
11
|
+
export declare const useInsideHost: () => boolean;
|
|
5
12
|
export declare const useHostGuard: (atom: string) => void;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hzblj/kara-components",
|
|
3
|
-
"version": "0.14.
|
|
3
|
+
"version": "0.14.1",
|
|
4
4
|
"description": "The atoms, molecules, organisms and templates of @hzblj/kara-ui — real SwiftUI and Jetpack Compose views through @expo/ui.",
|
|
5
5
|
"author": "Jan Blazej",
|
|
6
6
|
"license": "MIT",
|
|
@@ -61,9 +61,9 @@
|
|
|
61
61
|
"dependencies": {
|
|
62
62
|
"@hzblj/expo-native-icons": "^0.1.0",
|
|
63
63
|
"@hzblj/expo-rich-haptics": "^0.1.0",
|
|
64
|
-
"@hzblj/kara-native": "^0.14.
|
|
65
|
-
"@hzblj/kara-sx": "^0.14.
|
|
66
|
-
"@hzblj/kara-theme": "^0.14.
|
|
64
|
+
"@hzblj/kara-native": "^0.14.1",
|
|
65
|
+
"@hzblj/kara-sx": "^0.14.1",
|
|
66
|
+
"@hzblj/kara-theme": "^0.14.1",
|
|
67
67
|
"sf-symbols-typescript": "^2.2.0"
|
|
68
68
|
},
|
|
69
69
|
"peerDependencies": {
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import {Host, LoadingIndicator} from '@expo/ui/jetpack-compose'
|
|
2
|
-
import {fillMaxSize, testID as testIdModifier} from '@expo/ui/jetpack-compose/modifiers'
|
|
2
|
+
import {fillMaxSize, size as sizeModifier, testID as testIdModifier} from '@expo/ui/jetpack-compose/modifiers'
|
|
3
3
|
import {useAccessibilityModifiers} from '@hzblj/kara-sx'
|
|
4
4
|
import {useAppTheme} from '@hzblj/kara-theme'
|
|
5
5
|
import {useMemo} from 'react'
|
|
6
6
|
import {StyleSheet} from 'react-native'
|
|
7
7
|
|
|
8
|
+
import {useInsideHost} from '../../host-context'
|
|
8
9
|
import type {ActivityIndicatorComponent, ActivityIndicatorProps} from './activity-indicator.types'
|
|
9
10
|
|
|
10
11
|
/** Material's 48dp for a screen; half of it in a row, about where React Native's small spinner sat. */
|
|
@@ -15,7 +16,8 @@ const styles = StyleSheet.create({
|
|
|
15
16
|
|
|
16
17
|
/**
|
|
17
18
|
* Material 3's expressive `LoadingIndicator` in a host of its own, sized here rather than measured, so it renders
|
|
18
|
-
* anywhere and takes its place in the first frame.
|
|
19
|
+
* anywhere and takes its place in the first frame. Inside a `Screen` or `Island` it renders into that host instead (see
|
|
20
|
+
* `useInsideHost`), the same size.
|
|
19
21
|
*/
|
|
20
22
|
export const ActivityIndicator: ActivityIndicatorComponent = ({
|
|
21
23
|
accessibilityLabel,
|
|
@@ -24,15 +26,26 @@ export const ActivityIndicator: ActivityIndicatorComponent = ({
|
|
|
24
26
|
testID,
|
|
25
27
|
}: ActivityIndicatorProps) => {
|
|
26
28
|
const theme = useAppTheme()
|
|
29
|
+
const insideHost = useInsideHost()
|
|
27
30
|
const accessibilityModifiers = useAccessibilityModifiers({accessibilityLabel})
|
|
28
31
|
const modifiers = useMemo(
|
|
29
|
-
() => [
|
|
30
|
-
|
|
32
|
+
() => [
|
|
33
|
+
insideHost ? sizeModifier(styles[size].width, styles[size].height) : fillMaxSize(),
|
|
34
|
+
...(testID ? [testIdModifier(testID)] : []),
|
|
35
|
+
...accessibilityModifiers,
|
|
36
|
+
],
|
|
37
|
+
[accessibilityModifiers, insideHost, size, testID]
|
|
31
38
|
)
|
|
32
39
|
|
|
40
|
+
const indicator = <LoadingIndicator color={theme.color(color)} modifiers={modifiers} />
|
|
41
|
+
|
|
42
|
+
if (insideHost) {
|
|
43
|
+
return indicator
|
|
44
|
+
}
|
|
45
|
+
|
|
33
46
|
return (
|
|
34
47
|
<Host colorScheme={theme.dark ? 'dark' : 'light'} seedColor={theme.materialSeed} style={styles[size]}>
|
|
35
|
-
|
|
48
|
+
{indicator}
|
|
36
49
|
</Host>
|
|
37
50
|
)
|
|
38
51
|
}
|
|
@@ -1,9 +1,14 @@
|
|
|
1
|
+
import {RNHostView} from '@expo/ui/swift-ui'
|
|
1
2
|
import {useAppTheme} from '@hzblj/kara-theme'
|
|
2
3
|
import {ActivityIndicator as RNActivityIndicator} from 'react-native'
|
|
3
4
|
|
|
5
|
+
import {useInsideHost} from '../../host-context'
|
|
4
6
|
import type {ActivityIndicatorComponent, ActivityIndicatorProps} from './activity-indicator.types'
|
|
5
7
|
|
|
6
|
-
/**
|
|
8
|
+
/**
|
|
9
|
+
* React Native's spinner, which on iOS is UIKit's own `UIActivityIndicatorView`. Inside a `Screen` or `Island` it is
|
|
10
|
+
* carried into that host by `RNHostView`, sized to the spinner (see `useInsideHost`).
|
|
11
|
+
*/
|
|
7
12
|
export const ActivityIndicator: ActivityIndicatorComponent = ({
|
|
8
13
|
accessibilityLabel,
|
|
9
14
|
color = 'fill.accent.primary',
|
|
@@ -11,8 +16,9 @@ export const ActivityIndicator: ActivityIndicatorComponent = ({
|
|
|
11
16
|
testID,
|
|
12
17
|
}: ActivityIndicatorProps) => {
|
|
13
18
|
const theme = useAppTheme()
|
|
19
|
+
const insideHost = useInsideHost()
|
|
14
20
|
|
|
15
|
-
|
|
21
|
+
const spinner = (
|
|
16
22
|
<RNActivityIndicator
|
|
17
23
|
accessibilityLabel={accessibilityLabel}
|
|
18
24
|
color={theme.color(color)}
|
|
@@ -20,4 +26,6 @@ export const ActivityIndicator: ActivityIndicatorComponent = ({
|
|
|
20
26
|
testID={testID}
|
|
21
27
|
/>
|
|
22
28
|
)
|
|
29
|
+
|
|
30
|
+
return insideHost ? <RNHostView matchContents>{spinner}</RNHostView> : spinner
|
|
23
31
|
}
|
|
@@ -6,7 +6,8 @@ export type ActivityIndicatorSize = 'large' | 'small'
|
|
|
6
6
|
/**
|
|
7
7
|
* That something is loading, for as long as it takes: UIKit's spinner on iOS, Material 3's expressive
|
|
8
8
|
* `LoadingIndicator` on Android, its shapes morphing as it turns. A React Native view on iOS and a host of its own on
|
|
9
|
-
* Android, so it renders anywhere
|
|
9
|
+
* Android, so it renders anywhere; inside a `Screen` or `Island` it joins that host, beside other native views. For how
|
|
10
|
+
* far along something is, use `Progress`.
|
|
10
11
|
*/
|
|
11
12
|
export type ActivityIndicatorProps = {
|
|
12
13
|
/** What is loading, for VoiceOver and TalkBack: "Loading photos". Unset, the platform's own word ("In progress"). */
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import {DateTimePicker} from '@expo/ui/community/datetime-picker'
|
|
2
|
+
import {RNHostView} from '@expo/ui/jetpack-compose'
|
|
2
3
|
import {useAppTheme} from '@hzblj/kara-theme'
|
|
3
4
|
import {useCallback, useState} from 'react'
|
|
4
5
|
|
|
6
|
+
import {useInsideHost} from '../../host-context'
|
|
7
|
+
import {Box} from '../box'
|
|
5
8
|
import {Pressable} from '../pressable'
|
|
6
9
|
import {RnText} from '../rn-text'
|
|
7
10
|
import type {DatePickerComponent, DatePickerMode, DatePickerProps} from './date-picker.types'
|
|
@@ -21,6 +24,8 @@ const formatted = (value: Date, mode: DatePickerMode): string => {
|
|
|
21
24
|
/**
|
|
22
25
|
* The value as a field that opens Material's date picker dialog on a tap. Android's picker *is* a dialog — mounted, it
|
|
23
26
|
* opens — so it is mounted only while open, and unmounted on confirm or dismiss, as the component asks its caller to.
|
|
27
|
+
* Both are React Native views; inside a `Screen` or `Island` `RNHostView` carries them into that host, sized to the
|
|
28
|
+
* field (see `useInsideHost`).
|
|
24
29
|
*/
|
|
25
30
|
export const DatePicker: DatePickerComponent = ({
|
|
26
31
|
max,
|
|
@@ -31,6 +36,7 @@ export const DatePicker: DatePickerComponent = ({
|
|
|
31
36
|
value,
|
|
32
37
|
}: DatePickerProps) => {
|
|
33
38
|
const theme = useAppTheme()
|
|
39
|
+
const insideHost = useInsideHost()
|
|
34
40
|
const [open, setOpen] = useState(false)
|
|
35
41
|
|
|
36
42
|
const show = useCallback(() => setOpen(true), [])
|
|
@@ -44,7 +50,7 @@ export const DatePicker: DatePickerComponent = ({
|
|
|
44
50
|
[onValueChange]
|
|
45
51
|
)
|
|
46
52
|
|
|
47
|
-
|
|
53
|
+
const field = (
|
|
48
54
|
<>
|
|
49
55
|
<Pressable
|
|
50
56
|
alignSelf="flex-start"
|
|
@@ -71,4 +77,12 @@ export const DatePicker: DatePickerComponent = ({
|
|
|
71
77
|
) : null}
|
|
72
78
|
</>
|
|
73
79
|
)
|
|
80
|
+
|
|
81
|
+
return insideHost ? (
|
|
82
|
+
<RNHostView matchContents>
|
|
83
|
+
<Box>{field}</Box>
|
|
84
|
+
</RNHostView>
|
|
85
|
+
) : (
|
|
86
|
+
field
|
|
87
|
+
)
|
|
74
88
|
}
|
|
@@ -3,6 +3,7 @@ import {datePickerStyle, labelsHidden, tint} from '@expo/ui/swift-ui/modifiers'
|
|
|
3
3
|
import {useAppTheme} from '@hzblj/kara-theme'
|
|
4
4
|
import {useMemo} from 'react'
|
|
5
5
|
|
|
6
|
+
import {useInsideHost} from '../../host-context'
|
|
6
7
|
import type {DatePickerComponent, DatePickerMode, DatePickerProps} from './date-picker.types'
|
|
7
8
|
|
|
8
9
|
const COMPONENTS = {
|
|
@@ -15,7 +16,8 @@ const COMPONENTS = {
|
|
|
15
16
|
* The system's compact date picker: the value in a pill, and a tap on it opens the calendar popover UIKit owns. Its own
|
|
16
17
|
* host, sized to the pill on both axes — a host that took the width it was given would lay the pill out against the
|
|
17
18
|
* far edge of it, or past its container's — so it drops into a row the way a `Text` does. The label is the caller's,
|
|
18
|
-
* beside it; the calendar is in the accent.
|
|
19
|
+
* beside it; the calendar is in the accent. Inside a `Screen` or `Island` it renders into that host instead (see
|
|
20
|
+
* `useInsideHost`).
|
|
19
21
|
*/
|
|
20
22
|
export const DatePicker: DatePickerComponent = ({
|
|
21
23
|
max,
|
|
@@ -26,22 +28,31 @@ export const DatePicker: DatePickerComponent = ({
|
|
|
26
28
|
value,
|
|
27
29
|
}: DatePickerProps) => {
|
|
28
30
|
const theme = useAppTheme()
|
|
31
|
+
const insideHost = useInsideHost()
|
|
29
32
|
|
|
30
33
|
const modifiers = useMemo(
|
|
31
34
|
() => [datePickerStyle('compact'), labelsHidden(), tint(theme.color('fill.accent.primary'))],
|
|
32
35
|
[theme]
|
|
33
36
|
)
|
|
34
37
|
|
|
38
|
+
const picker = (
|
|
39
|
+
<SwiftUIDatePicker
|
|
40
|
+
displayedComponents={[...COMPONENTS[mode]]}
|
|
41
|
+
modifiers={modifiers}
|
|
42
|
+
onDateChange={onValueChange}
|
|
43
|
+
range={min || max ? {end: max, start: min} : undefined}
|
|
44
|
+
selection={value}
|
|
45
|
+
testID={testID}
|
|
46
|
+
/>
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
if (insideHost) {
|
|
50
|
+
return picker
|
|
51
|
+
}
|
|
52
|
+
|
|
35
53
|
return (
|
|
36
54
|
<Host colorScheme={theme.dark ? 'dark' : 'light'} matchContents>
|
|
37
|
-
|
|
38
|
-
displayedComponents={[...COMPONENTS[mode]]}
|
|
39
|
-
modifiers={modifiers}
|
|
40
|
-
onDateChange={onValueChange}
|
|
41
|
-
range={min || max ? {end: max, start: min} : undefined}
|
|
42
|
-
selection={value}
|
|
43
|
-
testID={testID}
|
|
44
|
-
/>
|
|
55
|
+
{picker}
|
|
45
56
|
</Host>
|
|
46
57
|
)
|
|
47
58
|
}
|
|
@@ -5,7 +5,7 @@ export type DatePickerMode = 'date' | 'datetime' | 'time'
|
|
|
5
5
|
/**
|
|
6
6
|
* A date or time picker: SwiftUI's compact `DatePicker` on iOS, a field that opens Material's picker dialog on a tap
|
|
7
7
|
* on Android. The React Native tier: it brings its own host and sizes itself to the value, so it sits in a row beside
|
|
8
|
-
* its label like a `Text`.
|
|
8
|
+
* its label like a `Text`. Inside a `Screen` or `Island` it joins that host instead, beside a native `Text` label.
|
|
9
9
|
*/
|
|
10
10
|
export type DatePickerProps = {
|
|
11
11
|
/** The latest date that can be picked. Android ignores it in `time` mode. */
|
|
@@ -12,7 +12,7 @@ import {useAppTheme} from '@hzblj/kara-theme'
|
|
|
12
12
|
import {useEffect, useMemo, useState} from 'react'
|
|
13
13
|
|
|
14
14
|
import {Glyph} from '../../glyph'
|
|
15
|
-
import {HostBoundary} from '../../host-context'
|
|
15
|
+
import {HostBoundary, useInsideHost} from '../../host-context'
|
|
16
16
|
import type {PickerComponent, PickerProps} from './picker.types'
|
|
17
17
|
|
|
18
18
|
const MATCH_HEIGHT_ONLY = {horizontal: false, vertical: true}
|
|
@@ -20,9 +20,12 @@ const MATCH_HEIGHT_ONLY = {horizontal: false, vertical: true}
|
|
|
20
20
|
/**
|
|
21
21
|
* Material's exposed dropdown: a read-only outlined field the width of its container, opening a menu of the options
|
|
22
22
|
* under it. Its own host, themed like an `Island`, so it drops into React Native layout like the iOS picker does.
|
|
23
|
+
* Inside a `Screen` or `Island` it renders into that host instead (see `useInsideHost`): a host of its own there
|
|
24
|
+
* would leave the outer one no width, and the native views beside it would disappear.
|
|
23
25
|
*/
|
|
24
26
|
export const Picker: PickerComponent = ({disabled = false, onValueChange, options, testID, value}: PickerProps) => {
|
|
25
27
|
const theme = useAppTheme()
|
|
28
|
+
const insideHost = useInsideHost()
|
|
26
29
|
const [expanded, setExpanded] = useState(false)
|
|
27
30
|
const selectedLabel = options.find(option => option.value === value)?.label ?? ''
|
|
28
31
|
const label = useNativeState(selectedLabel)
|
|
@@ -37,52 +40,58 @@ export const Picker: PickerComponent = ({disabled = false, onValueChange, option
|
|
|
37
40
|
label.set(selectedLabel)
|
|
38
41
|
}, [label, selectedLabel])
|
|
39
42
|
|
|
40
|
-
|
|
41
|
-
<
|
|
42
|
-
<
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
43
|
+
const menu = (
|
|
44
|
+
<ExposedDropdownMenuBox expanded={expanded} onExpandedChange={disabled ? undefined : setExpanded}>
|
|
45
|
+
<OutlinedTextField
|
|
46
|
+
enabled={!disabled}
|
|
47
|
+
modifiers={fieldModifiers}
|
|
48
|
+
readOnly
|
|
49
|
+
singleLine
|
|
50
|
+
textStyle={{
|
|
51
|
+
color: theme.colorString('content.primary'),
|
|
52
|
+
fontFamily: theme.typography.body.fontFamily,
|
|
53
|
+
fontWeight: theme.typography.body.fontWeight,
|
|
54
|
+
}}
|
|
55
|
+
value={label}
|
|
56
|
+
>
|
|
57
|
+
<OutlinedTextField.TrailingIcon>
|
|
58
|
+
<Glyph color="content.secondary" name="chevronDown" size="s20" />
|
|
59
|
+
</OutlinedTextField.TrailingIcon>
|
|
60
|
+
</OutlinedTextField>
|
|
61
|
+
<ExposedDropdownMenu
|
|
62
|
+
containerColor={theme.color('surface.primary')}
|
|
63
|
+
expanded={expanded}
|
|
64
|
+
onDismissRequest={() => setExpanded(false)}
|
|
65
|
+
>
|
|
66
|
+
{options.map(option => (
|
|
67
|
+
<DropdownMenuItem
|
|
68
|
+
key={option.value}
|
|
69
|
+
onClick={() => {
|
|
70
|
+
setExpanded(false)
|
|
71
|
+
onValueChange?.(option.value)
|
|
53
72
|
}}
|
|
54
|
-
value={label}
|
|
55
73
|
>
|
|
56
|
-
<
|
|
57
|
-
<
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
<ExposedDropdownMenu
|
|
61
|
-
containerColor={theme.color('surface.primary')}
|
|
62
|
-
expanded={expanded}
|
|
63
|
-
onDismissRequest={() => setExpanded(false)}
|
|
64
|
-
>
|
|
65
|
-
{options.map(option => (
|
|
66
|
-
<DropdownMenuItem
|
|
67
|
-
key={option.value}
|
|
68
|
-
onClick={() => {
|
|
69
|
-
setExpanded(false)
|
|
70
|
-
onValueChange?.(option.value)
|
|
71
|
-
}}
|
|
74
|
+
<DropdownMenuItem.Text>
|
|
75
|
+
<ComposeText
|
|
76
|
+
color={theme.colorString(option.value === value ? 'content.accent' : 'content.primary')}
|
|
77
|
+
style={theme.typography.body}
|
|
72
78
|
>
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
79
|
+
{option.label}
|
|
80
|
+
</ComposeText>
|
|
81
|
+
</DropdownMenuItem.Text>
|
|
82
|
+
</DropdownMenuItem>
|
|
83
|
+
))}
|
|
84
|
+
</ExposedDropdownMenu>
|
|
85
|
+
</ExposedDropdownMenuBox>
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
if (insideHost) {
|
|
89
|
+
return menu
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return (
|
|
93
|
+
<Host colorScheme={theme.dark ? 'dark' : 'light'} matchContents={MATCH_HEIGHT_ONLY} seedColor={theme.materialSeed}>
|
|
94
|
+
<HostBoundary>{menu}</HostBoundary>
|
|
86
95
|
</Host>
|
|
87
96
|
)
|
|
88
97
|
}
|
|
@@ -1,23 +1,44 @@
|
|
|
1
1
|
import {Picker as NativePicker} from '@expo/ui/community/picker'
|
|
2
|
+
import {Picker as SwiftUIPicker, Text as SwiftUIText} from '@expo/ui/swift-ui'
|
|
3
|
+
import {disabled as disabledModifier, foregroundStyle, pickerStyle, tag} from '@expo/ui/swift-ui/modifiers'
|
|
2
4
|
import {useAppTheme} from '@hzblj/kara-theme'
|
|
3
|
-
import {useCallback} from 'react'
|
|
5
|
+
import {useCallback, useMemo} from 'react'
|
|
4
6
|
|
|
7
|
+
import {useInsideHost} from '../../host-context'
|
|
5
8
|
import type {PickerComponent, PickerProps} from './picker.types'
|
|
6
9
|
|
|
10
|
+
/**
|
|
11
|
+
* A SwiftUI wheel. On its own it is `@expo/ui`'s community picker, which brings a host of its own. Inside a `Screen` or
|
|
12
|
+
* `Island` it is the same wheel rendered into that host instead (see `useInsideHost`): a host nested in the island's
|
|
13
|
+
* stack is given no height there, and the wheel spills over the views around it.
|
|
14
|
+
*/
|
|
7
15
|
export const Picker: PickerComponent = ({disabled = false, onValueChange, options, testID, value}: PickerProps) => {
|
|
8
16
|
const theme = useAppTheme()
|
|
17
|
+
const insideHost = useInsideHost()
|
|
18
|
+
const color = theme.colorString('content.primary')
|
|
9
19
|
|
|
10
20
|
const onChange = useCallback((next: string | number | null) => onValueChange?.(String(next)), [onValueChange])
|
|
21
|
+
const wheelModifiers = useMemo(
|
|
22
|
+
() => [pickerStyle('wheel'), ...(disabled ? [disabledModifier(true)] : [])],
|
|
23
|
+
[disabled]
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
if (insideHost) {
|
|
27
|
+
return (
|
|
28
|
+
<SwiftUIPicker modifiers={wheelModifiers} onSelectionChange={onChange} selection={value} testID={testID}>
|
|
29
|
+
{options.map(option => (
|
|
30
|
+
<SwiftUIText key={option.value} modifiers={[tag(option.value), foregroundStyle(color)]}>
|
|
31
|
+
{option.label}
|
|
32
|
+
</SwiftUIText>
|
|
33
|
+
))}
|
|
34
|
+
</SwiftUIPicker>
|
|
35
|
+
)
|
|
36
|
+
}
|
|
11
37
|
|
|
12
38
|
return (
|
|
13
39
|
<NativePicker enabled={!disabled} onValueChange={onChange} selectedValue={value} testID={testID}>
|
|
14
40
|
{options.map(option => (
|
|
15
|
-
<NativePicker.Item
|
|
16
|
-
color={theme.colorString('content.primary')}
|
|
17
|
-
key={option.value}
|
|
18
|
-
label={option.label}
|
|
19
|
-
value={option.value}
|
|
20
|
-
/>
|
|
41
|
+
<NativePicker.Item color={color} key={option.value} label={option.label} value={option.value} />
|
|
21
42
|
))}
|
|
22
43
|
</NativePicker>
|
|
23
44
|
)
|
|
@@ -10,7 +10,8 @@ export type PickerOption = {
|
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
12
|
* A single choice from a list: a SwiftUI wheel picker on iOS, Material's exposed dropdown on Android. It brings its
|
|
13
|
-
* own host, so it renders outside `Screen` and `Island` as well.
|
|
13
|
+
* own host, so it renders outside `Screen` and `Island` as well. Inside one it renders into that host instead, so it
|
|
14
|
+
* can share a stack with other native views.
|
|
14
15
|
*/
|
|
15
16
|
export type PickerProps = {
|
|
16
17
|
/** Stops the user changing the selection, and dims the picker. */
|
|
@@ -19,7 +20,10 @@ export type PickerProps = {
|
|
|
19
20
|
onValueChange?: (value: string) => void
|
|
20
21
|
/** The choices, in order. */
|
|
21
22
|
options: PickerOption[]
|
|
22
|
-
/**
|
|
23
|
+
/**
|
|
24
|
+
* Android: the dropdown field's test tag. iOS: the wheel's, inside a `Screen` or `Island` only; on its own,
|
|
25
|
+
* `@expo/ui`'s wheel picker drops it.
|
|
26
|
+
*/
|
|
23
27
|
testID?: string
|
|
24
28
|
/** The selected option's `value`. Controlled: pass back what `onValueChange` reports. */
|
|
25
29
|
value: string
|