@hh.ru/magritte-ui-chat-input 1.2.2 → 1.2.4
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.
- package/index.css +117 -117
- package/index.d.ts +10 -10
- package/index.js +1 -1
- package/index.mock.d.ts +9 -9
- package/index.mock.js +37 -36
- package/index.mock.js.map +1 -1
- package/internal/ChatInputContext.d.ts +1 -1
- package/internal/ChatInputContext.js.map +1 -1
- package/internal/ChatInputSlotButton.js +13 -13
- package/internal/ChatInputSlotButton.js.map +1 -1
- package/internal/FocusTarget.js +3 -10
- package/internal/FocusTarget.js.map +1 -1
- package/internal/ModeTransition.js +41 -11
- package/internal/ModeTransition.js.map +1 -1
- package/internal/TextInput.d.ts +1 -1
- package/internal/TextInput.js +146 -126
- package/internal/TextInput.js.map +1 -1
- package/internal/VoiceRecord.d.ts +1 -1
- package/internal/VoiceRecord.js +66 -24
- package/internal/VoiceRecord.js.map +1 -1
- package/internal/VoiceRecordIndicator.js +22 -9
- package/internal/VoiceRecordIndicator.js.map +1 -1
- package/internal/flattenFragments.js +9 -13
- package/internal/flattenFragments.js.map +1 -1
- package/internal/focusGroup.js +18 -28
- package/internal/focusGroup.js.map +1 -1
- package/internal/useHasSound.js +49 -64
- package/internal/useHasSound.js.map +1 -1
- package/internal/useObjectUrl.js +15 -21
- package/internal/useObjectUrl.js.map +1 -1
- package/internal/useRovingFocusGroup.js +102 -137
- package/internal/useRovingFocusGroup.js.map +1 -1
- package/internal/useSendOnEnter.js +44 -46
- package/internal/useSendOnEnter.js.map +1 -1
- package/internal/useVoiceRecordSession.d.ts +1 -1
- package/internal/useVoiceRecordSession.js +185 -219
- package/internal/useVoiceRecordSession.js.map +1 -1
- package/internal/voiceSlotRegistry.d.ts +1 -1
- package/internal/voiceSlotRegistry.js +3 -3
- package/internal/voiceSlotRegistry.js.map +1 -1
- package/package.json +17 -16
- package/public/ChatInput.d.ts +6 -6
- package/public/ChatInput.js +129 -73
- package/public/ChatInput.js.map +1 -1
- package/public/ChatInputChips.js +4 -4
- package/public/ChatInputChips.js.map +1 -1
- package/public/ChatInputFile.d.ts +1 -1
- package/public/ChatInputFile.js +107 -47
- package/public/ChatInputFile.js.map +1 -1
- package/public/ChatInputFiles.js +10 -3
- package/public/ChatInputFiles.js.map +1 -1
- package/public/ChatInputPreview.js +14 -9
- package/public/ChatInputPreview.js.map +1 -1
- package/public/ChatInputSendButton.d.ts +1 -1
- package/public/ChatInputSendButton.js +34 -23
- package/public/ChatInputSendButton.js.map +1 -1
- package/public/ChatInputVoiceButton.d.ts +1 -1
- package/public/ChatInputVoiceButton.js +87 -91
- package/public/ChatInputVoiceButton.js.map +1 -1
- package/public/getChatInputFileType.d.ts +1 -1
- package/public/getChatInputFileType.js +7 -10
- package/public/getChatInputFileType.js.map +1 -1
- package/styles-DofLkp5v.js +5 -0
- package/styles-DofLkp5v.js.map +1 -0
- package/styles-CHMi0EYL.js +0 -5
- package/styles-CHMi0EYL.js.map +0 -1
package/index.mock.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mock.js","sources":["../mock-component/lib/setupMockComponent.js","src/index.mock.ts"],"sourcesContent":["import { jsxs, jsx } from 'react/jsx-runtime';\nimport { forwardRef } from 'react';\n\nfunction enhance(
|
|
1
|
+
{"version":3,"file":"index.mock.js","sources":["../mock-component/lib/setupMockComponent.js","src/index.mock.ts"],"sourcesContent":["import { jsxs, jsx } from 'react/jsx-runtime';\nimport { forwardRef } from 'react';\n\nfunction enhance(mockComponent2, staticProps) {\n delete staticProps.type;\n delete staticProps.$$typeof;\n delete staticProps.render;\n for (const prop in staticProps) {\n mockComponent2[prop] = staticProps[prop];\n }\n}\nconst MockComponent = ({ style, ...props }) => (\n // eslint-disable-next-line @typescript-eslint/ban-ts-comment\n // @ts-ignore\n // eslint-disable-next-line react/jsx-no-undef\n /* @__PURE__ */ jsx(\"magritte-component-mock\", { \"style-data\": style, ...props })\n);\nconst MockComponentRenderProp = ({ style, ...props }) => (\n // eslint-disable-next-line @typescript-eslint/ban-ts-comment\n // @ts-ignore\n // eslint-disable-next-line react/jsx-no-undef\n /* @__PURE__ */ jsx(\"magritte-component-mock-render-prop\", { \"style-data\": style, ...props })\n);\nconst mockComponent = (componentName, staticProps = {}, { withChildren, innerDependence, WrapperComponent } = {}) => {\n const mockWrapper = forwardRef(({ children, ...props }, ref) => {\n const mock = (\n // eslint-disable-next-line react/jsx-no-undef\n /* @__PURE__ */ jsxs(MockComponent, { ...props, _component: componentName, children: [\n innerDependence ? innerDependence(props, ref) : null,\n withChildren ? children : null\n ] })\n );\n return WrapperComponent ? /* @__PURE__ */ jsx(WrapperComponent, { ...props, children: mock }) : mock;\n });\n staticProps && enhance(mockWrapper, staticProps);\n mockWrapper.displayName = \"magritte-ref\";\n return mockWrapper;\n};\nconst mockComponentRenderProp = (componentName, staticProps, { withChildren, innerDependence, renderArguments } = { renderArguments: [] }) => {\n const mockWrapper = ({\n render,\n children,\n ...props\n }) => /* @__PURE__ */ jsxs(MockComponentRenderProp, { ...props, _component: componentName, children: [\n innerDependence ? innerDependence(props) : null,\n withChildren ? children : null,\n render?.(...renderArguments || [])\n ] });\n staticProps && enhance(mockWrapper, staticProps);\n mockWrapper.displayName = \"magritte-component-mock-render-prop\";\n return mockWrapper;\n};\n\nexport { mockComponent, mockComponentRenderProp };\n//# sourceMappingURL=setupMockComponent.js.map\n","import { mockComponent } from '@hh.ru/magritte-ui-mock-component';\n\nexport { type ChatInputProps } from '@hh.ru/magritte-ui-chat-input/public/ChatInput';\nexport { type ChatInputVoiceRecordConfig } from '@hh.ru/magritte-ui-chat-input/types';\nexport { type ChatInputChipsProps } from '@hh.ru/magritte-ui-chat-input/public/ChatInputChips';\nexport {\n type ChatInputFileProps,\n type ChatInputFileState,\n type ChatInputFileType,\n} from '@hh.ru/magritte-ui-chat-input/public/ChatInputFile';\nexport { getChatInputFileType } from '@hh.ru/magritte-ui-chat-input/public/getChatInputFileType';\nexport { type ChatInputFilesProps } from '@hh.ru/magritte-ui-chat-input/public/ChatInputFiles';\nexport { type ChatInputPreviewProps } from '@hh.ru/magritte-ui-chat-input/public/ChatInputPreview';\nexport { type ChatInputSendButtonProps } from '@hh.ru/magritte-ui-chat-input/public/ChatInputSendButton';\nexport { type ChatInputVoiceButtonProps } from '@hh.ru/magritte-ui-chat-input/public/ChatInputVoiceButton';\n\nconst staticProps = {\n Chips: mockComponent('Chips', undefined, {\n withChildren: true,\n }),\n File: mockComponent('File'),\n Files: mockComponent('Files', undefined, {\n withChildren: true,\n }),\n Preview: mockComponent('Preview'),\n SendButton: mockComponent('SendButton'),\n VoiceButton: mockComponent('VoiceButton'),\n};\n\nexport const ChatInput = mockComponent('ChatInput', staticProps, {\n withChildren: true,\n});\n"],"names":[],"mappings":";;;;AAGA,SAAS,OAAO,CAAC,cAAc,EAAE,WAAW,EAAE;AAC9C,EAAE,OAAO,WAAW,CAAC,IAAI,CAAC;AAC1B,EAAE,OAAO,WAAW,CAAC,QAAQ,CAAC;AAC9B,EAAE,OAAO,WAAW,CAAC,MAAM,CAAC;AAC5B,EAAE,KAAK,MAAM,IAAI,IAAI,WAAW,EAAE;AAClC,IAAI,cAAc,CAAC,IAAI,CAAC,GAAG,WAAW,CAAC,IAAI,CAAC,CAAC;AAC7C,GAAG;AACH,CAAC;AACD,MAAM,aAAa,GAAG,CAAC,EAAE,KAAK,EAAE,GAAG,KAAK,EAAE;AAC1C;AACA;AACA;AACA,kBAAkB,GAAG,CAAC,yBAAyB,EAAE,EAAE,YAAY,EAAE,KAAK,EAAE,GAAG,KAAK,EAAE,CAAC;AACnF,CAAC,CAAC;AAOF,MAAM,aAAa,GAAG,CAAC,aAAa,EAAE,WAAW,GAAG,EAAE,EAAE,EAAE,YAAY,EAAE,eAAe,EAAE,gBAAgB,EAAE,GAAG,EAAE,KAAK;AACrH,EAAE,MAAM,WAAW,GAAG,UAAU,CAAC,CAAC,EAAE,QAAQ,EAAE,GAAG,KAAK,EAAE,EAAE,GAAG,KAAK;AAClE,IAAI,MAAM,IAAI;AACd;AACA,sBAAsB,IAAI,CAAC,aAAa,EAAE,EAAE,GAAG,KAAK,EAAE,UAAU,EAAE,aAAa,EAAE,QAAQ,EAAE;AAC3F,QAAQ,eAAe,GAAG,eAAe,CAAC,KAAK,EAAE,GAAG,CAAC,GAAG,IAAI;AAC5D,QAAQ,YAAY,GAAG,QAAQ,GAAG,IAAI;AACtC,OAAO,EAAE,CAAC;AACV,KAAK,CAAC;AACN,IAAI,OAAO,gBAAgB,mBAAmB,GAAG,CAAC,gBAAgB,EAAE,EAAE,GAAG,KAAK,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,GAAG,IAAI,CAAC;AACzG,GAAG,CAAC,CAAC;AACL,EAAE,WAAW,IAAI,OAAO,CAAC,WAAW,EAAE,WAAW,CAAC,CAAC;AACnD,EAAE,WAAW,CAAC,WAAW,GAAG,cAAc,CAAC;AAC3C,EAAE,OAAO,WAAW,CAAC;AACrB,CAAC;;ACrBD,MAAM,WAAc,GAAA;AAAA,EAChB,KAAA,EAAO,aAAc,CAAA,OAAA,EAAS,KAAW,CAAA,EAAA;AAAA,IACrC,YAAc,EAAA,IAAA;AAAA,GACjB,CAAA;AAAA,EACD,IAAA,EAAM,cAAc,MAAM,CAAA;AAAA,EAC1B,KAAA,EAAO,aAAc,CAAA,OAAA,EAAS,KAAW,CAAA,EAAA;AAAA,IACrC,YAAc,EAAA,IAAA;AAAA,GACjB,CAAA;AAAA,EACD,OAAA,EAAS,cAAc,SAAS,CAAA;AAAA,EAChC,UAAA,EAAY,cAAc,YAAY,CAAA;AAAA,EACtC,WAAA,EAAa,cAAc,aAAa,CAAA;AAC5C,CAAA,CAAA;AAEa,MAAA,SAAA,GAAY,aAAc,CAAA,WAAA,EAAa,WAAa,EAAA;AAAA,EAC7D,YAAc,EAAA,IAAA;AAClB,CAAC;;;;"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ChatInputContext.js","sources":["src/internal/ChatInputContext.ts"],"sourcesContent":["import { createContext, useContext } from 'react';\n\nimport { type VoiceRecordStatus } from '@hh.ru/magritte-ui-chat-input/types';\n\ninterface ChatInputContextValue {\n /** Хендлер отправки; SendButton вызывает его по клику */\n onSend?: VoidFunction;\n /** Старт записи голоса; VoiceButton вызывает его по клику. Есть внутри ChatInput\n * (снаружи контекст пустой, кнопка неактивна) */\n startVoiceRecord?: VoidFunction;\n /** Статус сессии записи — VoiceButton показывает по нему ожидание доступа к микрофону */\n voiceRecordStatus?: VoiceRecordStatus;\n}\n\nconst ChatInputContext = createContext<ChatInputContextValue>({});\n\nexport const ChatInputContextProvider = ChatInputContext.Provider;\n\nexport const useChatInputContext = (): ChatInputContextValue => useContext(ChatInputContext);\n"],"names":[],"mappings":";;AAcA,MAAM,
|
|
1
|
+
{"version":3,"file":"ChatInputContext.js","sources":["src/internal/ChatInputContext.ts"],"sourcesContent":["import { createContext, useContext } from 'react';\n\nimport { type VoiceRecordStatus } from '@hh.ru/magritte-ui-chat-input/types';\n\ninterface ChatInputContextValue {\n /** Хендлер отправки; SendButton вызывает его по клику */\n onSend?: VoidFunction;\n /** Старт записи голоса; VoiceButton вызывает его по клику. Есть внутри ChatInput\n * (снаружи контекст пустой, кнопка неактивна) */\n startVoiceRecord?: VoidFunction;\n /** Статус сессии записи — VoiceButton показывает по нему ожидание доступа к микрофону */\n voiceRecordStatus?: VoiceRecordStatus;\n}\n\nconst ChatInputContext = createContext<ChatInputContextValue>({});\n\nexport const ChatInputContextProvider = ChatInputContext.Provider;\n\nexport const useChatInputContext = (): ChatInputContextValue => useContext(ChatInputContext);\n"],"names":[],"mappings":";;AAcA,MAAM,gBAAA,GAAmB,aAAqC,CAAA,EAAE,CAAA,CAAA;AAEzD,MAAM,2BAA2B,gBAAiB,CAAA,SAAA;AAE5C,MAAA,mBAAA,GAAsB,MAA6B,UAAA,CAAW,gBAAgB;;;;"}
|
|
@@ -5,21 +5,21 @@ import { useDisabled } from '@hh.ru/magritte-common-use-disabled';
|
|
|
5
5
|
import { IconDefaultPropsContext } from '@hh.ru/magritte-ui-icon';
|
|
6
6
|
|
|
7
7
|
const ChatInputSlotButton = ({ icon, iconProps, onClick, disabled, children }) => {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
8
|
+
const handledEventRef = useRef(null);
|
|
9
|
+
const isDisabled = useDisabled() || !!disabled;
|
|
10
|
+
if (Children.toArray(children).length === 0) {
|
|
11
|
+
return icon;
|
|
12
|
+
}
|
|
13
|
+
const handleClickOnce = (event) => {
|
|
14
|
+
if (handledEventRef.current === event.nativeEvent || isDisabled) {
|
|
15
|
+
return;
|
|
12
16
|
}
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
handledEventRef.current = event.nativeEvent;
|
|
18
|
-
onClick?.(event);
|
|
19
|
-
};
|
|
20
|
-
return (jsx(IconDefaultPropsContext, { props: { ...iconProps, onClick: handleClickOnce }, children: jsx("div", { style: { display: 'contents' }, onClick: handleClickOnce, children: children }) }));
|
|
17
|
+
handledEventRef.current = event.nativeEvent;
|
|
18
|
+
onClick?.(event);
|
|
19
|
+
};
|
|
20
|
+
return /* @__PURE__ */ jsx(IconDefaultPropsContext, { props: { ...iconProps, onClick: handleClickOnce }, children: /* @__PURE__ */ jsx("div", { style: { display: "contents" }, onClick: handleClickOnce, children }) });
|
|
21
21
|
};
|
|
22
|
-
ChatInputSlotButton.displayName =
|
|
22
|
+
ChatInputSlotButton.displayName = "ChatInputSlotButton";
|
|
23
23
|
|
|
24
24
|
export { ChatInputSlotButton };
|
|
25
25
|
//# sourceMappingURL=ChatInputSlotButton.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ChatInputSlotButton.js","sources":["src/internal/ChatInputSlotButton.tsx"],"sourcesContent":["import {\n Children,\n type FC,\n type MouseEvent as ReactMouseEvent,\n type MouseEventHandler,\n type ReactElement,\n type ReactNode,\n useRef,\n} from 'react';\n\nimport { useDisabled } from '@hh.ru/magritte-common-use-disabled';\nimport { IconDefaultPropsContext } from '@hh.ru/magritte-ui-icon';\nimport { type IconProps } from '@hh.ru/magritte-ui-icon/types';\n\ninterface ChatInputSlotButtonProps {\n /** Встроенная иконка со всеми пропсами — рендерится, когда children не переданы */\n icon: ReactElement;\n /** Те же пропсы иконки — уходят в IconDefaultPropsContext, чтобы children-иконка получила\n * то же оформление/aria и (за счёт onClick) стала интерактивной кнопкой */\n iconProps: Partial<IconProps>;\n /** Обработчик клика (тот же, что в `iconProps.onClick`) — им ловим всплывший клик от\n * произвольных (не-иконочных) children */\n onClick?: MouseEventHandler<HTMLButtonElement>;\n /** Индивидуальная блокировка кнопки (в дополнение к общему `disabled` из `DisabledContext`) —\n * гасит клик по произвольным children так же, как контекстный disabled */\n disabled?: boolean;\n /** Кастомный контент вместо встроенной иконки */\n children?: ReactNode;\n}\n\nexport const ChatInputSlotButton: FC<ChatInputSlotButtonProps> = ({ icon, iconProps, onClick, disabled, children }) => {\n const handledEventRef = useRef<Event | null>(null);\n const isDisabled = useDisabled() || !!disabled;\n\n if (Children.toArray(children).length === 0) {\n return icon;\n }\n\n const handleClickOnce = (event: ReactMouseEvent<HTMLElement>) => {\n if (handledEventRef.current === event.nativeEvent || isDisabled) {\n return;\n }\n handledEventRef.current = event.nativeEvent;\n onClick?.(event as ReactMouseEvent<HTMLButtonElement>);\n };\n\n return (\n <IconDefaultPropsContext props={{ ...iconProps, onClick: handleClickOnce }}>\n <div style={{ display: 'contents' }} onClick={handleClickOnce}>\n {children}\n </div>\n </IconDefaultPropsContext>\n );\n};\n\nChatInputSlotButton.displayName = 'ChatInputSlotButton';\n"],"names":[
|
|
1
|
+
{"version":3,"file":"ChatInputSlotButton.js","sources":["src/internal/ChatInputSlotButton.tsx"],"sourcesContent":["import {\n Children,\n type FC,\n type MouseEvent as ReactMouseEvent,\n type MouseEventHandler,\n type ReactElement,\n type ReactNode,\n useRef,\n} from 'react';\n\nimport { useDisabled } from '@hh.ru/magritte-common-use-disabled';\nimport { IconDefaultPropsContext } from '@hh.ru/magritte-ui-icon';\nimport { type IconProps } from '@hh.ru/magritte-ui-icon/types';\n\ninterface ChatInputSlotButtonProps {\n /** Встроенная иконка со всеми пропсами — рендерится, когда children не переданы */\n icon: ReactElement;\n /** Те же пропсы иконки — уходят в IconDefaultPropsContext, чтобы children-иконка получила\n * то же оформление/aria и (за счёт onClick) стала интерактивной кнопкой */\n iconProps: Partial<IconProps>;\n /** Обработчик клика (тот же, что в `iconProps.onClick`) — им ловим всплывший клик от\n * произвольных (не-иконочных) children */\n onClick?: MouseEventHandler<HTMLButtonElement>;\n /** Индивидуальная блокировка кнопки (в дополнение к общему `disabled` из `DisabledContext`) —\n * гасит клик по произвольным children так же, как контекстный disabled */\n disabled?: boolean;\n /** Кастомный контент вместо встроенной иконки */\n children?: ReactNode;\n}\n\nexport const ChatInputSlotButton: FC<ChatInputSlotButtonProps> = ({ icon, iconProps, onClick, disabled, children }) => {\n const handledEventRef = useRef<Event | null>(null);\n const isDisabled = useDisabled() || !!disabled;\n\n if (Children.toArray(children).length === 0) {\n return icon;\n }\n\n const handleClickOnce = (event: ReactMouseEvent<HTMLElement>) => {\n if (handledEventRef.current === event.nativeEvent || isDisabled) {\n return;\n }\n handledEventRef.current = event.nativeEvent;\n onClick?.(event as ReactMouseEvent<HTMLButtonElement>);\n };\n\n return (\n <IconDefaultPropsContext props={{ ...iconProps, onClick: handleClickOnce }}>\n <div style={{ display: 'contents' }} onClick={handleClickOnce}>\n {children}\n </div>\n </IconDefaultPropsContext>\n );\n};\n\nChatInputSlotButton.displayName = 'ChatInputSlotButton';\n"],"names":[],"mappings":";;;;;AA8Ba,MAAA,mBAAA,GAAoD,CAAC,EAAE,IAAA,EAAM,WAAW,OAAS,EAAA,QAAA,EAAU,UAAe,KAAA;AACnH,EAAM,MAAA,eAAA,GAAkB,OAAqB,IAAI,CAAA,CAAA;AACjD,EAAA,MAAM,UAAa,GAAA,WAAA,EAAiB,IAAA,CAAC,CAAC,QAAA,CAAA;AAEtC,EAAA,IAAI,QAAS,CAAA,OAAA,CAAQ,QAAQ,CAAA,CAAE,WAAW,CAAG,EAAA;AACzC,IAAO,OAAA,IAAA,CAAA;AAAA,GACX;AAEA,EAAM,MAAA,eAAA,GAAkB,CAAC,KAAwC,KAAA;AAC7D,IAAA,IAAI,eAAgB,CAAA,OAAA,KAAY,KAAM,CAAA,WAAA,IAAe,UAAY,EAAA;AAC7D,MAAA,OAAA;AAAA,KACJ;AACA,IAAA,eAAA,CAAgB,UAAU,KAAM,CAAA,WAAA,CAAA;AAChC,IAAA,OAAA,GAAU,KAA2C,CAAA,CAAA;AAAA,GACzD,CAAA;AAEA,EAAA,2BACK,uBAAwB,EAAA,EAAA,KAAA,EAAO,EAAE,GAAG,SAAA,EAAW,SAAS,eAAgB,EAAA,EACrE,8BAAC,KAAI,EAAA,EAAA,KAAA,EAAO,EAAE,OAAS,EAAA,UAAA,IAAc,OAAS,EAAA,eAAA,EACzC,UACL,CACJ,EAAA,CAAA,CAAA;AAER,EAAA;AAEA,mBAAA,CAAoB,WAAc,GAAA,qBAAA;;;;"}
|
package/internal/FocusTarget.js
CHANGED
|
@@ -2,16 +2,9 @@ import './../index.css';
|
|
|
2
2
|
import { jsx } from 'react/jsx-runtime';
|
|
3
3
|
import { focusPriorityProps } from './focusGroup.js';
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
const
|
|
7
|
-
|
|
8
|
-
* Обёртка-маркер вокруг наших кнопок для roving-группы (useRovingFocusGroup). `display:contents` —
|
|
9
|
-
* не создаёт бокс, так что раскладка слота/блока записи не меняется, а менеджер находит кнопку
|
|
10
|
-
* внутри по data-атрибуту. Маркер вешаем на обёртку, а не на саму иконку: кастомный data-* на
|
|
11
|
-
* иконке уезжает на внутренний скрытый <svg> (не на фокусируемый <button>) и не проходит по типам.
|
|
12
|
-
*/
|
|
13
|
-
const FocusTarget = ({ priority, children }) => (jsx("span", { style: TARGET_STYLE, ...focusPriorityProps(priority), children: children }));
|
|
14
|
-
FocusTarget.displayName = 'ChatInputFocusTarget';
|
|
5
|
+
const TARGET_STYLE = { display: "contents" };
|
|
6
|
+
const FocusTarget = ({ priority, children }) => /* @__PURE__ */ jsx("span", { style: TARGET_STYLE, ...focusPriorityProps(priority), children });
|
|
7
|
+
FocusTarget.displayName = "ChatInputFocusTarget";
|
|
15
8
|
|
|
16
9
|
export { FocusTarget };
|
|
17
10
|
//# sourceMappingURL=FocusTarget.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"FocusTarget.js","sources":["src/internal/FocusTarget.tsx"],"sourcesContent":["import { type FC, type ReactNode } from 'react';\n\nimport { focusPriorityProps } from '@hh.ru/magritte-ui-chat-input/internal/focusGroup';\n\n// стиль обёртки-маркера: не создаёт бокс, дети остаются flex-элементами родительского слота\nconst TARGET_STYLE = { display: 'contents' } as const;\n\ninterface FocusTargetProps {\n /** Приоритет обхода стрелками в roving-группе (меньше — раньше), см. FOCUS_PRIORITY */\n priority: number;\n children: ReactNode;\n}\n\n/**\n * Обёртка-маркер вокруг наших кнопок для roving-группы (useRovingFocusGroup). `display:contents` —\n * не создаёт бокс, так что раскладка слота/блока записи не меняется, а менеджер находит кнопку\n * внутри по data-атрибуту. Маркер вешаем на обёртку, а не на саму иконку: кастомный data-* на\n * иконке уезжает на внутренний скрытый <svg> (не на фокусируемый <button>) и не проходит по типам.\n */\nexport const FocusTarget: FC<FocusTargetProps> = ({ priority, children }) => (\n <span style={TARGET_STYLE} {...focusPriorityProps(priority)}>\n {children}\n </span>\n);\n\nFocusTarget.displayName = 'ChatInputFocusTarget';\n"],"names":[
|
|
1
|
+
{"version":3,"file":"FocusTarget.js","sources":["src/internal/FocusTarget.tsx"],"sourcesContent":["import { type FC, type ReactNode } from 'react';\n\nimport { focusPriorityProps } from '@hh.ru/magritte-ui-chat-input/internal/focusGroup';\n\n// стиль обёртки-маркера: не создаёт бокс, дети остаются flex-элементами родительского слота\nconst TARGET_STYLE = { display: 'contents' } as const;\n\ninterface FocusTargetProps {\n /** Приоритет обхода стрелками в roving-группе (меньше — раньше), см. FOCUS_PRIORITY */\n priority: number;\n children: ReactNode;\n}\n\n/**\n * Обёртка-маркер вокруг наших кнопок для roving-группы (useRovingFocusGroup). `display:contents` —\n * не создаёт бокс, так что раскладка слота/блока записи не меняется, а менеджер находит кнопку\n * внутри по data-атрибуту. Маркер вешаем на обёртку, а не на саму иконку: кастомный data-* на\n * иконке уезжает на внутренний скрытый <svg> (не на фокусируемый <button>) и не проходит по типам.\n */\nexport const FocusTarget: FC<FocusTargetProps> = ({ priority, children }) => (\n <span style={TARGET_STYLE} {...focusPriorityProps(priority)}>\n {children}\n </span>\n);\n\nFocusTarget.displayName = 'ChatInputFocusTarget';\n"],"names":[],"mappings":";;;AAKA,MAAM,YAAA,GAAe,EAAE,OAAA,EAAS,UAAW,EAAA,CAAA;AAcpC,MAAM,WAAoC,GAAA,CAAC,EAAE,QAAA,EAAU,UAC1D,qBAAA,GAAA,CAAC,MAAK,EAAA,EAAA,KAAA,EAAO,YAAe,EAAA,GAAG,kBAAmB,CAAA,QAAQ,GACrD,QACL,EAAA,EAAA;AAGJ,WAAA,CAAY,WAAc,GAAA,sBAAA;;;;"}
|
|
@@ -2,23 +2,53 @@ import './../index.css';
|
|
|
2
2
|
import { jsxs, jsx } from 'react/jsx-runtime';
|
|
3
3
|
import { useReducedMotion, motion } from 'motion/react';
|
|
4
4
|
import CoreTokens from '@hh.ru/magritte-design-tokens/core-tokens';
|
|
5
|
-
import { s as styles } from '../styles-
|
|
5
|
+
import { s as styles } from '../styles-DofLkp5v.js';
|
|
6
6
|
|
|
7
|
-
const EASE_BASE = CoreTokens[
|
|
8
|
-
const MODE_TRANSITION_DURATION_MS = CoreTokens[
|
|
7
|
+
const EASE_BASE = CoreTokens["magritte-semantic-animation-ease-base"];
|
|
8
|
+
const MODE_TRANSITION_DURATION_MS = CoreTokens["magritte-semantic-animation-time-s"].duration;
|
|
9
9
|
const INSTANT_TRANSITION = { duration: 0 };
|
|
10
10
|
const MODE_CONTENT_TRANSITION = {
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
duration: MODE_TRANSITION_DURATION_MS / 1e3,
|
|
12
|
+
ease: EASE_BASE
|
|
13
13
|
};
|
|
14
|
-
const VISIBLE_LAYER = { opacity: 1, visibility:
|
|
15
|
-
const HIDDEN_LAYER = { opacity: 0, transitionEnd: { visibility:
|
|
14
|
+
const VISIBLE_LAYER = { opacity: 1, visibility: "visible" };
|
|
15
|
+
const HIDDEN_LAYER = { opacity: 0, transitionEnd: { visibility: "hidden" } };
|
|
16
16
|
const ModeTransition = ({ mode, text, voice }) => {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
17
|
+
const transition = useReducedMotion() ? INSTANT_TRANSITION : MODE_CONTENT_TRANSITION;
|
|
18
|
+
const isVoice = mode === "voice";
|
|
19
|
+
return /* @__PURE__ */ jsxs(
|
|
20
|
+
motion.div,
|
|
21
|
+
{
|
|
22
|
+
className: styles.modeContainer,
|
|
23
|
+
animate: { height: isVoice ? 40 : "auto" },
|
|
24
|
+
transition,
|
|
25
|
+
children: [
|
|
26
|
+
/* @__PURE__ */ jsx(
|
|
27
|
+
motion.div,
|
|
28
|
+
{
|
|
29
|
+
initial: false,
|
|
30
|
+
animate: isVoice ? HIDDEN_LAYER : VISIBLE_LAYER,
|
|
31
|
+
transition,
|
|
32
|
+
"aria-hidden": isVoice,
|
|
33
|
+
children: text
|
|
34
|
+
}
|
|
35
|
+
),
|
|
36
|
+
/* @__PURE__ */ jsx(
|
|
37
|
+
motion.div,
|
|
38
|
+
{
|
|
39
|
+
className: styles.voiceOverlay,
|
|
40
|
+
initial: false,
|
|
41
|
+
animate: isVoice ? VISIBLE_LAYER : HIDDEN_LAYER,
|
|
42
|
+
transition,
|
|
43
|
+
"aria-hidden": !isVoice,
|
|
44
|
+
children: voice
|
|
45
|
+
}
|
|
46
|
+
)
|
|
47
|
+
]
|
|
48
|
+
}
|
|
49
|
+
);
|
|
20
50
|
};
|
|
21
|
-
ModeTransition.displayName =
|
|
51
|
+
ModeTransition.displayName = "ChatInputModeTransition";
|
|
22
52
|
|
|
23
53
|
export { ModeTransition };
|
|
24
54
|
//# sourceMappingURL=ModeTransition.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ModeTransition.js","sources":["src/internal/ModeTransition.tsx"],"sourcesContent":["import { type FC, type ReactNode } from 'react';\nimport { motion, type TargetAndTransition, useReducedMotion } from 'motion/react';\n\nimport CoreTokens from '@hh.ru/magritte-design-tokens/core-tokens';\n\nimport styles from '../styles.less';\n\ntype ChatInputMode = 'text' | 'voice';\n\nexport interface ModeTransitionProps {\n /** Активный режим; смена запускает кроссфейд контента и анимацию высоты оболочки */\n mode: ChatInputMode;\n /** Контент текстового режима (сетка со слотами и вводом) */\n text: ReactNode;\n /** Контент режима записи голоса */\n voice: ReactNode;\n}\n\nconst EASE_BASE = CoreTokens['magritte-semantic-animation-ease-base'];\n\nconst MODE_TRANSITION_DURATION_MS = CoreTokens['magritte-semantic-animation-time-s'].duration;\nconst INSTANT_TRANSITION = { duration: 0 } as const;\n\nconst MODE_CONTENT_TRANSITION = {\n duration: MODE_TRANSITION_DURATION_MS / 1000,\n ease: EASE_BASE,\n} as const;\n\nconst VISIBLE_LAYER: TargetAndTransition = { opacity: 1, visibility: 'visible' };\nconst HIDDEN_LAYER: TargetAndTransition = { opacity: 0, transitionEnd: { visibility: 'hidden' } };\n\nexport const ModeTransition: FC<ModeTransitionProps> = ({ mode, text, voice }) => {\n const transition = useReducedMotion() ? INSTANT_TRANSITION : MODE_CONTENT_TRANSITION;\n const isVoice = mode === 'voice';\n\n return (\n <motion.div\n className={styles.modeContainer}\n animate={{ height: isVoice ? 40 : 'auto' }}\n transition={transition}\n >\n <motion.div\n initial={false}\n animate={isVoice ? HIDDEN_LAYER : VISIBLE_LAYER}\n transition={transition}\n aria-hidden={isVoice}\n >\n {text}\n </motion.div>\n <motion.div\n className={styles.voiceOverlay}\n initial={false}\n animate={isVoice ? VISIBLE_LAYER : HIDDEN_LAYER}\n transition={transition}\n aria-hidden={!isVoice}\n >\n {voice}\n </motion.div>\n </motion.div>\n );\n};\n\nModeTransition.displayName = 'ChatInputModeTransition';\n"],"names":[
|
|
1
|
+
{"version":3,"file":"ModeTransition.js","sources":["src/internal/ModeTransition.tsx"],"sourcesContent":["import { type FC, type ReactNode } from 'react';\nimport { motion, type TargetAndTransition, useReducedMotion } from 'motion/react';\n\nimport CoreTokens from '@hh.ru/magritte-design-tokens/core-tokens';\n\nimport styles from '../styles.less';\n\ntype ChatInputMode = 'text' | 'voice';\n\nexport interface ModeTransitionProps {\n /** Активный режим; смена запускает кроссфейд контента и анимацию высоты оболочки */\n mode: ChatInputMode;\n /** Контент текстового режима (сетка со слотами и вводом) */\n text: ReactNode;\n /** Контент режима записи голоса */\n voice: ReactNode;\n}\n\nconst EASE_BASE = CoreTokens['magritte-semantic-animation-ease-base'];\n\nconst MODE_TRANSITION_DURATION_MS = CoreTokens['magritte-semantic-animation-time-s'].duration;\nconst INSTANT_TRANSITION = { duration: 0 } as const;\n\nconst MODE_CONTENT_TRANSITION = {\n duration: MODE_TRANSITION_DURATION_MS / 1000,\n ease: EASE_BASE,\n} as const;\n\nconst VISIBLE_LAYER: TargetAndTransition = { opacity: 1, visibility: 'visible' };\nconst HIDDEN_LAYER: TargetAndTransition = { opacity: 0, transitionEnd: { visibility: 'hidden' } };\n\nexport const ModeTransition: FC<ModeTransitionProps> = ({ mode, text, voice }) => {\n const transition = useReducedMotion() ? INSTANT_TRANSITION : MODE_CONTENT_TRANSITION;\n const isVoice = mode === 'voice';\n\n return (\n <motion.div\n className={styles.modeContainer}\n animate={{ height: isVoice ? 40 : 'auto' }}\n transition={transition}\n >\n <motion.div\n initial={false}\n animate={isVoice ? HIDDEN_LAYER : VISIBLE_LAYER}\n transition={transition}\n aria-hidden={isVoice}\n >\n {text}\n </motion.div>\n <motion.div\n className={styles.voiceOverlay}\n initial={false}\n animate={isVoice ? VISIBLE_LAYER : HIDDEN_LAYER}\n transition={transition}\n aria-hidden={!isVoice}\n >\n {voice}\n </motion.div>\n </motion.div>\n );\n};\n\nModeTransition.displayName = 'ChatInputModeTransition';\n"],"names":[],"mappings":";;;;;AAkBA,MAAM,SAAA,GAAY,WAAW,uCAAuC,CAAA,CAAA;AAEpE,MAAM,2BAAA,GAA8B,UAAW,CAAA,oCAAoC,CAAE,CAAA,QAAA,CAAA;AACrF,MAAM,kBAAA,GAAqB,EAAE,QAAA,EAAU,CAAE,EAAA,CAAA;AAEzC,MAAM,uBAA0B,GAAA;AAAA,EAC5B,UAAU,2BAA8B,GAAA,GAAA;AAAA,EACxC,IAAM,EAAA,SAAA;AACV,CAAA,CAAA;AAEA,MAAM,aAAqC,GAAA,EAAE,OAAS,EAAA,CAAA,EAAG,YAAY,SAAU,EAAA,CAAA;AAC/E,MAAM,YAAA,GAAoC,EAAE,OAAS,EAAA,CAAA,EAAG,eAAe,EAAE,UAAA,EAAY,UAAW,EAAA,CAAA;AAEzF,MAAM,iBAA0C,CAAC,EAAE,IAAM,EAAA,IAAA,EAAM,OAAY,KAAA;AAC9E,EAAM,MAAA,UAAA,GAAa,gBAAiB,EAAA,GAAI,kBAAqB,GAAA,uBAAA,CAAA;AAC7D,EAAA,MAAM,UAAU,IAAS,KAAA,OAAA,CAAA;AAEzB,EACI,uBAAA,IAAA;AAAA,IAAC,MAAO,CAAA,GAAA;AAAA,IAAP;AAAA,MACG,WAAW,MAAO,CAAA,aAAA;AAAA,MAClB,OAAS,EAAA,EAAE,MAAQ,EAAA,OAAA,GAAU,KAAK,MAAO,EAAA;AAAA,MACzC,UAAA;AAAA,MAEA,QAAA,EAAA;AAAA,wBAAA,GAAA;AAAA,UAAC,MAAO,CAAA,GAAA;AAAA,UAAP;AAAA,YACG,OAAS,EAAA,KAAA;AAAA,YACT,OAAA,EAAS,UAAU,YAAe,GAAA,aAAA;AAAA,YAClC,UAAA;AAAA,YACA,aAAa,EAAA,OAAA;AAAA,YAEZ,QAAA,EAAA,IAAA;AAAA,WAAA;AAAA,SACL;AAAA,wBACA,GAAA;AAAA,UAAC,MAAO,CAAA,GAAA;AAAA,UAAP;AAAA,YACG,WAAW,MAAO,CAAA,YAAA;AAAA,YAClB,OAAS,EAAA,KAAA;AAAA,YACT,OAAA,EAAS,UAAU,aAAgB,GAAA,YAAA;AAAA,YACnC,UAAA;AAAA,YACA,eAAa,CAAC,OAAA;AAAA,YAEb,QAAA,EAAA,KAAA;AAAA,WAAA;AAAA,SACL;AAAA,OAAA;AAAA,KAAA;AAAA,GACJ,CAAA;AAER,EAAA;AAEA,cAAA,CAAe,WAAc,GAAA,yBAAA;;;;"}
|
package/internal/TextInput.d.ts
CHANGED
|
@@ -7,7 +7,7 @@ export declare const useFocus: <T extends HTMLTextAreaElement = HTMLTextAreaElem
|
|
|
7
7
|
export declare const TextInput: import("react").ForwardRefExoticComponent<Omit<TextareaHTMLAttributes<HTMLTextAreaElement>, "value"> & {
|
|
8
8
|
value: string;
|
|
9
9
|
maxHeight?: number;
|
|
10
|
-
layout:
|
|
10
|
+
layout: 'vertical' | 'horizontal';
|
|
11
11
|
onSend?: VoidFunction;
|
|
12
12
|
/** Ширина контейнера; её изменение пере-измеряет layout (перенос текста зависит от ширины) */
|
|
13
13
|
containerWidth?: number;
|
package/internal/TextInput.js
CHANGED
|
@@ -5,143 +5,163 @@ import classNames from 'classnames';
|
|
|
5
5
|
import { useMultipleRefs } from '@hh.ru/magritte-common-use-multiple-refs';
|
|
6
6
|
import { useSendOnEnter } from './useSendOnEnter.js';
|
|
7
7
|
import { useScrollbar, Scrollbar } from '@hh.ru/magritte-ui-scrollbar';
|
|
8
|
-
import { s as styles } from '../styles-
|
|
8
|
+
import { s as styles } from '../styles-DofLkp5v.js';
|
|
9
9
|
import '@hh.ru/magritte-common-keyboard';
|
|
10
10
|
|
|
11
11
|
const MIN_HEIGHT = 20;
|
|
12
|
-
// допуск на субпиксельные значения скролла
|
|
13
12
|
const HIDDEN_CONTENT_THRESHOLD = 4;
|
|
14
|
-
// вертикальные паддинги ввода - источник правды здесь, ставятся инлайном (в LESS только
|
|
15
|
-
// горизонтальные): фейд и раскладка считают по одним и тем же значениям без чтения стилей из DOM.
|
|
16
|
-
// spacious - ввод на своём ряду (не в нижнем, где контролы), base - все остальные раскладки
|
|
17
13
|
const VERTICAL_PADDING = {
|
|
18
|
-
|
|
19
|
-
|
|
14
|
+
base: { top: 8, bottom: 8 },
|
|
15
|
+
spacious: { top: 12, bottom: 16 }
|
|
20
16
|
};
|
|
21
17
|
const clamp = (value, min, max) => Math.max(Math.min(max, value), min);
|
|
22
|
-
const useTextInputHeight = ({
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
// currentState не должно быть в зависимостях, иначе зациклится обновление
|
|
73
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
74
|
-
}, [layout, maxHeight, value, inputRef, containerWidth]);
|
|
75
|
-
return currentState;
|
|
18
|
+
const useTextInputHeight = ({
|
|
19
|
+
inputRef,
|
|
20
|
+
layout,
|
|
21
|
+
maxHeight,
|
|
22
|
+
containerWidth,
|
|
23
|
+
value,
|
|
24
|
+
onScrollHeightChange
|
|
25
|
+
}) => {
|
|
26
|
+
const [currentState, setCurrentState] = useState(() => ({
|
|
27
|
+
layout,
|
|
28
|
+
height: MIN_HEIGHT
|
|
29
|
+
}));
|
|
30
|
+
const prevScrollHeightRef = useRef(null);
|
|
31
|
+
useLayoutEffect(() => {
|
|
32
|
+
const input = inputRef.current;
|
|
33
|
+
if (!input) {
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
if (prevScrollHeightRef.current !== null && prevScrollHeightRef.current !== input.scrollHeight) {
|
|
37
|
+
onScrollHeightChange?.();
|
|
38
|
+
}
|
|
39
|
+
prevScrollHeightRef.current = input.scrollHeight;
|
|
40
|
+
const measureContentHeight = (targetVertical) => {
|
|
41
|
+
input.classList.add(styles.measure);
|
|
42
|
+
input.classList.toggle(styles.layoutVertical, targetVertical);
|
|
43
|
+
const height = input.scrollHeight;
|
|
44
|
+
input.classList.toggle(styles.layoutVertical, currentState.layout === "vertical");
|
|
45
|
+
input.classList.remove(styles.measure);
|
|
46
|
+
return height;
|
|
47
|
+
};
|
|
48
|
+
let isVertical;
|
|
49
|
+
if (layout === "vertical" || value.includes("\n")) {
|
|
50
|
+
isVertical = true;
|
|
51
|
+
} else if (value === "") {
|
|
52
|
+
isVertical = false;
|
|
53
|
+
} else {
|
|
54
|
+
isVertical = measureContentHeight(false) > MIN_HEIGHT;
|
|
55
|
+
}
|
|
56
|
+
const contentHeight = isVertical ? measureContentHeight(true) : MIN_HEIGHT;
|
|
57
|
+
const newState = {
|
|
58
|
+
layout: isVertical ? "vertical" : "horizontal",
|
|
59
|
+
height: clamp(contentHeight, MIN_HEIGHT, maxHeight ?? contentHeight)
|
|
60
|
+
};
|
|
61
|
+
if (Object.keys(currentState).some(
|
|
62
|
+
(key) => currentState[key] !== newState[key]
|
|
63
|
+
)) {
|
|
64
|
+
setCurrentState(newState);
|
|
65
|
+
}
|
|
66
|
+
}, [layout, maxHeight, value, inputRef, containerWidth]);
|
|
67
|
+
return currentState;
|
|
76
68
|
};
|
|
77
69
|
const useFocus = () => {
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
70
|
+
const [isFocused, setFocused] = useState(false);
|
|
71
|
+
const focusHandler = useCallback((event) => {
|
|
72
|
+
setFocused(event.target.classList.contains("focus-visible"));
|
|
73
|
+
}, []);
|
|
74
|
+
const blurHandler = useCallback(() => {
|
|
75
|
+
setFocused(false);
|
|
76
|
+
}, []);
|
|
77
|
+
return { focusHandler, blurHandler, isFocused };
|
|
86
78
|
};
|
|
87
|
-
const TextInputComponent = ({
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
79
|
+
const TextInputComponent = ({
|
|
80
|
+
value,
|
|
81
|
+
maxHeight,
|
|
82
|
+
layout,
|
|
83
|
+
onSend,
|
|
84
|
+
containerWidth,
|
|
85
|
+
hasCentral,
|
|
86
|
+
hasBothSideControls,
|
|
87
|
+
onKeyDown,
|
|
88
|
+
onScroll,
|
|
89
|
+
className,
|
|
90
|
+
style,
|
|
91
|
+
...textareaProps
|
|
92
|
+
}, forwardedRef) => {
|
|
93
|
+
const inputRef = useRef(null);
|
|
94
|
+
const { hasVerticalScroll, verticalScrollbarProps, scrollContainerRef, resizeCallbackRef, hoverContainerCssClass } = useScrollbar({ axis: "vertical" });
|
|
95
|
+
const refCallback = useMultipleRefs(inputRef, scrollContainerRef, forwardedRef);
|
|
96
|
+
const handleScrollHeightChange = useCallback(() => {
|
|
97
|
+
resizeCallbackRef.current?.();
|
|
98
|
+
}, [resizeCallbackRef]);
|
|
99
|
+
const inputParams = useTextInputHeight({
|
|
100
|
+
inputRef,
|
|
101
|
+
layout,
|
|
102
|
+
maxHeight,
|
|
103
|
+
value,
|
|
104
|
+
containerWidth,
|
|
105
|
+
onScrollHeightChange: handleScrollHeightChange
|
|
106
|
+
});
|
|
107
|
+
const verticalPadding = hasCentral || hasBothSideControls && inputParams.layout === "vertical" ? VERTICAL_PADDING.spacious : VERTICAL_PADDING.base;
|
|
108
|
+
const [hasHiddenTop, setHasHiddenTop] = useState(false);
|
|
109
|
+
const [hasHiddenBottom, setHasHiddenBottom] = useState(false);
|
|
110
|
+
const updateFades = useCallback(() => {
|
|
111
|
+
const input = inputRef.current;
|
|
112
|
+
if (!input) {
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
const hiddenTop = input.scrollTop - verticalPadding.top;
|
|
116
|
+
const hiddenBottom = input.scrollHeight - input.scrollTop - input.clientHeight - verticalPadding.bottom;
|
|
117
|
+
setHasHiddenTop(hiddenTop > HIDDEN_CONTENT_THRESHOLD);
|
|
118
|
+
setHasHiddenBottom(hiddenBottom > HIDDEN_CONTENT_THRESHOLD);
|
|
119
|
+
}, [verticalPadding.top, verticalPadding.bottom]);
|
|
120
|
+
useLayoutEffect(() => {
|
|
121
|
+
updateFades();
|
|
122
|
+
}, [value, inputParams.height, containerWidth, updateFades]);
|
|
123
|
+
const handleKeyDown = useSendOnEnter({ onSend, onKeyDown });
|
|
124
|
+
const handleScroll = useCallback(
|
|
125
|
+
(event) => {
|
|
126
|
+
updateFades();
|
|
127
|
+
onScroll?.(event);
|
|
128
|
+
},
|
|
129
|
+
[updateFades, onScroll]
|
|
130
|
+
);
|
|
131
|
+
const { isFocused, focusHandler, blurHandler } = useFocus();
|
|
132
|
+
return /* @__PURE__ */ jsxs("div", { className: classNames(styles.textInputWrapper, hoverContainerCssClass), children: [
|
|
133
|
+
/* @__PURE__ */ jsx(
|
|
134
|
+
"textarea",
|
|
135
|
+
{
|
|
136
|
+
enterKeyHint: "enter",
|
|
137
|
+
...textareaProps,
|
|
138
|
+
onFocus: focusHandler,
|
|
139
|
+
onBlur: blurHandler,
|
|
140
|
+
className: classNames(
|
|
141
|
+
styles.textInput,
|
|
142
|
+
{
|
|
143
|
+
[styles.layoutVertical]: inputParams.layout === "vertical",
|
|
144
|
+
[styles.withTopFade]: hasHiddenTop,
|
|
145
|
+
[styles.withBottomFade]: hasHiddenBottom,
|
|
146
|
+
[styles.textInputFocused]: isFocused
|
|
147
|
+
},
|
|
148
|
+
className
|
|
149
|
+
),
|
|
150
|
+
style: {
|
|
151
|
+
...style,
|
|
152
|
+
height: inputParams.height,
|
|
153
|
+
paddingTop: verticalPadding.top,
|
|
154
|
+
paddingBottom: verticalPadding.bottom
|
|
155
|
+
},
|
|
156
|
+
ref: refCallback,
|
|
100
157
|
value,
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
const [hasHiddenTop, setHasHiddenTop] = useState(false);
|
|
109
|
-
const [hasHiddenBottom, setHasHiddenBottom] = useState(false);
|
|
110
|
-
const updateFades = useCallback(() => {
|
|
111
|
-
const input = inputRef.current;
|
|
112
|
-
if (!input) {
|
|
113
|
-
return;
|
|
114
|
-
}
|
|
115
|
-
// верхний и нижний паддинги - часть скроллируемой области, но контента в них нет:
|
|
116
|
-
// остаток скролла в их пределах не значит, что текст скрыт
|
|
117
|
-
const hiddenTop = input.scrollTop - verticalPadding.top;
|
|
118
|
-
const hiddenBottom = input.scrollHeight - input.scrollTop - input.clientHeight - verticalPadding.bottom;
|
|
119
|
-
setHasHiddenTop(hiddenTop > HIDDEN_CONTENT_THRESHOLD);
|
|
120
|
-
setHasHiddenBottom(hiddenBottom > HIDDEN_CONTENT_THRESHOLD);
|
|
121
|
-
}, [verticalPadding.top, verticalPadding.bottom]);
|
|
122
|
-
// позиция скролла отслеживается через onScroll, здесь - изменения контента, высоты и ширины
|
|
123
|
-
// контейнера: при упоре в maxHeight высота не меняется, но текст перетекает и за краями
|
|
124
|
-
// может появиться/исчезнуть скрытый контент
|
|
125
|
-
useLayoutEffect(() => {
|
|
126
|
-
updateFades();
|
|
127
|
-
}, [value, inputParams.height, containerWidth, updateFades]);
|
|
128
|
-
const handleKeyDown = useSendOnEnter({ onSend, onKeyDown });
|
|
129
|
-
const handleScroll = useCallback((event) => {
|
|
130
|
-
updateFades();
|
|
131
|
-
onScroll?.(event);
|
|
132
|
-
}, [updateFades, onScroll]);
|
|
133
|
-
const { isFocused, focusHandler, blurHandler } = useFocus();
|
|
134
|
-
return (jsxs("div", { className: classNames(styles.textInputWrapper, hoverContainerCssClass), children: [jsx("textarea", { enterKeyHint: "enter", ...textareaProps, onFocus: focusHandler, onBlur: blurHandler, className: classNames(styles.textInput, {
|
|
135
|
-
[styles.layoutVertical]: inputParams.layout === 'vertical',
|
|
136
|
-
[styles.withTopFade]: hasHiddenTop,
|
|
137
|
-
[styles.withBottomFade]: hasHiddenBottom,
|
|
138
|
-
[styles.textInputFocused]: isFocused,
|
|
139
|
-
}, className), style: {
|
|
140
|
-
...style,
|
|
141
|
-
height: inputParams.height,
|
|
142
|
-
paddingTop: verticalPadding.top,
|
|
143
|
-
paddingBottom: verticalPadding.bottom,
|
|
144
|
-
}, ref: refCallback, value: value, onKeyDown: handleKeyDown, onScroll: handleScroll, "data-qa": "text-input" }), hasVerticalScroll && (jsx("div", { className: styles.scrollbarCell, children: jsx(Scrollbar, { ...verticalScrollbarProps }) }))] }));
|
|
158
|
+
onKeyDown: handleKeyDown,
|
|
159
|
+
onScroll: handleScroll,
|
|
160
|
+
"data-qa": "text-input"
|
|
161
|
+
}
|
|
162
|
+
),
|
|
163
|
+
hasVerticalScroll && /* @__PURE__ */ jsx("div", { className: styles.scrollbarCell, children: /* @__PURE__ */ jsx(Scrollbar, { ...verticalScrollbarProps }) })
|
|
164
|
+
] });
|
|
145
165
|
};
|
|
146
166
|
const TextInput = forwardRef(TextInputComponent);
|
|
147
167
|
|