import type { GetProps } from '@tamagui/core' import { mergeSlotStyleProps, styled } from '@tamagui/core' import type { Scope } from '@tamagui/create-context' import { createContextScope } from '@tamagui/create-context' import { withStaticProperties } from '@tamagui/helpers' import { YStack } from '@tamagui/stacks' import React from 'react' import { useIndex, useIndexedChildren } from './useIndexedChildren' interface GroupContextValue { vertical: boolean disabled?: boolean } const GROUP_NAME = 'Group' type ScopedProps

= P & { __scopeGroup?: Scope } const [createGroupContext, createGroupScope] = createContextScope(GROUP_NAME) const [GroupProvider, useGroupContext] = createGroupContext(GROUP_NAME) export const GroupFrame = styled(YStack, { name: 'GroupFrame', variants: { unstyled: { false: { size: '$true', }, }, size: (val, { tokens }) => { const borderRadius = tokens.radius[val] ?? val ?? tokens.radius['$true'] return { borderRadius, } }, } as const, defaultVariants: { unstyled: process.env.TAMAGUI_HEADLESS === '1', }, }) export type GroupExtraProps = { orientation?: 'horizontal' | 'vertical' disabled?: boolean } export type GroupProps = GetProps & GroupExtraProps function createGroup(verticalDefault: boolean) { return withStaticProperties( GroupFrame.styleable>((props, ref) => { const { __scopeGroup, children: childrenProp, orientation = verticalDefault ? 'vertical' : 'horizontal', disabled, ...restProps } = props const vertical = orientation === 'vertical' const indexedChildren = useIndexedChildren(React.Children.toArray(childrenProp)) return ( {indexedChildren} ) }), { Item: GroupItem, } ) } export type GroupItemProps = { children: React.ReactNode /** * forces the item to be a starting, center or ending item and gets the respective styles */ forcePlacement?: 'first' | 'center' | 'last' } function GroupItem(props: ScopedProps>) { const { __scopeGroup, children, forcePlacement, ...forwardedProps } = props const context = useGroupContext('GroupItem', __scopeGroup) const treeIndex = useIndex() if (!treeIndex) { throw Error(' should only be used within a ') } if (!React.isValidElement(children)) { return children as any } const isFirst = forcePlacement === 'first' || (forcePlacement !== 'last' && treeIndex.index === 0) const isLast = forcePlacement === 'last' || (forcePlacement !== 'first' && treeIndex.index === treeIndex.maxIndex) // zero out border radius on connecting sides const radiusStyles = getZeroedRadius(isFirst, isLast, context.vertical) // start with forwarded props (from parent Slot via asChild) // then merge in group-specific props (radius styles, disabled) // child's own props win via mergeSlotStyleProps const groupProps: Record = { ...forwardedProps, ...radiusStyles, } if (context.disabled != null) { groupProps.disabled = (children.props as any).disabled ?? context.disabled } // use mergeSlotStyleProps to properly compose event handlers const mergedProps = mergeSlotStyleProps(groupProps, children.props as any) return React.cloneElement(children, mergedProps) } export const useGroupItem = ( childrenProps: { disabled?: boolean }, forcePlacement?: GroupItemProps['forcePlacement'], __scopeGroup?: Scope ) => { const treeIndex = useIndex() const context = useGroupContext('GroupItem', __scopeGroup) if (!treeIndex) { throw Error('useGroupItem should only be used within a ') } const isFirst = forcePlacement === 'first' || (forcePlacement !== 'last' && treeIndex.index === 0) const isLast = forcePlacement === 'last' || (forcePlacement !== 'first' && treeIndex.index === treeIndex.maxIndex) const radiusStyles = getZeroedRadius(isFirst, isLast, context.vertical) return { disabled: childrenProps.disabled ?? context.disabled, ...radiusStyles, } } export const Group = createGroup(true) export const YGroup = Group export const XGroup = createGroup(false) /** * returns styles that zero out border radius on the connecting/interior sides * children keep their own border radius on the exterior sides */ function getZeroedRadius(isFirst: boolean, isLast: boolean, vertical: boolean) { if (vertical) { // vertical: zero bottom radius of non-last items, zero top radius of non-first items return { ...(isFirst ? null : { borderTopLeftRadius: 0, borderTopRightRadius: 0 }), ...(isLast ? null : { borderBottomLeftRadius: 0, borderBottomRightRadius: 0 }), } } // horizontal: zero right radius of non-last items, zero left radius of non-first items return { ...(isFirst ? null : { borderTopLeftRadius: 0, borderBottomLeftRadius: 0 }), ...(isLast ? null : { borderTopRightRadius: 0, borderBottomRightRadius: 0 }), } } export { createGroupScope }