UNPKG

@atlaskit/lozenge

Version:

A lozenge is a visual indicator used to highlight an item's status for quick recognition.

281 lines (278 loc) 13.8 kB
/* lozenge-base.tsx generated by @compiled/babel-plugin v3.0.2 */ import _extends from "@babel/runtime/helpers/extends"; import "./lozenge-base.compiled.css"; import * as React from 'react'; import { ax, ix } from "@compiled/react/runtime"; import { forwardRef, memo, useLayoutEffect, useRef, useState } from 'react'; import Badge from '@atlaskit/badge/badge-new'; import { cx } from '@atlaskit/css'; import ChevronDownIcon from '@atlaskit/icon/core/chevron-down'; import { useResizing } from '@atlaskit/motion/use-resizing'; import { fg } from '@atlaskit/platform-feature-flags/fg'; // eslint-disable-next-line @atlaskit/design-system/no-emotion-primitives -- TODO: migrate to @atlaskit/primitives/compiled import Pressable from '@atlaskit/primitives/pressable'; import Spinner from '@atlaskit/spinner/spinner'; import { getThemeStyles } from './get-theme-styles'; import IconRenderer from './icon-renderer'; import { resolveLozengeColor } from './utils'; // Get the pressed background color for the selected lozenge dropdown trigger const pressedBackgroundMapping = { success: "var(--ds-background-success-subtler-pressed, #B3DF72)", warning: "var(--ds-background-warning-subtler-pressed, #FBC828)", danger: "var(--ds-background-danger-subtler-pressed, #FD9891)", information: "var(--ds-background-information-subtler-pressed, #8FB8F6)", neutral: "var(--ds-background-neutral-pressed, #080F214A)", discovery: "var(--ds-background-discovery-subtler-pressed, #D8A0F7)", 'accent-red': "var(--ds-background-accent-red-subtler-pressed, #FD9891)", 'accent-orange': "var(--ds-background-accent-orange-subtler-pressed, #FBC828)", 'accent-yellow': "var(--ds-background-accent-yellow-subtler-pressed, #EED12B)", 'accent-lime': "var(--ds-background-accent-lime-subtler-pressed, #B3DF72)", 'accent-green': "var(--ds-background-accent-green-subtler-pressed, #7EE2B8)", 'accent-teal': "var(--ds-background-accent-teal-subtler-pressed, #9DD9EE)", 'accent-blue': "var(--ds-background-accent-blue-subtler-pressed, #8FB8F6)", 'accent-purple': "var(--ds-background-accent-purple-subtler-pressed, #D8A0F7)", 'accent-magenta': "var(--ds-background-accent-magenta-subtler-pressed, #F797D2)", 'accent-gray': "var(--ds-background-accent-gray-subtlest-pressed, #B7B9BE)" }; const styles = { container: "_2rko12b0 _1reo15vq _18m915vq _189ee4h9 _1dqonqa1 _1h6d1j28 _kqswh2mm _1e0c116y _4cvr1h6o _vchhusvi _4t3i1k92 _1q51v77o _85i5v77o _bozg1b66 _y4ti1b66", motionContainer: "_k8m01rje _1oeci9qs _6fl41d08", containerSpacious: "_2rko1qi0 _1tkeviql _1q511b66 _85i51b66 _bozgutpp _y4tiutpp", containerBadgePadding: "_y4ti303w", text: "_11c8wadc _1reo15vq _18m915vq _o5721q9c", textEllipsis: "_1bto1l2s", textClip: "_1btozryt", textSpacious: "_11c8fhey _k48p1wq8", textSelected: "_syaz6x5g", loadingOverlay: "_1reo15vq _18m915vq _1e0c1txw _kqswstnw _4cvr1h6o _1bah1h6o _u7coze3t _152tze3t _rjxpze3t _1e02ze3t _lcxvglyw", loadingOverlayMotion: "_j7hq1bym _5sag1ign _1pglindk", metricBadgeWrapper: "_1e0c1txw", 'semantic.success': "_3wu02gvx _bfhk14j0 _syaz1p1e", 'semantic.warning': "_3wu01wfx _bfhk10f4 _syazmgs1", 'semantic.danger': "_3wu01mzp _bfhk18wg _syaz1blx", 'semantic.information': "_3wu0wn4u _bfhk8lbg _syaz1qd9", 'semantic.neutral': "_3wu01l7x _bfhki8nm _syazi7uo", 'semantic.discovery': "_3wu019v3 _bfhke670 _syaz1g72", 'accent.red': "_3wu0n9dc _bfhk1rtt _syazhwvp", 'accent.orange': "_3wu01ip5 _bfhk165s _syaz7i1p", 'accent.yellow': "_3wu01s7j _bfhk1773 _syazekll", 'accent.lime': "_3wu01m7d _bfhkz2ec _syaz1ik3", 'accent.green': "_3wu05d2u _bfhk1wnw _syaz1y78", 'accent.teal': "_3wu0131p _bfhk1a17 _syaz12v7", 'accent.blue': "_3wu018hp _bfhk1dy8 _syaz1kyx", 'accent.purple': "_3wu0luwt _bfhk1nm4 _syaz1mn1", 'accent.magenta': "_3wu0132u _bfhkv9ra _syaz1qur", 'accent.gray': "_3wu01l7x _bfhkysee _syaz1tmo", 'interactive.semantic.success': "_irr31r82 _1di6d6jc", 'interactive.semantic.warning': "_irr31yx4 _1di6bkku", 'interactive.semantic.danger': "_irr370oi _1di6jkmi", 'interactive.semantic.information': "_irr315aw _1di6ar1y", 'interactive.semantic.neutral': "_irr3plhp _1di61gdz", 'interactive.semantic.discovery': "_irr3rumk _1di65usi", 'interactive.accent.red': "_irr3affd _1di61j5a", 'interactive.accent.orange': "_irr31ei0 _1di6dpo3", 'interactive.accent.yellow': "_irr31uw7 _1di61r9w", 'interactive.accent.lime': "_irr31gf0 _1di62kxc", 'interactive.accent.green': "_irr31e2h _1di6ybhx", 'interactive.accent.teal': "_irr3kwwr _1di61jb0", 'interactive.accent.blue': "_irr3815t _1di6rwxf", 'interactive.accent.purple': "_irr3attl _1di63nfk", 'interactive.accent.magenta': "_irr31axx _1di642ri", 'interactive.accent.gray': "_irr3wejn _1di618ut", iconBorderFilter: "_1h6d74of", content: "_zulp1b66 _1e0c116y _4cvr1h6o", maxWidth: "_p12f1osq", contentSpacious: "_zulp12x7", loadingContent: "_tzy4idpf", loadingMotion: "_k8m0brmi _1oec1ign _6fl4indk", containerBadgePressed: "_1axmtoke _17i7toke", containerBadgeNeutralAppearance: "_1ftrnkaw _1axmnkaw _17i7nkaw" }; const interactiveMotionStyles = { motion: "_v564k6bl _1ggl5xtf" }; /** * __New Lozenge__ * * A lozenge is a visual indicator used to highlight an item's status for quick recognition. * This is the updated version with the new North Star visual language. */ const LozengeBase = /*#__PURE__*/memo( /*#__PURE__*/forwardRef(({ children, testId, appearance, iconBefore, trailingMetric, trailingMetricAppearance, maxWidth = 200, spacing = 'default', isSelected, // for dropdown trigger isLoading = false, // for dropdown trigger onClick, // for dropdown trigger style, analyticsContext, interactionName, 'aria-controls': ariaControls, 'aria-expanded': ariaExpanded, 'aria-haspopup': ariaHaspopup, 'aria-label': ariaLabel }, ref) => { const [resizing, setResizing] = useState(false); const onFinishMotion = () => setResizing(false); const resizingWidth = useResizing({ dimension: 'width', duration: "var(--ds-duration-medium, 200ms)", easing: "var(--ds-easing-inout-bold, cubic-bezier(0.4, 0, 0, 1))", onFinishMotion }); const isInitialRender = useRef(true); const isInteractive = typeof onClick === 'function'; // Determine the effective color, with fallback logic for legacy appearances const resolvedColor = resolveLozengeColor(appearance) || 'neutral'; const { category, key } = getThemeStyles(resolvedColor); const colorStyleKey = `${category}.${key}`; const interactiveStyleKey = `interactive.${category}.${key}`; const maxWidthValue = typeof maxWidth === 'string' ? maxWidth : `${maxWidth}px`; const maxWidthIsPc = typeof maxWidth === 'string' && maxWidth.endsWith('%'); const resolvedTrailingMetricAppearance = trailingMetricAppearance ? trailingMetricAppearance === 'inverse' ? 'inverse' : resolveLozengeColor(trailingMetricAppearance) : resolvedColor; // Map the resolved trailing metric appearance to a Badge appearance. // Semantic colors map to their new bold variants (e.g. successBold), // which provide sufficient color emphasis on top of the subtler // lozenge background. Accent colors and unknown values fall back to // the neutral appearance. const metricBadgeAppearance = (() => { if (resolvedTrailingMetricAppearance === 'inverse') { return 'inverse'; } if (resolvedTrailingMetricAppearance == null) { return 'neutral'; } if (resolvedTrailingMetricAppearance.startsWith('accent-')) { return 'neutral'; } switch (resolvedTrailingMetricAppearance) { case 'success': return 'successBold'; case 'warning': return 'warningBold'; case 'danger': return 'dangerBold'; case 'information': return 'informationBold'; case 'discovery': return 'discoveryBold'; case 'neutral': default: return 'neutral'; } })(); const commonStyleOverrides = { backgroundColor: style === null || style === void 0 ? void 0 : style.backgroundColor, // Constrain the container to the smaller of the explicit maxWidth and 100% of // the parent. Using min(...) means the lozenge never overflows its parent, // while still honouring the explicit maxWidth prop when the parent is wider. maxWidth: maxWidthIsPc ? maxWidth : `min(${maxWidthValue}, 100%)` }; const hasTrailingMetric = trailingMetric != null && trailingMetric !== ''; const childrenKey = typeof children === 'string' ? children : undefined; useLayoutEffect(() => { // Ignore initial render if (isInitialRender.current) { isInitialRender.current = false; return; } setResizing(true); }, [childrenKey]); const enableMotionFG = fg('platform-dst-motion-uplift'); const enableButtonMotionFG = fg('platform-dst-motion-uplift-button'); const innerContent = /*#__PURE__*/React.createElement("span", _extends({ "data-testid": testId ? `${testId}--content` : undefined }, enableMotionFG ? resizingWidth : undefined, { className: ax([styles.content, spacing === 'spacious' && styles.contentSpacious, isInteractive && enableButtonMotionFG && styles.loadingMotion, isLoading && styles.loadingContent, // Constrain the content wrapper to its container so text truncation // works correctly within the flex layout, regardless of whether maxWidth // is a percentage, fixed value, or the parent container is narrower than // the explicit maxWidth. styles.maxWidth]) }), iconBefore && /*#__PURE__*/React.createElement(IconRenderer, { size: spacing === 'spacious' ? 'medium' : 'small', icon: iconBefore, color: resolvedColor, testId: testId && `${testId}--icon` }), /*#__PURE__*/React.createElement("span", { style: { // The text fills 100% of the inner content wrapper. The container's // own max-width (set on the container via commonStyleOverrides) handles // the actual size constraint, so the text just needs to fill the available // space and truncate when it overflows. maxWidth: maxWidthIsPc ? '100%' : `calc(${maxWidthValue} - ${"var(--ds-space-100, 8px)"})`, color: style === null || style === void 0 ? void 0 : style.color }, "data-testid": testId && `${testId}--text`, className: ax([styles.text, enableMotionFG && resizing && !maxWidth ? styles.textClip : styles.textEllipsis, spacing === 'spacious' && styles.textSpacious]) }, children), hasTrailingMetric && !resolvedColor.startsWith('accent-') && /*#__PURE__*/React.createElement("span", { "data-lozenge-metric-wrapper": true, "data-testid": testId && `${testId}--metric`, className: ax([styles.metricBadgeWrapper]) }, /*#__PURE__*/React.createElement(Badge, { appearance: metricBadgeAppearance }, trailingMetric)), isInteractive && /*#__PURE__*/React.createElement(ChevronDownIcon, { label: "", size: "small", color: 'currentColor', testId: testId && `${testId}--chevron` })); if (isInteractive) { return /*#__PURE__*/React.createElement(Pressable, _extends({ ref: ref, xcss: cx(styles.container, !isLoading && enableButtonMotionFG ? interactiveMotionStyles.motion : enableMotionFG && styles.motionContainer, spacing === 'spacious' && styles.containerSpacious, !isSelected && styles.iconBorderFilter, styles[colorStyleKey], !isLoading && styles[interactiveStyleKey], hasTrailingMetric && styles.containerBadgePressed, hasTrailingMetric && resolvedTrailingMetricAppearance === 'neutral' && styles.containerBadgeNeutralAppearance) }, isLoading && { 'aria-busy': true, 'aria-disabled': true, isDisabled: true }, { "aria-label": isLoading ? 'Loading' : ariaLabel, "aria-controls": ariaControls, "aria-expanded": ariaExpanded, "aria-haspopup": ariaHaspopup, onClick: isLoading ? undefined : onClick, style: { // eslint-disable-next-line @atlaskit/ui-styling-standard/enforce-style-prop ...commonStyleOverrides, // Specified because Pressable has a default border:none which overrides the border specified on the container // eslint-disable-next-line @atlaskit/ui-styling-standard/enforce-style-prop border: `solid ${"var(--ds-border-width, 1px)"} ${isSelected ? 'var(--border-color) !important' : 'transparent'}`, backgroundColor: isSelected ? pressedBackgroundMapping[resolvedColor] : undefined, cursor: isLoading ? 'progress' : 'pointer' }, testId: testId, analyticsContext: analyticsContext, interactionName: interactionName, componentName: "LozengeDropdownTrigger" // `data-selected` mirrors the `isSelected` prop so CSS can target the // pressed-state badge override via attribute selector. }, isSelected && { 'data-selected': 'true' }), innerContent, isLoading && /*#__PURE__*/React.createElement("span", { "data-testid": testId ? `${testId}--loading-overlay` : undefined, className: ax([styles.loadingOverlay, enableButtonMotionFG && styles.loadingOverlayMotion]) }, /*#__PURE__*/React.createElement(Spinner, { size: spacing === 'spacious' ? 'small' : 'xsmall', label: ", Loading", testId: testId ? `${testId}--loading-spinner` : undefined }))); } return /*#__PURE__*/React.createElement("span", { ref: ref, style: commonStyleOverrides, "data-testid": testId, className: ax([styles.container, spacing === 'spacious' && styles.containerSpacious, spacing !== 'spacious' && hasTrailingMetric && styles.containerBadgePadding, styles[colorStyleKey], styles.iconBorderFilter, hasTrailingMetric && resolvedTrailingMetricAppearance === 'neutral' && styles.containerBadgeNeutralAppearance]) }, innerContent); })); export default LozengeBase;