more usage of tamagui primitives
This commit is contained in:
85
resources/js/admin/mobile/components/FormControls.test.tsx
Normal file
85
resources/js/admin/mobile/components/FormControls.test.tsx
Normal file
@@ -0,0 +1,85 @@
|
||||
import React from 'react';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { fireEvent, render, screen } from '@testing-library/react';
|
||||
|
||||
vi.mock('@tamagui/core', () => ({
|
||||
useTheme: () => ({
|
||||
color: { val: '#111827' },
|
||||
gray: { val: '#6b7280' },
|
||||
borderColor: { val: '#e5e7eb' },
|
||||
primary: { val: '#FF5A5F' },
|
||||
surface: { val: '#ffffff' },
|
||||
red10: { val: '#b91c1c' },
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock('@tamagui/stacks', () => ({
|
||||
YStack: ({ children, ...props }: { children: React.ReactNode }) => <div {...props}>{children}</div>,
|
||||
XStack: ({ children, ...props }: { children: React.ReactNode }) => <div {...props}>{children}</div>,
|
||||
}));
|
||||
|
||||
vi.mock('@tamagui/text', () => ({
|
||||
SizableText: ({ children, ...props }: { children: React.ReactNode }) => <span {...props}>{children}</span>,
|
||||
}));
|
||||
|
||||
vi.mock('tamagui', () => ({
|
||||
Input: ({ ...props }: React.InputHTMLAttributes<HTMLInputElement>) => <input {...props} />,
|
||||
TextArea: ({ ...props }: React.TextareaHTMLAttributes<HTMLTextAreaElement>) => <textarea {...props} />,
|
||||
}));
|
||||
|
||||
vi.mock('@tamagui/select', () => {
|
||||
const SelectContext = React.createContext<{ onValueChange?: (value: string) => void } | null>(null);
|
||||
const Select = ({ children, onValueChange }: { children: React.ReactNode; onValueChange?: (value: string) => void }) => (
|
||||
<SelectContext.Provider value={{ onValueChange }}>
|
||||
<div>{children}</div>
|
||||
</SelectContext.Provider>
|
||||
);
|
||||
const Trigger = ({ children, ...props }: { children: React.ReactNode }) => <div {...props}>{children}</div>;
|
||||
const Value = ({ placeholder }: { placeholder?: React.ReactNode }) => <span>{placeholder}</span>;
|
||||
const Content = ({ children }: { children: React.ReactNode }) => <div>{children}</div>;
|
||||
const Viewport = ({ children }: { children: React.ReactNode }) => <div>{children}</div>;
|
||||
const Group = ({ children }: { children: React.ReactNode }) => <div>{children}</div>;
|
||||
const Item = ({ children, value }: { children: React.ReactNode; value: string }) => {
|
||||
const ctx = React.useContext(SelectContext);
|
||||
return (
|
||||
<button type="button" data-value={value} onClick={() => ctx?.onValueChange?.(value)}>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
const ItemText = ({ children }: { children: React.ReactNode }) => <span>{children}</span>;
|
||||
|
||||
Select.Trigger = Trigger;
|
||||
Select.Value = Value;
|
||||
Select.Content = Content;
|
||||
Select.Viewport = Viewport;
|
||||
Select.Group = Group;
|
||||
Select.Item = Item;
|
||||
Select.ItemText = ItemText;
|
||||
|
||||
return { Select };
|
||||
});
|
||||
|
||||
import { MobileSelect } from './FormControls';
|
||||
|
||||
describe('MobileSelect', () => {
|
||||
it('maps options and forwards selection changes', () => {
|
||||
const handleChange = vi.fn();
|
||||
|
||||
render(
|
||||
<MobileSelect value="" onChange={handleChange}>
|
||||
<option value="">None</option>
|
||||
<option value="one">One</option>
|
||||
</MobileSelect>,
|
||||
);
|
||||
|
||||
const items = screen.getAllByRole('button');
|
||||
fireEvent.click(items[1]);
|
||||
|
||||
expect(handleChange).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
target: expect.objectContaining({ value: 'one' }),
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -2,6 +2,8 @@ import React from 'react';
|
||||
import { ChevronDown } from 'lucide-react';
|
||||
import { YStack, XStack } from '@tamagui/stacks';
|
||||
import { SizableText as Text } from '@tamagui/text';
|
||||
import { Input, TextArea } from 'tamagui';
|
||||
import { Select } from '@tamagui/select';
|
||||
import { withAlpha } from './colors';
|
||||
import { useAdminTheme } from '../theme';
|
||||
|
||||
@@ -40,41 +42,41 @@ type ControlProps = {
|
||||
compact?: boolean;
|
||||
};
|
||||
|
||||
export const MobileInput = React.forwardRef<HTMLInputElement, React.ComponentPropsWithoutRef<'input'> & ControlProps>(
|
||||
function MobileInput({ hasError = false, compact = false, style, ...props }, ref) {
|
||||
const { border, surface, text, primary, danger } = useAdminTheme();
|
||||
const [focused, setFocused] = React.useState(false);
|
||||
type MobileSelectProps = React.ComponentPropsWithoutRef<'select'> & ControlProps & {
|
||||
placeholder?: string;
|
||||
containerStyle?: React.CSSProperties;
|
||||
};
|
||||
|
||||
const height = compact ? 36 : 44;
|
||||
const borderColor = hasError ? danger : focused ? primary : border;
|
||||
export const MobileInput = React.forwardRef<HTMLInputElement, React.ComponentPropsWithoutRef<'input'> & ControlProps>(
|
||||
function MobileInput({ hasError = false, compact = false, style, onChange, ...props }, ref) {
|
||||
const { border, surface, text, primary, danger } = useAdminTheme();
|
||||
const borderColor = hasError ? danger : border;
|
||||
const ringColor = hasError ? withAlpha(danger, 0.18) : withAlpha(primary, 0.18);
|
||||
|
||||
return (
|
||||
<input
|
||||
ref={ref}
|
||||
<Input
|
||||
ref={ref as React.Ref<any>}
|
||||
{...props}
|
||||
onFocus={(event) => {
|
||||
setFocused(true);
|
||||
props.onFocus?.(event);
|
||||
onChangeText={(value) => {
|
||||
onChange?.({ target: { value } } as React.ChangeEvent<HTMLInputElement>);
|
||||
}}
|
||||
onBlur={(event) => {
|
||||
setFocused(false);
|
||||
props.onBlur?.(event);
|
||||
size={compact ? '$3' : '$4'}
|
||||
height={compact ? 36 : 44}
|
||||
paddingHorizontal="$3"
|
||||
borderRadius={12}
|
||||
width="100%"
|
||||
fontSize={compact ? 13 : 14}
|
||||
backgroundColor={surface}
|
||||
color={text}
|
||||
borderColor={borderColor}
|
||||
focusStyle={{
|
||||
borderColor: hasError ? danger : primary,
|
||||
boxShadow: `0 0 0 3px ${ringColor}`,
|
||||
}}
|
||||
style={{
|
||||
width: '100%',
|
||||
height,
|
||||
borderRadius: 12,
|
||||
border: `1px solid ${borderColor}`,
|
||||
padding: '0 12px',
|
||||
fontSize: compact ? 13 : 14,
|
||||
background: surface,
|
||||
color: text,
|
||||
outline: 'none',
|
||||
boxShadow: focused || hasError ? `0 0 0 3px ${ringColor}` : 'none',
|
||||
transition: 'border-color 150ms ease, box-shadow 150ms ease',
|
||||
...style,
|
||||
hoverStyle={{
|
||||
borderColor,
|
||||
}}
|
||||
style={style}
|
||||
/>
|
||||
);
|
||||
},
|
||||
@@ -83,40 +85,35 @@ export const MobileInput = React.forwardRef<HTMLInputElement, React.ComponentPro
|
||||
export const MobileTextArea = React.forwardRef<
|
||||
HTMLTextAreaElement,
|
||||
React.ComponentPropsWithoutRef<'textarea'> & ControlProps
|
||||
>(function MobileTextArea({ hasError = false, compact = false, style, ...props }, ref) {
|
||||
>(function MobileTextArea({ hasError = false, compact = false, style, onChange, ...props }, ref) {
|
||||
const { border, surface, text, primary, danger } = useAdminTheme();
|
||||
const [focused, setFocused] = React.useState(false);
|
||||
|
||||
const borderColor = hasError ? danger : focused ? primary : border;
|
||||
const borderColor = hasError ? danger : border;
|
||||
const ringColor = hasError ? withAlpha(danger, 0.18) : withAlpha(primary, 0.18);
|
||||
|
||||
return (
|
||||
<textarea
|
||||
ref={ref}
|
||||
<TextArea
|
||||
ref={ref as React.Ref<any>}
|
||||
{...props}
|
||||
onFocus={(event) => {
|
||||
setFocused(true);
|
||||
props.onFocus?.(event);
|
||||
onChangeText={(value) => {
|
||||
onChange?.({ target: { value } } as React.ChangeEvent<HTMLTextAreaElement>);
|
||||
}}
|
||||
onBlur={(event) => {
|
||||
setFocused(false);
|
||||
props.onBlur?.(event);
|
||||
size={compact ? '$3' : '$4'}
|
||||
minHeight={compact ? 72 : 96}
|
||||
borderRadius={12}
|
||||
padding="$3"
|
||||
width="100%"
|
||||
fontSize={compact ? 13 : 14}
|
||||
backgroundColor={surface}
|
||||
color={text}
|
||||
borderColor={borderColor}
|
||||
focusStyle={{
|
||||
borderColor: hasError ? danger : primary,
|
||||
boxShadow: `0 0 0 3px ${ringColor}`,
|
||||
}}
|
||||
style={{
|
||||
width: '100%',
|
||||
borderRadius: 12,
|
||||
border: `1px solid ${borderColor}`,
|
||||
padding: '10px 12px',
|
||||
fontSize: compact ? 13 : 14,
|
||||
background: surface,
|
||||
color: text,
|
||||
outline: 'none',
|
||||
minHeight: compact ? 72 : 96,
|
||||
boxShadow: focused || hasError ? `0 0 0 3px ${ringColor}` : 'none',
|
||||
transition: 'border-color 150ms ease, box-shadow 150ms ease',
|
||||
resize: 'vertical',
|
||||
...style,
|
||||
hoverStyle={{
|
||||
borderColor,
|
||||
}}
|
||||
style={{ resize: 'vertical', ...style }}
|
||||
/>
|
||||
);
|
||||
});
|
||||
@@ -125,50 +122,92 @@ export function MobileSelect({
|
||||
children,
|
||||
hasError = false,
|
||||
compact = false,
|
||||
containerStyle,
|
||||
style,
|
||||
...props
|
||||
}: React.ComponentPropsWithoutRef<'select'> & ControlProps) {
|
||||
}: MobileSelectProps) {
|
||||
const { border, surface, text, primary, danger, subtle } = useAdminTheme();
|
||||
const [focused, setFocused] = React.useState(false);
|
||||
|
||||
const height = compact ? 36 : 44;
|
||||
const borderColor = hasError ? danger : focused ? primary : border;
|
||||
const borderColor = hasError ? danger : border;
|
||||
const ringColor = hasError ? withAlpha(danger, 0.18) : withAlpha(primary, 0.18);
|
||||
const hasSizing =
|
||||
typeof containerStyle === 'object' &&
|
||||
containerStyle !== null &&
|
||||
('width' in containerStyle ||
|
||||
'maxWidth' in containerStyle ||
|
||||
'minWidth' in containerStyle ||
|
||||
'flex' in containerStyle ||
|
||||
'flexGrow' in containerStyle);
|
||||
const options = React.Children.toArray(children).flatMap((child) => {
|
||||
if (!React.isValidElement(child)) return [];
|
||||
const { value, children: label, disabled } = child.props as {
|
||||
value?: string | number;
|
||||
children?: React.ReactNode;
|
||||
disabled?: boolean;
|
||||
};
|
||||
return [
|
||||
{
|
||||
value: value === undefined ? '' : String(value),
|
||||
label: label ?? '',
|
||||
disabled: Boolean(disabled),
|
||||
},
|
||||
];
|
||||
});
|
||||
const emptyOption = options.find((option) => option.value === '');
|
||||
const selectValue = props.value === undefined ? undefined : String(props.value ?? '');
|
||||
const selectDefault = props.defaultValue === undefined ? undefined : String(props.defaultValue ?? '');
|
||||
|
||||
return (
|
||||
<XStack position="relative" alignItems="center">
|
||||
<select
|
||||
{...props}
|
||||
onFocus={(event) => {
|
||||
setFocused(true);
|
||||
props.onFocus?.(event);
|
||||
}}
|
||||
onBlur={(event) => {
|
||||
setFocused(false);
|
||||
props.onBlur?.(event);
|
||||
}}
|
||||
style={{
|
||||
width: '100%',
|
||||
height,
|
||||
borderRadius: 12,
|
||||
border: `1px solid ${borderColor}`,
|
||||
padding: '0 36px 0 12px',
|
||||
fontSize: compact ? 13 : 14,
|
||||
background: surface,
|
||||
color: text,
|
||||
outline: 'none',
|
||||
appearance: 'none',
|
||||
WebkitAppearance: 'none',
|
||||
boxShadow: focused || hasError ? `0 0 0 3px ${ringColor}` : 'none',
|
||||
transition: 'border-color 150ms ease, box-shadow 150ms ease',
|
||||
...style,
|
||||
<XStack position="relative" alignItems="center" width={hasSizing ? undefined : '100%'} style={containerStyle}>
|
||||
<Select
|
||||
value={selectValue}
|
||||
defaultValue={selectValue === undefined ? selectDefault : undefined}
|
||||
onValueChange={(next) => {
|
||||
props.onChange?.({ target: { value: next } } as React.ChangeEvent<HTMLSelectElement>);
|
||||
}}
|
||||
size={compact ? '$3' : '$4'}
|
||||
>
|
||||
{children}
|
||||
</select>
|
||||
<XStack position="absolute" right={12} pointerEvents="none">
|
||||
<ChevronDown size={16} color={subtle} />
|
||||
</XStack>
|
||||
<Select.Trigger
|
||||
width="100%"
|
||||
borderRadius={12}
|
||||
borderWidth={1}
|
||||
borderColor={borderColor}
|
||||
backgroundColor={surface}
|
||||
paddingVertical={compact ? 6 : 10}
|
||||
paddingHorizontal="$3"
|
||||
disabled={props.disabled}
|
||||
onFocus={props.onFocus}
|
||||
onBlur={props.onBlur}
|
||||
iconAfter={<ChevronDown size={16} color={subtle} />}
|
||||
focusStyle={{
|
||||
borderColor: hasError ? danger : primary,
|
||||
boxShadow: `0 0 0 3px ${ringColor}`,
|
||||
}}
|
||||
hoverStyle={{
|
||||
borderColor,
|
||||
}}
|
||||
style={style}
|
||||
>
|
||||
<Select.Value placeholder={props.placeholder ?? emptyOption?.label ?? ''} color={text} />
|
||||
</Select.Trigger>
|
||||
<Select.Content
|
||||
zIndex={200000}
|
||||
borderRadius={14}
|
||||
borderWidth={1}
|
||||
borderColor={border}
|
||||
backgroundColor={surface}
|
||||
>
|
||||
<Select.Viewport padding="$2">
|
||||
<Select.Group>
|
||||
{options.map((option, index) => (
|
||||
<Select.Item key={`${option.value}-${index}`} value={option.value} disabled={option.disabled}>
|
||||
<Select.ItemText>{option.label}</Select.ItemText>
|
||||
</Select.Item>
|
||||
))}
|
||||
</Select.Group>
|
||||
</Select.Viewport>
|
||||
</Select.Content>
|
||||
</Select>
|
||||
{props.name ? <input type="hidden" name={props.name} value={selectValue ?? selectDefault ?? ''} /> : null}
|
||||
</XStack>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -83,14 +83,18 @@ export function CTAButton({
|
||||
}: {
|
||||
label: string;
|
||||
onPress: () => void;
|
||||
tone?: 'primary' | 'ghost';
|
||||
tone?: 'primary' | 'ghost' | 'danger';
|
||||
fullWidth?: boolean;
|
||||
disabled?: boolean;
|
||||
loading?: boolean;
|
||||
}) {
|
||||
const { primary, surface, border, text } = useAdminTheme();
|
||||
const { primary, surface, border, text, danger } = useAdminTheme();
|
||||
const isPrimary = tone === 'primary';
|
||||
const isDanger = tone === 'danger';
|
||||
const isDisabled = disabled || loading;
|
||||
const backgroundColor = isDanger ? danger : isPrimary ? primary : surface;
|
||||
const borderColor = isPrimary || isDanger ? 'transparent' : border;
|
||||
const labelColor = isPrimary || isDanger ? 'white' : text;
|
||||
return (
|
||||
<Pressable
|
||||
onPress={isDisabled ? undefined : onPress}
|
||||
@@ -106,11 +110,11 @@ export function CTAButton({
|
||||
borderRadius={16}
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
backgroundColor={isPrimary ? primary : surface}
|
||||
borderWidth={isPrimary ? 0 : 1}
|
||||
borderColor={isPrimary ? 'transparent' : border}
|
||||
backgroundColor={backgroundColor}
|
||||
borderWidth={isPrimary || isDanger ? 0 : 1}
|
||||
borderColor={borderColor}
|
||||
>
|
||||
<Text fontSize="$sm" fontWeight="800" color={isPrimary ? 'white' : text}>
|
||||
<Text fontSize="$sm" fontWeight="800" color={labelColor}>
|
||||
{label}
|
||||
</Text>
|
||||
</XStack>
|
||||
|
||||
62
resources/js/admin/mobile/components/Sheet.test.tsx
Normal file
62
resources/js/admin/mobile/components/Sheet.test.tsx
Normal file
@@ -0,0 +1,62 @@
|
||||
import React from 'react';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { fireEvent, render, screen } from '@testing-library/react';
|
||||
|
||||
vi.mock('react-i18next', () => ({
|
||||
useTranslation: () => ({ t: (key: string, fallback?: string) => fallback ?? key }),
|
||||
}));
|
||||
|
||||
vi.mock('@tamagui/core', () => ({
|
||||
useTheme: () => ({
|
||||
surface: { val: '#ffffff' },
|
||||
color12: { val: '#111827' },
|
||||
gray: { val: '#6b7280' },
|
||||
gray12: { val: '#0f172a' },
|
||||
borderColor: { val: '#e5e7eb' },
|
||||
shadowColor: { val: 'rgba(0,0,0,0.1)' },
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock('@tamagui/stacks', () => ({
|
||||
YStack: ({ children, ...props }: { children: React.ReactNode }) => <div {...props}>{children}</div>,
|
||||
XStack: ({ children, ...props }: { children: React.ReactNode }) => <div {...props}>{children}</div>,
|
||||
}));
|
||||
|
||||
vi.mock('@tamagui/text', () => ({
|
||||
SizableText: ({ children, ...props }: { children: React.ReactNode }) => <span {...props}>{children}</span>,
|
||||
}));
|
||||
|
||||
vi.mock('@tamagui/react-native-web-lite', () => ({
|
||||
Pressable: ({ children, onPress, ...props }: { children: React.ReactNode; onPress?: () => void }) => (
|
||||
<button type="button" onClick={onPress} {...props}>
|
||||
{children}
|
||||
</button>
|
||||
),
|
||||
}));
|
||||
|
||||
vi.mock('@tamagui/sheet', () => {
|
||||
const Sheet = ({ children }: { children: React.ReactNode }) => <div>{children}</div>;
|
||||
Sheet.Frame = ({ children }: { children: React.ReactNode }) => <div>{children}</div>;
|
||||
Sheet.Overlay = ({ children }: { children?: React.ReactNode }) => <div>{children}</div>;
|
||||
Sheet.ScrollView = ({ children }: { children: React.ReactNode }) => <div>{children}</div>;
|
||||
Sheet.Handle = () => <div />;
|
||||
return { Sheet };
|
||||
});
|
||||
|
||||
import { MobileSheet } from './Sheet';
|
||||
|
||||
describe('MobileSheet', () => {
|
||||
it('renders title and closes via the close action', () => {
|
||||
const onClose = vi.fn();
|
||||
|
||||
render(
|
||||
<MobileSheet open title="Test Sheet" onClose={onClose}>
|
||||
<div>Body</div>
|
||||
</MobileSheet>,
|
||||
);
|
||||
|
||||
expect(screen.getByText('Test Sheet')).toBeInTheDocument();
|
||||
fireEvent.click(screen.getByText('Close'));
|
||||
expect(onClose).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
@@ -2,6 +2,7 @@ import React from 'react';
|
||||
import { YStack, XStack } from '@tamagui/stacks';
|
||||
import { SizableText as Text } from '@tamagui/text';
|
||||
import { Pressable } from '@tamagui/react-native-web-lite';
|
||||
import { Sheet } from '@tamagui/sheet';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useAdminTheme } from '../theme';
|
||||
|
||||
@@ -17,42 +18,64 @@ type SheetProps = {
|
||||
|
||||
export function MobileSheet({ open, title, onClose, children, footer, bottomOffsetPx = 88 }: SheetProps) {
|
||||
const { t } = useTranslation('mobile');
|
||||
const { surface, textStrong, muted, overlay, shadow } = useAdminTheme();
|
||||
const { surface, textStrong, muted, overlay, shadow, border } = useAdminTheme();
|
||||
const bottomOffset = `max(env(safe-area-inset-bottom, 0px), ${bottomOffsetPx}px)`;
|
||||
|
||||
if (!open) return null;
|
||||
if (!open) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-end justify-center backdrop-blur-sm" style={{ backgroundColor: `${overlay}66` }}>
|
||||
<YStack
|
||||
<Sheet
|
||||
modal
|
||||
open={open}
|
||||
onOpenChange={(next) => {
|
||||
if (!next) {
|
||||
onClose();
|
||||
}
|
||||
}}
|
||||
snapPoints={[82]}
|
||||
snapPointsMode="percent"
|
||||
dismissOnOverlayPress
|
||||
dismissOnSnapToBottom
|
||||
zIndex={100000}
|
||||
>
|
||||
<Sheet.Overlay backgroundColor={`${overlay}66`} />
|
||||
<Sheet.Frame
|
||||
width="100%"
|
||||
maxWidth={520}
|
||||
alignSelf="center"
|
||||
borderTopLeftRadius={24}
|
||||
borderTopRightRadius={24}
|
||||
backgroundColor={surface}
|
||||
padding="$4"
|
||||
paddingBottom="$7"
|
||||
space="$3"
|
||||
shadowColor={shadow}
|
||||
shadowOpacity={0.12}
|
||||
shadowRadius={18}
|
||||
shadowOffset={{ width: 0, height: -8 }}
|
||||
maxHeight="82vh"
|
||||
overflow="auto"
|
||||
// keep sheet above bottom nav / safe area
|
||||
style={{ marginBottom: `max(env(safe-area-inset-bottom, 0px), ${bottomOffsetPx}px)` }}
|
||||
style={{ marginBottom: bottomOffset }}
|
||||
>
|
||||
<XStack alignItems="center" justifyContent="space-between">
|
||||
<Text fontSize="$md" fontWeight="800" color={textStrong}>
|
||||
{title}
|
||||
</Text>
|
||||
<Pressable onPress={onClose}>
|
||||
<Text fontSize="$md" color={muted}>
|
||||
{t('actions.close', 'Close')}
|
||||
</Text>
|
||||
</Pressable>
|
||||
</XStack>
|
||||
{children}
|
||||
{footer ? footer : null}
|
||||
</YStack>
|
||||
</div>
|
||||
<Sheet.Handle height={5} width={48} backgroundColor={border} borderRadius={999} marginBottom="$3" />
|
||||
<Sheet.ScrollView
|
||||
showsVerticalScrollIndicator={false}
|
||||
contentContainerStyle={{ paddingBottom: 6 }}
|
||||
>
|
||||
<YStack space="$3">
|
||||
<XStack alignItems="center" justifyContent="space-between">
|
||||
<Text fontSize="$md" fontWeight="800" color={textStrong}>
|
||||
{title}
|
||||
</Text>
|
||||
<Pressable onPress={onClose}>
|
||||
<Text fontSize="$md" color={muted}>
|
||||
{t('actions.close', 'Close')}
|
||||
</Text>
|
||||
</Pressable>
|
||||
</XStack>
|
||||
{children}
|
||||
{footer ? footer : null}
|
||||
</YStack>
|
||||
</Sheet.ScrollView>
|
||||
</Sheet.Frame>
|
||||
</Sheet>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -23,6 +23,15 @@ vi.mock('@tamagui/react-native-web-lite', () => ({
|
||||
Pressable: ({ children }: { children: React.ReactNode }) => <button type="button">{children}</button>,
|
||||
}));
|
||||
|
||||
vi.mock('@tamagui/sheet', () => {
|
||||
const Sheet = ({ children }: { children: React.ReactNode }) => <div>{children}</div>;
|
||||
Sheet.Frame = ({ children }: { children: React.ReactNode }) => <div>{children}</div>;
|
||||
Sheet.Overlay = ({ children }: { children?: React.ReactNode }) => <div>{children}</div>;
|
||||
Sheet.ScrollView = ({ children }: { children: React.ReactNode }) => <div>{children}</div>;
|
||||
Sheet.Handle = () => <div />;
|
||||
return { Sheet };
|
||||
});
|
||||
|
||||
import { LegalConsentSheet } from '../LegalConsentSheet';
|
||||
|
||||
describe('LegalConsentSheet', () => {
|
||||
|
||||
Reference in New Issue
Block a user