@colixsystems/widget-sdk 0.207.0 → 0.212.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 +97 -2
- package/dist/checkbox-view.js +150 -0
- package/dist/checkbox.js +11 -0
- package/dist/checkbox.native.js +7 -0
- package/dist/contract.cjs +78 -4
- package/dist/contract.js +78 -4
- package/dist/form-control-logic.js +55 -0
- package/dist/form-control-tokens.js +121 -0
- package/dist/form-control-view.js +98 -0
- package/dist/hooks.js +13 -0
- package/dist/host.d.ts +46 -0
- package/dist/host.js +4 -0
- package/dist/index.d.ts +230 -6
- package/dist/index.js +11 -1
- package/dist/index.native.js +11 -1
- package/dist/markdown-input-view.js +2 -3
- package/dist/markdown-input.js +4 -2
- package/dist/markdown-input.native.js +4 -2
- package/dist/markdown-link-dialog.js +94 -108
- package/dist/modal-tokens.js +107 -0
- package/dist/modal-view.js +255 -0
- package/dist/modal.js +8 -0
- package/dist/modal.native.js +45 -0
- package/dist/overlay-tokens.js +69 -5
- package/dist/overlay-view.js +52 -16
- package/dist/overlay.native.js +8 -1
- package/dist/search-field-view.js +155 -0
- package/dist/search-field.js +13 -0
- package/dist/search-field.native.js +9 -0
- package/dist/select-view.js +312 -0
- package/dist/select.js +15 -0
- package/dist/select.native.js +9 -0
- package/dist/surface-theme.js +108 -0
- package/dist/switch-view.js +149 -0
- package/dist/switch.js +80 -0
- package/dist/switch.native.js +7 -0
- package/dist/text-field-view.js +54 -8
- package/dist/text-field.js +6 -1
- package/dist/text-field.native.js +3 -3
- package/dist/web-modal.js +24 -5
- package/package.json +1 -1
- package/src/contract.js +78 -4
package/dist/index.d.ts
CHANGED
|
@@ -3167,7 +3167,6 @@ export const TextInput: any;
|
|
|
3167
3167
|
export const FlatList: any;
|
|
3168
3168
|
export const SectionList: any;
|
|
3169
3169
|
export const ActivityIndicator: any;
|
|
3170
|
-
export const Switch: any;
|
|
3171
3170
|
export const StyleSheet: any;
|
|
3172
3171
|
|
|
3173
3172
|
/**
|
|
@@ -3257,13 +3256,107 @@ export const Overlay: (props: {
|
|
|
3257
3256
|
onRequestClose?: () => void;
|
|
3258
3257
|
/** Panel width tier. `full` runs edge to edge. Defaults to `md`. */
|
|
3259
3258
|
size?: "sm" | "md" | "lg" | "full";
|
|
3259
|
+
/** Centred (`dialog`, the default) or anchored to the bottom edge (`sheet`). */
|
|
3260
|
+
presentation?: OverlayPresentation;
|
|
3260
3261
|
/** Set false when only an explicit control may close the overlay. */
|
|
3261
3262
|
dismissOnBackdropPress?: boolean;
|
|
3263
|
+
/**
|
|
3264
|
+
* Set false when the content has its own close control: the backdrop stays
|
|
3265
|
+
* pressable but leaves the tab order and the accessibility tree.
|
|
3266
|
+
*/
|
|
3267
|
+
backdropAccessible?: boolean;
|
|
3268
|
+
/** The web dialog node's role. Defaults to "dialog". No native equivalent. */
|
|
3269
|
+
role?: "dialog" | "alertdialog";
|
|
3270
|
+
/**
|
|
3271
|
+
* Names the dialog: on the web the node that carries the role, in the
|
|
3272
|
+
* exported app the panel (React Native's Modal carries no name).
|
|
3273
|
+
*/
|
|
3262
3274
|
accessibilityLabel?: string;
|
|
3275
|
+
/** The id of the element that names the dialog; wins over the label. Web only. */
|
|
3276
|
+
accessibilityLabelledBy?: string;
|
|
3263
3277
|
/** Label for the backdrop's dismiss target. Defaults to "Close". */
|
|
3264
3278
|
closeAccessibilityLabel?: string;
|
|
3279
|
+
/** Focused on open instead of the dialog itself. Web only; a no-op on native. */
|
|
3280
|
+
initialFocusRef?: { current: any };
|
|
3265
3281
|
/** Extra styles merged onto the themed panel. */
|
|
3266
3282
|
style?: any;
|
|
3283
|
+
/** Set on the panel. */
|
|
3284
|
+
testID?: string;
|
|
3285
|
+
children?: ReactNode;
|
|
3286
|
+
}) => any;
|
|
3287
|
+
|
|
3288
|
+
export type OverlayPresentation = "dialog" | "sheet";
|
|
3289
|
+
|
|
3290
|
+
/**
|
|
3291
|
+
* The standard dialog, built on `<Overlay>`: a title, a close button, a body
|
|
3292
|
+
* that scrolls when it is long, and a footer action row laid out below it.
|
|
3293
|
+
* Its background, corner radius, padding, scrim, shadow and title colour come
|
|
3294
|
+
* from the theme's `modal` scope (Design > Components > Dialogs), so every dialog in
|
|
3295
|
+
* the app matches. Use it instead of hand-building a header and an action row
|
|
3296
|
+
* inside a bare `<Overlay>`.
|
|
3297
|
+
*
|
|
3298
|
+
* A dialog that paints its own background hands what is rendered inside it a
|
|
3299
|
+
* theme re-inked for that background, so SDK components and any component
|
|
3300
|
+
* calling `useTheme()` inside the Modal stay readable.
|
|
3301
|
+
*
|
|
3302
|
+
* In the exported app the dialog pads a `sheet` by the bottom safe-area inset
|
|
3303
|
+
* and a `full` dialog by the top and bottom ones, and wraps its panel in React
|
|
3304
|
+
* Native's `KeyboardAvoidingView`. This is not yet verified on a device, and
|
|
3305
|
+
* `KeyboardAvoidingView` inside a React Native `Modal` window is a known risk
|
|
3306
|
+
* on Android: until it is verified, keep a form with text inputs in the
|
|
3307
|
+
* default `dialog` presentation. A bare `<Overlay>` gets neither.
|
|
3308
|
+
*
|
|
3309
|
+
* @example
|
|
3310
|
+
* <Modal
|
|
3311
|
+
* visible={open}
|
|
3312
|
+
* onRequestClose={() => setOpen(false)}
|
|
3313
|
+
* title="Edit order"
|
|
3314
|
+
* footer={
|
|
3315
|
+
* <>
|
|
3316
|
+
* <Button variant="secondary" title="Cancel" onPress={() => setOpen(false)} />
|
|
3317
|
+
* <Button title="Save" onPress={save} />
|
|
3318
|
+
* </>
|
|
3319
|
+
* }
|
|
3320
|
+
* >
|
|
3321
|
+
* <TextField value={note} onChangeText={setNote} placeholder="Note" />
|
|
3322
|
+
* </Modal>
|
|
3323
|
+
*/
|
|
3324
|
+
export const Modal: (props: {
|
|
3325
|
+
/** Nothing renders (and nothing is mounted) while this is false. */
|
|
3326
|
+
visible?: boolean;
|
|
3327
|
+
/**
|
|
3328
|
+
* The close button, backdrop press, Escape (web) and Android back fire this.
|
|
3329
|
+
* Omitted, no close button is drawn and only your own controls close it.
|
|
3330
|
+
*/
|
|
3331
|
+
onRequestClose?: () => void;
|
|
3332
|
+
/**
|
|
3333
|
+
* Defaults to true. False while the dialog must stay open (a save in
|
|
3334
|
+
* flight): no close button, and the backdrop, Escape and Android back do
|
|
3335
|
+
* not fire `onRequestClose`. Only while that work runs, and always restore
|
|
3336
|
+
* it: with no footer button or in-body control, nobody can close the dialog.
|
|
3337
|
+
*/
|
|
3338
|
+
dismissible?: boolean;
|
|
3339
|
+
/** With no title and no close button the dialog has no header row. */
|
|
3340
|
+
title?: string;
|
|
3341
|
+
/** The action buttons: one right-aligned row that wraps. Omit for no footer. */
|
|
3342
|
+
footer?: ReactNode;
|
|
3343
|
+
/** Centred (`dialog`, the default) or anchored to the bottom edge (`sheet`). */
|
|
3344
|
+
presentation?: OverlayPresentation;
|
|
3345
|
+
/** Panel width tier. `full` runs edge to edge. Defaults to `md`. */
|
|
3346
|
+
size?: "sm" | "md" | "lg" | "full";
|
|
3347
|
+
/** Set false when only the close button or a footer action may close it. */
|
|
3348
|
+
dismissOnBackdropPress?: boolean;
|
|
3349
|
+
/** Defaults to "dialog". Web only; native has no equivalent. */
|
|
3350
|
+
role?: "dialog" | "alertdialog";
|
|
3351
|
+
/** Focused on open instead of the dialog itself. Web only; a no-op on native. */
|
|
3352
|
+
initialFocusRef?: { current: any };
|
|
3353
|
+
/** Label for the close button. Defaults to "Close". */
|
|
3354
|
+
closeAccessibilityLabel?: string;
|
|
3355
|
+
/** Names a dialog that has no title; the title names it otherwise. */
|
|
3356
|
+
accessibilityLabel?: string;
|
|
3357
|
+
/** On the panel; the parts take `-close`, `-body` and `-footer`. */
|
|
3358
|
+
testID?: string;
|
|
3359
|
+
/** The body. It scrolls on its own: do not wrap it in a ScrollView. */
|
|
3267
3360
|
children?: ReactNode;
|
|
3268
3361
|
}) => any;
|
|
3269
3362
|
|
|
@@ -3526,19 +3619,150 @@ export const Button: (props: {
|
|
|
3526
3619
|
}) => any;
|
|
3527
3620
|
|
|
3528
3621
|
/**
|
|
3529
|
-
*
|
|
3530
|
-
*
|
|
3531
|
-
*
|
|
3532
|
-
* <TextField value={name} onChangeText={setName} placeholder="Your name" accessibilityLabel="Name" />
|
|
3622
|
+
* What every form control shares: the label drawn with it (and read by a
|
|
3623
|
+
* screen reader), a hint below it, and an error that replaces the hint and
|
|
3624
|
+
* marks the control. `error: true` marks it without a message.
|
|
3533
3625
|
*/
|
|
3534
|
-
export
|
|
3626
|
+
export interface FormControlProps {
|
|
3627
|
+
label?: ReactNode;
|
|
3628
|
+
helper?: ReactNode;
|
|
3629
|
+
error?: ReactNode | boolean;
|
|
3630
|
+
disabled?: boolean;
|
|
3631
|
+
/** Required when there is no text `label`. */
|
|
3632
|
+
accessibilityLabel?: string;
|
|
3633
|
+
testID?: string;
|
|
3634
|
+
}
|
|
3635
|
+
|
|
3636
|
+
/** Per-control overrides of the theme's `muted` scope. */
|
|
3637
|
+
export interface FormFieldOverrides {
|
|
3535
3638
|
background?: string;
|
|
3536
3639
|
textColor?: string;
|
|
3537
3640
|
borderColor?: string;
|
|
3538
3641
|
radius?: number;
|
|
3539
3642
|
height?: number;
|
|
3643
|
+
}
|
|
3644
|
+
|
|
3645
|
+
/**
|
|
3646
|
+
* Themed text-entry field (Design > Components > Muted). Every TextInput prop
|
|
3647
|
+
* works.
|
|
3648
|
+
*
|
|
3649
|
+
* @example
|
|
3650
|
+
* <TextField label="Name" value={name} onChangeText={setName} placeholder="Your name" />
|
|
3651
|
+
*/
|
|
3652
|
+
export const TextField: (props: FormControlProps & FormFieldOverrides & {
|
|
3540
3653
|
multiline?: boolean;
|
|
3541
3654
|
style?: any;
|
|
3655
|
+
/** The box around a labelled field. */
|
|
3656
|
+
containerStyle?: any;
|
|
3657
|
+
[prop: string]: any;
|
|
3658
|
+
}) => any;
|
|
3659
|
+
|
|
3660
|
+
/**
|
|
3661
|
+
* Themed tick box: the input surface, filled with the primary colour when
|
|
3662
|
+
* checked. One 44px target with the checkbox role and state on both hosts.
|
|
3663
|
+
*
|
|
3664
|
+
* @example
|
|
3665
|
+
* <Checkbox label="Remember me" value={remember} onValueChange={setRemember} />
|
|
3666
|
+
*/
|
|
3667
|
+
export const Checkbox: (props: FormControlProps & {
|
|
3668
|
+
value?: boolean;
|
|
3669
|
+
onValueChange?: (next: boolean) => void;
|
|
3670
|
+
/** The row. */
|
|
3671
|
+
style?: any;
|
|
3672
|
+
/** The box around a row that shows a helper or an error. */
|
|
3673
|
+
containerStyle?: any;
|
|
3674
|
+
[prop: string]: any;
|
|
3675
|
+
}) => any;
|
|
3676
|
+
|
|
3677
|
+
/**
|
|
3678
|
+
* Themed on/off toggle: the platform's own switch with the theme's track
|
|
3679
|
+
* colours. Every React Native `Switch` prop works: a `trackColor` overrides
|
|
3680
|
+
* the theme for the states it names, and the thumb is the platform's unless
|
|
3681
|
+
* `thumbColor` is passed. Without a label, helper or error it renders the
|
|
3682
|
+
* bare switch.
|
|
3683
|
+
*
|
|
3684
|
+
* @example
|
|
3685
|
+
* <Switch label="Email me updates" value={on} onValueChange={setOn} />
|
|
3686
|
+
*/
|
|
3687
|
+
export const Switch: (props: FormControlProps & {
|
|
3688
|
+
value?: boolean;
|
|
3689
|
+
onValueChange?: (next: boolean) => void;
|
|
3690
|
+
trackColor?: { false?: string; true?: string };
|
|
3691
|
+
thumbColor?: string;
|
|
3692
|
+
style?: any;
|
|
3693
|
+
/** The box around a labelled switch. */
|
|
3694
|
+
containerStyle?: any;
|
|
3695
|
+
[prop: string]: any;
|
|
3696
|
+
}) => any;
|
|
3697
|
+
|
|
3698
|
+
export interface SelectOption<V extends string | number = string | number> {
|
|
3699
|
+
value: V;
|
|
3700
|
+
label: string;
|
|
3701
|
+
disabled?: boolean;
|
|
3702
|
+
testID?: string;
|
|
3703
|
+
}
|
|
3704
|
+
|
|
3705
|
+
/**
|
|
3706
|
+
* Themed single-choice picker. The trigger is the input field, a button that
|
|
3707
|
+
* opens a dialog; its options open at screen level (through `Overlay`) in a
|
|
3708
|
+
* sheet painted from the `popover` scope, so no container clips them and a
|
|
3709
|
+
* long list scrolls. Each option has the option role; the list is a listbox
|
|
3710
|
+
* on the web only. Other props land on the trigger.
|
|
3711
|
+
*
|
|
3712
|
+
* @example
|
|
3713
|
+
* <Select label="Status" value={status} onValueChange={setStatus}
|
|
3714
|
+
* options={[{ value: "open", label: "Open" }, { value: "done", label: "Done" }]} />
|
|
3715
|
+
*/
|
|
3716
|
+
export const Select: <V extends string | number = string | number>(
|
|
3717
|
+
props: FormControlProps & FormFieldOverrides & {
|
|
3718
|
+
options: Array<SelectOption<V>>;
|
|
3719
|
+
value?: V | null;
|
|
3720
|
+
onValueChange?: (value: V, option: SelectOption<V>) => void;
|
|
3721
|
+
/** Shown while nothing is chosen. Default "Select". */
|
|
3722
|
+
placeholder?: string;
|
|
3723
|
+
/** The sheet's heading. Defaults to a text `label`. */
|
|
3724
|
+
title?: string;
|
|
3725
|
+
/** Shown in the sheet when `options` is empty. Default "No options". */
|
|
3726
|
+
emptyText?: string;
|
|
3727
|
+
closeAccessibilityLabel?: string;
|
|
3728
|
+
/** The trigger. */
|
|
3729
|
+
style?: any;
|
|
3730
|
+
containerStyle?: any;
|
|
3731
|
+
[prop: string]: any;
|
|
3732
|
+
},
|
|
3733
|
+
) => any;
|
|
3734
|
+
|
|
3735
|
+
/**
|
|
3736
|
+
* Space-key activation for a control whose role is not a button: on the web a
|
|
3737
|
+
* `Pressable` answers Space only for a button. Native ignores `onKeyDown`.
|
|
3738
|
+
*
|
|
3739
|
+
* @example
|
|
3740
|
+
* <Pressable accessibilityRole="radio" onPress={pick} onKeyDown={spaceActivates(pick)} />
|
|
3741
|
+
*/
|
|
3742
|
+
export function spaceActivates(
|
|
3743
|
+
onPress: () => void,
|
|
3744
|
+
): (event: { key: string; repeat?: boolean; preventDefault: () => void }) => void;
|
|
3745
|
+
|
|
3746
|
+
/**
|
|
3747
|
+
* Themed search box: a `TextField` with a search glyph, the search key and a
|
|
3748
|
+
* clear control. Every `TextField` prop works.
|
|
3749
|
+
*
|
|
3750
|
+
* @example
|
|
3751
|
+
* <SearchField value={query} onChangeText={setQuery} placeholder="Search orders" />
|
|
3752
|
+
*/
|
|
3753
|
+
export const SearchField: (props: FormControlProps & {
|
|
3754
|
+
value?: string;
|
|
3755
|
+
onChangeText?: (text: string) => void;
|
|
3756
|
+
/** Called after the clear control empties the field. */
|
|
3757
|
+
onClear?: () => void;
|
|
3758
|
+
/** Default "Search". */
|
|
3759
|
+
placeholder?: string;
|
|
3760
|
+
/** Default "Clear search". */
|
|
3761
|
+
clearAccessibilityLabel?: string;
|
|
3762
|
+
/** The input. */
|
|
3763
|
+
style?: any;
|
|
3764
|
+
/** The box around it; use it for `flex: 1` in a toolbar. */
|
|
3765
|
+
containerStyle?: any;
|
|
3542
3766
|
[prop: string]: any;
|
|
3543
3767
|
}) => any;
|
|
3544
3768
|
|
package/dist/index.js
CHANGED
|
@@ -167,7 +167,6 @@ export {
|
|
|
167
167
|
FlatList,
|
|
168
168
|
SectionList,
|
|
169
169
|
ActivityIndicator,
|
|
170
|
-
Switch,
|
|
171
170
|
StyleSheet,
|
|
172
171
|
Linking,
|
|
173
172
|
Icon,
|
|
@@ -266,12 +265,23 @@ export {
|
|
|
266
265
|
export { TextField } from "./text-field.js";
|
|
267
266
|
// Themed collapsible sections, drawn from the `card` and `list` scopes.
|
|
268
267
|
export { Accordion, AccordionItem } from "./accordion.js";
|
|
268
|
+
// The themed form controls; they follow the theme's `muted` scope as
|
|
269
|
+
// `<TextField>` does, and `popover` for the Select's options. `<Switch>` is
|
|
270
|
+
// the platform's own.
|
|
271
|
+
export { Checkbox } from "./checkbox.js";
|
|
272
|
+
export { Switch } from "./switch.js";
|
|
273
|
+
export { Select } from "./select.js";
|
|
274
|
+
export { SearchField } from "./search-field.js";
|
|
275
|
+
// Space for a control whose role is not a button; a no-op on native.
|
|
276
|
+
export { spaceActivates } from "./form-control-logic.js";
|
|
269
277
|
// A themed tab strip; follows the theme's `tabs` scope.
|
|
270
278
|
export { Tabs } from "./tabs.js";
|
|
271
279
|
// Themed loading states; follow the theme's `loading` scope.
|
|
272
280
|
export { Spinner, Skeleton } from "./loading.js";
|
|
273
281
|
// A themed inline message; follows the theme's `alert` scope.
|
|
274
282
|
export { Banner } from "./banner.js";
|
|
283
|
+
// The standard dialog, on `<Overlay>`; follows the theme's `modal` scope.
|
|
284
|
+
export { Modal } from "./modal.js";
|
|
275
285
|
// The semantic text tiers, shared with the built-in widgets.
|
|
276
286
|
export {
|
|
277
287
|
textRoleStyle,
|
package/dist/index.native.js
CHANGED
|
@@ -160,7 +160,6 @@ export {
|
|
|
160
160
|
FlatList,
|
|
161
161
|
SectionList,
|
|
162
162
|
ActivityIndicator,
|
|
163
|
-
Switch,
|
|
164
163
|
StyleSheet,
|
|
165
164
|
Linking,
|
|
166
165
|
Icon,
|
|
@@ -259,12 +258,23 @@ export {
|
|
|
259
258
|
export { TextField } from "./text-field.native.js";
|
|
260
259
|
// Themed collapsible sections, drawn from the `card` and `list` scopes.
|
|
261
260
|
export { Accordion, AccordionItem } from "./accordion.native.js";
|
|
261
|
+
// The themed form controls; they follow the theme's `muted` scope as
|
|
262
|
+
// `<TextField>` does, and `popover` for the Select's options. `<Switch>` is
|
|
263
|
+
// the platform's own.
|
|
264
|
+
export { Checkbox } from "./checkbox.native.js";
|
|
265
|
+
export { Switch } from "./switch.native.js";
|
|
266
|
+
export { Select } from "./select.native.js";
|
|
267
|
+
export { SearchField } from "./search-field.native.js";
|
|
268
|
+
// Space for a control whose role is not a button; a no-op on native.
|
|
269
|
+
export { spaceActivates } from "./form-control-logic.js";
|
|
262
270
|
// A themed tab strip; follows the theme's `tabs` scope.
|
|
263
271
|
export { Tabs } from "./tabs.native.js";
|
|
264
272
|
// Themed loading states; follow the theme's `loading` scope.
|
|
265
273
|
export { Spinner, Skeleton } from "./loading.native.js";
|
|
266
274
|
// A themed inline message; follows the theme's `alert` scope.
|
|
267
275
|
export { Banner } from "./banner.native.js";
|
|
276
|
+
// The standard dialog, on `<Overlay>`; follows the theme's `modal` scope.
|
|
277
|
+
export { Modal } from "./modal.native.js";
|
|
268
278
|
// The semantic text tiers, shared with the built-in widgets.
|
|
269
279
|
export {
|
|
270
280
|
textRoleStyle,
|
|
@@ -34,9 +34,9 @@ const TOOLS = Object.freeze([
|
|
|
34
34
|
},
|
|
35
35
|
]);
|
|
36
36
|
|
|
37
|
-
export function makeMarkdownInput(rn, RichText,
|
|
37
|
+
export function makeMarkdownInput(rn, RichText, Modal, Button) {
|
|
38
38
|
const { Text, View, Pressable, TextInput } = rn;
|
|
39
|
-
const MarkdownLinkDialog = makeMarkdownLinkDialog(rn,
|
|
39
|
+
const MarkdownLinkDialog = makeMarkdownLinkDialog(rn, Modal, Button);
|
|
40
40
|
|
|
41
41
|
function MarkdownInput({
|
|
42
42
|
value,
|
|
@@ -210,7 +210,6 @@ export function makeMarkdownInput(rn, RichText, Overlay) {
|
|
|
210
210
|
visible: linkOpen,
|
|
211
211
|
onClose: () => setLinkOpen(false),
|
|
212
212
|
onSubmit: insertLink,
|
|
213
|
-
tokens,
|
|
214
213
|
pages,
|
|
215
214
|
initialLabel: selected,
|
|
216
215
|
testID,
|
package/dist/markdown-input.js
CHANGED
|
@@ -5,11 +5,13 @@
|
|
|
5
5
|
import * as ReactNative from "react-native-web";
|
|
6
6
|
import { makeMarkdownInput } from "./markdown-input-view.js";
|
|
7
7
|
import { RichText } from "./richtext.js";
|
|
8
|
-
import {
|
|
8
|
+
import { Modal } from "./modal.js";
|
|
9
|
+
import { Button } from "./button.js";
|
|
9
10
|
import { TextInput } from "./primitives.js";
|
|
10
11
|
|
|
11
12
|
export const MarkdownInput = makeMarkdownInput(
|
|
12
13
|
{ ...ReactNative, TextInput },
|
|
13
14
|
RichText,
|
|
14
|
-
|
|
15
|
+
Modal,
|
|
16
|
+
Button,
|
|
15
17
|
);
|
|
@@ -5,12 +5,14 @@
|
|
|
5
5
|
import { Text, View, Pressable, TextInput, ScrollView } from "react-native";
|
|
6
6
|
import { makeMarkdownInput } from "./markdown-input-view.js";
|
|
7
7
|
import { RichText } from "./richtext.native.js";
|
|
8
|
-
import {
|
|
8
|
+
import { Modal } from "./modal.native.js";
|
|
9
|
+
import { Button } from "./button.native.js";
|
|
9
10
|
|
|
10
11
|
// sc-7349: ScrollView joins the set for the link form's page list, which must
|
|
11
12
|
// scroll rather than grow an app's every page past the panel.
|
|
12
13
|
export const MarkdownInput = makeMarkdownInput(
|
|
13
14
|
{ Text, View, Pressable, TextInput, ScrollView },
|
|
14
15
|
RichText,
|
|
15
|
-
|
|
16
|
+
Modal,
|
|
17
|
+
Button,
|
|
16
18
|
);
|
|
@@ -2,20 +2,22 @@
|
|
|
2
2
|
//
|
|
3
3
|
// A link needs a TARGET, and no selection can supply one: the author has to
|
|
4
4
|
// name either a page in this app or an external address. That is a form, and a
|
|
5
|
-
// form belongs in
|
|
6
|
-
// own clipping box on both hosts — not in a bespoke absolutely-
|
|
7
|
-
// the layout container would crop.
|
|
5
|
+
// form belongs in a `<Modal>` — the SDK's standard dialog, which escapes the
|
|
6
|
+
// widget's own clipping box on both hosts — not in a bespoke absolutely-
|
|
7
|
+
// positioned panel the layout container would crop.
|
|
8
8
|
//
|
|
9
9
|
// Split out of markdown-input-view.js so each file stays one component's worth
|
|
10
10
|
// of markup. Like every view module here it is bound per host by the caller
|
|
11
11
|
// (see markdown-input.js / .native.js), so the two hosts cannot drift.
|
|
12
12
|
|
|
13
13
|
import React from "react";
|
|
14
|
+
import { useHostTheme } from "./hooks.js";
|
|
15
|
+
import { resolveRichTextTokens } from "./richtext-tokens.js";
|
|
14
16
|
|
|
15
17
|
// The page list scrolls rather than growing an app's 40 pages past the panel.
|
|
16
18
|
const PAGE_LIST_MAX_HEIGHT = 180;
|
|
17
19
|
|
|
18
|
-
export function makeMarkdownLinkDialog(rn,
|
|
20
|
+
export function makeMarkdownLinkDialog(rn, Modal, Button) {
|
|
19
21
|
const { Text, View, Pressable, TextInput, ScrollView } = rn;
|
|
20
22
|
|
|
21
23
|
function Field({ label, tokens, children }) {
|
|
@@ -38,50 +40,12 @@ export function makeMarkdownLinkDialog(rn, Overlay) {
|
|
|
38
40
|
);
|
|
39
41
|
}
|
|
40
42
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
* @param {Array<{id: string, name: string}>} props.pages — pickable pages;
|
|
48
|
-
* empty means this host offered none, so only the URL mode is shown.
|
|
49
|
-
* @param {string} props.initialLabel — the author's current selection.
|
|
50
|
-
* @param {string} [props.testID]
|
|
51
|
-
*/
|
|
52
|
-
function MarkdownLinkDialog({
|
|
53
|
-
visible,
|
|
54
|
-
onClose,
|
|
55
|
-
onSubmit,
|
|
56
|
-
tokens,
|
|
57
|
-
pages,
|
|
58
|
-
initialLabel,
|
|
59
|
-
testID,
|
|
60
|
-
}) {
|
|
61
|
-
const pageList = Array.isArray(pages) ? pages.filter((p) => p && p.id) : [];
|
|
62
|
-
const canPickPage = pageList.length > 0;
|
|
63
|
-
|
|
64
|
-
const [label, setLabel] = React.useState("");
|
|
65
|
-
const [url, setUrl] = React.useState("");
|
|
66
|
-
const [pageId, setPageId] = React.useState("");
|
|
67
|
-
const [mode, setMode] = React.useState("url");
|
|
68
|
-
|
|
69
|
-
// Re-seed on each open: the author's selection and the mode they should
|
|
70
|
-
// land in are both properties of THIS opening, not of the component.
|
|
71
|
-
React.useEffect(() => {
|
|
72
|
-
if (!visible) return;
|
|
73
|
-
setLabel(typeof initialLabel === "string" ? initialLabel : "");
|
|
74
|
-
setUrl("");
|
|
75
|
-
setPageId("");
|
|
76
|
-
setMode(canPickPage ? "page" : "url");
|
|
77
|
-
}, [visible, initialLabel, canPickPage]);
|
|
78
|
-
|
|
79
|
-
const target = mode === "page" ? pageId : url.trim();
|
|
80
|
-
|
|
81
|
-
const submit = () => {
|
|
82
|
-
if (!target) return;
|
|
83
|
-
if (typeof onSubmit === "function") onSubmit({ label, target });
|
|
84
|
-
};
|
|
43
|
+
// The form is its own component so its tokens resolve INSIDE the dialog: a
|
|
44
|
+
// dialog with its own background re-inks the theme its body reads.
|
|
45
|
+
function LinkForm({ form, pageList, canPickPage, testID }) {
|
|
46
|
+
const { label, setLabel, url, setUrl, pageId, setPageId, mode, setMode } = form;
|
|
47
|
+
const theme = useHostTheme();
|
|
48
|
+
const tokens = React.useMemo(() => resolveRichTextTokens(theme), [theme]);
|
|
85
49
|
|
|
86
50
|
const inputStyle = {
|
|
87
51
|
fontFamily: tokens.bodyFont,
|
|
@@ -130,49 +94,9 @@ export function makeMarkdownLinkDialog(rn, Overlay) {
|
|
|
130
94
|
),
|
|
131
95
|
);
|
|
132
96
|
|
|
133
|
-
const action = (key, text, { primary, disabled, onPress }) =>
|
|
134
|
-
React.createElement(
|
|
135
|
-
Pressable,
|
|
136
|
-
{
|
|
137
|
-
key,
|
|
138
|
-
onPress,
|
|
139
|
-
disabled,
|
|
140
|
-
accessibilityRole: "button",
|
|
141
|
-
testID: testID ? `${testID}-${key}` : undefined,
|
|
142
|
-
style: {
|
|
143
|
-
paddingVertical: tokens.markerGap,
|
|
144
|
-
paddingHorizontal: tokens.padding,
|
|
145
|
-
marginLeft: tokens.markerGap,
|
|
146
|
-
borderWidth: 1,
|
|
147
|
-
borderColor: primary ? tokens.accent : tokens.border,
|
|
148
|
-
borderRadius: tokens.radius,
|
|
149
|
-
opacity: disabled ? 0.5 : 1,
|
|
150
|
-
...(primary ? { backgroundColor: tokens.accent } : null),
|
|
151
|
-
},
|
|
152
|
-
},
|
|
153
|
-
React.createElement(
|
|
154
|
-
Text,
|
|
155
|
-
{
|
|
156
|
-
style: {
|
|
157
|
-
fontFamily: tokens.bodyFont,
|
|
158
|
-
fontSize: tokens.codeSize,
|
|
159
|
-
fontWeight: "500",
|
|
160
|
-
color: primary ? tokens.onAccent : tokens.color,
|
|
161
|
-
},
|
|
162
|
-
},
|
|
163
|
-
text,
|
|
164
|
-
),
|
|
165
|
-
);
|
|
166
|
-
|
|
167
97
|
return React.createElement(
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
visible: !!visible,
|
|
171
|
-
onRequestClose: onClose,
|
|
172
|
-
size: "sm",
|
|
173
|
-
accessibilityLabel: "Insert link",
|
|
174
|
-
testID: testID ? `${testID}-dialog` : undefined,
|
|
175
|
-
},
|
|
98
|
+
React.Fragment,
|
|
99
|
+
null,
|
|
176
100
|
React.createElement(
|
|
177
101
|
Field,
|
|
178
102
|
{ label: "Link text", tokens },
|
|
@@ -209,6 +133,8 @@ export function makeMarkdownLinkDialog(rn, Overlay) {
|
|
|
209
133
|
ScrollView,
|
|
210
134
|
{
|
|
211
135
|
accessibilityRole: "radiogroup",
|
|
136
|
+
// Inside the dialog's scrolling body; Android needs the opt-in.
|
|
137
|
+
nestedScrollEnabled: true,
|
|
212
138
|
style: {
|
|
213
139
|
maxHeight: PAGE_LIST_MAX_HEIGHT,
|
|
214
140
|
borderWidth: 1,
|
|
@@ -264,25 +190,85 @@ export function makeMarkdownLinkDialog(rn, Overlay) {
|
|
|
264
190
|
testID: testID ? `${testID}-url` : undefined,
|
|
265
191
|
}),
|
|
266
192
|
),
|
|
193
|
+
);
|
|
194
|
+
}
|
|
267
195
|
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
196
|
+
/**
|
|
197
|
+
* @param {object} props
|
|
198
|
+
* @param {boolean} props.visible
|
|
199
|
+
* @param {() => void} props.onClose
|
|
200
|
+
* @param {(result: {label: string, target: string}) => void} props.onSubmit
|
|
201
|
+
* @param {Array<{id: string, name: string}>} props.pages — pickable pages;
|
|
202
|
+
* empty means this host offered none, so only the URL mode is shown.
|
|
203
|
+
* @param {string} props.initialLabel — the author's current selection.
|
|
204
|
+
* @param {string} [props.testID]
|
|
205
|
+
*/
|
|
206
|
+
function MarkdownLinkDialog({
|
|
207
|
+
visible,
|
|
208
|
+
onClose,
|
|
209
|
+
onSubmit,
|
|
210
|
+
pages,
|
|
211
|
+
initialLabel,
|
|
212
|
+
testID,
|
|
213
|
+
}) {
|
|
214
|
+
const pageList = Array.isArray(pages) ? pages.filter((p) => p && p.id) : [];
|
|
215
|
+
const canPickPage = pageList.length > 0;
|
|
216
|
+
|
|
217
|
+
const [label, setLabel] = React.useState("");
|
|
218
|
+
const [url, setUrl] = React.useState("");
|
|
219
|
+
const [pageId, setPageId] = React.useState("");
|
|
220
|
+
const [mode, setMode] = React.useState("url");
|
|
221
|
+
|
|
222
|
+
// Re-seed on each open: the author's selection and the mode they should
|
|
223
|
+
// land in are both properties of THIS opening, not of the component.
|
|
224
|
+
React.useEffect(() => {
|
|
225
|
+
if (!visible) return;
|
|
226
|
+
setLabel(typeof initialLabel === "string" ? initialLabel : "");
|
|
227
|
+
setUrl("");
|
|
228
|
+
setPageId("");
|
|
229
|
+
setMode(canPickPage ? "page" : "url");
|
|
230
|
+
}, [visible, initialLabel, canPickPage]);
|
|
231
|
+
|
|
232
|
+
const target = mode === "page" ? pageId : url.trim();
|
|
233
|
+
|
|
234
|
+
const submit = () => {
|
|
235
|
+
if (!target) return;
|
|
236
|
+
if (typeof onSubmit === "function") onSubmit({ label, target });
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
const action = (key, title, props) =>
|
|
240
|
+
React.createElement(Button, {
|
|
241
|
+
key,
|
|
242
|
+
title,
|
|
243
|
+
testID: testID ? `${testID}-${key}` : undefined,
|
|
244
|
+
...props,
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
const actions = React.createElement(
|
|
248
|
+
React.Fragment,
|
|
249
|
+
null,
|
|
250
|
+
action("cancel", "Cancel", { variant: "secondary", onPress: onClose }),
|
|
251
|
+
// Disabled until there is somewhere to go — an empty target would
|
|
252
|
+
// write `[text]()`, which reads as a link and goes nowhere.
|
|
253
|
+
action("insert", "Insert link", { disabled: !target, onPress: submit }),
|
|
254
|
+
);
|
|
255
|
+
|
|
256
|
+
return React.createElement(
|
|
257
|
+
Modal,
|
|
258
|
+
{
|
|
259
|
+
visible: !!visible,
|
|
260
|
+
onRequestClose: onClose,
|
|
261
|
+
size: "sm",
|
|
262
|
+
title: "Insert link",
|
|
263
|
+
footer: actions,
|
|
264
|
+
testID: testID ? `${testID}-dialog` : undefined,
|
|
265
|
+
},
|
|
266
|
+
React.createElement(LinkForm, {
|
|
267
|
+
form: { label, setLabel, url, setUrl, pageId, setPageId, mode, setMode },
|
|
268
|
+
pageList,
|
|
269
|
+
canPickPage,
|
|
270
|
+
testID,
|
|
271
|
+
}),
|
|
286
272
|
);
|
|
287
273
|
}
|
|
288
274
|
|