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

170 lines • 11.5 kB
import { __rest } from "tslib"; // 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 { warnOnce } 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 { useSingleTabStopNavigation } from '../internal/context/single-tab-stop-navigation-context'; import { fireCancelableEvent, isPlainLeftClick } from '../internal/events'; import useForwardFocus from '../internal/hooks/forward-focus'; import useHiddenDescription from '../internal/hooks/use-hidden-description'; import { useMergeRefs } from '../internal/hooks/use-merge-refs'; import { useModalContextLoadingButtonComponent } from '../internal/hooks/use-modal-component-analytics'; import { usePerformanceMarks } from '../internal/hooks/use-performance-marks'; import { useUniqueId } from '../internal/hooks/use-unique-id'; import { checkSafeUrl } from '../internal/utils/check-safe-url'; import InternalLiveRegion from '../live-region/internal'; import { LeftIcon, RightIcon } from './icon-helper'; 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((_a, ref) => { var { 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, __nativeAttributes, __internalRootRef = null, __focusable = false, __injectAnalyticsComponentMetadata = false, __title, __emitPerformanceMarks = true, analyticsAction = 'click' } = _a, props = __rest(_a, ["children", "iconName", "__iconClass", "onClick", "onFollow", "iconAlign", "iconUrl", "iconSvg", "iconAlt", "variant", "loading", "loadingText", "disabled", "disabledReason", "wrapText", "href", "external", "target", "rel", "download", "formAction", "ariaLabel", "ariaDescribedby", "ariaExpanded", "ariaControls", "fullWidth", "badge", "i18nStrings", "__nativeAttributes", "__internalRootRef", "__focusable", "__injectAnalyticsComponentMetadata", "__title", "__emitPerformanceMarks", "analyticsAction"]); 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') && !!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 = __nativeAttributes && 'tabIndex' in __nativeAttributes ? __nativeAttributes.tabIndex : undefined; 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 = Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({}, props), __nativeAttributes), 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(children ? `.${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, Object.assign({}, 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, Object.assign({}, iconProps)))); const { loadingButtonCount } = useFunnel(); useEffect(() => { if (loading) { loadingButtonCount.current++; return () => { // eslint-disable-next-line react-hooks/exhaustive-deps loadingButtonCount.current--; }; } }, [loading, loadingButtonCount]); const disabledReasonProps = Object.assign({ 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) })))); if (isAnchor) { return ( // https://github.com/yannickcr/eslint-plugin-react/issues/2962 // eslint-disable-next-line react/jsx-no-target-blank React.createElement(React.Fragment, null, React.createElement("a", Object.assign({}, buttonProps, { href: href, 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 }, disabledReasonProps), buttonContent, isDisabledWithReason && disabledReasonContent), loading && loadingText && (React.createElement(InternalLiveRegion, { tagName: "span", hidden: true }, loadingText)))); } return (React.createElement(React.Fragment, null, React.createElement("button", Object.assign({}, buttonProps, { type: formAction === 'none' ? 'button' : 'submit', disabled: disabled && !__focusable && !isDisabledWithReason, "aria-disabled": hasAriaDisabled ? true : undefined }, disabledReasonProps), buttonContent, isDisabledWithReason && disabledReasonContent), loading && loadingText && (React.createElement(InternalLiveRegion, { tagName: "span", hidden: true }, loadingText)))); }); export default InternalButton; //# sourceMappingURL=internal.js.map