UNPKG

@base-ui-components/react

Version:

Base UI is a library of headless ('unstyled') React components and low-level hooks. You gain complete control over your app's CSS and accessibility features.

147 lines (144 loc) 5.42 kB
'use client'; import * as React from 'react'; import { isHTMLElement } from '@floating-ui/utils/dom'; import { useStableCallback } from '@base-ui-components/utils/useStableCallback'; import { error } from '@base-ui-components/utils/error'; import { useIsoLayoutEffect } from '@base-ui-components/utils/useIsoLayoutEffect'; import { makeEventPreventable, mergeProps } from "../merge-props/index.js"; import { useCompositeRootContext } from "../composite/root/CompositeRootContext.js"; import { useFocusableWhenDisabled } from "../utils/useFocusableWhenDisabled.js"; export function useButton(parameters = {}) { const { disabled = false, focusableWhenDisabled, tabIndex = 0, native: isNativeButton = true } = parameters; const elementRef = React.useRef(null); const isCompositeItem = useCompositeRootContext(true) !== undefined; const isValidLink = useStableCallback(() => { const element = elementRef.current; return Boolean(element?.tagName === 'A' && element?.href); }); const { props: focusableWhenDisabledProps } = useFocusableWhenDisabled({ focusableWhenDisabled, disabled, composite: isCompositeItem, tabIndex, isNativeButton }); if (process.env.NODE_ENV !== 'production') { // eslint-disable-next-line react-hooks/rules-of-hooks React.useEffect(() => { if (!elementRef.current) { return; } const isButtonTag = elementRef.current.tagName === 'BUTTON'; if (isNativeButton) { if (!isButtonTag) { error('A component that acts as a button was not rendered as a native <button>, which does not match the default. Ensure that the element passed to the `render` prop of the component is a real <button>, or set the `nativeButton` prop on the component to `false`.'); } } else if (isButtonTag) { error('A component that acts as a button was rendered as a native <button>, which does not match the default. Ensure that the element passed to the `render` prop of the component is not a real <button>, or set the `nativeButton` prop on the component to `true`.'); } }, [isNativeButton]); } // handles a disabled composite button rendering another button, e.g. // <Toolbar.Button disabled render={<Menu.Trigger />} /> // the `disabled` prop needs to pass through 2 `useButton`s then finally // delete the `disabled` attribute from DOM const updateDisabled = React.useCallback(() => { const element = elementRef.current; if (!isButtonElement(element)) { return; } if (isCompositeItem && disabled && focusableWhenDisabledProps.disabled === undefined && element.disabled) { element.disabled = false; } }, [disabled, focusableWhenDisabledProps.disabled, isCompositeItem]); useIsoLayoutEffect(updateDisabled, [updateDisabled]); const getButtonProps = React.useCallback((externalProps = {}) => { const { onClick: externalOnClick, onMouseDown: externalOnMouseDown, onKeyUp: externalOnKeyUp, onKeyDown: externalOnKeyDown, onPointerDown: externalOnPointerDown, ...otherExternalProps } = externalProps; const type = isNativeButton ? 'button' : undefined; return mergeProps({ type, onClick(event) { if (disabled) { event.preventDefault(); return; } externalOnClick?.(event); }, onMouseDown(event) { if (!disabled) { externalOnMouseDown?.(event); } }, onKeyDown(event) { if (!disabled) { makeEventPreventable(event); externalOnKeyDown?.(event); } if (event.baseUIHandlerPrevented) { return; } const shouldClick = event.target === event.currentTarget && !isNativeButton && !isValidLink() && !disabled; const isEnterKey = event.key === 'Enter'; const isSpaceKey = event.key === ' '; // Keyboard accessibility for non interactive elements if (shouldClick) { if (isSpaceKey || isEnterKey) { event.preventDefault(); } if (isEnterKey) { externalOnClick?.(event); } } }, onKeyUp(event) { // calling preventDefault in keyUp on a <button> will not dispatch a click event if Space is pressed // https://codesandbox.io/p/sandbox/button-keyup-preventdefault-dn7f0 // Keyboard accessibility for non interactive elements if (!disabled) { makeEventPreventable(event); externalOnKeyUp?.(event); } if (event.baseUIHandlerPrevented) { return; } if (event.target === event.currentTarget && !isNativeButton && !disabled && event.key === ' ') { externalOnClick?.(event); } }, onPointerDown(event) { if (disabled) { event.preventDefault(); return; } externalOnPointerDown?.(event); } }, !isNativeButton ? { role: 'button' } : undefined, focusableWhenDisabledProps, otherExternalProps); }, [disabled, focusableWhenDisabledProps, isNativeButton, isValidLink]); const buttonRef = useStableCallback(element => { elementRef.current = element; updateDisabled(); }); return { getButtonProps, buttonRef }; } function isButtonElement(elem) { return isHTMLElement(elem) && elem.tagName === 'BUTTON'; }