@colixsystems/widget-sdk 0.245.0 → 0.246.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 +37 -1
- package/dist/button-tokens.js +154 -0
- package/dist/button-view.js +251 -93
- package/dist/button.js +5 -3
- package/dist/button.native.js +6 -4
- package/dist/contract.cjs +86 -33
- package/dist/contract.js +86 -33
- package/dist/detail-parts-view.js +31 -2
- package/dist/file-image-logic.js +162 -0
- package/dist/file-image-source.js +71 -0
- package/dist/file-image-view.js +119 -0
- package/dist/file-image.js +8 -0
- package/dist/file-image.native.js +8 -0
- package/dist/form-control-logic.js +23 -0
- package/dist/form-control-tokens.js +25 -10
- package/dist/index.d.ts +247 -18
- package/dist/index.js +7 -1
- package/dist/index.native.js +7 -1
- package/dist/menu-view.js +168 -0
- package/dist/overlay-tokens.js +50 -0
- package/dist/overlay-view.js +67 -24
- package/dist/overlay.native.js +3 -1
- package/dist/popover-tokens.js +145 -0
- package/dist/popover-view.js +252 -0
- package/dist/popover.js +23 -0
- package/dist/popover.native.js +22 -0
- package/dist/radio-group-logic.js +61 -0
- package/dist/radio-group-tokens.js +62 -0
- package/dist/radio-group-view.js +262 -0
- package/dist/radio-group.js +12 -0
- package/dist/radio-group.native.js +7 -0
- package/dist/select-view.js +15 -31
- package/dist/tabs-tokens.js +59 -0
- package/dist/tabs-view.js +9 -43
- package/dist/tooltip-view.js +137 -0
- package/dist/web-modal.js +95 -23
- package/package.json +1 -1
- package/src/contract.js +86 -33
package/dist/index.d.ts
CHANGED
|
@@ -3870,6 +3870,11 @@ export const Avatar: (props: {
|
|
|
3870
3870
|
seed?: string;
|
|
3871
3871
|
/** An absolute image URL. */
|
|
3872
3872
|
url?: string;
|
|
3873
|
+
/**
|
|
3874
|
+
* A filestore file id (a FILE column's value). Takes precedence over `url`;
|
|
3875
|
+
* the initials show while it loads and stay if it cannot load.
|
|
3876
|
+
*/
|
|
3877
|
+
fileId?: string | null;
|
|
3873
3878
|
/** A size token, or a number of px (clamped to 16-160). */
|
|
3874
3879
|
size?: "sm" | "md" | "lg" | "xl" | number;
|
|
3875
3880
|
shape?: "circle" | "rounded";
|
|
@@ -3878,6 +3883,36 @@ export const Avatar: (props: {
|
|
|
3878
3883
|
testID?: string;
|
|
3879
3884
|
}) => any;
|
|
3880
3885
|
|
|
3886
|
+
/**
|
|
3887
|
+
* A stored file's picture, or one at a URL: a themed `<Skeleton>` while it
|
|
3888
|
+
* loads and a fallback, never a broken image, when it cannot. A `fileId` is
|
|
3889
|
+
* resolved through `useFilestoreFile`, and an expired signed URL is fetched
|
|
3890
|
+
* again once before the fallback shows.
|
|
3891
|
+
*
|
|
3892
|
+
* @example
|
|
3893
|
+
* <FileImage fileId={record.Photo} variant="card" aspectRatio={4 / 3} />
|
|
3894
|
+
*/
|
|
3895
|
+
export const FileImage: (props: {
|
|
3896
|
+
/** A filestore file id (a FILE column's value). Wins over `uri`. */
|
|
3897
|
+
fileId?: string | null;
|
|
3898
|
+
/** A URL you already have, e.g. a `useFilestoreFiles()` row's `urls.card`. */
|
|
3899
|
+
uri?: string | null;
|
|
3900
|
+
/** The size-ladder rung to load. Default "large". */
|
|
3901
|
+
variant?: "thumbnail" | "card" | "large" | "hero" | "original";
|
|
3902
|
+
/** Width / height. Unset, the box takes the picture's own shape once loaded. */
|
|
3903
|
+
aspectRatio?: number;
|
|
3904
|
+
/** Corner radius: px, a theme radius name, or "none". Default the theme's `md`. */
|
|
3905
|
+
radius?: number | "none" | "sm" | "md" | "lg" | "pill";
|
|
3906
|
+
resizeMode?: "cover" | "contain" | "stretch" | "center";
|
|
3907
|
+
/** What shows when the picture cannot load. Default "icon". */
|
|
3908
|
+
fallback?: "icon" | "blank";
|
|
3909
|
+
/** Unset, the picture is decorative and hidden from assistive technology. */
|
|
3910
|
+
accessibilityLabel?: string;
|
|
3911
|
+
style?: any;
|
|
3912
|
+
/** Also tags `<testID>-image`, `<testID>-loading` and `<testID>-fallback`. */
|
|
3913
|
+
testID?: string;
|
|
3914
|
+
}) => any;
|
|
3915
|
+
|
|
3881
3916
|
/** Themed completion bar or ring for real progress toward a known goal. */
|
|
3882
3917
|
export const Progress: (props: {
|
|
3883
3918
|
/** Percent complete; clamped to 0-100. */
|
|
@@ -3979,38 +4014,61 @@ export const Card: (props: {
|
|
|
3979
4014
|
export type CardScrim = "bottom" | "top" | "left" | "right" | "full" | "none";
|
|
3980
4015
|
export type CardImagePosition = "center" | "top" | "bottom" | "left" | "right";
|
|
3981
4016
|
|
|
3982
|
-
export type ButtonVariant = "primary" | "secondary" | "outlined" | "danger";
|
|
4017
|
+
export type ButtonVariant = "primary" | "secondary" | "outlined" | "danger" | "ghost" | "link";
|
|
4018
|
+
export type ButtonSize = "sm" | "md" | "lg";
|
|
3983
4019
|
export const BUTTON_VARIANTS: readonly ButtonVariant[];
|
|
3984
4020
|
|
|
4021
|
+
/** Per-button surface fields; unset ones follow the theme. */
|
|
4022
|
+
export interface ButtonFields {
|
|
4023
|
+
background?: string;
|
|
4024
|
+
textColor?: string;
|
|
4025
|
+
borderColor?: string;
|
|
4026
|
+
borderWidth?: number;
|
|
4027
|
+
paddingX?: number;
|
|
4028
|
+
paddingY?: number;
|
|
4029
|
+
radius?: number;
|
|
4030
|
+
fontWeight?: string;
|
|
4031
|
+
fontSize?: number;
|
|
4032
|
+
shadow?: "none" | "sm" | "md" | "lg";
|
|
4033
|
+
gradient?: { from: string; to: string; angle?: number };
|
|
4034
|
+
}
|
|
4035
|
+
|
|
3985
4036
|
/**
|
|
3986
4037
|
* Themed button (Design > Components > Buttons, Danger buttons, Button outline). Omit
|
|
3987
|
-
* `onPress` for a purely visual button inside something pressable.
|
|
4038
|
+
* `onPress` for a purely visual button inside something pressable. `ghost` and
|
|
4039
|
+
* `link` draw no fill or border and take only the look's shape and type.
|
|
3988
4040
|
*
|
|
3989
4041
|
* @example
|
|
3990
|
-
* <Button title="Save" onPress={save} />
|
|
4042
|
+
* <Button title="Save" onPress={save} busy={saving} />
|
|
3991
4043
|
* <Button title="Delete" variant="danger" icon="trash-2" onPress={remove} />
|
|
4044
|
+
* <Button title="Load more" variant="ghost" size="sm" onPress={next} />
|
|
4045
|
+
*/
|
|
4046
|
+
/**
|
|
4047
|
+
* What a `<Menu>`, `<Popover>` or `<Tooltip>` puts on the button it wraps, so
|
|
4048
|
+
* a button can open one and expose whether it is open.
|
|
3992
4049
|
*/
|
|
3993
|
-
export
|
|
4050
|
+
export interface ButtonTriggerProps {
|
|
4051
|
+
/** A `<Tooltip>` shows on a long press. */
|
|
4052
|
+
onLongPress?: () => void;
|
|
4053
|
+
"aria-haspopup"?: "menu" | "dialog" | boolean;
|
|
4054
|
+
"aria-expanded"?: boolean;
|
|
4055
|
+
"aria-describedby"?: string;
|
|
4056
|
+
}
|
|
4057
|
+
|
|
4058
|
+
export const Button: (props: ButtonTriggerProps & {
|
|
3994
4059
|
title: ReactNode;
|
|
3995
4060
|
variant?: ButtonVariant;
|
|
4061
|
+
/** sm (36 high) | md (44, default) | lg (52). */
|
|
4062
|
+
size?: ButtonSize;
|
|
3996
4063
|
icon?: string;
|
|
3997
4064
|
iconPosition?: "leading" | "trailing";
|
|
3998
4065
|
onPress?: () => void;
|
|
3999
4066
|
disabled?: boolean;
|
|
4000
|
-
/**
|
|
4001
|
-
|
|
4002
|
-
|
|
4003
|
-
|
|
4004
|
-
|
|
4005
|
-
borderWidth?: number;
|
|
4006
|
-
paddingX?: number;
|
|
4007
|
-
paddingY?: number;
|
|
4008
|
-
radius?: number;
|
|
4009
|
-
fontWeight?: string;
|
|
4010
|
-
fontSize?: number;
|
|
4011
|
-
shadow?: "none" | "sm" | "md" | "lg";
|
|
4012
|
-
gradient?: { from: string; to: string; angle?: number };
|
|
4013
|
-
};
|
|
4067
|
+
/** A pending action: a spinner over the kept label; it cannot be pressed. */
|
|
4068
|
+
busy?: boolean;
|
|
4069
|
+
/** A toggle's on/off state; omit for a plain button. */
|
|
4070
|
+
selected?: boolean;
|
|
4071
|
+
fields?: ButtonFields;
|
|
4014
4072
|
style?: any;
|
|
4015
4073
|
labelStyle?: any;
|
|
4016
4074
|
accessibilityLabel?: string;
|
|
@@ -4019,6 +4077,34 @@ export const Button: (props: {
|
|
|
4019
4077
|
ref?: { current: any } | ((node: any) => void);
|
|
4020
4078
|
}) => any;
|
|
4021
4079
|
|
|
4080
|
+
/**
|
|
4081
|
+
* A square themed button that shows only an icon, at least 36px on both hosts.
|
|
4082
|
+
* `accessibilityLabel` is its only name, so it is required.
|
|
4083
|
+
*
|
|
4084
|
+
* @example
|
|
4085
|
+
* <IconButton icon="pencil" accessibilityLabel="Edit order" onPress={edit} />
|
|
4086
|
+
* <IconButton icon="bold" accessibilityLabel="Bold" selected={bold} onPress={toggleBold} />
|
|
4087
|
+
*/
|
|
4088
|
+
export const IconButton: (props: ButtonTriggerProps & {
|
|
4089
|
+
/** A lucide icon name, drawn with `<Icon>`. */
|
|
4090
|
+
icon: string;
|
|
4091
|
+
accessibilityLabel: string;
|
|
4092
|
+
/** ghost (default) | primary | secondary | outlined | danger. */
|
|
4093
|
+
variant?: Exclude<ButtonVariant, "link">;
|
|
4094
|
+
/** sm (36) | md (44, default) | lg (52). */
|
|
4095
|
+
size?: ButtonSize;
|
|
4096
|
+
/** The glyph's colour, over the variant's. */
|
|
4097
|
+
color?: string;
|
|
4098
|
+
onPress?: () => void;
|
|
4099
|
+
disabled?: boolean;
|
|
4100
|
+
busy?: boolean;
|
|
4101
|
+
selected?: boolean;
|
|
4102
|
+
fields?: ButtonFields;
|
|
4103
|
+
style?: any;
|
|
4104
|
+
testID?: string;
|
|
4105
|
+
ref?: { current: any } | ((node: any) => void);
|
|
4106
|
+
}) => any;
|
|
4107
|
+
|
|
4022
4108
|
/**
|
|
4023
4109
|
* What every form control shares: the label drawn with it (and read by a
|
|
4024
4110
|
* screen reader), a hint below it, and an error that replaces the hint and
|
|
@@ -4204,6 +4290,48 @@ export const SearchField: (props: FormControlProps & {
|
|
|
4204
4290
|
[prop: string]: any;
|
|
4205
4291
|
}) => any;
|
|
4206
4292
|
|
|
4293
|
+
export type RadioGroupVariant = "list" | "card" | "segmented";
|
|
4294
|
+
|
|
4295
|
+
export interface RadioOption<V extends string | number = string | number> {
|
|
4296
|
+
value: V;
|
|
4297
|
+
label: ReactNode;
|
|
4298
|
+
/** A second line under the label; not drawn by the segmented variant. */
|
|
4299
|
+
description?: ReactNode;
|
|
4300
|
+
/** A lucide icon name drawn before the label. */
|
|
4301
|
+
icon?: string;
|
|
4302
|
+
disabled?: boolean;
|
|
4303
|
+
accessibilityLabel?: string;
|
|
4304
|
+
testID?: string;
|
|
4305
|
+
}
|
|
4306
|
+
|
|
4307
|
+
/**
|
|
4308
|
+
* Themed pick-ONE of a short list that stays on screen. `list` (default) is
|
|
4309
|
+
* the app's input panel with one row per option, `card` one bordered card per
|
|
4310
|
+
* option (with its `description`), both from Design > Components > Inputs;
|
|
4311
|
+
* `segmented` is the segmented tab strip from Design > Components > Tabs,
|
|
4312
|
+
* its options sharing the row evenly. Radio role and checked state on both
|
|
4313
|
+
* hosts; on the web the group is one tab stop, the arrow keys move the
|
|
4314
|
+
* choice and Space picks the focused option. Other props land on the group.
|
|
4315
|
+
*
|
|
4316
|
+
* @example
|
|
4317
|
+
* <RadioGroup label="Delivery" value={speed} onValueChange={setSpeed}
|
|
4318
|
+
* options={[{ value: "standard", label: "Standard" }, { value: "express", label: "Express" }]} />
|
|
4319
|
+
*/
|
|
4320
|
+
export const RadioGroup: <V extends string | number = string | number>(
|
|
4321
|
+
props: FormControlProps & {
|
|
4322
|
+
options: Array<RadioOption<V>>;
|
|
4323
|
+
value?: V | null;
|
|
4324
|
+
/** Called when a different option is picked. */
|
|
4325
|
+
onValueChange?: (value: V, option: RadioOption<V>) => void;
|
|
4326
|
+
variant?: RadioGroupVariant;
|
|
4327
|
+
/** The group. */
|
|
4328
|
+
style?: any;
|
|
4329
|
+
/** The box around a group that shows a label, helper or error. */
|
|
4330
|
+
containerStyle?: any;
|
|
4331
|
+
[prop: string]: any;
|
|
4332
|
+
},
|
|
4333
|
+
) => any;
|
|
4334
|
+
|
|
4207
4335
|
export type TabStyle = "segmented" | "pill" | "underline";
|
|
4208
4336
|
|
|
4209
4337
|
/**
|
|
@@ -4429,6 +4557,107 @@ export const FileUpload: (props: FormControlProps & {
|
|
|
4429
4557
|
containerStyle?: any;
|
|
4430
4558
|
}) => any;
|
|
4431
4559
|
|
|
4560
|
+
export type PopoverPlacement = "bottom" | "top";
|
|
4561
|
+
|
|
4562
|
+
/** One `<Menu>` item. */
|
|
4563
|
+
export interface MenuItem {
|
|
4564
|
+
label: string;
|
|
4565
|
+
/** Runs after the menu closes. */
|
|
4566
|
+
onPress?: () => void;
|
|
4567
|
+
/** A Lucide icon name, or an element drawn before the label. */
|
|
4568
|
+
icon?: string | ReactNode;
|
|
4569
|
+
/** Drawn in the theme's danger colour. */
|
|
4570
|
+
danger?: boolean;
|
|
4571
|
+
/** Dimmed, skipped by the arrow keys, and cannot be picked. */
|
|
4572
|
+
disabled?: boolean;
|
|
4573
|
+
accessibilityLabel?: string;
|
|
4574
|
+
testID?: string;
|
|
4575
|
+
key?: string;
|
|
4576
|
+
}
|
|
4577
|
+
|
|
4578
|
+
/**
|
|
4579
|
+
* A list of actions opened from a trigger: a row's "…" button, a toolbar's
|
|
4580
|
+
* overflow. Beside the trigger on a wide screen, a bottom sheet on a phone;
|
|
4581
|
+
* drawn over the screen, so no card or list clips it. The backdrop, Escape
|
|
4582
|
+
* (web) and the Android back button close it, and focus returns to the
|
|
4583
|
+
* trigger. On the web it is a `menu` of `menuitem`s: the arrow keys, Home and
|
|
4584
|
+
* End move between enabled items and Space or Enter picks one. A picked
|
|
4585
|
+
* item's `onPress` runs once the menu has left the screen, so it can open a
|
|
4586
|
+
* `<ConfirmDialog>`. Follows the theme's `popover` scope (Design > Components
|
|
4587
|
+
* > Dropdown menus).
|
|
4588
|
+
*
|
|
4589
|
+
* @example
|
|
4590
|
+
* <Menu
|
|
4591
|
+
* trigger={<IconButton icon="more-horizontal" accessibilityLabel="More actions" />}
|
|
4592
|
+
* accessibilityLabel="Order actions"
|
|
4593
|
+
* items={[
|
|
4594
|
+
* { label: "Edit", icon: "pencil", onPress: edit },
|
|
4595
|
+
* canDelete && { label: "Delete", icon: "trash-2", danger: true, onPress: askDelete },
|
|
4596
|
+
* ]}
|
|
4597
|
+
* />
|
|
4598
|
+
*/
|
|
4599
|
+
export const Menu: (props: {
|
|
4600
|
+
/** ONE pressable element; the menu adds the press that opens it. */
|
|
4601
|
+
trigger: ReactNode;
|
|
4602
|
+
/** Falsy entries are skipped, so `cond && { … }` works. */
|
|
4603
|
+
items: Array<MenuItem | null | false | undefined>;
|
|
4604
|
+
accessibilityLabel?: string;
|
|
4605
|
+
/** Default "bottom"; it flips at the screen edge. */
|
|
4606
|
+
placement?: PopoverPlacement;
|
|
4607
|
+
/** The sheet backdrop's name. Default "Close". */
|
|
4608
|
+
closeAccessibilityLabel?: string;
|
|
4609
|
+
/** On the panel; the item list takes `-items`. */
|
|
4610
|
+
testID?: string;
|
|
4611
|
+
}) => any;
|
|
4612
|
+
|
|
4613
|
+
/**
|
|
4614
|
+
* Free content beside a trigger: quick filters, a swatch picker, a short
|
|
4615
|
+
* detail card. Placed like `<Menu>`, closed the same ways. Components inside
|
|
4616
|
+
* read a theme inked for the panel. Follows the theme's `popover` scope.
|
|
4617
|
+
*
|
|
4618
|
+
* @example
|
|
4619
|
+
* <Popover trigger={<Button variant="secondary" title="Filters" />} accessibilityLabel="Filters">
|
|
4620
|
+
* {({ close }) => <Button title="Apply" onPress={() => { apply(); close(); }} />}
|
|
4621
|
+
* </Popover>
|
|
4622
|
+
*/
|
|
4623
|
+
export const Popover: (props: {
|
|
4624
|
+
/** ONE pressable element; the popover adds the press that opens it. */
|
|
4625
|
+
trigger: ReactNode;
|
|
4626
|
+
children?: ReactNode | ((api: { close: () => void }) => ReactNode);
|
|
4627
|
+
/** Pass with `onVisibleChange` to open and close it from your own code. */
|
|
4628
|
+
visible?: boolean;
|
|
4629
|
+
onVisibleChange?: (visible: boolean) => void;
|
|
4630
|
+
/** Default "bottom"; it flips at the screen edge. */
|
|
4631
|
+
placement?: PopoverPlacement;
|
|
4632
|
+
accessibilityLabel?: string;
|
|
4633
|
+
/** The sheet backdrop's name. Default "Close". */
|
|
4634
|
+
closeAccessibilityLabel?: string;
|
|
4635
|
+
/** On the panel. */
|
|
4636
|
+
testID?: string;
|
|
4637
|
+
}) => any;
|
|
4638
|
+
|
|
4639
|
+
/**
|
|
4640
|
+
* A short label for the one pressable element it wraps, usually an
|
|
4641
|
+
* icon-only button that keeps its own `accessibilityLabel`. On the web it
|
|
4642
|
+
* shows on hover and keyboard focus; a touch screen has no hover, so there a
|
|
4643
|
+
* long press shows it (a bottom sheet on a phone). A child with its own
|
|
4644
|
+
* `onLongPress` keeps it and gets no long-press tooltip. The tooltip wraps
|
|
4645
|
+
* the child in a `View`. Follows the theme's `popover` scope.
|
|
4646
|
+
*
|
|
4647
|
+
* @example
|
|
4648
|
+
* <Tooltip label="Delete">
|
|
4649
|
+
* <IconButton icon="trash-2" accessibilityLabel="Delete" onPress={remove} />
|
|
4650
|
+
* </Tooltip>
|
|
4651
|
+
*/
|
|
4652
|
+
export const Tooltip: (props: {
|
|
4653
|
+
label: string;
|
|
4654
|
+
/** Default "top"; it flips at the screen edge. */
|
|
4655
|
+
placement?: PopoverPlacement;
|
|
4656
|
+
/** On the label. */
|
|
4657
|
+
testID?: string;
|
|
4658
|
+
children: ReactNode;
|
|
4659
|
+
}) => any;
|
|
4660
|
+
|
|
4432
4661
|
/**
|
|
4433
4662
|
* Themed collapsible sections. The surface comes from the theme's `card`
|
|
4434
4663
|
* scope and the headers and dividers from its `list` scope, so every
|
package/dist/index.js
CHANGED
|
@@ -241,6 +241,8 @@ export { List, ListRow, ListHeader, EmptyState } from "./list.js";
|
|
|
241
241
|
export { Badge, Avatar, Progress, Stat, Divider } from "./detail-parts.js";
|
|
242
242
|
export { initialsFor } from "./initials.js";
|
|
243
243
|
export { avatarColorFor } from "./avatar-color.js";
|
|
244
|
+
// A stored file's picture: a themed placeholder, then a fallback on failure.
|
|
245
|
+
export { FileImage } from "./file-image.js";
|
|
244
246
|
// The one card surface: the theme's `card` scope, a gradient fill, margin.
|
|
245
247
|
export { Card } from "./card.js";
|
|
246
248
|
export {
|
|
@@ -258,7 +260,7 @@ export {
|
|
|
258
260
|
cardGradientProps,
|
|
259
261
|
} from "./card-style.js";
|
|
260
262
|
// The themed button; follows the theme's button, destructive and ring scopes.
|
|
261
|
-
export { Button, BUTTON_VARIANTS } from "./button.js";
|
|
263
|
+
export { Button, IconButton, BUTTON_VARIANTS } from "./button.js";
|
|
262
264
|
export {
|
|
263
265
|
BUTTON_PADDING_MAX,
|
|
264
266
|
BUTTON_BORDER_WIDTH_MAX,
|
|
@@ -278,6 +280,8 @@ export { Checkbox } from "./checkbox.js";
|
|
|
278
280
|
export { Switch } from "./switch.js";
|
|
279
281
|
export { Select } from "./select.js";
|
|
280
282
|
export { SearchField } from "./search-field.js";
|
|
283
|
+
// Pick one of a short visible list; `muted` scope, or `tabs` when segmented.
|
|
284
|
+
export { RadioGroup } from "./radio-group.js";
|
|
281
285
|
// Space for a control whose role is not a button; a no-op on native.
|
|
282
286
|
export { spaceActivates } from "./form-control-logic.js";
|
|
283
287
|
// A themed tab strip; follows the theme's `tabs` scope.
|
|
@@ -294,6 +298,8 @@ export { ConfirmDialog } from "./confirm-dialog.js";
|
|
|
294
298
|
export { Chip, ChipGroup } from "./chip.js";
|
|
295
299
|
// A themed file chooser on `<FilePicker>`; follows the theme's `muted` scope.
|
|
296
300
|
export { FileUpload } from "./file-upload.js";
|
|
301
|
+
// Anchored actions, content and labels; follow the theme's `popover` scope.
|
|
302
|
+
export { Menu, Popover, Tooltip } from "./popover.js";
|
|
297
303
|
// The semantic text tiers, shared with the built-in widgets.
|
|
298
304
|
export {
|
|
299
305
|
textRoleStyle,
|
package/dist/index.native.js
CHANGED
|
@@ -234,6 +234,8 @@ export { List, ListRow, ListHeader, EmptyState } from "./list.native.js";
|
|
|
234
234
|
export { Badge, Avatar, Progress, Stat, Divider } from "./detail-parts.native.js";
|
|
235
235
|
export { initialsFor } from "./initials.js";
|
|
236
236
|
export { avatarColorFor } from "./avatar-color.js";
|
|
237
|
+
// A stored file's picture: a themed placeholder, then a fallback on failure.
|
|
238
|
+
export { FileImage } from "./file-image.native.js";
|
|
237
239
|
// The one card surface: the theme's `card` scope, a gradient fill, margin.
|
|
238
240
|
export { Card } from "./card.native.js";
|
|
239
241
|
export {
|
|
@@ -251,7 +253,7 @@ export {
|
|
|
251
253
|
cardGradientProps,
|
|
252
254
|
} from "./card-style.js";
|
|
253
255
|
// The themed button; follows the theme's button, destructive and ring scopes.
|
|
254
|
-
export { Button, BUTTON_VARIANTS } from "./button.native.js";
|
|
256
|
+
export { Button, IconButton, BUTTON_VARIANTS } from "./button.native.js";
|
|
255
257
|
export {
|
|
256
258
|
BUTTON_PADDING_MAX,
|
|
257
259
|
BUTTON_BORDER_WIDTH_MAX,
|
|
@@ -271,6 +273,8 @@ export { Checkbox } from "./checkbox.native.js";
|
|
|
271
273
|
export { Switch } from "./switch.native.js";
|
|
272
274
|
export { Select } from "./select.native.js";
|
|
273
275
|
export { SearchField } from "./search-field.native.js";
|
|
276
|
+
// Pick one of a short visible list; `muted` scope, or `tabs` when segmented.
|
|
277
|
+
export { RadioGroup } from "./radio-group.native.js";
|
|
274
278
|
// Space for a control whose role is not a button; a no-op on native.
|
|
275
279
|
export { spaceActivates } from "./form-control-logic.js";
|
|
276
280
|
// A themed tab strip; follows the theme's `tabs` scope.
|
|
@@ -287,6 +291,8 @@ export { ConfirmDialog } from "./confirm-dialog.native.js";
|
|
|
287
291
|
export { Chip, ChipGroup } from "./chip.native.js";
|
|
288
292
|
// A themed file chooser on `<FilePicker>`; follows the theme's `muted` scope.
|
|
289
293
|
export { FileUpload } from "./file-upload.native.js";
|
|
294
|
+
// Anchored actions, content and labels; follow the theme's `popover` scope.
|
|
295
|
+
export { Menu, Popover, Tooltip } from "./popover.native.js";
|
|
290
296
|
// The semantic text tiers, shared with the built-in widgets.
|
|
291
297
|
export {
|
|
292
298
|
textRoleStyle,
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
// The ONE implementation behind `<Menu>`: a list of actions opened from a
|
|
2
|
+
// trigger, on the anchored surface `<Popover>` uses. Its rows are `<Select>`'s
|
|
3
|
+
// option rows, so a menu and a dropdown in one app look alike.
|
|
4
|
+
|
|
5
|
+
import React from "react";
|
|
6
|
+
import { useHostTheme } from "./hooks.js";
|
|
7
|
+
import { DEFAULT_THEME_TOKENS } from "./_theme-tokens.js";
|
|
8
|
+
import { Icon } from "./icon.js";
|
|
9
|
+
import { optionRowStyle, resolveFormControlTokens } from "./form-control-tokens.js";
|
|
10
|
+
import { isText, optionKeyDown } from "./form-control-logic.js";
|
|
11
|
+
import { initialFocusIndex } from "./select-logic.js";
|
|
12
|
+
import { POPOVER_DEFAULTS, menuItemList } from "./popover-tokens.js";
|
|
13
|
+
import { triggerWith } from "./popover-view.js";
|
|
14
|
+
|
|
15
|
+
const h = React.createElement;
|
|
16
|
+
const ICON_SIZE = 18;
|
|
17
|
+
const NOTHING_PICKED = () => false;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Bind the shared menu to one platform's primitives and anchored surface.
|
|
21
|
+
*
|
|
22
|
+
* @param {{View: any, Text: any, Pressable: any, ScrollView: any, Platform: any}} rn
|
|
23
|
+
* @param {any} AnchoredSurface — from `makePopover()` for the same host.
|
|
24
|
+
*/
|
|
25
|
+
export function makeMenu(rn, AnchoredSurface) {
|
|
26
|
+
const { Text, Pressable, ScrollView, Platform } = rn;
|
|
27
|
+
|
|
28
|
+
// Keyboard focus is a web concern: a device moves its reader into the
|
|
29
|
+
// sheet by itself, and a programmatic focus there would fight it.
|
|
30
|
+
function focusNode(node) {
|
|
31
|
+
if (Platform.OS === "web" && node && typeof node.focus === "function") node.focus();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function itemGlyph(icon, color) {
|
|
35
|
+
if (typeof icon === "string" && icon) return h(Icon, { name: icon, size: ICON_SIZE, color });
|
|
36
|
+
return React.isValidElement(icon) ? icon : null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function MenuItems({ t, items, onPick, accessibilityLabel, testID }) {
|
|
40
|
+
const nodes = React.useRef([]);
|
|
41
|
+
const initial = initialFocusIndex(items, NOTHING_PICKED);
|
|
42
|
+
const pendingFocus = React.useRef(true);
|
|
43
|
+
return h(
|
|
44
|
+
ScrollView,
|
|
45
|
+
{
|
|
46
|
+
style: { flexShrink: 1 },
|
|
47
|
+
role: "menu",
|
|
48
|
+
accessibilityLabel,
|
|
49
|
+
keyboardShouldPersistTaps: "handled",
|
|
50
|
+
testID: testID ? `${testID}-items` : undefined,
|
|
51
|
+
},
|
|
52
|
+
...items.map((item, index) => {
|
|
53
|
+
// The web host still delivers keys to a disabled Pressable.
|
|
54
|
+
const activate = () => {
|
|
55
|
+
if (!item.disabled) onPick(item);
|
|
56
|
+
};
|
|
57
|
+
const color = item.danger ? t.popover.dangerColor : t.popover.textColor;
|
|
58
|
+
return h(
|
|
59
|
+
Pressable,
|
|
60
|
+
{
|
|
61
|
+
key: item.key,
|
|
62
|
+
ref: (node) => {
|
|
63
|
+
nodes.current[index] = node;
|
|
64
|
+
if (node && index === initial && pendingFocus.current) {
|
|
65
|
+
pendingFocus.current = false;
|
|
66
|
+
focusNode(node);
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
onPress: activate,
|
|
70
|
+
onKeyDown: optionKeyDown({
|
|
71
|
+
options: items,
|
|
72
|
+
index,
|
|
73
|
+
focusAt: (next) => focusNode(nodes.current[next]),
|
|
74
|
+
activate,
|
|
75
|
+
}),
|
|
76
|
+
disabled: item.disabled,
|
|
77
|
+
role: "menuitem",
|
|
78
|
+
"aria-disabled": item.disabled,
|
|
79
|
+
accessibilityLabel: item.accessibilityLabel,
|
|
80
|
+
testID: item.testID,
|
|
81
|
+
style: ({ pressed }) => optionRowStyle(t, { pressed, disabled: item.disabled }),
|
|
82
|
+
},
|
|
83
|
+
itemGlyph(item.icon, color),
|
|
84
|
+
h(
|
|
85
|
+
Text,
|
|
86
|
+
{ style: { fontFamily: t.fontFamily, fontSize: t.fontSize, color, flexShrink: 1 } },
|
|
87
|
+
item.label,
|
|
88
|
+
),
|
|
89
|
+
);
|
|
90
|
+
}),
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* A list of actions opened from a trigger: a row's "…" button, a toolbar's
|
|
96
|
+
* overflow. Beside the trigger on a wide screen, a bottom sheet on a phone.
|
|
97
|
+
* Each item closes the menu, then runs its `onPress` once the menu has left
|
|
98
|
+
* the screen, so an item can open a dialog of its own.
|
|
99
|
+
*
|
|
100
|
+
* @param {object} props
|
|
101
|
+
* @param {any} props.trigger — the pressable element that opens it.
|
|
102
|
+
* @param {Array<{label: string, onPress?: () => void, icon?: string|any,
|
|
103
|
+
* danger?: boolean, disabled?: boolean, testID?: string,
|
|
104
|
+
* accessibilityLabel?: string, key?: string}|null|false>} props.items —
|
|
105
|
+
* falsy entries are skipped, so `cond && {…}` works.
|
|
106
|
+
* @param {string} [props.accessibilityLabel] — names the menu.
|
|
107
|
+
* @param {"bottom"|"top"} [props.placement] — default "bottom"; flips.
|
|
108
|
+
* @param {string} [props.closeAccessibilityLabel] — the sheet backdrop's name.
|
|
109
|
+
* @param {string} [props.testID] — on the panel; the list takes `-items`.
|
|
110
|
+
*/
|
|
111
|
+
function Menu({
|
|
112
|
+
trigger,
|
|
113
|
+
items,
|
|
114
|
+
accessibilityLabel,
|
|
115
|
+
placement = "bottom",
|
|
116
|
+
closeAccessibilityLabel,
|
|
117
|
+
testID,
|
|
118
|
+
}) {
|
|
119
|
+
const theme = useHostTheme() || DEFAULT_THEME_TOKENS;
|
|
120
|
+
const t = resolveFormControlTokens(theme);
|
|
121
|
+
const anchorRef = React.useRef(null);
|
|
122
|
+
const [open, setOpen] = React.useState(false);
|
|
123
|
+
const list = menuItemList(items);
|
|
124
|
+
const close = () => setOpen(false);
|
|
125
|
+
// A dialog opened before the menu is gone would lose focus on the web and
|
|
126
|
+
// fail to present on iOS.
|
|
127
|
+
const picked = React.useRef(null);
|
|
128
|
+
const pick = (item) => {
|
|
129
|
+
picked.current = item.onPress;
|
|
130
|
+
close();
|
|
131
|
+
};
|
|
132
|
+
const runPicked = () => {
|
|
133
|
+
const run = picked.current;
|
|
134
|
+
picked.current = null;
|
|
135
|
+
if (typeof run === "function") run();
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
return h(
|
|
139
|
+
React.Fragment,
|
|
140
|
+
null,
|
|
141
|
+
triggerWith(trigger, {
|
|
142
|
+
anchorRef,
|
|
143
|
+
onOpen: () => setOpen(true),
|
|
144
|
+
extraProps: { "aria-haspopup": "menu", "aria-expanded": open },
|
|
145
|
+
}),
|
|
146
|
+
h(
|
|
147
|
+
AnchoredSurface,
|
|
148
|
+
{
|
|
149
|
+
visible: open && list.length > 0,
|
|
150
|
+
onRequestClose: close,
|
|
151
|
+
onDismiss: runPicked,
|
|
152
|
+
anchorRef,
|
|
153
|
+
placement,
|
|
154
|
+
padding: t.rowGap,
|
|
155
|
+
minWidth: POPOVER_DEFAULTS.menuMinWidth,
|
|
156
|
+
maxWidth: POPOVER_DEFAULTS.menuMaxWidth,
|
|
157
|
+
accessibilityLabel: isText(accessibilityLabel) ? String(accessibilityLabel) : undefined,
|
|
158
|
+
closeAccessibilityLabel,
|
|
159
|
+
testID,
|
|
160
|
+
},
|
|
161
|
+
h(MenuItems, { t, items: list, onPick: pick, accessibilityLabel, testID }),
|
|
162
|
+
),
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
Menu.displayName = "Menu";
|
|
167
|
+
return Menu;
|
|
168
|
+
}
|
package/dist/overlay-tokens.js
CHANGED
|
@@ -85,6 +85,56 @@ export function overlayCornerStyle(radius, presentation) {
|
|
|
85
85
|
: { borderRadius: radius };
|
|
86
86
|
}
|
|
87
87
|
|
|
88
|
+
/**
|
|
89
|
+
* The screen box and the panel box of one overlay. An anchored overlay (a
|
|
90
|
+
* menu or popover placed at its trigger) has no scrim, and its panel sits at
|
|
91
|
+
* `anchoredAt`, a window position, instead of in the centre or at the bottom.
|
|
92
|
+
*
|
|
93
|
+
* @param {object} tokens — `resolveOverlayTokens()`'s result.
|
|
94
|
+
* @param {{top: number, left: number}|null} [anchoredAt]
|
|
95
|
+
*/
|
|
96
|
+
export function overlayFrameStyles(tokens, anchoredAt) {
|
|
97
|
+
const look = {
|
|
98
|
+
backgroundColor: tokens.surface,
|
|
99
|
+
padding: tokens.padding,
|
|
100
|
+
...tokens.elevation,
|
|
101
|
+
};
|
|
102
|
+
if (anchoredAt) {
|
|
103
|
+
return {
|
|
104
|
+
backdrop: { flex: 1, backgroundColor: "transparent" },
|
|
105
|
+
panel: {
|
|
106
|
+
position: "absolute",
|
|
107
|
+
top: anchoredAt.top,
|
|
108
|
+
left: anchoredAt.left,
|
|
109
|
+
...look,
|
|
110
|
+
...overlayCornerStyle(tokens.radius, "dialog"),
|
|
111
|
+
},
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
return {
|
|
115
|
+
backdrop: {
|
|
116
|
+
flex: 1,
|
|
117
|
+
alignItems: "center",
|
|
118
|
+
justifyContent: tokens.verticalAlign,
|
|
119
|
+
padding: tokens.screenPadding,
|
|
120
|
+
...(tokens.topGap ? { paddingTop: tokens.topGap } : null),
|
|
121
|
+
backgroundColor: tokens.scrim,
|
|
122
|
+
},
|
|
123
|
+
panel: {
|
|
124
|
+
width: "100%",
|
|
125
|
+
// Capped to the padded screen box so a tall panel scrolls inside its own
|
|
126
|
+
// ScrollView instead of overflowing past the viewport unreachably.
|
|
127
|
+
maxHeight: "100%",
|
|
128
|
+
flexShrink: 1,
|
|
129
|
+
...(tokens.maxWidth ? { maxWidth: tokens.maxWidth } : { flex: 1 }),
|
|
130
|
+
backgroundColor: look.backgroundColor,
|
|
131
|
+
...overlayCornerStyle(tokens.radius, tokens.presentation),
|
|
132
|
+
padding: look.padding,
|
|
133
|
+
...tokens.elevation,
|
|
134
|
+
},
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
88
138
|
/**
|
|
89
139
|
* Resolve the values one overlay is painted with, from the workspace theme.
|
|
90
140
|
*
|