import type { ButtonHTMLAttributes, CSSProperties, ReactNode } from "react";
import { cloneElement, forwardRef, isValidElement } from "react";
import cn from "classnames";
import { TextIconSpacing, useIcon } from "@react-md/icon";
import { useInteractionStates } from "@react-md/states";
import { bem } from "@react-md/utils";
type ButtonAttributes = Omit, "type">;
export interface ChipProps extends ButtonAttributes {
/**
* The theme for the button.
*/
theme?: "outline" | "solid";
/**
* An optional icon to place to the left of the children. There will
* automatically be some margin placed between this icon and the children if
* defined.
*/
leftIcon?: ReactNode;
/**
* An optional icon to place to the right of the children. There will
* automatically be some margin placed between this icon and the children if
* defined.
*/
rightIcon?: ReactNode;
/**
* Boolean if the chip should gain elevation while the user is pressing the
* chip with mouse, touch, or keyboard click.
*/
raisable?: boolean;
/**
* An optional style to provide to the `` that surrounds the `children`
* of the chip.
*
* This prop will do nothing if the `disableContentWrap` prop is enabled.
*/
contentStyle?: CSSProperties;
/**
* An optional className to provide to the `` that surrounds the
* `children` of the chip.
*
* This prop will do nothing if the `disableContentWrap` prop is enabled.
*/
contentClassName?: string;
/**
* Boolean if the children should no longer be wrapped in a `` that adds
* some default styles to ellipsis and truncate the children based on the
* chip's width.
*/
disableContentWrap?: boolean;
/**
* Boolean if the chip is selected or deselected which is `undefined` by
* default. Setting this prop to a boolean updates the chip to render a
* selected icon to the left of the content as well as adding a darker
* background when set to `true`. The icon will only appear once the state is
* `true` and will transition in and out when swapped between `true` and
* `false`.
*
* @remarks
*
* See the `disableIconTransition` and `selectedIcon` props for more details
* about the icon behavior
*/
selected?: boolean;
/**
* Boolean if the selection state should use a swatch of the primary color
* instead of rendering a check icon and the normal background color changes.
*/
selectedThemed?: boolean;
/**
* The icon to use as the `leftIcon` when the `selected` prop is a boolean.
* When this is omitted, it will inherit the `selected` icon from the main
* `Configuration` / `IconProvider`.
*
* If this is set to `null`, no icon will be rendered when the `selected` is set
* to `"selected"` or `"unselected"`.
*
* If the `leftIcon` prop is not `undefined`, the `leftIcon` prop will always
* be used instead of this prop.
*/
selectedIcon?: ReactNode;
/**
* Boolean if the selected icon should not animate when the `selected` is a
* boolean. This transition is just a simple "appear" transition with the
* `max-width` of the icon.
*/
disableIconTransition?: boolean;
/**
* Boolean if the chip should render as a non-interactable element (``)
* instead of a button. This can be used to just apply the chip styles.
*
* @remarks \@since 2.6.0
*/
noninteractable?: boolean;
}
const block = bem("rmd-chip");
/**
* A chip is a simplified and condensed button component that be used to create
* compact radio groups, checkboxes, and trigger actions. The chip only has a
* `"solid"` and `"outline"` theme but can be raisable once clicked or
* selectable with an inline icon. A chip also supports rendering icons, avatars,
* or circular progress bars to the left and right of the children.
*/
export const Chip = forwardRef(function Chip(
{
"aria-pressed": ariaPressed,
className: propClassName,
children,
theme = "solid",
leftIcon: propLeftIcon,
rightIcon,
raisable = false,
disabled = false,
selected,
selectedThemed = false,
contentStyle,
contentClassName,
disableContentWrap = false,
selectedIcon: propSelectedIcon,
noninteractable = false,
disableIconTransition = false,
...props
},
ref
) {
const { ripples, className, handlers } = useInteractionStates({
handlers: props,
className: propClassName,
disabled: disabled || noninteractable,
enablePressedAndRipple: raisable && !noninteractable,
});
let content = children;
if (!disableContentWrap) {
content = (
{children}
);
}
let leftIcon = propLeftIcon;
const selectable = typeof selected === "boolean";
const selectedIcon = useIcon("selected", propSelectedIcon);
let isHiddenIcon = false;
if (
selectable &&
!selectedThemed &&
typeof leftIcon === "undefined" &&
selectedIcon
) {
leftIcon = selectedIcon;
if (!disableIconTransition && isValidElement(selectedIcon)) {
isHiddenIcon = !selected;
leftIcon = cloneElement(selectedIcon, {
className: block("selected-icon", { visible: selected }),
});
} else if (disableIconTransition && !selected) {
// don't want to render it when not selected if there's no transition
leftIcon = null;
}
}
const leading = leftIcon && !isHiddenIcon;
const trailing = rightIcon;
const Component = noninteractable ? "span" : "button";
const buttonProps = {
"aria-pressed": ariaPressed ?? (!!selected || undefined),
type: "button",
disabled,
} as const;
return (
{content}
{ripples}
);
});