Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/add-interaction-policy.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@ankhorage/surface': minor
---

Add the uniform interactionPolicy="enabled" | "passive" contract to Surface-owned interactive primitives.
7 changes: 5 additions & 2 deletions src/components/action-sheet/ActionSheet.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,12 @@ export function ActionSheet({
children,
cancelLabel = 'Cancel',
closeOnBackdrop = true,
interactionPolicy = 'enabled',
testID,
}: ActionSheetProps) {
const { theme } = useTheme();
const animation = resolveOverlayAnimation('modal');
const passive = interactionPolicy === 'passive';

if (!visible) {
return null;
Expand All @@ -40,7 +42,7 @@ export function ActionSheet({
}}
>
<Pressable
onPress={closeOnBackdrop ? onDismiss : undefined}
onPress={passive ? undefined : closeOnBackdrop ? onDismiss : undefined}
style={{
backgroundColor: theme.semantics.neutral.text,
bottom: 0,
Expand All @@ -54,7 +56,7 @@ export function ActionSheet({
/>
<FocusScope
active={visible}
onEscape={onDismiss}
onEscape={passive ? undefined : onDismiss}
testID={testID ? `${testID}-focus` : undefined}
>
<Box
Expand Down Expand Up @@ -106,6 +108,7 @@ export function ActionSheet({
{onDismiss ? (
<ActionSheetItem
label={cancelLabel}
interactionPolicy={interactionPolicy}
onPress={onDismiss}
testID={testID ? `${testID}-cancel` : undefined}
/>
Expand Down
2 changes: 2 additions & 0 deletions src/components/action-sheet/ActionSheetItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export function ActionSheetItem({
color,
disabled = false,
selected = false,
interactionPolicy = 'enabled',
onPress,
testID,
}: ActionSheetItemProps) {
Expand All @@ -21,6 +22,7 @@ export function ActionSheetItem({
accessibilityRole="button"
accessibilityState={{ disabled, selected }}
disabled={disabled}
interactionPolicy={interactionPolicy}
onPress={onPress}
radius="m"
testID={testID}
Expand Down
3 changes: 3 additions & 0 deletions src/components/action-sheet/types.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type React from 'react';

import type { InteractionPolicy } from '../../interactionPolicy';
import type { SurfaceColor } from '../../surfaceColor';

export interface ActionSheetProps {
Expand All @@ -10,6 +11,7 @@ export interface ActionSheetProps {
children?: React.ReactNode;
cancelLabel?: React.ReactNode;
closeOnBackdrop?: boolean;
interactionPolicy?: InteractionPolicy;
testID?: string;
}

Expand All @@ -21,6 +23,7 @@ export interface ActionSheetItemProps {
color?: SurfaceColor;
disabled?: boolean;
selected?: boolean;
interactionPolicy?: InteractionPolicy;
onPress?: (() => void) | undefined;
testID?: string;
}
6 changes: 4 additions & 2 deletions src/components/drawer/Drawer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,12 @@ export function Drawer({
position = 'right',
children,
closeOnBackdrop = true,
interactionPolicy = 'enabled',
testID,
}: DrawerProps) {
const { theme } = useTheme();
const animation = resolveOverlayAnimation('drawer');
const passive = interactionPolicy === 'passive';

if (!visible) {
return null;
Expand All @@ -36,7 +38,7 @@ export function Drawer({
}}
>
<Pressable
onPress={closeOnBackdrop ? onDismiss : undefined}
onPress={passive ? undefined : closeOnBackdrop ? onDismiss : undefined}
style={{
backgroundColor: theme.semantics.neutral.text,
bottom: 0,
Expand All @@ -50,7 +52,7 @@ export function Drawer({
/>
<FocusScope
active={visible}
onEscape={onDismiss}
onEscape={passive ? undefined : onDismiss}
testID={testID ? `${testID}-focus` : undefined}
>
<Box
Expand Down
3 changes: 3 additions & 0 deletions src/components/drawer/types.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import type React from 'react';

import type { InteractionPolicy } from '../../interactionPolicy';

export interface DrawerProps {
visible: boolean;
onDismiss?: (() => void) | undefined;
position?: 'left' | 'right';
children?: React.ReactNode;
closeOnBackdrop?: boolean;
interactionPolicy?: InteractionPolicy;
testID?: string;
}
24 changes: 18 additions & 6 deletions src/components/menu/Menu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,14 +53,22 @@ function renderActionContent(action: MenuAction, active: boolean) {
);
}

export function Menu({ trigger, actions, dismiss, closeOnSelect = true, testID }: MenuProps) {
export function Menu({
trigger,
actions,
dismiss,
closeOnSelect = true,
interactionPolicy = 'enabled',
testID,
}: MenuProps) {
const { theme } = useTheme();
const { bindKeydown } = useFocusManager();
const animation = resolveOverlayAnimation('menu');
const anchorRef = React.useRef<View | null>(null);
const [open, setOpen] = React.useState(false);
const [layout, setLayout] = React.useState<LayoutRectangle | null>(null);
const [activeIndex, setActiveIndex] = React.useState(0);
const passive = interactionPolicy === 'passive';

const closeMenu = React.useCallback(() => {
setOpen(false);
Expand Down Expand Up @@ -99,6 +107,10 @@ export function Menu({ trigger, actions, dismiss, closeOnSelect = true, testID }
}

return bindKeydown((event) => {
if (passive) {
return;
}

const { key } = event;
if (key === 'ArrowDown' || key === 'ArrowUp' || key === 'Home' || key === 'End') {
event.preventDefault();
Expand All @@ -118,12 +130,12 @@ export function Menu({ trigger, actions, dismiss, closeOnSelect = true, testID }
closeMenu();
}
});
}, [activateAction, actions, activeIndex, bindKeydown, closeMenu, open]);
}, [activateAction, actions, activeIndex, bindKeydown, closeMenu, open, passive]);

return (
<View collapsable={false} ref={anchorRef}>
<ButtonBase
onPress={open ? closeMenu : openMenu}
onPress={passive ? undefined : open ? closeMenu : openMenu}
testID={testID ? `${testID}-trigger` : undefined}
>
{trigger}
Expand All @@ -140,7 +152,7 @@ export function Menu({ trigger, actions, dismiss, closeOnSelect = true, testID }
}}
>
<Pressable
onPress={closeMenu}
onPress={passive ? undefined : closeMenu}
style={{
bottom: 0,
left: 0,
Expand All @@ -149,7 +161,7 @@ export function Menu({ trigger, actions, dismiss, closeOnSelect = true, testID }
top: 0,
}}
/>
<FocusScope active={open} onEscape={closeMenu}>
<FocusScope active={open} onEscape={passive ? undefined : closeMenu}>
<View
style={{
left: layout?.x ?? 0,
Expand Down Expand Up @@ -179,7 +191,7 @@ export function Menu({ trigger, actions, dismiss, closeOnSelect = true, testID }
accessibilityState={{ disabled: action.disabled, selected }}
disabled={action.disabled}
key={action.id}
onPress={() => activateAction(action)}
onPress={passive ? undefined : () => activateAction(action)}
>
<Box
px="m"
Expand Down
3 changes: 3 additions & 0 deletions src/components/menu/types.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import type React from 'react';

import type { InteractionPolicy } from '../../interactionPolicy';

export type MenuActionIntent = 'default' | 'danger';

export interface MenuAction {
Expand All @@ -19,5 +21,6 @@ export interface MenuProps {
actions: readonly MenuAction[];
dismiss?: () => void;
closeOnSelect?: boolean;
interactionPolicy?: InteractionPolicy;
testID?: string;
}
6 changes: 4 additions & 2 deletions src/components/modal/Modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,12 @@ export function Modal({
onDismiss,
children,
closeOnBackdrop = true,
interactionPolicy = 'enabled',
testID,
}: ModalProps) {
const { theme } = useTheme();
const animation = resolveOverlayAnimation('modal');
const passive = interactionPolicy === 'passive';

if (!visible) {
return null;
Expand All @@ -35,7 +37,7 @@ export function Modal({
}}
>
<Pressable
onPress={closeOnBackdrop ? onDismiss : undefined}
onPress={passive ? undefined : closeOnBackdrop ? onDismiss : undefined}
style={{
backgroundColor: theme.semantics.neutral.text,
bottom: 0,
Expand All @@ -49,7 +51,7 @@ export function Modal({
/>
<FocusScope
active={visible}
onEscape={onDismiss}
onEscape={passive ? undefined : onDismiss}
testID={testID ? `${testID}-focus` : undefined}
>
<Center
Expand Down
3 changes: 3 additions & 0 deletions src/components/modal/types.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import type React from 'react';

import type { InteractionPolicy } from '../../interactionPolicy';

export interface ModalProps {
visible: boolean;
onDismiss?: (() => void) | undefined;
children?: React.ReactNode;
closeOnBackdrop?: boolean;
interactionPolicy?: InteractionPolicy;
testID?: string;
}
9 changes: 8 additions & 1 deletion src/components/navigation/DrawerNavigation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,20 @@ export function DrawerNavigation({
footer,
compact = false,
testID,
interactionPolicy,
}: DrawerNavigationProps) {
return (
<Stack gap={compact ? 's' : 'm'} testID={testID}>
{header}
<Stack gap={compact ? 'xs' : 's'}>
{items.map((item) => (
<DrawerNavigationItem compact={compact} item={item} key={item.id} testID={testID} />
<DrawerNavigationItem
compact={compact}
interactionPolicy={interactionPolicy}
item={item}
key={item.id}
testID={testID}
/>
))}
</Stack>
{footer}
Expand Down
8 changes: 7 additions & 1 deletion src/components/navigation/DrawerNavigationItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,16 @@ import React from 'react';
import { NavigationItem } from './NavigationItem';
import type { DrawerNavigationItemProps } from './types';

export function DrawerNavigationItem({ item, compact = false, testID }: DrawerNavigationItemProps) {
export function DrawerNavigationItem({
item,
compact = false,
testID,
interactionPolicy,
}: DrawerNavigationItemProps) {
return (
<NavigationItem
compact={compact}
interactionPolicy={interactionPolicy}
item={{
...item,
accessibilityRole: item.accessibilityRole ?? 'button',
Expand Down
8 changes: 7 additions & 1 deletion src/components/navigation/NavigationItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,12 @@ import { useTheme } from '../../theme/ThemeContext';
import { resolveNavigationAccessibilityState, resolveNavigationItemPresentation } from './helpers';
import type { NavigationItemProps } from './types';

export function NavigationItem({ item, compact = false, testID }: NavigationItemProps) {
export function NavigationItem({
item,
compact = false,
testID,
interactionPolicy,
}: NavigationItemProps) {
const { theme } = useTheme();
const active = Boolean(item.active);
const disabled = Boolean(item.disabled);
Expand All @@ -26,6 +31,7 @@ export function NavigationItem({ item, compact = false, testID }: NavigationItem
accessibilityRole={item.accessibilityRole ?? 'button'}
accessibilityState={resolvedAccessibilityState}
disabled={disabled}
interactionPolicy={interactionPolicy}
onPress={item.onPress}
testID={resolvedTestID}
>
Expand Down
9 changes: 8 additions & 1 deletion src/components/navigation/NavigationList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export function NavigationList({
orientation = 'vertical',
compact = false,
testID,
interactionPolicy,
}: NavigationListProps) {
return (
<Stack
Expand All @@ -17,7 +18,13 @@ export function NavigationList({
testID={testID}
>
{items.map((item) => (
<NavigationItem compact={compact} item={item} key={item.id} testID={testID} />
<NavigationItem
compact={compact}
interactionPolicy={interactionPolicy}
item={item}
key={item.id}
testID={testID}
/>
))}
</Stack>
);
Expand Down
10 changes: 8 additions & 2 deletions src/components/navigation/TabBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { useTheme } from '../../theme/ThemeContext';
import { TabBarItem } from './TabBarItem';
import type { TabBarProps } from './types';

export function TabBar({ items, compact = false, testID }: TabBarProps) {
export function TabBar({ items, compact = false, testID, interactionPolicy }: TabBarProps) {
const { theme } = useTheme();

return (
Expand All @@ -18,7 +18,13 @@ export function TabBar({ items, compact = false, testID }: TabBarProps) {
testID={testID}
>
{items.map((item) => (
<TabBarItem compact={compact} item={item} key={item.id} testID={testID} />
<TabBarItem
compact={compact}
interactionPolicy={interactionPolicy}
item={item}
key={item.id}
testID={testID}
/>
))}
</Box>
);
Expand Down
3 changes: 2 additions & 1 deletion src/components/navigation/TabBarItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { useTheme } from '../../theme/ThemeContext';
import { resolveNavigationAccessibilityState, resolveNavigationItemPresentation } from './helpers';
import type { TabBarItemProps } from './types';

export function TabBarItem({ item, compact = false, testID }: TabBarItemProps) {
export function TabBarItem({ item, compact = false, testID, interactionPolicy }: TabBarItemProps) {
const { theme } = useTheme();
const active = Boolean(item.active);
const disabled = Boolean(item.disabled);
Expand All @@ -26,6 +26,7 @@ export function TabBarItem({ item, compact = false, testID }: TabBarItemProps) {
accessibilityRole={item.accessibilityRole ?? 'tab'}
accessibilityState={resolvedAccessibilityState}
disabled={disabled}
interactionPolicy={interactionPolicy}
onPress={item.onPress}
style={{ flex: 1 }}
testID={resolvedTestID}
Expand Down
Loading
Loading