@koslibs/components 1.14.1 → 1.15.0

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/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # @koslibs/components
2
2
 
3
+ ## 1.15.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [#18](https://github.com/koslibs/components/pull/18) [`d845497`](https://github.com/koslibs/components/commit/d845497056e4d88881bb25814102fe9fb4bb9e07) Thanks [@holypower777](https://github.com/holypower777)! - Добавлен Sidepanel.onExited?: VoidFunction для уведомления после анимации ухода и фактического закрытия dialog. При reduced motion callback вызывается сразу после закрытия. Повторное открытие отменяет прежнее завершение; первоначальное open=false, ререндер и принудительное размонтирование не вызывают callback. onClose остаётся запросом закрытия.
8
+
9
+ Добавлены тесты закрытия, отмены, reduced motion и StrictMode, а также документация и пример удаления экземпляра панели менеджером приложения через onExited.
10
+
11
+ ## 1.14.2
12
+
13
+ ### Patch Changes
14
+
15
+ - [#17](https://github.com/koslibs/components/pull/17) [`b2d48a4`](https://github.com/koslibs/components/commit/b2d48a4827770b38f6320f3e760eb568b3cc6b1d) Thanks [@holypower777](https://github.com/holypower777)! - Исправлен Sidepanel Footer: primary и secondary принимают готовые JSX-элементы кнопок вместо типов компонентов. Пропсы, обработчики и ref кнопок задаются снаружи и сохраняются. primary остаётся обязательным, secondary — необязательным; gap, layout и sticky работают как прежде. Обновлены типы, Storybook и тесты.
16
+
3
17
  ## 1.14.1
4
18
 
5
19
  ### Patch Changes
@@ -1,16 +1,16 @@
1
- import { type ComponentType, type HTMLAttributes } from 'react';
1
+ import { type HTMLAttributes, type ReactElement } from 'react';
2
2
  import type { ButtonProps } from '../../../Button/index.js';
3
3
  export type FooterProps = Omit<HTMLAttributes<HTMLDivElement>, 'children' | 'dangerouslySetInnerHTML'> & {
4
4
  /**
5
- * Компонент основной кнопки. Обязателен; располагается после secondary (справа в строке).
6
- * Внешний вид и обработчики задаются внутри переданного компонента.
5
+ * JSX-элемент основной кнопки. Обязателен; располагается после secondary (справа в строке).
6
+ * Внешний вид, обработчики и ref задаются непосредственно на кнопке.
7
7
  */
8
- primary: ComponentType<ButtonProps>;
8
+ primary: ReactElement<ButtonProps>;
9
9
  /**
10
- * Компонент вторичной кнопки. Располагается перед primary (слева в строке).
10
+ * JSX-элемент вторичной кнопки. Располагается перед primary (слева в строке).
11
11
  * Не выводится, если проп не передан.
12
12
  */
13
- secondary?: ComponentType<ButtonProps>;
13
+ secondary?: ReactElement<ButtonProps>;
14
14
  /**
15
15
  * Минимальное расстояние между кнопками в пикселях из общей шкалы отступов.
16
16
  * @default 12
@@ -34,15 +34,15 @@ export type FooterProps = Omit<HTMLAttributes<HTMLDivElement>, 'children' | 'dan
34
34
  };
35
35
  export declare const Footer: import("react").ForwardRefExoticComponent<Omit<HTMLAttributes<HTMLDivElement>, "children" | "dangerouslySetInnerHTML"> & {
36
36
  /**
37
- * Компонент основной кнопки. Обязателен; располагается после secondary (справа в строке).
38
- * Внешний вид и обработчики задаются внутри переданного компонента.
37
+ * JSX-элемент основной кнопки. Обязателен; располагается после secondary (справа в строке).
38
+ * Внешний вид, обработчики и ref задаются непосредственно на кнопке.
39
39
  */
40
- primary: ComponentType<ButtonProps>;
40
+ primary: ReactElement<ButtonProps>;
41
41
  /**
42
- * Компонент вторичной кнопки. Располагается перед primary (слева в строке).
42
+ * JSX-элемент вторичной кнопки. Располагается перед primary (слева в строке).
43
43
  * Не выводится, если проп не передан.
44
44
  */
45
- secondary?: ComponentType<ButtonProps>;
45
+ secondary?: ReactElement<ButtonProps>;
46
46
  /**
47
47
  * Минимальное расстояние между кнопками в пикселях из общей шкалы отступов.
48
48
  * @default 12
@@ -1,11 +1,11 @@
1
- import { jsx, jsxs } from "react/jsx-runtime";
1
+ import { jsxs } from "react/jsx-runtime";
2
2
  import classnames from "classnames";
3
3
  import { forwardRef, useContext } from "react";
4
4
  import { useDataTestId } from "../../../../hooks/use-data-test-id.js";
5
5
  import { SidepanelContext } from "../../context.js";
6
6
  import index_module from "../../index.module.js";
7
7
  import index_module_0 from "./index.module.js";
8
- const Footer = /*#__PURE__*/ forwardRef(({ primary: Primary, secondary: Secondary, gap = 12, sticky = true, layout = 'space-between', className, dataTestId, ...restProps }, ref)=>{
8
+ const Footer = /*#__PURE__*/ forwardRef(({ primary, secondary, gap = 12, sticky = true, layout = 'space-between', className, dataTestId, ...restProps }, ref)=>{
9
9
  const { dataTestId: panelDataTestId } = useContext(SidepanelContext);
10
10
  const getDataTestId = useDataTestId(dataTestId ?? panelDataTestId);
11
11
  return /*#__PURE__*/ jsxs("div", {
@@ -16,8 +16,8 @@ const Footer = /*#__PURE__*/ forwardRef(({ primary: Primary, secondary: Secondar
16
16
  }, className),
17
17
  "data-test-id": getDataTestId('footer'),
18
18
  children: [
19
- Secondary && /*#__PURE__*/ jsx(Secondary, {}),
20
- /*#__PURE__*/ jsx(Primary, {})
19
+ secondary,
20
+ primary
21
21
  ]
22
22
  });
23
23
  });
@@ -18,6 +18,7 @@ function useDialogTransition(dialogRef, open, onExited) {
18
18
  open,
19
19
  onExited
20
20
  });
21
+ const phase = useRef('closed');
21
22
  useLayoutEffect(()=>{
22
23
  latest.current = {
23
24
  open,
@@ -29,7 +30,8 @@ function useDialogTransition(dialogRef, open, onExited) {
29
30
  ]);
30
31
  const finishClosing = useCallback(()=>{
31
32
  const dialog = dialogRef.current;
32
- if (!dialog || latest.current.open) return;
33
+ if (!dialog || latest.current.open || 'closing' !== phase.current) return;
34
+ phase.current = 'closed';
33
35
  if (dialog.open) dialog.close();
34
36
  setActive(false);
35
37
  latest.current.onExited();
@@ -42,7 +44,9 @@ function useDialogTransition(dialogRef, open, onExited) {
42
44
  if (!dialog || !view) return;
43
45
  let frame;
44
46
  let timeout;
47
+ let cancelled = false;
45
48
  if (open) {
49
+ phase.current = 'open';
46
50
  if (!dialog.open) {
47
51
  dialog.dataset.state = 'closed';
48
52
  dialog.showModal();
@@ -52,13 +56,19 @@ function useDialogTransition(dialogRef, open, onExited) {
52
56
  frame = view.requestAnimationFrame(()=>{
53
57
  dialog.dataset.state = 'open';
54
58
  });
55
- } else if (dialog.open && 'open' === dialog.dataset.state) {
56
- dialog.dataset.state = 'closed';
57
- const duration = transitionTime(dialog);
58
- if (0 === duration) finishClosing();
59
- else timeout = view.setTimeout(finishClosing, duration + 50);
60
- } else finishClosing();
59
+ } else if ('closed' !== phase.current) {
60
+ phase.current = 'closing';
61
+ if (dialog.open && 'open' === dialog.dataset.state) {
62
+ dialog.dataset.state = 'closed';
63
+ const duration = transitionTime(dialog);
64
+ if (0 === duration) finishClosing();
65
+ else timeout = view.setTimeout(()=>{
66
+ if (!cancelled) finishClosing();
67
+ }, duration + 50);
68
+ } else finishClosing();
69
+ }
61
70
  return ()=>{
71
+ cancelled = true;
62
72
  if (void 0 !== frame) view.cancelAnimationFrame(frame);
63
73
  if (void 0 !== timeout) view.clearTimeout(timeout);
64
74
  };
@@ -70,6 +80,7 @@ function useDialogTransition(dialogRef, open, onExited) {
70
80
  useLayoutEffect(()=>{
71
81
  const dialog = dialogRef.current;
72
82
  return ()=>{
83
+ phase.current = 'closed';
73
84
  if (dialog?.open) dialog.close();
74
85
  };
75
86
  }, [
@@ -1,6 +1,7 @@
1
1
  export declare const Sidepanel: import("react").ForwardRefExoticComponent<Omit<import("react").DialogHTMLAttributes<HTMLDialogElement>, "open" | "onClose" | "onCancel"> & {
2
2
  open: boolean;
3
3
  onClose?: VoidFunction;
4
+ onExited?: VoidFunction;
4
5
  escapeKeyDown?: boolean;
5
6
  backdropClick?: boolean;
6
7
  lockScroll?: boolean;
@@ -2,7 +2,7 @@ import { jsx } from "react/jsx-runtime";
2
2
  import { forwardRef, useCallback, useLayoutEffect, useState } from "react";
3
3
  import { Portal } from "../Portal/index.js";
4
4
  import { SidepanelSurface } from "./components/sidepanel-surface/index.js";
5
- const Sidepanel = /*#__PURE__*/ forwardRef(({ open, getPortalContainer, ...restProps }, ref)=>{
5
+ const Sidepanel = /*#__PURE__*/ forwardRef(({ open, onExited, getPortalContainer, ...restProps }, ref)=>{
6
6
  const [mounted, setMounted] = useState(open);
7
7
  useLayoutEffect(()=>{
8
8
  if (open) setMounted(true);
@@ -10,9 +10,10 @@ const Sidepanel = /*#__PURE__*/ forwardRef(({ open, getPortalContainer, ...restP
10
10
  open
11
11
  ]);
12
12
  const handleExited = useCallback(()=>{
13
- if (!open) setMounted(false);
13
+ setMounted(false);
14
+ onExited?.();
14
15
  }, [
15
- open
16
+ onExited
16
17
  ]);
17
18
  return mounted ? /*#__PURE__*/ jsx(Portal, {
18
19
  getPortalContainer: getPortalContainer,
@@ -10,6 +10,10 @@ export type SidepanelProps = Omit<DialogHTMLAttributes<HTMLDialogElement>, 'open
10
10
  * Запрос закрытия по крестику, Escape или бекдропу.
11
11
  */
12
12
  onClose?: VoidFunction;
13
+ /**
14
+ * Вызывается после завершения закрытия панели.
15
+ */
16
+ onExited?: VoidFunction;
13
17
  /**
14
18
  * Разрешает запрос закрытия по Escape.
15
19
  * @default true
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@koslibs/components",
3
- "version": "1.14.1",
3
+ "version": "1.15.0",
4
4
  "description": "Shared React components for koslibs projects",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",