@selfcommunity/react-ui 0.7.0-alpha.332 → 0.7.0-alpha.333

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -118,7 +118,7 @@ function CommentObjectReply(inProps) {
118
118
  return _html === '' || _html === '<p class="SCEditor-paragraph"></p>' || _html === '<p class="SCEditor-paragraph"><br></p>';
119
119
  }, [html]);
120
120
  // RENDER
121
- return (react_1.default.createElement(Root, Object.assign({}, rest, { disableTypography: true, onClick: handleEditorFocus, elevation: elevation, className: (0, classnames_1.default)(classes.root, className), image: !scUserContext.user ? (react_1.default.createElement(material_1.Avatar, { variant: "circular", className: classes.avatar })) : (react_1.default.createElement(UserAvatar_1.default, { hide: !scUserContext.user.community_badge },
121
+ return (react_1.default.createElement(Root, Object.assign({ id: id }, rest, { disableTypography: true, onClick: handleEditorFocus, elevation: elevation, className: (0, classnames_1.default)(classes.root, className), image: !scUserContext.user ? (react_1.default.createElement(material_1.Avatar, { variant: "circular", className: classes.avatar })) : (react_1.default.createElement(UserAvatar_1.default, { hide: !scUserContext.user.community_badge },
122
122
  react_1.default.createElement(material_1.Avatar, { alt: scUserContext.user.username, variant: "circular", src: scUserContext.user.avatar, classes: { root: classes.avatar } }))), secondary: react_1.default.createElement(Widget_1.default, Object.assign({ className: (0, classnames_1.default)(classes.comment, { [classes.hasValue]: !isEditorEmpty }) }, WidgetProps),
123
123
  react_1.default.createElement(Editor_1.default, { ref: editor, onChange: handleChangeText, defaultValue: html, editable: editable, uploadImage: true }),
124
124
  !isEditorEmpty && (react_1.default.createElement(material_1.Stack, { direction: "row", spacing: 2, className: classes.actions },
@@ -1,3 +1,15 @@
1
- export default function MentionsPlugin({ containerSelector }: {
2
- containerSelector?: string;
3
- }): JSX.Element;
1
+ import { MutableRefObject } from 'react';
2
+ declare type MentionMatch = {
3
+ leadOffset: number;
4
+ matchingString: string;
5
+ replaceableString: string;
6
+ };
7
+ declare type Resolution = {
8
+ match: MentionMatch;
9
+ range: Range;
10
+ };
11
+ export declare function getScrollParent(element: HTMLElement, includeHidden: boolean): HTMLElement | HTMLBodyElement;
12
+ export declare function useDynamicPositioning(resolution: Resolution | null, targetElement: HTMLElement | null, onReposition: () => void, onVisibilityChange?: (isInView: boolean) => void): void;
13
+ export declare function useMenuAnchorRef(resolution: Resolution | null, setResolution: (r: Resolution | null) => void, className?: string): MutableRefObject<HTMLElement>;
14
+ export default function MentionsPlugin(): JSX.Element;
15
+ export {};
@@ -1,5 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.useMenuAnchorRef = exports.useDynamicPositioning = exports.getScrollParent = void 0;
3
4
  const tslib_1 = require("tslib");
4
5
  const react_1 = tslib_1.__importStar(require("react"));
5
6
  const react_core_1 = require("@selfcommunity/react-core");
@@ -40,6 +41,137 @@ const ALIAS_LENGTH_LIMIT = 50;
40
41
  const AtSignMentionsRegexAliasRegex = new RegExp('(^|\\s|\\()(' + '[' + TRIGGERS + ']' + '((?:' + VALID_CHARS + '){0,' + ALIAS_LENGTH_LIMIT + '})' + ')$');
41
42
  // At most, 5 suggestions are shown in the popup.
42
43
  const SUGGESTION_LIST_LENGTH_LIMIT = 5;
44
+ function isTriggerVisibleInNearestScrollContainer(targetElement, containerElement) {
45
+ const tRect = targetElement.getBoundingClientRect();
46
+ const cRect = containerElement.getBoundingClientRect();
47
+ return tRect.top > cRect.top && tRect.top < cRect.bottom;
48
+ }
49
+ function getScrollParent(element, includeHidden) {
50
+ let style = getComputedStyle(element);
51
+ const excludeStaticParent = style.position === 'absolute';
52
+ const overflowRegex = includeHidden ? /(auto|scroll|hidden)/ : /(auto|scroll)/;
53
+ if (style.position === 'fixed') {
54
+ return document.body;
55
+ }
56
+ for (let parent = element; (parent = parent.parentElement);) {
57
+ style = getComputedStyle(parent);
58
+ if (excludeStaticParent && style.position === 'static') {
59
+ continue;
60
+ }
61
+ if (overflowRegex.test(style.overflow + style.overflowY + style.overflowX)) {
62
+ return parent;
63
+ }
64
+ }
65
+ return document.body;
66
+ }
67
+ exports.getScrollParent = getScrollParent;
68
+ function useDynamicPositioning(resolution, targetElement, onReposition, onVisibilityChange) {
69
+ const [editor] = (0, LexicalComposerContext_1.useLexicalComposerContext)();
70
+ (0, react_1.useEffect)(() => {
71
+ if (targetElement != null && resolution != null) {
72
+ const rootElement = editor.getRootElement();
73
+ const rootScrollParent = rootElement != null ? getScrollParent(rootElement, false) : document.body;
74
+ let ticking = false;
75
+ let previousIsInView = isTriggerVisibleInNearestScrollContainer(targetElement, rootScrollParent);
76
+ const handleScroll = function () {
77
+ if (!ticking) {
78
+ window.requestAnimationFrame(function () {
79
+ onReposition();
80
+ ticking = false;
81
+ });
82
+ ticking = true;
83
+ }
84
+ const isInView = isTriggerVisibleInNearestScrollContainer(targetElement, rootScrollParent);
85
+ if (isInView !== previousIsInView) {
86
+ previousIsInView = isInView;
87
+ if (onVisibilityChange != null) {
88
+ onVisibilityChange(isInView);
89
+ }
90
+ }
91
+ };
92
+ const resizeObserver = new ResizeObserver(onReposition);
93
+ window.addEventListener('resize', onReposition);
94
+ document.addEventListener('scroll', handleScroll, {
95
+ capture: true,
96
+ passive: true
97
+ });
98
+ resizeObserver.observe(targetElement);
99
+ return () => {
100
+ resizeObserver.unobserve(targetElement);
101
+ window.removeEventListener('resize', onReposition);
102
+ document.removeEventListener('scroll', handleScroll, true);
103
+ };
104
+ }
105
+ }, [targetElement, editor, onVisibilityChange, onReposition, resolution]);
106
+ }
107
+ exports.useDynamicPositioning = useDynamicPositioning;
108
+ function useMenuAnchorRef(resolution, setResolution, className) {
109
+ const [editor] = (0, LexicalComposerContext_1.useLexicalComposerContext)();
110
+ const anchorElementRef = (0, react_1.useRef)(document.createElement('div'));
111
+ const positionMenu = (0, react_1.useCallback)(() => {
112
+ const rootElement = editor.getRootElement();
113
+ const containerDiv = anchorElementRef.current;
114
+ const menuEle = containerDiv.firstChild;
115
+ if (rootElement !== null && resolution !== null) {
116
+ const { left, top, width, height } = resolution.range.getBoundingClientRect();
117
+ containerDiv.style.top = `${top + window.pageYOffset}px`;
118
+ containerDiv.style.left = `${left + window.pageXOffset}px`;
119
+ containerDiv.style.height = `${height}px`;
120
+ containerDiv.style.width = `${width}px`;
121
+ if (menuEle !== null) {
122
+ const menuRect = menuEle.getBoundingClientRect();
123
+ const menuHeight = menuRect.height;
124
+ const menuWidth = menuRect.width;
125
+ const rootElementRect = rootElement.getBoundingClientRect();
126
+ if (left + menuWidth > rootElementRect.right) {
127
+ containerDiv.style.left = `${rootElementRect.right - menuWidth + window.pageXOffset}px`;
128
+ }
129
+ const margin = 10;
130
+ if ((top + menuHeight > window.innerHeight || top + menuHeight > rootElementRect.bottom) && top - rootElementRect.top > menuHeight) {
131
+ containerDiv.style.top = `${top - menuHeight + window.pageYOffset - (height + margin)}px`;
132
+ }
133
+ }
134
+ if (!containerDiv.isConnected) {
135
+ if (className != null) {
136
+ containerDiv.className = className;
137
+ }
138
+ containerDiv.setAttribute('aria-label', 'Typeahead menu');
139
+ containerDiv.setAttribute('id', 'typeahead-menu');
140
+ containerDiv.setAttribute('role', 'listbox');
141
+ containerDiv.style.display = 'block';
142
+ containerDiv.style.position = 'absolute';
143
+ document.body.append(containerDiv);
144
+ }
145
+ anchorElementRef.current = containerDiv;
146
+ rootElement.setAttribute('aria-controls', 'typeahead-menu');
147
+ }
148
+ }, [editor, resolution, className]);
149
+ (0, react_1.useEffect)(() => {
150
+ const rootElement = editor.getRootElement();
151
+ if (resolution !== null) {
152
+ positionMenu();
153
+ return () => {
154
+ if (rootElement !== null) {
155
+ rootElement.removeAttribute('aria-controls');
156
+ }
157
+ const containerDiv = anchorElementRef.current;
158
+ if (containerDiv !== null && containerDiv.isConnected) {
159
+ containerDiv.remove();
160
+ }
161
+ };
162
+ }
163
+ }, [editor, positionMenu, resolution]);
164
+ const onVisibilityChange = (0, react_1.useCallback)((isInView) => {
165
+ if (resolution !== null) {
166
+ if (!isInView) {
167
+ setResolution(null);
168
+ }
169
+ }
170
+ }, [resolution, setResolution]);
171
+ useDynamicPositioning(resolution, anchorElementRef.current, positionMenu, onVisibilityChange);
172
+ return anchorElementRef;
173
+ }
174
+ exports.useMenuAnchorRef = useMenuAnchorRef;
43
175
  const mentionsCache = new Map();
44
176
  function useMentionLookupService(mentionString) {
45
177
  const [results, setResults] = (0, react_1.useState)(null);
@@ -73,34 +205,12 @@ function MentionsTypeaheadItem({ index, isHovered, isSelected, onClick, onMouseE
73
205
  " ",
74
206
  result.username));
75
207
  }
76
- function MentionsTypeahead({ close, editor, resolution, className = '', containerEl = null }) {
208
+ function MentionsTypeahead({ close, editor, resolution, className = '' }) {
77
209
  const divRef = (0, react_1.useRef)(null);
78
210
  const match = resolution.match;
79
211
  const results = useMentionLookupService(match.matchingString);
80
212
  const [selectedIndex, setSelectedIndex] = (0, react_1.useState)(null);
81
213
  const [hoveredIndex, setHoveredIndex] = (0, react_1.useState)(null);
82
- (0, react_1.useEffect)(() => {
83
- const div = divRef.current;
84
- const rootElement = editor.getRootElement();
85
- const parentContainerElement = containerEl ? containerEl : rootElement.parentElement;
86
- if (results !== null && div !== null && rootElement !== null) {
87
- const range = resolution.range;
88
- // Re-calc, relative to the parent container, prevent scroll problems
89
- const parentRootPos = parentContainerElement.getBoundingClientRect();
90
- const { left, right, top, height } = range.getBoundingClientRect();
91
- let relativePosTop = top - parentRootPos.top;
92
- let relativePosLeft = right - parentRootPos.left;
93
- div.style.position = 'absolute';
94
- div.style.top = `${relativePosTop + height + 7}px`;
95
- div.style.left = `${relativePosLeft - 14}px`;
96
- div.style.display = 'block';
97
- rootElement.setAttribute('aria-controls', 'mentions-typeahead');
98
- return () => {
99
- div.style.display = 'none';
100
- rootElement.removeAttribute('aria-controls');
101
- };
102
- }
103
- }, [editor, resolution, results]);
104
214
  const applyCurrentSelected = (0, react_1.useCallback)((index) => {
105
215
  index = index || selectedIndex;
106
216
  if (results === null || index === null) {
@@ -139,9 +249,9 @@ function MentionsTypeahead({ close, editor, resolution, className = '', containe
139
249
  if (results !== null && selectedIndex !== null) {
140
250
  if (selectedIndex < SUGGESTION_LIST_LENGTH_LIMIT - 1 && selectedIndex !== results.length - 1) {
141
251
  updateSelectedIndex(selectedIndex + 1);
252
+ event.preventDefault();
253
+ event.stopImmediatePropagation();
142
254
  }
143
- event.preventDefault();
144
- event.stopImmediatePropagation();
145
255
  }
146
256
  return true;
147
257
  }, lexical_1.COMMAND_PRIORITY_LOW), editor.registerCommand(lexical_1.KEY_ARROW_UP_COMMAND, (payload) => {
@@ -149,9 +259,9 @@ function MentionsTypeahead({ close, editor, resolution, className = '', containe
149
259
  if (results !== null && selectedIndex !== null) {
150
260
  if (selectedIndex !== 0) {
151
261
  updateSelectedIndex(selectedIndex - 1);
262
+ event.preventDefault();
263
+ event.stopImmediatePropagation();
152
264
  }
153
- event.preventDefault();
154
- event.stopImmediatePropagation();
155
265
  }
156
266
  return true;
157
267
  }, lexical_1.COMMAND_PRIORITY_LOW), editor.registerCommand(lexical_1.KEY_ESCAPE_COMMAND, (payload) => {
@@ -260,6 +370,9 @@ function tryToPositionRange(match, range) {
260
370
  const anchorNode = domSelection.anchorNode;
261
371
  const startOffset = match.leadOffset;
262
372
  const endOffset = domSelection.anchorOffset;
373
+ if (anchorNode == null || endOffset == null) {
374
+ return false;
375
+ }
263
376
  try {
264
377
  range.setStart(anchorNode, startOffset);
265
378
  range.setEnd(anchorNode, endOffset);
@@ -366,8 +479,9 @@ const Root = (0, styles_1.styled)(MentionsTypeahead, {
366
479
  slot: 'Root',
367
480
  overridesResolver: (props, styles) => styles.root
368
481
  })(({ theme }) => ({}));
369
- function useMentions(editor, containerSelector = null) {
482
+ function useMentions(editor, anchorClassName = null) {
370
483
  const [resolution, setResolution] = (0, react_1.useState)(null);
484
+ const anchorElementRef = useMenuAnchorRef(resolution, setResolution, anchorClassName);
371
485
  (0, react_1.useEffect)(() => {
372
486
  if (!editor.hasNodes([MentionNode_1.MentionNode])) {
373
487
  throw new Error('MentionsPlugin: MentionNode not registered on editor');
@@ -410,18 +524,14 @@ function useMentions(editor, containerSelector = null) {
410
524
  }, [editor]);
411
525
  const closeTypeahead = (0, react_1.useCallback)(() => {
412
526
  setResolution(null);
413
- }, []);
527
+ }, [resolution]);
414
528
  if (resolution === null || editor === null) {
415
529
  return null;
416
530
  }
417
- // Set portal container
418
- const portalContainer = containerSelector
419
- ? editor.getRootElement().parentElement.closest(containerSelector)
420
- : editor.getRootElement().parentElement;
421
- return (0, react_dom_1.createPortal)(react_1.default.createElement(Root, { close: closeTypeahead, resolution: resolution, editor: editor, className: classes.root, containerEl: portalContainer }), portalContainer);
531
+ return (0, react_dom_1.createPortal)(react_1.default.createElement(Root, { close: closeTypeahead, resolution: resolution, editor: editor, className: classes.root }), anchorElementRef.current);
422
532
  }
423
- function MentionsPlugin({ containerSelector = 'body' }) {
533
+ function MentionsPlugin() {
424
534
  const [editor] = (0, LexicalComposerContext_1.useLexicalComposerContext)();
425
- return useMentions(editor, containerSelector);
535
+ return useMentions(editor);
426
536
  }
427
537
  exports.default = MentionsPlugin;
@@ -457,7 +457,7 @@ function FeedObject(inProps) {
457
457
  react_1.default.createElement(material_1.CardActions, { className: classes.actionsSection },
458
458
  react_1.default.createElement(Actions_1.default, Object.assign({ feedObjectId: feedObjectId, feedObjectType: feedObjectType, feedObject: obj, hideCommentAction: template === feedObject_1.SCFeedObjectTemplateType.DETAIL, handleExpandActivities: template === feedObject_1.SCFeedObjectTemplateType.PREVIEW ? handleExpandActivities : null, VoteActionProps: { onVoteAction: handleVoteSuccess } }, ActionsProps)),
459
459
  (template === feedObject_1.SCFeedObjectTemplateType.DETAIL || expandedActivities) && (react_1.default.createElement(material_1.Box, { className: classes.replyContent },
460
- react_1.default.createElement(CommentObjectReplyComponent, Object.assign({ onReply: handleReply, editable: !isReplying || Boolean(obj), key: Number(isReplying) }, CommentObjectReplyComponentProps))))),
460
+ react_1.default.createElement(CommentObjectReplyComponent, Object.assign({ id: `reply-feedObject-${obj.id}`, onReply: handleReply, editable: !isReplying || Boolean(obj), key: Number(isReplying) }, CommentObjectReplyComponentProps))))),
461
461
  template === feedObject_1.SCFeedObjectTemplateType.PREVIEW && (obj.comment_count > 0 || (feedObjectActivities && feedObjectActivities.length > 0)) && (react_1.default.createElement(material_1.Collapse, { in: expandedActivities, timeout: "auto", classes: { root: classes.activitiesSection } },
462
462
  react_1.default.createElement(CardContent_1.default, { className: classes.activitiesContent },
463
463
  react_1.default.createElement(Activities_1.default, Object.assign({ feedObject: obj, key: selectedActivities, feedObjectActivities: feedObjectActivities, activitiesType: selectedActivities, onSetSelectedActivities: handleSelectedActivities, comments: comments, CommentsObjectProps: {
@@ -116,7 +116,7 @@ export default function CommentObjectReply(inProps) {
116
116
  return _html === '' || _html === '<p class="SCEditor-paragraph"></p>' || _html === '<p class="SCEditor-paragraph"><br></p>';
117
117
  }, [html]);
118
118
  // RENDER
119
- return (React.createElement(Root, Object.assign({}, rest, { disableTypography: true, onClick: handleEditorFocus, elevation: elevation, className: classNames(classes.root, className), image: !scUserContext.user ? (React.createElement(Avatar, { variant: "circular", className: classes.avatar })) : (React.createElement(UserAvatar, { hide: !scUserContext.user.community_badge },
119
+ return (React.createElement(Root, Object.assign({ id: id }, rest, { disableTypography: true, onClick: handleEditorFocus, elevation: elevation, className: classNames(classes.root, className), image: !scUserContext.user ? (React.createElement(Avatar, { variant: "circular", className: classes.avatar })) : (React.createElement(UserAvatar, { hide: !scUserContext.user.community_badge },
120
120
  React.createElement(Avatar, { alt: scUserContext.user.username, variant: "circular", src: scUserContext.user.avatar, classes: { root: classes.avatar } }))), secondary: React.createElement(Widget, Object.assign({ className: classNames(classes.comment, { [classes.hasValue]: !isEditorEmpty }) }, WidgetProps),
121
121
  React.createElement(Editor, { ref: editor, onChange: handleChangeText, defaultValue: html, editable: editable, uploadImage: true }),
122
122
  !isEditorEmpty && (React.createElement(Stack, { direction: "row", spacing: 2, className: classes.actions },
@@ -1,3 +1,15 @@
1
- export default function MentionsPlugin({ containerSelector }: {
2
- containerSelector?: string;
3
- }): JSX.Element;
1
+ import { MutableRefObject } from 'react';
2
+ declare type MentionMatch = {
3
+ leadOffset: number;
4
+ matchingString: string;
5
+ replaceableString: string;
6
+ };
7
+ declare type Resolution = {
8
+ match: MentionMatch;
9
+ range: Range;
10
+ };
11
+ export declare function getScrollParent(element: HTMLElement, includeHidden: boolean): HTMLElement | HTMLBodyElement;
12
+ export declare function useDynamicPositioning(resolution: Resolution | null, targetElement: HTMLElement | null, onReposition: () => void, onVisibilityChange?: (isInView: boolean) => void): void;
13
+ export declare function useMenuAnchorRef(resolution: Resolution | null, setResolution: (r: Resolution | null) => void, className?: string): MutableRefObject<HTMLElement>;
14
+ export default function MentionsPlugin(): JSX.Element;
15
+ export {};
@@ -37,6 +37,134 @@ const ALIAS_LENGTH_LIMIT = 50;
37
37
  const AtSignMentionsRegexAliasRegex = new RegExp('(^|\\s|\\()(' + '[' + TRIGGERS + ']' + '((?:' + VALID_CHARS + '){0,' + ALIAS_LENGTH_LIMIT + '})' + ')$');
38
38
  // At most, 5 suggestions are shown in the popup.
39
39
  const SUGGESTION_LIST_LENGTH_LIMIT = 5;
40
+ function isTriggerVisibleInNearestScrollContainer(targetElement, containerElement) {
41
+ const tRect = targetElement.getBoundingClientRect();
42
+ const cRect = containerElement.getBoundingClientRect();
43
+ return tRect.top > cRect.top && tRect.top < cRect.bottom;
44
+ }
45
+ export function getScrollParent(element, includeHidden) {
46
+ let style = getComputedStyle(element);
47
+ const excludeStaticParent = style.position === 'absolute';
48
+ const overflowRegex = includeHidden ? /(auto|scroll|hidden)/ : /(auto|scroll)/;
49
+ if (style.position === 'fixed') {
50
+ return document.body;
51
+ }
52
+ for (let parent = element; (parent = parent.parentElement);) {
53
+ style = getComputedStyle(parent);
54
+ if (excludeStaticParent && style.position === 'static') {
55
+ continue;
56
+ }
57
+ if (overflowRegex.test(style.overflow + style.overflowY + style.overflowX)) {
58
+ return parent;
59
+ }
60
+ }
61
+ return document.body;
62
+ }
63
+ export function useDynamicPositioning(resolution, targetElement, onReposition, onVisibilityChange) {
64
+ const [editor] = useLexicalComposerContext();
65
+ useEffect(() => {
66
+ if (targetElement != null && resolution != null) {
67
+ const rootElement = editor.getRootElement();
68
+ const rootScrollParent = rootElement != null ? getScrollParent(rootElement, false) : document.body;
69
+ let ticking = false;
70
+ let previousIsInView = isTriggerVisibleInNearestScrollContainer(targetElement, rootScrollParent);
71
+ const handleScroll = function () {
72
+ if (!ticking) {
73
+ window.requestAnimationFrame(function () {
74
+ onReposition();
75
+ ticking = false;
76
+ });
77
+ ticking = true;
78
+ }
79
+ const isInView = isTriggerVisibleInNearestScrollContainer(targetElement, rootScrollParent);
80
+ if (isInView !== previousIsInView) {
81
+ previousIsInView = isInView;
82
+ if (onVisibilityChange != null) {
83
+ onVisibilityChange(isInView);
84
+ }
85
+ }
86
+ };
87
+ const resizeObserver = new ResizeObserver(onReposition);
88
+ window.addEventListener('resize', onReposition);
89
+ document.addEventListener('scroll', handleScroll, {
90
+ capture: true,
91
+ passive: true
92
+ });
93
+ resizeObserver.observe(targetElement);
94
+ return () => {
95
+ resizeObserver.unobserve(targetElement);
96
+ window.removeEventListener('resize', onReposition);
97
+ document.removeEventListener('scroll', handleScroll, true);
98
+ };
99
+ }
100
+ }, [targetElement, editor, onVisibilityChange, onReposition, resolution]);
101
+ }
102
+ export function useMenuAnchorRef(resolution, setResolution, className) {
103
+ const [editor] = useLexicalComposerContext();
104
+ const anchorElementRef = useRef(document.createElement('div'));
105
+ const positionMenu = useCallback(() => {
106
+ const rootElement = editor.getRootElement();
107
+ const containerDiv = anchorElementRef.current;
108
+ const menuEle = containerDiv.firstChild;
109
+ if (rootElement !== null && resolution !== null) {
110
+ const { left, top, width, height } = resolution.range.getBoundingClientRect();
111
+ containerDiv.style.top = `${top + window.pageYOffset}px`;
112
+ containerDiv.style.left = `${left + window.pageXOffset}px`;
113
+ containerDiv.style.height = `${height}px`;
114
+ containerDiv.style.width = `${width}px`;
115
+ if (menuEle !== null) {
116
+ const menuRect = menuEle.getBoundingClientRect();
117
+ const menuHeight = menuRect.height;
118
+ const menuWidth = menuRect.width;
119
+ const rootElementRect = rootElement.getBoundingClientRect();
120
+ if (left + menuWidth > rootElementRect.right) {
121
+ containerDiv.style.left = `${rootElementRect.right - menuWidth + window.pageXOffset}px`;
122
+ }
123
+ const margin = 10;
124
+ if ((top + menuHeight > window.innerHeight || top + menuHeight > rootElementRect.bottom) && top - rootElementRect.top > menuHeight) {
125
+ containerDiv.style.top = `${top - menuHeight + window.pageYOffset - (height + margin)}px`;
126
+ }
127
+ }
128
+ if (!containerDiv.isConnected) {
129
+ if (className != null) {
130
+ containerDiv.className = className;
131
+ }
132
+ containerDiv.setAttribute('aria-label', 'Typeahead menu');
133
+ containerDiv.setAttribute('id', 'typeahead-menu');
134
+ containerDiv.setAttribute('role', 'listbox');
135
+ containerDiv.style.display = 'block';
136
+ containerDiv.style.position = 'absolute';
137
+ document.body.append(containerDiv);
138
+ }
139
+ anchorElementRef.current = containerDiv;
140
+ rootElement.setAttribute('aria-controls', 'typeahead-menu');
141
+ }
142
+ }, [editor, resolution, className]);
143
+ useEffect(() => {
144
+ const rootElement = editor.getRootElement();
145
+ if (resolution !== null) {
146
+ positionMenu();
147
+ return () => {
148
+ if (rootElement !== null) {
149
+ rootElement.removeAttribute('aria-controls');
150
+ }
151
+ const containerDiv = anchorElementRef.current;
152
+ if (containerDiv !== null && containerDiv.isConnected) {
153
+ containerDiv.remove();
154
+ }
155
+ };
156
+ }
157
+ }, [editor, positionMenu, resolution]);
158
+ const onVisibilityChange = useCallback((isInView) => {
159
+ if (resolution !== null) {
160
+ if (!isInView) {
161
+ setResolution(null);
162
+ }
163
+ }
164
+ }, [resolution, setResolution]);
165
+ useDynamicPositioning(resolution, anchorElementRef.current, positionMenu, onVisibilityChange);
166
+ return anchorElementRef;
167
+ }
40
168
  const mentionsCache = new Map();
41
169
  function useMentionLookupService(mentionString) {
42
170
  const [results, setResults] = useState(null);
@@ -70,34 +198,12 @@ function MentionsTypeaheadItem({ index, isHovered, isSelected, onClick, onMouseE
70
198
  " ",
71
199
  result.username));
72
200
  }
73
- function MentionsTypeahead({ close, editor, resolution, className = '', containerEl = null }) {
201
+ function MentionsTypeahead({ close, editor, resolution, className = '' }) {
74
202
  const divRef = useRef(null);
75
203
  const match = resolution.match;
76
204
  const results = useMentionLookupService(match.matchingString);
77
205
  const [selectedIndex, setSelectedIndex] = useState(null);
78
206
  const [hoveredIndex, setHoveredIndex] = useState(null);
79
- useEffect(() => {
80
- const div = divRef.current;
81
- const rootElement = editor.getRootElement();
82
- const parentContainerElement = containerEl ? containerEl : rootElement.parentElement;
83
- if (results !== null && div !== null && rootElement !== null) {
84
- const range = resolution.range;
85
- // Re-calc, relative to the parent container, prevent scroll problems
86
- const parentRootPos = parentContainerElement.getBoundingClientRect();
87
- const { left, right, top, height } = range.getBoundingClientRect();
88
- let relativePosTop = top - parentRootPos.top;
89
- let relativePosLeft = right - parentRootPos.left;
90
- div.style.position = 'absolute';
91
- div.style.top = `${relativePosTop + height + 7}px`;
92
- div.style.left = `${relativePosLeft - 14}px`;
93
- div.style.display = 'block';
94
- rootElement.setAttribute('aria-controls', 'mentions-typeahead');
95
- return () => {
96
- div.style.display = 'none';
97
- rootElement.removeAttribute('aria-controls');
98
- };
99
- }
100
- }, [editor, resolution, results]);
101
207
  const applyCurrentSelected = useCallback((index) => {
102
208
  index = index || selectedIndex;
103
209
  if (results === null || index === null) {
@@ -136,9 +242,9 @@ function MentionsTypeahead({ close, editor, resolution, className = '', containe
136
242
  if (results !== null && selectedIndex !== null) {
137
243
  if (selectedIndex < SUGGESTION_LIST_LENGTH_LIMIT - 1 && selectedIndex !== results.length - 1) {
138
244
  updateSelectedIndex(selectedIndex + 1);
245
+ event.preventDefault();
246
+ event.stopImmediatePropagation();
139
247
  }
140
- event.preventDefault();
141
- event.stopImmediatePropagation();
142
248
  }
143
249
  return true;
144
250
  }, COMMAND_PRIORITY_LOW), editor.registerCommand(KEY_ARROW_UP_COMMAND, (payload) => {
@@ -146,9 +252,9 @@ function MentionsTypeahead({ close, editor, resolution, className = '', containe
146
252
  if (results !== null && selectedIndex !== null) {
147
253
  if (selectedIndex !== 0) {
148
254
  updateSelectedIndex(selectedIndex - 1);
255
+ event.preventDefault();
256
+ event.stopImmediatePropagation();
149
257
  }
150
- event.preventDefault();
151
- event.stopImmediatePropagation();
152
258
  }
153
259
  return true;
154
260
  }, COMMAND_PRIORITY_LOW), editor.registerCommand(KEY_ESCAPE_COMMAND, (payload) => {
@@ -257,6 +363,9 @@ function tryToPositionRange(match, range) {
257
363
  const anchorNode = domSelection.anchorNode;
258
364
  const startOffset = match.leadOffset;
259
365
  const endOffset = domSelection.anchorOffset;
366
+ if (anchorNode == null || endOffset == null) {
367
+ return false;
368
+ }
260
369
  try {
261
370
  range.setStart(anchorNode, startOffset);
262
371
  range.setEnd(anchorNode, endOffset);
@@ -363,8 +472,9 @@ const Root = styled(MentionsTypeahead, {
363
472
  slot: 'Root',
364
473
  overridesResolver: (props, styles) => styles.root
365
474
  })(({ theme }) => ({}));
366
- function useMentions(editor, containerSelector = null) {
475
+ function useMentions(editor, anchorClassName = null) {
367
476
  const [resolution, setResolution] = useState(null);
477
+ const anchorElementRef = useMenuAnchorRef(resolution, setResolution, anchorClassName);
368
478
  useEffect(() => {
369
479
  if (!editor.hasNodes([MentionNode])) {
370
480
  throw new Error('MentionsPlugin: MentionNode not registered on editor');
@@ -407,17 +517,13 @@ function useMentions(editor, containerSelector = null) {
407
517
  }, [editor]);
408
518
  const closeTypeahead = useCallback(() => {
409
519
  setResolution(null);
410
- }, []);
520
+ }, [resolution]);
411
521
  if (resolution === null || editor === null) {
412
522
  return null;
413
523
  }
414
- // Set portal container
415
- const portalContainer = containerSelector
416
- ? editor.getRootElement().parentElement.closest(containerSelector)
417
- : editor.getRootElement().parentElement;
418
- return createPortal(React.createElement(Root, { close: closeTypeahead, resolution: resolution, editor: editor, className: classes.root, containerEl: portalContainer }), portalContainer);
524
+ return createPortal(React.createElement(Root, { close: closeTypeahead, resolution: resolution, editor: editor, className: classes.root }), anchorElementRef.current);
419
525
  }
420
- export default function MentionsPlugin({ containerSelector = 'body' }) {
526
+ export default function MentionsPlugin() {
421
527
  const [editor] = useLexicalComposerContext();
422
- return useMentions(editor, containerSelector);
528
+ return useMentions(editor);
423
529
  }
@@ -455,7 +455,7 @@ export default function FeedObject(inProps) {
455
455
  React.createElement(CardActions, { className: classes.actionsSection },
456
456
  React.createElement(Actions, Object.assign({ feedObjectId: feedObjectId, feedObjectType: feedObjectType, feedObject: obj, hideCommentAction: template === SCFeedObjectTemplateType.DETAIL, handleExpandActivities: template === SCFeedObjectTemplateType.PREVIEW ? handleExpandActivities : null, VoteActionProps: { onVoteAction: handleVoteSuccess } }, ActionsProps)),
457
457
  (template === SCFeedObjectTemplateType.DETAIL || expandedActivities) && (React.createElement(Box, { className: classes.replyContent },
458
- React.createElement(CommentObjectReplyComponent, Object.assign({ onReply: handleReply, editable: !isReplying || Boolean(obj), key: Number(isReplying) }, CommentObjectReplyComponentProps))))),
458
+ React.createElement(CommentObjectReplyComponent, Object.assign({ id: `reply-feedObject-${obj.id}`, onReply: handleReply, editable: !isReplying || Boolean(obj), key: Number(isReplying) }, CommentObjectReplyComponentProps))))),
459
459
  template === SCFeedObjectTemplateType.PREVIEW && (obj.comment_count > 0 || (feedObjectActivities && feedObjectActivities.length > 0)) && (React.createElement(Collapse, { in: expandedActivities, timeout: "auto", classes: { root: classes.activitiesSection } },
460
460
  React.createElement(CardContent, { className: classes.activitiesContent },
461
461
  React.createElement(Activities, Object.assign({ feedObject: obj, key: selectedActivities, feedObjectActivities: feedObjectActivities, activitiesType: selectedActivities, onSetSelectedActivities: handleSelectedActivities, comments: comments, CommentsObjectProps: {