@atlaskit/editor-common
Version:
A package that contains common classes and components for editor and renderer
487 lines (477 loc) • 18 kB
JavaScript
import _extends from "@babel/runtime/helpers/extends";
/**
* @jsxRuntime classic
* @jsx jsx
*/
import React, { memo, useCallback, useEffect, useRef, useState } from 'react';
// eslint-disable-next-line @atlaskit/ui-styling-standard/use-compiled -- Ignored via go/DSP-18766
import { css, jsx } from '@emotion/react';
import { FormattedMessage } from 'react-intl';
import withAnalyticsContext from '@atlaskit/analytics-next/withAnalyticsContext';
import withAnalyticsEvents from '@atlaskit/analytics-next/withAnalyticsEvents';
import { expValEquals } from '@atlaskit/tmp-editor-statsig/exp-val-equals';
import { ACTION, ACTION_SUBJECT, EVENT_TYPE, fireAnalyticsEvent } from '../../analytics';
import editorUGCToken from '../../ugc-tokens/get-editor-ugc-token';
import { ViewMore } from '../components/ViewMore';
import { DEVICE_BREAKPOINT_NUMBERS, ELEMENT_BROWSER_ID, ELEMENT_BROWSER_LIST_ID, GRID_SIZE, INLINE_SIDEBAR_HEIGHT, SIDEBAR_HEADING_WRAPPER_HEIGHT, SIDEBAR_WIDTH } from '../constants';
import useContainerWidth from '../hooks/use-container-width';
import useSelectAndFocusOnArrowNavigation from '../hooks/use-select-and-focus-on-arrow-navigation';
import CategoryList from './CategoryList';
import ElementList from './ElementList/ElementList';
import ElementSearch from './ElementSearch';
const wrapper = css({
width: '100%',
maxHeight: 'inherit',
overflow: 'hidden'
});
const baseBrowserContainerStyles = css({
display: 'flex',
height: '100%',
minHeight: '-webkit-fill-available'
});
// eslint-disable-next-line @atlaskit/ui-styling-standard/no-unsafe-values, @atlaskit/design-system/consistent-css-prop-usage -- Ignored via go/DSP-18766
const mobileElementBrowserContainer = css(baseBrowserContainerStyles, {
flexDirection: 'column'
});
// eslint-disable-next-line @atlaskit/ui-styling-standard/no-unsafe-values, @atlaskit/design-system/consistent-css-prop-usage -- Ignored via go/DSP-18766
const elementBrowserContainer = css(baseBrowserContainerStyles, {
flexDirection: 'row'
});
const baseSidebarStyles = css({
display: 'flex',
flexDirection: 'column',
overflowX: 'auto',
overflowY: 'hidden'
});
// eslint-disable-next-line @atlaskit/ui-styling-standard/no-unsafe-values, @atlaskit/design-system/consistent-css-prop-usage -- Ignored via go/DSP-18766
const mobileSideBar = css(baseSidebarStyles, {
// eslint-disable-next-line @atlaskit/ui-styling-standard/no-imported-style-values, @atlaskit/ui-styling-standard/no-unsafe-values -- Ignored via go/DSP-18766
flex: `0 0 ${INLINE_SIDEBAR_HEIGHT}`,
padding: `${"var(--ds-space-150, 12px)"} ${"var(--ds-space-150, 12px)"} 0 ${"var(--ds-space-150, 12px)"}`
});
const mobileSideBarShowCategories = css({
flex: '0 0 auto'
});
// eslint-disable-next-line @atlaskit/ui-styling-standard/no-unsafe-values, @atlaskit/design-system/consistent-css-prop-usage -- Ignored via go/DSP-18766
const sideBar = css(baseSidebarStyles, {
flex: "0 0 'auto'"
});
// eslint-disable-next-line @atlaskit/ui-styling-standard/no-unsafe-values, @atlaskit/design-system/consistent-css-prop-usage -- Ignored via go/DSP-18766
const sideBarShowCategories = css(baseSidebarStyles, {
// eslint-disable-next-line @atlaskit/ui-styling-standard/no-imported-style-values, @atlaskit/ui-styling-standard/no-unsafe-values -- Ignored via go/DSP-18766
flex: `0 0 ${SIDEBAR_WIDTH}`
});
const sidebarHeading = css({
// eslint-disable-next-line @atlaskit/ui-styling-standard/no-imported-style-values, @atlaskit/ui-styling-standard/no-unsafe-values -- Ignored via go/DSP-18766
flex: `0 0 ${SIDEBAR_HEADING_WRAPPER_HEIGHT}`,
display: 'inline-flex',
alignItems: 'center',
paddingLeft: "var(--ds-space-150, 12px)",
// eslint-disable-next-line @atlaskit/ui-styling-standard/no-imported-style-values, @atlaskit/ui-styling-standard/no-unsafe-values
font: editorUGCToken('editor.font.heading.h1')
});
const mobileMainContent = css({
flex: '1 1 auto',
display: 'flex',
flexDirection: 'column',
overflowY: 'auto',
height: '100%'
});
// eslint-disable-next-line @atlaskit/ui-styling-standard/no-unsafe-values, @atlaskit/design-system/consistent-css-prop-usage -- Ignored via go/DSP-18766
const mainContent = css(mobileMainContent, {
marginLeft: "var(--ds-space-200, 16px)",
height: 'auto'
});
const searchContainer = css({
paddingBottom: "var(--ds-space-200, 16px)"
});
const mobileCategoryListWrapper = css({
display: 'flex',
overflowX: 'auto',
padding: `${"var(--ds-space-200, 16px)"} 0 ${"var(--ds-space-200, 16px)"} 0`,
// eslint-disable-next-line @atlaskit/ui-styling-standard/no-imported-style-values, @atlaskit/ui-styling-standard/no-unsafe-values -- Ignored via go/DSP-18766
minHeight: `${GRID_SIZE * 4}px`,
overflow: '-moz-scrollbars-none',
// eslint-disable-next-line @atlaskit/ui-styling-standard/no-unsafe-selectors -- Ignored via go/DSP-18766
'&::-webkit-scrollbar': {
display: 'none'
},
scrollbarWidth: 'none',
MsOverflowStyle: 'none'
});
// eslint-disable-next-line @atlaskit/ui-styling-standard/no-unsafe-values, @atlaskit/design-system/consistent-css-prop-usage -- Ignored via go/DSP-18766
const categoryListWrapper = css(mobileCategoryListWrapper, {
padding: 0,
marginTop: "var(--ds-space-200, 16px)",
flexDirection: 'column'
});
function StatelessElementBrowser(props) {
var _props$categories;
const {
items,
onSelectItem,
onInsertItem,
onViewMore,
selectedCategory,
onSelectCategory,
searchTerm,
showCategories,
cache,
autoFocusSearch = true
} = props;
const {
containerWidth,
ContainerWidthMonitor
} = useContainerWidth();
const categoryBeenChosen = useRef(false);
const [columnCount, setColumnCount] = useState(1);
let isFocusSearch;
const selectedCategoryIndex = (_props$categories = props.categories) === null || _props$categories === void 0 ? void 0 : _props$categories.findIndex(category => {
return category.name === selectedCategory;
});
const [canFocusSearch, setCanFocusSearch] = useState(autoFocusSearch);
if (showCategories) {
const isEmptySearchTerm = !searchTerm || (searchTerm === null || searchTerm === void 0 ? void 0 : searchTerm.length) === 0;
if (!isEmptySearchTerm) {
// clear the flag if the search happens after a user has chosen the category
categoryBeenChosen.current = false;
}
// A11Y: if categories exists and search can be focused, on the initial render it should receive focus.
// After user pick some category the category should stay focused.
isFocusSearch = canFocusSearch && (!categoryBeenChosen.current || !isEmptySearchTerm);
}
const itemIsDisabled = useCallback(index => {
var _items$index$isDisabl, _items$index;
return (_items$index$isDisabl = (_items$index = items[index]) === null || _items$index === void 0 ? void 0 : _items$index.isDisabled) !== null && _items$index$isDisabl !== void 0 ? _items$index$isDisabl : false;
}, [items]);
const {
selectedItemIndex,
focusedItemIndex,
setFocusedItemIndex,
setFocusedCategoryIndex,
focusOnEmptyStateButton,
focusedCategoryIndex,
focusOnSearch,
focusOnViewMore,
onKeyDown,
setFocusOnSearch
} = useSelectAndFocusOnArrowNavigation(items.length - 1, columnCount, !!onViewMore, itemIsDisabled, isFocusSearch, autoFocusSearch);
useEffect(() => {
fireAnalyticsEvent(props.createAnalyticsEvent)({
payload: {
action: ACTION.OPENED,
actionSubject: ACTION_SUBJECT.ELEMENT_BROWSER,
eventType: EVENT_TYPE.UI,
attributes: {
mode: props.mode
}
}
});
return () => {
fireAnalyticsEvent(props.createAnalyticsEvent)({
payload: {
action: ACTION.CLOSED,
actionSubject: ACTION_SUBJECT.ELEMENT_BROWSER,
eventType: EVENT_TYPE.UI,
attributes: {
mode: props.mode
}
}
});
};
}, [props.createAnalyticsEvent, props.mode]);
/* Only for hitting enter to select item when focused on search bar,
* The actual enter key press is handled on individual items level.
*/
const selectedItem = selectedItemIndex !== undefined ? items[selectedItemIndex] : null;
const onItemsEnterTabKeyPress = useCallback(e => {
var _selectedItem$isDisab;
if (e.key !== 'Enter' && (e.key !== 'Tab' || !showCategories)) {
return;
}
if (showCategories && e.key === 'Tab' && selectedCategoryIndex !== undefined) {
// A11Y: Set focus on first category if tab pressed on search
setFocusedCategoryIndex(selectedCategoryIndex);
e.preventDefault();
return;
}
if (onInsertItem && selectedItem != null && !((_selectedItem$isDisab = selectedItem.isDisabled) !== null && _selectedItem$isDisab !== void 0 ? _selectedItem$isDisab : false)) {
onInsertItem(selectedItem);
}
e.preventDefault();
}, [onInsertItem, selectedItem, setFocusedCategoryIndex, showCategories, selectedCategoryIndex]);
/**
* On arrow key selection and clicks the selectedItemIndex will change.
* Making sure to update parent component.
*/
useEffect(() => {
if (onSelectItem && selectedItem != null) {
onSelectItem(selectedItem);
}
}, [onSelectItem, selectedItem]);
const onSelectCategoryCB = useCallback(category => {
onSelectCategory(category);
categoryBeenChosen.current = true;
}, [categoryBeenChosen, onSelectCategory]);
const handleKeyPress = e => {
if (e.key === 'Tab') {
// only Tab key can change focus from close button (if present)
setCanFocusSearch(true);
}
};
const handleClick = () => {
setCanFocusSearch(true);
};
const browserContent = jsx(React.Fragment, null, jsx(ContainerWidthMonitor, null), containerWidth < DEVICE_BREAKPOINT_NUMBERS.medium ? jsx(MobileBrowser
// Ignored via go/ees005
// eslint-disable-next-line react/jsx-props-no-spreading
, _extends({}, props, {
selectedItemIndex: selectedItemIndex,
focusOnEmptyStateButton: focusOnEmptyStateButton,
focusedItemIndex: focusedItemIndex,
setFocusedItemIndex: setFocusedItemIndex,
focusedCategoryIndex: focusedCategoryIndex,
setFocusedCategoryIndex: setFocusedCategoryIndex,
focusOnSearch: focusOnSearch,
columnCount: columnCount,
setColumnCount: setColumnCount,
setFocusOnSearch: setFocusOnSearch,
onKeyPress: onItemsEnterTabKeyPress,
onKeyDown: onKeyDown,
onViewMore: onViewMore,
focusOnViewMore: focusOnViewMore,
cache: cache
})) : jsx(DesktopBrowser
// Ignored via go/ees005
// eslint-disable-next-line react/jsx-props-no-spreading
, _extends({}, props, {
selectedItemIndex: selectedItemIndex,
focusOnEmptyStateButton: focusOnEmptyStateButton,
focusedItemIndex: focusedItemIndex,
setFocusedItemIndex: setFocusedItemIndex,
focusOnSearch: focusOnSearch,
setColumnCount: setColumnCount,
columnCount: columnCount,
setFocusOnSearch: setFocusOnSearch,
onKeyPress: onItemsEnterTabKeyPress,
onKeyDown: onKeyDown,
focusedCategoryIndex: focusedCategoryIndex,
setFocusedCategoryIndex: setFocusedCategoryIndex,
selectedCategoryIndex: selectedCategoryIndex,
onSelectCategory: onSelectCategoryCB,
cache: cache
})));
if (expValEquals('editor_a11y__enghealth-46814_fy26', 'isEnabled', true)) {
return jsx("div", {
css: wrapper,
"data-testid": "element-browser",
id: ELEMENT_BROWSER_ID,
role: "none",
onKeyUp: canFocusSearch ? undefined : handleKeyPress,
onClick: canFocusSearch ? undefined : handleClick
}, browserContent);
}
return (
// eslint-disable-next-line @atlassian/a11y/interactive-element-not-keyboard-focusable
jsx("div", {
css: wrapper,
"data-testid": "element-browser",
id: ELEMENT_BROWSER_ID,
onKeyUp: canFocusSearch ? undefined : handleKeyPress,
onClick: canFocusSearch ? undefined : handleClick
}, browserContent)
);
}
function MobileBrowser({
showCategories,
showSearch,
onSearch,
mode,
categories,
onSelectCategory,
items,
onInsertItem,
selectedCategory,
selectedItemIndex,
focusedItemIndex,
setFocusedItemIndex,
focusedCategoryIndex,
setFocusedCategoryIndex,
focusOnSearch,
focusOnViewMore,
columnCount,
setColumnCount,
setFocusOnSearch,
onKeyPress,
onKeyDown,
searchTerm,
createAnalyticsEvent,
emptyStateHandler,
onViewMore,
cache,
focusOnEmptyStateButton = false
}) {
return jsx("div", {
css: mobileElementBrowserContainer,
role: expValEquals('editor_a11y__enghealth-46814_fy26', 'isEnabled', true) ? 'none' : undefined,
onKeyDown: onKeyDown,
"data-testid": "mobile__element-browser"
}, jsx("div", {
css: showCategories ? [mobileSideBar, mobileSideBarShowCategories] : mobileSideBar
}, showSearch && jsx(ElementSearch, {
onSearch: onSearch,
onKeyDown: onKeyPress,
mode: mode,
focus: focusOnSearch,
onClick: setFocusOnSearch,
searchTerm: searchTerm,
items: items,
selectedItemIndex: selectedItemIndex,
ariaControlsId: expValEquals('platform_editor_august_a11y', 'isEnabled', true) ? ELEMENT_BROWSER_LIST_ID : undefined
}), showCategories && jsx("nav", {
css: mobileCategoryListWrapper,
tabIndex: -1
}, jsx(CategoryList, {
categories: categories,
onSelectCategory: onSelectCategory,
selectedCategory: selectedCategory,
focusedCategoryIndex: focusedCategoryIndex,
setFocusedCategoryIndex: setFocusedCategoryIndex,
setFocusedItemIndex: setFocusedItemIndex,
setFocusOnSearch: setFocusOnSearch
}))), jsx("div", {
css: mobileMainContent,
id: expValEquals('platform_editor_august_a11y', 'isEnabled', true) ? ELEMENT_BROWSER_LIST_ID : undefined
}, jsx(ElementList, {
items: items,
mode: mode,
onInsertItem: onInsertItem,
selectedItemIndex: selectedItemIndex,
focusedItemIndex: focusedItemIndex,
focusOnEmptyStateButton: focusOnEmptyStateButton,
setFocusedItemIndex: setFocusedItemIndex,
columnCount: columnCount,
setColumnCount: setColumnCount,
createAnalyticsEvent: createAnalyticsEvent,
emptyStateHandler: emptyStateHandler,
selectedCategory: selectedCategory,
searchTerm: searchTerm,
cache: cache,
hasTabListContext: false
})), onViewMore && jsx(ViewMore, {
onViewMore: onViewMore,
focus: focusOnViewMore
}));
}
function DesktopBrowser({
showCategories,
showSearch,
onSearch,
mode,
categories,
onSelectCategory,
items,
onInsertItem,
selectedCategory,
selectedItemIndex,
focusedItemIndex,
setFocusedItemIndex,
focusedCategoryIndex,
setFocusedCategoryIndex,
selectedCategoryIndex,
focusOnSearch,
columnCount,
setColumnCount,
setFocusOnSearch,
onKeyPress,
onKeyDown,
searchTerm,
createAnalyticsEvent,
emptyStateHandler,
cache,
focusOnEmptyStateButton = false
}) {
return jsx("div", {
css: elementBrowserContainer,
"data-testid": "desktop__element-browser"
}, showCategories && jsx("div", {
css: showCategories ? sideBarShowCategories : sideBar
}, jsx("div", {
css: sidebarHeading,
"data-testid": "sidebar-heading",
id: "sidebar-heading"
}, jsx(FormattedMessage, {
id: "fabric.editor.elementbrowser.sidebar.heading",
defaultMessage: "Browse",
description: "Sidebar heading"
})), expValEquals('editor_a11y__enghealth-46814_fy26', 'isEnabled', true) ? jsx("div", {
role: "tablist",
"aria-labelledby": "sidebar-heading",
css: categoryListWrapper
}, jsx(CategoryList, {
categories: categories,
onSelectCategory: onSelectCategory,
selectedCategory: selectedCategory,
createAnalyticsEvent: createAnalyticsEvent,
focusedCategoryIndex: focusedCategoryIndex,
setFocusedCategoryIndex: setFocusedCategoryIndex,
setFocusedItemIndex: setFocusedItemIndex,
setFocusOnSearch: setFocusOnSearch
})) :
// eslint-disable-next-line @atlassian/a11y/no-noninteractive-element-to-interactive-role
jsx("nav", {
role: "tablist",
"aria-labelledby": "sidebar-heading",
css: categoryListWrapper
}, jsx(CategoryList, {
categories: categories,
onSelectCategory: onSelectCategory,
selectedCategory: selectedCategory,
createAnalyticsEvent: createAnalyticsEvent,
focusedCategoryIndex: focusedCategoryIndex,
setFocusedCategoryIndex: setFocusedCategoryIndex,
setFocusedItemIndex: setFocusedItemIndex,
setFocusOnSearch: setFocusOnSearch
}))), jsx("div", {
css: mainContent,
role: expValEquals('editor_a11y__enghealth-46814_fy26', 'isEnabled', true) ? 'none' : undefined,
onKeyDown: onKeyDown,
"data-testid": "main-content"
}, showSearch &&
// eslint-disable-next-line
jsx("div", {
css: searchContainer
}, jsx(ElementSearch, {
onSearch: onSearch,
onKeyDown: onKeyPress,
mode: mode,
focus: focusOnSearch,
onClick: setFocusOnSearch,
searchTerm: searchTerm,
items: items,
selectedItemIndex: selectedItemIndex,
ariaControlsId: selectedCategory ? `browse-category-${selectedCategory}-tab` : 'browse-category-tab'
})), jsx(ElementList, {
items: items,
mode: mode,
onInsertItem: onInsertItem,
selectedItemIndex: selectedItemIndex,
focusedItemIndex: focusedItemIndex,
focusOnEmptyStateButton: focusOnEmptyStateButton,
setFocusedItemIndex: setFocusedItemIndex,
columnCount: columnCount,
setColumnCount: setColumnCount,
createAnalyticsEvent: createAnalyticsEvent,
emptyStateHandler: emptyStateHandler,
selectedCategory: selectedCategory,
selectedCategoryIndex: selectedCategoryIndex,
searchTerm: searchTerm,
setFocusedCategoryIndex: showCategories ? setFocusedCategoryIndex : undefined,
cache: cache,
hasTabListContext: showCategories
})));
}
const MemoizedElementBrowser = /*#__PURE__*/memo(withAnalyticsContext({
source: 'ElementBrowser'
})(withAnalyticsEvents()(StatelessElementBrowser)));
export default MemoizedElementBrowser;