UNPKG

grommet

Version:

focus on the essential experience

495 lines (486 loc) 20 kB
var _excluded = ["active", "align", "aria-label", "badge", "busy", "color", "children", "disabled", "icon", "focusIndicator", "gap", "fill", "href", "justify", "kind", "label", "messages", "onBlur", "onClick", "onFocus", "onMouseOut", "onMouseOver", "pad", "plain", "primary", "reverse", "secondary", "selected", "size", "success", "tip", "type", "a11yTitle", "as"]; function _extends() { return _extends = Object.assign ? Object.assign.bind() : function (n) { for (var e = 1; e < arguments.length; e++) { var t = arguments[e]; for (var r in t) ({}).hasOwnProperty.call(t, r) && (n[r] = t[r]); } return n; }, _extends.apply(null, arguments); } function _objectWithoutPropertiesLoose(r, e) { if (null == r) return {}; var t = {}; for (var n in r) if ({}.hasOwnProperty.call(r, n)) { if (-1 !== e.indexOf(n)) continue; t[n] = r[n]; } return t; } import React, { cloneElement, Children, forwardRef, useContext, useMemo, useState, useCallback, useEffect } from 'react'; import styled from 'styled-components'; import { backgroundAndTextColors, colorIsDark, findButtonParent, useSizedIcon, normalizeBackground, normalizeColor } from '../../utils'; import { ButtonPropTypes } from './propTypes'; import { AnnounceContext } from '../../contexts/AnnounceContext'; import { MessageContext } from '../../contexts/MessageContext'; import { Box } from '../Box'; import { Tip } from '../Tip'; import { Badge } from './Badge'; import { StyledButton } from './StyledButton'; import { StyledButtonKind } from './StyledButtonKind'; import { useAnalytics } from '../../contexts/AnalyticsContext'; import { Skeleton, useSkeleton } from '../Skeleton'; import { EllipsisAnimation, GrowCheckmark, StyledBusyContents } from './BusyAnimation'; import { useThemeValue } from '../../utils/useThemeValue'; var RelativeBox = styled(Box).withConfig({ displayName: "Button__RelativeBox", componentId: "sc-zuqsuw-0" })(["position:relative;"]); // We have two Styled* components to separate // the newer default|primary|secondary approach, // which we use the term "kind" to refer to, // from the previous approach. Hopefully, when we get to grommet v3, // we can drop the old way and just use kind. // // In the kind approach, we rely on the basic structure of the theme // being repeated. For example: button.default, button.active, // button.active.default all refer to a similar object containing // { background, border, color, padding }. // This allows us to use the same code to evaluate color and generate CSS. // We just need to build up CSS, since selectors override previous ones. // See StyledButtonKind.kindStyles() for this. // And we build down to determine icon color, once we have a color from // the latest applicable state, we can stop. See Button.getIconColor() for this. // backgroundAndTextColor() is used in both cases to ensure we are determining // color in the same way, so the icon and label align. // only when default is in the theme // Used to get the color for the icon to match what StyledButtonKind // and backgroundStyle() will do for the label. // The paths are ordered from basic to specific. Go through them // specific to base until we find one that has a color and use that. var getIconColor = function getIconColor(paths, theme, colorProp, kind) { if (paths === void 0) { paths = []; } var result = []; var index = paths.length - 1; // caller has specified a themeObj to use for styling // relevant for cases like pagination which looks to theme.pagination.button if (typeof kind === 'object') index = 0; // stop when we have a color or no more paths while (index >= 0 && !result[1]) { var baseObj = typeof kind === 'object' && kind || theme.button; var obj = baseObj; // find sub-object under the button theme that corresponds with this path // for example: 'active.primary' if (paths[index]) { var parts = paths[index].split('.'); while (obj && parts.length) obj = obj[parts.shift()]; } if (obj) { var _obj; // use passed in color for background if the theme has a background color var background = colorProp && obj.background && obj.background.color ? colorProp : obj.background; // if theme object explicitly sets the color to undefined, pass false // to indicate that the theme doesn't want any text color var objColor = obj.color || (Object.prototype.hasOwnProperty.call(obj, 'color') && obj.color === undefined ? false : undefined); var color = void 0; if ((_obj = obj) != null && (_obj = _obj.icon) != null && (_obj = _obj.props) != null && _obj.color) color = obj.icon.props.color; // if no icon defined for this state, see if there is an icon // with color defined at one higher level else if (paths[index + 1]) { var _obj2; var _parts = paths[index + 1].split('.'); while (baseObj && _parts.length) obj = baseObj[_parts.shift()]; if ((_obj2 = obj) != null && (_obj2 = _obj2.icon) != null && (_obj2 = _obj2.props) != null && _obj2.color) color = obj.icon.props.color; } // use passed in color for text if the theme doesn't have // background or border color if (!color) color = colorProp && (!obj.background || !obj.background.color) && (!obj.border || !obj.border.color) ? colorProp : objColor; result = backgroundAndTextColors(background, color, theme); } index -= 1; } return result[1] || undefined; }; // get the icon for the current button state var getKindIcon = function getKindIcon(paths, theme, kind) { if (paths === void 0) { paths = []; } var result; var index = paths.length - 1; // caller has specified a themeObj to use for styling // relevant for cases like pagination which looks to theme.pagination.button if (typeof kind === 'object') index = 0; // stop when we have a color or no more paths while (index >= 0 && !result) { var _obj3; var obj = typeof kind === 'object' && kind || theme.button; // find sub-object under the button theme that corresponds with this path // for example: 'active.primary' if (paths[index]) { var parts = paths[index].split('.'); while (obj && parts.length) obj = obj[parts.shift()]; } if ((_obj3 = obj) != null && _obj3.icon) result = obj.icon; index -= 1; } return result || undefined; }; var getPropertyColor = function getPropertyColor(property, paths, theme, kind, primary) { if (paths === void 0) { paths = []; } var result; if (kind) { var obj = typeof kind === 'object' && kind || theme.button; // index 0 is default state if (paths[0]) { var parts = paths[0].split('.'); while (obj && parts.length) obj = obj[parts.shift()]; } if (obj) { result = obj[property] || obj[property] && obj[property].color; } } else if (primary && theme && theme.button && theme.button.primary) { result = theme.button.primary[property] || theme.button.primary[property] && theme.button.primary[property].color; } else { result = theme && theme.button && theme.button[property] || theme && theme.button && theme.button[property] && theme.button[property].color; } return result; }; var Button = /*#__PURE__*/forwardRef(function (_ref, ref) { var _theme$button$kind, _theme$button2; var active = _ref.active, _ref$align = _ref.align, align = _ref$align === void 0 ? 'center' : _ref$align, ariaLabel = _ref['aria-label'], badgeProp = _ref.badge, busy = _ref.busy, color = _ref.color, children = _ref.children, disabled = _ref.disabled, icon = _ref.icon, _ref$focusIndicator = _ref.focusIndicator, focusIndicator = _ref$focusIndicator === void 0 ? true : _ref$focusIndicator, gap = _ref.gap, fill = _ref.fill, href = _ref.href, justify = _ref.justify, kindArg = _ref.kind, label = _ref.label, messages = _ref.messages, _onBlur = _ref.onBlur, onClickProp = _ref.onClick, _onFocus = _ref.onFocus, onMouseOut = _ref.onMouseOut, onMouseOver = _ref.onMouseOver, pad = _ref.pad, plain = _ref.plain, primary = _ref.primary, reverseProp = _ref.reverse, secondary = _ref.secondary, selected = _ref.selected, sizeProp = _ref.size, success = _ref.success, tip = _ref.tip, _ref$type = _ref.type, type = _ref$type === void 0 ? 'button' : _ref$type, _ref$a11yTitle = _ref.a11yTitle, a11yTitle = _ref$a11yTitle === void 0 ? typeof tip === 'string' ? tip : undefined : _ref$a11yTitle, as = _ref.as, rest = _objectWithoutPropertiesLoose(_ref, _excluded); var _useThemeValue = useThemeValue(), theme = _useThemeValue.theme, passThemeFlag = _useThemeValue.passThemeFlag; var _useState = useState(), focus = _useState[0], setFocus = _useState[1]; var _useState2 = useState(false), hover = _useState2[0], setHover = _useState2[1]; var announce = useContext(AnnounceContext); var _useContext = useContext(MessageContext), format = _useContext.format; if (busy && success) { console.warn('Button cannot have both busy and success set to true.'); } useEffect(function () { if (busy) announce(format({ id: 'button.busy', messages: messages }));else if (success) announce(format({ id: 'button.success', messages: messages })); }, [announce, busy, format, messages, success]); if ((icon || label) && children) { console.warn('Button should not have children if icon or label is provided'); } var skeleton = useSkeleton(); var sendAnalytics = useAnalytics(); var onClick = useCallback(function (event) { sendAnalytics({ type: 'buttonClick', element: findButtonParent(event.target), event: event, href: href, label: typeof label === 'string' ? label : undefined }); if (onClickProp) onClickProp(event); }, [onClickProp, sendAnalytics, href, label]); // kindArg is object if we are referencing a theme object // outside of theme.button var kindObj = useMemo(function () { return typeof kindArg === 'object'; }, [kindArg]); // if the theme has button.default, what kind of Button is this var kind = useMemo(function () { if (theme.button["default"] || kindObj) { if (kindArg) return kindArg; if (primary) return 'primary'; if (secondary) return 'secondary'; return 'default'; } return undefined; // pre-default, no kind }, [kindArg, kindObj, primary, secondary, theme]); // for backwards compatibility, no-kind button theme did not // default to size "medium" on buttons with no size prop var size = sizeProp || kind && 'medium' || undefined; // When we have a kind and are not plain, themePaths stores the relative // paths within the theme for the current kind and state of the button. // These paths are used with getIconColor() above and kindStyle() within // StyledButtonKind. var themePaths = useMemo(function () { if (!kind || plain) return undefined; var result = { base: [], hover: [] }; if (!kindObj) result.base.push(kind); if (selected) { result.base.push('selected'); if (!kindObj) result.base.push("selected." + kind); } if (disabled) { result.base.push('disabled'); if (!kindObj) result.base.push("disabled." + kind); } else { if (active) { result.base.push('active'); if (!kindObj) result.base.push("active." + kind); } result.hover.push('hover'); if (!kindObj) result.hover.push("hover." + kind); if (active) { result.hover.push("hover.active"); if (!kindObj) { result.hover.push("hover.active." + kind); } } } return result; }, [active, disabled, kind, kindObj, plain, selected]); // only used when theme does not have button.default var isDarkBackground = function isDarkBackground() { var backgroundColor = normalizeBackground(normalizeColor(color || theme.button.primary && theme.button.primary.color || theme.global.colors.control || 'brand', theme), theme); return colorIsDark(backgroundColor, theme); }; var onMouseOverButton = function onMouseOverButton(event) { setHover(true); if (onMouseOver) { onMouseOver(event); } }; var onMouseOutButton = function onMouseOutButton(event) { setHover(false); if (onMouseOut) { onMouseOut(event); } }; var kindIcon = hover && getKindIcon(themePaths == null ? void 0 : themePaths.hover, theme, kind) || getKindIcon(themePaths == null ? void 0 : themePaths.base, theme, kind); var buttonIcon = icon || kindIcon; // only change color if user did not specify the color themselves... if (icon && !icon.props.color) { if (kind) { if (!plain) { // match what the label will use var iconColor = hover && getIconColor(themePaths.hover, theme) || getIconColor(themePaths.base, theme, color, kind); if (iconColor) buttonIcon = /*#__PURE__*/cloneElement(icon, { color: iconColor }); } } else if (primary) { buttonIcon = /*#__PURE__*/cloneElement(icon, { color: theme.global.colors.text[isDarkBackground() ? 'dark' : 'light'] }); } } else if (kindIcon && !plain) { var _iconColor = hover && getIconColor(themePaths.hover, theme) || getIconColor(themePaths.base, theme, color, kind); if (_iconColor) buttonIcon = /*#__PURE__*/cloneElement(kindIcon, { color: _iconColor }); } buttonIcon = useSizedIcon(buttonIcon, size, theme); if (skeleton) { var _theme$text, _theme$button$size; return /*#__PURE__*/React.createElement(Skeleton, _extends({ ref: ref, height: ((_theme$text = theme.text[size || 'medium']) == null ? void 0 : _theme$text.height) || size, a11yTitle: a11yTitle }, rest, (_theme$button$size = theme.button.size) == null ? void 0 : _theme$button$size[size || 'medium'], theme.button.skeleton)); } var reverse = reverseProp != null ? reverseProp : (_theme$button$kind = theme.button[kind]) == null ? void 0 : _theme$button$kind.reverse; var domTag = !as && href ? 'a' : as; var first = reverse ? label : buttonIcon; var second = reverse ? buttonIcon : label; var contents; if (first && second) { var _theme$button; contents = /*#__PURE__*/React.createElement(Box, { direction: ((_theme$button = theme.button) == null || (_theme$button = _theme$button[kind]) == null ? void 0 : _theme$button.direction) || 'row', align: "center", justify: justify || (align === 'center' ? 'center' : 'between'), gap: gap || theme.button.gap, responsive: false }, first, second); } else if (typeof children === 'function') { contents = children({ active: active, disabled: disabled, hover: hover, focus: focus }); } else { contents = first || second || children; } var background = getPropertyColor('background', themePaths && themePaths.base, theme, kind, primary); var border = getPropertyColor('border', themePaths && themePaths.base, theme, kind, primary); // set the badge relative to the button content // when the button doesn't have background or border // (!kind && icon && !label) is necessary because for old button logic, // if button has icon but not label, it will be considered "plain", // so no border or background will be applied var innerBadge = ((_theme$button2 = theme.button) == null || (_theme$button2 = _theme$button2.badge) == null ? void 0 : _theme$button2.align) !== 'container' && (!background && !border || !kind && icon && !label); if (badgeProp && innerBadge) { contents = /*#__PURE__*/React.createElement(Badge, { content: badgeProp }, contents); } if (busy || success) { var _theme$button3; // match what the label will use var animationColor; if (kind) { if (!plain) { animationColor = hover && getIconColor(themePaths.hover, theme) || getIconColor(themePaths.base, theme, color, kind); } } else if (primary) { animationColor = theme.global.colors.text[isDarkBackground() ? 'dark' : 'light']; } contents = /*#__PURE__*/ // position relative is necessary to have the animation // display over the button content React.createElement(RelativeBox, { flex: false }, busy && /*#__PURE__*/React.createElement(EllipsisAnimation, null), success && /*#__PURE__*/React.createElement(Box, { style: { position: 'absolute' }, fill: true, alignContent: "center", justify: "center" }, /*#__PURE__*/React.createElement(GrowCheckmark, { color: animationColor, "aria-hidden": true, as: (_theme$button3 = theme.button) == null || (_theme$button3 = _theme$button3.busy) == null || (_theme$button3 = _theme$button3.icons) == null ? void 0 : _theme$button3.success })), /*#__PURE__*/React.createElement(StyledBusyContents, { animating: busy || success }, contents)); } var styledButtonResult; if (kind) { styledButtonResult = /*#__PURE__*/React.createElement(StyledButtonKind, _extends({}, rest, { as: domTag, ref: ref, active: active, align: align, "aria-label": ariaLabel || a11yTitle, busy: busy, badge: badgeProp, colorValue: color, disabled: disabled, hasIcon: !!icon, gap: gap, hasLabel: !!label, icon: icon, fillContainer: fill, focus: focus, focusIndicator: focusIndicator, href: href, kind: kind, themePaths: themePaths, onClick: !busy && !success ? onClick : undefined, onFocus: function onFocus(event) { setFocus(true); if (_onFocus) _onFocus(event); }, onBlur: function onBlur(event) { setFocus(false); if (_onBlur) _onBlur(event); }, onMouseOver: onMouseOverButton, onMouseOut: onMouseOutButton, pad: pad, plain: plain || Children.count(children) > 0, primary: primary, selected: selected, sizeProp: size, success: success, type: !href ? type : undefined }, passThemeFlag), contents); } else { styledButtonResult = /*#__PURE__*/React.createElement(StyledButton, _extends({}, rest, { as: domTag, ref: ref, "aria-label": ariaLabel || a11yTitle, busy: busy, colorValue: color, active: active, selected: selected, disabled: disabled, hasIcon: !!icon, gap: gap, hasLabel: !!label, fillContainer: fill, focus: focus, focusIndicator: focusIndicator, href: href, kind: kind, themePaths: themePaths, onClick: !busy && !success ? onClick : undefined, onFocus: function onFocus(event) { setFocus(true); if (_onFocus) _onFocus(event); }, onBlur: function onBlur(event) { setFocus(false); if (_onBlur) _onBlur(event); }, onMouseOver: onMouseOverButton, onMouseOut: onMouseOutButton, pad: pad || !plain, plain: typeof plain !== 'undefined' ? plain : Children.count(children) > 0 || icon && !label, primary: primary, sizeProp: size, success: success, type: !href ? type : undefined }, passThemeFlag), contents); } if (tip) { if (typeof tip === 'string') { styledButtonResult = /*#__PURE__*/React.createElement(Tip, { content: tip }, styledButtonResult); } else { styledButtonResult = /*#__PURE__*/React.createElement(Tip, tip, styledButtonResult); } } // if button has background or border, place badge relative // to outer edge of button if (badgeProp && !innerBadge) { styledButtonResult = /*#__PURE__*/React.createElement(Badge, { content: badgeProp }, styledButtonResult); } return styledButtonResult; }); Button.displayName = 'Button'; Button.propTypes = ButtonPropTypes; export { Button };