@elliemae/ds-chat-container 3.80.0-next.30 → 3.80.0-next.35

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.
Files changed (52) hide show
  1. package/dist/cjs/DSChatContainerDefinitions.js +7 -1
  2. package/dist/cjs/DSChatContainerDefinitions.js.map +2 -2
  3. package/dist/cjs/parts/chat-container-messages-list/ChatContainerMessagesList.js +134 -71
  4. package/dist/cjs/parts/chat-container-messages-list/ChatContainerMessagesList.js.map +2 -2
  5. package/dist/cjs/parts/chat-container-messages-list/styled.js +33 -43
  6. package/dist/cjs/parts/chat-container-messages-list/styled.js.map +2 -2
  7. package/dist/cjs/parts/chat-container-messages-list/useActionRef.js +3 -9
  8. package/dist/cjs/parts/chat-container-messages-list/useActionRef.js.map +2 -2
  9. package/dist/cjs/parts/chat-container-messages-list/useChatContainerMessageList.js +30 -22
  10. package/dist/cjs/parts/chat-container-messages-list/useChatContainerMessageList.js.map +2 -2
  11. package/dist/cjs/parts/chat-container-messages-list/useFocusAndScrollHelpers.js +38 -38
  12. package/dist/cjs/parts/chat-container-messages-list/useFocusAndScrollHelpers.js.map +2 -2
  13. package/dist/cjs/parts/chat-container-messages-list/useGlobalEventListener.js.map +1 -1
  14. package/dist/cjs/parts/chat-container-messages-list/useKeyboard.js +8 -15
  15. package/dist/cjs/parts/chat-container-messages-list/useKeyboard.js.map +2 -2
  16. package/dist/cjs/parts/chat-container-messages-list/useMessageFocusTracker.js +132 -0
  17. package/dist/cjs/parts/chat-container-messages-list/useMessageFocusTracker.js.map +7 -0
  18. package/dist/cjs/react-desc-prop-types.js +39 -23
  19. package/dist/cjs/react-desc-prop-types.js.map +2 -2
  20. package/dist/cjs/typescript-testing/typescript-chat-container-valid.js +31 -0
  21. package/dist/cjs/typescript-testing/typescript-chat-container-valid.js.map +2 -2
  22. package/dist/esm/DSChatContainerDefinitions.js +7 -1
  23. package/dist/esm/DSChatContainerDefinitions.js.map +2 -2
  24. package/dist/esm/parts/chat-container-messages-list/ChatContainerMessagesList.js +135 -74
  25. package/dist/esm/parts/chat-container-messages-list/ChatContainerMessagesList.js.map +2 -2
  26. package/dist/esm/parts/chat-container-messages-list/styled.js +33 -43
  27. package/dist/esm/parts/chat-container-messages-list/styled.js.map +2 -2
  28. package/dist/esm/parts/chat-container-messages-list/useActionRef.js +3 -9
  29. package/dist/esm/parts/chat-container-messages-list/useActionRef.js.map +2 -2
  30. package/dist/esm/parts/chat-container-messages-list/useChatContainerMessageList.js +30 -22
  31. package/dist/esm/parts/chat-container-messages-list/useChatContainerMessageList.js.map +2 -2
  32. package/dist/esm/parts/chat-container-messages-list/useFocusAndScrollHelpers.js +39 -39
  33. package/dist/esm/parts/chat-container-messages-list/useFocusAndScrollHelpers.js.map +2 -2
  34. package/dist/esm/parts/chat-container-messages-list/useGlobalEventListener.js.map +1 -1
  35. package/dist/esm/parts/chat-container-messages-list/useKeyboard.js +8 -15
  36. package/dist/esm/parts/chat-container-messages-list/useKeyboard.js.map +2 -2
  37. package/dist/esm/parts/chat-container-messages-list/useMessageFocusTracker.js +102 -0
  38. package/dist/esm/parts/chat-container-messages-list/useMessageFocusTracker.js.map +7 -0
  39. package/dist/esm/react-desc-prop-types.js +40 -24
  40. package/dist/esm/react-desc-prop-types.js.map +2 -2
  41. package/dist/esm/typescript-testing/typescript-chat-container-valid.js +31 -0
  42. package/dist/esm/typescript-testing/typescript-chat-container-valid.js.map +2 -2
  43. package/dist/types/DSChatContainerDefinitions.d.ts +8 -0
  44. package/dist/types/parts/chat-container-messages-list/styled.d.ts +18 -5
  45. package/dist/types/parts/chat-container-messages-list/useActionRef.d.ts +4 -6
  46. package/dist/types/parts/chat-container-messages-list/useChatContainerMessageList.d.ts +7 -2
  47. package/dist/types/parts/chat-container-messages-list/useFocusAndScrollHelpers.d.ts +9 -5
  48. package/dist/types/parts/chat-container-messages-list/useGlobalEventListener.d.ts +1 -1
  49. package/dist/types/parts/chat-container-messages-list/useKeyboard.d.ts +7 -3
  50. package/dist/types/parts/chat-container-messages-list/useMessageFocusTracker.d.ts +52 -0
  51. package/dist/types/react-desc-prop-types.d.ts +798 -793
  52. package/package.json +16 -16
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../../../scripts/build/transpile/react-shim.js", "../../../../src/parts/chat-container-messages-list/ChatContainerMessagesList.tsx"],
4
- "sourcesContent": ["import * as React from 'react';\nexport { React };\n", "/* eslint-disable complexity */\n/* eslint-disable jsx-a11y/no-noninteractive-tabindex */\n/* eslint-disable max-lines */\nimport type { CSSProperties, WeakValidationMap } from 'react';\nimport React from 'react';\nimport { describe } from '@elliemae/ds-props-helpers';\nimport { DSButtonV2, type DSButtonV2T } from '@elliemae/ds-button-v2';\nimport { ChatBubble, BUBBLE_TYPES } from '@elliemae/ds-chat-bubble';\nimport type { DSChatMessageDelimeterT } from '@elliemae/ds-chat-message-delimeter';\nimport { MessageDelimiter } from '@elliemae/ds-chat-message-delimeter';\nimport type { DSChatSystemMessageT } from '@elliemae/ds-chat-system-message';\nimport { SystemMessage } from '@elliemae/ds-chat-system-message';\nimport {\n StyledWrapperListItem,\n StyledChatContainerContentList,\n StyledChatContainerContentListScroll,\n StyledWrapper,\n StyledLoadMoreContainer,\n ListItem,\n StyledNewMessagesHiddenContainer,\n} from './styled.js';\nimport type { DSChatT } from '../../react-desc-prop-types.js';\nimport { ChatContainerMessagesListPropTypes } from '../../react-desc-prop-types.js';\nimport { useGetNewMessages } from './useGetNewMessages.js';\nimport { useChatContainerMessageList } from './useChatContainerMessageList.js';\nimport { ChatContainerDataTestIds } from '../../ChatContainerDataTestids.js';\n\ntype ButtonV2InnerRefT = DSButtonV2T.Props['innerRef'];\n\nconst ChatContainerMessagesList: React.ComponentType<DSChatT.MessagesListProps> = (props) => {\n const {\n propsWithDefault,\n globalAttributes,\n useVirtualHelpers,\n handleKey,\n handleListKey,\n loadMoreRef,\n virtualListRef,\n } = useChatContainerMessageList(props);\n\n const { sendingText, sendingMessages, messages, getMoreMessages, hasMoreItems, isLoading } = propsWithDefault;\n const totalSize = useVirtualHelpers.getTotalSize();\n const virtualItems = useVirtualHelpers.getVirtualItems();\n const { measureElement } = useVirtualHelpers;\n\n const newMessages = useGetNewMessages(messages);\n\n const render = React.useMemo(\n () =>\n virtualItems.map((virtualItem) => {\n const { index, start } = virtualItem;\n const row = messages[index];\n const isSending = sendingMessages && sendingMessages.find((m) => m === row.dsId);\n const style: CSSProperties = {\n position: 'absolute',\n top: 0,\n left: 0,\n transform: `translateY(${start}px)`,\n width: '100%',\n };\n\n return (\n <ListItem\n data-element=\"chat-element-list-item\"\n data-index={index}\n key={`ds-chat-bubble-${index}-${row.dsId}-${row.type}`}\n style={style}\n innerRef={measureElement}\n tabIndex={-1}\n onKeyDown={handleKey}\n role=\"listitem\"\n >\n <StyledWrapperListItem type={row.type}>\n {row.type === BUBBLE_TYPES.SENDER || row.type === BUBBLE_TYPES.RECIPIENT ? (\n <ChatBubble {...row} helpMessage={isSending ? sendingText : row.helpMessage} />\n ) : null}\n {row.type === BUBBLE_TYPES.DELIMITER ? (\n <MessageDelimiter {...(row as unknown as DSChatMessageDelimeterT.Props)} />\n ) : null}\n {row.type === BUBBLE_TYPES.SYSTEM ? <SystemMessage {...(row as DSChatSystemMessageT.Props)} /> : null}\n </StyledWrapperListItem>\n </ListItem>\n );\n }),\n [virtualItems, messages, sendingMessages, handleKey, sendingText, measureElement],\n );\n\n return (\n <StyledWrapper {...globalAttributes}>\n <StyledChatContainerContentList\n data-testid={ChatContainerDataTestIds.THREAD_CONTAINER}\n tabIndex={0}\n innerRef={virtualListRef as React.RefObject<HTMLDivElement>}\n role=\"list\"\n onKeyDown={handleListKey}\n >\n <StyledChatContainerContentListScroll height={`${totalSize}px`}>\n {!isLoading && hasMoreItems ? (\n <StyledLoadMoreContainer hasMoreItems={hasMoreItems} role=\"listitem\">\n <DSButtonV2\n buttonType=\"outline\"\n onClick={getMoreMessages}\n data-testid={ChatContainerDataTestIds.LOAD_MORE_BUTTON}\n innerRef={loadMoreRef as ButtonV2InnerRefT}\n >\n Load More\n </DSButtonV2>\n </StyledLoadMoreContainer>\n ) : null}\n {render}\n </StyledChatContainerContentListScroll>\n </StyledChatContainerContentList>\n {/* We repeat the render with the new available messages */}\n <StyledNewMessagesHiddenContainer aria-live=\"polite\" aria-hidden=\"false\">\n {newMessages.map((newMessage) => (\n <p key={newMessage.dsId}>\n {newMessage.title} {newMessage.time} {newMessage.body} {newMessage.errorMessage} {newMessage.helpMessage}\n </p>\n ))}\n </StyledNewMessagesHiddenContainer>\n </StyledWrapper>\n );\n};\n\nChatContainerMessagesList.displayName = 'ChatContainerMessagesList';\nconst ChatContainerMessagesListWithSchema = describe(ChatContainerMessagesList);\nChatContainerMessagesListWithSchema.propTypes = ChatContainerMessagesListPropTypes as WeakValidationMap<unknown>;\n\nexport { ChatContainerMessagesList, ChatContainerMessagesListWithSchema };\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import * as React from 'react';\nexport { React };\n", "/* eslint-disable complexity */\n/* eslint-disable jsx-a11y/no-noninteractive-tabindex */\n/* eslint-disable max-lines */\nimport type { WeakValidationMap } from 'react';\nimport React from 'react';\nimport { describe } from '@elliemae/ds-props-helpers';\nimport { DSButtonV2, type DSButtonV2T } from '@elliemae/ds-button-v2';\nimport { ChatBubble, BUBBLE_TYPES } from '@elliemae/ds-chat-bubble';\nimport type { DSChatMessageDelimeterT } from '@elliemae/ds-chat-message-delimeter';\nimport { MessageDelimiter } from '@elliemae/ds-chat-message-delimeter';\nimport type { DSChatSystemMessageT } from '@elliemae/ds-chat-system-message';\nimport { SystemMessage } from '@elliemae/ds-chat-system-message';\nimport {\n StyledWrapperListItem,\n StyledFastList,\n StyledWrapper,\n StyledLoadMoreContainer,\n StyledNewMessagesHiddenContainer,\n} from './styled.js';\nimport type { DSChatT } from '../../react-desc-prop-types.js';\nimport { ChatContainerMessagesListPropTypes } from '../../react-desc-prop-types.js';\nimport { useGetNewMessages } from './useGetNewMessages.js';\nimport { useChatContainerMessageList } from './useChatContainerMessageList.js';\nimport { getMessageFocusId, getMessagePositionId } from './useMessageFocusTracker.js';\nimport type { MessageFocusRequest, useMessageFocusTracker } from './useMessageFocusTracker.js';\nimport { ChatContainerDataTestIds } from '../../ChatContainerDataTestids.js';\n\ntype ButtonV2InnerRefT = DSButtonV2T.Props['innerRef'];\n\ntype FocusTracker = ReturnType<typeof useMessageFocusTracker>;\ntype ItemFocusCallbacks = Pick<FocusTracker, 'mayTakeFocus' | 'markFulfilled'>;\n\ntype ItemExtraProps = {\n messages: DSChatT.MessagesListProps['messages'];\n sendingMessages: DSChatT.MessagesListProps['sendingMessages'];\n sendingText: DSChatT.MessagesListProps['sendingText'];\n focusRequest: MessageFocusRequest | null;\n focusCallbacks: ItemFocusCallbacks;\n};\n\nconst ESTIMATED_MESSAGE_HEIGHT = 80;\nconst estimateSize = () => ESTIMATED_MESSAGE_HEIGHT;\n// the wrapper `ds-fast-list` renders is neutral: the scroll container owns the `list` role\nconst WRAPPER_LIST_PROPS = { role: 'none' };\n\n/**\n * Module-level on purpose: `DSFastList` renders it as a component, so its identity must be stable or\n * every item would remount on each render of the list.\n */\nconst ItemRenderer: React.ComponentType<{ index: number; extraItemProps?: object }> = ({ index, extraItemProps }) => {\n // `styled(DSFastList)` drops DSFastList's generic, so `extraItemProps` arrives typed as `object`\n const { messages, sendingMessages, sendingText, focusRequest, focusCallbacks } = (extraItemProps ??\n {}) as Partial<ItemExtraProps>;\n const row = messages?.[index];\n const isFocusTarget = Boolean(\n row &&\n focusRequest &&\n (focusRequest.messageId === getMessageFocusId(row, index) ||\n focusRequest.messageId === getMessagePositionId(index)),\n );\n const requestId = isFocusTarget && focusRequest ? focusRequest.requestId : 0;\n const preventScroll = isFocusTarget && focusRequest ? focusRequest.preventScroll : false;\n /**\n * The only place a message is focused. It runs once React has committed the message, so a target that is\n * not mounted yet (far away, or remounted after older messages were loaded) is focused when it mounts. The\n * focusable element is the item wrapper `ds-fast-list` renders around this component, the one carrying\n * `data-index` and QA's `data-element`, so it is reached with `closest` from this message's own node.\n */\n const focusRef = React.useCallback(\n (node: HTMLDivElement | null) => {\n if (!node || !isFocusTarget || !focusCallbacks) return;\n const item = node.closest<HTMLElement>('[data-index]');\n const takeFocus = focusCallbacks.mayTakeFocus(requestId, item);\n if (!takeFocus) return;\n item?.focus({ preventScroll: takeFocus === 'restore' || preventScroll });\n focusCallbacks.markFulfilled(requestId);\n },\n [isFocusTarget, requestId, preventScroll, focusCallbacks],\n );\n if (!row) return null;\n const isSending = sendingMessages && sendingMessages.find((m) => m === row.dsId);\n return (\n <StyledWrapperListItem type={row.type} innerRef={focusRef}>\n {row.type === BUBBLE_TYPES.SENDER || row.type === BUBBLE_TYPES.RECIPIENT ? (\n <ChatBubble {...row} helpMessage={isSending ? sendingText : row.helpMessage} />\n ) : null}\n {row.type === BUBBLE_TYPES.DELIMITER ? (\n <MessageDelimiter {...(row as unknown as DSChatMessageDelimeterT.Props)} />\n ) : null}\n {row.type === BUBBLE_TYPES.SYSTEM ? <SystemMessage {...(row as DSChatSystemMessageT.Props)} /> : null}\n </StyledWrapperListItem>\n );\n};\n\nconst ChatContainerMessagesList: React.ComponentType<DSChatT.MessagesListProps> = (props) => {\n const {\n propsWithDefault,\n globalAttributes,\n fastListActionRef,\n paddingStart,\n paddingEnd,\n handleKey,\n handleListKey,\n loadMoreRef,\n virtualListRef,\n focusTracker,\n } = useChatContainerMessageList(props);\n\n const { sendingText, sendingMessages, messages, getMoreMessages, hasMoreItems, isLoading } = propsWithDefault;\n\n const newMessages = useGetNewMessages(messages);\n\n const getOwnerProps = React.useCallback(() => propsWithDefault, [propsWithDefault]);\n /**\n * The key must carry the index. `ds-fast-list` reuses an item's DOM node for as long as its key is the same,\n * but the virtualizer measures each node by its `data-index` and does not measure a node again when only its\n * index changes. With a key made only of `dsId`, loading older messages (which shifts every index) keeps the\n * nodes and leaves their measurements pointing at another position: items overlap or leave gaps, and the list\n * flickers while scrolling. An index-bearing key recreates the shifted items so they are measured at their\n * new index, as the list did before it was moved onto `ds-fast-list`.\n */\n const getId = React.useCallback(\n (index: number) => `${index}-${messages[index].dsId}-${messages[index].type}`,\n [messages],\n );\n const {\n focusRequest,\n mayTakeFocus,\n markFulfilled,\n trackFocusedMessage,\n trackFocusResetter,\n handleFocusOut,\n listRootRef,\n } = focusTracker;\n const focusCallbacks = React.useMemo(() => ({ mayTakeFocus, markFulfilled }), [mayTakeFocus, markFulfilled]);\n const extraItemProps = React.useMemo(\n () => ({ messages, sendingMessages, sendingText, focusRequest, focusCallbacks }),\n [messages, sendingMessages, sendingText, focusRequest, focusCallbacks],\n );\n const { onFocus: onFocusFromConsumer, onBlur: onBlurFromConsumer } = globalAttributes as {\n onFocus?: React.FocusEventHandler<HTMLElement>;\n onBlur?: React.FocusEventHandler<HTMLElement>;\n };\n /**\n * Keeps the focus intent equal to the message that actually got the focus (keyboard, mouse or API), also when\n * the focus is on a descendant of the message, and clears it when the focus moves to anything else in the list.\n */\n const handleFocusIn = React.useCallback(\n (e: React.FocusEvent<HTMLElement>) => {\n const item = (e.target as HTMLElement).closest<HTMLElement>('[data-element=\"chat-element-list-item\"]');\n const index = item?.dataset.index;\n const position = Number(index);\n const row = index !== undefined ? messages[position] : undefined;\n if (row) trackFocusedMessage(getMessageFocusId(row, position));\n else trackFocusResetter();\n onFocusFromConsumer?.(e);\n },\n [messages, trackFocusedMessage, trackFocusResetter, onFocusFromConsumer],\n );\n const handleFocusOutWithConsumer = React.useCallback(\n (e: React.FocusEvent<HTMLElement>) => {\n handleFocusOut(e);\n onBlurFromConsumer?.(e);\n },\n [handleFocusOut, onBlurFromConsumer],\n );\n const itemProps = React.useMemo(\n () => ({ 'data-element': 'chat-element-list-item', tabIndex: -1, onKeyDown: handleKey }),\n [handleKey],\n );\n const extraContent = React.useMemo(\n () =>\n !isLoading && hasMoreItems ? (\n <StyledLoadMoreContainer hasMoreItems={hasMoreItems} role=\"listitem\">\n <DSButtonV2\n buttonType=\"outline\"\n onClick={getMoreMessages}\n data-testid={ChatContainerDataTestIds.LOAD_MORE_BUTTON}\n innerRef={loadMoreRef as ButtonV2InnerRefT}\n >\n Load More\n </DSButtonV2>\n </StyledLoadMoreContainer>\n ) : null,\n [isLoading, hasMoreItems, getMoreMessages, loadMoreRef],\n );\n\n return (\n <StyledWrapper\n {...globalAttributes}\n innerRef={listRootRef}\n onFocus={handleFocusIn}\n onBlur={handleFocusOutWithConsumer}\n >\n <StyledFastList\n getOwnerProps={getOwnerProps}\n data-testid={ChatContainerDataTestIds.THREAD_CONTAINER}\n role=\"list\"\n tabIndex={0}\n onKeyDown={handleListKey}\n innerRef={virtualListRef}\n actionRef={fastListActionRef}\n remountOnCountChange={false}\n count={messages.length}\n getId={getId}\n estimateSize={estimateSize}\n paddingStart={paddingStart}\n paddingEnd={paddingEnd}\n ItemRenderer={ItemRenderer}\n extraItemProps={extraItemProps}\n extraContent={extraContent}\n dsFastlistWrapperList={WRAPPER_LIST_PROPS}\n dsFastlistItem={itemProps}\n />\n {/* We repeat the render with the new available messages */}\n <StyledNewMessagesHiddenContainer aria-live=\"polite\" aria-hidden=\"false\">\n {newMessages.map((newMessage) => (\n <p key={newMessage.dsId}>\n {newMessage.title} {newMessage.time} {newMessage.body} {newMessage.errorMessage} {newMessage.helpMessage}\n </p>\n ))}\n </StyledNewMessagesHiddenContainer>\n </StyledWrapper>\n );\n};\n\nChatContainerMessagesList.displayName = 'ChatContainerMessagesList';\nconst ChatContainerMessagesListWithSchema = describe(ChatContainerMessagesList);\nChatContainerMessagesListWithSchema.propTypes = ChatContainerMessagesListPropTypes as WeakValidationMap<unknown>;\n\nexport { ChatContainerMessagesList, ChatContainerMessagesListWithSchema };\n"],
5
+ "mappings": "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",
6
6
  "names": ["React"]
7
7
  }
@@ -1,6 +1,8 @@
1
1
  import * as React from "react";
2
2
  import { styled } from "@elliemae/ds-system";
3
+ import { DSFastList } from "@elliemae/ds-fast-list";
3
4
  import { BUBBLE_TYPES } from "@elliemae/ds-chat-bubble";
5
+ import { DSChatContainerSlotName, DSChatContainerSlots } from "../../DSChatContainerDefinitions.js";
4
6
  const StyledWrapper = styled.div`
5
7
  width: 100%;
6
8
  height: 100%;
@@ -15,21 +17,36 @@ const StyledWrapperListItem = styled.div`
15
17
  ${(props) => props.type === BUBBLE_TYPES.SYSTEM && `padding: ${props.theme.space.xxs} 0px
16
18
  `};
17
19
  `;
18
- const StyledChatContainerContentList = styled.div`
19
- width: 100%;
20
- height: 100%;
21
- /**
22
- * No padding-top here on purpose: this element is also react-virtual's getScrollElement, and
23
- * virtual-core compares raw el.scrollTop against its own content-relative item positions with no
24
- * padding awareness — a CSS top-padding on the scroll element itself desyncs those two frames,
25
- * making calculateRange() (and therefore getMessagesInView()) misjudge the visible range near the
26
- * top boundary by roughly the padding's own size. The equivalent top space is provided instead via
27
- * the virtualizer's own paddingStart option (useChatContainerMessageList.ts), which is baked into
28
- * its measurements and stays in the same frame as scrollTop.
29
- */
30
- padding: 0 ${(props) => props.theme.space.xs} 0 ${(props) => props.theme.space.xs};
31
- overflow-y: auto;
32
- overflow-x: hidden;
20
+ const StyledFastList = styled(DSFastList, {
21
+ name: DSChatContainerSlotName,
22
+ slot: DSChatContainerSlots.MESSAGES_LIST_SCROLL_AREA
23
+ })`
24
+ && {
25
+ padding: 0 ${(props) => props.theme.space.xs} 0 ${(props) => props.theme.space.xs};
26
+ overflow-y: auto;
27
+ overflow-x: hidden;
28
+ }
29
+
30
+ [data-element='chat-element-list-item'] {
31
+ &:focus,
32
+ &:focus-visible {
33
+ outline: none;
34
+ &:after {
35
+ content: '';
36
+ display: block;
37
+ position: absolute;
38
+ top: 0;
39
+ left: -${(props) => props.theme.space.xxs};
40
+ pointer-events: none;
41
+ border-radius: 2px;
42
+ background-color: #ebedf0;
43
+ width: calc(100% + 24px);
44
+ border: 2px solid #1e79c2;
45
+ height: 100%;
46
+ z-index: -1;
47
+ }
48
+ }
49
+ }
33
50
  `;
34
51
  const StyledNewMessagesHiddenContainer = styled.div`
35
52
  position: absolute;
@@ -43,11 +60,6 @@ const StyledNewMessagesHiddenContainer = styled.div`
43
60
  clip-path: inset(100%);
44
61
  white-space: nowrap;
45
62
  `;
46
- const StyledChatContainerContentListScroll = styled.div`
47
- width: 100%;
48
- height: ${(props) => props.height};
49
- position: relative;
50
- `;
51
63
  const StyledLoadMoreContainer = styled.div`
52
64
  position: absolute;
53
65
  width: calc(100% + ${(props) => props.theme.space.xxs});
@@ -60,30 +72,8 @@ const StyledLoadMoreContainer = styled.div`
60
72
  ${(props) => props.hasMoreItems === "top" ? "top: 0;" : ""}
61
73
  ${(props) => props.hasMoreItems === "bottom" ? "bottom: 0;" : ""}
62
74
  `;
63
- const ListItem = styled.div`
64
- &:focus,
65
- &:focus-visible {
66
- outline: none;
67
- &:after {
68
- content: '';
69
- display: block;
70
- position: absolute;
71
- top: 0;
72
- left: -${(props) => props.theme.space.xxs};
73
- pointer-events: none;
74
- border-radius: 2px;
75
- background-color: #ebedf0;
76
- width: calc(100% + 24px);
77
- border: 2px solid #1e79c2;
78
- height: 100%;
79
- z-index: -1;
80
- }
81
- }
82
- `;
83
75
  export {
84
- ListItem,
85
- StyledChatContainerContentList,
86
- StyledChatContainerContentListScroll,
76
+ StyledFastList,
87
77
  StyledLoadMoreContainer,
88
78
  StyledNewMessagesHiddenContainer,
89
79
  StyledWrapper,
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../../../scripts/build/transpile/react-shim.js", "../../../../src/parts/chat-container-messages-list/styled.ts"],
4
- "sourcesContent": ["import * as React from 'react';\nexport { React };\n", "import { styled } from '@elliemae/ds-system';\nimport type { DSChatBubbleT } from '@elliemae/ds-chat-bubble';\nimport { BUBBLE_TYPES } from '@elliemae/ds-chat-bubble';\n\nexport const StyledWrapper = styled.div`\n width: 100%;\n height: 100%;\n`;\n\n// list\nexport const StyledWrapperListItem = styled.div<Partial<DSChatBubbleT.Props>>`\n min-width: 100%;\n ${(props) =>\n props.type === BUBBLE_TYPES.SENDER &&\n `padding: ${props.theme.space.xxs} ${props.theme.space.xxs} ${props.theme.space.xxs} 13%\n `};\n ${(props) =>\n props.type === BUBBLE_TYPES.RECIPIENT &&\n `padding: ${props.theme.space.xxs} 13% ${props.theme.space.xxs}\n ${props.theme.space.xxs}\n `};\n ${(props) =>\n props.type === BUBBLE_TYPES.SYSTEM &&\n `padding: ${props.theme.space.xxs} 0px\n `};\n`;\n\nexport const StyledChatContainerContentList = styled.div`\n width: 100%;\n height: 100%;\n /**\n * No padding-top here on purpose: this element is also react-virtual's getScrollElement, and\n * virtual-core compares raw el.scrollTop against its own content-relative item positions with no\n * padding awareness \u2014 a CSS top-padding on the scroll element itself desyncs those two frames,\n * making calculateRange() (and therefore getMessagesInView()) misjudge the visible range near the\n * top boundary by roughly the padding's own size. The equivalent top space is provided instead via\n * the virtualizer's own paddingStart option (useChatContainerMessageList.ts), which is baked into\n * its measurements and stays in the same frame as scrollTop.\n */\n padding: 0 ${(props) => props.theme.space.xs} 0 ${(props) => props.theme.space.xs};\n overflow-y: auto;\n overflow-x: hidden;\n`;\n\nexport const StyledNewMessagesHiddenContainer = styled.div`\n position: absolute;\n width: 1px;\n height: 1px;\n margin: -1px;\n border: 0;\n padding: 0;\n overflow: hidden;\n clip: rect(0 0 0 0);\n clip-path: inset(100%);\n white-space: nowrap;\n`;\n\nexport const StyledChatContainerContentListScroll = styled.div<{ height: string }>`\n width: 100%;\n height: ${(props) => props.height};\n position: relative;\n`;\n\nexport const StyledLoadMoreContainer = styled.div<{ hasMoreItems?: 'top' | 'bottom' }>`\n position: absolute;\n width: calc(100% + ${(props) => props.theme.space.xxs});\n padding: ${(props) => props.theme.space.xxs};\n left: -${(props) => props.theme.space.xxs};\n right: -${(props) => props.theme.space.xxs};\n display: grid;\n justify-content: center;\n align-items: center;\n ${(props) => (props.hasMoreItems === 'top' ? 'top: 0;' : '')}\n ${(props) => (props.hasMoreItems === 'bottom' ? 'bottom: 0;' : '')}\n`;\nexport const ListItem = styled.div`\n &:focus,\n &:focus-visible {\n outline: none;\n &:after {\n content: '';\n display: block;\n position: absolute;\n top: 0;\n left: -${(props) => props.theme.space.xxs};\n pointer-events: none;\n border-radius: 2px;\n background-color: #ebedf0;\n width: calc(100% + 24px);\n border: 2px solid #1e79c2;\n height: 100%;\n z-index: -1;\n }\n }\n`;\n"],
5
- "mappings": "AAAA,YAAY,WAAW;ACAvB,SAAS,cAAc;AAEvB,SAAS,oBAAoB;AAEtB,MAAM,gBAAgB,OAAO;AAAA;AAAA;AAAA;AAM7B,MAAM,wBAAwB,OAAO;AAAA;AAAA,IAExC,CAAC,UACD,MAAM,SAAS,aAAa,UAC5B,YAAY,MAAM,MAAM,MAAM,GAAG,IAAI,MAAM,MAAM,MAAM,GAAG,IAAI,MAAM,MAAM,MAAM,GAAG;AAAA,GACpF;AAAA,IACC,CAAC,UACD,MAAM,SAAS,aAAa,aAC5B,YAAY,MAAM,MAAM,MAAM,GAAG,QAAQ,MAAM,MAAM,MAAM,GAAG;AAAA,MAC5D,MAAM,MAAM,MAAM,GAAG;AAAA,GACxB;AAAA,IACC,CAAC,UACD,MAAM,SAAS,aAAa,UAC5B,YAAY,MAAM,MAAM,MAAM,GAAG;AAAA,GAClC;AAAA;AAGI,MAAM,iCAAiC,OAAO;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,eAYtC,CAAC,UAAU,MAAM,MAAM,MAAM,EAAE,MAAM,CAAC,UAAU,MAAM,MAAM,MAAM,EAAE;AAAA;AAAA;AAAA;AAK5E,MAAM,mCAAmC,OAAO;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAahD,MAAM,uCAAuC,OAAO;AAAA;AAAA,YAE/C,CAAC,UAAU,MAAM,MAAM;AAAA;AAAA;AAI5B,MAAM,0BAA0B,OAAO;AAAA;AAAA,uBAEvB,CAAC,UAAU,MAAM,MAAM,MAAM,GAAG;AAAA,aAC1C,CAAC,UAAU,MAAM,MAAM,MAAM,GAAG;AAAA,WAClC,CAAC,UAAU,MAAM,MAAM,MAAM,GAAG;AAAA,YAC/B,CAAC,UAAU,MAAM,MAAM,MAAM,GAAG;AAAA;AAAA;AAAA;AAAA,IAIxC,CAAC,UAAW,MAAM,iBAAiB,QAAQ,YAAY,EAAG;AAAA,IAC1D,CAAC,UAAW,MAAM,iBAAiB,WAAW,eAAe,EAAG;AAAA;AAE7D,MAAM,WAAW,OAAO;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,eAShB,CAAC,UAAU,MAAM,MAAM,MAAM,GAAG;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;",
4
+ "sourcesContent": ["import * as React from 'react';\nexport { React };\n", "import { styled } from '@elliemae/ds-system';\nimport { DSFastList } from '@elliemae/ds-fast-list';\nimport type { DSChatBubbleT } from '@elliemae/ds-chat-bubble';\nimport { BUBBLE_TYPES } from '@elliemae/ds-chat-bubble';\nimport { DSChatContainerSlotName, DSChatContainerSlots } from '../../DSChatContainerDefinitions.js';\n\nexport const StyledWrapper = styled.div`\n width: 100%;\n height: 100%;\n`;\n\n// list\nexport const StyledWrapperListItem = styled.div<Partial<DSChatBubbleT.Props>>`\n min-width: 100%;\n ${(props) =>\n props.type === BUBBLE_TYPES.SENDER &&\n `padding: ${props.theme.space.xxs} ${props.theme.space.xxs} ${props.theme.space.xxs} 13%\n `};\n ${(props) =>\n props.type === BUBBLE_TYPES.RECIPIENT &&\n `padding: ${props.theme.space.xxs} 13% ${props.theme.space.xxs}\n ${props.theme.space.xxs}\n `};\n ${(props) =>\n props.type === BUBBLE_TYPES.SYSTEM &&\n `padding: ${props.theme.space.xxs} 0px\n `};\n`;\n\n/**\n * The scroll container is DSFastList's own container, so its horizontal padding and overflow rules are\n * applied through the class that `styled(DSFastList)` forwards to it. `&&` doubles the class so these\n * rules win over the container's own `overflow: auto` regardless of style injection order.\n *\n * No padding-top here on purpose: the scroll container is also the virtualizer's scroll element, and\n * the virtualizer compares the raw `scrollTop` with its own content-relative item positions with no\n * awareness of CSS padding, so a CSS top-padding would desync `getVisibleItems()` near the top boundary\n * by roughly the padding's own size. The equivalent top space is passed as the virtualizer's\n * `paddingStart` instead (see useChatContainerMessageList.ts).\n */\nexport const StyledFastList = styled(DSFastList, {\n name: DSChatContainerSlotName,\n slot: DSChatContainerSlots.MESSAGES_LIST_SCROLL_AREA,\n})`\n && {\n padding: 0 ${(props) => props.theme.space.xs} 0 ${(props) => props.theme.space.xs};\n overflow-y: auto;\n overflow-x: hidden;\n }\n\n [data-element='chat-element-list-item'] {\n &:focus,\n &:focus-visible {\n outline: none;\n &:after {\n content: '';\n display: block;\n position: absolute;\n top: 0;\n left: -${(props) => props.theme.space.xxs};\n pointer-events: none;\n border-radius: 2px;\n background-color: #ebedf0;\n width: calc(100% + 24px);\n border: 2px solid #1e79c2;\n height: 100%;\n z-index: -1;\n }\n }\n }\n`;\n\nexport const StyledNewMessagesHiddenContainer = styled.div`\n position: absolute;\n width: 1px;\n height: 1px;\n margin: -1px;\n border: 0;\n padding: 0;\n overflow: hidden;\n clip: rect(0 0 0 0);\n clip-path: inset(100%);\n white-space: nowrap;\n`;\n\nexport const StyledLoadMoreContainer = styled.div<{ hasMoreItems?: 'top' | 'bottom' }>`\n position: absolute;\n width: calc(100% + ${(props) => props.theme.space.xxs});\n padding: ${(props) => props.theme.space.xxs};\n left: -${(props) => props.theme.space.xxs};\n right: -${(props) => props.theme.space.xxs};\n display: grid;\n justify-content: center;\n align-items: center;\n ${(props) => (props.hasMoreItems === 'top' ? 'top: 0;' : '')}\n ${(props) => (props.hasMoreItems === 'bottom' ? 'bottom: 0;' : '')}\n`;\n"],
5
+ "mappings": "AAAA,YAAY,WAAW;ACAvB,SAAS,cAAc;AACvB,SAAS,kBAAkB;AAE3B,SAAS,oBAAoB;AAC7B,SAAS,yBAAyB,4BAA4B;AAEvD,MAAM,gBAAgB,OAAO;AAAA;AAAA;AAAA;AAM7B,MAAM,wBAAwB,OAAO;AAAA;AAAA,IAExC,CAAC,UACD,MAAM,SAAS,aAAa,UAC5B,YAAY,MAAM,MAAM,MAAM,GAAG,IAAI,MAAM,MAAM,MAAM,GAAG,IAAI,MAAM,MAAM,MAAM,GAAG;AAAA,GACpF;AAAA,IACC,CAAC,UACD,MAAM,SAAS,aAAa,aAC5B,YAAY,MAAM,MAAM,MAAM,GAAG,QAAQ,MAAM,MAAM,MAAM,GAAG;AAAA,MAC5D,MAAM,MAAM,MAAM,GAAG;AAAA,GACxB;AAAA,IACC,CAAC,UACD,MAAM,SAAS,aAAa,UAC5B,YAAY,MAAM,MAAM,MAAM,GAAG;AAAA,GAClC;AAAA;AAcI,MAAM,iBAAiB,OAAO,YAAY;AAAA,EAC/C,MAAM;AAAA,EACN,MAAM,qBAAqB;AAC7B,CAAC;AAAA;AAAA,iBAEgB,CAAC,UAAU,MAAM,MAAM,MAAM,EAAE,MAAM,CAAC,UAAU,MAAM,MAAM,MAAM,EAAE;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,iBAcpE,CAAC,UAAU,MAAM,MAAM,MAAM,GAAG;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAa1C,MAAM,mCAAmC,OAAO;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAahD,MAAM,0BAA0B,OAAO;AAAA;AAAA,uBAEvB,CAAC,UAAU,MAAM,MAAM,MAAM,GAAG;AAAA,aAC1C,CAAC,UAAU,MAAM,MAAM,MAAM,GAAG;AAAA,WAClC,CAAC,UAAU,MAAM,MAAM,MAAM,GAAG;AAAA,YAC/B,CAAC,UAAU,MAAM,MAAM,MAAM,GAAG;AAAA;AAAA;AAAA;AAAA,IAIxC,CAAC,UAAW,MAAM,iBAAiB,QAAQ,YAAY,EAAG;AAAA,IAC1D,CAAC,UAAW,MAAM,iBAAiB,WAAW,eAAe,EAAG;AAAA;",
6
6
  "names": []
7
7
  }
@@ -2,13 +2,11 @@ import * as React from "react";
2
2
  import React2 from "react";
3
3
  const useActionRef = ({
4
4
  actionRef,
5
- useVirtualHelpers: { scrollToIndex: vScroll },
5
+ vScroll,
6
6
  virtualListRef,
7
- updateCache,
8
7
  getMessagesInView,
9
8
  onlyFocus,
10
- scrollAndFocus,
11
- hasVirtualItems
9
+ scrollAndFocus
12
10
  }) => {
13
11
  React2.useEffect(() => {
14
12
  if (actionRef && actionRef.current) {
@@ -17,7 +15,6 @@ const useActionRef = ({
17
15
  vScroll(index, opts);
18
16
  requestAnimationFrame(() => {
19
17
  vScroll(index, opts);
20
- updateCache();
21
18
  });
22
19
  }, 0);
23
20
  };
@@ -33,10 +30,7 @@ const useActionRef = ({
33
30
  actionRef.current.focusToIndexWithoutScroll = onlyFocus;
34
31
  actionRef.current.focusToList = focusToList;
35
32
  }
36
- }, [actionRef, getMessagesInView, onlyFocus, scrollAndFocus, updateCache, vScroll, virtualListRef]);
37
- React2.useEffect(() => {
38
- if (hasVirtualItems) updateCache();
39
- }, [hasVirtualItems, updateCache]);
33
+ }, [actionRef, getMessagesInView, onlyFocus, scrollAndFocus, vScroll, virtualListRef]);
40
34
  };
41
35
  export {
42
36
  useActionRef
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../../../scripts/build/transpile/react-shim.js", "../../../../src/parts/chat-container-messages-list/useActionRef.ts"],
4
- "sourcesContent": ["import * as React from 'react';\nexport { React };\n", "/* eslint-disable complexity */\n/* eslint-disable jsx-a11y/no-noninteractive-tabindex */\n/* eslint-disable max-lines */\nimport React from 'react';\nimport type { DSChatT } from '../../react-desc-prop-types.js';\n\ntype UseActionRefArgs = {\n actionRef: DSChatT.ActionRef;\n useVirtualHelpers: DSChatT.ChatVirtualizer;\n virtualListRef: React.MutableRefObject<HTMLDivElement | undefined>;\n updateCache: () => void;\n getMessagesInView: () => string[];\n onlyFocus: (index: string) => void;\n scrollAndFocus: (index: number, opts?: DSChatT.ScrollToIndexOptions) => void;\n hasVirtualItems: boolean;\n};\n\nexport const useActionRef = ({\n actionRef,\n useVirtualHelpers: { scrollToIndex: vScroll },\n virtualListRef,\n updateCache,\n getMessagesInView,\n onlyFocus,\n scrollAndFocus,\n hasVirtualItems,\n}: UseActionRefArgs) => {\n React.useEffect(() => {\n if (actionRef && actionRef.current) {\n const scrollToIndex: DSChatT.ActionRef['current']['scrollToIndex'] = (index, opts = { align: 'center' }) => {\n setTimeout(() => {\n vScroll(index, opts);\n /* [PUI-15629](https://jira.elliemae.io/browse/PUI-15629)\n * requestAnimationFrame is needed because the first scroll might not calculate positions\n * correctly if the DOM hasn't been fully updated yet. By waiting for the next animation\n * frame, we ensure that:\n * 1. The DOM has been updated with new positions after the first scroll\n * 2. Element height and position measurements are accurate\n * 3. The second scroll can center the element with correct measurements\n *\n * TODO: It might be worth consider upgrading to react-virtual v3(@tanstack/react-virtual), which handles DOM measurements and scroll\n * synchronization better. This RAF workaround might not be necessary with v3.\n */\n requestAnimationFrame(() => {\n vScroll(index, opts);\n updateCache();\n });\n }, 0);\n };\n const focusToIndex: DSChatT.ActionRef['current']['focusToIndex'] = (index, opts = { align: 'center' }) => {\n scrollAndFocus(index, opts);\n };\n const focusToList = () => {\n virtualListRef.current?.focus();\n };\n\n actionRef.current.scrollToIndex = scrollToIndex;\n actionRef.current.getMessagesInView = getMessagesInView;\n actionRef.current.focusToIndex = focusToIndex;\n actionRef.current.focusToIndexWithoutScroll = onlyFocus;\n actionRef.current.focusToList = focusToList;\n }\n }, [actionRef, getMessagesInView, onlyFocus, scrollAndFocus, updateCache, vScroll, virtualListRef]);\n\n /**\n * Populates the visible-items cache once the virtualizer has actually rendered rows. Unlike v2 \u2014\n * whose calculateRange never gated on the scroll container's measured size, so it always rendered\n * overscan+1 rows synchronously on the first commit \u2014 v3 gates rendering on a real, measured\n * container size, so `getVirtualItems()` is empty for the first render(s) until that measurement\n * resolves (confirmed empirically: 2 empty renders observed before the first non-empty one).\n * Calling updateCache() unconditionally on mount (the v2-era approach) raced this and cached an\n * empty list, which Shift+Tab-from-composer then read once and never revisited.\n *\n * Residual, narrower risk: interacting with the thread in the brief window before `hasVirtualItems`\n * flips true (the first render, before real layout exists) still finds an empty cache \u2014 this is a\n * known, accepted limitation of `onlyFocus`'s querySelector-based focus lookup, not something this\n * migration introduces new architecture to close. See KNOWN_INTENTIONAL_DEVIATIONS.md.\n */\n React.useEffect(() => {\n if (hasVirtualItems) updateCache();\n }, [hasVirtualItems, updateCache]);\n};\n"],
5
- "mappings": "AAAA,YAAY,WAAW;ACGvB,OAAOA,YAAW;AAcX,MAAM,eAAe,CAAC;AAAA,EAC3B;AAAA,EACA,mBAAmB,EAAE,eAAe,QAAQ;AAAA,EAC5C;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,MAAwB;AACtB,EAAAA,OAAM,UAAU,MAAM;AACpB,QAAI,aAAa,UAAU,SAAS;AAClC,YAAM,gBAA+D,CAAC,OAAO,OAAO,EAAE,OAAO,SAAS,MAAM;AAC1G,mBAAW,MAAM;AACf,kBAAQ,OAAO,IAAI;AAYnB,gCAAsB,MAAM;AAC1B,oBAAQ,OAAO,IAAI;AACnB,wBAAY;AAAA,UACd,CAAC;AAAA,QACH,GAAG,CAAC;AAAA,MACN;AACA,YAAM,eAA6D,CAAC,OAAO,OAAO,EAAE,OAAO,SAAS,MAAM;AACxG,uBAAe,OAAO,IAAI;AAAA,MAC5B;AACA,YAAM,cAAc,MAAM;AACxB,uBAAe,SAAS,MAAM;AAAA,MAChC;AAEA,gBAAU,QAAQ,gBAAgB;AAClC,gBAAU,QAAQ,oBAAoB;AACtC,gBAAU,QAAQ,eAAe;AACjC,gBAAU,QAAQ,4BAA4B;AAC9C,gBAAU,QAAQ,cAAc;AAAA,IAClC;AAAA,EACF,GAAG,CAAC,WAAW,mBAAmB,WAAW,gBAAgB,aAAa,SAAS,cAAc,CAAC;AAgBlG,EAAAA,OAAM,UAAU,MAAM;AACpB,QAAI,gBAAiB,aAAY;AAAA,EACnC,GAAG,CAAC,iBAAiB,WAAW,CAAC;AACnC;",
4
+ "sourcesContent": ["import * as React from 'react';\nexport { React };\n", "/* eslint-disable complexity */\n/* eslint-disable jsx-a11y/no-noninteractive-tabindex */\n/* eslint-disable max-lines */\nimport React from 'react';\nimport type { DSChatT } from '../../react-desc-prop-types.js';\n\ntype UseActionRefArgs = {\n actionRef: DSChatT.ActionRef;\n vScroll: (index: number, opts?: DSChatT.ScrollActionOptions) => void;\n virtualListRef: React.MutableRefObject<HTMLDivElement | null>;\n getMessagesInView: () => string[];\n onlyFocus: (index: string) => void;\n scrollAndFocus: (index: number, opts?: DSChatT.ScrollActionOptions) => void;\n};\n\nexport const useActionRef = ({\n actionRef,\n vScroll,\n virtualListRef,\n getMessagesInView,\n onlyFocus,\n scrollAndFocus,\n}: UseActionRefArgs) => {\n React.useEffect(() => {\n if (actionRef && actionRef.current) {\n const scrollToIndex: DSChatT.ActionRef['current']['scrollToIndex'] = (index, opts = { align: 'center' }) => {\n setTimeout(() => {\n vScroll(index, opts);\n /* [PUI-15629](https://jira.elliemae.io/browse/PUI-15629)\n * requestAnimationFrame is needed because the first scroll might not calculate positions\n * correctly if the DOM hasn't been fully updated yet. By waiting for the next animation\n * frame, we ensure that:\n * 1. The DOM has been updated with new positions after the first scroll\n * 2. Element height and position measurements are accurate\n * 3. The second scroll can align the element with correct measurements\n */\n requestAnimationFrame(() => {\n vScroll(index, opts);\n });\n }, 0);\n };\n const focusToIndex: DSChatT.ActionRef['current']['focusToIndex'] = (index, opts = { align: 'center' }) => {\n scrollAndFocus(index, opts);\n };\n const focusToList = () => {\n virtualListRef.current?.focus();\n };\n\n actionRef.current.scrollToIndex = scrollToIndex;\n actionRef.current.getMessagesInView = getMessagesInView;\n actionRef.current.focusToIndex = focusToIndex;\n actionRef.current.focusToIndexWithoutScroll = onlyFocus;\n actionRef.current.focusToList = focusToList;\n }\n }, [actionRef, getMessagesInView, onlyFocus, scrollAndFocus, vScroll, virtualListRef]);\n};\n"],
5
+ "mappings": "AAAA,YAAY,WAAW;ACGvB,OAAOA,YAAW;AAYX,MAAM,eAAe,CAAC;AAAA,EAC3B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,MAAwB;AACtB,EAAAA,OAAM,UAAU,MAAM;AACpB,QAAI,aAAa,UAAU,SAAS;AAClC,YAAM,gBAA+D,CAAC,OAAO,OAAO,EAAE,OAAO,SAAS,MAAM;AAC1G,mBAAW,MAAM;AACf,kBAAQ,OAAO,IAAI;AASnB,gCAAsB,MAAM;AAC1B,oBAAQ,OAAO,IAAI;AAAA,UACrB,CAAC;AAAA,QACH,GAAG,CAAC;AAAA,MACN;AACA,YAAM,eAA6D,CAAC,OAAO,OAAO,EAAE,OAAO,SAAS,MAAM;AACxG,uBAAe,OAAO,IAAI;AAAA,MAC5B;AACA,YAAM,cAAc,MAAM;AACxB,uBAAe,SAAS,MAAM;AAAA,MAChC;AAEA,gBAAU,QAAQ,gBAAgB;AAClC,gBAAU,QAAQ,oBAAoB;AACtC,gBAAU,QAAQ,eAAe;AACjC,gBAAU,QAAQ,4BAA4B;AAC9C,gBAAU,QAAQ,cAAc;AAAA,IAClC;AAAA,EACF,GAAG,CAAC,WAAW,mBAAmB,WAAW,gBAAgB,SAAS,cAAc,CAAC;AACvF;",
6
6
  "names": ["React"]
7
7
  }
@@ -6,11 +6,11 @@ import {
6
6
  useGetGlobalAttributes,
7
7
  useValidateTypescriptPropTypes
8
8
  } from "@elliemae/ds-props-helpers";
9
- import { useVirtualizer } from "@tanstack/react-virtual";
10
9
  import { useActionRef } from "./useActionRef.js";
11
10
  import { useGlobalEventListener } from "./useGlobalEventListener.js";
12
11
  import { useKeyboard } from "./useKeyboard.js";
13
12
  import { useFocusAndScrollHelpers } from "./useFocusAndScrollHelpers.js";
13
+ import { useMessageFocusTracker } from "./useMessageFocusTracker.js";
14
14
  import { ChatContainerMessagesListPropTypes, defaultPropsMessagesList } from "../../react-desc-prop-types.js";
15
15
  import { DSChatContainerMessagesList } from "../../DSChatContainerDefinitions.js";
16
16
  const getPadding = ({ hasMoreItems, bannerPosition, isLoading }) => {
@@ -31,36 +31,30 @@ const useChatContainerMessageList = (props) => {
31
31
  );
32
32
  const globalAttributes = useGetGlobalAttributes(propsWithDefault);
33
33
  const loadMoreRef = React2.useRef();
34
- const virtualListRef = React2.useRef();
34
+ const virtualListRef = React2.useRef(null);
35
+ const fastListActionRef = React2.useRef({});
35
36
  const pendingOnScroll = React2.useRef(false);
36
- const { bannerPosition, messages, actionRef, hasMoreItems, isLoading } = propsWithDefault;
37
+ const { bannerPosition, actionRef, hasMoreItems, isLoading } = propsWithDefault;
37
38
  const theme = useTheme();
38
39
  const containerPaddingStart = parseInt(theme.space.xs, 10);
39
40
  const padding = getPadding({ hasMoreItems, bannerPosition, isLoading });
40
- const useVirtualHelpers = useVirtualizer({
41
- count: messages?.length ?? 0,
42
- getScrollElement: () => virtualListRef.current ?? null,
43
- overscan: 15,
44
- paddingStart: padding.paddingStart + containerPaddingStart,
45
- paddingEnd: padding.paddingEnd,
46
- // Unlike v2, v3 has no internal default for estimateSize — it's required, not optional.
47
- estimateSize: React2.useCallback(() => 80, [])
48
- });
49
- const { onScrollStop, updateCache, getMessagesInView, onlyFocus, scrollAndFocus, checkPendingOnScroll } = useFocusAndScrollHelpers({
41
+ const paddingStart = padding.paddingStart + containerPaddingStart;
42
+ const { paddingEnd } = padding;
43
+ const focusTracker = useMessageFocusTracker(virtualListRef);
44
+ const { trackFocusMessage } = focusTracker;
45
+ const { vScroll, onScrollStop, getMessagesInView, onlyFocus, scrollAndFocus, checkPendingOnScroll } = useFocusAndScrollHelpers({
50
46
  propsWithDefault,
51
- useVirtualHelpers,
47
+ fastListActionRef,
52
48
  pendingOnScroll,
53
- virtualListRef
49
+ trackFocusMessage
54
50
  });
55
51
  useActionRef({
56
52
  actionRef,
57
- useVirtualHelpers,
53
+ vScroll,
58
54
  virtualListRef,
59
- updateCache,
60
55
  getMessagesInView,
61
56
  onlyFocus,
62
- scrollAndFocus,
63
- hasVirtualItems: useVirtualHelpers.getVirtualItems().length > 0
57
+ scrollAndFocus
64
58
  });
65
59
  useGlobalEventListener({
66
60
  propsWithDefault,
@@ -75,18 +69,32 @@ const useChatContainerMessageList = (props) => {
75
69
  loadMoreRef,
76
70
  scrollAndFocus,
77
71
  onlyFocus,
78
- getMessagesInView
72
+ getMessagesInView,
73
+ trackFocusMessage
79
74
  });
80
75
  return React2.useMemo(
81
76
  () => ({
82
77
  propsWithDefault,
83
78
  globalAttributes,
84
- useVirtualHelpers,
79
+ fastListActionRef,
80
+ paddingStart,
81
+ paddingEnd,
85
82
  loadMoreRef,
86
83
  virtualListRef,
84
+ focusTracker,
87
85
  ...keyboardMethods
88
86
  }),
89
- [globalAttributes, keyboardMethods, propsWithDefault, useVirtualHelpers, loadMoreRef, virtualListRef]
87
+ [
88
+ globalAttributes,
89
+ keyboardMethods,
90
+ propsWithDefault,
91
+ fastListActionRef,
92
+ paddingStart,
93
+ paddingEnd,
94
+ loadMoreRef,
95
+ virtualListRef,
96
+ focusTracker
97
+ ]
90
98
  );
91
99
  };
92
100
  export {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../../../scripts/build/transpile/react-shim.js", "../../../../src/parts/chat-container-messages-list/useChatContainerMessageList.ts"],
4
- "sourcesContent": ["import * as React from 'react';\nexport { React };\n", "/* eslint-disable complexity */\n/* eslint-disable jsx-a11y/no-noninteractive-tabindex */\n/* eslint-disable max-lines */\nimport React from 'react';\nimport { useTheme } from 'styled-components';\nimport {\n useMemoMergePropsWithDefault,\n useGetGlobalAttributes,\n useValidateTypescriptPropTypes,\n} from '@elliemae/ds-props-helpers';\nimport { useVirtualizer } from '@tanstack/react-virtual';\nimport { useActionRef } from './useActionRef.js';\nimport { useGlobalEventListener } from './useGlobalEventListener.js';\nimport { useKeyboard } from './useKeyboard.js';\nimport { useFocusAndScrollHelpers } from './useFocusAndScrollHelpers.js';\nimport type { DSChatT } from '../../react-desc-prop-types.js';\nimport { ChatContainerMessagesListPropTypes, defaultPropsMessagesList } from '../../react-desc-prop-types.js';\nimport { DSChatContainerMessagesList } from '../../DSChatContainerDefinitions.js';\n\nconst getPadding = ({ hasMoreItems, bannerPosition, isLoading }: DSChatT.GetPaddingT) => {\n const padding = {\n paddingStart: 0,\n paddingEnd: -5,\n };\n if ([hasMoreItems, bannerPosition, isLoading].some((item) => item === 'top')) padding.paddingStart = 42;\n else if ([hasMoreItems, bannerPosition, isLoading].some((item) => item === 'bottom')) padding.paddingEnd = 42;\n return padding;\n};\n\ntype UseChatContainerMessageList = (props: DSChatT.MessagesListProps) => {\n propsWithDefault: DSChatT.MessagesListProps;\n globalAttributes: ReturnType<typeof useGetGlobalAttributes>;\n useVirtualHelpers: DSChatT.ChatVirtualizer;\n loadMoreRef: React.MutableRefObject<HTMLButtonElement | undefined>;\n virtualListRef: React.MutableRefObject<HTMLDivElement | undefined>;\n} & ReturnType<typeof useKeyboard>;\n\nexport const useChatContainerMessageList: UseChatContainerMessageList = (props) => {\n const propsWithDefault = useMemoMergePropsWithDefault<DSChatT.MessagesListProps>(props, defaultPropsMessagesList);\n useValidateTypescriptPropTypes<DSChatT.MessagesListProps>(\n propsWithDefault,\n ChatContainerMessagesListPropTypes,\n DSChatContainerMessagesList,\n );\n const globalAttributes = useGetGlobalAttributes(propsWithDefault);\n const loadMoreRef = React.useRef<HTMLButtonElement>();\n const virtualListRef = React.useRef<HTMLDivElement>();\n\n const pendingOnScroll = React.useRef(false);\n const { bannerPosition, messages, actionRef, hasMoreItems, isLoading } = propsWithDefault;\n const theme = useTheme();\n // The scroll container's own CSS top-padding was removed (see styled.ts's StyledChatContainerContentList)\n // and replaced with this \u2014 an equivalent virtualizer-space paddingStart keeps calculateRange()'s\n // scrollOffset comparisons in the same frame as its item measurements.\n const containerPaddingStart = parseInt(theme.space.xs, 10);\n const padding = getPadding({ hasMoreItems, bannerPosition, isLoading });\n const useVirtualHelpers = useVirtualizer<HTMLDivElement, HTMLDivElement>({\n count: messages?.length ?? 0,\n getScrollElement: () => virtualListRef.current ?? null,\n overscan: 15,\n paddingStart: padding.paddingStart + containerPaddingStart,\n paddingEnd: padding.paddingEnd,\n // Unlike v2, v3 has no internal default for estimateSize \u2014 it's required, not optional.\n estimateSize: React.useCallback(() => 80, []),\n });\n\n const { onScrollStop, updateCache, getMessagesInView, onlyFocus, scrollAndFocus, checkPendingOnScroll } =\n useFocusAndScrollHelpers({\n propsWithDefault,\n useVirtualHelpers,\n pendingOnScroll,\n virtualListRef,\n });\n\n useActionRef({\n actionRef,\n useVirtualHelpers,\n virtualListRef,\n updateCache,\n getMessagesInView,\n onlyFocus,\n scrollAndFocus,\n hasVirtualItems: useVirtualHelpers.getVirtualItems().length > 0,\n });\n\n useGlobalEventListener({\n propsWithDefault,\n virtualListRef,\n onScrollStop,\n checkPendingOnScroll,\n });\n\n const keyboardMethods = useKeyboard({\n propsWithDefault,\n actionRef,\n virtualListRef,\n loadMoreRef,\n scrollAndFocus,\n onlyFocus,\n getMessagesInView,\n });\n\n return React.useMemo(\n () => ({\n propsWithDefault,\n globalAttributes,\n useVirtualHelpers,\n loadMoreRef,\n virtualListRef,\n ...keyboardMethods,\n }),\n [globalAttributes, keyboardMethods, propsWithDefault, useVirtualHelpers, loadMoreRef, virtualListRef],\n );\n};\n"],
5
- "mappings": "AAAA,YAAY,WAAW;ACGvB,OAAOA,YAAW;AAClB,SAAS,gBAAgB;AACzB;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,sBAAsB;AAC/B,SAAS,oBAAoB;AAC7B,SAAS,8BAA8B;AACvC,SAAS,mBAAmB;AAC5B,SAAS,gCAAgC;AAEzC,SAAS,oCAAoC,gCAAgC;AAC7E,SAAS,mCAAmC;AAE5C,MAAM,aAAa,CAAC,EAAE,cAAc,gBAAgB,UAAU,MAA2B;AACvF,QAAM,UAAU;AAAA,IACd,cAAc;AAAA,IACd,YAAY;AAAA,EACd;AACA,MAAI,CAAC,cAAc,gBAAgB,SAAS,EAAE,KAAK,CAAC,SAAS,SAAS,KAAK,EAAG,SAAQ,eAAe;AAAA,WAC5F,CAAC,cAAc,gBAAgB,SAAS,EAAE,KAAK,CAAC,SAAS,SAAS,QAAQ,EAAG,SAAQ,aAAa;AAC3G,SAAO;AACT;AAUO,MAAM,8BAA2D,CAAC,UAAU;AACjF,QAAM,mBAAmB,6BAAwD,OAAO,wBAAwB;AAChH;AAAA,IACE;AAAA,IACA;AAAA,IACA;AAAA,EACF;AACA,QAAM,mBAAmB,uBAAuB,gBAAgB;AAChE,QAAM,cAAcA,OAAM,OAA0B;AACpD,QAAM,iBAAiBA,OAAM,OAAuB;AAEpD,QAAM,kBAAkBA,OAAM,OAAO,KAAK;AAC1C,QAAM,EAAE,gBAAgB,UAAU,WAAW,cAAc,UAAU,IAAI;AACzE,QAAM,QAAQ,SAAS;AAIvB,QAAM,wBAAwB,SAAS,MAAM,MAAM,IAAI,EAAE;AACzD,QAAM,UAAU,WAAW,EAAE,cAAc,gBAAgB,UAAU,CAAC;AACtE,QAAM,oBAAoB,eAA+C;AAAA,IACvE,OAAO,UAAU,UAAU;AAAA,IAC3B,kBAAkB,MAAM,eAAe,WAAW;AAAA,IAClD,UAAU;AAAA,IACV,cAAc,QAAQ,eAAe;AAAA,IACrC,YAAY,QAAQ;AAAA;AAAA,IAEpB,cAAcA,OAAM,YAAY,MAAM,IAAI,CAAC,CAAC;AAAA,EAC9C,CAAC;AAED,QAAM,EAAE,cAAc,aAAa,mBAAmB,WAAW,gBAAgB,qBAAqB,IACpG,yBAAyB;AAAA,IACvB;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF,CAAC;AAEH,eAAa;AAAA,IACX;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,iBAAiB,kBAAkB,gBAAgB,EAAE,SAAS;AAAA,EAChE,CAAC;AAED,yBAAuB;AAAA,IACrB;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF,CAAC;AAED,QAAM,kBAAkB,YAAY;AAAA,IAClC;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF,CAAC;AAED,SAAOA,OAAM;AAAA,IACX,OAAO;AAAA,MACL;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA,GAAG;AAAA,IACL;AAAA,IACA,CAAC,kBAAkB,iBAAiB,kBAAkB,mBAAmB,aAAa,cAAc;AAAA,EACtG;AACF;",
4
+ "sourcesContent": ["import * as React from 'react';\nexport { React };\n", "/* eslint-disable complexity */\n/* eslint-disable jsx-a11y/no-noninteractive-tabindex */\n/* eslint-disable max-lines */\nimport React from 'react';\nimport { useTheme } from 'styled-components';\nimport {\n useMemoMergePropsWithDefault,\n useGetGlobalAttributes,\n useValidateTypescriptPropTypes,\n} from '@elliemae/ds-props-helpers';\nimport type { DSFastListT } from '@elliemae/ds-fast-list';\nimport { useActionRef } from './useActionRef.js';\nimport { useGlobalEventListener } from './useGlobalEventListener.js';\nimport { useKeyboard } from './useKeyboard.js';\nimport { useFocusAndScrollHelpers } from './useFocusAndScrollHelpers.js';\nimport { useMessageFocusTracker } from './useMessageFocusTracker.js';\nimport type { DSChatT } from '../../react-desc-prop-types.js';\nimport { ChatContainerMessagesListPropTypes, defaultPropsMessagesList } from '../../react-desc-prop-types.js';\nimport { DSChatContainerMessagesList } from '../../DSChatContainerDefinitions.js';\n\nconst getPadding = ({ hasMoreItems, bannerPosition, isLoading }: DSChatT.GetPaddingT) => {\n const padding = {\n paddingStart: 0,\n paddingEnd: -5,\n };\n if ([hasMoreItems, bannerPosition, isLoading].some((item) => item === 'top')) padding.paddingStart = 42;\n else if ([hasMoreItems, bannerPosition, isLoading].some((item) => item === 'bottom')) padding.paddingEnd = 42;\n return padding;\n};\n\ntype UseChatContainerMessageList = (props: DSChatT.MessagesListProps) => {\n propsWithDefault: DSChatT.MessagesListProps;\n globalAttributes: ReturnType<typeof useGetGlobalAttributes>;\n fastListActionRef: React.MutableRefObject<DSFastListT.ActionRef>;\n paddingStart: number;\n paddingEnd: number;\n loadMoreRef: React.MutableRefObject<HTMLButtonElement | undefined>;\n virtualListRef: React.MutableRefObject<HTMLDivElement | null>;\n focusTracker: ReturnType<typeof useMessageFocusTracker>;\n} & ReturnType<typeof useKeyboard>;\n\nexport const useChatContainerMessageList: UseChatContainerMessageList = (props) => {\n const propsWithDefault = useMemoMergePropsWithDefault<DSChatT.MessagesListProps>(props, defaultPropsMessagesList);\n useValidateTypescriptPropTypes<DSChatT.MessagesListProps>(\n propsWithDefault,\n ChatContainerMessagesListPropTypes,\n DSChatContainerMessagesList,\n );\n const globalAttributes = useGetGlobalAttributes(propsWithDefault);\n const loadMoreRef = React.useRef<HTMLButtonElement>();\n const virtualListRef = React.useRef<HTMLDivElement | null>(null);\n const fastListActionRef = React.useRef<DSFastListT.ActionRef>({});\n\n const pendingOnScroll = React.useRef(false);\n const { bannerPosition, actionRef, hasMoreItems, isLoading } = propsWithDefault;\n const theme = useTheme();\n // The scroll container's own CSS top-padding was removed (see styled.ts's StyledFastList) and\n // replaced with this \u2014 an equivalent virtualizer-space paddingStart keeps getVisibleItems()'s\n // scrollOffset comparisons in the same frame as its item measurements.\n const containerPaddingStart = parseInt(theme.space.xs, 10);\n const padding = getPadding({ hasMoreItems, bannerPosition, isLoading });\n const paddingStart = padding.paddingStart + containerPaddingStart;\n const { paddingEnd } = padding;\n\n const focusTracker = useMessageFocusTracker(virtualListRef);\n const { trackFocusMessage } = focusTracker;\n\n const { vScroll, onScrollStop, getMessagesInView, onlyFocus, scrollAndFocus, checkPendingOnScroll } =\n useFocusAndScrollHelpers({\n propsWithDefault,\n fastListActionRef,\n pendingOnScroll,\n trackFocusMessage,\n });\n\n useActionRef({\n actionRef,\n vScroll,\n virtualListRef,\n getMessagesInView,\n onlyFocus,\n scrollAndFocus,\n });\n\n useGlobalEventListener({\n propsWithDefault,\n virtualListRef,\n onScrollStop,\n checkPendingOnScroll,\n });\n\n const keyboardMethods = useKeyboard({\n propsWithDefault,\n actionRef,\n virtualListRef,\n loadMoreRef,\n scrollAndFocus,\n onlyFocus,\n getMessagesInView,\n trackFocusMessage,\n });\n\n return React.useMemo(\n () => ({\n propsWithDefault,\n globalAttributes,\n fastListActionRef,\n paddingStart,\n paddingEnd,\n loadMoreRef,\n virtualListRef,\n focusTracker,\n ...keyboardMethods,\n }),\n [\n globalAttributes,\n keyboardMethods,\n propsWithDefault,\n fastListActionRef,\n paddingStart,\n paddingEnd,\n loadMoreRef,\n virtualListRef,\n focusTracker,\n ],\n );\n};\n"],
5
+ "mappings": "AAAA,YAAY,WAAW;ACGvB,OAAOA,YAAW;AAClB,SAAS,gBAAgB;AACzB;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAEP,SAAS,oBAAoB;AAC7B,SAAS,8BAA8B;AACvC,SAAS,mBAAmB;AAC5B,SAAS,gCAAgC;AACzC,SAAS,8BAA8B;AAEvC,SAAS,oCAAoC,gCAAgC;AAC7E,SAAS,mCAAmC;AAE5C,MAAM,aAAa,CAAC,EAAE,cAAc,gBAAgB,UAAU,MAA2B;AACvF,QAAM,UAAU;AAAA,IACd,cAAc;AAAA,IACd,YAAY;AAAA,EACd;AACA,MAAI,CAAC,cAAc,gBAAgB,SAAS,EAAE,KAAK,CAAC,SAAS,SAAS,KAAK,EAAG,SAAQ,eAAe;AAAA,WAC5F,CAAC,cAAc,gBAAgB,SAAS,EAAE,KAAK,CAAC,SAAS,SAAS,QAAQ,EAAG,SAAQ,aAAa;AAC3G,SAAO;AACT;AAaO,MAAM,8BAA2D,CAAC,UAAU;AACjF,QAAM,mBAAmB,6BAAwD,OAAO,wBAAwB;AAChH;AAAA,IACE;AAAA,IACA;AAAA,IACA;AAAA,EACF;AACA,QAAM,mBAAmB,uBAAuB,gBAAgB;AAChE,QAAM,cAAcA,OAAM,OAA0B;AACpD,QAAM,iBAAiBA,OAAM,OAA8B,IAAI;AAC/D,QAAM,oBAAoBA,OAAM,OAA8B,CAAC,CAAC;AAEhE,QAAM,kBAAkBA,OAAM,OAAO,KAAK;AAC1C,QAAM,EAAE,gBAAgB,WAAW,cAAc,UAAU,IAAI;AAC/D,QAAM,QAAQ,SAAS;AAIvB,QAAM,wBAAwB,SAAS,MAAM,MAAM,IAAI,EAAE;AACzD,QAAM,UAAU,WAAW,EAAE,cAAc,gBAAgB,UAAU,CAAC;AACtE,QAAM,eAAe,QAAQ,eAAe;AAC5C,QAAM,EAAE,WAAW,IAAI;AAEvB,QAAM,eAAe,uBAAuB,cAAc;AAC1D,QAAM,EAAE,kBAAkB,IAAI;AAE9B,QAAM,EAAE,SAAS,cAAc,mBAAmB,WAAW,gBAAgB,qBAAqB,IAChG,yBAAyB;AAAA,IACvB;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF,CAAC;AAEH,eAAa;AAAA,IACX;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF,CAAC;AAED,yBAAuB;AAAA,IACrB;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF,CAAC;AAED,QAAM,kBAAkB,YAAY;AAAA,IAClC;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF,CAAC;AAED,SAAOA,OAAM;AAAA,IACX,OAAO;AAAA,MACL;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA,GAAG;AAAA,IACL;AAAA,IACA;AAAA,MACE;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IACF;AAAA,EACF;AACF;",
6
6
  "names": ["React"]
7
7
  }
@@ -1,36 +1,36 @@
1
1
  import * as React from "react";
2
2
  import React2 from "react";
3
- import { debounce, throttle } from "lodash-es";
3
+ import { debounce } from "lodash-es";
4
+ import { getMessageFocusId, getMessagePositionId } from "./useMessageFocusTracker.js";
4
5
  const useFocusAndScrollHelpers = ({
5
- propsWithDefault: { onScrollEnds },
6
- useVirtualHelpers: { scrollToIndex: vScroll, calculateRange },
6
+ propsWithDefault: { onScrollEnds, messages },
7
+ fastListActionRef,
7
8
  pendingOnScroll,
8
- virtualListRef
9
+ trackFocusMessage
9
10
  }) => {
11
+ const vScroll = React2.useCallback(
12
+ (index, opts) => {
13
+ const scrollOnce = () => {
14
+ const fastList = fastListActionRef.current;
15
+ const offset = fastList.getOffsetForIndex?.(index, opts?.align ?? "center");
16
+ if (offset !== void 0) fastList.scrollToOffset?.(offset, { behavior: opts?.behavior });
17
+ };
18
+ scrollOnce();
19
+ requestAnimationFrame(scrollOnce);
20
+ },
21
+ [fastListActionRef]
22
+ );
10
23
  const computeVisibleItems = React2.useCallback(() => {
11
24
  if (document.visibilityState === "hidden") {
12
25
  pendingOnScroll.current = true;
13
26
  return [];
14
27
  }
15
- const range = calculateRange();
16
- if (!range) return [];
28
+ const visibleItems = fastListActionRef.current.getVisibleItems?.() ?? [];
29
+ if (visibleItems.length === 0) return [];
17
30
  pendingOnScroll.current = false;
18
- const indexList = [];
19
- for (let index = range.startIndex; index <= range.endIndex; index += 1) indexList.push(String(index));
20
- return indexList;
21
- }, [calculateRange, pendingOnScroll]);
22
- const visibleItemsCache = React2.useRef([]);
23
- const updateCache = React2.useMemo(
24
- () => throttle(
25
- () => {
26
- visibleItemsCache.current = computeVisibleItems();
27
- },
28
- 500,
29
- { leading: true }
30
- ),
31
- [computeVisibleItems]
32
- );
33
- const getMessagesInView = React2.useCallback(() => [...visibleItemsCache.current], []);
31
+ return visibleItems.map(String);
32
+ }, [fastListActionRef, pendingOnScroll]);
33
+ const getMessagesInView = computeVisibleItems;
34
34
  const onScrollStop = React2.useMemo(
35
35
  () => debounce(() => {
36
36
  const indexList = computeVisibleItems();
@@ -41,39 +41,39 @@ const useFocusAndScrollHelpers = ({
41
41
  );
42
42
  const onlyFocus = React2.useCallback(
43
43
  (index) => {
44
- virtualListRef.current?.querySelector(`[data-index="${index}"]`)?.focus();
44
+ const position = Number(index);
45
+ const row = messages?.[position];
46
+ const mounted = fastListActionRef.current.getMountedItems?.() ?? [];
47
+ if (row && mounted.includes(position))
48
+ trackFocusMessage(getMessageFocusId(row, position), { preventScroll: true });
45
49
  },
46
- [virtualListRef]
50
+ [messages, fastListActionRef, trackFocusMessage]
47
51
  );
48
52
  const scrollAndFocus = React2.useCallback(
49
- (index) => {
50
- vScroll(index, { align: "center" });
53
+ (index, opts = { align: "center" }) => {
54
+ const row = messages?.[index];
55
+ const messageId = row ? getMessageFocusId(row, index) : getMessagePositionId(index);
56
+ trackFocusMessage(messageId, { preventScroll: true });
57
+ vScroll(index, opts);
51
58
  requestAnimationFrame(() => {
52
- vScroll(index, { align: "center" });
59
+ vScroll(index, opts);
53
60
  });
54
- setTimeout(() => {
55
- onlyFocus(index.toString());
56
- }, 300);
57
- updateCache();
58
61
  },
59
- [updateCache, onlyFocus, vScroll]
62
+ [messages, trackFocusMessage, vScroll]
60
63
  );
61
64
  const checkPendingOnScroll = React2.useCallback(() => {
62
- if (pendingOnScroll.current === true) {
63
- updateCache();
64
- onScrollStop();
65
- }
66
- }, [updateCache, onScrollStop, pendingOnScroll]);
65
+ if (pendingOnScroll.current === true) onScrollStop();
66
+ }, [onScrollStop, pendingOnScroll]);
67
67
  return React2.useMemo(
68
68
  () => ({
69
+ vScroll,
69
70
  onScrollStop,
70
- updateCache,
71
71
  getMessagesInView,
72
72
  onlyFocus,
73
73
  scrollAndFocus,
74
74
  checkPendingOnScroll
75
75
  }),
76
- [onScrollStop, updateCache, getMessagesInView, onlyFocus, scrollAndFocus, checkPendingOnScroll]
76
+ [vScroll, onScrollStop, getMessagesInView, onlyFocus, scrollAndFocus, checkPendingOnScroll]
77
77
  );
78
78
  };
79
79
  export {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../../../scripts/build/transpile/react-shim.js", "../../../../src/parts/chat-container-messages-list/useFocusAndScrollHelpers.ts"],
4
- "sourcesContent": ["import * as React from 'react';\nexport { React };\n", "/* eslint-disable complexity */\n/* eslint-disable jsx-a11y/no-noninteractive-tabindex */\n/* eslint-disable max-lines */\nimport React from 'react';\nimport { debounce, throttle } from 'lodash-es';\nimport type { DSChatT } from '../../react-desc-prop-types.js';\n\ntype UseFocusAndScrollHelpersArgs = {\n propsWithDefault: DSChatT.MessagesListProps;\n useVirtualHelpers: DSChatT.ChatVirtualizer;\n pendingOnScroll: React.MutableRefObject<boolean>;\n virtualListRef: React.MutableRefObject<HTMLDivElement | undefined>;\n};\n\nexport const useFocusAndScrollHelpers = ({\n propsWithDefault: { onScrollEnds },\n useVirtualHelpers: { scrollToIndex: vScroll, calculateRange },\n pendingOnScroll,\n virtualListRef,\n}: UseFocusAndScrollHelpersArgs) => {\n /**\n * `calculateRange()` is the virtualizer's own pre-overscan viewport calculation (pure scroll\n * geometry, no DOM scraping) \u2014 mirrors the pattern already shipped in ds-fast-list/ds-virtual-list\n * (PUI-17898's `actionRef.getVisibleItems`). It returns `null` before the container's first real\n * measurement, same gating condition `getVirtualItems()` uses (see useActionRef.ts's\n * `hasVirtualItems`). Replaces the previous `getBoundingClientRect()`/`querySelectorAll` DOM scan,\n * which re-measured real layout on every call instead of reading the virtualizer's own tracked\n * scroll state.\n */\n const computeVisibleItems = React.useCallback((): string[] => {\n if (document.visibilityState === 'hidden') {\n pendingOnScroll.current = true;\n return [];\n }\n const range = calculateRange();\n if (!range) return [];\n pendingOnScroll.current = false;\n const indexList: string[] = [];\n for (let index = range.startIndex; index <= range.endIndex; index += 1) indexList.push(String(index));\n return indexList;\n }, [calculateRange, pendingOnScroll]);\n\n // Throttled with leading: populates the cache immediately on first call and keeps it\n // fresh. No side effects beyond DOM reads \u2014 onScrollEnds is not involved.\n const visibleItemsCache = React.useRef<string[]>([]);\n const updateCache = React.useMemo(\n () =>\n throttle(\n () => {\n visibleItemsCache.current = computeVisibleItems();\n },\n 500,\n { leading: true },\n ),\n [computeVisibleItems],\n );\n\n // Synchronous getter \u2014 returns a copy so callers using .pop()/.shift() don't mutate the cache.\n const getMessagesInView = React.useCallback((): string[] => [...visibleItemsCache.current], []);\n\n // Debounced trailing: notifies onScrollEnds consumers after scrolling settles.\n // Semantics and timing unchanged \u2014 this path is isolated from the cache.\n const onScrollStop = React.useMemo(\n () =>\n debounce((): string[] => {\n const indexList = computeVisibleItems();\n if (onScrollEnds) onScrollEnds(indexList);\n return indexList;\n }, 500),\n [computeVisibleItems, onScrollEnds],\n ) as () => string[];\n\n const onlyFocus = React.useCallback(\n (index: string) => {\n (virtualListRef.current?.querySelector(`[data-index=\"${index}\"]`) as HTMLDivElement)?.focus();\n },\n [virtualListRef],\n );\n\n const scrollAndFocus = React.useCallback(\n (index: number) => {\n vScroll(index, { align: 'center' });\n /* [PUI-15629](https://jira.elliemae.io/browse/PUI-15629)\n * requestAnimationFrame is needed because the first scroll might not calculate positions\n * correctly if the DOM hasn't been fully updated yet. By waiting for the next animation\n * frame, we ensure that:\n * 1. The DOM has been updated with new positions after the first scroll\n * 2. Element height and position measurements are accurate\n * 3. The second scroll can center the element with correct measurements\n *\n * TODO: It might be worth consider upgrading to react-virtual v3(@tanstack/react-virtual), which handles DOM measurements and scroll\n * synchronization better. This RAF workaround might not be necessary with v3.\n */\n requestAnimationFrame(() => {\n vScroll(index, { align: 'center' });\n });\n\n setTimeout(() => {\n onlyFocus(index.toString());\n }, 300);\n\n updateCache();\n },\n [updateCache, onlyFocus, vScroll],\n );\n\n const checkPendingOnScroll = React.useCallback(() => {\n if (pendingOnScroll.current === true) {\n updateCache();\n onScrollStop();\n }\n }, [updateCache, onScrollStop, pendingOnScroll]);\n\n return React.useMemo(\n () => ({\n onScrollStop,\n updateCache,\n getMessagesInView,\n onlyFocus,\n scrollAndFocus,\n checkPendingOnScroll,\n }),\n [onScrollStop, updateCache, getMessagesInView, onlyFocus, scrollAndFocus, checkPendingOnScroll],\n );\n};\n"],
5
- "mappings": "AAAA,YAAY,WAAW;ACGvB,OAAOA,YAAW;AAClB,SAAS,UAAU,gBAAgB;AAU5B,MAAM,2BAA2B,CAAC;AAAA,EACvC,kBAAkB,EAAE,aAAa;AAAA,EACjC,mBAAmB,EAAE,eAAe,SAAS,eAAe;AAAA,EAC5D;AAAA,EACA;AACF,MAAoC;AAUlC,QAAM,sBAAsBA,OAAM,YAAY,MAAgB;AAC5D,QAAI,SAAS,oBAAoB,UAAU;AACzC,sBAAgB,UAAU;AAC1B,aAAO,CAAC;AAAA,IACV;AACA,UAAM,QAAQ,eAAe;AAC7B,QAAI,CAAC,MAAO,QAAO,CAAC;AACpB,oBAAgB,UAAU;AAC1B,UAAM,YAAsB,CAAC;AAC7B,aAAS,QAAQ,MAAM,YAAY,SAAS,MAAM,UAAU,SAAS,EAAG,WAAU,KAAK,OAAO,KAAK,CAAC;AACpG,WAAO;AAAA,EACT,GAAG,CAAC,gBAAgB,eAAe,CAAC;AAIpC,QAAM,oBAAoBA,OAAM,OAAiB,CAAC,CAAC;AACnD,QAAM,cAAcA,OAAM;AAAA,IACxB,MACE;AAAA,MACE,MAAM;AACJ,0BAAkB,UAAU,oBAAoB;AAAA,MAClD;AAAA,MACA;AAAA,MACA,EAAE,SAAS,KAAK;AAAA,IAClB;AAAA,IACF,CAAC,mBAAmB;AAAA,EACtB;AAGA,QAAM,oBAAoBA,OAAM,YAAY,MAAgB,CAAC,GAAG,kBAAkB,OAAO,GAAG,CAAC,CAAC;AAI9F,QAAM,eAAeA,OAAM;AAAA,IACzB,MACE,SAAS,MAAgB;AACvB,YAAM,YAAY,oBAAoB;AACtC,UAAI,aAAc,cAAa,SAAS;AACxC,aAAO;AAAA,IACT,GAAG,GAAG;AAAA,IACR,CAAC,qBAAqB,YAAY;AAAA,EACpC;AAEA,QAAM,YAAYA,OAAM;AAAA,IACtB,CAAC,UAAkB;AACjB,MAAC,eAAe,SAAS,cAAc,gBAAgB,KAAK,IAAI,GAAsB,MAAM;AAAA,IAC9F;AAAA,IACA,CAAC,cAAc;AAAA,EACjB;AAEA,QAAM,iBAAiBA,OAAM;AAAA,IAC3B,CAAC,UAAkB;AACjB,cAAQ,OAAO,EAAE,OAAO,SAAS,CAAC;AAYlC,4BAAsB,MAAM;AAC1B,gBAAQ,OAAO,EAAE,OAAO,SAAS,CAAC;AAAA,MACpC,CAAC;AAED,iBAAW,MAAM;AACf,kBAAU,MAAM,SAAS,CAAC;AAAA,MAC5B,GAAG,GAAG;AAEN,kBAAY;AAAA,IACd;AAAA,IACA,CAAC,aAAa,WAAW,OAAO;AAAA,EAClC;AAEA,QAAM,uBAAuBA,OAAM,YAAY,MAAM;AACnD,QAAI,gBAAgB,YAAY,MAAM;AACpC,kBAAY;AACZ,mBAAa;AAAA,IACf;AAAA,EACF,GAAG,CAAC,aAAa,cAAc,eAAe,CAAC;AAE/C,SAAOA,OAAM;AAAA,IACX,OAAO;AAAA,MACL;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IACF;AAAA,IACA,CAAC,cAAc,aAAa,mBAAmB,WAAW,gBAAgB,oBAAoB;AAAA,EAChG;AACF;",
4
+ "sourcesContent": ["import * as React from 'react';\nexport { React };\n", "/* eslint-disable complexity */\n/* eslint-disable jsx-a11y/no-noninteractive-tabindex */\n/* eslint-disable max-lines */\nimport React from 'react';\nimport { debounce } from 'lodash-es';\nimport type { DSFastListT } from '@elliemae/ds-fast-list';\nimport type { DSChatT } from '../../react-desc-prop-types.js';\nimport { getMessageFocusId, getMessagePositionId } from './useMessageFocusTracker.js';\nimport type { MessageId } from './useMessageFocusTracker.js';\n\ntype UseFocusAndScrollHelpersArgs = {\n propsWithDefault: DSChatT.MessagesListProps;\n fastListActionRef: React.MutableRefObject<DSFastListT.ActionRef>;\n pendingOnScroll: React.MutableRefObject<boolean>;\n trackFocusMessage: (messageId: MessageId, options?: { preventScroll?: boolean }) => void;\n};\n\nexport const useFocusAndScrollHelpers = ({\n propsWithDefault: { onScrollEnds, messages },\n fastListActionRef,\n pendingOnScroll,\n trackFocusMessage,\n}: UseFocusAndScrollHelpersArgs) => {\n /**\n * Does not use `scrollTo` (the virtualizer's `scrollToIndex`): since @tanstack/react-virtual v3 every call\n * starts a loop that checks the position on the following frames and scrolls again, up to 10 times, and it\n * cannot tell the user's scrolling from its own, so a user who scrolls while it runs is pulled back to the\n * target over and over (PUI-19237). This scrolls the way react-virtual v2's `scrollToIndex` did instead:\n * once now and once on the next frame, when the items just rendered have been measured, and never again.\n */\n const vScroll = React.useCallback(\n (index: number, opts?: DSChatT.ScrollActionOptions) => {\n const scrollOnce = () => {\n const fastList = fastListActionRef.current;\n /** The chat's own default alignment also applies when only `behavior` is provided. */\n const offset = fastList.getOffsetForIndex?.(index, opts?.align ?? 'center');\n if (offset !== undefined) fastList.scrollToOffset?.(offset, { behavior: opts?.behavior });\n };\n scrollOnce();\n requestAnimationFrame(scrollOnce);\n },\n [fastListActionRef],\n );\n\n /**\n * Evaluated at call time from `ds-fast-list`'s `getVisibleItems()` (the virtualizer's own pre-overscan\n * viewport range), so it always reflects the current viewport, including after the user scrolls by hand.\n * It is empty before the container's first real measurement.\n */\n const computeVisibleItems = React.useCallback((): string[] => {\n if (document.visibilityState === 'hidden') {\n pendingOnScroll.current = true;\n return [];\n }\n const visibleItems = fastListActionRef.current.getVisibleItems?.() ?? [];\n if (visibleItems.length === 0) return [];\n pendingOnScroll.current = false;\n return visibleItems.map(String);\n }, [fastListActionRef, pendingOnScroll]);\n\n // Each call returns a fresh array, so callers using .pop()/.shift() can't affect later calls.\n const getMessagesInView = computeVisibleItems;\n\n // Debounced trailing: notifies onScrollEnds consumers after scrolling settles.\n const onScrollStop = React.useMemo(\n () =>\n debounce((): string[] => {\n const indexList = computeVisibleItems();\n if (onScrollEnds) onScrollEnds(indexList);\n return indexList;\n }, 500),\n [computeVisibleItems, onScrollEnds],\n ) as () => string[];\n\n /**\n * Focuses a message only if it is mounted now: it never focuses one later, when it scrolls into view. It never\n * scrolls either, as its name says, even when the message is only partly visible.\n */\n const onlyFocus = React.useCallback(\n (index: string) => {\n const position = Number(index);\n const row = messages?.[position];\n const mounted = fastListActionRef.current.getMountedItems?.() ?? [];\n if (row && mounted.includes(position))\n trackFocusMessage(getMessageFocusId(row, position), { preventScroll: true });\n },\n [messages, fastListActionRef, trackFocusMessage],\n );\n\n const scrollAndFocus = React.useCallback(\n (index: number, opts: DSChatT.ScrollActionOptions = { align: 'center' }) => {\n const row = messages?.[index];\n /** A message not rendered yet (added in the same handler) is targeted by its position; `vScroll`'s second write runs after the commit. */\n const messageId = row ? getMessageFocusId(row, index) : getMessagePositionId(index);\n trackFocusMessage(messageId, { preventScroll: true });\n vScroll(index, opts);\n /* [PUI-15629](https://jira.elliemae.io/browse/PUI-15629)\n * requestAnimationFrame is needed because the first scroll might not calculate positions\n * correctly if the DOM hasn't been fully updated yet. By waiting for the next animation\n * frame, we ensure that:\n * 1. The DOM has been updated with new positions after the first scroll\n * 2. Element height and position measurements are accurate\n * 3. The second scroll can align the element with correct measurements\n */\n requestAnimationFrame(() => {\n vScroll(index, opts);\n });\n },\n [messages, trackFocusMessage, vScroll],\n );\n\n const checkPendingOnScroll = React.useCallback(() => {\n if (pendingOnScroll.current === true) onScrollStop();\n }, [onScrollStop, pendingOnScroll]);\n\n return React.useMemo(\n () => ({\n vScroll,\n onScrollStop,\n getMessagesInView,\n onlyFocus,\n scrollAndFocus,\n checkPendingOnScroll,\n }),\n [vScroll, onScrollStop, getMessagesInView, onlyFocus, scrollAndFocus, checkPendingOnScroll],\n );\n};\n"],
5
+ "mappings": "AAAA,YAAY,WAAW;ACGvB,OAAOA,YAAW;AAClB,SAAS,gBAAgB;AAGzB,SAAS,mBAAmB,4BAA4B;AAUjD,MAAM,2BAA2B,CAAC;AAAA,EACvC,kBAAkB,EAAE,cAAc,SAAS;AAAA,EAC3C;AAAA,EACA;AAAA,EACA;AACF,MAAoC;AAQlC,QAAM,UAAUA,OAAM;AAAA,IACpB,CAAC,OAAe,SAAuC;AACrD,YAAM,aAAa,MAAM;AACvB,cAAM,WAAW,kBAAkB;AAEnC,cAAM,SAAS,SAAS,oBAAoB,OAAO,MAAM,SAAS,QAAQ;AAC1E,YAAI,WAAW,OAAW,UAAS,iBAAiB,QAAQ,EAAE,UAAU,MAAM,SAAS,CAAC;AAAA,MAC1F;AACA,iBAAW;AACX,4BAAsB,UAAU;AAAA,IAClC;AAAA,IACA,CAAC,iBAAiB;AAAA,EACpB;AAOA,QAAM,sBAAsBA,OAAM,YAAY,MAAgB;AAC5D,QAAI,SAAS,oBAAoB,UAAU;AACzC,sBAAgB,UAAU;AAC1B,aAAO,CAAC;AAAA,IACV;AACA,UAAM,eAAe,kBAAkB,QAAQ,kBAAkB,KAAK,CAAC;AACvE,QAAI,aAAa,WAAW,EAAG,QAAO,CAAC;AACvC,oBAAgB,UAAU;AAC1B,WAAO,aAAa,IAAI,MAAM;AAAA,EAChC,GAAG,CAAC,mBAAmB,eAAe,CAAC;AAGvC,QAAM,oBAAoB;AAG1B,QAAM,eAAeA,OAAM;AAAA,IACzB,MACE,SAAS,MAAgB;AACvB,YAAM,YAAY,oBAAoB;AACtC,UAAI,aAAc,cAAa,SAAS;AACxC,aAAO;AAAA,IACT,GAAG,GAAG;AAAA,IACR,CAAC,qBAAqB,YAAY;AAAA,EACpC;AAMA,QAAM,YAAYA,OAAM;AAAA,IACtB,CAAC,UAAkB;AACjB,YAAM,WAAW,OAAO,KAAK;AAC7B,YAAM,MAAM,WAAW,QAAQ;AAC/B,YAAM,UAAU,kBAAkB,QAAQ,kBAAkB,KAAK,CAAC;AAClE,UAAI,OAAO,QAAQ,SAAS,QAAQ;AAClC,0BAAkB,kBAAkB,KAAK,QAAQ,GAAG,EAAE,eAAe,KAAK,CAAC;AAAA,IAC/E;AAAA,IACA,CAAC,UAAU,mBAAmB,iBAAiB;AAAA,EACjD;AAEA,QAAM,iBAAiBA,OAAM;AAAA,IAC3B,CAAC,OAAe,OAAoC,EAAE,OAAO,SAAS,MAAM;AAC1E,YAAM,MAAM,WAAW,KAAK;AAE5B,YAAM,YAAY,MAAM,kBAAkB,KAAK,KAAK,IAAI,qBAAqB,KAAK;AAClF,wBAAkB,WAAW,EAAE,eAAe,KAAK,CAAC;AACpD,cAAQ,OAAO,IAAI;AASnB,4BAAsB,MAAM;AAC1B,gBAAQ,OAAO,IAAI;AAAA,MACrB,CAAC;AAAA,IACH;AAAA,IACA,CAAC,UAAU,mBAAmB,OAAO;AAAA,EACvC;AAEA,QAAM,uBAAuBA,OAAM,YAAY,MAAM;AACnD,QAAI,gBAAgB,YAAY,KAAM,cAAa;AAAA,EACrD,GAAG,CAAC,cAAc,eAAe,CAAC;AAElC,SAAOA,OAAM;AAAA,IACX,OAAO;AAAA,MACL;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IACF;AAAA,IACA,CAAC,SAAS,cAAc,mBAAmB,WAAW,gBAAgB,oBAAoB;AAAA,EAC5F;AACF;",
6
6
  "names": ["React"]
7
7
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../../../scripts/build/transpile/react-shim.js", "../../../../src/parts/chat-container-messages-list/useGlobalEventListener.tsx"],
4
- "sourcesContent": ["import * as React from 'react';\nexport { React };\n", "import React from 'react';\nimport { type TypescriptHelpersT } from '@elliemae/ds-typescript-helpers';\nimport { type DSChatT } from '../../react-desc-prop-types.js';\n\ntype UseGlobalEventListenerArgs = {\n propsWithDefault: DSChatT.MessagesListProps;\n virtualListRef: React.MutableRefObject<HTMLDivElement | undefined>;\n onScrollStop: () => string[];\n checkPendingOnScroll: TypescriptHelpersT.GenericFunc;\n};\n\nexport const useGlobalEventListener = ({\n propsWithDefault: { onScrollEnds },\n virtualListRef,\n onScrollStop,\n checkPendingOnScroll,\n}: UseGlobalEventListenerArgs) => {\n React.useEffect(() => {\n let handlerCallback: ReturnType<typeof setTimeout>;\n const localVirtualRef = virtualListRef.current;\n const onScroll = () => {\n if (handlerCallback) clearTimeout(handlerCallback);\n handlerCallback = setTimeout(onScrollStop, 300);\n };\n if (onScrollEnds) {\n virtualListRef.current?.addEventListener('scroll', onScroll);\n }\n document.addEventListener('visibilitychange', checkPendingOnScroll);\n return () => {\n if (handlerCallback) clearTimeout(handlerCallback);\n if (onScrollEnds && localVirtualRef) localVirtualRef.removeEventListener('scroll', onScroll);\n document.removeEventListener('visibilitychange', checkPendingOnScroll);\n };\n }, [onScrollStop, onScrollEnds, checkPendingOnScroll, virtualListRef]);\n};\n"],
4
+ "sourcesContent": ["import * as React from 'react';\nexport { React };\n", "import React from 'react';\nimport { type TypescriptHelpersT } from '@elliemae/ds-typescript-helpers';\nimport { type DSChatT } from '../../react-desc-prop-types.js';\n\ntype UseGlobalEventListenerArgs = {\n propsWithDefault: DSChatT.MessagesListProps;\n virtualListRef: React.MutableRefObject<HTMLDivElement | null>;\n onScrollStop: () => string[];\n checkPendingOnScroll: TypescriptHelpersT.GenericFunc;\n};\n\nexport const useGlobalEventListener = ({\n propsWithDefault: { onScrollEnds },\n virtualListRef,\n onScrollStop,\n checkPendingOnScroll,\n}: UseGlobalEventListenerArgs) => {\n React.useEffect(() => {\n let handlerCallback: ReturnType<typeof setTimeout>;\n const localVirtualRef = virtualListRef.current;\n const onScroll = () => {\n if (handlerCallback) clearTimeout(handlerCallback);\n handlerCallback = setTimeout(onScrollStop, 300);\n };\n if (onScrollEnds) {\n virtualListRef.current?.addEventListener('scroll', onScroll);\n }\n document.addEventListener('visibilitychange', checkPendingOnScroll);\n return () => {\n if (handlerCallback) clearTimeout(handlerCallback);\n if (onScrollEnds && localVirtualRef) localVirtualRef.removeEventListener('scroll', onScroll);\n document.removeEventListener('visibilitychange', checkPendingOnScroll);\n };\n }, [onScrollStop, onScrollEnds, checkPendingOnScroll, virtualListRef]);\n};\n"],
5
5
  "mappings": "AAAA,YAAY,WAAW;ACAvB,OAAOA,YAAW;AAWX,MAAM,yBAAyB,CAAC;AAAA,EACrC,kBAAkB,EAAE,aAAa;AAAA,EACjC;AAAA,EACA;AAAA,EACA;AACF,MAAkC;AAChC,EAAAA,OAAM,UAAU,MAAM;AACpB,QAAI;AACJ,UAAM,kBAAkB,eAAe;AACvC,UAAM,WAAW,MAAM;AACrB,UAAI,gBAAiB,cAAa,eAAe;AACjD,wBAAkB,WAAW,cAAc,GAAG;AAAA,IAChD;AACA,QAAI,cAAc;AAChB,qBAAe,SAAS,iBAAiB,UAAU,QAAQ;AAAA,IAC7D;AACA,aAAS,iBAAiB,oBAAoB,oBAAoB;AAClE,WAAO,MAAM;AACX,UAAI,gBAAiB,cAAa,eAAe;AACjD,UAAI,gBAAgB,gBAAiB,iBAAgB,oBAAoB,UAAU,QAAQ;AAC3F,eAAS,oBAAoB,oBAAoB,oBAAoB;AAAA,IACvE;AAAA,EACF,GAAG,CAAC,cAAc,cAAc,sBAAsB,cAAc,CAAC;AACvE;",
6
6
  "names": ["React"]
7
7
  }