@bacon-pos/ui 0.6.0 → 0.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/commonjs/components/Dropdown/Dropdown.stories.js +99 -1
- package/lib/commonjs/components/Dropdown/Dropdown.stories.js.map +1 -1
- package/lib/commonjs/components/Menu/Menu.js +25 -52
- package/lib/commonjs/components/Menu/Menu.js.map +1 -1
- package/lib/commonjs/components/Menu/MenuButton.js +4 -0
- package/lib/commonjs/components/Menu/MenuButton.js.map +1 -1
- package/lib/commonjs/components/Menu/SelectButton.js +4 -0
- package/lib/commonjs/components/Menu/SelectButton.js.map +1 -1
- package/lib/commonjs/components/Menu/UserMenu.js +77 -0
- package/lib/commonjs/components/Menu/UserMenu.js.map +1 -0
- package/lib/commonjs/components/Menu/UserMenu.stories.js +85 -0
- package/lib/commonjs/components/Menu/UserMenu.stories.js.map +1 -0
- package/lib/commonjs/components/Menu/UserMenu.test.js +52 -0
- package/lib/commonjs/components/Menu/UserMenu.test.js.map +1 -0
- package/lib/commonjs/components/Menu/index.js +7 -0
- package/lib/commonjs/components/Menu/index.js.map +1 -1
- package/lib/commonjs/components/Menu/placement.js +84 -0
- package/lib/commonjs/components/Menu/placement.js.map +1 -0
- package/lib/commonjs/components/Menu/placement.test.js +186 -0
- package/lib/commonjs/components/Menu/placement.test.js.map +1 -0
- package/lib/commonjs/components/Select/Select.js +2 -0
- package/lib/commonjs/components/Select/Select.js.map +1 -1
- package/lib/module/components/Dropdown/Dropdown.stories.js +98 -0
- package/lib/module/components/Dropdown/Dropdown.stories.js.map +1 -1
- package/lib/module/components/Menu/Menu.js +27 -54
- package/lib/module/components/Menu/Menu.js.map +1 -1
- package/lib/module/components/Menu/MenuButton.js +4 -0
- package/lib/module/components/Menu/MenuButton.js.map +1 -1
- package/lib/module/components/Menu/SelectButton.js +4 -0
- package/lib/module/components/Menu/SelectButton.js.map +1 -1
- package/lib/module/components/Menu/UserMenu.js +73 -0
- package/lib/module/components/Menu/UserMenu.js.map +1 -0
- package/lib/module/components/Menu/UserMenu.stories.js +81 -0
- package/lib/module/components/Menu/UserMenu.stories.js.map +1 -0
- package/lib/module/components/Menu/UserMenu.test.js +52 -0
- package/lib/module/components/Menu/UserMenu.test.js.map +1 -0
- package/lib/module/components/Menu/index.js +1 -0
- package/lib/module/components/Menu/index.js.map +1 -1
- package/lib/module/components/Menu/placement.js +80 -0
- package/lib/module/components/Menu/placement.js.map +1 -0
- package/lib/module/components/Menu/placement.test.js +186 -0
- package/lib/module/components/Menu/placement.test.js.map +1 -0
- package/lib/module/components/Select/Select.js +2 -0
- package/lib/module/components/Select/Select.js.map +1 -1
- package/lib/typescript/commonjs/components/Menu/Menu.d.ts +7 -1
- package/lib/typescript/commonjs/components/Menu/Menu.d.ts.map +1 -1
- package/lib/typescript/commonjs/components/Menu/MenuButton.d.ts +6 -2
- package/lib/typescript/commonjs/components/Menu/MenuButton.d.ts.map +1 -1
- package/lib/typescript/commonjs/components/Menu/SelectButton.d.ts +6 -2
- package/lib/typescript/commonjs/components/Menu/SelectButton.d.ts.map +1 -1
- package/lib/typescript/commonjs/components/Menu/UserMenu.d.ts +50 -0
- package/lib/typescript/commonjs/components/Menu/UserMenu.d.ts.map +1 -0
- package/lib/typescript/commonjs/components/Menu/index.d.ts +2 -0
- package/lib/typescript/commonjs/components/Menu/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/components/Menu/placement.d.ts +47 -0
- package/lib/typescript/commonjs/components/Menu/placement.d.ts.map +1 -0
- package/lib/typescript/commonjs/components/Menu/placement.test.d.ts +2 -0
- package/lib/typescript/commonjs/components/Menu/placement.test.d.ts.map +1 -0
- package/lib/typescript/commonjs/components/Select/Select.d.ts +3 -1
- package/lib/typescript/commonjs/components/Select/Select.d.ts.map +1 -1
- package/lib/typescript/module/components/Menu/Menu.d.ts +7 -1
- package/lib/typescript/module/components/Menu/Menu.d.ts.map +1 -1
- package/lib/typescript/module/components/Menu/MenuButton.d.ts +6 -2
- package/lib/typescript/module/components/Menu/MenuButton.d.ts.map +1 -1
- package/lib/typescript/module/components/Menu/SelectButton.d.ts +6 -2
- package/lib/typescript/module/components/Menu/SelectButton.d.ts.map +1 -1
- package/lib/typescript/module/components/Menu/UserMenu.d.ts +50 -0
- package/lib/typescript/module/components/Menu/UserMenu.d.ts.map +1 -0
- package/lib/typescript/module/components/Menu/index.d.ts +2 -0
- package/lib/typescript/module/components/Menu/index.d.ts.map +1 -1
- package/lib/typescript/module/components/Menu/placement.d.ts +47 -0
- package/lib/typescript/module/components/Menu/placement.d.ts.map +1 -0
- package/lib/typescript/module/components/Menu/placement.test.d.ts +2 -0
- package/lib/typescript/module/components/Menu/placement.test.d.ts.map +1 -0
- package/lib/typescript/module/components/Select/Select.d.ts +3 -1
- package/lib/typescript/module/components/Select/Select.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/components/Menu/Menu.tsx +37 -46
- package/src/components/Menu/MenuButton.tsx +9 -1
- package/src/components/Menu/SelectButton.tsx +9 -1
- package/src/components/Menu/UserMenu.tsx +108 -0
- package/src/components/Menu/index.ts +2 -0
- package/src/components/Menu/placement.ts +107 -0
- package/src/components/Select/Select.tsx +4 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type ButtonSize, type ButtonVariant } from '../Button/Button';
|
|
2
2
|
import type { IconName } from '../Icon/icons.generated';
|
|
3
|
-
import { type MenuOrigin } from './Menu';
|
|
3
|
+
import { type EdgeInsets, type MenuOrigin } from './Menu';
|
|
4
4
|
/** A selectable option. Its `icon` shows both in the row and on the trigger when chosen. */
|
|
5
5
|
export interface SelectButtonOption {
|
|
6
6
|
key: string;
|
|
@@ -39,6 +39,10 @@ export interface SelectButtonProps {
|
|
|
39
39
|
menuOrigin?: MenuOrigin;
|
|
40
40
|
/** Gap between trigger and menu. Defaults to 8. */
|
|
41
41
|
offset?: number;
|
|
42
|
+
/** Safe-area insets to keep clear. */
|
|
43
|
+
insets?: EdgeInsets;
|
|
44
|
+
/** Status-bar coordinate offset for the menu (see `Menu`). Pass 0 when immersive. */
|
|
45
|
+
statusBarHeight?: number;
|
|
42
46
|
/** Menu width. Defaults to the `Dropdown` default (350). */
|
|
43
47
|
menuWidth?: number;
|
|
44
48
|
/** Draw dividers between rows. Defaults to the `Dropdown` default (true). */
|
|
@@ -56,5 +60,5 @@ export interface SelectButtonProps {
|
|
|
56
60
|
* A composed component built from `Button` + `Menu` + `Dropdown`. Controlled via
|
|
57
61
|
* `value` / `onChange`, or uncontrolled via `defaultValue`.
|
|
58
62
|
*/
|
|
59
|
-
export declare function SelectButton({ options, value: valueProp, defaultValue, onChange, placeholder, placeholderIcon, showSelectedIcon, chevron, variant, size, fullWidth, disabled, anchorOrigin, menuOrigin, offset, menuWidth, showDividers, closeOnSelect, open, onOpenChange, }: SelectButtonProps): import("react/jsx-runtime").JSX.Element;
|
|
63
|
+
export declare function SelectButton({ options, value: valueProp, defaultValue, onChange, placeholder, placeholderIcon, showSelectedIcon, chevron, variant, size, fullWidth, disabled, anchorOrigin, menuOrigin, offset, insets, statusBarHeight, menuWidth, showDividers, closeOnSelect, open, onOpenChange, }: SelectButtonProps): import("react/jsx-runtime").JSX.Element;
|
|
60
64
|
//# sourceMappingURL=SelectButton.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SelectButton.d.ts","sourceRoot":"","sources":["../../../../../src/components/Menu/SelectButton.tsx"],"names":[],"mappings":"AAEA,OAAO,EAAU,KAAK,UAAU,EAAE,KAAK,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAG/E,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,yBAAyB,CAAC;AACxD,OAAO,EAAQ,KAAK,UAAU,EAAE,MAAM,QAAQ,CAAC;
|
|
1
|
+
{"version":3,"file":"SelectButton.d.ts","sourceRoot":"","sources":["../../../../../src/components/Menu/SelectButton.tsx"],"names":[],"mappings":"AAEA,OAAO,EAAU,KAAK,UAAU,EAAE,KAAK,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAG/E,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,yBAAyB,CAAC;AACxD,OAAO,EAAQ,KAAK,UAAU,EAAE,KAAK,UAAU,EAAE,MAAM,QAAQ,CAAC;AAEhE,4FAA4F;AAC5F,MAAM,WAAW,kBAAkB;IACjC,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,QAAQ,CAAC;IAChB,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED,MAAM,WAAW,iBAAiB;IAChC,kCAAkC;IAClC,OAAO,EAAE,kBAAkB,EAAE,CAAC;IAC9B,iCAAiC;IACjC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,8CAA8C;IAC9C,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,0CAA0C;IAC1C,QAAQ,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;IACjC,8CAA8C;IAC9C,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,qDAAqD;IACrD,eAAe,CAAC,EAAE,QAAQ,CAAC;IAC3B,wEAAwE;IACxE,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B,sFAAsF;IACtF,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,+CAA+C;IAC/C,OAAO,CAAC,EAAE,aAAa,CAAC;IACxB,sCAAsC;IACtC,IAAI,CAAC,EAAE,UAAU,CAAC;IAClB,+CAA+C;IAC/C,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,2BAA2B;IAC3B,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,gFAAgF;IAChF,YAAY,CAAC,EAAE,UAAU,CAAC;IAC1B,2EAA2E;IAC3E,UAAU,CAAC,EAAE,UAAU,CAAC;IACxB,mDAAmD;IACnD,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,sCAAsC;IACtC,MAAM,CAAC,EAAE,UAAU,CAAC;IACpB,qFAAqF;IACrF,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,4DAA4D;IAC5D,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,6EAA6E;IAC7E,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,wDAAwD;IACxD,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,6BAA6B;IAC7B,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,gDAAgD;IAChD,YAAY,CAAC,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAC;CACxC;AAqBD;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,EAC3B,OAAO,EACP,KAAK,EAAE,SAAS,EAChB,YAAY,EACZ,QAAQ,EACR,WAAsB,EACtB,eAAe,EACf,gBAAuB,EACvB,OAAe,EACf,OAAqB,EACrB,IAAW,EACX,SAAiB,EACjB,QAAgB,EAChB,YAAyD,EACzD,UAAoD,EACpD,MAAU,EACV,MAAM,EACN,eAAe,EACf,SAAS,EACT,YAAY,EACZ,aAAoB,EACpB,IAAI,EACJ,YAAY,GACb,EAAE,iBAAiB,2CAmEnB"}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { ImageSourcePropType } from 'react-native';
|
|
2
|
+
import { type DropdownEntry } from '../Dropdown/Dropdown';
|
|
3
|
+
import { type UserChipSize } from '../UserChip/UserChip';
|
|
4
|
+
import { type EdgeInsets, type MenuOrigin } from './Menu';
|
|
5
|
+
export interface UserMenuProps {
|
|
6
|
+
/** The menu rows (see `Dropdown`). */
|
|
7
|
+
options: DropdownEntry[];
|
|
8
|
+
/** Called with the key of the selected row. */
|
|
9
|
+
onSelect?: (key: string) => void;
|
|
10
|
+
/** Avatar image for the trigger. */
|
|
11
|
+
source?: ImageSourcePropType;
|
|
12
|
+
/** Name for the trigger. With a name the trigger is the `UserChip` pill; without it, just the avatar. */
|
|
13
|
+
name?: string;
|
|
14
|
+
/** Explicit avatar initials. */
|
|
15
|
+
initials?: string;
|
|
16
|
+
/** Trigger size. Defaults to `md`. */
|
|
17
|
+
size?: UserChipSize;
|
|
18
|
+
/** Trigger pill background (when a name is shown). */
|
|
19
|
+
chipBackgroundColor?: string;
|
|
20
|
+
/** Trigger text color (when a name is shown). */
|
|
21
|
+
chipColor?: string;
|
|
22
|
+
/** Point on the trigger the menu latches to. Defaults to `{ bottom, left }`. */
|
|
23
|
+
anchorOrigin?: MenuOrigin;
|
|
24
|
+
/** Point on the menu placed at the anchor. Defaults to `{ top, left }`. */
|
|
25
|
+
menuOrigin?: MenuOrigin;
|
|
26
|
+
/** Gap between trigger and menu. Defaults to 8. */
|
|
27
|
+
offset?: number;
|
|
28
|
+
/** Safe-area insets to keep clear. */
|
|
29
|
+
insets?: EdgeInsets;
|
|
30
|
+
/** Status-bar coordinate offset for the menu (see `Menu`). Pass 0 when immersive. */
|
|
31
|
+
statusBarHeight?: number;
|
|
32
|
+
/** Menu width. Defaults to the `Dropdown` default (350). */
|
|
33
|
+
menuWidth?: number;
|
|
34
|
+
/** Draw dividers between rows. Defaults to the `Dropdown` default (true). */
|
|
35
|
+
showDividers?: boolean;
|
|
36
|
+
/** Close the menu after a row is selected. Defaults to true. */
|
|
37
|
+
closeOnSelect?: boolean;
|
|
38
|
+
/** Controlled open state. */
|
|
39
|
+
open?: boolean;
|
|
40
|
+
/** Notified whenever the open state changes. */
|
|
41
|
+
onOpenChange?: (open: boolean) => void;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* User menu — an avatar / `UserChip` that opens an anchored `Dropdown` (e.g. an
|
|
45
|
+
* account menu). A composed component built from `UserChip` + `Menu` +
|
|
46
|
+
* `Dropdown`. Pass a `name` to use the labelled pill as the trigger, or omit it
|
|
47
|
+
* for just the avatar. Dismisses on outside press / back button.
|
|
48
|
+
*/
|
|
49
|
+
export declare function UserMenu({ options, onSelect, source, name, initials, size, chipBackgroundColor, chipColor, anchorOrigin, menuOrigin, offset, insets, statusBarHeight, menuWidth, showDividers, closeOnSelect, open, onOpenChange, }: UserMenuProps): import("react/jsx-runtime").JSX.Element;
|
|
50
|
+
//# sourceMappingURL=UserMenu.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"UserMenu.d.ts","sourceRoot":"","sources":["../../../../../src/components/Menu/UserMenu.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,cAAc,CAAC;AACxD,OAAO,EAAY,KAAK,aAAa,EAAE,MAAM,sBAAsB,CAAC;AACpE,OAAO,EAAY,KAAK,YAAY,EAAE,MAAM,sBAAsB,CAAC;AACnE,OAAO,EAAQ,KAAK,UAAU,EAAE,KAAK,UAAU,EAAE,MAAM,QAAQ,CAAC;AAEhE,MAAM,WAAW,aAAa;IAC5B,sCAAsC;IACtC,OAAO,EAAE,aAAa,EAAE,CAAC;IACzB,+CAA+C;IAC/C,QAAQ,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;IAEjC,oCAAoC;IACpC,MAAM,CAAC,EAAE,mBAAmB,CAAC;IAC7B,yGAAyG;IACzG,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,gCAAgC;IAChC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,sCAAsC;IACtC,IAAI,CAAC,EAAE,YAAY,CAAC;IACpB,sDAAsD;IACtD,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,iDAAiD;IACjD,SAAS,CAAC,EAAE,MAAM,CAAC;IAEnB,gFAAgF;IAChF,YAAY,CAAC,EAAE,UAAU,CAAC;IAC1B,2EAA2E;IAC3E,UAAU,CAAC,EAAE,UAAU,CAAC;IACxB,mDAAmD;IACnD,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,sCAAsC;IACtC,MAAM,CAAC,EAAE,UAAU,CAAC;IACpB,qFAAqF;IACrF,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,4DAA4D;IAC5D,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,6EAA6E;IAC7E,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,gEAAgE;IAChE,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,6BAA6B;IAC7B,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,gDAAgD;IAChD,YAAY,CAAC,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAC;CACxC;AAED;;;;;GAKG;AACH,wBAAgB,QAAQ,CAAC,EACvB,OAAO,EACP,QAAQ,EACR,MAAM,EACN,IAAI,EACJ,QAAQ,EACR,IAAW,EACX,mBAAmB,EACnB,SAAS,EACT,YAAyD,EACzD,UAAoD,EACpD,MAAU,EACV,MAAM,EACN,eAAe,EACf,SAAS,EACT,YAAY,EACZ,aAAoB,EACpB,IAAI,EACJ,YAAY,GACb,EAAE,aAAa,2CAoCf"}
|
|
@@ -4,4 +4,6 @@ export { MenuButton } from './MenuButton';
|
|
|
4
4
|
export type { MenuButtonProps } from './MenuButton';
|
|
5
5
|
export { SelectButton } from './SelectButton';
|
|
6
6
|
export type { SelectButtonProps, SelectButtonOption } from './SelectButton';
|
|
7
|
+
export { UserMenu } from './UserMenu';
|
|
8
|
+
export type { UserMenuProps } from './UserMenu';
|
|
7
9
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../src/components/Menu/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAC;AAC9B,YAAY,EACV,SAAS,EACT,UAAU,EACV,gBAAgB,EAChB,cAAc,EACd,gBAAgB,GACjB,MAAM,QAAQ,CAAC;AAChB,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAC1C,YAAY,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AACpD,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAC9C,YAAY,EAAE,iBAAiB,EAAE,kBAAkB,EAAE,MAAM,gBAAgB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../src/components/Menu/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAC;AAC9B,YAAY,EACV,SAAS,EACT,UAAU,EACV,gBAAgB,EAChB,cAAc,EACd,gBAAgB,GACjB,MAAM,QAAQ,CAAC;AAChB,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAC1C,YAAY,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AACpD,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAC9C,YAAY,EAAE,iBAAiB,EAAE,kBAAkB,EAAE,MAAM,gBAAgB,CAAC;AAC5E,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AACtC,YAAY,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC"}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { EdgeInsets, MenuOrigin } from './Menu';
|
|
2
|
+
export interface PlacementInput {
|
|
3
|
+
/** Trigger rect in window coordinates (as returned by `measureInWindow`). */
|
|
4
|
+
rect: {
|
|
5
|
+
x: number;
|
|
6
|
+
y: number;
|
|
7
|
+
width: number;
|
|
8
|
+
height: number;
|
|
9
|
+
};
|
|
10
|
+
/** Measured menu size (0 before the first layout). */
|
|
11
|
+
menuWidth: number;
|
|
12
|
+
menuHeight: number;
|
|
13
|
+
/** The content's natural (uncapped) height — drives the flip / cap decisions. */
|
|
14
|
+
naturalHeight: number;
|
|
15
|
+
anchorOrigin: MenuOrigin;
|
|
16
|
+
menuOrigin: MenuOrigin;
|
|
17
|
+
offset: number;
|
|
18
|
+
screenPadding: number;
|
|
19
|
+
insets: EdgeInsets;
|
|
20
|
+
/** Window size (`Dimensions.get('window')`). */
|
|
21
|
+
screenWidth: number;
|
|
22
|
+
screenHeight: number;
|
|
23
|
+
/**
|
|
24
|
+
* Difference between the window's coordinate origin (where `measureInWindow`
|
|
25
|
+
* reports) and the full-screen Modal's origin. On Android with a translucent
|
|
26
|
+
* status bar this is the status bar height; 0 on iOS / web / immersive.
|
|
27
|
+
*/
|
|
28
|
+
statusBarHeight: number;
|
|
29
|
+
autoFit: boolean;
|
|
30
|
+
}
|
|
31
|
+
export interface Placement {
|
|
32
|
+
top: number;
|
|
33
|
+
left: number;
|
|
34
|
+
/** Auto-fit cap for the content so it scrolls instead of overflowing. */
|
|
35
|
+
maxHeight?: number;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Pure placement math for `Menu`. Kept side-effect-free so it can be unit-tested
|
|
39
|
+
* exhaustively (status-bar offset, flip, cap, clamp) independent of RN layout.
|
|
40
|
+
*
|
|
41
|
+
* The key correctness point: the trigger is measured in *window* coordinates but
|
|
42
|
+
* the menu is painted in a full-screen (`statusBarTranslucent`) Modal, so we add
|
|
43
|
+
* `statusBarHeight` when converting the trigger position into the Modal's space.
|
|
44
|
+
* Available-space math cancels the offset out, so it uses window coordinates.
|
|
45
|
+
*/
|
|
46
|
+
export declare function computeMenuPlacement(input: PlacementInput): Placement;
|
|
47
|
+
//# sourceMappingURL=placement.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"placement.d.ts","sourceRoot":"","sources":["../../../../../src/components/Menu/placement.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,QAAQ,CAAC;AAKrD,MAAM,WAAW,cAAc;IAC7B,6EAA6E;IAC7E,IAAI,EAAE;QAAE,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;IAC9D,sDAAsD;IACtD,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,iFAAiF;IACjF,aAAa,EAAE,MAAM,CAAC;IACtB,YAAY,EAAE,UAAU,CAAC;IACzB,UAAU,EAAE,UAAU,CAAC;IACvB,MAAM,EAAE,MAAM,CAAC;IACf,aAAa,EAAE,MAAM,CAAC;IACtB,MAAM,EAAE,UAAU,CAAC;IACnB,gDAAgD;IAChD,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,CAAC;IACrB;;;;OAIG;IACH,eAAe,EAAE,MAAM,CAAC;IACxB,OAAO,EAAE,OAAO,CAAC;CAClB;AAED,MAAM,WAAW,SAAS;IACxB,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,yEAAyE;IACzE,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAOD;;;;;;;;GAQG;AACH,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,cAAc,GAAG,SAAS,CAuDrE"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"placement.test.d.ts","sourceRoot":"","sources":["../../../../../src/components/Menu/placement.test.ts"],"names":[],"mappings":""}
|
|
@@ -57,6 +57,8 @@ export interface SelectProps {
|
|
|
57
57
|
autoFit?: boolean;
|
|
58
58
|
/** Safe-area insets to keep clear. */
|
|
59
59
|
insets?: EdgeInsets;
|
|
60
|
+
/** Status-bar coordinate offset for the menu (see `Menu`). Pass 0 when immersive. */
|
|
61
|
+
statusBarHeight?: number;
|
|
60
62
|
/** Close after selecting. Defaults to `!multiple`. */
|
|
61
63
|
closeOnSelect?: boolean;
|
|
62
64
|
/** Container style. */
|
|
@@ -69,5 +71,5 @@ export interface SelectProps {
|
|
|
69
71
|
* current option is marked with a check. With `multiple`, rows become checkboxes,
|
|
70
72
|
* the value is an array, and the menu stays open while picking.
|
|
71
73
|
*/
|
|
72
|
-
export declare function Select({ options, multiple, value, defaultValue, onChange, label, helperText, error, placeholder, leftIcon, size, width, fullWidth, disabled, renderValue, matchTriggerWidth, menuWidth, maxHeight, anchorOrigin, menuOrigin, offset, autoFit, insets, closeOnSelect, style, }: SelectProps): import("react/jsx-runtime").JSX.Element;
|
|
74
|
+
export declare function Select({ options, multiple, value, defaultValue, onChange, label, helperText, error, placeholder, leftIcon, size, width, fullWidth, disabled, renderValue, matchTriggerWidth, menuWidth, maxHeight, anchorOrigin, menuOrigin, offset, autoFit, insets, statusBarHeight, closeOnSelect, style, }: SelectProps): import("react/jsx-runtime").JSX.Element;
|
|
73
75
|
//# sourceMappingURL=Select.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Select.d.ts","sourceRoot":"","sources":["../../../../../src/components/Select/Select.tsx"],"names":[],"mappings":"AACA,OAAO,EAGL,KAAK,SAAS,EAEd,KAAK,SAAS,EACf,MAAM,cAAc,CAAC;AAKtB,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,yBAAyB,CAAC;AACxD,OAAO,EAAQ,KAAK,UAAU,EAAE,KAAK,UAAU,EAAE,MAAM,cAAc,CAAC;AAiBtE,MAAM,MAAM,UAAU,GAAG,IAAI,GAAG,IAAI,CAAC;AAUrC,+BAA+B;AAC/B,MAAM,WAAW,YAAY;IAC3B,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;IACd,4CAA4C;IAC5C,IAAI,CAAC,EAAE,QAAQ,CAAC;IAChB,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED,MAAM,WAAW,WAAW;IAC1B,kCAAkC;IAClC,OAAO,EAAE,YAAY,EAAE,CAAC;IACxB,yEAAyE;IACzE,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,8EAA8E;IAC9E,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;IAC1B,mDAAmD;IACnD,YAAY,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;IACjC,gFAAgF;IAChF,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,KAAK,IAAI,CAAC;IAE9C,6BAA6B;IAC7B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,2FAA2F;IAC3F,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,mEAAmE;IACnE,KAAK,CAAC,EAAE,OAAO,GAAG,MAAM,CAAC;IACzB,wEAAwE;IACxE,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,uCAAuC;IACvC,QAAQ,CAAC,EAAE,QAAQ,CAAC;IACpB,4DAA4D;IAC5D,IAAI,CAAC,EAAE,UAAU,CAAC;IAClB,sEAAsE;IACtE,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,8FAA8F;IAC9F,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,yBAAyB;IACzB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,gDAAgD;IAChD,WAAW,CAAC,EAAE,CAAC,QAAQ,EAAE,YAAY,EAAE,KAAK,MAAM,CAAC;IAEnD,iEAAiE;IACjE,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B,oDAAoD;IACpD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,0CAA0C;IAC1C,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,8EAA8E;IAC9E,YAAY,CAAC,EAAE,UAAU,CAAC;IAC1B,2EAA2E;IAC3E,UAAU,CAAC,EAAE,UAAU,CAAC;IACxB,iDAAiD;IACjD,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,uFAAuF;IACvF,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,sCAAsC;IACtC,MAAM,CAAC,EAAE,UAAU,CAAC;IACpB,sDAAsD;IACtD,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,uBAAuB;IACvB,KAAK,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;CAC9B;AAED;;;;;;GAMG;AACH,wBAAgB,MAAM,CAAC,EACrB,OAAO,EACP,QAAgB,EAChB,KAAK,EACL,YAAY,EACZ,QAAQ,EACR,KAAK,EACL,UAAU,EACV,KAAK,EACL,WAA6B,EAC7B,QAAQ,EACR,IAAW,EACX,KAAK,EACL,SAAgB,EAChB,QAAgB,EAChB,WAAW,EACX,iBAAwB,EACxB,SAAS,EACT,SAAS,EACT,YAAyD,EACzD,UAAoD,EACpD,MAAU,EACV,OAAc,EACd,MAAM,EACN,aAAa,EACb,KAAK,GACN,EAAE,WAAW,
|
|
1
|
+
{"version":3,"file":"Select.d.ts","sourceRoot":"","sources":["../../../../../src/components/Select/Select.tsx"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -6,11 +6,13 @@ import {
|
|
|
6
6
|
Modal,
|
|
7
7
|
Platform,
|
|
8
8
|
Pressable,
|
|
9
|
+
StatusBar,
|
|
9
10
|
View,
|
|
10
11
|
type LayoutChangeEvent,
|
|
11
12
|
type StyleProp,
|
|
12
13
|
type ViewStyle,
|
|
13
14
|
} from 'react-native';
|
|
15
|
+
import { computeMenuPlacement } from './placement';
|
|
14
16
|
|
|
15
17
|
export type OriginVertical = 'top' | 'center' | 'bottom';
|
|
16
18
|
export type OriginHorizontal = 'left' | 'center' | 'right';
|
|
@@ -21,9 +23,6 @@ export interface MenuOrigin {
|
|
|
21
23
|
horizontal: OriginHorizontal;
|
|
22
24
|
}
|
|
23
25
|
|
|
24
|
-
const V_FRAC: Record<OriginVertical, number> = { top: 0, center: 0.5, bottom: 1 };
|
|
25
|
-
const H_FRAC: Record<OriginHorizontal, number> = { left: 0, center: 0.5, right: 1 };
|
|
26
|
-
|
|
27
26
|
/** State handed to the trigger renderer and (optionally) the content renderer. */
|
|
28
27
|
export interface MenuTriggerState {
|
|
29
28
|
open: boolean;
|
|
@@ -56,6 +55,12 @@ export interface MenuProps {
|
|
|
56
55
|
autoFit?: boolean;
|
|
57
56
|
/** Safe-area insets to keep clear (pass from `react-native-safe-area-context` if you use it). */
|
|
58
57
|
insets?: EdgeInsets;
|
|
58
|
+
/**
|
|
59
|
+
* Offset between window coordinates (where the trigger is measured) and the
|
|
60
|
+
* full-screen Modal the menu paints into. Defaults to the Android status bar
|
|
61
|
+
* height (0 on iOS / web). Pass 0 when the host runs edge-to-edge / immersive.
|
|
62
|
+
*/
|
|
63
|
+
statusBarHeight?: number;
|
|
59
64
|
/** Controlled open state. Omit for uncontrolled. */
|
|
60
65
|
open?: boolean;
|
|
61
66
|
/** Notified whenever the open state changes. */
|
|
@@ -75,6 +80,9 @@ export interface EdgeInsets {
|
|
|
75
80
|
|
|
76
81
|
const ZERO_INSETS: EdgeInsets = { top: 0, right: 0, bottom: 0, left: 0 };
|
|
77
82
|
|
|
83
|
+
/** Default coordinate offset for the translucent Modal (Android status bar). */
|
|
84
|
+
const DEFAULT_STATUS_BAR_HEIGHT = Platform.OS === 'android' ? StatusBar.currentHeight ?? 0 : 0;
|
|
85
|
+
|
|
78
86
|
interface Rect {
|
|
79
87
|
x: number;
|
|
80
88
|
y: number;
|
|
@@ -82,11 +90,6 @@ interface Rect {
|
|
|
82
90
|
height: number;
|
|
83
91
|
}
|
|
84
92
|
|
|
85
|
-
function clamp(value: number, min: number, max: number): number {
|
|
86
|
-
if (max < min) return min;
|
|
87
|
-
return Math.min(Math.max(value, min), max);
|
|
88
|
-
}
|
|
89
|
-
|
|
90
93
|
/**
|
|
91
94
|
* Anchored overlay menu. Renders a trigger inline and, when open, a floating
|
|
92
95
|
* panel positioned relative to the trigger via two origins — `anchorOrigin`
|
|
@@ -102,6 +105,7 @@ export function Menu({
|
|
|
102
105
|
offset = 8,
|
|
103
106
|
autoFit = true,
|
|
104
107
|
insets = ZERO_INSETS,
|
|
108
|
+
statusBarHeight = DEFAULT_STATUS_BAR_HEIGHT,
|
|
105
109
|
open: openProp,
|
|
106
110
|
onOpenChange,
|
|
107
111
|
screenPadding = 8,
|
|
@@ -161,44 +165,31 @@ export function Menu({
|
|
|
161
165
|
if (naturalHeight === null) setNaturalHeight(height);
|
|
162
166
|
};
|
|
163
167
|
|
|
164
|
-
//
|
|
165
|
-
//
|
|
166
|
-
|
|
167
|
-
const
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
let top = 0;
|
|
190
|
-
let left = 0;
|
|
191
|
-
if (rect && menuSize) {
|
|
192
|
-
const screen = Dimensions.get('window');
|
|
193
|
-
const anchorX = rect.x + H_FRAC[effAnchor.horizontal] * rect.width;
|
|
194
|
-
const anchorY = rect.y + V_FRAC[effAnchor.vertical] * rect.height;
|
|
195
|
-
const offsetY =
|
|
196
|
-
effMenu.vertical === 'top' ? offset : effMenu.vertical === 'bottom' ? -offset : 0;
|
|
197
|
-
left = anchorX - H_FRAC[effMenu.horizontal] * menuSize.width;
|
|
198
|
-
top = anchorY - V_FRAC[effMenu.vertical] * menuSize.height + offsetY;
|
|
199
|
-
left = clamp(left, screenPadding + insets.left, screen.width - insets.right - menuSize.width - screenPadding);
|
|
200
|
-
top = clamp(top, screenPadding + insets.top, screen.height - insets.bottom - menuSize.height - screenPadding);
|
|
201
|
-
}
|
|
168
|
+
// All positioning math lives in the pure `computeMenuPlacement` (unit-tested):
|
|
169
|
+
// it applies the status-bar offset, auto-fit flip, height cap and screen clamp.
|
|
170
|
+
const screen = Dimensions.get('window');
|
|
171
|
+
const placement =
|
|
172
|
+
rect && naturalHeight !== null
|
|
173
|
+
? computeMenuPlacement({
|
|
174
|
+
rect,
|
|
175
|
+
menuWidth: menuSize?.width ?? 0,
|
|
176
|
+
menuHeight: menuSize?.height ?? naturalHeight,
|
|
177
|
+
naturalHeight,
|
|
178
|
+
anchorOrigin,
|
|
179
|
+
menuOrigin,
|
|
180
|
+
offset,
|
|
181
|
+
screenPadding,
|
|
182
|
+
insets,
|
|
183
|
+
screenWidth: screen.width,
|
|
184
|
+
screenHeight: screen.height,
|
|
185
|
+
statusBarHeight,
|
|
186
|
+
autoFit,
|
|
187
|
+
})
|
|
188
|
+
: null;
|
|
189
|
+
|
|
190
|
+
const top = placement?.top ?? 0;
|
|
191
|
+
const left = placement?.left ?? 0;
|
|
192
|
+
const autoMaxHeight = placement?.maxHeight;
|
|
202
193
|
|
|
203
194
|
const placed = Boolean(rect && menuSize);
|
|
204
195
|
const content =
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Dropdown, type DropdownEntry } from '../Dropdown/Dropdown';
|
|
2
2
|
import { IconButton, type IconButtonShape, type IconButtonSize, type IconButtonVariant } from '../IconButton/IconButton';
|
|
3
3
|
import type { IconName } from '../Icon/icons.generated';
|
|
4
|
-
import { Menu, type MenuOrigin } from './Menu';
|
|
4
|
+
import { Menu, type EdgeInsets, type MenuOrigin } from './Menu';
|
|
5
5
|
|
|
6
6
|
export interface MenuButtonProps {
|
|
7
7
|
/** The menu rows (see `Dropdown`). */
|
|
@@ -26,6 +26,10 @@ export interface MenuButtonProps {
|
|
|
26
26
|
menuOrigin?: MenuOrigin;
|
|
27
27
|
/** Gap between trigger and menu. Defaults to 8. */
|
|
28
28
|
offset?: number;
|
|
29
|
+
/** Safe-area insets to keep clear. */
|
|
30
|
+
insets?: EdgeInsets;
|
|
31
|
+
/** Status-bar coordinate offset for the menu (see `Menu`). Pass 0 when immersive. */
|
|
32
|
+
statusBarHeight?: number;
|
|
29
33
|
/** Menu width. Defaults to the `Dropdown` default (350). */
|
|
30
34
|
menuWidth?: number;
|
|
31
35
|
/** Draw dividers between rows. Defaults to the `Dropdown` default (true). */
|
|
@@ -56,6 +60,8 @@ export function MenuButton({
|
|
|
56
60
|
anchorOrigin = { vertical: 'bottom', horizontal: 'right' },
|
|
57
61
|
menuOrigin = { vertical: 'top', horizontal: 'right' },
|
|
58
62
|
offset = 8,
|
|
63
|
+
insets,
|
|
64
|
+
statusBarHeight,
|
|
59
65
|
menuWidth,
|
|
60
66
|
showDividers,
|
|
61
67
|
closeOnSelect = true,
|
|
@@ -67,6 +73,8 @@ export function MenuButton({
|
|
|
67
73
|
anchorOrigin={anchorOrigin}
|
|
68
74
|
menuOrigin={menuOrigin}
|
|
69
75
|
offset={offset}
|
|
76
|
+
insets={insets}
|
|
77
|
+
statusBarHeight={statusBarHeight}
|
|
70
78
|
open={open}
|
|
71
79
|
onOpenChange={onOpenChange}
|
|
72
80
|
renderTrigger={({ toggle }) => (
|
|
@@ -4,7 +4,7 @@ import { Button, type ButtonSize, type ButtonVariant } from '../Button/Button';
|
|
|
4
4
|
import { Dropdown, type DropdownEntry } from '../Dropdown/Dropdown';
|
|
5
5
|
import { Icon } from '../Icon/Icon';
|
|
6
6
|
import type { IconName } from '../Icon/icons.generated';
|
|
7
|
-
import { Menu, type MenuOrigin } from './Menu';
|
|
7
|
+
import { Menu, type EdgeInsets, type MenuOrigin } from './Menu';
|
|
8
8
|
|
|
9
9
|
/** A selectable option. Its `icon` shows both in the row and on the trigger when chosen. */
|
|
10
10
|
export interface SelectButtonOption {
|
|
@@ -45,6 +45,10 @@ export interface SelectButtonProps {
|
|
|
45
45
|
menuOrigin?: MenuOrigin;
|
|
46
46
|
/** Gap between trigger and menu. Defaults to 8. */
|
|
47
47
|
offset?: number;
|
|
48
|
+
/** Safe-area insets to keep clear. */
|
|
49
|
+
insets?: EdgeInsets;
|
|
50
|
+
/** Status-bar coordinate offset for the menu (see `Menu`). Pass 0 when immersive. */
|
|
51
|
+
statusBarHeight?: number;
|
|
48
52
|
/** Menu width. Defaults to the `Dropdown` default (350). */
|
|
49
53
|
menuWidth?: number;
|
|
50
54
|
/** Draw dividers between rows. Defaults to the `Dropdown` default (true). */
|
|
@@ -98,6 +102,8 @@ export function SelectButton({
|
|
|
98
102
|
anchorOrigin = { vertical: 'bottom', horizontal: 'left' },
|
|
99
103
|
menuOrigin = { vertical: 'top', horizontal: 'left' },
|
|
100
104
|
offset = 8,
|
|
105
|
+
insets,
|
|
106
|
+
statusBarHeight,
|
|
101
107
|
menuWidth,
|
|
102
108
|
showDividers,
|
|
103
109
|
closeOnSelect = true,
|
|
@@ -134,6 +140,8 @@ export function SelectButton({
|
|
|
134
140
|
anchorOrigin={anchorOrigin}
|
|
135
141
|
menuOrigin={menuOrigin}
|
|
136
142
|
offset={offset}
|
|
143
|
+
insets={insets}
|
|
144
|
+
statusBarHeight={statusBarHeight}
|
|
137
145
|
open={open}
|
|
138
146
|
onOpenChange={onOpenChange}
|
|
139
147
|
triggerStyle={fullWidth ? { alignSelf: 'stretch' } : undefined}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import type { ImageSourcePropType } from 'react-native';
|
|
2
|
+
import { Dropdown, type DropdownEntry } from '../Dropdown/Dropdown';
|
|
3
|
+
import { UserChip, type UserChipSize } from '../UserChip/UserChip';
|
|
4
|
+
import { Menu, type EdgeInsets, type MenuOrigin } from './Menu';
|
|
5
|
+
|
|
6
|
+
export interface UserMenuProps {
|
|
7
|
+
/** The menu rows (see `Dropdown`). */
|
|
8
|
+
options: DropdownEntry[];
|
|
9
|
+
/** Called with the key of the selected row. */
|
|
10
|
+
onSelect?: (key: string) => void;
|
|
11
|
+
|
|
12
|
+
/** Avatar image for the trigger. */
|
|
13
|
+
source?: ImageSourcePropType;
|
|
14
|
+
/** Name for the trigger. With a name the trigger is the `UserChip` pill; without it, just the avatar. */
|
|
15
|
+
name?: string;
|
|
16
|
+
/** Explicit avatar initials. */
|
|
17
|
+
initials?: string;
|
|
18
|
+
/** Trigger size. Defaults to `md`. */
|
|
19
|
+
size?: UserChipSize;
|
|
20
|
+
/** Trigger pill background (when a name is shown). */
|
|
21
|
+
chipBackgroundColor?: string;
|
|
22
|
+
/** Trigger text color (when a name is shown). */
|
|
23
|
+
chipColor?: string;
|
|
24
|
+
|
|
25
|
+
/** Point on the trigger the menu latches to. Defaults to `{ bottom, left }`. */
|
|
26
|
+
anchorOrigin?: MenuOrigin;
|
|
27
|
+
/** Point on the menu placed at the anchor. Defaults to `{ top, left }`. */
|
|
28
|
+
menuOrigin?: MenuOrigin;
|
|
29
|
+
/** Gap between trigger and menu. Defaults to 8. */
|
|
30
|
+
offset?: number;
|
|
31
|
+
/** Safe-area insets to keep clear. */
|
|
32
|
+
insets?: EdgeInsets;
|
|
33
|
+
/** Status-bar coordinate offset for the menu (see `Menu`). Pass 0 when immersive. */
|
|
34
|
+
statusBarHeight?: number;
|
|
35
|
+
/** Menu width. Defaults to the `Dropdown` default (350). */
|
|
36
|
+
menuWidth?: number;
|
|
37
|
+
/** Draw dividers between rows. Defaults to the `Dropdown` default (true). */
|
|
38
|
+
showDividers?: boolean;
|
|
39
|
+
/** Close the menu after a row is selected. Defaults to true. */
|
|
40
|
+
closeOnSelect?: boolean;
|
|
41
|
+
/** Controlled open state. */
|
|
42
|
+
open?: boolean;
|
|
43
|
+
/** Notified whenever the open state changes. */
|
|
44
|
+
onOpenChange?: (open: boolean) => void;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* User menu — an avatar / `UserChip` that opens an anchored `Dropdown` (e.g. an
|
|
49
|
+
* account menu). A composed component built from `UserChip` + `Menu` +
|
|
50
|
+
* `Dropdown`. Pass a `name` to use the labelled pill as the trigger, or omit it
|
|
51
|
+
* for just the avatar. Dismisses on outside press / back button.
|
|
52
|
+
*/
|
|
53
|
+
export function UserMenu({
|
|
54
|
+
options,
|
|
55
|
+
onSelect,
|
|
56
|
+
source,
|
|
57
|
+
name,
|
|
58
|
+
initials,
|
|
59
|
+
size = 'md',
|
|
60
|
+
chipBackgroundColor,
|
|
61
|
+
chipColor,
|
|
62
|
+
anchorOrigin = { vertical: 'bottom', horizontal: 'left' },
|
|
63
|
+
menuOrigin = { vertical: 'top', horizontal: 'left' },
|
|
64
|
+
offset = 8,
|
|
65
|
+
insets,
|
|
66
|
+
statusBarHeight,
|
|
67
|
+
menuWidth,
|
|
68
|
+
showDividers,
|
|
69
|
+
closeOnSelect = true,
|
|
70
|
+
open,
|
|
71
|
+
onOpenChange,
|
|
72
|
+
}: UserMenuProps) {
|
|
73
|
+
return (
|
|
74
|
+
<Menu
|
|
75
|
+
anchorOrigin={anchorOrigin}
|
|
76
|
+
menuOrigin={menuOrigin}
|
|
77
|
+
offset={offset}
|
|
78
|
+
insets={insets}
|
|
79
|
+
statusBarHeight={statusBarHeight}
|
|
80
|
+
open={open}
|
|
81
|
+
onOpenChange={onOpenChange}
|
|
82
|
+
renderTrigger={({ toggle }) => (
|
|
83
|
+
<UserChip
|
|
84
|
+
source={source}
|
|
85
|
+
name={name}
|
|
86
|
+
initials={initials}
|
|
87
|
+
size={size}
|
|
88
|
+
backgroundColor={chipBackgroundColor}
|
|
89
|
+
color={chipColor}
|
|
90
|
+
onPress={toggle}
|
|
91
|
+
/>
|
|
92
|
+
)}
|
|
93
|
+
>
|
|
94
|
+
{({ close, maxHeight }) => (
|
|
95
|
+
<Dropdown
|
|
96
|
+
options={options}
|
|
97
|
+
width={menuWidth}
|
|
98
|
+
maxHeight={maxHeight}
|
|
99
|
+
showDividers={showDividers}
|
|
100
|
+
onSelect={(key) => {
|
|
101
|
+
onSelect?.(key);
|
|
102
|
+
if (closeOnSelect) close();
|
|
103
|
+
}}
|
|
104
|
+
/>
|
|
105
|
+
)}
|
|
106
|
+
</Menu>
|
|
107
|
+
);
|
|
108
|
+
}
|
|
@@ -10,3 +10,5 @@ export { MenuButton } from './MenuButton';
|
|
|
10
10
|
export type { MenuButtonProps } from './MenuButton';
|
|
11
11
|
export { SelectButton } from './SelectButton';
|
|
12
12
|
export type { SelectButtonProps, SelectButtonOption } from './SelectButton';
|
|
13
|
+
export { UserMenu } from './UserMenu';
|
|
14
|
+
export type { UserMenuProps } from './UserMenu';
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import type { EdgeInsets, MenuOrigin } from './Menu';
|
|
2
|
+
|
|
3
|
+
const V_FRAC = { top: 0, center: 0.5, bottom: 1 } as const;
|
|
4
|
+
const H_FRAC = { left: 0, center: 0.5, right: 1 } as const;
|
|
5
|
+
|
|
6
|
+
export interface PlacementInput {
|
|
7
|
+
/** Trigger rect in window coordinates (as returned by `measureInWindow`). */
|
|
8
|
+
rect: { x: number; y: number; width: number; height: number };
|
|
9
|
+
/** Measured menu size (0 before the first layout). */
|
|
10
|
+
menuWidth: number;
|
|
11
|
+
menuHeight: number;
|
|
12
|
+
/** The content's natural (uncapped) height — drives the flip / cap decisions. */
|
|
13
|
+
naturalHeight: number;
|
|
14
|
+
anchorOrigin: MenuOrigin;
|
|
15
|
+
menuOrigin: MenuOrigin;
|
|
16
|
+
offset: number;
|
|
17
|
+
screenPadding: number;
|
|
18
|
+
insets: EdgeInsets;
|
|
19
|
+
/** Window size (`Dimensions.get('window')`). */
|
|
20
|
+
screenWidth: number;
|
|
21
|
+
screenHeight: number;
|
|
22
|
+
/**
|
|
23
|
+
* Difference between the window's coordinate origin (where `measureInWindow`
|
|
24
|
+
* reports) and the full-screen Modal's origin. On Android with a translucent
|
|
25
|
+
* status bar this is the status bar height; 0 on iOS / web / immersive.
|
|
26
|
+
*/
|
|
27
|
+
statusBarHeight: number;
|
|
28
|
+
autoFit: boolean;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface Placement {
|
|
32
|
+
top: number;
|
|
33
|
+
left: number;
|
|
34
|
+
/** Auto-fit cap for the content so it scrolls instead of overflowing. */
|
|
35
|
+
maxHeight?: number;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function clamp(value: number, min: number, max: number): number {
|
|
39
|
+
if (max < min) return min;
|
|
40
|
+
return Math.min(Math.max(value, min), max);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Pure placement math for `Menu`. Kept side-effect-free so it can be unit-tested
|
|
45
|
+
* exhaustively (status-bar offset, flip, cap, clamp) independent of RN layout.
|
|
46
|
+
*
|
|
47
|
+
* The key correctness point: the trigger is measured in *window* coordinates but
|
|
48
|
+
* the menu is painted in a full-screen (`statusBarTranslucent`) Modal, so we add
|
|
49
|
+
* `statusBarHeight` when converting the trigger position into the Modal's space.
|
|
50
|
+
* Available-space math cancels the offset out, so it uses window coordinates.
|
|
51
|
+
*/
|
|
52
|
+
export function computeMenuPlacement(input: PlacementInput): Placement {
|
|
53
|
+
const {
|
|
54
|
+
rect,
|
|
55
|
+
menuWidth,
|
|
56
|
+
menuHeight,
|
|
57
|
+
naturalHeight,
|
|
58
|
+
anchorOrigin,
|
|
59
|
+
menuOrigin,
|
|
60
|
+
offset,
|
|
61
|
+
screenPadding,
|
|
62
|
+
insets,
|
|
63
|
+
screenWidth,
|
|
64
|
+
screenHeight,
|
|
65
|
+
statusBarHeight,
|
|
66
|
+
autoFit,
|
|
67
|
+
} = input;
|
|
68
|
+
|
|
69
|
+
const isDropConfig =
|
|
70
|
+
(anchorOrigin.vertical === 'bottom' && menuOrigin.vertical === 'top') ||
|
|
71
|
+
(anchorOrigin.vertical === 'top' && menuOrigin.vertical === 'bottom');
|
|
72
|
+
const preferredUp = anchorOrigin.vertical === 'top' && menuOrigin.vertical === 'bottom';
|
|
73
|
+
|
|
74
|
+
let effAnchor = anchorOrigin;
|
|
75
|
+
let effMenu = menuOrigin;
|
|
76
|
+
let maxHeight: number | undefined;
|
|
77
|
+
|
|
78
|
+
if (autoFit && isDropConfig) {
|
|
79
|
+
// Space math is in window coordinates (the status-bar offset cancels out).
|
|
80
|
+
const spaceBelow = screenHeight - insets.bottom - (rect.y + rect.height) - offset - screenPadding;
|
|
81
|
+
const spaceAbove = rect.y - insets.top - offset - screenPadding;
|
|
82
|
+
let up = preferredUp;
|
|
83
|
+
if (!preferredUp) up = naturalHeight > spaceBelow && spaceAbove > spaceBelow;
|
|
84
|
+
else up = !(naturalHeight > spaceAbove && spaceBelow > spaceAbove);
|
|
85
|
+
const chosen = up ? spaceAbove : spaceBelow;
|
|
86
|
+
if (naturalHeight > chosen && chosen > 0) maxHeight = chosen;
|
|
87
|
+
effAnchor = { horizontal: anchorOrigin.horizontal, vertical: up ? 'top' : 'bottom' };
|
|
88
|
+
effMenu = { horizontal: menuOrigin.horizontal, vertical: up ? 'bottom' : 'top' };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Convert the trigger position into the Modal's (full-screen) coordinate space.
|
|
92
|
+
const anchorX = rect.x + H_FRAC[effAnchor.horizontal] * rect.width;
|
|
93
|
+
const anchorY = rect.y + statusBarHeight + V_FRAC[effAnchor.vertical] * rect.height;
|
|
94
|
+
const offsetY = effMenu.vertical === 'top' ? offset : effMenu.vertical === 'bottom' ? -offset : 0;
|
|
95
|
+
|
|
96
|
+
let left = anchorX - H_FRAC[effMenu.horizontal] * menuWidth;
|
|
97
|
+
let top = anchorY - V_FRAC[effMenu.vertical] * menuHeight + offsetY;
|
|
98
|
+
|
|
99
|
+
left = clamp(left, screenPadding + insets.left, screenWidth - insets.right - menuWidth - screenPadding);
|
|
100
|
+
top = clamp(
|
|
101
|
+
top,
|
|
102
|
+
statusBarHeight + screenPadding + insets.top,
|
|
103
|
+
screenHeight + statusBarHeight - insets.bottom - menuHeight - screenPadding,
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
return { top, left, maxHeight };
|
|
107
|
+
}
|