From d2c15bd8c90f6c3c486613094423931eb055a527 Mon Sep 17 00:00:00 2001 From: Slizhevsky Vladislav Date: Wed, 24 Jun 2026 20:23:54 +0200 Subject: [PATCH] [UIK-5339][modal] rewrite component to TS --- semcore/fullscreen-modal/src/index.d.ts | 4 +- semcore/modal/package.json | 2 +- semcore/modal/src/{Modal.jsx => Modal.tsx} | 50 +++++++--- semcore/modal/src/Modal.type.ts | 93 +++++++++++++++++++ semcore/modal/src/index.d.ts | 53 ----------- semcore/modal/src/{index.js => index.ts} | 1 + .../__intergalactic-dynamic-locales.ts | 2 + semcore/modal/vite.config.ts | 2 +- semcore/wizard/src/Wizard.types.ts | 4 +- .../close_only_esc_or_close_button.tsx | 6 +- .../modal/tests/examples/basic_usage.tsx | 4 +- .../examples/with-table-and-ellipsis.tsx | 4 +- .../examples/with-table-link-and-ellipsis.tsx | 4 +- website/docs/components/modal/modal-api.md | 2 +- 14 files changed, 149 insertions(+), 82 deletions(-) rename semcore/modal/src/{Modal.jsx => Modal.tsx} (81%) create mode 100644 semcore/modal/src/Modal.type.ts delete mode 100644 semcore/modal/src/index.d.ts rename semcore/modal/src/{index.js => index.ts} (53%) diff --git a/semcore/fullscreen-modal/src/index.d.ts b/semcore/fullscreen-modal/src/index.d.ts index 31f4539393..f8ba21bb6f 100644 --- a/semcore/fullscreen-modal/src/index.d.ts +++ b/semcore/fullscreen-modal/src/index.d.ts @@ -1,10 +1,10 @@ import type { Flex, Box, BoxProps } from '@semcore/base-components'; import type Button from '@semcore/button'; import type { PropGetterFn, Intergalactic } from '@semcore/core'; -import type { ModalProps } from '@semcore/modal'; +import type { NSModal } from '@semcore/modal'; import type { Text } from '@semcore/typography'; -export type FullscreenModalProps = ModalProps & { +export type FullscreenModalProps = NSModal.Props & { /** Function that is invoked when hiding a component */ onClose?: ( trigger: 'onBackClick' | 'onCloseClick' | 'onEscape' | 'onOutsideClick', diff --git a/semcore/modal/package.json b/semcore/modal/package.json index c02865ff7a..9856f39577 100644 --- a/semcore/modal/package.json +++ b/semcore/modal/package.json @@ -9,7 +9,7 @@ "author": "UI-kit team ", "license": "MIT", "scripts": { - "build": "pnpm semcore-builder --source=js && pnpm vite build" + "build": "pnpm semcore-builder && pnpm vite build" }, "exports": { "types": "./lib/types/index.d.ts", diff --git a/semcore/modal/src/Modal.jsx b/semcore/modal/src/Modal.tsx similarity index 81% rename from semcore/modal/src/Modal.jsx rename to semcore/modal/src/Modal.tsx index a7f0fda991..b13a4e44b7 100644 --- a/semcore/modal/src/Modal.jsx +++ b/semcore/modal/src/Modal.tsx @@ -1,6 +1,8 @@ import { FadeInOut, Slide, Flex, OutsideClick, Portal, PortalProvider } from '@semcore/base-components'; import Button from '@semcore/button'; +import type { Intergalactic } from '@semcore/core'; import { createComponent, Component, sstyled, Root } from '@semcore/core'; +import type { WithI18nEnhanceProps } from '@semcore/core/lib/utils/enhances/i18nEnhance'; import i18nEnhance from '@semcore/core/lib/utils/enhances/i18nEnhance'; import { isAdvanceMode } from '@semcore/core/lib/utils/findComponent'; import fire from '@semcore/core/lib/utils/fire'; @@ -18,10 +20,18 @@ import CloseIcon from '@semcore/icon/Close/l'; import { Text } from '@semcore/typography'; import React from 'react'; +import type { NSModal } from './Modal.type'; import style from './style/modal.shadow.css'; import { localizedMessages } from './translations/__intergalactic-dynamic-locales'; -class ModalRoot extends Component { +class ModalRoot extends Component< + Intergalactic.InternalTypings.InferComponentProps, + typeof ModalRoot.enhance, + {}, + WithI18nEnhanceProps, + NSModal.State, + NSModal.DefaultProps +> { static displayName = 'Modal'; static style = style; static enhance = [ @@ -30,34 +40,37 @@ class ModalRoot extends Component { cssVariableEnhance({ variable: '--intergalactic-duration-modal', fallback: '200', + // TODO: Types are incompatible. For some reason string type isn't recognized as a valid value. + // Leave it with ts-ignore annotation. + // @ts-ignore map: Number.parseInt, prop: 'duration', }), - ]; + ] as const; static defaultProps = { closable: true, i18n: localizedMessages, locale: 'en', disablePreventScroll: false, - }; + } as const; - windowRef = React.createRef(); + windowRef = React.createRef(); - state = { hasTitle: false }; + state: NSModal.State = { hasTitle: false }; - handleKeyDown = (e) => { + handleKeyDown = (e: React.KeyboardEvent) => { if (e.key === 'Escape') { e.stopPropagation(); fire(this, 'onClose', 'onEscape', e); } }; - handleIconCloseClick = (e) => { + handleIconCloseClick = (e: React.MouseEvent) => { fire(this, 'onClose', 'onCloseClick', e); }; - handleOutsideClick = (e) => { + handleOutsideClick = (e?: React.SyntheticEvent) => { fire(this, 'onClose', 'onOutsideClick', e); // Keep focus on modal if overlay clicks don't close the modal. @@ -136,7 +149,9 @@ class ModalRoot extends Component { } } -function Window(props) { +function Window( + props: Intergalactic.InternalTypings.InferChildComponentProps, +) { const SWindow = Root; const { Children, styles, visible, closable, duration } = props; const windowRef = React.useRef(null); @@ -165,7 +180,9 @@ function Window(props) { ); } -function Overlay(props) { +function Overlay( + props: Intergalactic.InternalTypings.InferChildComponentProps, +) { const SOverlay = Root; const SOverlayContentWrapper = Flex; const { Children, styles, onOutsideClick, visible } = props; @@ -186,7 +203,9 @@ function Overlay(props) { ); } -function Close(props) { +function Close( + props: Intergalactic.InternalTypings.InferChildComponentProps, +) { const SClose = Root; const { Children, children: hasChildren, getI18nText, ghost } = props; return sstyled(props.styles)( @@ -210,7 +229,9 @@ function Close(props) { ); } -function Title(props) { +function Title( + props: Intergalactic.InternalTypings.InferChildComponentProps, +) { const { setHasTitle, styles, color } = props; const STitle = Root; @@ -225,7 +246,10 @@ function Title(props) { * * {@link https://developer.semrush.com/intergalactic/components/modal/modal-api/|API} | {@link https://developer.semrush.com/intergalactic/components/modal/modal-code/|Examples} */ -const Modal = createComponent(ModalRoot, { +const Modal = createComponent< + NSModal.Component, + typeof ModalRoot +>(ModalRoot, { Window, Overlay, Close, diff --git a/semcore/modal/src/Modal.type.ts b/semcore/modal/src/Modal.type.ts new file mode 100644 index 0000000000..85059ff904 --- /dev/null +++ b/semcore/modal/src/Modal.type.ts @@ -0,0 +1,93 @@ +import type { FadeInOutProps, SlideProps, Box, BoxProps, PortalProps } from '@semcore/base-components'; +import type Button from '@semcore/button'; +import type { PropGetterFn, Intergalactic } from '@semcore/core'; +import type { NSText } from '@semcore/typography'; +import type React from 'react'; + +import type { LocalizedMessages } from './translations/__intergalactic-dynamic-locales'; + +declare namespace NSModal { + type Props = PortalProps & + BoxProps & + FadeInOutProps & { + /** Duration of animation, ms + * @default 200 + */ + duration?: number; + /** This property is responsible for the visibility of the modal window */ + visible?: boolean; + /** Function called when the component is hidden */ + onClose?: ( + trigger: 'onOutsideClick' | 'onCloseClick' | 'onEscape', + e?: React.MouseEvent | React.KeyboardEvent, + ) => void; + /** Displaying the close button(x) in the upper-right corner of the modal dialog + * @default true + * */ + closable?: boolean; + /** + * Setting `true` disables mechanism that hides document body scrollbar when Modal is visible + * @default false + */ + disablePreventScroll?: boolean; + /** Specifies the locale for i18n support */ + locale?: string; + /** + * Props for render modal without background and paddings. Useful in carousel for example + */ + ghost?: boolean; + /** Force advanced mode */ + /** @deprecated */ + forcedAdvancedMode?: boolean; + }; + type DefaultProps = { + closable: true; + i18n: LocalizedMessages; + locale: 'en'; + disablePreventScroll: false; + }; + type Ctx = { + getOverlayProps: PropGetterFn; + getWindowProps: PropGetterFn; + getCloseProps: PropGetterFn; + }; + type State = { + hasTitle: boolean; + }; + + namespace Window { + type Props = BoxProps & SlideProps; + + type Component = Intergalactic.Component<'div', Props>; + } + + namespace Overlay { + type Component = typeof Box; + } + + namespace Close { + type Component = typeof Button; + } + + namespace Title { + type Props = NSText.Props; + + type Component = Intergalactic.Component<'div', Props>; + } + + type Component = Intergalactic.Component<'div', Props, Ctx> & { + Window: Window.Component; + Overlay: Overlay.Component; + Close: Close.Component; + Title: Title.Component; + }; +} + +/** @deprecated It will be removed in v18. */ +export type ModalProps = NSModal.Props; +/** @deprecated It will be removed in v18. */ +export type WindowProps = NSModal.Window.Props; +/** @deprecated It will be removed in v18. */ +export type ModalContext = NSModal.Ctx; + +export type { NSModal }; diff --git a/semcore/modal/src/index.d.ts b/semcore/modal/src/index.d.ts deleted file mode 100644 index 2070e1a632..0000000000 --- a/semcore/modal/src/index.d.ts +++ /dev/null @@ -1,53 +0,0 @@ -import type { FadeInOutProps, SlideProps, Box, BoxProps, PortalProps } from '@semcore/base-components'; -import type Button from '@semcore/button'; -import type { PropGetterFn, Intergalactic } from '@semcore/core'; -import type { NSText } from '@semcore/typography'; -import type React from 'react'; - -export type ModalProps = PortalProps & - BoxProps & - FadeInOutProps & { - /** Duration of animation, ms - * @default 200 - */ - duration?: number; - /** This property is responsible for the visibility of the modal window */ - visible?: boolean; - /** Function called when the component is hidden */ - onClose?: ( - trigger: 'onOutsideClick' | 'onCloseClick' | 'onEscape', - e?: React.MouseEvent | React.KeyboardEvent, - ) => void; - /** Displaying the close button(x) in the upper-right corner of the modal dialog - * @default true - * */ - closable?: boolean; - /** - * Setting `true` disables mechanism that hides document body scrollbar when Modal is visible - * @default false - */ - disablePreventScroll?: boolean; - /** Specifies the locale for i18n support */ - locale?: string; - /** - * Props for render modal without background and paddings. Useful in carousel for example - */ - ghost?: boolean; - }; - -export type WindowProps = BoxProps & SlideProps & {}; - -export type ModalContext = { - getOverlayProps: PropGetterFn; - getWindowProps: PropGetterFn; - getCloseProps: PropGetterFn; -}; - -declare const Modal: Intergalactic.Component<'div', ModalProps, ModalContext> & { - Window: Intergalactic.Component<'div', WindowProps>; - Overlay: typeof Box; - Close: typeof Button; - Title: Intergalactic.Component<'div', NSText.Props>; -}; - -export default Modal; diff --git a/semcore/modal/src/index.js b/semcore/modal/src/index.ts similarity index 53% rename from semcore/modal/src/index.js rename to semcore/modal/src/index.ts index 0690fecf67..9870bbf32b 100644 --- a/semcore/modal/src/index.js +++ b/semcore/modal/src/index.ts @@ -1 +1,2 @@ export { default } from './Modal'; +export * from './Modal.type'; diff --git a/semcore/modal/src/translations/__intergalactic-dynamic-locales.ts b/semcore/modal/src/translations/__intergalactic-dynamic-locales.ts index ccae777b73..65cdb67688 100644 --- a/semcore/modal/src/translations/__intergalactic-dynamic-locales.ts +++ b/semcore/modal/src/translations/__intergalactic-dynamic-locales.ts @@ -29,3 +29,5 @@ export const localizedMessages = { pl, sv, }; + +export type LocalizedMessages = typeof localizedMessages; diff --git a/semcore/modal/vite.config.ts b/semcore/modal/vite.config.ts index 0991a16528..90f46b4cb2 100644 --- a/semcore/modal/vite.config.ts +++ b/semcore/modal/vite.config.ts @@ -7,7 +7,7 @@ export default mergeConfig( defineConfig({ build: { lib: { - entry: './src/index.js', + entry: './src/index.ts', }, rollupOptions: { external: ['react', 'react-dom', 'react/jsx-runtime', /@babel\/runtime\/*/, /@semcore\/*/], diff --git a/semcore/wizard/src/Wizard.types.ts b/semcore/wizard/src/Wizard.types.ts index 1551c7ac95..32a69079c0 100644 --- a/semcore/wizard/src/Wizard.types.ts +++ b/semcore/wizard/src/Wizard.types.ts @@ -2,14 +2,14 @@ import type { BoxProps } from '@semcore/base-components'; import type { ButtonProps } from '@semcore/button'; import type { Intergalactic } from '@semcore/core'; import type { useI18n } from '@semcore/core/lib/utils/enhances/WithI18n'; -import type { ModalProps } from '@semcore/modal'; +import type { NSModal } from '@semcore/modal'; import type { Text, NSText } from '@semcore/typography'; import type React from 'react'; /** Ordered step position from 0 */ export type WizardStep = number; -export type WizardProps = ModalProps & { +export type WizardProps = NSModal.Props & { /** * Active step value */ diff --git a/stories/components/modal/advanced/examples/close_only_esc_or_close_button.tsx b/stories/components/modal/advanced/examples/close_only_esc_or_close_button.tsx index c9d8e54bbc..bb12064e47 100644 --- a/stories/components/modal/advanced/examples/close_only_esc_or_close_button.tsx +++ b/stories/components/modal/advanced/examples/close_only_esc_or_close_button.tsx @@ -1,5 +1,5 @@ import Button from '@semcore/ui/button'; -import type { ModalProps } from '@semcore/ui/modal'; +import type { NSModal } from '@semcore/ui/modal'; import Modal from '@semcore/ui/modal'; import type { ReactNode } from 'react'; import React, { useState, useCallback } from 'react'; @@ -15,14 +15,14 @@ export default function Demo() { ); } -type GoalSurveyModalProps = ModalProps & { +type GoalSurveyModalProps = NSModal.Props & { children?: ReactNode; }; export const GoalSurveyModal = (props: GoalSurveyModalProps) => { const { children, onClose, ...modalProps } = props; - const handleClose = useCallback>( + const handleClose = useCallback>( (trigger, e) => { if (trigger !== 'onOutsideClick') { return onClose?.(trigger, e); diff --git a/stories/components/modal/tests/examples/basic_usage.tsx b/stories/components/modal/tests/examples/basic_usage.tsx index 7055aae75f..3c134638f8 100644 --- a/stories/components/modal/tests/examples/basic_usage.tsx +++ b/stories/components/modal/tests/examples/basic_usage.tsx @@ -1,10 +1,10 @@ import Button from '@semcore/ui/button'; import Modal from '@semcore/ui/modal'; -import type { ModalProps } from '@semcore/ui/modal'; +import type { NSModal } from '@semcore/ui/modal'; import { Text } from '@semcore/ui/typography'; import React from 'react'; -type BasicModalProps = ModalProps & { +type BasicModalProps = NSModal.Props & { title?: string; content?: string; showCloseButton?: boolean; diff --git a/stories/components/modal/tests/examples/with-table-and-ellipsis.tsx b/stories/components/modal/tests/examples/with-table-and-ellipsis.tsx index cf166bf6ac..eaa0f64acf 100644 --- a/stories/components/modal/tests/examples/with-table-and-ellipsis.tsx +++ b/stories/components/modal/tests/examples/with-table-and-ellipsis.tsx @@ -4,13 +4,13 @@ import Button from '@semcore/ui/button'; import { DataTable, type DataTableProps } from '@semcore/ui/data-table'; import Link from '@semcore/ui/link'; import Modal from '@semcore/ui/modal'; -import type { ModalProps } from '@semcore/ui/modal'; +import type { NSModal } from '@semcore/ui/modal'; import Tag from '@semcore/ui/tag'; import Tooltip from '@semcore/ui/tooltip'; import { Text } from '@semcore/ui/typography'; import React from 'react'; -type BasicModalProps = ModalProps & { +type BasicModalProps = NSModal.Props & { title?: string; content?: string; showCloseButton?: boolean; diff --git a/stories/components/modal/tests/examples/with-table-link-and-ellipsis.tsx b/stories/components/modal/tests/examples/with-table-link-and-ellipsis.tsx index d5f2757c00..bac18a2170 100644 --- a/stories/components/modal/tests/examples/with-table-link-and-ellipsis.tsx +++ b/stories/components/modal/tests/examples/with-table-link-and-ellipsis.tsx @@ -4,10 +4,10 @@ import Button from '@semcore/ui/button'; import { DataTable } from '@semcore/ui/data-table'; import Link from '@semcore/ui/link'; import Modal from '@semcore/ui/modal'; -import type { ModalProps } from '@semcore/ui/modal'; +import type { NSModal } from '@semcore/ui/modal'; import { Text } from '@semcore/ui/typography'; import React from 'react'; -type WithTableLinkProps = ModalProps & { +type WithTableLinkProps = NSModal.Props & { title?: string; content?: string; showCloseButton?: boolean; diff --git a/website/docs/components/modal/modal-api.md b/website/docs/components/modal/modal-api.md index 6521eb59b9..dce02523cb 100644 --- a/website/docs/components/modal/modal-api.md +++ b/website/docs/components/modal/modal-api.md @@ -11,7 +11,7 @@ import Modal from '@semcore/ui/modal'; ; ``` - + ## Modal.Overlay