@atlaskit/editor-plugin-status
Version:
Status plugin for @atlaskit/editor-core
508 lines (505 loc) • 21.7 kB
JavaScript
import _classCallCheck from "@babel/runtime/helpers/classCallCheck";
import _createClass from "@babel/runtime/helpers/createClass";
import _possibleConstructorReturn from "@babel/runtime/helpers/possibleConstructorReturn";
import _getPrototypeOf from "@babel/runtime/helpers/getPrototypeOf";
import _inherits from "@babel/runtime/helpers/inherits";
import _defineProperty from "@babel/runtime/helpers/defineProperty";
function _callSuper(t, o, e) { return o = _getPrototypeOf(o), _possibleConstructorReturn(t, _isNativeReflectConstruct() ? Reflect.construct(o, e || [], _getPrototypeOf(t).constructor) : o.apply(t, e)); }
function _isNativeReflectConstruct() { try { var t = !Boolean.prototype.valueOf.call(Reflect.construct(Boolean, [], function () {})); } catch (t) {} return (_isNativeReflectConstruct = function _isNativeReflectConstruct() { return !!t; })(); }
/**
* @jsxRuntime classic
* @jsx jsx
*/
import React from 'react';
// eslint-disable-next-line @atlaskit/ui-styling-standard/use-compiled -- Ignored via go/DSP-18766
import { css, jsx } from '@emotion/react';
import { injectIntl } from 'react-intl';
import { withAnalyticsEvents } from '@atlaskit/analytics-next';
import { getDocument } from '@atlaskit/browser-apis';
import { statusMessages as messages } from '@atlaskit/editor-common/messages';
import { Popup } from '@atlaskit/editor-common/ui';
import { OutsideClickTargetRefContext, withReactEditorViewOuterListeners as withOuterListeners } from '@atlaskit/editor-common/ui-react';
import { UserIntentPopupWrapper } from '@atlaskit/editor-common/user-intent';
import { akEditorFloatingDialogZIndex } from '@atlaskit/editor-shared-styles';
import { fg } from '@atlaskit/platform-feature-flags';
import { Status } from '@atlaskit/status/element';
import { StatusPicker as AkStatusPicker } from '@atlaskit/status/picker';
import { expValEquals } from '@atlaskit/tmp-editor-statsig/exp-val-equals';
import { expValEqualsNoExposure } from '@atlaskit/tmp-editor-statsig/exp-val-equals-no-exposure';
import VisuallyHidden from '@atlaskit/visually-hidden/visually-hidden';
import { DEFAULT_STATUS } from '../pm-plugins/actions';
import { analyticsState, createStatusAnalyticsAndFire } from './analytics';
var PopupWithListeners = withOuterListeners(Popup);
export var InputMethod = /*#__PURE__*/function (InputMethod) {
InputMethod["blur"] = "blur";
InputMethod["escKey"] = "escKey";
InputMethod["enterKey"] = "enterKey";
return InputMethod;
}({});
export var closingMethods = /*#__PURE__*/function (closingMethods) {
closingMethods["ArrowLeft"] = "arrowLeft";
closingMethods["ArrowRight"] = "arrowRight";
return closingMethods;
}({});
var pickerContainerStyles = css({
background: "var(--ds-surface-overlay, #FFFFFF)",
padding: "var(--ds-space-100, 8px)".concat(" 0"),
borderRadius: "var(--ds-radius-small, 3px)",
boxShadow: "var(--ds-shadow-overlay, 0px 8px 12px #1E1F2126, 0px 0px 1px #1E1F214f)",
// eslint-disable-next-line @atlaskit/ui-styling-standard/no-unsafe-selectors -- Ignored via go/DSP-18766
':focus': {
outline: 'none'
},
// eslint-disable-next-line @atlaskit/design-system/no-nested-styles, @atlaskit/ui-styling-standard/no-nested-selectors -- Ignored via go/DSP-18766
input: {
// eslint-disable-next-line @atlaskit/design-system/use-tokens-typography
textTransform: 'uppercase'
}
});
var pickerContainerStylesTeam26 = css({
background: "var(--ds-surface-overlay, #FFFFFF)",
padding: "var(--ds-space-100, 8px)".concat(" 0"),
borderRadius: "var(--ds-radius-small, 3px)",
boxShadow: "var(--ds-shadow-overlay, 0 0 1px rgba(9, 30, 66, 0.31), 0 4px 8px -2px rgba(9, 30, 66, 0.25))",
// eslint-disable-next-line @atlaskit/ui-styling-standard/no-unsafe-selectors
':focus': {
outline: 'none'
}
});
var suggestedStatusesContainerStyles = css({
display: 'flex',
flexDirection: 'column',
gap: "var(--ds-space-100, 8px)",
margin: 0,
padding: "var(--ds-space-100, 8px)".concat(" ", "var(--ds-space-150, 12px)", " ", "var(--ds-space-050, 4px)")
});
var suggestedStatusButtonStyles = css({
background: 'transparent',
border: 0,
borderRadius: "var(--ds-radius-small, 3px)",
cursor: 'pointer',
display: 'flex',
justifyContent: 'flex-start',
padding: 0,
width: '100%',
'&:focus-visible': {
outline: "2px solid ".concat("var(--ds-border-focused, #0C66E4)"),
outlineOffset: "var(--ds-space-025, 2px)"
}
});
// eslint-disable-next-line @repo/internal/react/no-class-components
var StatusPickerWithIntl = /*#__PURE__*/function (_React$Component) {
function StatusPickerWithIntl(props) {
var _this;
_classCallCheck(this, StatusPickerWithIntl);
_this = _callSuper(this, StatusPickerWithIntl, [props]);
_defineProperty(_this, "handleClickOutside", function (event) {
event.preventDefault();
_this.inputMethod = InputMethod.blur;
var selectedText = window.getSelection();
if (!selectedText) {
_this.props.closeStatusPicker();
}
});
_defineProperty(_this, "handleEscapeKeydown", function (event) {
event.preventDefault();
_this.inputMethod = InputMethod.escKey;
_this.props.onEnter(_this.state);
});
_defineProperty(_this, "handleTabPress", function (event) {
var _getDocument2;
if (!expValEqualsNoExposure('platform_editor_status_popup_suggestions', 'isEnabled', true)) {
var _getDocument;
/* original tab-navigation behaviour: cycle between color buttons and the input field */
var _colorButtons = event.currentTarget.querySelectorAll('button');
var _inputField = event.currentTarget.querySelector('input');
var _activeElement = (_getDocument = getDocument()) === null || _getDocument === void 0 ? void 0 : _getDocument.activeElement;
var _isInputFocussed = _activeElement === _inputField;
var _isButtonFocussed = Array.from(_colorButtons).some(function (buttonElement) {
return _activeElement === buttonElement;
});
if (event !== null && event !== void 0 && event.shiftKey) {
/* shift + tab */
if (_isInputFocussed) {
_colorButtons[0].focus();
event.preventDefault();
}
/* After the user presses shift + tab the color-palette component updates tab index for the first color to be 0.
To correctly set focus to the input field instead of the first color button we need to set focus manually
*/
if (_isButtonFocussed) {
_inputField === null || _inputField === void 0 || _inputField.focus();
event.preventDefault();
}
} else {
/* tab */
if (_isButtonFocussed) {
_inputField === null || _inputField === void 0 || _inputField.focus();
event.preventDefault();
}
}
return;
}
var colorButtons = event.currentTarget.querySelectorAll('button:not([data-suggested-status-button])');
var suggestedStatusButtons = event.currentTarget.querySelectorAll('button[data-suggested-status-button]');
var inputField = event.currentTarget.querySelector('input');
var activeElement = (_getDocument2 = getDocument()) === null || _getDocument2 === void 0 ? void 0 : _getDocument2.activeElement;
var isInputFocussed = activeElement === inputField;
var isButtonFocussed = Array.from(colorButtons).some(function (buttonElement) {
return activeElement === buttonElement;
});
var isSuggestedStatusButtonFocussed = Array.from(suggestedStatusButtons).some(function (buttonElement) {
return activeElement === buttonElement;
});
if (event !== null && event !== void 0 && event.shiftKey) {
/* shift + tab */
if (isInputFocussed) {
if (suggestedStatusButtons.length > 0) {
suggestedStatusButtons[suggestedStatusButtons.length - 1].focus();
} else {
var _colorButtons$;
(_colorButtons$ = colorButtons[0]) === null || _colorButtons$ === void 0 || _colorButtons$.focus();
}
event.preventDefault();
}
/* After the user presses shift + tab the color-palette component updates tab index for the first color to be 0.
To correctly set focus to the input field instead of the first color button we need to set focus manually
*/
if (isButtonFocussed) {
inputField === null || inputField === void 0 || inputField.focus();
event.preventDefault();
}
if (isSuggestedStatusButtonFocussed) {
var _colorButtons2;
(_colorButtons2 = colorButtons[colorButtons.length - 1]) === null || _colorButtons2 === void 0 || _colorButtons2.focus();
event.preventDefault();
}
} else {
/* tab */
if (isButtonFocussed) {
if (suggestedStatusButtons.length > 0) {
suggestedStatusButtons[0].focus();
} else {
inputField === null || inputField === void 0 || inputField.focus();
}
event.preventDefault();
}
if (isSuggestedStatusButtonFocussed) {
inputField === null || inputField === void 0 || inputField.focus();
event.preventDefault();
}
}
});
_defineProperty(_this, "handleArrow", function (event, closingMethod) {
var _getDocument3;
var activeElement = (_getDocument3 = getDocument()) === null || _getDocument3 === void 0 ? void 0 : _getDocument3.activeElement;
if (activeElement === _this.popupBodyWrapper.current) {
var _this$popupBodyWrappe;
event.preventDefault();
(_this$popupBodyWrappe = _this.popupBodyWrapper) === null || _this$popupBodyWrappe === void 0 || (_this$popupBodyWrappe = _this$popupBodyWrappe.current) === null || _this$popupBodyWrappe === void 0 || _this$popupBodyWrappe.blur();
_this.props.closeStatusPicker({
closingMethod: closingMethod
});
}
});
_defineProperty(_this, "onKeyDown", function (event) {
var isTabPressed = event.key === 'Tab';
if (isTabPressed) {
return _this.handleTabPress(event);
}
if (event.key in closingMethods) {
return _this.handleArrow(event, closingMethods[event.key]);
}
});
_defineProperty(_this, "onColorHover", function (color) {
_this.createStatusAnalyticsAndFireFunc({
action: 'hovered',
actionSubject: 'statusColorPicker',
attributes: {
color: color,
localId: _this.state.localId,
state: analyticsState(_this.props.isNew)
}
});
});
_defineProperty(_this, "onColorClick", function (color) {
var _this$state = _this.state,
text = _this$state.text,
localId = _this$state.localId;
if (color === _this.state.color) {
_this.createStatusAnalyticsAndFireFunc({
action: 'clicked',
actionSubject: 'statusColorPicker',
attributes: {
color: color,
localId: localId,
state: analyticsState(_this.props.isNew)
}
});
// closes status box and commits colour
_this.onEnter();
} else {
_this.setState({
color: color
});
_this.props.onSelect({
text: text,
color: color,
localId: localId
});
}
});
_defineProperty(_this, "onTextChanged", function (text) {
var _this$state2 = _this.state,
color = _this$state2.color,
localId = _this$state2.localId;
_this.setState({
text: text
});
_this.props.onTextChanged({
text: text,
color: color,
localId: localId
}, !!_this.props.isNew);
});
_defineProperty(_this, "onEnter", function () {
_this.inputMethod = InputMethod.enterKey;
_this.props.onEnter(_this.state);
});
_defineProperty(_this, "onSuggestedStatusClick", function (status, position) {
_this.fireSuggestedStatusClickedAnalytics(status, position);
_this.props.onSuggestedStatusClick(status);
});
_defineProperty(_this, "handleSuggestedStatusKeyDown", function (event) {
// Selection (Enter/Space) is handled natively via the button's onClick.
// Keydown only drives ArrowUp/ArrowDown navigation between suggestions.
if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
event.preventDefault();
event.stopPropagation();
_this.focusAdjacentSuggestedStatus(event.currentTarget, event.key === 'ArrowDown' ? 1 : -1);
}
});
// cancel bubbling to fix clickOutside logic:
// popup re-renders its content before the click event bubbles up to the document
// therefore click target element would be different from the popup content
_defineProperty(_this, "handlePopupClick", function (event) {
return event.nativeEvent.stopImmediatePropagation();
});
_this.state = _this.extractStateFromProps(props);
_this.createStatusAnalyticsAndFireFunc = createStatusAnalyticsAndFire(props.createAnalyticsEvent);
_this.popupBodyWrapper = /*#__PURE__*/React.createRef();
return _this;
}
_inherits(StatusPickerWithIntl, _React$Component);
return _createClass(StatusPickerWithIntl, [{
key: "fireStatusPopupOpenedAnalytics",
value: function fireStatusPopupOpenedAnalytics(state) {
var color = state.color,
text = state.text,
localId = state.localId,
isNew = state.isNew;
this.startTime = Date.now();
this.createStatusAnalyticsAndFireFunc({
action: 'opened',
actionSubject: 'statusPopup',
attributes: {
textLength: text ? text.length : 0,
selectedColor: color,
localId: localId,
state: analyticsState(isNew)
}
});
}
}, {
key: "fireStatusPopupClosedAnalytics",
value: function fireStatusPopupClosedAnalytics(state) {
var color = state.color,
text = state.text,
localId = state.localId,
isNew = state.isNew;
this.createStatusAnalyticsAndFireFunc({
action: 'closed',
actionSubject: 'statusPopup',
attributes: {
inputMethod: this.inputMethod,
duration: Date.now() - this.startTime,
textLength: text ? text.length : 0,
selectedColor: color,
localId: localId,
state: analyticsState(isNew)
}
});
}
}, {
key: "reset",
value: function reset() {
this.startTime = Date.now();
this.inputMethod = InputMethod.blur;
}
}, {
key: "componentDidMount",
value: function componentDidMount() {
this.reset();
this.fireStatusPopupOpenedAnalytics(this.state);
}
}, {
key: "componentWillUnmount",
value: function componentWillUnmount() {
this.focusTimeout && cancelAnimationFrame(this.focusTimeout);
this.fireStatusPopupClosedAnalytics(this.state);
this.startTime = 0;
}
}, {
key: "componentDidUpdate",
value: function componentDidUpdate(prevProps, prevState) {
var element = this.props.target;
if (prevProps.target !== element) {
var newState = this.extractStateFromProps(this.props);
this.setState(newState);
this.fireStatusPopupClosedAnalytics(prevState);
this.reset();
this.fireStatusPopupOpenedAnalytics(newState);
}
}
}, {
key: "extractStateFromProps",
value: function extractStateFromProps(props) {
var defaultColor = props.defaultColor,
defaultText = props.defaultText,
defaultLocalId = props.defaultLocalId,
isNew = props.isNew;
return {
color: defaultColor || DEFAULT_STATUS.color,
text: defaultText || DEFAULT_STATUS.text,
localId: defaultLocalId,
isNew: isNew
};
}
}, {
key: "setRef",
value: function setRef(setOutsideClickTargetRef) {
var _this2 = this;
return function (ref) {
setOutsideClickTargetRef(ref);
_this2.popupBodyWrapper.current = ref;
};
}
}, {
key: "renderWithSetOutsideClickTargetRef",
value: function renderWithSetOutsideClickTargetRef(setOutsideClickTargetRef) {
var _this3 = this;
var _this$props = this.props,
isNew = _this$props.isNew,
focusStatusInput = _this$props.focusStatusInput,
api = _this$props.api,
suggestedStatuses = _this$props.suggestedStatuses;
var _this$state3 = this.state,
color = _this$state3.color,
text = _this$state3.text;
return jsx(UserIntentPopupWrapper, {
api: api,
userIntent: "statusPickerOpen"
}, jsx("div", {
css: fg('platform-dst-lozenge-tag-badge-visual-uplifts') ? pickerContainerStylesTeam26 : pickerContainerStyles,
role: "none",
ref: this.setRef(setOutsideClickTargetRef),
onClick: this.handlePopupClick,
onKeyDown: this.onKeyDown
}, jsx(AkStatusPicker, {
autoFocus: isNew || focusStatusInput,
selectedColor: color,
text: text,
onColorClick: this.onColorClick,
onColorHover: this.onColorHover,
onTextChanged: this.onTextChanged,
onEnter: this.onEnter
}), suggestedStatuses !== null && suggestedStatuses !== void 0 && suggestedStatuses.length && expValEquals('platform_editor_status_popup_suggestions', 'isEnabled', true) ? jsx("div", {
css: suggestedStatusesContainerStyles,
"data-suggested-status-list": true
}, suggestedStatuses.map(function (suggestedStatus, index) {
return jsx("div", {
key: "".concat(suggestedStatus.color, ":").concat(suggestedStatus.text)
}, jsx("button", {
type: "button",
css: suggestedStatusButtonStyles,
onClick: function onClick() {
return _this3.onSuggestedStatusClick(suggestedStatus, index + 1);
},
onKeyDown: _this3.handleSuggestedStatusKeyDown,
"aria-label": suggestedStatus.displayText,
"data-suggested-status-button": true
}, jsx(Status, {
color: suggestedStatus.color,
text: suggestedStatus.displayText
})));
})) : null));
}
}, {
key: "render",
value: function render() {
var _this$props2 = this.props,
target = _this$props2.target,
mountTo = _this$props2.mountTo,
boundariesElement = _this$props2.boundariesElement,
scrollableElement = _this$props2.scrollableElement,
editorView = _this$props2.editorView,
intl = _this$props2.intl;
if (!(editorView !== null && editorView !== void 0 && editorView.editable)) {
return null;
}
return target && jsx(PopupWithListeners, {
ariaLabel: intl.formatMessage(messages.statusEditorLabel),
target: target
// eslint-disable-next-line @atlassian/perf-linting/no-unstable-inline-props -- Ignored via go/ees017 (to be fixed)
,
offset: [0, 8],
handleClickOutside: this.handleClickOutside,
handleEscapeKeydown: this.handleEscapeKeydown,
zIndex: akEditorFloatingDialogZIndex,
fitHeight: 40,
mountTo: mountTo,
boundariesElement: boundariesElement,
scrollableElement: scrollableElement,
closeOnTab: false
}, jsx(VisuallyHidden, {
"aria-atomic": true,
role: "alert"
}, intl.formatMessage(messages.statusPickerOpenedAlert)), jsx(OutsideClickTargetRefContext.Consumer, null, this.renderWithSetOutsideClickTargetRef.bind(this)));
}
}, {
key: "fireSuggestedStatusClickedAnalytics",
value: function fireSuggestedStatusClickedAnalytics(status, position) {
var _this$props$suggested, _this$props$suggested2;
this.createStatusAnalyticsAndFireFunc({
action: 'clicked',
actionSubject: 'statusSuggestion',
attributes: {
color: status.color,
localId: this.state.localId,
position: position,
state: analyticsState(this.props.isNew),
textLength: status.text ? status.text.length : 0,
totalSuggestions: (_this$props$suggested = (_this$props$suggested2 = this.props.suggestedStatuses) === null || _this$props$suggested2 === void 0 ? void 0 : _this$props$suggested2.length) !== null && _this$props$suggested !== void 0 ? _this$props$suggested : 0
}
});
}
}, {
key: "focusAdjacentSuggestedStatus",
value: function focusAdjacentSuggestedStatus(currentButton, direction) {
var _this$popupBodyWrappe2, _suggestedStatusButto;
var suggestedStatusButtons = (_this$popupBodyWrappe2 = this.popupBodyWrapper.current) === null || _this$popupBodyWrappe2 === void 0 ? void 0 : _this$popupBodyWrappe2.querySelectorAll('button[data-suggested-status-button]');
if (!(suggestedStatusButtons !== null && suggestedStatusButtons !== void 0 && suggestedStatusButtons.length)) {
return;
}
var currentIndex = Array.from(suggestedStatusButtons).indexOf(currentButton);
if (currentIndex < 0) {
return;
}
var nextIndex = (currentIndex + direction + suggestedStatusButtons.length) % suggestedStatusButtons.length;
(_suggestedStatusButto = suggestedStatusButtons[nextIndex]) === null || _suggestedStatusButto === void 0 || _suggestedStatusButto.focus();
}
}]);
}(React.Component); // eslint-disable-next-line @typescript-eslint/ban-types
export var StatusPickerWithoutAnalytcs = injectIntl(StatusPickerWithIntl);
var _default_1 = withAnalyticsEvents()(StatusPickerWithoutAnalytcs);
export default _default_1;