@atlaskit/editor-common
Version:
A package that contains common classes and components for editor and renderer
883 lines (874 loc) • 31.4 kB
JavaScript
import _defineProperty from "@babel/runtime/helpers/defineProperty";
/* eslint-disable @atlaskit/ui-styling-standard/use-compiled -- Pre-existing lint debt surfaced by this mechanical type-import-only PR. */
/**
* @jsxRuntime classic
* @jsx jsx
*/
import React, { PureComponent } from 'react';
// eslint-disable-next-line @atlaskit/ui-styling-standard/use-compiled, @typescript-eslint/consistent-type-imports -- Ignored via go/DSP-18766; jsx required at runtime for @jsxRuntime classic
import { css, jsx } from '@emotion/react';
import debounce from 'lodash/debounce';
import { flushSync } from 'react-dom';
import FocusLock from 'react-focus-lock';
import { defineMessages, injectIntl } from 'react-intl';
import { isSafeUrl } from '@atlaskit/adf-schema';
import withAnalyticsEvents from '@atlaskit/analytics-next/withAnalyticsEvents';
import CrossCircleIcon from '@atlaskit/icon/core/cross-circle';
import PageObject from '@atlaskit/object/page';
// eslint-disable-next-line @atlaskit/design-system/no-emotion-primitives -- to be migrated to @atlaskit/primitives/compiled – go/akcss
import { Pressable, xcss } from '@atlaskit/primitives';
import { expValEquals } from '@atlaskit/tmp-editor-statsig/exp-val-equals';
import Tooltip from '@atlaskit/tooltip';
import { ACTION, ACTION_SUBJECT, ACTION_SUBJECT_ID, EVENT_TYPE, fireAnalyticsEvent, INPUT_METHOD } from '../../../analytics';
import { Announcer, PanelTextInput } from '../../../ui';
import { normalizeUrl } from '../../../utils';
import { getBrowserInfo } from '../../../utils/browser';
import LinkSearchList from '../../LinkSearch/LinkSearchList';
import { container, narrowContainerWidth, inputWrapper } from '../../LinkSearch/ToolbarComponents';
import { transformTimeStamp } from '../../LinkSearch/transformTimeStamp';
import { filterUniqueItems, mapContentTypeToIcon, sha1, wordCount } from './utils';
/**
* Visible only to screenreaders. Use when there is a need
* to provide more context to a non-sighted user.
*/
// eslint-disable-next-line @atlaskit/ui-styling-standard/no-exported-styles -- Ignored via go/DSP-18766
export const visuallyHiddenStyles = css({
clip: 'rect(1px, 1px, 1px, 1px)',
clipPath: 'inset(50%)',
height: '1px',
width: '1px',
margin: "var(--ds-space-negative-025, -2px)",
overflow: 'hidden',
padding: 0,
position: 'absolute'
});
export const RECENT_SEARCH_LIST_SIZE = 5;
const clearTextButtonStyles = xcss({
padding: 'space.0',
marginRight: 'space.100',
backgroundColor: 'color.background.neutral.subtle',
border: 'none'
});
const clearTextWrapper = css({
position: 'absolute',
right: 0
});
const containerPadding = css({
padding: `${"var(--ds-space-150, 12px)"} ${"var(--ds-space-100, 8px)"}`
});
const textLabelMargin = css({
marginTop: "var(--ds-space-150, 12px)"
});
const inputLabel = css({
color: "var(--ds-text-subtlest, #6B6E76)",
paddingBottom: "var(--ds-space-050, 4px)",
font: "var(--ds-font-body-small, normal 400 12px/16px \"Atlassian Sans\", ui-sans-serif, -apple-system, BlinkMacSystemFont, \"Segoe UI\", Ubuntu, \"Helvetica Neue\", sans-serif)",
fontWeight: "var(--ds-font-weight-medium, 500)"
});
const inputWrapperPosition = css({
position: 'relative'
});
export const messages = defineMessages({
displayText: {
id: 'fabric.editor.displayText',
defaultMessage: 'Text to display',
description: 'Label for the text input field in the hyperlink toolbar where users enter the display text for a link.'
},
clearText: {
id: 'fabric.editor.clearLinkText',
defaultMessage: 'Clear text',
description: 'The text is shown on a button in the hyperlink toolbar that clears the display text field, removing any custom link text the user has entered.'
},
clearLink: {
id: 'fabric.editor.clearLink',
defaultMessage: 'Clear link',
description: 'The text is shown on a button in the hyperlink toolbar that clears the URL field, removing any link address the user has entered.'
},
hyperlinkAriaLabel: {
id: 'fabric.editor.hyperlink.ariaLabel',
defaultMessage: 'Hyperlink Edit',
description: 'Aria label for the Hyperlink Add Toolbar'
},
searchLinkAriaDescription: {
id: 'fabric.editor.hyperlink.searchLinkAriaDescription',
defaultMessage: 'Suggestions will appear below as you type into the field',
description: 'Describes what the search field does for screen reader users.'
},
searchLinkResults: {
id: 'fabric.editor.hyperlink.searchLinkResults',
defaultMessage: '{count, plural, =0 {no results} one {# result} other {# results}} found',
description: 'Announce search results for screen-reader users.'
},
linkVisibleLabel: {
id: 'fabric.editor.hyperlink.linkVisibleLabel',
defaultMessage: 'Paste or search for link',
description: 'Visible label for link input in hyperlink floating control'
},
textVisibleLabel: {
id: 'fabric.editor.hyperlink.textVisibleLabel',
defaultMessage: 'Display text (optional)',
description: 'Visible label for text input in hyperlink floating control'
}
});
const defaultIcon = jsx(PageObject, {
label: "page"
});
const mapActivityProviderResultToLinkSearchItemData = ({
name,
container,
iconUrl,
objectId,
url,
viewedTimestamp
}) => ({
objectId,
name,
container,
iconUrl,
url,
lastViewedDate: viewedTimestamp ? new Date(viewedTimestamp) : undefined,
prefetch: true
});
const mapSearchProviderResultToLinkSearchItemData = ({
objectId,
container,
title,
contentType,
url,
updatedTimestamp
}) => ({
objectId,
container,
name: title,
url,
lastUpdatedDate: updatedTimestamp ? new Date(updatedTimestamp) : undefined,
icon: contentType && mapContentTypeToIcon[contentType] || defaultIcon,
prefetch: false
});
// Ignored via go/ees005
// eslint-disable-next-line @repo/internal/react/no-class-components
export class HyperlinkLinkAddToolbar extends PureComponent {
constructor(props) {
super(props);
/* To prevent double submit */
_defineProperty(this, "submitted", false);
_defineProperty(this, "urlInputContainer", null);
_defineProperty(this, "displayTextInputContainer", null);
_defineProperty(this, "wrapperRef", /*#__PURE__*/React.createRef());
_defineProperty(this, "listItemRefs", {
current: {}
});
_defineProperty(this, "quickSearchQueryVersion", 0);
_defineProperty(this, "analyticSource", 'createLinkInlineDialog');
_defineProperty(this, "quickSearch", async (input, items, quickSearchLimit) => {
const {
searchProvider,
displayUrl
} = this.state;
const {
searchSessionId
} = this.props;
if (!searchProvider) {
return;
}
const queryVersion = ++this.quickSearchQueryVersion;
this.fireAnalytics({
action: ACTION.ENTERED,
actionSubject: ACTION_SUBJECT.TEXT,
actionSubjectId: ACTION_SUBJECT_ID.LINK_SEARCH_INPUT,
attributes: {
queryLength: input.length,
queryVersion,
queryHash: sha1(input),
searchSessionId: searchSessionId !== null && searchSessionId !== void 0 ? searchSessionId : '',
wordCount: wordCount(input),
source: this.analyticSource
},
nonPrivacySafeAttributes: {
query: input
},
eventType: EVENT_TYPE.UI
});
const perfStart = performance.now();
try {
const searchProviderResultItems = await searchProvider.quickSearch(input, quickSearchLimit);
const searchItems = limit(filterUniqueItems([...items, ...searchProviderResultItems.map(mapSearchProviderResultToLinkSearchItemData)], (firstItem, secondItem) => firstItem.objectId === secondItem.objectId));
if (displayUrl === input && queryVersion === this.quickSearchQueryVersion) {
this.setState({
items: searchItems,
isLoading: false
});
}
const perfStop = performance.now();
const duration = perfStop - perfStart;
this.fireAnalytics({
action: ACTION.INVOKED,
actionSubject: ACTION_SUBJECT.SEARCH_RESULT,
actionSubjectId: ACTION_SUBJECT_ID.QUICK_SEARCH,
attributes: {
duration,
count: searchProviderResultItems.length
},
eventType: EVENT_TYPE.OPERATIONAL
});
this.fireAnalytics({
action: ACTION.SHOWN,
actionSubject: ACTION_SUBJECT.SEARCH_RESULT,
actionSubjectId: ACTION_SUBJECT_ID.POST_QUERY_SEARCH_RESULTS,
attributes: {
source: this.analyticSource,
postQueryRequestDurationMs: duration,
searchSessionId: searchSessionId !== null && searchSessionId !== void 0 ? searchSessionId : '',
resultCount: searchProviderResultItems.length,
results: searchProviderResultItems.map(item => ({
resultContentId: item.objectId,
resultType: item.contentType
}))
},
eventType: EVENT_TYPE.UI
});
// Ignored via go/ees005
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (err) {
const perfStop = performance.now();
const duration = perfStop - perfStart;
this.fireAnalytics({
action: ACTION.INVOKED,
actionSubject: ACTION_SUBJECT.SEARCH_RESULT,
actionSubjectId: ACTION_SUBJECT_ID.QUICK_SEARCH,
attributes: {
duration,
count: -1,
errorCode: err.status
},
eventType: EVENT_TYPE.OPERATIONAL
});
}
});
_defineProperty(this, "updateInput", async input => {
const {
activityProvider,
searchProvider
} = this.state;
this.setState({
displayUrl: input
});
if (activityProvider) {
if (input.length === 0) {
this.setState({
items: await this.getRecentItems(activityProvider),
selectedIndex: -1
});
} else if (isSafeUrl(input)) {
this.setState({
items: [],
selectedIndex: -1,
isLoading: false
});
} else {
const items = await this.getRecentItems(activityProvider, input);
const shouldQuerySearchProvider = items.length < RECENT_SEARCH_LIST_SIZE && !!searchProvider;
this.setState({
items,
isLoading: shouldQuerySearchProvider
});
if (shouldQuerySearchProvider) {
this.debouncedQuickSearch(input, items, RECENT_SEARCH_LIST_SIZE);
}
}
}
});
_defineProperty(this, "createClearHandler", field => {
return async () => {
const {
activityProvider
} = this.state;
switch (field) {
case 'displayUrl':
{
this.setState({
[field]: '',
items: !!activityProvider ? limit(await activityProvider.getRecentItems()) : []
});
if (this.urlInputContainer) {
this.urlInputContainer.focus();
}
break;
}
case 'displayText':
{
this.setState({
[field]: ''
});
if (this.displayTextInputContainer) {
this.displayTextInputContainer.focus();
}
}
}
};
});
_defineProperty(this, "handleClickOutside", event => {
if (event.target instanceof Element && this.wrapperRef.current && !this.wrapperRef.current.contains(event.target)) {
const {
view: {
state,
dispatch
},
onClickAwayCallback
} = this.props;
onClickAwayCallback === null || onClickAwayCallback === void 0 ? void 0 : onClickAwayCallback(state, dispatch);
}
});
_defineProperty(this, "getScreenReaderText", () => {
const {
intl
} = this.props;
const {
items,
selectedIndex
} = this.state;
if (items.length && selectedIndex > -1) {
const {
name,
container,
lastUpdatedDate,
lastViewedDate
} = items[selectedIndex];
const date = transformTimeStamp(intl, lastViewedDate, lastUpdatedDate);
return `${name}, ${container}, ${date === null || date === void 0 ? void 0 : date.pageAction} ${date === null || date === void 0 ? void 0 : date.dateString} ${(date === null || date === void 0 ? void 0 : date.timeSince) || ''}`;
}
});
_defineProperty(this, "listItemRefCallback", (el, id) => {
if (!this.listItemRefs.current) {
return;
}
if (el === null) {
delete this.listItemRefs.current[id];
} else {
this.listItemRefs.current[id] = el;
}
});
_defineProperty(this, "isUrlPopulatedWithSelectedItem", () => {
/**
* When we use ArrowKey to navigate through result items,
* the URL field will be populated with the content of
* selected item.
* This function will check if the URL field is populated
* with selected item.
* It can be useful to detect whether we want to insert a
* smartlink or a hyperlink with customized title
*/
const {
items,
selectedIndex,
displayUrl
} = this.state;
const selectedItem = items[selectedIndex];
if (selectedItem && selectedItem.url === displayUrl) {
return true;
}
return false;
});
_defineProperty(this, "handleSelected", (href, text) => {
this.handleInsert(href, text, INPUT_METHOD.TYPEAHEAD, 'click');
});
_defineProperty(this, "handleInsert", (href, title, inputType, interaction) => {
const {
searchSessionId,
onSubmit
} = this.props;
const {
items,
selectedIndex,
displayText
} = this.state;
if (onSubmit) {
this.submitted = true;
onSubmit(href, title, displayText, inputType);
}
if (interaction === 'click' || this.isUrlPopulatedWithSelectedItem()) {
var _selectedItem$prefetc;
/**
* When it's a mouse click even or the selectedItem.url matches displayUrl, we think
* it's selected from the result list and fire the
* analytic
*/
const selectedItem = items[selectedIndex];
this.fireAnalytics({
action: ACTION.SELECTED,
actionSubject: ACTION_SUBJECT.SEARCH_RESULT,
attributes: {
source: this.analyticSource,
searchSessionId: searchSessionId !== null && searchSessionId !== void 0 ? searchSessionId : '',
trigger: interaction,
resultCount: items.length,
selectedResultId: selectedItem.objectId,
selectedRelativePosition: selectedIndex,
prefetch: (_selectedItem$prefetc = selectedItem.prefetch) !== null && _selectedItem$prefetc !== void 0 ? _selectedItem$prefetc : false
},
eventType: EVENT_TYPE.UI
});
}
});
_defineProperty(this, "handleMouseEnterResultItem", objectId => {
const {
items
} = this.state;
const index = findIndex(items, item => item.objectId === objectId);
this.setState({
selectedIndex: index
});
});
_defineProperty(this, "handleMouseLeaveResultItem", objectId => {
const {
items,
selectedIndex
} = this.state;
const index = findIndex(items, item => item.objectId === objectId);
// This is to avoid updating index that was set by other mouseenter event
if (selectedIndex === index) {
this.setState({
selectedIndex: -1
});
}
});
_defineProperty(this, "handleSubmit", () => {
const {
displayUrl,
selectedIndex,
items
} = this.state;
const selectedItem = items[selectedIndex];
if (this.isUrlPopulatedWithSelectedItem()) {
this.handleInsert(normalizeUrl(selectedItem.url), selectedItem.name, INPUT_METHOD.TYPEAHEAD, 'keyboard');
} else if (displayUrl && displayUrl.length > 0) {
const url = normalizeUrl(displayUrl);
if (url) {
this.handleInsert(url, displayUrl, INPUT_METHOD.MANUAL, 'notselected');
}
}
});
_defineProperty(this, "handleClearTextKeyDown", event => {
const KEY_CODE_TAB = 9;
const {
keyCode
} = event;
if (keyCode === KEY_CODE_TAB) {
/** If there are items in the list, allow normal tabbing so focus moves to the next interactive element (in this case, a search result item).
*/
if (this.state.items.length > 0) {
return;
}
if (!this.submitted) {
const {
displayUrl,
displayText
} = this.state;
const url = normalizeUrl(displayUrl);
this.handleInsert(url, displayText || displayUrl, INPUT_METHOD.MANUAL, 'notselected');
}
event.preventDefault();
return;
}
});
_defineProperty(this, "handleListItemFocus", index => {
this.setState({
selectedIndex: index
});
});
_defineProperty(this, "handleListItemBlur", () => {
this.setState({
selectedIndex: -1
});
});
// Ignored via go/ees005
// eslint-disable-next-line @typescript-eslint/no-explicit-any
_defineProperty(this, "handleKeyDown", event => {
const {
items,
selectedIndex
} = this.state;
const {
view,
onEscapeCallback,
searchSessionId
} = this.props;
const {
keyCode
} = event;
const KEY_CODE_ESCAPE = 27;
const KEY_CODE_ARROW_DOWN = 40;
const KEY_CODE_ARROW_UP = 38;
if (keyCode === KEY_CODE_ESCAPE) {
// escape
event.preventDefault();
const {
state,
dispatch
} = view;
onEscapeCallback === null || onEscapeCallback === void 0 ? void 0 : onEscapeCallback(state, dispatch);
return;
}
if (!items || !items.length) {
return;
}
let updatedIndex = selectedIndex;
if (keyCode === KEY_CODE_ARROW_DOWN) {
// down
event.preventDefault();
updatedIndex = (selectedIndex + 1) % items.length;
} else if (keyCode === KEY_CODE_ARROW_UP) {
// up
event.preventDefault();
updatedIndex = selectedIndex > 0 ? selectedIndex - 1 : items.length - 1;
}
if ([KEY_CODE_ARROW_DOWN, KEY_CODE_ARROW_UP].includes(keyCode) && items[updatedIndex]) {
if (this.listItemRefs.current) {
var _this$listItemRefs$cu;
(_this$listItemRefs$cu = this.listItemRefs.current[items[updatedIndex].objectId]) === null || _this$listItemRefs$cu === void 0 ? void 0 : _this$listItemRefs$cu.focus();
}
this.setState({
selectedIndex: updatedIndex,
displayUrl: items[updatedIndex].url
});
this.fireAnalytics({
action: ACTION.HIGHLIGHTED,
actionSubject: ACTION_SUBJECT.SEARCH_RESULT,
attributes: {
source: this.analyticSource,
searchSessionId: searchSessionId !== null && searchSessionId !== void 0 ? searchSessionId : '',
selectedResultId: items[updatedIndex].objectId,
selectedRelativePosition: updatedIndex
},
eventType: EVENT_TYPE.UI
});
}
});
_defineProperty(this, "updateTextInput", displayText => {
this.setState({
displayText
});
});
// Ignored via go/ees005
// eslint-disable-next-line @typescript-eslint/no-explicit-any
_defineProperty(this, "handleCancel", e => {
const {
view: {
state,
dispatch
},
onClickAwayCallback
} = this.props;
e.preventDefault();
onClickAwayCallback === null || onClickAwayCallback === void 0 ? void 0 : onClickAwayCallback(state, dispatch);
});
this.state = {
selectedIndex: -1,
isLoading: false,
displayUrl: normalizeUrl(props.displayUrl),
displayText: props.displayText,
items: []
};
/* Cache functions */
this.handleClearText = this.createClearHandler('displayUrl');
this.handleClearDisplayText = this.createClearHandler('displayText');
this.debouncedQuickSearch = debounce(this.quickSearch, 400);
this.fireCustomAnalytics = fireAnalyticsEvent(props.createAnalyticsEvent);
}
async componentDidMount() {
const {
timesViewed,
inputMethod,
searchSessionId
} = this.props;
// Ignored via go/ees005
// eslint-disable-next-line @repo/internal/dom-events/no-unsafe-event-listeners, @atlaskit/platform/no-direct-document-usage
document.addEventListener('mousedown', this.handleClickOutside);
this.fireAnalytics({
action: ACTION.VIEWED,
actionSubject: ACTION_SUBJECT.CREATE_LINK_INLINE_DIALOG,
attributes: {
timesViewed: timesViewed !== null && timesViewed !== void 0 ? timesViewed : 0,
searchSessionId: searchSessionId !== null && searchSessionId !== void 0 ? searchSessionId : '',
trigger: inputMethod !== null && inputMethod !== void 0 ? inputMethod : ''
},
eventType: EVENT_TYPE.SCREEN
});
const [activityProvider, searchProvider] = await Promise.all([this.props.activityProvider, this.props.searchProvider]);
// loadInitialLinkSearchResult and updateInput rely on activityProvider being set in state
flushSync(() => this.setState({
activityProvider,
searchProvider
}));
await this.loadInitialLinkSearchResult();
}
componentWillUnmount() {
const {
searchSessionId
} = this.props;
// Ignored via go/ees005
// eslint-disable-next-line @repo/internal/dom-events/no-unsafe-event-listeners, @atlaskit/platform/no-direct-document-usage
document.removeEventListener('mousedown', this.handleClickOutside);
if (!this.submitted) {
this.fireAnalytics({
action: ACTION.DISMISSED,
actionSubject: ACTION_SUBJECT.CREATE_LINK_INLINE_DIALOG,
attributes: {
source: this.analyticSource,
searchSessionId: searchSessionId !== null && searchSessionId !== void 0 ? searchSessionId : '',
trigger: 'blur'
},
eventType: EVENT_TYPE.UI
});
}
}
async getRecentItems(activityProvider, query) {
const {
searchSessionId
} = this.props;
const perfStart = performance.now();
try {
const activityRecentItems = limit(query ? await activityProvider.searchRecent(query) : await activityProvider.getRecentItems());
const items = activityRecentItems.map(mapActivityProviderResultToLinkSearchItemData);
const perfStop = performance.now();
const duration = perfStop - perfStart;
this.fireAnalytics({
action: ACTION.INVOKED,
actionSubject: ACTION_SUBJECT.SEARCH_RESULT,
actionSubjectId: ACTION_SUBJECT_ID.RECENT_ACTIVITIES,
attributes: {
duration,
count: items.length
},
eventType: EVENT_TYPE.OPERATIONAL
});
this.fireAnalytics({
action: ACTION.SHOWN,
actionSubject: ACTION_SUBJECT.SEARCH_RESULT,
actionSubjectId: ACTION_SUBJECT_ID.PRE_QUERY_SEARCH_RESULTS,
attributes: {
source: this.analyticSource,
preQueryRequestDurationMs: duration,
searchSessionId: searchSessionId !== null && searchSessionId !== void 0 ? searchSessionId : '',
resultCount: items.length,
results: activityRecentItems.map(item => {
var _item$type;
return {
resultContentId: item.objectId,
resultType: (_item$type = item.type) !== null && _item$type !== void 0 ? _item$type : ''
};
})
},
eventType: EVENT_TYPE.UI
});
return items;
// Ignored via go/ees005
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (err) {
const perfStop = performance.now();
const duration = perfStop - perfStart;
this.fireAnalytics({
action: ACTION.INVOKED,
actionSubject: ACTION_SUBJECT.SEARCH_RESULT,
actionSubjectId: ACTION_SUBJECT_ID.RECENT_ACTIVITIES,
attributes: {
duration,
count: -1,
errorCode: err.status
},
eventType: EVENT_TYPE.OPERATIONAL
});
return [];
}
}
fireAnalytics(payload) {
if (this.props.createAnalyticsEvent && this.fireCustomAnalytics) {
this.fireCustomAnalytics({
payload
});
}
}
async loadInitialLinkSearchResult() {
const {
displayUrl,
activityProvider
} = this.state;
try {
if (!displayUrl && activityProvider) {
this.setState({
isLoading: true
});
const items = await this.getRecentItems(activityProvider);
this.setState({
items
});
}
} finally {
this.setState({
isLoading: false
});
}
}
render() {
const {
items,
isLoading,
selectedIndex,
displayUrl,
displayText
} = this.state;
const {
intl: {
formatMessage
}
} = this.props;
const formatClearLinkText = formatMessage(messages.clearLink);
const screenReaderDescriptionId = 'search-recent-links-field-description';
const linkSearchListId = 'hyperlink-search-list';
const ariaActiveDescendant = selectedIndex > -1 ? `link-search-list-item-${selectedIndex}` : '';
const linkSearchInputId = 'search-recent-links-field-id';
const displayTextInputId = 'display-text-filed-id';
const browser = getBrowserInfo();
// Added workaround with a screen reader Announcer specifically for VoiceOver + Safari
// as the Aria design pattern for combobox does not work in this case
// for details: https://a11y-internal.atlassian.net/browse/AK-740
const screenReaderText = browser.safari && this.getScreenReaderText();
const hyperlinkElement = jsx("div", {
"aria-label": formatMessage(messages.hyperlinkAriaLabel)
// eslint-disable-next-line @atlaskit/ui-styling-standard/no-classname-prop -- Ignored via go/DSP-18766
,
className: "recent-list",
"data-testid": "hyperlink-add-toolbar"
}, jsx("div", {
// eslint-disable-next-line @atlaskit/ui-styling-standard/no-imported-style-values, @atlaskit/design-system/consistent-css-prop-usage -- Ignored via go/DSP-18766
css: [container, narrowContainerWidth, containerPadding],
ref: this.wrapperRef
}, jsx("label", {
htmlFor: linkSearchInputId,
css: inputLabel
}, formatMessage(messages.linkVisibleLabel)), jsx("div", {
css: [inputWrapper, inputWrapperPosition]
}, screenReaderText && jsx(Announcer, {
ariaLive: "assertive",
text: screenReaderText,
ariaRelevant: "additions",
delay: 250
}), jsx("div", {
css: visuallyHiddenStyles,
"aria-hidden": "true",
id: screenReaderDescriptionId
}, formatMessage(messages.searchLinkAriaDescription)), jsx(PanelTextInput, {
role: "combobox",
ariaExpanded: items && items.length > 0 && !isLoading,
ariaActiveDescendant: ariaActiveDescendant,
ariaControls: linkSearchListId,
ariaAutoComplete: true,
describedById: screenReaderDescriptionId
// eslint-disable-next-line @atlassian/perf-linting/no-unstable-inline-props -- Ignored via go/ees017 (to be fixed)
,
ref: ele => this.urlInputContainer = ele,
testId: 'link-url',
onSubmit: this.handleSubmit,
onChange: this.updateInput
// eslint-disable-next-line @atlassian/perf-linting/no-unstable-inline-props -- Ignored via go/ees017 (to be fixed)
,
autoFocus: {
preventScroll: true
},
onCancel: this.handleCancel,
defaultValue: displayUrl,
onKeyDown: this.handleKeyDown,
inputId: linkSearchInputId
}), displayUrl && jsx("div", {
css: clearTextWrapper
}, jsx(Tooltip, {
content: formatClearLinkText
}, jsx(Pressable, {
xcss: clearTextButtonStyles,
onClick: this.handleClearText
}, jsx(CrossCircleIcon, {
label: formatClearLinkText,
color: "var(--ds-icon-subtle, #505258)"
}))))), jsx("label", {
htmlFor: displayTextInputId,
css: [inputLabel, textLabelMargin]
}, formatMessage(messages.textVisibleLabel)), jsx("div", {
css: [inputWrapper, inputWrapperPosition]
}, jsx(PanelTextInput
// eslint-disable-next-line @atlassian/perf-linting/no-unstable-inline-props -- Ignored via go/ees017 (to be fixed)
, {
ref: ele => this.displayTextInputContainer = ele,
testId: 'link-text',
onChange: this.updateTextInput,
onCancel: this.handleCancel,
defaultValue: displayText,
onSubmit: this.handleSubmit,
onKeyDown: this.handleKeyDown,
inputId: displayTextInputId
}), displayText && jsx("div", {
css: clearTextWrapper
}, jsx(Tooltip, {
content: formatMessage(messages.clearText)
}, jsx(Pressable, {
xcss: clearTextButtonStyles,
onClick: this.handleClearDisplayText,
onKeyDown: this.handleClearTextKeyDown
}, jsx(CrossCircleIcon, {
label: formatMessage(messages.clearText),
color: "var(--ds-icon-subtle, #505258)"
}))))), jsx("div", {
css: visuallyHiddenStyles,
"aria-live": "polite",
"aria-atomic": "true",
id: "fabric.editor.hyperlink.suggested.results"
}, displayUrl && formatMessage(messages.searchLinkResults, {
count: items.length
})), jsx(LinkSearchList, {
ariaControls: "fabric.editor.hyperlink.suggested.results",
id: linkSearchListId,
role: "listbox",
items: items,
isLoading: isLoading,
selectedIndex: selectedIndex,
listItemRefCallback: this.listItemRefCallback,
onFocus: this.handleListItemFocus,
onBlur: this.handleListItemBlur,
onKeyDown: this.handleKeyDown,
onSelect: this.handleSelected,
onMouseEnter: this.handleMouseEnterResultItem,
onMouseLeave: this.handleMouseLeaveResultItem
})));
if (expValEquals('platform_editor_a11y_escape_link_dialog', 'isEnabled', true)) {
return (
// eslint-disable-next-line @atlassian/perf-linting/no-unstable-inline-props -- Ignored via go/ees017 (to be fixed)
jsx(FocusLock, {
returnFocus: {
preventScroll: true
},
focusOptions: {
preventScroll: true
}
}, hyperlinkElement)
);
}
return hyperlinkElement;
}
}
function findIndex(array, predicate) {
let index = -1;
array.some((item, i) => {
if (predicate(item)) {
index = i;
return true;
}
return false;
});
return index;
}
function limit(items) {
return items.slice(0, RECENT_SEARCH_LIST_SIZE);
}
// eslint-disable-next-line @typescript-eslint/ban-types
export const HyperlinkLinkAddToolbarWithIntl = injectIntl(HyperlinkLinkAddToolbar);
const _default_1 = withAnalyticsEvents()(HyperlinkLinkAddToolbarWithIntl);
export default _default_1;