UNPKG

@awsui/components-react

Version:

On July 19th, 2022, we launched [Cloudscape Design System](https://cloudscape.design). Cloudscape is an evolution of AWS-UI. It consists of user interface guidelines, front-end components, design resources, and development tools for building intuitive, en

194 lines • 11.9 kB
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 import React, { useEffect, useRef, useState } from 'react'; import clsx from 'clsx'; import { useMergeRefs, useUniqueId, warnOnce } from '@awsui/component-toolkit/internal'; import { useSingleTabStopNavigation } from '@awsui/component-toolkit/internal'; import { getAnalyticsLabelAttribute, getAnalyticsMetadataAttribute, } from '@awsui/component-toolkit/internal/analytics-metadata'; import { useInternalI18n } from '../i18n/context'; import Icon from '../icon/internal'; import { FunnelMetrics } from '../internal/analytics'; import { useFunnel, useFunnelStep, useFunnelSubStep } from '../internal/analytics/hooks/use-funnel'; import { DATA_ATTR_FUNNEL_VALUE, getFunnelValueSelector, getSubStepAllSelector, getTextFromSelector, } from '../internal/analytics/selectors'; import Tooltip from '../internal/components/tooltip/index.js'; import { useButtonContext } from '../internal/context/button-context'; import { fireCancelableEvent, isPlainLeftClick } from '../internal/events'; import useForwardFocus from '../internal/hooks/forward-focus'; import useHiddenDescription from '../internal/hooks/use-hidden-description'; import { useModalContextLoadingButtonComponent } from '../internal/hooks/use-modal-component-analytics'; import { usePerformanceMarks } from '../internal/hooks/use-performance-marks'; import { checkSafeUrl } from '../internal/utils/check-safe-url'; import WithNativeAttributes from '../internal/utils/with-native-attributes'; import InternalLiveRegion from '../live-region/internal'; import { LeftIcon, RightIcon } from './icon-helper'; import { getButtonStyles } from './style'; import analyticsSelectors from './analytics-metadata/styles.css.js'; import styles from './styles.css.js'; import testUtilStyles from './test-classes/styles.css.js'; export const InternalButton = React.forwardRef(({ children, iconName, __iconClass, onClick, onFollow, iconAlign = 'left', iconUrl, iconSvg, iconAlt, variant = 'normal', loading = false, loadingText, disabled = false, disabledReason, wrapText = true, href, external, target: targetOverride, rel, download, formAction = 'submit', ariaLabel, ariaDescribedby, ariaExpanded, ariaControls, fullWidth, badge, i18nStrings, style, nativeButtonAttributes, nativeAnchorAttributes, __internalRootRef, __focusable = false, __injectAnalyticsComponentMetadata = false, __title, __emitPerformanceMarks = true, __skipNativeAttributesWarnings, analyticsAction = 'click', ...props }, ref) => { var _a; const [showTooltip, setShowTooltip] = useState(false); checkSafeUrl('Button', href); const isAnchor = Boolean(href); const target = targetOverride !== null && targetOverride !== void 0 ? targetOverride : (external ? '_blank' : undefined); const isNotInteractive = loading || disabled; const isDisabledWithReason = (variant === 'normal' || variant === 'primary' || variant === 'icon') && !!disabledReason && disabled; const hasAriaDisabled = (loading && !disabled) || (disabled && __focusable) || isDisabledWithReason; const shouldHaveContent = children && ['icon', 'inline-icon', 'flashbar-icon', 'modal-dismiss', 'inline-icon-pointer-target'].indexOf(variant) === -1; if ((iconName || iconUrl || iconSvg) && iconAlign === 'right' && external) { warnOnce('Button', 'A right-aligned icon should not be combined with an external icon.'); } const buttonRef = useRef(null); useForwardFocus(ref, buttonRef); const buttonContext = useButtonContext(); const i18n = useInternalI18n('button'); const uniqueId = useUniqueId('button'); const { funnelInteractionId } = useFunnel(); const { stepNumber, stepNameSelector } = useFunnelStep(); const { subStepSelector, subStepNameSelector } = useFunnelSubStep(); const performanceMarkAttributes = usePerformanceMarks('primaryButton', () => variant === 'primary' && __emitPerformanceMarks && !loading && !disabled, buttonRef, () => { var _a; return ({ loading, disabled, text: (_a = buttonRef.current) === null || _a === void 0 ? void 0 : _a.innerText, }); }, [loading, disabled]); useModalContextLoadingButtonComponent(variant === 'primary', loading); const { targetProps, descriptionEl } = useHiddenDescription(disabledReason); const handleClick = (event) => { if (isNotInteractive) { return event.preventDefault(); } if (isAnchor && isPlainLeftClick(event)) { fireCancelableEvent(onFollow, { href, target }, event); if ((iconName === 'external' || target === '_blank') && funnelInteractionId) { const stepName = getTextFromSelector(stepNameSelector); const subStepName = getTextFromSelector(subStepNameSelector); FunnelMetrics.externalLinkInteracted({ funnelInteractionId, stepNumber, stepName, stepNameSelector, subStepSelector, subStepName, subStepNameSelector, elementSelector: getFunnelValueSelector(uniqueId), subStepAllSelector: getSubStepAllSelector(), }); } } const { altKey, button, ctrlKey, metaKey, shiftKey } = event; fireCancelableEvent(onClick, { altKey, button, ctrlKey, metaKey, shiftKey }, event); buttonContext.onClick({ variant }); }; const buttonClass = clsx(props.className, styles.button, styles[`variant-${variant}`], { [styles.disabled]: isNotInteractive, [styles['disabled-with-reason']]: isDisabledWithReason, [styles['button-no-wrap']]: !wrapText, [styles['button-no-text']]: !shouldHaveContent, [styles['full-width']]: shouldHaveContent && fullWidth, [styles.link]: isAnchor, }); const explicitTabIndex = (_a = nativeButtonAttributes === null || nativeButtonAttributes === void 0 ? void 0 : nativeButtonAttributes.tabIndex) !== null && _a !== void 0 ? _a : nativeAnchorAttributes === null || nativeAnchorAttributes === void 0 ? void 0 : nativeAnchorAttributes.tabIndex; const { tabIndex } = useSingleTabStopNavigation(buttonRef, { tabIndex: isAnchor && isNotInteractive && !isDisabledWithReason ? -1 : explicitTabIndex, }); const analyticsMetadata = disabled ? {} : { action: analyticsAction, detail: { label: { root: 'self' } }, }; if (__injectAnalyticsComponentMetadata) { analyticsMetadata.component = { name: 'awsui.Button', label: { root: 'self' }, properties: { variant, disabled: `${disabled}` }, }; } const buttonProps = { ...props, ...performanceMarkAttributes, tabIndex, // https://github.com/microsoft/TypeScript/issues/36659 ref: useMergeRefs(buttonRef, __internalRootRef), 'aria-label': ariaLabel, 'aria-describedby': ariaDescribedby, 'aria-expanded': ariaExpanded, 'aria-controls': ariaControls, // add ariaLabel as `title` as visible hint text title: __title !== null && __title !== void 0 ? __title : ariaLabel, className: buttonClass, onClick: handleClick, [DATA_ATTR_FUNNEL_VALUE]: uniqueId, ...getAnalyticsMetadataAttribute(analyticsMetadata), ...getAnalyticsLabelAttribute(shouldHaveContent ? `.${analyticsSelectors.label}` : ''), }; const iconProps = { loading, iconName, iconAlign, iconUrl, iconSvg, iconAlt, variant, badge, iconClass: __iconClass, iconSize: variant === 'modal-dismiss' ? 'medium' : 'normal', }; const buttonContent = (React.createElement(React.Fragment, null, React.createElement(LeftIcon, { ...iconProps }), shouldHaveContent && (React.createElement(React.Fragment, null, React.createElement("span", { className: clsx(styles.content, analyticsSelectors.label) }, children), external && (React.createElement(React.Fragment, null, "\u00A0", React.createElement(Icon, { name: "external", className: testUtilStyles['external-icon'], ariaLabel: i18n('i18nStrings.externalIconAriaLabel', i18nStrings === null || i18nStrings === void 0 ? void 0 : i18nStrings.externalIconAriaLabel) }))))), React.createElement(RightIcon, { ...iconProps }))); const { loadingButtonCount } = useFunnel(); useEffect(() => { if (loading) { loadingButtonCount.current++; return () => { // eslint-disable-next-line react-hooks/exhaustive-deps loadingButtonCount.current--; }; } }, [loading, loadingButtonCount]); const disabledReasonProps = { onFocus: isDisabledWithReason ? () => setShowTooltip(true) : undefined, onBlur: isDisabledWithReason ? () => setShowTooltip(false) : undefined, onMouseEnter: isDisabledWithReason ? () => setShowTooltip(true) : undefined, onMouseLeave: isDisabledWithReason ? () => setShowTooltip(false) : undefined, ...(isDisabledWithReason ? targetProps : {}), }; const disabledReasonContent = (React.createElement(React.Fragment, null, descriptionEl, showTooltip && (React.createElement(Tooltip, { className: testUtilStyles['disabled-reason-tooltip'], trackRef: buttonRef, value: disabledReason, onDismiss: () => setShowTooltip(false) })))); const stylePropertiesAndVariables = getButtonStyles(style); if (isAnchor) { const getAnchorTabIndex = () => { if (isNotInteractive) { // If disabled with a reason, make it focusable so users can access the tooltip // Otherwise, resolve to the default button props tabIndex. return disabledReason ? 0 : buttonProps.tabIndex; } return buttonProps.tabIndex; }; return (React.createElement(React.Fragment, null, React.createElement(WithNativeAttributes, { ...buttonProps, ...disabledReasonProps, tag: "a", componentName: "Button", nativeAttributes: nativeAnchorAttributes, skipWarnings: __skipNativeAttributesWarnings, href: isNotInteractive ? undefined : href, role: isNotInteractive ? 'link' : undefined, tabIndex: getAnchorTabIndex(), target: target, // security recommendation: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/a#target rel: rel !== null && rel !== void 0 ? rel : (target === '_blank' ? 'noopener noreferrer' : undefined), "aria-disabled": isNotInteractive ? true : undefined, download: download, style: stylePropertiesAndVariables }, buttonContent, isDisabledWithReason && disabledReasonContent), loading && loadingText && (React.createElement(InternalLiveRegion, { tagName: "span", hidden: true }, loadingText)))); } return (React.createElement(React.Fragment, null, React.createElement(WithNativeAttributes, { ...buttonProps, ...disabledReasonProps, tag: "button", componentName: "Button", nativeAttributes: nativeButtonAttributes, skipWarnings: __skipNativeAttributesWarnings, type: formAction === 'none' ? 'button' : 'submit', disabled: disabled && !__focusable && !isDisabledWithReason, "aria-disabled": hasAriaDisabled ? true : undefined, style: stylePropertiesAndVariables }, buttonContent, isDisabledWithReason && disabledReasonContent), loading && loadingText && (React.createElement(InternalLiveRegion, { tagName: "span", hidden: true }, loadingText)))); }); export default InternalButton; //# sourceMappingURL=internal.js.map