@ankhorage/surface 4.2.1 → 4.4.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/CHANGELOG.md +12 -0
- package/README.md +4 -4
- package/dist/components/tabs/Tabs.js +2 -2
- package/dist/components/tabs/Tabs.js.map +1 -1
- package/dist/components/tabs/context.js +1 -1
- package/dist/components/tabs/context.js.map +1 -1
- package/dist/context/FontContext.d.ts.map +1 -1
- package/dist/context/FontContext.js +3 -3
- package/dist/context/FontContext.js.map +1 -1
- package/dist/context/TranslationContext.d.ts.map +1 -1
- package/dist/context/TranslationContext.js +3 -4
- package/dist/context/TranslationContext.js.map +1 -1
- package/dist/core/responsive/ResponsiveProvider.d.ts.map +1 -1
- package/dist/core/responsive/ResponsiveProvider.js +3 -3
- package/dist/core/responsive/ResponsiveProvider.js.map +1 -1
- package/dist/examples/DocsExamples.js +1 -1
- package/dist/examples/DocsExamples.js.map +1 -1
- package/dist/features/accordion/adapters/inbound/Accordion.d.ts +5 -0
- package/dist/features/accordion/adapters/inbound/Accordion.d.ts.map +1 -0
- package/dist/features/accordion/adapters/inbound/Accordion.js +46 -0
- package/dist/features/accordion/adapters/inbound/Accordion.js.map +1 -0
- package/dist/features/accordion/adapters/inbound/AccordionContent.d.ts +5 -0
- package/dist/features/accordion/adapters/inbound/AccordionContent.d.ts.map +1 -0
- package/dist/features/accordion/adapters/inbound/AccordionContent.js +16 -0
- package/dist/features/accordion/adapters/inbound/AccordionContent.js.map +1 -0
- package/dist/features/accordion/adapters/inbound/AccordionContext.d.ts +4 -0
- package/dist/features/accordion/adapters/inbound/AccordionContext.d.ts.map +1 -0
- package/dist/features/accordion/adapters/inbound/AccordionContext.js +3 -0
- package/dist/features/accordion/adapters/inbound/AccordionContext.js.map +1 -0
- package/dist/features/accordion/adapters/inbound/AccordionItem.d.ts +5 -0
- package/dist/features/accordion/adapters/inbound/AccordionItem.d.ts.map +1 -0
- package/dist/features/accordion/adapters/inbound/AccordionItem.js +28 -0
- package/dist/features/accordion/adapters/inbound/AccordionItem.js.map +1 -0
- package/dist/features/accordion/adapters/inbound/AccordionItemContext.d.ts +4 -0
- package/dist/features/accordion/adapters/inbound/AccordionItemContext.d.ts.map +1 -0
- package/dist/features/accordion/adapters/inbound/AccordionItemContext.js +3 -0
- package/dist/features/accordion/adapters/inbound/AccordionItemContext.js.map +1 -0
- package/dist/features/accordion/adapters/inbound/AccordionTrigger.d.ts +5 -0
- package/dist/features/accordion/adapters/inbound/AccordionTrigger.d.ts.map +1 -0
- package/dist/features/accordion/adapters/inbound/AccordionTrigger.js +17 -0
- package/dist/features/accordion/adapters/inbound/AccordionTrigger.js.map +1 -0
- package/dist/features/accordion/public.d.ts +6 -0
- package/dist/features/accordion/public.d.ts.map +1 -0
- package/dist/features/accordion/public.js +5 -0
- package/dist/features/accordion/public.js.map +1 -0
- package/dist/features/accordion/utils/resolveAccordionToggle.d.ts +11 -0
- package/dist/features/accordion/utils/resolveAccordionToggle.d.ts.map +1 -0
- package/dist/features/accordion/utils/resolveAccordionToggle.js +13 -0
- package/dist/features/accordion/utils/resolveAccordionToggle.js.map +1 -0
- package/dist/features/bottom-sheet/BottomSheetProvider.js +2 -2
- package/dist/features/bottom-sheet/BottomSheetProvider.js.map +1 -1
- package/dist/features/bottom-sheet/useBottomSheet.js +1 -1
- package/dist/features/bottom-sheet/useBottomSheet.js.map +1 -1
- package/dist/features/popover/adapters/inbound/Popover.d.ts +5 -0
- package/dist/features/popover/adapters/inbound/Popover.d.ts.map +1 -0
- package/dist/features/popover/adapters/inbound/Popover.js +31 -0
- package/dist/features/popover/adapters/inbound/Popover.js.map +1 -0
- package/dist/features/popover/adapters/inbound/PopoverOverlay.d.ts +17 -0
- package/dist/features/popover/adapters/inbound/PopoverOverlay.d.ts.map +1 -0
- package/dist/features/popover/adapters/inbound/PopoverOverlay.js +34 -0
- package/dist/features/popover/adapters/inbound/PopoverOverlay.js.map +1 -0
- package/dist/features/popover/adapters/inbound/usePopoverLayout.d.ts +21 -0
- package/dist/features/popover/adapters/inbound/usePopoverLayout.d.ts.map +1 -0
- package/dist/features/popover/adapters/inbound/usePopoverLayout.js +33 -0
- package/dist/features/popover/adapters/inbound/usePopoverLayout.js.map +1 -0
- package/dist/features/popover/public.d.ts +3 -0
- package/dist/features/popover/public.d.ts.map +1 -0
- package/dist/features/popover/public.js +2 -0
- package/dist/features/popover/public.js.map +1 -0
- package/dist/features/popover/utils/resolvePopoverPosition.d.ts +25 -0
- package/dist/features/popover/utils/resolvePopoverPosition.d.ts.map +1 -0
- package/dist/features/popover/utils/resolvePopoverPosition.js +23 -0
- package/dist/features/popover/utils/resolvePopoverPosition.js.map +1 -0
- package/dist/{components/toast → features/toast/adapters/inbound}/Toast.d.ts +2 -1
- package/dist/features/toast/adapters/inbound/Toast.d.ts.map +1 -0
- package/dist/{components/toast → features/toast/adapters/inbound}/Toast.js +37 -32
- package/dist/features/toast/adapters/inbound/Toast.js.map +1 -0
- package/dist/features/toast/adapters/inbound/ToastProvider.d.ts +5 -0
- package/dist/features/toast/adapters/inbound/ToastProvider.d.ts.map +1 -0
- package/dist/features/toast/adapters/inbound/ToastProvider.js +29 -0
- package/dist/features/toast/adapters/inbound/ToastProvider.js.map +1 -0
- package/dist/features/toast/adapters/inbound/useToast.d.ts +4 -0
- package/dist/features/toast/adapters/inbound/useToast.d.ts.map +1 -0
- package/dist/features/toast/adapters/inbound/useToast.js +11 -0
- package/dist/features/toast/adapters/inbound/useToast.js.map +1 -0
- package/dist/features/toast/composition/ToastContext.d.ts +4 -0
- package/dist/features/toast/composition/ToastContext.d.ts.map +1 -0
- package/dist/features/toast/composition/ToastContext.js +4 -0
- package/dist/features/toast/composition/ToastContext.js.map +1 -0
- package/dist/features/toast/composition/useToastRuntime.d.ts +11 -0
- package/dist/features/toast/composition/useToastRuntime.d.ts.map +1 -0
- package/dist/features/toast/composition/useToastRuntime.js +50 -0
- package/dist/features/toast/composition/useToastRuntime.js.map +1 -0
- package/dist/features/toast/public.d.ts +5 -0
- package/dist/features/toast/public.d.ts.map +1 -0
- package/dist/features/toast/public.js +4 -0
- package/dist/features/toast/public.js.map +1 -0
- package/dist/index.d.ts +6 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/internal/overlay/OverlayProvider.d.ts +1 -0
- package/dist/internal/overlay/OverlayProvider.d.ts.map +1 -1
- package/dist/internal/overlay/OverlayProvider.js +9 -7
- package/dist/internal/overlay/OverlayProvider.js.map +1 -1
- package/dist/internal/overlay/useOverlayStack.js +1 -1
- package/dist/internal/overlay/useOverlayStack.js.map +1 -1
- package/dist/internal/resolvers/resolveOverlayZIndex.d.ts +1 -1
- package/dist/internal/resolvers/resolveOverlayZIndex.d.ts.map +1 -1
- package/dist/internal/resolvers/resolveOverlayZIndex.js +1 -0
- package/dist/internal/resolvers/resolveOverlayZIndex.js.map +1 -1
- package/dist/layout/AppBar.js +1 -1
- package/dist/layout/AppBar.js.map +1 -1
- package/dist/theme/ThemeContext.d.ts.map +1 -1
- package/dist/theme/ThemeContext.js +4 -5
- package/dist/theme/ThemeContext.js.map +1 -1
- package/dist/types/accordion.d.ts +54 -0
- package/dist/types/accordion.d.ts.map +1 -0
- package/dist/types/accordion.js +2 -0
- package/dist/types/accordion.js.map +1 -0
- package/dist/types/popover.d.ts +21 -0
- package/dist/types/popover.d.ts.map +1 -0
- package/dist/types/popover.js +2 -0
- package/dist/types/popover.js.map +1 -0
- package/dist/{components/toast/types.d.ts → types/toast.d.ts} +11 -3
- package/dist/types/toast.d.ts.map +1 -0
- package/dist/types/toast.js +2 -0
- package/dist/types/toast.js.map +1 -0
- package/package.json +1 -1
- package/src/components/tabs/Tabs.tsx +2 -2
- package/src/components/tabs/context.tsx +1 -1
- package/src/context/FontContext.tsx +3 -3
- package/src/context/TranslationContext.tsx +3 -4
- package/src/core/responsive/ResponsiveProvider.tsx +3 -3
- package/src/examples/DocsExamples.tsx +1 -1
- package/src/features/accordion/adapters/inbound/Accordion.tsx +83 -0
- package/src/features/accordion/adapters/inbound/AccordionContent.tsx +30 -0
- package/src/features/accordion/adapters/inbound/AccordionContext.ts +5 -0
- package/src/features/accordion/adapters/inbound/AccordionItem.tsx +42 -0
- package/src/features/accordion/adapters/inbound/AccordionItemContext.ts +5 -0
- package/src/features/accordion/adapters/inbound/AccordionTrigger.tsx +34 -0
- package/src/features/accordion/public.ts +13 -0
- package/src/features/accordion/utils/resolveAccordionToggle.test.ts +57 -0
- package/src/features/accordion/utils/resolveAccordionToggle.ts +28 -0
- package/src/features/bottom-sheet/BottomSheetProvider.tsx +2 -2
- package/src/features/bottom-sheet/useBottomSheet.ts +1 -1
- package/src/features/popover/adapters/inbound/Popover.tsx +49 -0
- package/src/features/popover/adapters/inbound/PopoverOverlay.tsx +72 -0
- package/src/features/popover/adapters/inbound/usePopoverLayout.ts +51 -0
- package/src/features/popover/public.ts +2 -0
- package/src/features/popover/utils/resolvePopoverPosition.test.ts +31 -0
- package/src/features/popover/utils/resolvePopoverPosition.ts +40 -0
- package/src/{components/toast → features/toast/adapters/inbound}/Toast.tsx +58 -54
- package/src/features/toast/adapters/inbound/ToastProvider.tsx +47 -0
- package/src/features/toast/adapters/inbound/useToast.test.tsx +18 -0
- package/src/features/toast/adapters/inbound/useToast.ts +15 -0
- package/src/features/toast/composition/ToastContext.ts +6 -0
- package/src/features/toast/composition/useToastRuntime.ts +76 -0
- package/src/features/toast/public.ts +10 -0
- package/src/index.test.ts +3 -1
- package/src/index.ts +29 -2
- package/src/internal/overlay/OverlayProvider.tsx +10 -8
- package/src/internal/overlay/useOverlayStack.ts +1 -1
- package/src/internal/resolvers/resolveOverlayZIndex.test.ts +2 -1
- package/src/internal/resolvers/resolveOverlayZIndex.ts +2 -1
- package/src/layout/AppBar.tsx +1 -1
- package/src/theme/ThemeContext.tsx +4 -5
- package/src/types/accordion.ts +69 -0
- package/src/types/popover.ts +25 -0
- package/src/{components/toast/types.ts → types/toast.ts} +12 -2
- package/dist/components/toast/Toast.d.ts.map +0 -1
- package/dist/components/toast/Toast.js.map +0 -1
- package/dist/components/toast/ToastProvider.d.ts +0 -13
- package/dist/components/toast/ToastProvider.d.ts.map +0 -1
- package/dist/components/toast/ToastProvider.js +0 -70
- package/dist/components/toast/ToastProvider.js.map +0 -1
- package/dist/components/toast/index.d.ts +0 -4
- package/dist/components/toast/index.d.ts.map +0 -1
- package/dist/components/toast/index.js +0 -3
- package/dist/components/toast/index.js.map +0 -1
- package/dist/components/toast/types.d.ts.map +0 -1
- package/dist/components/toast/types.js +0 -2
- package/dist/components/toast/types.js.map +0 -1
- package/src/components/toast/ToastProvider.tsx +0 -115
- package/src/components/toast/index.ts +0 -3
|
@@ -60,8 +60,8 @@ export function Tabs({ children, value, defaultValue, onValueChange, testID }: T
|
|
|
60
60
|
);
|
|
61
61
|
|
|
62
62
|
return (
|
|
63
|
-
<TabsContext
|
|
63
|
+
<TabsContext value={contextValue}>
|
|
64
64
|
<Box testID={testID}>{children}</Box>
|
|
65
|
-
</TabsContext
|
|
65
|
+
</TabsContext>
|
|
66
66
|
);
|
|
67
67
|
}
|
|
@@ -21,7 +21,7 @@ export interface TabsContextValue {
|
|
|
21
21
|
export const TabsContext = React.createContext<TabsContextValue | null>(null);
|
|
22
22
|
|
|
23
23
|
export function useTabsContext(): TabsContextValue {
|
|
24
|
-
const value = React.
|
|
24
|
+
const value = React.use(TabsContext);
|
|
25
25
|
|
|
26
26
|
if (!value) {
|
|
27
27
|
throw new Error('Tabs components must be used within <Tabs>.');
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React, { createContext,
|
|
1
|
+
import React, { createContext, use, useMemo, useState } from 'react';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Runtime font state consumed by theme and text primitives.
|
|
@@ -51,9 +51,9 @@ export function FontProvider(props: {
|
|
|
51
51
|
[fontsLoaded, activeFontId, onActiveFontChange],
|
|
52
52
|
);
|
|
53
53
|
|
|
54
|
-
return <FontContext
|
|
54
|
+
return <FontContext value={value}>{children}</FontContext>;
|
|
55
55
|
}
|
|
56
56
|
|
|
57
57
|
export function useFontContext(): FontRuntime {
|
|
58
|
-
return
|
|
58
|
+
return use(FontContext);
|
|
59
59
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React, { createContext,
|
|
1
|
+
import React, { createContext, use } from 'react';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Minimal translation runtime surface.
|
|
@@ -35,7 +35,6 @@ export function TranslationProvider(props: {
|
|
|
35
35
|
t: (key: string, options?: Record<string, unknown>) => {
|
|
36
36
|
if (i18n?.t) {
|
|
37
37
|
const result = i18n.t(key, options);
|
|
38
|
-
// If translation returns the key, it's missing in the current dictionary
|
|
39
38
|
if (result !== key) return result;
|
|
40
39
|
}
|
|
41
40
|
return t(key, options);
|
|
@@ -43,9 +42,9 @@ export function TranslationProvider(props: {
|
|
|
43
42
|
i18n: i18n ?? null,
|
|
44
43
|
};
|
|
45
44
|
|
|
46
|
-
return <TranslationContext
|
|
45
|
+
return <TranslationContext value={value}>{children}</TranslationContext>;
|
|
47
46
|
}
|
|
48
47
|
|
|
49
48
|
export function useTranslationContext(): TranslationRuntime {
|
|
50
|
-
return
|
|
49
|
+
return use(TranslationContext);
|
|
51
50
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React, { createContext,
|
|
1
|
+
import React, { createContext, use, useMemo } from 'react';
|
|
2
2
|
import { useWindowDimensions } from 'react-native';
|
|
3
3
|
|
|
4
4
|
import { getBreakpointFromWidth } from './getBreakpointFromWidth';
|
|
@@ -17,11 +17,11 @@ export function ResponsiveProvider({ children }: { children: React.ReactNode })
|
|
|
17
17
|
[width],
|
|
18
18
|
);
|
|
19
19
|
|
|
20
|
-
return <ResponsiveContext
|
|
20
|
+
return <ResponsiveContext value={value}>{children}</ResponsiveContext>;
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
export function useResponsiveRuntime(): ResponsiveRuntime {
|
|
24
|
-
const runtime =
|
|
24
|
+
const runtime = use(ResponsiveContext);
|
|
25
25
|
|
|
26
26
|
if (!runtime) {
|
|
27
27
|
throw new Error('useResponsiveRuntime must be used within a ResponsiveProvider');
|
|
@@ -5,13 +5,13 @@ import { HelperText } from '../components/helper-text';
|
|
|
5
5
|
import { Menu } from '../components/menu';
|
|
6
6
|
import { Modal } from '../components/modal';
|
|
7
7
|
import { Tab, TabList, TabPanel, Tabs } from '../components/tabs';
|
|
8
|
-
import { ToastProvider, useToast } from '../components/toast';
|
|
9
8
|
import { Tooltip } from '../components/tooltip';
|
|
10
9
|
import { Button } from '../features/button/public';
|
|
11
10
|
import { Card } from '../features/card/public';
|
|
12
11
|
import { Checkbox } from '../features/form/checkbox/public';
|
|
13
12
|
import { TextInput } from '../features/form/text-input/public';
|
|
14
13
|
import { Box, Stack } from '../features/layout/public';
|
|
14
|
+
import { ToastProvider, useToast } from '../features/toast/public';
|
|
15
15
|
import { Text } from '../features/typography/public';
|
|
16
16
|
import { ThemeProvider } from '../theme';
|
|
17
17
|
import type { ThemeConfig } from '../theme/types';
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { View } from 'react-native';
|
|
3
|
+
|
|
4
|
+
import type {
|
|
5
|
+
AccordionMode,
|
|
6
|
+
AccordionMultipleProps,
|
|
7
|
+
AccordionProps,
|
|
8
|
+
AccordionSingleProps,
|
|
9
|
+
} from '../../../../types/accordion';
|
|
10
|
+
import { resolveAccordionToggle } from '../../utils/resolveAccordionToggle';
|
|
11
|
+
import { AccordionContext } from './AccordionContext';
|
|
12
|
+
|
|
13
|
+
/*** Coordinates controlled or uncontrolled accordion expansion state. */
|
|
14
|
+
export function Accordion(props: AccordionProps) {
|
|
15
|
+
const {
|
|
16
|
+
children,
|
|
17
|
+
collapsible = false,
|
|
18
|
+
defaultValue,
|
|
19
|
+
disabled = false,
|
|
20
|
+
interactionPolicy = 'enabled',
|
|
21
|
+
onValueChange: _onValueChange,
|
|
22
|
+
type = 'single',
|
|
23
|
+
value,
|
|
24
|
+
...viewProps
|
|
25
|
+
} = props;
|
|
26
|
+
const [uncontrolledValues, setUncontrolledValues] = React.useState<readonly string[]>(() =>
|
|
27
|
+
normalizeAccordionValues(type, defaultValue),
|
|
28
|
+
);
|
|
29
|
+
const controlled = value !== undefined;
|
|
30
|
+
const openValues = controlled ? normalizeAccordionValues(type, value) : uncontrolledValues;
|
|
31
|
+
|
|
32
|
+
const toggleValue = React.useCallback(
|
|
33
|
+
(itemValue: string) => {
|
|
34
|
+
if (disabled || interactionPolicy === 'passive') return;
|
|
35
|
+
|
|
36
|
+
const nextValues = resolveAccordionToggle({
|
|
37
|
+
collapsible,
|
|
38
|
+
mode: type,
|
|
39
|
+
openValues,
|
|
40
|
+
value: itemValue,
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
if (!controlled) setUncontrolledValues(nextValues);
|
|
44
|
+
notifyValueChange(props, nextValues);
|
|
45
|
+
},
|
|
46
|
+
[collapsible, controlled, disabled, interactionPolicy, openValues, props, type],
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
const contextValue = React.useMemo(
|
|
50
|
+
() => ({ disabled, interactionPolicy, mode: type, openValues, toggleValue }),
|
|
51
|
+
[disabled, interactionPolicy, openValues, toggleValue, type],
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
return (
|
|
55
|
+
<AccordionContext value={contextValue}>
|
|
56
|
+
<View {...viewProps}>{children}</View>
|
|
57
|
+
</AccordionContext>
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/*** Normalizes the public single/multiple value shapes for internal state. */
|
|
62
|
+
function normalizeAccordionValues(
|
|
63
|
+
mode: AccordionMode,
|
|
64
|
+
value: string | readonly string[] | undefined,
|
|
65
|
+
): readonly string[] {
|
|
66
|
+
if (mode === 'multiple') {
|
|
67
|
+
return typeof value === 'string' || value === undefined ? [] : value;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return typeof value === 'string' ? [value] : [];
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/*** Emits the public value shape matching the configured accordion mode. */
|
|
74
|
+
function notifyValueChange(props: AccordionProps, values: readonly string[]) {
|
|
75
|
+
if (props.type === 'multiple') {
|
|
76
|
+
const multipleProps: AccordionMultipleProps = props;
|
|
77
|
+
multipleProps.onValueChange?.(values);
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const singleProps: AccordionSingleProps = props;
|
|
82
|
+
singleProps.onValueChange?.(values[0]);
|
|
83
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { View, type ViewStyle } from 'react-native';
|
|
3
|
+
|
|
4
|
+
import type { AccordionContentProps } from '../../../../types/accordion';
|
|
5
|
+
import { AccordionItemContext } from './AccordionItemContext';
|
|
6
|
+
|
|
7
|
+
/*** Renders an accordion item's content when expanded or force-mounted. */
|
|
8
|
+
export function AccordionContent({
|
|
9
|
+
children,
|
|
10
|
+
forceMount = false,
|
|
11
|
+
style,
|
|
12
|
+
...viewProps
|
|
13
|
+
}: AccordionContentProps) {
|
|
14
|
+
const item = React.use(AccordionItemContext);
|
|
15
|
+
if (!item) throw new Error('AccordionContent must be rendered inside AccordionItem.');
|
|
16
|
+
if (!item.open && !forceMount) return null;
|
|
17
|
+
|
|
18
|
+
return (
|
|
19
|
+
<View
|
|
20
|
+
{...viewProps}
|
|
21
|
+
accessibilityLabelledBy={item.triggerId}
|
|
22
|
+
nativeID={item.contentId}
|
|
23
|
+
style={[!item.open ? hiddenContentStyle : undefined, style]}
|
|
24
|
+
>
|
|
25
|
+
{children}
|
|
26
|
+
</View>
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const hiddenContentStyle: ViewStyle = { display: 'none' };
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { View } from 'react-native';
|
|
3
|
+
|
|
4
|
+
import type { AccordionItemProps } from '../../../../types/accordion';
|
|
5
|
+
import { AccordionContext } from './AccordionContext';
|
|
6
|
+
import { AccordionItemContext } from './AccordionItemContext';
|
|
7
|
+
|
|
8
|
+
/*** Provides one accordion item's value, state, and trigger/content identifiers. */
|
|
9
|
+
export function AccordionItem({
|
|
10
|
+
children,
|
|
11
|
+
disabled = false,
|
|
12
|
+
interactionPolicy,
|
|
13
|
+
value,
|
|
14
|
+
...viewProps
|
|
15
|
+
}: AccordionItemProps) {
|
|
16
|
+
const accordion = React.use(AccordionContext);
|
|
17
|
+
if (!accordion) throw new Error('AccordionItem must be rendered inside Accordion.');
|
|
18
|
+
|
|
19
|
+
const reactId = React.useId();
|
|
20
|
+
const resolvedDisabled = accordion.disabled || disabled;
|
|
21
|
+
const resolvedInteractionPolicy = interactionPolicy ?? accordion.interactionPolicy;
|
|
22
|
+
const open = accordion.openValues.includes(value);
|
|
23
|
+
const toggle = React.useCallback(() => accordion.toggleValue(value), [accordion, value]);
|
|
24
|
+
const contextValue = React.useMemo(
|
|
25
|
+
() => ({
|
|
26
|
+
contentId: `accordion-${reactId}-content`,
|
|
27
|
+
disabled: resolvedDisabled,
|
|
28
|
+
interactionPolicy: resolvedInteractionPolicy,
|
|
29
|
+
open,
|
|
30
|
+
toggle,
|
|
31
|
+
triggerId: `accordion-${reactId}-trigger`,
|
|
32
|
+
value,
|
|
33
|
+
}),
|
|
34
|
+
[open, reactId, resolvedDisabled, resolvedInteractionPolicy, toggle, value],
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<AccordionItemContext value={contextValue}>
|
|
39
|
+
<View {...viewProps}>{children}</View>
|
|
40
|
+
</AccordionItemContext>
|
|
41
|
+
);
|
|
42
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Pressable, View } from 'react-native';
|
|
3
|
+
|
|
4
|
+
import type { AccordionTriggerProps } from '../../../../types/accordion';
|
|
5
|
+
import { AccordionItemContext } from './AccordionItemContext';
|
|
6
|
+
|
|
7
|
+
/*** Renders the accessible button that toggles its owning accordion item. */
|
|
8
|
+
export function AccordionTrigger({
|
|
9
|
+
children,
|
|
10
|
+
disabled = false,
|
|
11
|
+
interactionPolicy,
|
|
12
|
+
...pressableProps
|
|
13
|
+
}: AccordionTriggerProps) {
|
|
14
|
+
const item = React.use(AccordionItemContext);
|
|
15
|
+
if (!item) throw new Error('AccordionTrigger must be rendered inside AccordionItem.');
|
|
16
|
+
|
|
17
|
+
const resolvedDisabled = item.disabled || disabled;
|
|
18
|
+
const resolvedInteractionPolicy = interactionPolicy ?? item.interactionPolicy;
|
|
19
|
+
|
|
20
|
+
return (
|
|
21
|
+
<View accessibilityRole="header">
|
|
22
|
+
<Pressable
|
|
23
|
+
{...pressableProps}
|
|
24
|
+
accessibilityRole="button"
|
|
25
|
+
accessibilityState={{ disabled: resolvedDisabled, expanded: item.open }}
|
|
26
|
+
disabled={resolvedDisabled}
|
|
27
|
+
nativeID={item.triggerId}
|
|
28
|
+
onPress={resolvedInteractionPolicy === 'passive' ? undefined : item.toggle}
|
|
29
|
+
>
|
|
30
|
+
{children}
|
|
31
|
+
</Pressable>
|
|
32
|
+
</View>
|
|
33
|
+
);
|
|
34
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export type {
|
|
2
|
+
AccordionContentProps,
|
|
3
|
+
AccordionItemProps,
|
|
4
|
+
AccordionMode,
|
|
5
|
+
AccordionMultipleProps,
|
|
6
|
+
AccordionProps,
|
|
7
|
+
AccordionSingleProps,
|
|
8
|
+
AccordionTriggerProps,
|
|
9
|
+
} from '../../types/accordion';
|
|
10
|
+
export { Accordion } from './adapters/inbound/Accordion';
|
|
11
|
+
export { AccordionContent } from './adapters/inbound/AccordionContent';
|
|
12
|
+
export { AccordionItem } from './adapters/inbound/AccordionItem';
|
|
13
|
+
export { AccordionTrigger } from './adapters/inbound/AccordionTrigger';
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test';
|
|
2
|
+
|
|
3
|
+
import { resolveAccordionToggle } from './resolveAccordionToggle';
|
|
4
|
+
|
|
5
|
+
describe('resolveAccordionToggle', () => {
|
|
6
|
+
test('opens one value in single mode', () => {
|
|
7
|
+
expect(
|
|
8
|
+
resolveAccordionToggle({
|
|
9
|
+
collapsible: false,
|
|
10
|
+
mode: 'single',
|
|
11
|
+
openValues: ['general'],
|
|
12
|
+
value: 'appearance',
|
|
13
|
+
}),
|
|
14
|
+
).toEqual(['appearance']);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
test('keeps the active value open when single mode is not collapsible', () => {
|
|
18
|
+
const openValues = ['general'] as const;
|
|
19
|
+
expect(
|
|
20
|
+
resolveAccordionToggle({
|
|
21
|
+
collapsible: false,
|
|
22
|
+
mode: 'single',
|
|
23
|
+
openValues,
|
|
24
|
+
value: 'general',
|
|
25
|
+
}),
|
|
26
|
+
).toBe(openValues);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test('closes the active value when single mode is collapsible', () => {
|
|
30
|
+
expect(
|
|
31
|
+
resolveAccordionToggle({
|
|
32
|
+
collapsible: true,
|
|
33
|
+
mode: 'single',
|
|
34
|
+
openValues: ['general'],
|
|
35
|
+
value: 'general',
|
|
36
|
+
}),
|
|
37
|
+
).toEqual([]);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test('adds and removes values independently in multiple mode', () => {
|
|
41
|
+
const opened = resolveAccordionToggle({
|
|
42
|
+
collapsible: false,
|
|
43
|
+
mode: 'multiple',
|
|
44
|
+
openValues: ['general'],
|
|
45
|
+
value: 'appearance',
|
|
46
|
+
});
|
|
47
|
+
expect(opened).toEqual(['general', 'appearance']);
|
|
48
|
+
expect(
|
|
49
|
+
resolveAccordionToggle({
|
|
50
|
+
collapsible: false,
|
|
51
|
+
mode: 'multiple',
|
|
52
|
+
openValues: opened,
|
|
53
|
+
value: 'general',
|
|
54
|
+
}),
|
|
55
|
+
).toEqual(['appearance']);
|
|
56
|
+
});
|
|
57
|
+
});
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { AccordionMode } from '../../../types/accordion';
|
|
2
|
+
|
|
3
|
+
/*** Resolves the next immutable set of open accordion item values. */
|
|
4
|
+
export function resolveAccordionToggle({
|
|
5
|
+
collapsible,
|
|
6
|
+
mode,
|
|
7
|
+
openValues,
|
|
8
|
+
value,
|
|
9
|
+
}: ResolveAccordionToggleInput): readonly string[] {
|
|
10
|
+
if (mode === 'multiple') {
|
|
11
|
+
return openValues.includes(value)
|
|
12
|
+
? openValues.filter((openValue) => openValue !== value)
|
|
13
|
+
: [...openValues, value];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
if (openValues.includes(value)) {
|
|
17
|
+
return collapsible ? [] : openValues;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return [value];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface ResolveAccordionToggleInput {
|
|
24
|
+
collapsible: boolean;
|
|
25
|
+
mode: AccordionMode;
|
|
26
|
+
openValues: readonly string[];
|
|
27
|
+
value: string;
|
|
28
|
+
}
|
|
@@ -48,7 +48,7 @@ export function BottomSheetProvider({ children }: { children: React.ReactNode })
|
|
|
48
48
|
);
|
|
49
49
|
|
|
50
50
|
return (
|
|
51
|
-
<BottomSheetContext
|
|
51
|
+
<BottomSheetContext value={controller}>
|
|
52
52
|
<BottomSheetModalProvider>
|
|
53
53
|
{children}
|
|
54
54
|
<BottomSheetHost
|
|
@@ -58,7 +58,7 @@ export function BottomSheetProvider({ children }: { children: React.ReactNode })
|
|
|
58
58
|
presentedRef={presentedRef}
|
|
59
59
|
/>
|
|
60
60
|
</BottomSheetModalProvider>
|
|
61
|
-
</BottomSheetContext
|
|
61
|
+
</BottomSheetContext>
|
|
62
62
|
);
|
|
63
63
|
}
|
|
64
64
|
|
|
@@ -5,7 +5,7 @@ import { BottomSheetContext } from './BottomSheetContext';
|
|
|
5
5
|
|
|
6
6
|
/*** Returns the shared bottom-sheet controller installed by BottomSheetProvider. */
|
|
7
7
|
export function useBottomSheet(): BottomSheetController {
|
|
8
|
-
const controller = React.
|
|
8
|
+
const controller = React.use(BottomSheetContext);
|
|
9
9
|
|
|
10
10
|
if (!controller) {
|
|
11
11
|
throw new Error('useBottomSheet must be used within BottomSheetProvider.');
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { View } from 'react-native';
|
|
3
|
+
|
|
4
|
+
import { useControllableState } from '../../../../internal/useControllableState';
|
|
5
|
+
import type { PopoverProps } from '../../../../types/popover';
|
|
6
|
+
import { PopoverOverlay } from './PopoverOverlay';
|
|
7
|
+
|
|
8
|
+
/*** Renders anchored overlay content through the shared Surface overlay stack. */
|
|
9
|
+
export function Popover(props: PopoverProps) {
|
|
10
|
+
const anchorRef = React.useRef<View | null>(null);
|
|
11
|
+
const [resolvedOpen, setResolvedOpen] = useControllableState<boolean>({
|
|
12
|
+
defaultValue: props.defaultOpen ?? false,
|
|
13
|
+
onChange: props.onOpenChange,
|
|
14
|
+
value: props.open,
|
|
15
|
+
});
|
|
16
|
+
const passive = props.interactionPolicy === 'passive';
|
|
17
|
+
|
|
18
|
+
const close = React.useCallback(() => {
|
|
19
|
+
if (!passive) setResolvedOpen(false);
|
|
20
|
+
}, [passive, setResolvedOpen]);
|
|
21
|
+
const toggle = React.useCallback(() => {
|
|
22
|
+
if (!passive) setResolvedOpen(!resolvedOpen);
|
|
23
|
+
}, [passive, resolvedOpen, setResolvedOpen]);
|
|
24
|
+
|
|
25
|
+
return (
|
|
26
|
+
<>
|
|
27
|
+
<View
|
|
28
|
+
collapsable={false}
|
|
29
|
+
ref={anchorRef}
|
|
30
|
+
testID={props.testID ? `${props.testID}-anchor` : undefined}
|
|
31
|
+
>
|
|
32
|
+
{props.anchor({ close, open: resolvedOpen, toggle })}
|
|
33
|
+
</View>
|
|
34
|
+
{resolvedOpen ? (
|
|
35
|
+
<PopoverOverlay
|
|
36
|
+
anchorRef={anchorRef}
|
|
37
|
+
closeOnOutsidePress={props.closeOnOutsidePress ?? true}
|
|
38
|
+
offset={props.offset ?? 8}
|
|
39
|
+
onClose={close}
|
|
40
|
+
passive={passive}
|
|
41
|
+
placement={props.placement ?? 'bottom-start'}
|
|
42
|
+
testID={props.testID}
|
|
43
|
+
>
|
|
44
|
+
{props.children}
|
|
45
|
+
</PopoverOverlay>
|
|
46
|
+
) : null}
|
|
47
|
+
</>
|
|
48
|
+
);
|
|
49
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Pressable, StyleSheet, View, type ViewStyle } from 'react-native';
|
|
3
|
+
|
|
4
|
+
import { Portal } from '../../../../internal/overlay/Portal';
|
|
5
|
+
import type { PopoverPlacement } from '../../../../types/popover';
|
|
6
|
+
import { usePopoverLayout } from './usePopoverLayout';
|
|
7
|
+
|
|
8
|
+
/*** Renders and positions one mounted popover overlay for an open anchor. */
|
|
9
|
+
export function PopoverOverlay({
|
|
10
|
+
anchorRef,
|
|
11
|
+
children,
|
|
12
|
+
closeOnOutsidePress,
|
|
13
|
+
offset,
|
|
14
|
+
onClose,
|
|
15
|
+
passive,
|
|
16
|
+
placement,
|
|
17
|
+
testID,
|
|
18
|
+
}: PopoverOverlayProps) {
|
|
19
|
+
const { onContentLayout, position, ready } = usePopoverLayout({
|
|
20
|
+
anchorRef,
|
|
21
|
+
offset,
|
|
22
|
+
open: true,
|
|
23
|
+
placement,
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<Portal layer="popover" visible={ready}>
|
|
28
|
+
<View pointerEvents="box-none" style={styles.overlay}>
|
|
29
|
+
{closeOnOutsidePress ? (
|
|
30
|
+
<Pressable
|
|
31
|
+
accessibilityLabel="Close popover"
|
|
32
|
+
onPress={passive ? undefined : onClose}
|
|
33
|
+
style={styles.backdrop}
|
|
34
|
+
testID={testID ? `${testID}-backdrop` : undefined}
|
|
35
|
+
/>
|
|
36
|
+
) : null}
|
|
37
|
+
<View
|
|
38
|
+
onLayout={onContentLayout}
|
|
39
|
+
style={[styles.content, position]}
|
|
40
|
+
testID={testID ? `${testID}-content` : undefined}
|
|
41
|
+
>
|
|
42
|
+
{children}
|
|
43
|
+
</View>
|
|
44
|
+
</View>
|
|
45
|
+
</Portal>
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
interface PopoverOverlayProps {
|
|
50
|
+
anchorRef: React.RefObject<View | null>;
|
|
51
|
+
children?: React.ReactNode;
|
|
52
|
+
closeOnOutsidePress: boolean;
|
|
53
|
+
offset: number;
|
|
54
|
+
onClose: () => void;
|
|
55
|
+
passive: boolean;
|
|
56
|
+
placement: PopoverPlacement;
|
|
57
|
+
testID?: string;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const absoluteFill: ViewStyle = {
|
|
61
|
+
bottom: 0,
|
|
62
|
+
left: 0,
|
|
63
|
+
position: 'absolute',
|
|
64
|
+
right: 0,
|
|
65
|
+
top: 0,
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const styles = StyleSheet.create({
|
|
69
|
+
backdrop: absoluteFill,
|
|
70
|
+
content: { position: 'absolute' },
|
|
71
|
+
overlay: absoluteFill,
|
|
72
|
+
});
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import {
|
|
3
|
+
type LayoutChangeEvent,
|
|
4
|
+
type LayoutRectangle,
|
|
5
|
+
useWindowDimensions,
|
|
6
|
+
type View,
|
|
7
|
+
} from 'react-native';
|
|
8
|
+
|
|
9
|
+
import type { PopoverPlacement } from '../../../../types/popover';
|
|
10
|
+
import { resolvePopoverPosition } from '../../utils/resolvePopoverPosition';
|
|
11
|
+
|
|
12
|
+
/*** Measures the anchor and resolves the current viewport-safe popover position. */
|
|
13
|
+
export function usePopoverLayout({ anchorRef, offset, open, placement }: UsePopoverLayoutInput) {
|
|
14
|
+
const [anchorLayout, setAnchorLayout] = React.useState<LayoutRectangle | null>(null);
|
|
15
|
+
const [contentSize, setContentSize] = React.useState({ height: 0, width: 0 });
|
|
16
|
+
const { height: viewportHeight, width: viewportWidth } = useWindowDimensions();
|
|
17
|
+
|
|
18
|
+
const measureAnchor = React.useCallback(() => {
|
|
19
|
+
anchorRef.current?.measureInWindow((x, y, width, height) => {
|
|
20
|
+
setAnchorLayout({ height, width, x, y });
|
|
21
|
+
});
|
|
22
|
+
}, [anchorRef]);
|
|
23
|
+
|
|
24
|
+
React.useEffect(() => {
|
|
25
|
+
if (open) measureAnchor();
|
|
26
|
+
}, [measureAnchor, open]);
|
|
27
|
+
|
|
28
|
+
const onContentLayout = React.useCallback((event: LayoutChangeEvent) => {
|
|
29
|
+
setContentSize(event.nativeEvent.layout);
|
|
30
|
+
}, []);
|
|
31
|
+
|
|
32
|
+
const position = anchorLayout
|
|
33
|
+
? resolvePopoverPosition({
|
|
34
|
+
anchor: anchorLayout,
|
|
35
|
+
contentSize,
|
|
36
|
+
offset,
|
|
37
|
+
placement,
|
|
38
|
+
viewport: { height: viewportHeight, width: viewportWidth },
|
|
39
|
+
viewportPadding: 8,
|
|
40
|
+
})
|
|
41
|
+
: undefined;
|
|
42
|
+
|
|
43
|
+
return { measureAnchor, onContentLayout, position, ready: Boolean(anchorLayout) };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
interface UsePopoverLayoutInput {
|
|
47
|
+
anchorRef: React.RefObject<View | null>;
|
|
48
|
+
offset: number;
|
|
49
|
+
open: boolean;
|
|
50
|
+
placement: PopoverPlacement;
|
|
51
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { describe, expect, it } from 'bun:test';
|
|
2
|
+
|
|
3
|
+
import { resolvePopoverPosition } from './resolvePopoverPosition';
|
|
4
|
+
|
|
5
|
+
describe('resolvePopoverPosition', () => {
|
|
6
|
+
it('places bottom-start content below the anchor', () => {
|
|
7
|
+
expect(
|
|
8
|
+
resolvePopoverPosition({
|
|
9
|
+
anchor: { height: 40, width: 120, x: 100, y: 80 },
|
|
10
|
+
contentSize: { height: 180, width: 240 },
|
|
11
|
+
offset: 8,
|
|
12
|
+
placement: 'bottom-start',
|
|
13
|
+
viewport: { height: 800, width: 1200 },
|
|
14
|
+
viewportPadding: 8,
|
|
15
|
+
}),
|
|
16
|
+
).toEqual({ left: 100, top: 128 });
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('clamps content to the available viewport', () => {
|
|
20
|
+
expect(
|
|
21
|
+
resolvePopoverPosition({
|
|
22
|
+
anchor: { height: 40, width: 80, x: 980, y: 700 },
|
|
23
|
+
contentSize: { height: 240, width: 300 },
|
|
24
|
+
offset: 8,
|
|
25
|
+
placement: 'bottom-end',
|
|
26
|
+
viewport: { height: 800, width: 1200 },
|
|
27
|
+
viewportPadding: 8,
|
|
28
|
+
}),
|
|
29
|
+
).toEqual({ left: 760, top: 552 });
|
|
30
|
+
});
|
|
31
|
+
});
|