@ankhorage/surface 2.0.0 → 2.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/components/action-sheet/ActionSheet.d.ts +4 -0
  3. package/dist/components/action-sheet/ActionSheet.d.ts.map +1 -0
  4. package/dist/components/action-sheet/ActionSheet.js +71 -0
  5. package/dist/components/action-sheet/ActionSheet.js.map +1 -0
  6. package/dist/components/action-sheet/ActionSheetItem.d.ts +4 -0
  7. package/dist/components/action-sheet/ActionSheetItem.d.ts.map +1 -0
  8. package/dist/components/action-sheet/ActionSheetItem.js +27 -0
  9. package/dist/components/action-sheet/ActionSheetItem.js.map +1 -0
  10. package/dist/components/action-sheet/index.d.ts +4 -0
  11. package/dist/components/action-sheet/index.d.ts.map +1 -0
  12. package/dist/components/action-sheet/index.js +3 -0
  13. package/dist/components/action-sheet/index.js.map +1 -0
  14. package/dist/components/action-sheet/types.d.ts +24 -0
  15. package/dist/components/action-sheet/types.d.ts.map +1 -0
  16. package/dist/components/action-sheet/types.js +2 -0
  17. package/dist/components/action-sheet/types.js.map +1 -0
  18. package/dist/components/menu/Menu.d.ts +1 -1
  19. package/dist/components/menu/Menu.d.ts.map +1 -1
  20. package/dist/components/menu/Menu.js +50 -32
  21. package/dist/components/menu/Menu.js.map +1 -1
  22. package/dist/components/menu/index.d.ts +1 -1
  23. package/dist/components/menu/index.d.ts.map +1 -1
  24. package/dist/components/menu/index.js.map +1 -1
  25. package/dist/components/menu/navigation.d.ts +1 -1
  26. package/dist/components/menu/navigation.d.ts.map +1 -1
  27. package/dist/components/menu/navigation.js.map +1 -1
  28. package/dist/components/menu/types.d.ts +11 -5
  29. package/dist/components/menu/types.d.ts.map +1 -1
  30. package/dist/components/menu/types.js.map +1 -1
  31. package/dist/examples/DocsExamples.d.ts +3 -2
  32. package/dist/examples/DocsExamples.d.ts.map +1 -1
  33. package/dist/examples/DocsExamples.js +61 -37
  34. package/dist/examples/DocsExamples.js.map +1 -1
  35. package/dist/index.d.ts +3 -1
  36. package/dist/index.d.ts.map +1 -1
  37. package/dist/index.js +1 -0
  38. package/dist/index.js.map +1 -1
  39. package/package.json +2 -2
  40. package/src/components/action-sheet/ActionSheet.tsx +121 -0
  41. package/src/components/action-sheet/ActionSheetItem.tsx +57 -0
  42. package/src/components/action-sheet/index.ts +3 -0
  43. package/src/components/action-sheet/types.ts +26 -0
  44. package/src/components/menu/Menu.tsx +71 -36
  45. package/src/components/menu/index.ts +1 -1
  46. package/src/components/menu/navigation.ts +1 -1
  47. package/src/components/menu/types.ts +12 -5
  48. package/src/examples/DocsExamples.tsx +74 -60
  49. package/src/index.ts +3 -1
@@ -5,12 +5,12 @@ import { FocusScope } from '../../internal/focus/FocusScope';
5
5
  import { useFocusManager } from '../../internal/focus/useFocusManager';
6
6
  import { Portal } from '../../internal/overlay/Portal';
7
7
  import { resolveOverlayAnimation } from '../../internal/resolvers';
8
- import { Box, Surface } from '../../layout';
8
+ import { Box, Inline, Stack, Surface } from '../../layout';
9
9
  import { ButtonBase } from '../../primitives/button-base';
10
10
  import { Text } from '../../primitives/text';
11
11
  import { useTheme } from '../../theme/ThemeContext';
12
12
  import { resolveNextMenuIndex } from './navigation';
13
- import type { MenuProps } from './types';
13
+ import type { MenuAction, MenuProps } from './types';
14
14
 
15
15
  interface MeasurableNode {
16
16
  measureInWindow?: (
@@ -25,7 +25,35 @@ function measureNode(node: unknown, callback: (layout: LayoutRectangle) => void)
25
25
  });
26
26
  }
27
27
 
28
- export function Menu({ trigger, items, onDismiss, closeOnSelect = true, testID }: MenuProps) {
28
+ function renderActionContent(action: MenuAction, active: boolean) {
29
+ const titleColor =
30
+ action.intent === 'danger' ? 'danger' : active || action.selected ? 'neutral' : undefined;
31
+
32
+ return (
33
+ <Inline align="center" gap="s" justify="space-between" wrap="nowrap">
34
+ {action.leading ? <Box>{action.leading}</Box> : null}
35
+ <Box flex={1}>
36
+ <Stack gap="xxs">
37
+ <Text
38
+ color={titleColor}
39
+ variant="bodySmall"
40
+ weight={action.selected ? 'semiBold' : 'medium'}
41
+ >
42
+ {action.title}
43
+ </Text>
44
+ {action.description ? (
45
+ <Text emphasis="muted" variant="caption">
46
+ {action.description}
47
+ </Text>
48
+ ) : null}
49
+ </Stack>
50
+ </Box>
51
+ {action.trailing ? <Box>{action.trailing}</Box> : null}
52
+ </Inline>
53
+ );
54
+ }
55
+
56
+ export function Menu({ trigger, actions, dismiss, closeOnSelect = true, testID }: MenuProps) {
29
57
  const { theme } = useTheme();
30
58
  const { bindKeydown } = useFocusManager();
31
59
  const animation = resolveOverlayAnimation('menu');
@@ -36,15 +64,34 @@ export function Menu({ trigger, items, onDismiss, closeOnSelect = true, testID }
36
64
 
37
65
  const closeMenu = React.useCallback(() => {
38
66
  setOpen(false);
39
- onDismiss?.();
40
- }, [onDismiss]);
67
+ if (dismiss) {
68
+ dismiss();
69
+ }
70
+ }, [dismiss]);
71
+
72
+ const activateAction = React.useCallback(
73
+ (action: MenuAction) => {
74
+ if (action.disabled) {
75
+ return;
76
+ }
77
+
78
+ if (action.activate) {
79
+ action.activate();
80
+ }
81
+
82
+ if (closeOnSelect) {
83
+ closeMenu();
84
+ }
85
+ },
86
+ [closeMenu, closeOnSelect],
87
+ );
41
88
 
42
89
  const openMenu = React.useCallback(() => {
43
90
  measureNode(anchorRef.current, setLayout);
44
- const firstEnabledIndex = items.findIndex((item) => !item.disabled);
91
+ const firstEnabledIndex = actions.findIndex((action) => !action.disabled);
45
92
  setActiveIndex(firstEnabledIndex === -1 ? 0 : firstEnabledIndex);
46
93
  setOpen(true);
47
- }, [items]);
94
+ }, [actions]);
48
95
 
49
96
  React.useEffect(() => {
50
97
  if (!open) {
@@ -55,17 +102,14 @@ export function Menu({ trigger, items, onDismiss, closeOnSelect = true, testID }
55
102
  const { key } = event;
56
103
  if (key === 'ArrowDown' || key === 'ArrowUp' || key === 'Home' || key === 'End') {
57
104
  event.preventDefault();
58
- setActiveIndex((current) => resolveNextMenuIndex(items, current, key));
105
+ setActiveIndex((current) => resolveNextMenuIndex(actions, current, key));
59
106
  }
60
107
 
61
108
  if (event.key === 'Enter') {
62
109
  event.preventDefault();
63
- const activeItem = items[activeIndex];
64
- if (activeItem && !activeItem.disabled) {
65
- activeItem.onPress?.();
66
- if (closeOnSelect) {
67
- closeMenu();
68
- }
110
+ const activeAction = actions[activeIndex];
111
+ if (activeAction) {
112
+ activateAction(activeAction);
69
113
  }
70
114
  }
71
115
 
@@ -74,7 +118,7 @@ export function Menu({ trigger, items, onDismiss, closeOnSelect = true, testID }
74
118
  closeMenu();
75
119
  }
76
120
  });
77
- }, [activeIndex, bindKeydown, closeMenu, closeOnSelect, items, open]);
121
+ }, [activateAction, actions, activeIndex, bindKeydown, closeMenu, open]);
78
122
 
79
123
  return (
80
124
  <View collapsable={false} ref={anchorRef}>
@@ -117,7 +161,7 @@ export function Menu({ trigger, items, onDismiss, closeOnSelect = true, testID }
117
161
  accessibilityRole="menu"
118
162
  p="xs"
119
163
  style={{
120
- minWidth: Math.max(layout?.width ?? 0, 180),
164
+ minWidth: Math.max(layout?.width ?? 0, 220),
121
165
  shadowOpacity: 0.12,
122
166
  shadowRadius: 12,
123
167
  shadowOffset: { width: 0, height: 6 },
@@ -125,40 +169,31 @@ export function Menu({ trigger, items, onDismiss, closeOnSelect = true, testID }
125
169
  testID={testID}
126
170
  variant="raised"
127
171
  >
128
- {items.map((item, index) => {
129
- const selected = index === activeIndex;
172
+ {actions.map((action, index) => {
173
+ const active = index === activeIndex;
174
+ const selected = action.selected ?? active;
130
175
 
131
176
  return (
132
177
  <Pressable
133
178
  accessibilityRole="menuitem"
134
- accessibilityState={{ disabled: item.disabled, selected }}
135
- disabled={item.disabled}
136
- key={item.id}
137
- onPress={() => {
138
- if (item.disabled) {
139
- return;
140
- }
141
-
142
- item.onPress?.();
143
- if (closeOnSelect) {
144
- closeMenu();
145
- }
146
- }}
179
+ accessibilityState={{ disabled: action.disabled, selected }}
180
+ disabled={action.disabled}
181
+ key={action.id}
182
+ onPress={() => activateAction(action)}
147
183
  >
148
184
  <Box
149
185
  px="m"
150
186
  py="s"
187
+ radius="s"
151
188
  style={{
152
189
  backgroundColor: selected
153
190
  ? theme.semantics.action.neutral.softBg
154
191
  : 'transparent',
155
- opacity: item.disabled ? 0.56 : 1,
192
+ opacity: action.disabled ? 0.56 : 1,
156
193
  }}
157
- testID={testID ? `${testID}-item-${item.id}` : undefined}
194
+ testID={testID ? `${testID}-item-${action.id}` : undefined}
158
195
  >
159
- <Text color={selected ? 'neutral' : undefined} variant="bodySmall">
160
- {item.label}
161
- </Text>
196
+ {renderActionContent(action, active)}
162
197
  </Box>
163
198
  </Pressable>
164
199
  );
@@ -1,2 +1,2 @@
1
1
  export { Menu } from './Menu';
2
- export type { MenuItem, MenuProps } from './types';
2
+ export type { MenuAction, MenuActionIntent, MenuAction as MenuItem, MenuProps } from './types';
@@ -1,5 +1,5 @@
1
1
  export function resolveNextMenuIndex(
2
- items: { disabled?: boolean }[],
2
+ items: readonly { disabled?: boolean }[],
3
3
  currentIndex: number,
4
4
  key: 'ArrowDown' | 'ArrowUp' | 'Home' | 'End',
5
5
  ): number {
@@ -1,16 +1,23 @@
1
1
  import type React from 'react';
2
2
 
3
- export interface MenuItem {
3
+ export type MenuActionIntent = 'default' | 'danger';
4
+
5
+ export interface MenuAction {
4
6
  id: string;
5
- label: React.ReactNode;
7
+ title: React.ReactNode;
8
+ description?: React.ReactNode;
9
+ leading?: React.ReactNode;
10
+ trailing?: React.ReactNode;
11
+ intent?: MenuActionIntent;
6
12
  disabled?: boolean;
7
- onPress?: (() => void) | undefined;
13
+ selected?: boolean;
14
+ activate?: () => void;
8
15
  }
9
16
 
10
17
  export interface MenuProps {
11
18
  trigger?: React.ReactNode;
12
- items: MenuItem[];
13
- onDismiss?: (() => void) | undefined;
19
+ actions: readonly MenuAction[];
20
+ dismiss?: () => void;
14
21
  closeOnSelect?: boolean;
15
22
  testID?: string;
16
23
  }
@@ -1,77 +1,91 @@
1
1
  import React from 'react';
2
2
 
3
- import {
4
- Badge,
5
- Box,
6
- Button,
7
- Checkbox,
8
- Drawer,
9
- Field,
10
- HelperText,
11
- Label,
12
- Menu,
13
- Modal,
14
- Radio,
15
- Stack,
16
- Switch,
17
- Tab,
18
- TabList,
19
- TabPanel,
20
- Tabs,
21
- Text,
22
- Textarea,
23
- TextInput,
24
- ThemeProvider,
25
- } from '../index';
3
+ import { Button } from '../components/button';
4
+ import { Card } from '../components/card';
5
+ import { Checkbox } from '../components/checkbox';
6
+ import { Drawer } from '../components/drawer';
7
+ import { Field } from '../components/field';
8
+ import { HelperText } from '../components/helper-text';
9
+ import { Menu } from '../components/menu';
10
+ import { Modal } from '../components/modal';
11
+ import { Tab, TabList, TabPanel, Tabs } from '../components/tabs';
12
+ import { TextInput } from '../components/text-input';
13
+ import { ToastProvider, useToast } from '../components/toast';
14
+ import { Tooltip } from '../components/tooltip';
15
+ import { Box, Stack } from '../layout';
16
+ import { Text } from '../primitives/text';
17
+ import { ThemeProvider } from '../theme';
18
+ import type { ThemeConfig } from '../theme/types';
26
19
 
27
- const docsThemeConfig = {
28
- id: 'docs-example',
29
- name: 'Docs Example',
30
- light: {
31
- harmony: 'monochromatic' as const,
32
- primaryColor: '#2563eb',
33
- },
34
- dark: {
35
- harmony: 'monochromatic' as const,
36
- primaryColor: '#2563eb',
37
- },
20
+ const docsThemeConfig: ThemeConfig = {
21
+ id: 'docs',
22
+ name: 'Docs',
23
+ light: { primaryColor: '#2563EB', harmony: 'analogous' },
24
+ dark: { primaryColor: '#60A5FA', harmony: 'analogous' },
38
25
  };
39
26
 
40
- export function ProviderExample() {
27
+ export function FormExample() {
41
28
  return (
42
29
  <ThemeProvider initialConfig={docsThemeConfig}>
43
30
  <Stack gap="m" p="l">
44
- <Text variant="body">Surface starter</Text>
45
- <Badge color="success" content="Foundation" />
46
- <Button color="warning" variant="soft">
47
- Continue
48
- </Button>
31
+ <Field helperText="Use your work email." label="Email" required>
32
+ <TextInput placeholder="you@example.com" />
33
+ </Field>
34
+ <Checkbox checked>Accept terms</Checkbox>
35
+ <HelperText emphasis="muted">Looks good.</HelperText>
49
36
  </Stack>
50
37
  </ThemeProvider>
51
38
  );
52
39
  }
53
40
 
54
- export function FormAndOverlayExample() {
41
+ function ToastButton() {
42
+ const { showToast } = useToast();
43
+ return (
44
+ <Button
45
+ onPress={() =>
46
+ showToast({ description: 'Your changes were saved.', status: 'success', title: 'Saved' })
47
+ }
48
+ >
49
+ Show toast
50
+ </Button>
51
+ );
52
+ }
53
+
54
+ export function FeedbackExample() {
55
55
  return (
56
56
  <ThemeProvider initialConfig={docsThemeConfig}>
57
- <Stack gap="m" p="l">
58
- <Field helperText="We only use this for sign-in." label="Email">
59
- <TextInput placeholder="name@example.com" />
60
- </Field>
61
- <Field errorText="Bio is required." invalid label="Bio">
62
- <Textarea placeholder="Tell us a little about yourself" />
63
- </Field>
64
- <Stack gap="s">
65
- <Label required>Preferences</Label>
66
- <Checkbox defaultChecked>Weekly updates</Checkbox>
67
- <Radio>Product announcements</Radio>
68
- <Switch readOnly>Read-only setting</Switch>
69
- <HelperText color="error">Invalid fields should reuse the same error color.</HelperText>
57
+ <ToastProvider>
58
+ <Stack gap="m" p="l">
59
+ <Tooltip content="Helpful contextual information">
60
+ <Text>Hover for tooltip</Text>
61
+ </Tooltip>
62
+ <ToastButton />
70
63
  </Stack>
71
- <Modal visible={false}>
72
- <Text>Modal content</Text>
64
+ </ToastProvider>
65
+ </ThemeProvider>
66
+ );
67
+ }
68
+
69
+ export function OverlayExample() {
70
+ const [modalVisible, setModalVisible] = React.useState(false);
71
+ const [drawerVisible, setDrawerVisible] = React.useState(false);
72
+
73
+ return (
74
+ <ThemeProvider initialConfig={docsThemeConfig}>
75
+ <Stack gap="m" p="l">
76
+ <Button onPress={() => setModalVisible(true)}>Open modal</Button>
77
+ <Button onPress={() => setDrawerVisible(true)}>Open drawer</Button>
78
+ <Modal onDismiss={() => setModalVisible(false)} visible={modalVisible}>
79
+ <Card>
80
+ <Stack gap="s">
81
+ <Text variant="label" weight="semiBold">
82
+ Modal title
83
+ </Text>
84
+ <Text>Modal content</Text>
85
+ </Stack>
86
+ </Card>
73
87
  </Modal>
74
- <Drawer visible={false}>
88
+ <Drawer onDismiss={() => setDrawerVisible(false)} visible={drawerVisible}>
75
89
  <Text>Drawer content</Text>
76
90
  </Drawer>
77
91
  </Stack>
@@ -99,9 +113,9 @@ export function NavigationExample() {
99
113
  </TabPanel>
100
114
  </Tabs>
101
115
  <Menu
102
- items={[
103
- { id: 'edit', label: 'Edit' },
104
- { disabled: true, id: 'archive', label: 'Archive' },
116
+ actions={[
117
+ { id: 'edit', title: 'Edit' },
118
+ { disabled: true, id: 'archive', title: 'Archive' },
105
119
  ]}
106
120
  trigger={<Text>Open menu</Text>}
107
121
  />
package/src/index.ts CHANGED
@@ -1,3 +1,5 @@
1
+ export type { ActionSheetItemProps, ActionSheetProps } from './components/action-sheet';
2
+ export { ActionSheet, ActionSheetItem } from './components/action-sheet';
1
3
  export type { BadgeProps } from './components/badge';
2
4
  export { Badge } from './components/badge';
3
5
  export type { ButtonIconSpec, ButtonProps } from './components/button';
@@ -18,7 +20,7 @@ export type { LabelProps } from './components/label';
18
20
  export { Label } from './components/label';
19
21
  export type { ListItemProps } from './components/list-item';
20
22
  export { ListItem } from './components/list-item';
21
- export type { MenuItem, MenuProps } from './components/menu';
23
+ export type { MenuAction, MenuActionIntent, MenuItem, MenuProps } from './components/menu';
22
24
  export { Menu } from './components/menu';
23
25
  export type { ModalProps } from './components/modal';
24
26
  export { Modal } from './components/modal';