addons/components/Button.tsxblame
View source
b69ab311/**
b69ab312 * Copyright (c) Meta Platforms, Inc. and affiliates.
b69ab313 *
b69ab314 * This source code is licensed under the MIT license found in the
b69ab315 * LICENSE file in the root directory of this source tree.
b69ab316 */
b69ab317
b69ab318import type {ForwardedRef} from 'react';
b69ab319import type {ExclusiveOr} from './Types';
b69ab3110import type {ReactProps} from './utils';
b69ab3111
b69ab3112import * as stylex from '@stylexjs/stylex';
b69ab3113import {forwardRef, type ReactNode} from 'react';
b69ab3114import {layout} from './theme/layout';
b69ab3115import {colors} from './theme/tokens.stylex';
b69ab3116
b69ab3117/**
b69ab3118 * StyleX tries to evaluate CSS variables and store them separately.
b69ab3119 * Use a layer of indirection so the CSS variable is used literally.
b69ab3120 */
b69ab3121export const vars = {
b69ab3122 fg: 'var(--foreground)',
b69ab3123 border: 'var(--contrast-border)',
b69ab3124 /** very bright border, usually only set in high-contrast themes */
b69ab3125 activeBorder: 'var(--contrast-active-border, transparent)',
b69ab3126 focusBorder: 'var(--focus-border, transparent)',
b69ab3127};
b69ab3128
b69ab3129export const buttonStyles = stylex.create({
b69ab3130 button: {
b69ab3131 background: {
b69ab3132 default: 'var(--button-secondary-background)',
b69ab3133 ':hover': 'var(--button-secondary-hover-background)',
b69ab3134 },
b69ab3135 color: 'var(--button-secondary-foreground)',
b69ab3136 border: '1px solid var(--button-border)',
b69ab3137 borderRadius: '2px',
b69ab3138 padding: '4px 11px',
b69ab3139 fontFamily: 'var(--font-family)',
b69ab3140 lineHeight: '16px',
b69ab3141 cursor: 'pointer',
b69ab3142 gap: '8px',
b69ab3143 outlineOffset: '2px',
b69ab3144 outlineStyle: 'solid',
b69ab3145 outlineWidth: '1px',
b69ab3146 outlineColor: {
b69ab3147 default: 'transparent',
b69ab3148 ':focus-visible': vars.focusBorder,
b69ab3149 },
b69ab3150 flexWrap: 'nowrap',
b69ab3151 whiteSpace: 'nowrap',
b69ab3152 },
b69ab3153 primary: {
b69ab3154 background: {
b69ab3155 default: 'var(--button-primary-background)',
b69ab3156 ':hover': 'var(--button-primary-hover-background)',
b69ab3157 },
b69ab3158 color: 'var(--button-primary-foreground)',
b69ab3159 },
b69ab3160 icon: {
b69ab3161 border: '1px solid',
b69ab3162 borderColor: colors.subtleHoverDarken,
b69ab3163 background: {
b69ab3164 default: colors.subtleHoverDarken,
b69ab3165 ':hover': 'var(--button-icon-hover-background, rgba(90, 93, 94, 0.31))',
b69ab3166 },
b69ab3167 borderRadius: '5px',
b69ab3168 color: vars.fg,
b69ab3169 padding: '3px',
b69ab3170 outlineStyle: {
b69ab3171 default: 'solid',
b69ab3172 ':hover': 'dotted',
b69ab3173 ':focus-within': 'solid',
b69ab3174 },
b69ab3175 outlineOffset: 0,
b69ab3176 outlineColor: {
b69ab3177 default: 'transparent',
b69ab3178 ':hover': vars.activeBorder,
b69ab3179 ':focus-visible': vars.focusBorder,
b69ab3180 },
b69ab3181 },
b69ab3182 disabled: {
b69ab3183 opacity: '0.4',
b69ab3184 cursor: 'not-allowed',
b69ab3185 },
b69ab3186});
b69ab3187
b69ab3188export const Button = forwardRef(
b69ab3189 (
b69ab3190 {
b69ab3191 icon: iconProp,
b69ab3192 primary: primaryProp,
b69ab3193 disabled,
b69ab3194 onClick,
b69ab3195 children,
b69ab3196 xstyle,
b69ab3197 kind,
b69ab3198 className,
b69ab3199 ...rest
b69ab31100 }: {
b69ab31101 className?: string;
b69ab31102 children?: ReactNode;
b69ab31103 disabled?: boolean;
b69ab31104 xstyle?: stylex.StyleXStyles;
b69ab31105 primary?: boolean;
b69ab31106 icon?: boolean;
b69ab31107 } & Omit<ReactProps<HTMLButtonElement>, 'className'> &
b69ab31108 ExclusiveOr<
b69ab31109 ExclusiveOr<
b69ab31110 {
b69ab31111 /**
b69ab31112 * Render as a bright button, encouraged the primary confirmation action.
b69ab31113 * Equivalent to kind='primary'.
b69ab31114 */
b69ab31115 primary?: boolean;
b69ab31116 },
b69ab31117 {
b69ab31118 /**
b69ab31119 * Render as a smaller, more subtle button. Useful in toolbars or when using an icon instead of a label.
b69ab31120 * Equivalent to kind='icon'.
b69ab31121 */
b69ab31122 icon?: boolean;
b69ab31123 }
b69ab31124 >,
b69ab31125 /** How to display the button. Can also provide `primary` or `icon` shorthand bool props instead. */
b69ab31126 {kind?: 'primary' | 'icon' | undefined}
b69ab31127 >,
b69ab31128 ref: ForwardedRef<HTMLButtonElement>,
b69ab31129 ) => {
b69ab31130 const primary = kind === 'primary' || primaryProp === true;
b69ab31131 const icon = kind === 'icon' || iconProp === true;
b69ab31132 const {className: stylexClassName, ...otherStylex} = stylex.props(
b69ab31133 layout.flexRow,
b69ab31134 buttonStyles.button,
b69ab31135 primary && buttonStyles.primary,
b69ab31136 icon && buttonStyles.icon,
b69ab31137 disabled && buttonStyles.disabled,
b69ab31138 xstyle,
b69ab31139 );
b69ab31140 return (
b69ab31141 <button
b69ab31142 tabIndex={disabled ? -1 : 0}
b69ab31143 onClick={e => {
b69ab31144 // don't allow clicking a disabled button
b69ab31145 disabled !== true && onClick?.(e);
b69ab31146 }}
b69ab31147 ref={ref}
b69ab31148 className={stylexClassName + (className ? ' ' + className : '')}
b69ab31149 {...otherStylex}
b69ab31150 disabled={disabled}
b69ab31151 {...rest}>
b69ab31152 {children}
b69ab31153 </button>
b69ab31154 );
b69ab31155 },
b69ab31156);