@hzblj/kara-components 0.14.0 → 0.14.2

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.
Files changed (42) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/lib/module/atoms/activity-indicator/activity-indicator.android.js +14 -7
  3. package/lib/module/atoms/activity-indicator/activity-indicator.android.js.map +1 -1
  4. package/lib/module/atoms/activity-indicator/activity-indicator.ios.js +12 -2
  5. package/lib/module/atoms/activity-indicator/activity-indicator.ios.js.map +1 -1
  6. package/lib/module/atoms/date-picker/date-picker.android.js +13 -1
  7. package/lib/module/atoms/date-picker/date-picker.android.js.map +1 -1
  8. package/lib/module/atoms/date-picker/date-picker.ios.js +19 -12
  9. package/lib/module/atoms/date-picker/date-picker.ios.js.map +1 -1
  10. package/lib/module/atoms/picker/picker.android.js +48 -41
  11. package/lib/module/atoms/picker/picker.android.js.map +1 -1
  12. package/lib/module/atoms/picker/picker.ios.js +25 -2
  13. package/lib/module/atoms/picker/picker.ios.js.map +1 -1
  14. package/lib/module/atoms/segmented-control/segmented-control.android.js +25 -18
  15. package/lib/module/atoms/segmented-control/segmented-control.android.js.map +1 -1
  16. package/lib/module/atoms/segmented-control/segmented-control.ios.js +15 -2
  17. package/lib/module/atoms/segmented-control/segmented-control.ios.js.map +1 -1
  18. package/lib/module/host-context.js +8 -0
  19. package/lib/module/host-context.js.map +1 -1
  20. package/lib/typescript/atoms/activity-indicator/activity-indicator.ios.d.ts +4 -1
  21. package/lib/typescript/atoms/activity-indicator/activity-indicator.types.d.ts +2 -1
  22. package/lib/typescript/atoms/date-picker/date-picker.ios.d.ts +2 -1
  23. package/lib/typescript/atoms/date-picker/date-picker.types.d.ts +1 -1
  24. package/lib/typescript/atoms/picker/picker.ios.d.ts +5 -0
  25. package/lib/typescript/atoms/picker/picker.types.d.ts +6 -2
  26. package/lib/typescript/atoms/segmented-control/segmented-control.ios.d.ts +2 -1
  27. package/lib/typescript/atoms/segmented-control/segmented-control.types.d.ts +2 -1
  28. package/lib/typescript/host-context.d.ts +7 -0
  29. package/package.json +4 -4
  30. package/src/atoms/activity-indicator/activity-indicator.android.tsx +18 -5
  31. package/src/atoms/activity-indicator/activity-indicator.ios.tsx +10 -2
  32. package/src/atoms/activity-indicator/activity-indicator.types.ts +2 -1
  33. package/src/atoms/date-picker/date-picker.android.tsx +15 -1
  34. package/src/atoms/date-picker/date-picker.ios.tsx +20 -9
  35. package/src/atoms/date-picker/date-picker.types.ts +1 -1
  36. package/src/atoms/picker/picker.android.tsx +53 -44
  37. package/src/atoms/picker/picker.ios.tsx +28 -7
  38. package/src/atoms/picker/picker.types.ts +6 -2
  39. package/src/atoms/segmented-control/segmented-control.android.tsx +29 -18
  40. package/src/atoms/segmented-control/segmented-control.ios.tsx +10 -2
  41. package/src/atoms/segmented-control/segmented-control.types.ts +2 -1
  42. package/src/host-context.tsx +8 -0
@@ -1,13 +1,21 @@
1
1
  "use strict";
2
2
 
3
+ import { RNHostView } from '@expo/ui/swift-ui';
3
4
  import { SegmentedControl as NativeSegmentedControl } from '@hzblj/kara-native/swift-ui';
4
5
  import { useAppTheme } from '@hzblj/kara-theme';
5
6
  import { useCallback, useMemo } from 'react';
7
+ import { useInsideHost } from "../../host-context.js";
6
8
  import { jsx as _jsx } from "react/jsx-runtime";
9
+ const MATCH_HEIGHT_ONLY = {
10
+ horizontal: false,
11
+ vertical: true
12
+ };
13
+
7
14
  /**
8
15
  * kara's own `UISegmentedControl`, with the pill in the accent and white text on it — the system control tints only the
9
16
  * pill, and SwiftUI's segmented picker not even that. A plain `UIView` in React Native layout: it needs no host, takes
10
- * the width it is given and UIKit's height. UIKit plays the control's own feedback.
17
+ * the width it is given and UIKit's height. Inside a `Screen` or `Island` it is carried into that host by `RNHostView`
18
+ * (see `useInsideHost`), the host's width and its own height. UIKit plays the control's own feedback.
11
19
  */
12
20
  export const SegmentedControl = ({
13
21
  disabled = false,
@@ -17,6 +25,7 @@ export const SegmentedControl = ({
17
25
  values
18
26
  }) => {
19
27
  const theme = useAppTheme();
28
+ const insideHost = useInsideHost();
20
29
  const face = theme.typography.footnoteMedium;
21
30
 
22
31
  // The native control speaks in values; a segment's is its index, so two labels may read the same.
@@ -28,7 +37,7 @@ export const SegmentedControl = ({
28
37
  const index = Number(value);
29
38
  onChange?.(index, values[index] ?? '');
30
39
  }, [onChange, values]);
31
- return /*#__PURE__*/_jsx(NativeSegmentedControl, {
40
+ const control = /*#__PURE__*/_jsx(NativeSegmentedControl, {
32
41
  activePillColor: theme.color('fill.accent.primary'),
33
42
  activeTextColor: theme.color('content.on.accent'),
34
43
  disabled: disabled,
@@ -40,5 +49,9 @@ export const SegmentedControl = ({
40
49
  selectedValue: String(selectedIndex),
41
50
  testID: testID
42
51
  });
52
+ return insideHost ? /*#__PURE__*/_jsx(RNHostView, {
53
+ matchContents: MATCH_HEIGHT_ONLY,
54
+ children: control
55
+ }) : control;
43
56
  };
44
57
  //# sourceMappingURL=segmented-control.ios.js.map
@@ -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;AAI1C;AACA;AACA;AACA;AACA;AACA,OAAO,MAAMN,gBAA2C,GAAGA,CAAC;EAC1DO,QAAQ,GAAG,KAAK;EAChBC,QAAQ;EACRC,aAAa;EACbC,MAAM;EACNC;AACqB,CAAC,KAAK;EAC3B,MAAMC,KAAK,GAAGV,WAAW,CAAC,CAAC;EAC3B,MAAMW,IAAI,GAAGD,KAAK,CAACE,UAAU,CAACC,cAAc;;EAE5C;EACA,MAAMC,OAAO,GAAGZ,OAAO,CAAC,MAAMO,MAAM,CAACM,GAAG,CAAC,CAACC,KAAK,EAAEC,KAAK,MAAM;IAACD,KAAK;IAAEE,KAAK,EAAEC,MAAM,CAACF,KAAK;EAAC,CAAC,CAAC,CAAC,EAAE,CAACR,MAAM,CAAC,CAAC;EAEtG,MAAMW,aAAa,GAAGnB,WAAW,CAC9BiB,KAAa,IAAK;IACjB,MAAMD,KAAK,GAAGI,MAAM,CAACH,KAAK,CAAC;IAC3BZ,QAAQ,GAAGW,KAAK,EAAER,MAAM,CAACQ,KAAK,CAAC,IAAI,EAAE,CAAC;EACxC,CAAC,EACD,CAACX,QAAQ,EAAEG,MAAM,CACnB,CAAC;EAED,oBACEL,IAAA,CAACL,sBAAsB;IACrBuB,eAAe,EAAEZ,KAAK,CAACa,KAAK,CAAC,qBAAqB,CAAE;IACpDC,eAAe,EAAEd,KAAK,CAACa,KAAK,CAAC,mBAAmB,CAAE;IAClDlB,QAAQ,EAAEA,QAAS;IACnBoB,UAAU,EAAEd,IAAI,CAACc,UAAW;IAC5BC,QAAQ,EAAEf,IAAI,CAACe,QAAS;IACxBC,iBAAiB,EAAEjB,KAAK,CAACa,KAAK,CAAC,iBAAiB,CAAE;IAClDH,aAAa,EAAEA,aAAc;IAC7BN,OAAO,EAAEA,OAAQ;IACjBc,aAAa,EAAET,MAAM,CAACZ,aAAa,CAAE;IACrCC,MAAM,EAAEA;EAAO,CAChB,CAAC;AAEN,CAAC","ignoreList":[]}
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;AAED,OAAO,MAAMG,YAAY,GAAIC,IAAY,IAAK;EAC5C,MAAMC,UAAU,GAAGV,UAAU,CAACG,WAAW,CAAC;EAE1C,IAAIQ,OAAO,IAAI,CAACD,UAAU,EAAE;IAC1BE,OAAO,CAACC,IAAI,CACV,cAAcJ,IAAI,mGACpB,CAAC;EACH;AACF,CAAC","ignoreList":[]}
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
- /** React Native's spinner, which on iOS is UIKit's own `UIActivityIndicatorView`. */
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. For how far along something is, use `Progress`.
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
- /** Android: the dropdown field's test tag. iOS: `@expo/ui`'s wheel picker drops it. */
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. UIKit plays the control's own feedback.
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. It takes the width it is given; the height is the platform's.
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.0",
3
+ "version": "0.14.2",
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.0",
65
- "@hzblj/kara-sx": "^0.14.0",
66
- "@hzblj/kara-theme": "^0.14.0",
64
+ "@hzblj/kara-native": "^0.14.2",
65
+ "@hzblj/kara-sx": "^0.14.2",
66
+ "@hzblj/kara-theme": "^0.14.2",
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
- () => [fillMaxSize(), ...(testID ? [testIdModifier(testID)] : []), ...accessibilityModifiers],
30
- [accessibilityModifiers, testID]
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
- <LoadingIndicator color={theme.color(color)} modifiers={modifiers} />
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
- /** React Native's spinner, which on iOS is UIKit's own `UIActivityIndicatorView`. */
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
- return (
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. For how far along something is, use `Progress`.
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
- return (
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
- <SwiftUIDatePicker
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
- return (
41
- <Host colorScheme={theme.dark ? 'dark' : 'light'} matchContents={MATCH_HEIGHT_ONLY} seedColor={theme.materialSeed}>
42
- <HostBoundary>
43
- <ExposedDropdownMenuBox expanded={expanded} onExpandedChange={disabled ? undefined : setExpanded}>
44
- <OutlinedTextField
45
- enabled={!disabled}
46
- modifiers={fieldModifiers}
47
- readOnly
48
- singleLine
49
- textStyle={{
50
- color: theme.colorString('content.primary'),
51
- fontFamily: theme.typography.body.fontFamily,
52
- fontWeight: theme.typography.body.fontWeight,
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
- <OutlinedTextField.TrailingIcon>
57
- <Glyph color="content.secondary" name="chevronDown" size="s20" />
58
- </OutlinedTextField.TrailingIcon>
59
- </OutlinedTextField>
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
- <DropdownMenuItem.Text>
74
- <ComposeText
75
- color={theme.colorString(option.value === value ? 'content.accent' : 'content.primary')}
76
- style={theme.typography.body}
77
- >
78
- {option.label}
79
- </ComposeText>
80
- </DropdownMenuItem.Text>
81
- </DropdownMenuItem>
82
- ))}
83
- </ExposedDropdownMenu>
84
- </ExposedDropdownMenuBox>
85
- </HostBoundary>
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
- /** Android: the dropdown field's test tag. iOS: `@expo/ui`'s wheel picker drops it. */
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