UNPKG

@atlaskit/profilecard

Version:

A React component to display a card with user information.

401 lines 15.2 kB
import _extends from "@babel/runtime/helpers/extends"; import React, { Suspense, useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { useIntl } from 'react-intl-next'; import { GiveKudosLauncherLazy, KudosType } from '@atlaskit/give-kudos'; import { fg } from '@atlaskit/platform-feature-flags'; import Popup from '@atlaskit/popup'; import { useAnalyticsEvents } from '@atlaskit/teams-app-internal-analytics'; import { layers } from '@atlaskit/theme/constants'; import { expValEquals } from '@atlaskit/tmp-editor-statsig/exp-val-equals'; import filterActionsInner from '../../internal/filterActions'; import getLabelMessage from '../../internal/getLabelMessage'; import { CardWrapper } from '../../styled/UserTrigger'; import { PACKAGE_META_DATA } from '../../util/analytics'; import { DELAY_MS_HIDE, DELAY_MS_SHOW } from '../../util/config'; import { getPageTime } from '../../util/performance'; import { AgentProfileCardResourced } from '../Agent/AgentProfileCardResourced'; import { ProfileCardLazy } from './lazyProfileCard'; import UserLoadingState from './UserLoadingState'; function ProfileCardContent({ profilecardProps, userId, cloudId, resourceClient, trigger, isAgent, profileCardAction, hasError, errorType, agentActions, isRenderedInPortal, addFlag, hideAgentMoreActions, hideAiDisclaimer }) { if (isAgent) { return /*#__PURE__*/React.createElement(AgentProfileCardResourced, { accountId: userId, cloudId: cloudId, resourceClient: resourceClient, trigger: trigger, onChatClick: agentActions === null || agentActions === void 0 ? void 0 : agentActions.onChatClick, onConversationStartersClick: agentActions === null || agentActions === void 0 ? void 0 : agentActions.onConversationStartersClick, addFlag: addFlag, hideMoreActions: fg('jira_ai_profilecard_hide_agent_actions') && !!hideAgentMoreActions, hideConversationStarters: fg('jira_ai_hide_conversation_starters_profilecard') && !!profilecardProps.hideAgentConversationStarters, hideAiDisclaimer: hideAiDisclaimer }); } else { return /*#__PURE__*/React.createElement(Suspense, { fallback: null }, /*#__PURE__*/React.createElement(ProfileCardLazy, _extends({}, profilecardProps, { isRenderedInPortal: isRenderedInPortal, actions: profileCardAction, hasError: hasError, errorType: errorType, withoutElevation: true }))); } } export default function ProfilecardTriggerNext({ autoFocus, trigger = 'hover', userId, cloudId, resourceClient, actions = [], position = 'bottom-start', children, testId, addFlag, onReportingLinesClick, ariaLabel, ariaLabelledBy, prepopulatedData, disabledAriaAttributes, onVisibilityChange, offset, viewingUserId, product, agentActions, hideAgentMoreActions, hideAiDisclaimer, hideAgentConversationStarters, hideReportingLines, ariaHideProfileTrigger = false, isVisible: propsIsVisible, isRenderedInPortal, ssrPlaceholderId, showDelay: customShowDelay, hideDelay: customHideDelay }) { const { formatMessage } = useIntl(); const isMounted = useRef(false); const [visible, setVisible] = useState(false); const showTimer = useRef(0); const hideTimer = useRef(0); const isExternalControl = propsIsVisible !== undefined && propsIsVisible !== visible; const showDelay = trigger === 'click' || isExternalControl && fg('fix_profilecard_trigger_isvisible') ? 0 : customShowDelay !== null && customShowDelay !== void 0 ? customShowDelay : DELAY_MS_SHOW; const hideDelay = trigger === 'click' || isExternalControl && fg('fix_profilecard_trigger_isvisible') ? 0 : customHideDelay !== null && customHideDelay !== void 0 ? customHideDelay : DELAY_MS_HIDE; const [isLoading, setIsLoading] = useState(undefined); const [hasError, setHasError] = useState(false); const [error, setError] = useState(null); const [data, setData] = useState(null); const [reportingLinesData, setReportingLinesData] = useState(undefined); const [shouldShowGiveKudos, setShouldShowGiveKudos] = useState(false); const [teamCentralBaseUrl, setTeamCentralBaseUrl] = useState(undefined); const [kudosDrawerOpen, setKudosDrawerOpen] = useState(false); const [isTriggeredUsingKeyboard, setTriggeredUsingKeyboard] = useState(false); const triggerRef = useRef(null); const { fireEvent } = useAnalyticsEvents(); useEffect(() => { isMounted.current = true; return () => { isMounted.current = false; clearTimeout(showTimer.current); clearTimeout(hideTimer.current); }; }, []); useEffect(() => { // Reset state when the userId changes setIsLoading(undefined); setHasError(false); setError(null); setData(null); setReportingLinesData(undefined); setShouldShowGiveKudos(false); setTeamCentralBaseUrl(undefined); }, [userId]); const fireAnalytics = useCallback((eventKey, ...attributes) => { if (!isMounted.current) { return; } fireEvent(eventKey, ...attributes); }, [fireEvent]); const hideProfilecard = useCallback(() => { clearTimeout(showTimer.current); clearTimeout(hideTimer.current); if (!isTriggeredUsingKeyboard) { hideTimer.current = window.setTimeout(() => { setVisible(false); onVisibilityChange && onVisibilityChange(false); }, hideDelay); } }, [hideDelay, isTriggeredUsingKeyboard, onVisibilityChange]); const handleKeyboardClose = useCallback(event => { if (event.key && event.key !== 'Escape') { return; } if (triggerRef.current) { triggerRef.current.focus(); } setTriggeredUsingKeyboard(false); setVisible(false); onVisibilityChange && onVisibilityChange(false); }, [setTriggeredUsingKeyboard, setVisible, onVisibilityChange]); const handleClientSuccess = useCallback((profileData, reportingLinesData, shouldShowGiveKudos, teamCentralBaseUrl) => { if (!isMounted.current) { return; } setIsLoading(false); setHasError(false); setData(profileData); setReportingLinesData(reportingLinesData); setTeamCentralBaseUrl(teamCentralBaseUrl); setShouldShowGiveKudos(shouldShowGiveKudos); }, [setHasError, setIsLoading, setData, setReportingLinesData, setShouldShowGiveKudos]); const handleClientError = useCallback(err => { if (!isMounted.current) { return; } setIsLoading(false); setHasError(true); setError(err); }, [setHasError, setIsLoading, setError]); const clientFetchProfile = useCallback(async () => { if (isLoading === true) { // don't fetch data when fetching is in process return; } setIsLoading(true); setHasError(false); setError(null); setData(null); try { const shouldHideReportingLines = fg('jira_ai_profilecard_hide_reportinglines') && hideReportingLines; const requests = Promise.all([resourceClient.getProfile(cloudId || '', userId, fireAnalytics), shouldHideReportingLines ? Promise.resolve({ managers: [], reports: [] }) : resourceClient.getReportingLines(userId), resourceClient.shouldShowGiveKudos(), resourceClient.getTeamCentralBaseUrl({ withOrgContext: true, withSiteContext: true })]); const responses = await requests; handleClientSuccess(...responses); } catch (err) { handleClientError(err); } }, [cloudId, fireAnalytics, isLoading, resourceClient, userId, handleClientSuccess, handleClientError, hideReportingLines]); const showProfilecard = useCallback(() => { clearTimeout(hideTimer.current); clearTimeout(showTimer.current); showTimer.current = window.setTimeout(() => { if (!visible) { void clientFetchProfile(); setVisible(true); onVisibilityChange && onVisibilityChange(true); } }, showDelay); }, [showDelay, visible, clientFetchProfile, onVisibilityChange]); const onClick = useCallback(event => { // If the user clicks on the trigger then we don't want that click event to // propagate out to parent containers. For example when clicking a mention // lozenge in an inline-edit. event.stopPropagation(); showProfilecard(); if (!visible) { fireAnalytics('ui.profilecard.triggered', { method: 'click', firedAt: Math.round(getPageTime()), ...PACKAGE_META_DATA }); } }, [fireAnalytics, showProfilecard, visible]); const onMouseEnter = useCallback(() => { showProfilecard(); if (!visible) { fireAnalytics('ui.profilecard.triggered', { method: 'hover', firedAt: Math.round(getPageTime()), ...PACKAGE_META_DATA }); } }, [fireAnalytics, showProfilecard, visible]); const onKeyPress = useCallback(event => { if (event.key === 'Enter' || event.key === ' ') { event.preventDefault(); setTriggeredUsingKeyboard(true); showProfilecard(); if (!visible) { fireAnalytics('ui.profilecard.triggered', { method: 'click', firedAt: Math.round(getPageTime()), ...PACKAGE_META_DATA }); } } }, [fireAnalytics, showProfilecard, visible]); const onFocus = useCallback(() => { showProfilecard(); }, [showProfilecard]); useEffect(() => { if (!fg('fix_profilecard_trigger_isvisible')) { return; } // If the prop isVisible is not defined, we don't want to do anything if (propsIsVisible === undefined) { return; } // If the prop isVisible is defined, we want to show or hide the profile card based on the value if (propsIsVisible) { showProfilecard(); } else { hideProfilecard(); } }, [hideProfilecard, propsIsVisible, showProfilecard]); const containerListeners = useMemo(() => trigger === 'hover' ? { onMouseEnter: onMouseEnter, onMouseLeave: hideProfilecard, onBlur: hideProfilecard, onKeyPress: onKeyPress } : { onClick: onClick, onKeyPress: onKeyPress }, [hideProfilecard, onClick, onKeyPress, onMouseEnter, trigger]); const filterActions = useCallback(() => { return filterActionsInner(actions, data); }, [actions, data]); const openKudosDrawer = () => { hideProfilecard(); setKudosDrawerOpen(true); }; const closeKudosDrawer = () => { setKudosDrawerOpen(false); }; const showLoading = isLoading === true || isLoading === undefined; const wrapperProps = useMemo(() => trigger === 'hover' ? { onMouseEnter: onMouseEnter, onMouseLeave: hideProfilecard, onFocus: onFocus } : {}, [hideProfilecard, onFocus, onMouseEnter, trigger]); const profilecardProps = { userId: userId, fullName: prepopulatedData === null || prepopulatedData === void 0 ? void 0 : prepopulatedData.fullName, isCurrentUser: userId === viewingUserId, clientFetchProfile: clientFetchProfile, ...data, reportingLines: reportingLinesData, onReportingLinesClick: onReportingLinesClick, isKudosEnabled: shouldShowGiveKudos, teamCentralBaseUrl: teamCentralBaseUrl, cloudId: cloudId, openKudosDrawer: openKudosDrawer, isTriggeredUsingKeyboard: isTriggeredUsingKeyboard, disabledAriaAttributes: disabledAriaAttributes, hideReportingLines: fg('jira_ai_profilecard_hide_reportinglines') && hideReportingLines, hideAgentConversationStarters: fg('jira_ai_hide_conversation_starters_profilecard') && hideAgentConversationStarters }; const ssrPlaceholderProp = ssrPlaceholderId ? { 'data-ssr-placeholder-replace': ssrPlaceholderId } : {}; return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(Popup, { isOpen: !!visible, onClose: event => { hideProfilecard(); handleKeyboardClose(event); }, placement: position, offset: offset !== null && offset !== void 0 ? offset : [0, 8], content: () => /*#__PURE__*/React.createElement("div", wrapperProps, showLoading ? /*#__PURE__*/React.createElement(LoadingView, { fireAnalytics: fireAnalytics }) : visible && /*#__PURE__*/React.createElement(ProfileCardContent, { profilecardProps: profilecardProps, isAgent: !!(data !== null && data !== void 0 && data.isAgent), userId: userId, cloudId: cloudId, resourceClient: resourceClient, viewingUserId: viewingUserId, trigger: trigger, product: product, profileCardAction: filterActions(), errorType: error, hasError: hasError, agentActions: agentActions, addFlag: addFlag, hideAgentMoreActions: hideAgentMoreActions, hideAiDisclaimer: hideAiDisclaimer, isRenderedInPortal: isRenderedInPortal })), trigger: triggerProps => { const { ref: callbackRef, ...innerProps } = triggerProps; const ref = element => { triggerRef.current = element; if (typeof callbackRef === 'function') { callbackRef(element); } }; const { 'aria-expanded': _, 'aria-haspopup': __, ...restInnerProps } = innerProps; return /*#__PURE__*/React.createElement("span", _extends({}, disabledAriaAttributes ? restInnerProps : triggerProps, containerListeners, { ref: ref, "data-testid": testId }, !ariaHideProfileTrigger && { 'aria-labelledby': ariaLabelledBy }, disabledAriaAttributes ? {} : { role: 'button', // aria-hidden cannot contain focusable elements: https://dequeuniversity.com/rules/axe/3.5/aria-hidden-focus tabIndex: ariaHideProfileTrigger ? -1 : 0, 'aria-label': ariaHideProfileTrigger ? undefined : getLabelMessage(ariaLabel, profilecardProps.fullName, formatMessage) }, ariaHideProfileTrigger && { 'aria-hidden': 'true' }, ssrPlaceholderProp), children); }, zIndex: layers.modal(), shouldUseCaptureOnOutsideClick: true, autoFocus: expValEquals('editor_a11y_7152_profile_card_tab_order', 'isEnabled', true) ? isRenderedInPortal && isTriggeredUsingKeyboard ? false : autoFocus !== null && autoFocus !== void 0 ? autoFocus : trigger === 'click' : autoFocus !== null && autoFocus !== void 0 ? autoFocus : trigger === 'click' // This feature gate is currently enabled only for Jira_Web to avoid UI issues in Confluence_Web. , shouldRenderToParent: fg('enable_appropriate_reading_order_in_profile_card'), shouldDisableFocusLock: fg('enable_appropriate_reading_order_in_profile_card') }), shouldShowGiveKudos && teamCentralBaseUrl && /*#__PURE__*/React.createElement(Suspense, { fallback: null }, /*#__PURE__*/React.createElement(GiveKudosLauncherLazy, { isOpen: kudosDrawerOpen, recipient: { type: KudosType.INDIVIDUAL, recipientId: userId }, analyticsSource: "profile-card", teamCentralBaseUrl: teamCentralBaseUrl, cloudId: cloudId, addFlag: addFlag, onClose: closeKudosDrawer }))); } const LoadingView = ({ fireAnalytics }) => /*#__PURE__*/React.createElement(CardWrapper, { testId: "profilecard.profilecardtrigger.loading" }, /*#__PURE__*/React.createElement(UserLoadingState, { fireAnalytics: fireAnalytics }));