Collapsible Primitive
A dynamic element that facilitates the expansion or collapse of a panel.
Installation
Install the component via your command line.
npx expo install @rn-primitives/collapsible
Install @radix-ui/react-collapsible
npx expo install @radix-ui/react-collapsible
Copy/paste the following code for web to ~/components/primitives/collapsible/collapsible.web.tsx
import * as Collapsible from '@radix-ui/react-collapsible';import { useAugmentedRef, useControllableState, useIsomorphicLayoutEffect,} from '~/components/primitives/hooks';import * as Slot from '~/components/primitives/slot';import * as React from 'react';import { Pressable, View, type GestureResponderEvent } from 'react-native';import type { ContentProps, ContentRef, RootContext, RootProps, RootRef, TriggerProps, TriggerRef,} from './types';
const CollapsibleContext = React.createContext<RootContext | null>(null);
const Root = React.forwardRef<RootRef, RootProps>( ( { asChild, disabled = false, open: openProp, defaultOpen, onOpenChange: onOpenChangeProp, ...viewProps }, ref ) => { const [open = false, onOpenChange] = useControllableState({ prop: openProp, defaultProp: defaultOpen, onChange: onOpenChangeProp, }); const augmentedRef = useAugmentedRef({ ref });
useIsomorphicLayoutEffect(() => { if (augmentedRef.current) { const augRef = augmentedRef.current as unknown as HTMLDivElement; augRef.dataset.state = open ? 'open' : 'closed'; } }, [open]);
useIsomorphicLayoutEffect(() => { if (augmentedRef.current) { const augRef = augmentedRef.current as unknown as HTMLDivElement; if (disabled) { augRef.dataset.disabled = 'true'; } else { augRef.dataset.disabled = undefined; } } }, [disabled]);
const Component = asChild ? Slot.View : View; return ( <CollapsibleContext.Provider value={{ disabled, open, onOpenChange, }} > <Collapsible.Root open={open} defaultOpen={defaultOpen} onOpenChange={onOpenChange} disabled={disabled} > <Component ref={ref} {...viewProps} /> </Collapsible.Root> </CollapsibleContext.Provider> ); });
Root.displayName = 'RootWebCollapsible';
function useCollapsibleContext() { const context = React.useContext(CollapsibleContext); if (!context) { throw new Error( 'Collapsible compound components cannot be rendered outside the Collapsible component' ); } return context;}
const Trigger = React.forwardRef<TriggerRef, TriggerProps>( ({ asChild, onPress: onPressProp, disabled: disabledProp = false, ...props }, ref) => { const { disabled, open, onOpenChange } = useCollapsibleContext(); const augmentedRef = useAugmentedRef({ ref });
useIsomorphicLayoutEffect(() => { if (augmentedRef.current) { const augRef = augmentedRef.current as unknown as HTMLButtonElement; augRef.dataset.state = open ? 'open' : 'closed'; } }, [open]);
useIsomorphicLayoutEffect(() => { if (augmentedRef.current) { const augRef = augmentedRef.current as unknown as HTMLButtonElement; augRef.type = 'button';
if (disabled) { augRef.dataset.disabled = 'true'; } else { augRef.dataset.disabled = undefined; } } }, [disabled]);
function onPress(ev: GestureResponderEvent) { onPressProp?.(ev); onOpenChange(!open); }
const Component = asChild ? Slot.Pressable : Pressable; return ( <Collapsible.Trigger disabled={disabled} asChild> <Component ref={augmentedRef} role='button' onPress={onPress} disabled={disabled} {...props} /> </Collapsible.Trigger> ); });
Trigger.displayName = 'TriggerWebCollapsible';
const Content = React.forwardRef<ContentRef, ContentProps>( ({ asChild, forceMount, ...props }, ref) => { const augmentedRef = useAugmentedRef({ ref }); const { open } = useCollapsibleContext();
useIsomorphicLayoutEffect(() => { if (augmentedRef.current) { const augRef = augmentedRef.current as unknown as HTMLDivElement; augRef.dataset.state = open ? 'open' : 'closed'; } }, [open]);
const Component = asChild ? Slot.View : View; return ( <Collapsible.Content forceMount={forceMount} asChild> <Component ref={augmentedRef} {...props} /> </Collapsible.Content> ); });
Content.displayName = 'ContentWebCollapsible';
export { Content, Root, Trigger };
Copy/paste the following code for native to ~/components/primitives/collapsible/collapsible.tsx
import { useControllableState } from '~/components/primitives/hooks';import * as Slot from '~/components/primitives/slot';import * as React from 'react';import { Pressable, View, type GestureResponderEvent } from 'react-native';import type { ContentProps, ContentRef, RootContext, RootProps, RootRef, TriggerProps, TriggerRef,} from './types';
const CollapsibleContext = React.createContext<(RootContext & { nativeID: string }) | null>(null);
const Root = React.forwardRef<RootRef, RootProps>( ( { asChild, disabled = false, open: openProp, defaultOpen, onOpenChange: onOpenChangeProp, ...viewProps }, ref ) => { const nativeID = React.useId(); const [open = false, onOpenChange] = useControllableState({ prop: openProp, defaultProp: defaultOpen, onChange: onOpenChangeProp, });
const Component = asChild ? Slot.View : View; return ( <CollapsibleContext.Provider value={{ disabled, open, onOpenChange, nativeID, }} > <Component ref={ref} {...viewProps} /> </CollapsibleContext.Provider> ); });
Root.displayName = 'RootNativeCollapsible';
function useCollapsibleContext() { const context = React.useContext(CollapsibleContext); if (!context) { throw new Error( 'Collapsible compound components cannot be rendered outside the Collapsible component' ); } return context;}
const Trigger = React.forwardRef<TriggerRef, TriggerProps>( ({ asChild, onPress: onPressProp, disabled: disabledProp = false, ...props }, ref) => { const { disabled, open, onOpenChange, nativeID } = useCollapsibleContext();
function onPress(ev: GestureResponderEvent) { if (disabled || disabledProp) return; onOpenChange(!open); onPressProp?.(ev); }
const Component = asChild ? Slot.Pressable : Pressable; return ( <Component ref={ref} nativeID={nativeID} aria-disabled={(disabled || disabledProp) ?? undefined} role='button' onPress={onPress} accessibilityState={{ expanded: open, disabled: (disabled || disabledProp) ?? undefined, }} disabled={disabled || disabledProp} {...props} /> ); });
Trigger.displayName = 'TriggerNativeCollapsible';
const Content = React.forwardRef<ContentRef, ContentProps>( ({ asChild, forceMount, ...props }, ref) => { const { nativeID, open } = useCollapsibleContext();
if (!forceMount) { if (!open) { return null; } }
const Component = asChild ? Slot.View : View; return ( <Component ref={ref} aria-hidden={!(forceMount || open)} aria-labelledby={nativeID} role={'region'} {...props} /> ); });
Content.displayName = 'ContentNativeCollapsible';
export { Content, Root, Trigger };
Copy/paste the following code for types to ~/components/primitives/collapsible/types.ts
import type { ForceMountable, PressableRef, SlottablePressableProps, SlottableViewProps, ViewRef,} from '~/components/primitives/types';
interface RootContext { open: boolean; onOpenChange: (open: boolean) => void; disabled: boolean;}
type RootProps = SlottableViewProps & { open?: boolean; defaultOpen?: boolean; onOpenChange?: (open: boolean) => void; disabled?: boolean;};
type TriggerProps = SlottablePressableProps;type ContentProps = ForceMountable & SlottableViewProps;
type RootRef = ViewRef;type TriggerRef = PressableRef;type ContentRef = ViewRef;
export type { ContentProps, ContentRef, RootContext, RootProps, RootRef, TriggerProps, TriggerRef };
Copy/paste the following code for exporting to ~/components/primitives/collapsible/index.ts
export * from './collapsible';export * from './types';
Copy/paste the following code for native to ~/components/primitives/collapsible/index.tsx
import { useControllableState } from '~/components/primitives/hooks';import * as Slot from '~/components/primitives/slot';import * as React from 'react';import { Pressable, View, type GestureResponderEvent } from 'react-native';import type { ContentProps, ContentRef, RootContext, RootProps, RootRef, TriggerProps, TriggerRef,} from './types';
const CollapsibleContext = React.createContext<(RootContext & { nativeID: string }) | null>(null);
const Root = React.forwardRef<RootRef, RootProps>( ( { asChild, disabled = false, open: openProp, defaultOpen, onOpenChange: onOpenChangeProp, ...viewProps }, ref ) => { const nativeID = React.useId(); const [open = false, onOpenChange] = useControllableState({ prop: openProp, defaultProp: defaultOpen, onChange: onOpenChangeProp, });
const Component = asChild ? Slot.View : View; return ( <CollapsibleContext.Provider value={{ disabled, open, onOpenChange, nativeID, }} > <Component ref={ref} {...viewProps} /> </CollapsibleContext.Provider> ); });
Root.displayName = 'RootNativeCollapsible';
function useCollapsibleContext() { const context = React.useContext(CollapsibleContext); if (!context) { throw new Error( 'Collapsible compound components cannot be rendered outside the Collapsible component' ); } return context;}
const Trigger = React.forwardRef<TriggerRef, TriggerProps>( ({ asChild, onPress: onPressProp, disabled: disabledProp = false, ...props }, ref) => { const { disabled, open, onOpenChange, nativeID } = useCollapsibleContext();
function onPress(ev: GestureResponderEvent) { if (disabled || disabledProp) return; onOpenChange(!open); onPressProp?.(ev); }
const Component = asChild ? Slot.Pressable : Pressable; return ( <Component ref={ref} nativeID={nativeID} aria-disabled={(disabled || disabledProp) ?? undefined} role='button' onPress={onPress} accessibilityState={{ expanded: open, disabled: (disabled || disabledProp) ?? undefined, }} disabled={disabled || disabledProp} {...props} /> ); });
Trigger.displayName = 'TriggerNativeCollapsible';
const Content = React.forwardRef<ContentRef, ContentProps>( ({ asChild, forceMount, ...props }, ref) => { const { nativeID, open } = useCollapsibleContext();
if (!forceMount) { if (!open) { return null; } }
const Component = asChild ? Slot.View : View; return ( <Component ref={ref} aria-hidden={!(forceMount || open)} aria-labelledby={nativeID} role={'region'} {...props} /> ); });
Content.displayName = 'ContentNativeCollapsible';
export { Content, Root, Trigger };
Copy/paste the following code for types to ~/components/primitives/collapsible/types.ts
import type { ForceMountable, PressableRef, SlottablePressableProps, SlottableViewProps, ViewRef,} from '~/components/primitives/types';
interface RootContext { open: boolean; onOpenChange: (open: boolean) => void; disabled: boolean;}
type RootProps = SlottableViewProps & { open?: boolean; defaultOpen?: boolean; onOpenChange?: (open: boolean) => void; disabled?: boolean;};
type TriggerProps = SlottablePressableProps;type ContentProps = ForceMountable & SlottableViewProps;
type RootRef = ViewRef;type TriggerRef = PressableRef;type ContentRef = ViewRef;
export type { ContentProps, ContentRef, RootContext, RootProps, RootRef, TriggerProps, TriggerRef };
Usage
import * as CollapsiblePrimitive from '@rn-primitives/collapsible';
function Example() { return ( <CollapsiblePrimitive.Root> <CollapsiblePrimitive.Trigger > <Text>Toggle</Text> </CollapsiblePrimitive.Trigger> <CollapsiblePrimitive.Content > <Text>@radix-ui/react</Text> </CollapsiblePrimitive.Content> </CollapsiblePrimitive.Root> );}
Props
Root
Extends View
props
Prop | Type | Note |
---|---|---|
asChild | boolean | (optional) |
open | boolean | (optional) |
onOpenChange | (value: boolean) => void | (optional) |
defaultOpen | boolean | (optional) |
disabled | boolean | (optional) |
Trigger
Extends Pessable
props
Prop | Type | Note |
---|---|---|
asChild | boolean | (optional) |
Content
Extends View
props
Prop | Type | Note |
---|---|---|
asChild | boolean | (optional) |
forceMount | true | undefined; | (optional) |