/*
* SPDX-License-Identifier: Apache-2.0
*
* The OpenSearch Contributors require contributions made to
* this file be licensed under the Apache-2.0 license or a
* compatible open source license.
*
* Modifications Copyright OpenSearch Contributors. See
* GitHub history for details.
*/
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React, {
forwardRef,
FunctionComponent,
Ref,
ButtonHTMLAttributes,
CSSProperties,
HTMLAttributes,
ReactNode,
} from 'react';
import classNames from 'classnames';
import {
CommonProps,
ExclusiveUnion,
PropsForAnchor,
PropsForButton,
keysOf,
} from '../common';
import { getSecureRelForTarget } from '../../services';
import {
OuiButtonContentProps,
OuiButtonContentType,
OuiButtonContent,
} from './button_content';
import { validateHref } from '../../services/security/href_validator';
export type ButtonColor =
| 'primary'
| 'accent'
| 'secondary'
| 'success'
| 'warning'
| 'danger'
| 'ghost'
| 'text';
export type ButtonSize = 's' | 'm';
export const colorToClassNameMap: { [color in ButtonColor]: string } = {
primary: '--primary',
accent: '--accent',
secondary: '--secondary',
success: '--success',
warning: '--warning',
danger: '--danger',
ghost: '--ghost',
text: '--text',
};
export const COLORS = keysOf(colorToClassNameMap);
export const sizeToClassNameMap: { [size in ButtonSize]: string | null } = {
s: '--small',
m: null,
};
export const SIZES = keysOf(sizeToClassNameMap);
/**
* Extends OuiButtonContentProps which provides
* `iconType`, `iconSide`, and `textProps`
*/
export interface OuiButtonProps extends OuiButtonContentProps, CommonProps {
children?: ReactNode;
/**
* Make button a solid color for prominence
*/
fill?: boolean;
/**
* Any of our named colors.
* **`secondary` color is DEPRECATED, use `success` instead**
*/
color?: ButtonColor;
/**
* Use size `s` in confined spaces
*/
size?: ButtonSize;
/**
* `disabled` is also allowed
*/
isDisabled?: boolean;
/**
* Applies the boolean state as the `aria-pressed` property to create a toggle button.
* *Only use when the readable text does not change between states.*
*/
isSelected?: boolean;
/**
* Extends the button to 100% width
*/
fullWidth?: boolean;
/**
* Override the default minimum width
*/
minWidth?: CSSProperties['minWidth'];
/**
* Force disables the button and changes the icon to a loading spinner
*/
isLoading?: boolean;
/**
* Object of props passed to the wrapping the button's content
*/
contentProps?: OuiButtonContentType;
style?: CSSProperties;
}
export type OuiButtonDisplayProps = OuiButtonProps &
HTMLAttributes & {
/**
* Provide a valid element to render the element as
*/
element: 'a' | 'button' | 'span' | 'label';
/**
* Provide the component's base class name to build the class list on
*/
baseClassName: string;
};
/**
* *INTERNAL ONLY*
* Component for displaying any element as a button
* OuiButton is largely responsible for providing relevant props
* and the logic for element-specific attributes
*/
const OuiButtonDisplay = forwardRef(
(
{
element = 'button',
baseClassName,
children,
className,
iconType,
iconSide = 'left',
color = 'primary',
size = 'm',
fill = false,
isDisabled,
isLoading,
isSelected,
contentProps,
textProps,
fullWidth,
minWidth,
style,
...rest
},
ref
) => {
const buttonIsDisabled = isLoading || isDisabled;
const classes = classNames(
baseClassName,
color ? `${baseClassName}${colorToClassNameMap[color]}` : null,
size && sizeToClassNameMap[size]
? `${baseClassName}${sizeToClassNameMap[size]}`
: null,
fill && `${baseClassName}--fill`,
fullWidth && `${baseClassName}--fullWidth`,
buttonIsDisabled && `${baseClassName}-isDisabled`,
className
);
/**
* Not changing the content or text class names to match baseClassName yet,
* as it is a major breaking change.
*/
const contentClassNames = classNames(
'ouiButton__content',
contentProps && contentProps.className
);
const textClassNames = classNames(
'ouiButton__text',
textProps && textProps.className
);
const innerNode = (
{children}
);
let calculatedStyle: CSSProperties | undefined = style;
if (minWidth !== undefined || minWidth !== null) {
calculatedStyle = {
...calculatedStyle,
minWidth,
};
}
return React.createElement(
element,
{
className: classes,
style: calculatedStyle,
disabled: element === 'button' && buttonIsDisabled,
'aria-pressed': element === 'button' ? isSelected : undefined,
ref,
...rest,
},
innerNode
);
}
);
OuiButtonDisplay.displayName = 'OuiButtonDisplay';
export { OuiButtonDisplay };
export type OuiButtonPropsForAnchor = PropsForAnchor<
OuiButtonProps,
{
buttonRef?: Ref;
}
>;
export type OuiButtonPropsForButton = PropsForButton<
OuiButtonProps,
{
buttonRef?: Ref;
}
>;
export type Props = ExclusiveUnion<
OuiButtonPropsForAnchor,
OuiButtonPropsForButton
>;
export const OuiButton: FunctionComponent = ({
isDisabled: _isDisabled,
disabled: _disabled,
href,
target,
rel,
type = 'button',
buttonRef,
...rest
}) => {
const isHrefValid = !href || validateHref(href);
const disabled = _disabled || !isHrefValid;
const isDisabled = _isDisabled || !isHrefValid;
const buttonIsDisabled = rest.isLoading || isDisabled || disabled;
const element = href && !isDisabled ? 'a' : 'button';
let elementProps = {};
// Props for all elements
elementProps = { ...elementProps, isDisabled: buttonIsDisabled };
// Element-specific attributes
if (element === 'button') {
elementProps = { ...elementProps, disabled: buttonIsDisabled };
}
const relObj: {
rel?: string;
href?: string;
type?: ButtonHTMLAttributes['type'];
target?: string;
} = {};
if (href && !buttonIsDisabled) {
relObj.href = href;
relObj.rel = getSecureRelForTarget({ href, target, rel });
relObj.target = target;
} else {
relObj.type = type as ButtonHTMLAttributes['type'];
}
return (
);
};