import { useComposedRefs } from '@tamagui/compose-refs' import { isWeb } from '@tamagui/constants' import { createContext } from '@tamagui/create-context' import { focusFocusable } from '@tamagui/focusable' import { getButtonSized } from '@tamagui/get-button-sized' import { getFontSized } from '@tamagui/get-font-sized' import { SizableText } from '@tamagui/text' import type { FontSizeTokens, GetProps } from '@tamagui/web' import { styled } from '@tamagui/web' import * as React from 'react' const NAME = 'Label' type LabelContextValue = { id?: string controlRef: React.MutableRefObject } const [LabelProvider, useLabelContextImpl] = createContext(NAME, { id: undefined, controlRef: { current: null }, }) export const LabelFrame = styled(SizableText, { name: 'Label', render: 'label', variants: { unstyled: { false: { size: '$true', color: '$color', backgroundColor: 'transparent', display: 'flex', alignItems: 'center', userSelect: 'none', cursor: 'default', pressStyle: { color: '$colorPress', }, }, }, size: { '...size': (val, extras) => { const buttonStyle = getButtonSized(val, extras) const buttonHeight = buttonStyle?.height const fontStyle = getFontSized(val as FontSizeTokens, extras as any) return { ...fontStyle, lineHeight: buttonHeight ? extras.tokens.size[buttonHeight] : undefined, } }, }, } as const, defaultVariants: { unstyled: process.env.TAMAGUI_HEADLESS === '1', }, }) export type LabelProps = GetProps & { htmlFor?: string } export const Label = LabelFrame.styleable(function Label(props, forwardedRef) { const { htmlFor, id: idProp, ...labelProps } = props const controlRef = React.useRef(null) const ref = React.useRef(null) const composedRefs = useComposedRefs(forwardedRef, ref) const backupId = React.useId() const id = idProp ?? backupId if (isWeb) { React.useEffect(() => { if (htmlFor) { const element = document.getElementById(htmlFor) const label = ref.current if (label && element) { const getAriaLabel = () => element.getAttribute('aria-labelledby') const ariaLabelledBy = [id, getAriaLabel()].filter(Boolean).join(' ') element.setAttribute('aria-labelledby', ariaLabelledBy) controlRef.current = element return () => { /** * We get the latest attribute value because at the time that this cleanup fires, * the values from the closure may have changed. */ if (!id) return const ariaLabelledBy = getAriaLabel()?.replace(id, '') if (ariaLabelledBy === '') { element.removeAttribute('aria-labelledby') } else if (ariaLabelledBy) { element.setAttribute('aria-labelledby', ariaLabelledBy) } } } } }, [id, htmlFor]) } return ( { props.onMouseDown?.(event) // prevent text selection when double clicking label if (!event.defaultPrevented && event.detail > 1) { event.preventDefault() } }} onPress={(event) => { props.onPress?.(event) if (isWeb) { if (htmlFor || !controlRef.current || event.defaultPrevented) return const isClickingControl = controlRef.current.contains( event.target as any as Node ) // Ensure event was generated by a user action // https://developer.mozilla.org/en-US/docs/Web/API/Event/isTrusted const isUserClick = event.isTrusted === true /** * When a label is wrapped around the control it labels, we trigger the appropriate events * on the control when the label is clicked. We do nothing if the user is already clicking the * control inside the label. */ if (!isClickingControl && isUserClick) { controlRef.current.click() controlRef.current.focus() } } else { if (props.htmlFor) { focusFocusable(props.htmlFor) } } }} /> ) }) export const useLabelContext = (element?: HTMLElement | null) => { const context = useLabelContextImpl('LabelConsumer') const { controlRef } = context React.useEffect(() => { if (element) controlRef.current = element }, [element, controlRef]) return context.id }