@umodeapporg/ui 0.12.0 → 0.13.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/dist/components/Form/ControlledButton.d.ts +12 -0
- package/dist/components/Form/ControlledButton.js +10 -5
- package/dist/components/Form/ControlledButton.js.map +1 -1
- package/dist/components/Form/ControlledDialog.d.ts +9 -2
- package/dist/components/Form/ControlledDialog.js +14 -8
- package/dist/components/Form/ControlledDialog.js.map +1 -1
- package/dist/components/Form/ControlledDialogPrimitives.d.ts +23 -0
- package/dist/components/Form/ControlledDialogPrimitives.js +114 -5
- package/dist/components/Form/ControlledDialogPrimitives.js.map +1 -1
- package/dist/components/Form/ControlledSelect.d.ts +11 -2
- package/dist/components/Form/ControlledSelect.js +273 -48
- package/dist/components/Form/ControlledSelect.js.map +1 -1
- package/dist/components/Form/ControlledSwitch.d.ts +9 -2
- package/dist/components/Form/ControlledSwitch.js +88 -69
- package/dist/components/Form/ControlledSwitch.js.map +1 -1
- package/dist/components/Navigation/LinkButton.d.ts +26 -0
- package/dist/components/Navigation/LinkButton.js +82 -0
- package/dist/components/Navigation/LinkButton.js.map +1 -0
- package/dist/components/UI/ControlledMenu.d.ts +6 -0
- package/dist/components/UI/ControlledMenu.js +3 -4
- package/dist/components/UI/ControlledMenu.js.map +1 -1
- package/dist/components/UI/DataTable/DataTable.js +1 -0
- package/dist/components/UI/DataTable/DataTable.js.map +1 -1
- package/dist/components/UI/DataTable/DataTableBulkEditRowsPicker.js +2 -1
- package/dist/components/UI/DataTable/DataTableBulkEditRowsPicker.js.map +1 -1
- package/dist/components/UI/DataTable/DataTableFilters.js +1 -0
- package/dist/components/UI/DataTable/DataTableFilters.js.map +1 -1
- package/dist/components/UI/DataTable/DataTableToolbar.js +2 -1
- package/dist/components/UI/DataTable/DataTableToolbar.js.map +1 -1
- package/dist/components/UI/DataTable/applyDataTableFilters.js +1 -0
- package/dist/components/UI/DataTable/applyDataTableFilters.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +50 -47
- package/dist/index.js.map +1 -1
- package/package.json +9 -2
|
@@ -9,5 +9,17 @@ interface IControlledButtonProps extends React.ButtonHTMLAttributes<HTMLButtonEl
|
|
|
9
9
|
size?: TButtonSize;
|
|
10
10
|
fullWidth?: boolean;
|
|
11
11
|
}
|
|
12
|
+
export interface IButtonClassOptions {
|
|
13
|
+
variant?: TButtonVariant;
|
|
14
|
+
tone?: TButtonTone;
|
|
15
|
+
size?: TButtonSize;
|
|
16
|
+
fullWidth?: boolean;
|
|
17
|
+
className?: string;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* The themed button recipe, for surfaces that must look like a button without being one (a
|
|
21
|
+
* link: see `LinkButton`). `className` is merged last with `twMerge`, like on the button.
|
|
22
|
+
*/
|
|
23
|
+
export declare const getButtonClassName: ({ variant, tone, size, fullWidth, className, }?: IButtonClassOptions) => string;
|
|
12
24
|
declare const ControlledButton: import('react').ForwardRefExoticComponent<IControlledButtonProps & import('react').RefAttributes<HTMLButtonElement>>;
|
|
13
25
|
export default ControlledButton;
|
|
@@ -31,6 +31,13 @@ const TONE_CLASS = {
|
|
|
31
31
|
info: "text-info-emphasis hover:bg-info-soft"
|
|
32
32
|
}
|
|
33
33
|
};
|
|
34
|
+
const getButtonClassName = ({
|
|
35
|
+
variant = "filled",
|
|
36
|
+
tone = "brand",
|
|
37
|
+
size = "md",
|
|
38
|
+
fullWidth = false,
|
|
39
|
+
className
|
|
40
|
+
} = {}) => twMerge(classNames(BASE_CLASS, SIZE_CLASS[size], TONE_CLASS[variant][tone], { "w-full": fullWidth }), className);
|
|
34
41
|
const ControlledButton = forwardRef(
|
|
35
42
|
({
|
|
36
43
|
defaultHtmlButton = false,
|
|
@@ -52,10 +59,7 @@ const ControlledButton = forwardRef(
|
|
|
52
59
|
type,
|
|
53
60
|
...rest,
|
|
54
61
|
ref,
|
|
55
|
-
className:
|
|
56
|
-
classNames(BASE_CLASS, SIZE_CLASS[size], TONE_CLASS[variant][tone], { "w-full": fullWidth }),
|
|
57
|
-
className
|
|
58
|
-
),
|
|
62
|
+
className: getButtonClassName({ variant, tone, size, fullWidth, className }),
|
|
59
63
|
children
|
|
60
64
|
}
|
|
61
65
|
);
|
|
@@ -63,6 +67,7 @@ const ControlledButton = forwardRef(
|
|
|
63
67
|
);
|
|
64
68
|
ControlledButton.displayName = "ControlledButton";
|
|
65
69
|
export {
|
|
66
|
-
ControlledButton as default
|
|
70
|
+
ControlledButton as default,
|
|
71
|
+
getButtonClassName
|
|
67
72
|
};
|
|
68
73
|
//# sourceMappingURL=ControlledButton.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ControlledButton.js","sources":["../../../src/components/Form/ControlledButton.tsx"],"sourcesContent":["import { forwardRef } from 'react';\nimport classNames from 'classnames';\nimport { twMerge } from 'tailwind-merge';\n\nexport type TButtonVariant = 'filled' | 'outlined' | 'text';\nexport type TButtonTone = 'brand' | 'neutral' | 'danger' | 'success' | 'info';\nexport type TButtonSize = 'sm' | 'md' | 'lg';\n\ninterface IControlledButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {\n /** Renders a bare <button> with no theme, for clickable surfaces that are not buttons. */\n defaultHtmlButton?: boolean;\n variant?: TButtonVariant;\n tone?: TButtonTone;\n size?: TButtonSize;\n fullWidth?: boolean;\n}\n\nconst BASE_CLASS =\n 'inline-flex select-none items-center justify-center gap-x-2 rounded-xl font-bold capitalize align-middle ' +\n 'transition-colors duration-fast focus:outline-none focus-visible:ring-2 focus-visible:ring-ring ' +\n 'disabled:pointer-events-none disabled:opacity-50';\n\nconst SIZE_CLASS: Record<TButtonSize, string> = {\n sm: 'px-4 py-2 text-xs',\n md: 'px-6 py-3 text-xs',\n lg: 'px-7 py-3.5 text-sm',\n};\n\nconst TONE_CLASS: Record<TButtonVariant, Record<TButtonTone, string>> = {\n filled: {\n brand: 'bg-brand text-brand-fg shadow-sm hover:bg-brand-hover',\n neutral: 'bg-muted-active text-foreground shadow-sm hover:bg-muted-hover',\n danger: 'bg-danger text-danger-fg shadow-sm hover:bg-danger-hover',\n success: 'bg-success text-success-fg shadow-sm hover:bg-success-hover',\n info: 'bg-info text-info-fg shadow-sm hover:bg-info-hover',\n },\n outlined: {\n brand: 'border border-brand-border text-brand-emphasis hover:bg-brand-soft',\n neutral: 'border border-border text-foreground-muted hover:bg-muted',\n danger: 'border border-danger-border text-danger-emphasis hover:bg-danger-soft',\n success: 'border border-success-border text-success-emphasis hover:bg-success-soft',\n info: 'border border-info-border text-info-emphasis hover:bg-info-soft',\n },\n text: {\n brand: 'text-brand-emphasis hover:bg-brand-soft',\n neutral: 'text-foreground-muted hover:bg-muted',\n danger: 'text-danger-emphasis hover:bg-danger-soft',\n success: 'text-success-emphasis hover:bg-success-soft',\n info: 'text-info-emphasis hover:bg-info-soft',\n },\n};\n\nconst ControlledButton = forwardRef<HTMLButtonElement, IControlledButtonProps>(\n (\n {\n defaultHtmlButton = false,\n variant = 'filled',\n tone = 'brand',\n size = 'md',\n fullWidth = false,\n type = 'button',\n className,\n children,\n ...rest\n },\n ref,\n ) => {\n if (defaultHtmlButton) {\n return (\n <button type={type} {...rest} ref={ref} className={className}>\n {children}\n </button>\n );\n }\n\n return (\n <button\n type={type}\n {...rest}\n ref={ref}\n className={
|
|
1
|
+
{"version":3,"file":"ControlledButton.js","sources":["../../../src/components/Form/ControlledButton.tsx"],"sourcesContent":["import { forwardRef } from 'react';\nimport classNames from 'classnames';\nimport { twMerge } from 'tailwind-merge';\n\nexport type TButtonVariant = 'filled' | 'outlined' | 'text';\nexport type TButtonTone = 'brand' | 'neutral' | 'danger' | 'success' | 'info';\nexport type TButtonSize = 'sm' | 'md' | 'lg';\n\ninterface IControlledButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {\n /** Renders a bare <button> with no theme, for clickable surfaces that are not buttons. */\n defaultHtmlButton?: boolean;\n variant?: TButtonVariant;\n tone?: TButtonTone;\n size?: TButtonSize;\n fullWidth?: boolean;\n}\n\nconst BASE_CLASS =\n 'inline-flex select-none items-center justify-center gap-x-2 rounded-xl font-bold capitalize align-middle ' +\n 'transition-colors duration-fast focus:outline-none focus-visible:ring-2 focus-visible:ring-ring ' +\n 'disabled:pointer-events-none disabled:opacity-50';\n\nconst SIZE_CLASS: Record<TButtonSize, string> = {\n sm: 'px-4 py-2 text-xs',\n md: 'px-6 py-3 text-xs',\n lg: 'px-7 py-3.5 text-sm',\n};\n\nconst TONE_CLASS: Record<TButtonVariant, Record<TButtonTone, string>> = {\n filled: {\n brand: 'bg-brand text-brand-fg shadow-sm hover:bg-brand-hover',\n neutral: 'bg-muted-active text-foreground shadow-sm hover:bg-muted-hover',\n danger: 'bg-danger text-danger-fg shadow-sm hover:bg-danger-hover',\n success: 'bg-success text-success-fg shadow-sm hover:bg-success-hover',\n info: 'bg-info text-info-fg shadow-sm hover:bg-info-hover',\n },\n outlined: {\n brand: 'border border-brand-border text-brand-emphasis hover:bg-brand-soft',\n neutral: 'border border-border text-foreground-muted hover:bg-muted',\n danger: 'border border-danger-border text-danger-emphasis hover:bg-danger-soft',\n success: 'border border-success-border text-success-emphasis hover:bg-success-soft',\n info: 'border border-info-border text-info-emphasis hover:bg-info-soft',\n },\n text: {\n brand: 'text-brand-emphasis hover:bg-brand-soft',\n neutral: 'text-foreground-muted hover:bg-muted',\n danger: 'text-danger-emphasis hover:bg-danger-soft',\n success: 'text-success-emphasis hover:bg-success-soft',\n info: 'text-info-emphasis hover:bg-info-soft',\n },\n};\n\nexport interface IButtonClassOptions {\n variant?: TButtonVariant;\n tone?: TButtonTone;\n size?: TButtonSize;\n fullWidth?: boolean;\n className?: string;\n}\n\n/**\n * The themed button recipe, for surfaces that must look like a button without being one (a\n * link: see `LinkButton`). `className` is merged last with `twMerge`, like on the button.\n */\nexport const getButtonClassName = ({\n variant = 'filled',\n tone = 'brand',\n size = 'md',\n fullWidth = false,\n className,\n}: IButtonClassOptions = {}): string =>\n twMerge(classNames(BASE_CLASS, SIZE_CLASS[size], TONE_CLASS[variant][tone], { 'w-full': fullWidth }), className);\n\nconst ControlledButton = forwardRef<HTMLButtonElement, IControlledButtonProps>(\n (\n {\n defaultHtmlButton = false,\n variant = 'filled',\n tone = 'brand',\n size = 'md',\n fullWidth = false,\n type = 'button',\n className,\n children,\n ...rest\n },\n ref,\n ) => {\n if (defaultHtmlButton) {\n return (\n <button type={type} {...rest} ref={ref} className={className}>\n {children}\n </button>\n );\n }\n\n return (\n <button\n type={type}\n {...rest}\n ref={ref}\n className={getButtonClassName({ variant, tone, size, fullWidth, className })}\n >\n {children}\n </button>\n );\n },\n);\n\nControlledButton.displayName = 'ControlledButton';\n\nexport default ControlledButton;\n"],"names":[],"mappings":";;;;AAiBA,MAAM,aACJ;AAIF,MAAM,aAA0C;AAAA,EAC9C,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AACN;AAEA,MAAM,aAAkE;AAAA,EACtE,QAAQ;AAAA,IACN,OAAO;AAAA,IACP,SAAS;AAAA,IACT,QAAQ;AAAA,IACR,SAAS;AAAA,IACT,MAAM;AAAA,EAAA;AAAA,EAER,UAAU;AAAA,IACR,OAAO;AAAA,IACP,SAAS;AAAA,IACT,QAAQ;AAAA,IACR,SAAS;AAAA,IACT,MAAM;AAAA,EAAA;AAAA,EAER,MAAM;AAAA,IACJ,OAAO;AAAA,IACP,SAAS;AAAA,IACT,QAAQ;AAAA,IACR,SAAS;AAAA,IACT,MAAM;AAAA,EAAA;AAEV;AAcO,MAAM,qBAAqB,CAAC;AAAA,EACjC,UAAU;AAAA,EACV,OAAO;AAAA,EACP,OAAO;AAAA,EACP,YAAY;AAAA,EACZ;AACF,IAAyB,CAAA,MACvB,QAAQ,WAAW,YAAY,WAAW,IAAI,GAAG,WAAW,OAAO,EAAE,IAAI,GAAG,EAAE,UAAU,UAAA,CAAW,GAAG,SAAS;AAEjH,MAAM,mBAAmB;AAAA,EACvB,CACE;AAAA,IACE,oBAAoB;AAAA,IACpB,UAAU;AAAA,IACV,OAAO;AAAA,IACP,OAAO;AAAA,IACP,YAAY;AAAA,IACZ,OAAO;AAAA,IACP;AAAA,IACA;AAAA,IACA,GAAG;AAAA,EAAA,GAEL,QACG;AACH,QAAI,mBAAmB;AACrB,iCACG,UAAA,EAAO,MAAa,GAAG,MAAM,KAAU,WACrC,UACH;AAAA,IAEJ;AAEA,WACE;AAAA,MAAC;AAAA,MAAA;AAAA,QACC;AAAA,QACC,GAAG;AAAA,QACJ;AAAA,QACA,WAAW,mBAAmB,EAAE,SAAS,MAAM,MAAM,WAAW,WAAW;AAAA,QAE1E;AAAA,MAAA;AAAA,IAAA;AAAA,EAGP;AACF;AAEA,iBAAiB,cAAc;"}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { TDialogSize } from './ControlledDialogPrimitives';
|
|
2
|
-
export {
|
|
1
|
+
import { TDialogInitialFocus, TDialogSize } from './ControlledDialogPrimitives';
|
|
2
|
+
export type { IDialogDismissConfig, TDialogInitialFocus, TDialogSize } from './ControlledDialogPrimitives';
|
|
3
|
+
export { ControlledDialogBody, ControlledDialogFooter, ControlledDialogHeader, ControlledDialogRoot, ControlledDialogTitle, } from './ControlledDialogPrimitives';
|
|
3
4
|
interface IDialogProps {
|
|
4
5
|
className?: string;
|
|
5
6
|
size?: TDialogSize;
|
|
@@ -7,6 +8,12 @@ interface IDialogProps {
|
|
|
7
8
|
title: React.ReactNode;
|
|
8
9
|
subtitle?: string;
|
|
9
10
|
ariaLabel?: string;
|
|
11
|
+
/**
|
|
12
|
+
* Where focus lands on open. Default `auto`: the first field or button of the body/footer
|
|
13
|
+
* (the header close button is skipped), else the panel. Pass a ref to pick the element, or
|
|
14
|
+
* `panel` for the previous behaviour.
|
|
15
|
+
*/
|
|
16
|
+
initialFocus?: TDialogInitialFocus;
|
|
10
17
|
onCancel?: () => void;
|
|
11
18
|
onConfirm?: () => void;
|
|
12
19
|
/** Fired when the user presses ESC. Omitted = falls back to `onCancel`. */
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { jsxs, jsx, Fragment } from "react/jsx-runtime";
|
|
2
|
-
import { memo, useCallback } from "react";
|
|
2
|
+
import { memo, useCallback, useId } from "react";
|
|
3
3
|
import classNames from "classnames";
|
|
4
4
|
import { useTranslation } from "react-i18next";
|
|
5
5
|
import ControlledTooltip from "../UI/ControlledTooltip.js";
|
|
6
6
|
import { MaterialSymbol } from "../UI/MaterialSymbol/index.js";
|
|
7
7
|
import { useBreakpoint } from "../../hooks/useBreakpoint.js";
|
|
8
8
|
import ControlledButton from "./ControlledButton.js";
|
|
9
|
-
import { ControlledDialogRoot, ControlledDialogHeader, ControlledDialogBody, ControlledDialogFooter } from "./ControlledDialogPrimitives.js";
|
|
9
|
+
import { ControlledDialogRoot, ControlledDialogHeader, ControlledDialogTitle, DIALOG_SKIP_INITIAL_FOCUS_PROPS, ControlledDialogBody, ControlledDialogFooter } from "./ControlledDialogPrimitives.js";
|
|
10
10
|
const DIALOG_DISMISS = { escapeKey: true, outsidePress: false };
|
|
11
11
|
const ControlledDialog = ({
|
|
12
12
|
size = "md",
|
|
@@ -15,6 +15,7 @@ const ControlledDialog = ({
|
|
|
15
15
|
title,
|
|
16
16
|
subtitle,
|
|
17
17
|
ariaLabel,
|
|
18
|
+
initialFocus,
|
|
18
19
|
onCancel,
|
|
19
20
|
onConfirm,
|
|
20
21
|
onDismiss,
|
|
@@ -67,7 +68,9 @@ const ControlledDialog = ({
|
|
|
67
68
|
const isDangerConfirm = confirmTone === "danger" || confirmTone === void 0 && (confirmLabel === "Excluir" || confirmLabel === "Suspender" || confirmLabel === "Cancelar Item");
|
|
68
69
|
const hasHeaderTitle = title !== null && title !== void 0 && title !== "" && !(typeof title === "string" && title.trim() === "");
|
|
69
70
|
const shouldRenderHeader = showHeader && hasHeaderTitle;
|
|
70
|
-
const accessibleName = ariaLabel ?? (typeof title === "string" ? title : void 0);
|
|
71
|
+
const accessibleName = ariaLabel ?? (!shouldRenderHeader && typeof title === "string" ? title : void 0);
|
|
72
|
+
const subtitleId = useId();
|
|
73
|
+
const describedBy = shouldRenderHeader && subtitle ? subtitleId : void 0;
|
|
71
74
|
const usesDefaultScrollWrapper = !scrollContainerRef && !onScroll && !fitBody;
|
|
72
75
|
return /* @__PURE__ */ jsxs(
|
|
73
76
|
ControlledDialogRoot,
|
|
@@ -76,7 +79,9 @@ const ControlledDialog = ({
|
|
|
76
79
|
handler: handleDismiss,
|
|
77
80
|
dismiss: DIALOG_DISMISS,
|
|
78
81
|
"aria-label": accessibleName,
|
|
82
|
+
"aria-describedby": describedBy,
|
|
79
83
|
"aria-modal": true,
|
|
84
|
+
initialFocus,
|
|
80
85
|
size,
|
|
81
86
|
className: classNames(
|
|
82
87
|
"zIndexMax focus:shadow-none focus:outline-none focus:ring-0 focus:ring-offset-0",
|
|
@@ -95,10 +100,10 @@ const ControlledDialog = ({
|
|
|
95
100
|
children: [
|
|
96
101
|
shouldRenderHeader && /* @__PURE__ */ jsxs(ControlledDialogHeader, { className: "flex items-center justify-between gap-3", children: [
|
|
97
102
|
/* @__PURE__ */ jsxs("div", { className: "flex min-w-0 flex-1 flex-col", children: [
|
|
98
|
-
/* @__PURE__ */ jsx(
|
|
99
|
-
subtitle && /* @__PURE__ */ jsx("p", { className: "text-xs font-normal opacity-70", children: subtitle })
|
|
103
|
+
/* @__PURE__ */ jsx(ControlledDialogTitle, { children: title }),
|
|
104
|
+
subtitle && /* @__PURE__ */ jsx("p", { id: subtitleId, className: "text-xs font-normal opacity-70", children: subtitle })
|
|
100
105
|
] }),
|
|
101
|
-
showClose && /* @__PURE__ */ jsx(ControlledTooltip, { content: t("actions.close"), className: "z-[9999]", children: /* @__PURE__ */ jsx(
|
|
106
|
+
showClose && /* @__PURE__ */ jsx("div", { ...DIALOG_SKIP_INITIAL_FOCUS_PROPS, className: "flex shrink-0", children: /* @__PURE__ */ jsx(ControlledTooltip, { content: t("actions.close"), className: "z-[9999]", children: /* @__PURE__ */ jsx(
|
|
102
107
|
ControlledButton,
|
|
103
108
|
{
|
|
104
109
|
defaultHtmlButton: true,
|
|
@@ -107,7 +112,7 @@ const ControlledDialog = ({
|
|
|
107
112
|
className: "flex size-8 shrink-0 cursor-pointer items-center justify-center rounded-full text-foreground-subtle transition-colors hover:bg-muted-hover hover:text-foreground-muted",
|
|
108
113
|
children: /* @__PURE__ */ jsx(MaterialSymbol, { icon: "close", size: 20 })
|
|
109
114
|
}
|
|
110
|
-
) })
|
|
115
|
+
) }) })
|
|
111
116
|
] }),
|
|
112
117
|
/* @__PURE__ */ jsxs(
|
|
113
118
|
ControlledDialogBody,
|
|
@@ -119,7 +124,7 @@ const ControlledDialog = ({
|
|
|
119
124
|
bodyClass
|
|
120
125
|
),
|
|
121
126
|
children: [
|
|
122
|
-
!shouldRenderHeader && showClose && /* @__PURE__ */ jsx("div", { className: "absolute right-3 top-3 z-10", children: /* @__PURE__ */ jsx(ControlledTooltip, { content: t("actions.close"), className: "z-[9999]", children: /* @__PURE__ */ jsx(
|
|
127
|
+
!shouldRenderHeader && showClose && /* @__PURE__ */ jsx("div", { ...DIALOG_SKIP_INITIAL_FOCUS_PROPS, className: "absolute right-3 top-3 z-10", children: /* @__PURE__ */ jsx(ControlledTooltip, { content: t("actions.close"), className: "z-[9999]", children: /* @__PURE__ */ jsx(
|
|
123
128
|
ControlledButton,
|
|
124
129
|
{
|
|
125
130
|
defaultHtmlButton: true,
|
|
@@ -180,6 +185,7 @@ export {
|
|
|
180
185
|
ControlledDialogFooter,
|
|
181
186
|
ControlledDialogHeader,
|
|
182
187
|
ControlledDialogRoot,
|
|
188
|
+
ControlledDialogTitle,
|
|
183
189
|
ControlledDialog$1 as default
|
|
184
190
|
};
|
|
185
191
|
//# sourceMappingURL=ControlledDialog.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ControlledDialog.js","sources":["../../../src/components/Form/ControlledDialog.tsx"],"sourcesContent":["import { memo, useCallback } from 'react';\nimport classNames from 'classnames';\nimport { useTranslation } from 'react-i18next';\n\nimport ControlledTooltip from '@/components/UI/ControlledTooltip';\nimport { MaterialSymbol } from '@/components/UI/MaterialSymbol';\nimport { useBreakpoint } from '@/hooks/useBreakpoint';\n\nimport ControlledButton from './ControlledButton';\nimport {\n ControlledDialogBody,\n ControlledDialogFooter,\n ControlledDialogHeader,\n ControlledDialogRoot,\n TDialogSize,\n} from './ControlledDialogPrimitives';\n\nexport {\n ControlledDialogBody,\n ControlledDialogFooter,\n ControlledDialogHeader,\n ControlledDialogRoot,\n} from './ControlledDialogPrimitives';\n\ninterface IDialogProps {\n className?: string;\n size?: TDialogSize;\n open: boolean;\n title: React.ReactNode;\n subtitle?: string;\n ariaLabel?: string;\n\n onCancel?: () => void;\n onConfirm?: () => void;\n /** Fired when the user presses ESC. Omitted = falls back to `onCancel`. */\n onDismiss?: () => void;\n\n showClose?: boolean;\n /** When false, never renders the header bar (close moves into the body). Default true. */\n showHeader?: boolean;\n showCancel?: boolean;\n cancelLabel?: string;\n cancelButtonClassName?: string;\n showConfirm?: boolean;\n confirmLabel?: string;\n /** Cor do botão de confirmação. `danger` para ações destrutivas. */\n confirmTone?: 'default' | 'danger';\n disableCancel?: boolean;\n disableConfirm?: boolean;\n /** While true, blocks confirm/cancel/close/dismiss and shows a spinner on the confirm button. */\n loading?: boolean;\n\n bodyClass?: string;\n footerClass?: string;\n /**\n * Skip the default `max-h-[70vh]` scroll wrapper around `children`. Use when the\n * dialog body manages its own height/scroll (e.g. a fixed-size editor whose inner\n * region scrolls), so there is a single content wrapper instead of two.\n */\n fitBody?: boolean;\n useFooter?: boolean;\n\n leftButtonSlot?: React.ReactNode;\n\n style?: React.CSSProperties;\n scrollContainerRef?: React.Ref<HTMLDivElement>;\n onScroll?: React.UIEventHandler<HTMLDivElement>;\n children: React.ReactNode;\n}\n\nconst DIALOG_DISMISS = { escapeKey: true, outsidePress: false };\n\nconst ControlledDialog: React.FC<IDialogProps> = ({\n size = 'md',\n className,\n open,\n title,\n subtitle,\n ariaLabel,\n\n onCancel,\n onConfirm,\n onDismiss,\n\n showClose = true,\n showHeader = true,\n showCancel = true,\n cancelLabel = 'Cancelar',\n cancelButtonClassName,\n showConfirm = true,\n confirmLabel = 'Confirmar',\n confirmTone,\n disableCancel = false,\n disableConfirm = false,\n loading = false,\n\n bodyClass,\n footerClass,\n fitBody = false,\n useFooter = true,\n\n leftButtonSlot,\n\n style,\n scrollContainerRef,\n onScroll,\n children,\n}) => {\n const { t } = useTranslation('ui');\n const { isMobile } = useBreakpoint();\n const handleDialogClick = useCallback((e: React.MouseEvent<HTMLDivElement>) => {\n e.stopPropagation();\n }, []);\n\n const handleCloseClick = useCallback(() => {\n if (loading) return;\n onCancel?.();\n }, [onCancel, loading]);\n\n const handleCancelClick = useCallback(() => {\n if (loading) return;\n onCancel?.();\n }, [onCancel, loading]);\n\n const handleConfirmClick = useCallback(() => {\n if (loading) return;\n onConfirm?.();\n }, [onConfirm, loading]);\n\n const handleDismiss = useCallback(() => {\n if (loading) return;\n if (onDismiss) {\n onDismiss();\n return;\n }\n onCancel?.();\n }, [onCancel, onDismiss, loading]);\n\n const isDangerConfirm =\n confirmTone === 'danger' ||\n (confirmTone === undefined &&\n (confirmLabel === 'Excluir' || confirmLabel === 'Suspender' || confirmLabel === 'Cancelar Item'));\n\n const hasHeaderTitle =\n title !== null && title !== undefined && title !== '' && !(typeof title === 'string' && title.trim() === '');\n const shouldRenderHeader = showHeader && hasHeaderTitle;\n const accessibleName = ariaLabel ?? (typeof title === 'string' ? title : undefined);\n // The wrapper only clips when it is a flex child whose height the body can shrink: `h-full`\n // resolves to `auto` here (the panel has a max height, not a definite one), so content used to\n // overflow underneath the footer on short viewports.\n const usesDefaultScrollWrapper = !scrollContainerRef && !onScroll && !fitBody;\n\n return (\n <ControlledDialogRoot\n open={open}\n handler={handleDismiss}\n dismiss={DIALOG_DISMISS}\n aria-label={accessibleName}\n aria-modal={true}\n size={size}\n className={classNames(\n 'zIndexMax focus:shadow-none focus:outline-none focus:ring-0 focus:ring-offset-0',\n // Material Tailwind sizes the dialog in viewport percentages, so on a phone\n // an `xs`/`sm` dialog is still only 80% wide while its content (a form row,\n // a pair of buttons) needs everything it can get. Below the tablet edge the\n // semantic size stops mattering and every dialog goes near-full-bleed.\n // Gated in JS rather than with a `max-tablet:` variant because Tailwind does\n // not emit `max-*` variants for this config (withMT declares `lg-max` in\n // object form, which turns them off). Desktop keeps MT's sizing untouched.\n isMobile && '!w-[calc(100vw-2rem)] !min-w-[calc(100vw-2rem)] !max-w-[calc(100vw-2rem)]',\n className,\n )}\n style={style}\n onClick={handleDialogClick}\n >\n {shouldRenderHeader && (\n <ControlledDialogHeader className=\"flex items-center justify-between gap-3\">\n <div className=\"flex min-w-0 flex-1 flex-col\">\n <div>{title}</div>\n {subtitle && <p className=\"text-xs font-normal opacity-70\">{subtitle}</p>}\n </div>\n {showClose && (\n <ControlledTooltip content={t('actions.close')} className=\"z-[9999]\">\n <ControlledButton\n defaultHtmlButton\n aria-label={t('actions.close')}\n onClick={handleCloseClick}\n className=\"flex size-8 shrink-0 cursor-pointer items-center justify-center rounded-full text-foreground-subtle transition-colors hover:bg-muted-hover hover:text-foreground-muted\"\n >\n <MaterialSymbol icon=\"close\" size={20} />\n </ControlledButton>\n </ControlledTooltip>\n )}\n </ControlledDialogHeader>\n )}\n <ControlledDialogBody\n className={classNames(\n 'relative p-2',\n !shouldRenderHeader && 'pt-3',\n usesDefaultScrollWrapper && 'flex flex-col',\n bodyClass,\n )}\n >\n {!shouldRenderHeader && showClose && (\n <div className=\"absolute right-3 top-3 z-10\">\n <ControlledTooltip content={t('actions.close')} className=\"z-[9999]\">\n <ControlledButton\n defaultHtmlButton\n aria-label={t('actions.close')}\n onClick={handleCloseClick}\n className=\"flex size-8 cursor-pointer items-center justify-center rounded-full text-foreground-subtle transition-colors hover:bg-muted-hover hover:text-foreground-muted\"\n >\n <MaterialSymbol icon=\"close\" size={20} />\n </ControlledButton>\n </ControlledTooltip>\n </div>\n )}\n {usesDefaultScrollWrapper ? (\n <div className=\"max-h-[70vh] min-h-0 w-full flex-auto overflow-y-auto scroll-smooth\">{children}</div>\n ) : (\n children\n )}\n </ControlledDialogBody>\n\n {useFooter && (\n <ControlledDialogFooter\n className={classNames('flex w-full shrink-0 flex-wrap items-center justify-between gap-y-2', footerClass)}\n >\n {leftButtonSlot ? <>{leftButtonSlot}</> : <div />}\n\n <div className=\"flex flex-1 items-center justify-end gap-x-2\">\n {showCancel && (\n <ControlledButton\n variant=\"outlined\"\n className={classNames('focus:shadow-none focus:outline-none focus:ring-0', cancelButtonClassName)}\n onClick={handleCancelClick}\n disabled={disableCancel || loading}\n >\n <span>{cancelLabel}</span>\n </ControlledButton>\n )}\n\n {showConfirm && (\n <ControlledButton\n variant=\"filled\"\n className={classNames('hover:shadow-none focus:outline-none focus:ring-0', {\n 'bg-brand hover:bg-brand-hover': !isDangerConfirm,\n 'bg-danger hover:bg-danger-soft': isDangerConfirm,\n })}\n onClick={handleConfirmClick}\n disabled={disableConfirm || loading}\n >\n <span className=\"flex items-center gap-x-1.5\">\n {loading && <MaterialSymbol icon=\"progress_activity\" size={16} className=\"animate-spin\" />}\n <span>{confirmLabel}</span>\n </span>\n </ControlledButton>\n )}\n </div>\n </ControlledDialogFooter>\n )}\n </ControlledDialogRoot>\n );\n};\n\nexport default memo(ControlledDialog);\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"ControlledDialog.js","sources":["../../../src/components/Form/ControlledDialog.tsx"],"sourcesContent":["import { memo, useCallback, useId } from 'react';\nimport classNames from 'classnames';\nimport { useTranslation } from 'react-i18next';\n\nimport ControlledTooltip from '@/components/UI/ControlledTooltip';\nimport { MaterialSymbol } from '@/components/UI/MaterialSymbol';\nimport { useBreakpoint } from '@/hooks/useBreakpoint';\n\nimport ControlledButton from './ControlledButton';\nimport {\n ControlledDialogBody,\n ControlledDialogFooter,\n ControlledDialogHeader,\n ControlledDialogRoot,\n ControlledDialogTitle,\n DIALOG_SKIP_INITIAL_FOCUS_PROPS,\n TDialogInitialFocus,\n TDialogSize,\n} from './ControlledDialogPrimitives';\n\nexport type { IDialogDismissConfig, TDialogInitialFocus, TDialogSize } from './ControlledDialogPrimitives';\nexport {\n ControlledDialogBody,\n ControlledDialogFooter,\n ControlledDialogHeader,\n ControlledDialogRoot,\n ControlledDialogTitle,\n} from './ControlledDialogPrimitives';\n\ninterface IDialogProps {\n className?: string;\n size?: TDialogSize;\n open: boolean;\n title: React.ReactNode;\n subtitle?: string;\n ariaLabel?: string;\n /**\n * Where focus lands on open. Default `auto`: the first field or button of the body/footer\n * (the header close button is skipped), else the panel. Pass a ref to pick the element, or\n * `panel` for the previous behaviour.\n */\n initialFocus?: TDialogInitialFocus;\n\n onCancel?: () => void;\n onConfirm?: () => void;\n /** Fired when the user presses ESC. Omitted = falls back to `onCancel`. */\n onDismiss?: () => void;\n\n showClose?: boolean;\n /** When false, never renders the header bar (close moves into the body). Default true. */\n showHeader?: boolean;\n showCancel?: boolean;\n cancelLabel?: string;\n cancelButtonClassName?: string;\n showConfirm?: boolean;\n confirmLabel?: string;\n /** Cor do botão de confirmação. `danger` para ações destrutivas. */\n confirmTone?: 'default' | 'danger';\n disableCancel?: boolean;\n disableConfirm?: boolean;\n /** While true, blocks confirm/cancel/close/dismiss and shows a spinner on the confirm button. */\n loading?: boolean;\n\n bodyClass?: string;\n footerClass?: string;\n /**\n * Skip the default `max-h-[70vh]` scroll wrapper around `children`. Use when the\n * dialog body manages its own height/scroll (e.g. a fixed-size editor whose inner\n * region scrolls), so there is a single content wrapper instead of two.\n */\n fitBody?: boolean;\n useFooter?: boolean;\n\n leftButtonSlot?: React.ReactNode;\n\n style?: React.CSSProperties;\n scrollContainerRef?: React.Ref<HTMLDivElement>;\n onScroll?: React.UIEventHandler<HTMLDivElement>;\n children: React.ReactNode;\n}\n\nconst DIALOG_DISMISS = { escapeKey: true, outsidePress: false };\n\nconst ControlledDialog: React.FC<IDialogProps> = ({\n size = 'md',\n className,\n open,\n title,\n subtitle,\n ariaLabel,\n initialFocus,\n\n onCancel,\n onConfirm,\n onDismiss,\n\n showClose = true,\n showHeader = true,\n showCancel = true,\n cancelLabel = 'Cancelar',\n cancelButtonClassName,\n showConfirm = true,\n confirmLabel = 'Confirmar',\n confirmTone,\n disableCancel = false,\n disableConfirm = false,\n loading = false,\n\n bodyClass,\n footerClass,\n fitBody = false,\n useFooter = true,\n\n leftButtonSlot,\n\n style,\n scrollContainerRef,\n onScroll,\n children,\n}) => {\n const { t } = useTranslation('ui');\n const { isMobile } = useBreakpoint();\n const handleDialogClick = useCallback((e: React.MouseEvent<HTMLDivElement>) => {\n e.stopPropagation();\n }, []);\n\n const handleCloseClick = useCallback(() => {\n if (loading) return;\n onCancel?.();\n }, [onCancel, loading]);\n\n const handleCancelClick = useCallback(() => {\n if (loading) return;\n onCancel?.();\n }, [onCancel, loading]);\n\n const handleConfirmClick = useCallback(() => {\n if (loading) return;\n onConfirm?.();\n }, [onConfirm, loading]);\n\n const handleDismiss = useCallback(() => {\n if (loading) return;\n if (onDismiss) {\n onDismiss();\n return;\n }\n onCancel?.();\n }, [onCancel, onDismiss, loading]);\n\n const isDangerConfirm =\n confirmTone === 'danger' ||\n (confirmTone === undefined &&\n (confirmLabel === 'Excluir' || confirmLabel === 'Suspender' || confirmLabel === 'Cancelar Item'));\n\n const hasHeaderTitle =\n title !== null && title !== undefined && title !== '' && !(typeof title === 'string' && title.trim() === '');\n const shouldRenderHeader = showHeader && hasHeaderTitle;\n // With a rendered header the title element names the dialog (aria-labelledby, wired by the\n // root); without one, a string title still becomes the aria-label.\n const accessibleName = ariaLabel ?? (!shouldRenderHeader && typeof title === 'string' ? title : undefined);\n const subtitleId = useId();\n const describedBy = shouldRenderHeader && subtitle ? subtitleId : undefined;\n // The wrapper only clips when it is a flex child whose height the body can shrink: `h-full`\n // resolves to `auto` here (the panel has a max height, not a definite one), so content used to\n // overflow underneath the footer on short viewports.\n const usesDefaultScrollWrapper = !scrollContainerRef && !onScroll && !fitBody;\n\n return (\n <ControlledDialogRoot\n open={open}\n handler={handleDismiss}\n dismiss={DIALOG_DISMISS}\n aria-label={accessibleName}\n aria-describedby={describedBy}\n aria-modal={true}\n initialFocus={initialFocus}\n size={size}\n className={classNames(\n 'zIndexMax focus:shadow-none focus:outline-none focus:ring-0 focus:ring-offset-0',\n // Material Tailwind sizes the dialog in viewport percentages, so on a phone\n // an `xs`/`sm` dialog is still only 80% wide while its content (a form row,\n // a pair of buttons) needs everything it can get. Below the tablet edge the\n // semantic size stops mattering and every dialog goes near-full-bleed.\n // Gated in JS rather than with a `max-tablet:` variant because Tailwind does\n // not emit `max-*` variants for this config (withMT declares `lg-max` in\n // object form, which turns them off). Desktop keeps MT's sizing untouched.\n isMobile && '!w-[calc(100vw-2rem)] !min-w-[calc(100vw-2rem)] !max-w-[calc(100vw-2rem)]',\n className,\n )}\n style={style}\n onClick={handleDialogClick}\n >\n {shouldRenderHeader && (\n <ControlledDialogHeader className=\"flex items-center justify-between gap-3\">\n <div className=\"flex min-w-0 flex-1 flex-col\">\n <ControlledDialogTitle>{title}</ControlledDialogTitle>\n {subtitle && (\n <p id={subtitleId} className=\"text-xs font-normal opacity-70\">\n {subtitle}\n </p>\n )}\n </div>\n {showClose && (\n <div {...DIALOG_SKIP_INITIAL_FOCUS_PROPS} className=\"flex shrink-0\">\n <ControlledTooltip content={t('actions.close')} className=\"z-[9999]\">\n <ControlledButton\n defaultHtmlButton\n aria-label={t('actions.close')}\n onClick={handleCloseClick}\n className=\"flex size-8 shrink-0 cursor-pointer items-center justify-center rounded-full text-foreground-subtle transition-colors hover:bg-muted-hover hover:text-foreground-muted\"\n >\n <MaterialSymbol icon=\"close\" size={20} />\n </ControlledButton>\n </ControlledTooltip>\n </div>\n )}\n </ControlledDialogHeader>\n )}\n <ControlledDialogBody\n className={classNames(\n 'relative p-2',\n !shouldRenderHeader && 'pt-3',\n usesDefaultScrollWrapper && 'flex flex-col',\n bodyClass,\n )}\n >\n {!shouldRenderHeader && showClose && (\n <div {...DIALOG_SKIP_INITIAL_FOCUS_PROPS} className=\"absolute right-3 top-3 z-10\">\n <ControlledTooltip content={t('actions.close')} className=\"z-[9999]\">\n <ControlledButton\n defaultHtmlButton\n aria-label={t('actions.close')}\n onClick={handleCloseClick}\n className=\"flex size-8 cursor-pointer items-center justify-center rounded-full text-foreground-subtle transition-colors hover:bg-muted-hover hover:text-foreground-muted\"\n >\n <MaterialSymbol icon=\"close\" size={20} />\n </ControlledButton>\n </ControlledTooltip>\n </div>\n )}\n {usesDefaultScrollWrapper ? (\n <div className=\"max-h-[70vh] min-h-0 w-full flex-auto overflow-y-auto scroll-smooth\">{children}</div>\n ) : (\n children\n )}\n </ControlledDialogBody>\n\n {useFooter && (\n <ControlledDialogFooter\n className={classNames('flex w-full shrink-0 flex-wrap items-center justify-between gap-y-2', footerClass)}\n >\n {leftButtonSlot ? <>{leftButtonSlot}</> : <div />}\n\n <div className=\"flex flex-1 items-center justify-end gap-x-2\">\n {showCancel && (\n <ControlledButton\n variant=\"outlined\"\n className={classNames('focus:shadow-none focus:outline-none focus:ring-0', cancelButtonClassName)}\n onClick={handleCancelClick}\n disabled={disableCancel || loading}\n >\n <span>{cancelLabel}</span>\n </ControlledButton>\n )}\n\n {showConfirm && (\n <ControlledButton\n variant=\"filled\"\n className={classNames('hover:shadow-none focus:outline-none focus:ring-0', {\n 'bg-brand hover:bg-brand-hover': !isDangerConfirm,\n 'bg-danger hover:bg-danger-soft': isDangerConfirm,\n })}\n onClick={handleConfirmClick}\n disabled={disableConfirm || loading}\n >\n <span className=\"flex items-center gap-x-1.5\">\n {loading && <MaterialSymbol icon=\"progress_activity\" size={16} className=\"animate-spin\" />}\n <span>{confirmLabel}</span>\n </span>\n </ControlledButton>\n )}\n </div>\n </ControlledDialogFooter>\n )}\n </ControlledDialogRoot>\n );\n};\n\nexport default memo(ControlledDialog);\n"],"names":[],"mappings":"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"}
|
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
import { default as React } from 'react';
|
|
2
2
|
export type TDialogSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'xxl';
|
|
3
|
+
/**
|
|
4
|
+
* Where focus lands when the dialog opens. `auto` (default): an element that asked for it
|
|
5
|
+
* (`autoFocus` / `data-autofocus`), else the first tabbable element outside a
|
|
6
|
+
* `data-dialog-initial-focus="skip"` zone, else the panel itself. `panel`: the panel, the
|
|
7
|
+
* pre-0.13 behaviour. A ref: that element.
|
|
8
|
+
*/
|
|
9
|
+
export type TDialogInitialFocus = 'auto' | 'panel' | React.RefObject<HTMLElement | null>;
|
|
3
10
|
export interface IDialogDismissConfig {
|
|
4
11
|
enabled?: boolean;
|
|
5
12
|
escapeKey?: boolean;
|
|
@@ -13,8 +20,14 @@ interface IControlledDialogRootProps extends TDivPropsWithoutMotionConflicts {
|
|
|
13
20
|
dismiss?: IDialogDismissConfig;
|
|
14
21
|
className?: string;
|
|
15
22
|
overlayClassName?: string;
|
|
23
|
+
initialFocus?: TDialogInitialFocus;
|
|
16
24
|
children: React.ReactNode;
|
|
17
25
|
}
|
|
26
|
+
export declare const getTabbableElements: (container: HTMLElement) => Array<HTMLElement>;
|
|
27
|
+
/** Spread on a wrapper whose controls must not take the initial focus (the header close button). */
|
|
28
|
+
export declare const DIALOG_SKIP_INITIAL_FOCUS_PROPS: {
|
|
29
|
+
readonly 'data-dialog-initial-focus': "skip";
|
|
30
|
+
};
|
|
18
31
|
/**
|
|
19
32
|
* Portals only while open (plus its exit transition), so a closed dialog inside a mapped
|
|
20
33
|
* list renders nothing at all.
|
|
@@ -27,4 +40,14 @@ interface IDialogSectionProps {
|
|
|
27
40
|
export declare const ControlledDialogHeader: React.FC<IDialogSectionProps>;
|
|
28
41
|
export declare const ControlledDialogBody: React.FC<IDialogSectionProps>;
|
|
29
42
|
export declare const ControlledDialogFooter: React.FC<IDialogSectionProps>;
|
|
43
|
+
interface IControlledDialogTitleProps {
|
|
44
|
+
children: React.ReactNode;
|
|
45
|
+
className?: string;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* The dialog's name: a level-2 heading whose id the root wires into `aria-labelledby`, unless
|
|
49
|
+
* the root received its own `aria-label` / `aria-labelledby`. A `div` with the heading role, so
|
|
50
|
+
* any title content (blocks included) stays valid markup.
|
|
51
|
+
*/
|
|
52
|
+
export declare const ControlledDialogTitle: React.FC<IControlledDialogTitleProps>;
|
|
30
53
|
export {};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { useRef, useState, useEffect } from "react";
|
|
2
|
+
import { createContext, useRef, useState, useContext, useId, useMemo, useEffect } from "react";
|
|
3
3
|
import classNames from "classnames";
|
|
4
4
|
import { AnimatePresence, motion } from "motion/react";
|
|
5
5
|
import { createPortal } from "react-dom";
|
|
@@ -15,6 +15,52 @@ const SIZE_CLASS = {
|
|
|
15
15
|
xl: "w-full md:w-5/6 2xl:w-3/4 min-w-[95%] md:min-w-[83.333333%] 2xl:min-w-[75%] max-w-[95%] md:max-w-[83.333333%] 2xl:max-w-[75%]",
|
|
16
16
|
xxl: "w-screen min-w-[100vw] max-w-[100vw]"
|
|
17
17
|
};
|
|
18
|
+
const TABBABLE_SELECTOR = [
|
|
19
|
+
"a[href]",
|
|
20
|
+
"area[href]",
|
|
21
|
+
"button:not([disabled])",
|
|
22
|
+
'input:not([disabled]):not([type="hidden"])',
|
|
23
|
+
"select:not([disabled])",
|
|
24
|
+
"textarea:not([disabled])",
|
|
25
|
+
"iframe",
|
|
26
|
+
"audio[controls]",
|
|
27
|
+
"video[controls]",
|
|
28
|
+
'[contenteditable]:not([contenteditable="false"])',
|
|
29
|
+
"[tabindex]"
|
|
30
|
+
].join(",");
|
|
31
|
+
const isTabbable = (element) => {
|
|
32
|
+
if (element.tabIndex < 0) return false;
|
|
33
|
+
if (element.closest("[hidden], [inert]")) return false;
|
|
34
|
+
if (typeof element.checkVisibility === "function" && !element.checkVisibility()) return false;
|
|
35
|
+
return true;
|
|
36
|
+
};
|
|
37
|
+
const getTabbableElements = (container) => Array.from(container.querySelectorAll(TABBABLE_SELECTOR)).filter(isTabbable);
|
|
38
|
+
const openDialogStack = [];
|
|
39
|
+
const pushOpenDialog = (panel, depth) => {
|
|
40
|
+
openDialogStack.push({ panel, depth });
|
|
41
|
+
openDialogStack.sort((a, b) => {
|
|
42
|
+
if (a.depth !== b.depth) return a.depth - b.depth;
|
|
43
|
+
return a.panel.compareDocumentPosition(b.panel) & Node.DOCUMENT_POSITION_FOLLOWING ? -1 : 1;
|
|
44
|
+
});
|
|
45
|
+
};
|
|
46
|
+
const removeOpenDialog = (panel) => {
|
|
47
|
+
const index = openDialogStack.findIndex((entry) => entry.panel === panel);
|
|
48
|
+
if (index >= 0) openDialogStack.splice(index, 1);
|
|
49
|
+
};
|
|
50
|
+
const DialogDepthContext = createContext(0);
|
|
51
|
+
const isTopDialog = (panel) => panel !== null && openDialogStack[openDialogStack.length - 1]?.panel === panel;
|
|
52
|
+
const resolveInitialFocus = (panel, initialFocus) => {
|
|
53
|
+
if (initialFocus === "panel") return panel;
|
|
54
|
+
if (initialFocus !== "auto") return initialFocus.current ?? panel;
|
|
55
|
+
const requested = panel.querySelector("[data-autofocus], [autofocus]");
|
|
56
|
+
if (requested) return requested;
|
|
57
|
+
const firstTabbable = getTabbableElements(panel).find(
|
|
58
|
+
(element) => !element.closest('[data-dialog-initial-focus="skip"]')
|
|
59
|
+
);
|
|
60
|
+
return firstTabbable ?? panel;
|
|
61
|
+
};
|
|
62
|
+
const DIALOG_SKIP_INITIAL_FOCUS_PROPS = { "data-dialog-initial-focus": "skip" };
|
|
63
|
+
const DialogTitleContext = createContext(null);
|
|
18
64
|
const ControlledDialogRoot = ({
|
|
19
65
|
open,
|
|
20
66
|
handler,
|
|
@@ -22,12 +68,20 @@ const ControlledDialogRoot = ({
|
|
|
22
68
|
dismiss,
|
|
23
69
|
className,
|
|
24
70
|
overlayClassName,
|
|
71
|
+
initialFocus = "auto",
|
|
25
72
|
children,
|
|
26
73
|
...rest
|
|
27
74
|
}) => {
|
|
28
75
|
const panelRef = useRef(null);
|
|
29
76
|
const [isLeaving, setIsLeaving] = useState(false);
|
|
30
77
|
const previouslyFocusedRef = useRef(null);
|
|
78
|
+
const initialFocusRef = useRef(initialFocus);
|
|
79
|
+
initialFocusRef.current = initialFocus;
|
|
80
|
+
const depth = useContext(DialogDepthContext) + 1;
|
|
81
|
+
const titleId = useId();
|
|
82
|
+
const [hasTitle, setHasTitle] = useState(false);
|
|
83
|
+
const titleContext = useMemo(() => ({ titleId, registerTitle: setHasTitle }), [titleId]);
|
|
84
|
+
const hasOwnName = rest["aria-label"] !== void 0 || rest["aria-labelledby"] !== void 0;
|
|
31
85
|
useEffect(() => {
|
|
32
86
|
if (open) setIsLeaving(true);
|
|
33
87
|
}, [open]);
|
|
@@ -42,16 +96,57 @@ const ControlledDialogRoot = ({
|
|
|
42
96
|
useEffect(() => {
|
|
43
97
|
if (!open) return void 0;
|
|
44
98
|
previouslyFocusedRef.current = document.activeElement;
|
|
45
|
-
const frame = requestAnimationFrame(() =>
|
|
99
|
+
const frame = requestAnimationFrame(() => {
|
|
100
|
+
const panel = panelRef.current;
|
|
101
|
+
if (!panel) return;
|
|
102
|
+
if (panel !== document.activeElement && panel.contains(document.activeElement)) return;
|
|
103
|
+
resolveInitialFocus(panel, initialFocusRef.current).focus();
|
|
104
|
+
});
|
|
46
105
|
return () => {
|
|
47
106
|
cancelAnimationFrame(frame);
|
|
48
107
|
previouslyFocusedRef.current?.focus?.();
|
|
49
108
|
};
|
|
50
109
|
}, [open]);
|
|
110
|
+
useEffect(() => {
|
|
111
|
+
const panel = panelRef.current;
|
|
112
|
+
if (!open || !panel) return void 0;
|
|
113
|
+
pushOpenDialog(panel, depth);
|
|
114
|
+
const handleTab = (event) => {
|
|
115
|
+
if (event.key !== "Tab" || event.defaultPrevented || !isTopDialog(panel)) return;
|
|
116
|
+
const active = document.activeElement;
|
|
117
|
+
const isInside = active !== null && panel.contains(active);
|
|
118
|
+
const isInPageBehind = !active || active === document.body || Boolean(panel.compareDocumentPosition(active) & Node.DOCUMENT_POSITION_PRECEDING);
|
|
119
|
+
if (!isInside && !isInPageBehind) return;
|
|
120
|
+
const tabbables = getTabbableElements(panel);
|
|
121
|
+
if (tabbables.length === 0) {
|
|
122
|
+
event.preventDefault();
|
|
123
|
+
panel.focus();
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
const first = tabbables[0];
|
|
127
|
+
const last = tabbables[tabbables.length - 1];
|
|
128
|
+
if (!isInside || active === panel) {
|
|
129
|
+
event.preventDefault();
|
|
130
|
+
(event.shiftKey ? last : first).focus();
|
|
131
|
+
} else if (event.shiftKey && active === first) {
|
|
132
|
+
event.preventDefault();
|
|
133
|
+
last.focus();
|
|
134
|
+
} else if (!event.shiftKey && active === last) {
|
|
135
|
+
event.preventDefault();
|
|
136
|
+
first.focus();
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
document.addEventListener("keydown", handleTab);
|
|
140
|
+
return () => {
|
|
141
|
+
document.removeEventListener("keydown", handleTab);
|
|
142
|
+
removeOpenDialog(panel);
|
|
143
|
+
};
|
|
144
|
+
}, [open]);
|
|
51
145
|
useEffect(() => {
|
|
52
146
|
if (!open || dismiss?.enabled === false || dismiss?.escapeKey === false) return void 0;
|
|
53
147
|
const handleKeyDown = (event) => {
|
|
54
|
-
if (event.key !== "Escape") return;
|
|
148
|
+
if (event.key !== "Escape" || event.defaultPrevented) return;
|
|
149
|
+
if (panelRef.current && openDialogStack.length > 0 && !isTopDialog(panelRef.current)) return;
|
|
55
150
|
handler?.();
|
|
56
151
|
};
|
|
57
152
|
document.addEventListener("keydown", handleKeyDown);
|
|
@@ -86,6 +181,7 @@ const ControlledDialogRoot = ({
|
|
|
86
181
|
exit: "exit",
|
|
87
182
|
role: "dialog",
|
|
88
183
|
"aria-modal": true,
|
|
184
|
+
"aria-labelledby": hasTitle && !hasOwnName ? titleId : void 0,
|
|
89
185
|
tabIndex: -1,
|
|
90
186
|
...rest,
|
|
91
187
|
className: twMerge(
|
|
@@ -96,7 +192,7 @@ const ControlledDialogRoot = ({
|
|
|
96
192
|
),
|
|
97
193
|
className
|
|
98
194
|
),
|
|
99
|
-
children
|
|
195
|
+
children: /* @__PURE__ */ jsx(DialogDepthContext.Provider, { value: depth, children: /* @__PURE__ */ jsx(DialogTitleContext.Provider, { value: titleContext, children }) })
|
|
100
196
|
}
|
|
101
197
|
)
|
|
102
198
|
] }, "dialog-layer") }),
|
|
@@ -115,10 +211,23 @@ const ControlledDialogHeader = ({ children, className }) => /* @__PURE__ */ jsx(
|
|
|
115
211
|
);
|
|
116
212
|
const ControlledDialogBody = ({ children, className }) => /* @__PURE__ */ jsx("div", { className: twMerge("relative min-h-0 flex-1 p-4 text-base font-light leading-relaxed antialiased", className), children });
|
|
117
213
|
const ControlledDialogFooter = ({ children, className }) => /* @__PURE__ */ jsx("div", { className: twMerge("flex shrink-0 flex-wrap items-center justify-end p-4 text-foreground-muted", className), children });
|
|
214
|
+
const ControlledDialogTitle = ({ children, className }) => {
|
|
215
|
+
const context = useContext(DialogTitleContext);
|
|
216
|
+
const registerTitle = context?.registerTitle;
|
|
217
|
+
useEffect(() => {
|
|
218
|
+
if (!registerTitle) return void 0;
|
|
219
|
+
registerTitle(true);
|
|
220
|
+
return () => registerTitle(false);
|
|
221
|
+
}, [registerTitle]);
|
|
222
|
+
return /* @__PURE__ */ jsx("div", { id: context?.titleId, role: "heading", "aria-level": 2, className, children });
|
|
223
|
+
};
|
|
118
224
|
export {
|
|
119
225
|
ControlledDialogBody,
|
|
120
226
|
ControlledDialogFooter,
|
|
121
227
|
ControlledDialogHeader,
|
|
122
|
-
ControlledDialogRoot
|
|
228
|
+
ControlledDialogRoot,
|
|
229
|
+
ControlledDialogTitle,
|
|
230
|
+
DIALOG_SKIP_INITIAL_FOCUS_PROPS,
|
|
231
|
+
getTabbableElements
|
|
123
232
|
};
|
|
124
233
|
//# sourceMappingURL=ControlledDialogPrimitives.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ControlledDialogPrimitives.js","sources":["../../../src/components/Form/ControlledDialogPrimitives.tsx"],"sourcesContent":["import React, { useEffect, useRef, useState } from 'react';\nimport classNames from 'classnames';\nimport { AnimatePresence, motion } from 'motion/react';\nimport { createPortal } from 'react-dom';\nimport { twMerge } from 'tailwind-merge';\n\nimport { getPortalHost } from '@/helpers/portalHost';\nimport { dialogVariants, scrimVariants } from '@/motion';\n\nexport type TDialogSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'xxl';\n\nexport interface IDialogDismissConfig {\n enabled?: boolean;\n escapeKey?: boolean;\n outsidePress?: boolean;\n}\n\nconst SIZE_CLASS: Record<TDialogSize, string> = {\n xs: 'w-full md:w-3/5 lg:w-2/5 2xl:w-1/4 min-w-[80%] md:min-w-[60%] lg:min-w-[40%] 2xl:min-w-[25%] max-w-[80%] md:max-w-[60%] lg:max-w-[40%] 2xl:max-w-[25%]',\n sm: 'w-full md:w-2/3 lg:w-2/4 2xl:w-1/3 min-w-[80%] md:min-w-[66.666667%] lg:min-w-[50%] 2xl:min-w-[33.333333%] max-w-[80%] md:max-w-[66.666667%] lg:max-w-[50%] 2xl:max-w-[33.333333%]',\n md: 'w-full md:w-3/4 lg:w-3/5 2xl:w-2/5 min-w-[90%] md:min-w-[75%] lg:min-w-[60%] 2xl:min-w-[40%] max-w-[90%] md:max-w-[75%] lg:max-w-[60%] 2xl:max-w-[40%]',\n lg: 'w-full md:w-5/6 lg:w-3/4 2xl:w-3/5 min-w-[90%] md:min-w-[83.333333%] lg:min-w-[75%] 2xl:min-w-[60%] max-w-[90%] md:max-w-[83.333333%] lg:max-w-[75%] 2xl:max-w-[60%]',\n xl: 'w-full md:w-5/6 2xl:w-3/4 min-w-[95%] md:min-w-[83.333333%] 2xl:min-w-[75%] max-w-[95%] md:max-w-[83.333333%] 2xl:max-w-[75%]',\n xxl: 'w-screen min-w-[100vw] max-w-[100vw]',\n};\n\ntype TDivPropsWithoutMotionConflicts = Omit<\n React.HTMLAttributes<HTMLDivElement>,\n | 'className'\n | 'onScroll'\n | 'onDrag'\n | 'onDragStart'\n | 'onDragEnd'\n | 'onAnimationStart'\n | 'onAnimationEnd'\n | 'onAnimationIteration'\n>;\n\ninterface IControlledDialogRootProps extends TDivPropsWithoutMotionConflicts {\n open: boolean;\n handler?: () => void;\n size?: TDialogSize;\n dismiss?: IDialogDismissConfig;\n className?: string;\n overlayClassName?: string;\n children: React.ReactNode;\n}\n\n/**\n * Portals only while open (plus its exit transition), so a closed dialog inside a mapped\n * list renders nothing at all.\n */\nexport const ControlledDialogRoot: React.FC<IControlledDialogRootProps> = ({\n open,\n handler,\n size = 'md',\n dismiss,\n className,\n overlayClassName,\n children,\n ...rest\n}) => {\n const panelRef = useRef<HTMLDivElement>(null);\n const [isLeaving, setIsLeaving] = useState(false);\n const previouslyFocusedRef = useRef<HTMLElement | null>(null);\n\n useEffect(() => {\n if (open) setIsLeaving(true);\n }, [open]);\n\n /* Held until the exit finishes, not until `open` flips: releasing it early brings the\n page scrollbar back while the panel is still leaving, and the content behind jumps. */\n useEffect(() => {\n if (!open && !isLeaving) return undefined;\n\n const { overflow } = document.body.style;\n document.body.style.overflow = 'hidden';\n return () => {\n document.body.style.overflow = overflow;\n };\n }, [open, isLeaving]);\n\n useEffect(() => {\n if (!open) return undefined;\n\n previouslyFocusedRef.current = document.activeElement as HTMLElement | null;\n const frame = requestAnimationFrame(() => panelRef.current?.focus());\n\n return () => {\n cancelAnimationFrame(frame);\n previouslyFocusedRef.current?.focus?.();\n };\n }, [open]);\n\n useEffect(() => {\n if (!open || dismiss?.enabled === false || dismiss?.escapeKey === false) return undefined;\n\n const handleKeyDown = (event: KeyboardEvent) => {\n if (event.key !== 'Escape') return;\n handler?.();\n };\n\n document.addEventListener('keydown', handleKeyDown);\n return () => document.removeEventListener('keydown', handleKeyDown);\n }, [open, dismiss, handler]);\n\n const portalHost = getPortalHost();\n if (portalHost === null) return null;\n\n return createPortal(\n <AnimatePresence onExitComplete={() => setIsLeaving(false)}>\n {open && (\n <motion.div key=\"dialog-layer\" className=\"fixed inset-0 z-[9999] flex items-center justify-center\">\n <motion.div\n variants={scrimVariants}\n initial=\"hidden\"\n animate=\"visible\"\n exit=\"exit\"\n aria-hidden\n onClick={() => {\n if (dismiss?.enabled === false || !dismiss?.outsidePress) return;\n handler?.();\n }}\n className={classNames('absolute inset-0 glass-scrim', overlayClassName)}\n />\n\n <motion.div\n ref={panelRef}\n variants={dialogVariants}\n initial=\"hidden\"\n animate=\"visible\"\n exit=\"exit\"\n role=\"dialog\"\n aria-modal={true}\n tabIndex={-1}\n {...rest}\n className={twMerge(\n classNames(\n 'relative m-4 flex max-h-[90vh] flex-col rounded-lg bg-surface text-base font-light leading-relaxed',\n 'text-foreground-muted shadow-xl outline-none',\n SIZE_CLASS[size],\n ),\n className,\n )}\n >\n {children}\n </motion.div>\n </motion.div>\n )}\n </AnimatePresence>,\n portalHost,\n );\n};\n\ninterface IDialogSectionProps {\n children: React.ReactNode;\n className?: string;\n}\n\nexport const ControlledDialogHeader: React.FC<IDialogSectionProps> = ({ children, className }) => (\n <div\n className={twMerge(\n 'flex shrink-0 items-center p-4 text-2xl font-semibold leading-snug text-foreground antialiased',\n className,\n )}\n >\n {children}\n </div>\n);\n\nexport const ControlledDialogBody: React.FC<IDialogSectionProps> = ({ children, className }) => (\n <div className={twMerge('relative min-h-0 flex-1 p-4 text-base font-light leading-relaxed antialiased', className)}>\n {children}\n </div>\n);\n\nexport const ControlledDialogFooter: React.FC<IDialogSectionProps> = ({ children, className }) => (\n <div className={twMerge('flex shrink-0 flex-wrap items-center justify-end p-4 text-foreground-muted', className)}>\n {children}\n </div>\n);\n"],"names":[],"mappings":";;;;;;;;;AAiBA,MAAM,aAA0C;AAAA,EAC9C,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,KAAK;AACP;AA4BO,MAAM,uBAA6D,CAAC;AAAA,EACzE;AAAA,EACA;AAAA,EACA,OAAO;AAAA,EACP;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,GAAG;AACL,MAAM;AACJ,QAAM,WAAW,OAAuB,IAAI;AAC5C,QAAM,CAAC,WAAW,YAAY,IAAI,SAAS,KAAK;AAChD,QAAM,uBAAuB,OAA2B,IAAI;AAE5D,YAAU,MAAM;AACd,QAAI,mBAAmB,IAAI;AAAA,EAC7B,GAAG,CAAC,IAAI,CAAC;AAIT,YAAU,MAAM;AACd,QAAI,CAAC,QAAQ,CAAC,UAAW,QAAO;AAEhC,UAAM,EAAE,SAAA,IAAa,SAAS,KAAK;AACnC,aAAS,KAAK,MAAM,WAAW;AAC/B,WAAO,MAAM;AACX,eAAS,KAAK,MAAM,WAAW;AAAA,IACjC;AAAA,EACF,GAAG,CAAC,MAAM,SAAS,CAAC;AAEpB,YAAU,MAAM;AACd,QAAI,CAAC,KAAM,QAAO;AAElB,yBAAqB,UAAU,SAAS;AACxC,UAAM,QAAQ,sBAAsB,MAAM,SAAS,SAAS,OAAO;AAEnE,WAAO,MAAM;AACX,2BAAqB,KAAK;AAC1B,2BAAqB,SAAS,QAAA;AAAA,IAChC;AAAA,EACF,GAAG,CAAC,IAAI,CAAC;AAET,YAAU,MAAM;AACd,QAAI,CAAC,QAAQ,SAAS,YAAY,SAAS,SAAS,cAAc,MAAO,QAAO;AAEhF,UAAM,gBAAgB,CAAC,UAAyB;AAC9C,UAAI,MAAM,QAAQ,SAAU;AAC5B,gBAAA;AAAA,IACF;AAEA,aAAS,iBAAiB,WAAW,aAAa;AAClD,WAAO,MAAM,SAAS,oBAAoB,WAAW,aAAa;AAAA,EACpE,GAAG,CAAC,MAAM,SAAS,OAAO,CAAC;AAE3B,QAAM,aAAa,cAAA;AACnB,MAAI,eAAe,KAAM,QAAO;AAEhC,SAAO;AAAA,IACL,oBAAC,iBAAA,EAAgB,gBAAgB,MAAM,aAAa,KAAK,GACtD,UAAA,QACC,qBAAC,OAAO,KAAP,EAA8B,WAAU,2DACvC,UAAA;AAAA,MAAA;AAAA,QAAC,OAAO;AAAA,QAAP;AAAA,UACC,UAAU;AAAA,UACV,SAAQ;AAAA,UACR,SAAQ;AAAA,UACR,MAAK;AAAA,UACL,eAAW;AAAA,UACX,SAAS,MAAM;AACb,gBAAI,SAAS,YAAY,SAAS,CAAC,SAAS,aAAc;AAC1D,sBAAA;AAAA,UACF;AAAA,UACA,WAAW,WAAW,gCAAgC,gBAAgB;AAAA,QAAA;AAAA,MAAA;AAAA,MAGxE;AAAA,QAAC,OAAO;AAAA,QAAP;AAAA,UACC,KAAK;AAAA,UACL,UAAU;AAAA,UACV,SAAQ;AAAA,UACR,SAAQ;AAAA,UACR,MAAK;AAAA,UACL,MAAK;AAAA,UACL,cAAY;AAAA,UACZ,UAAU;AAAA,UACT,GAAG;AAAA,UACJ,WAAW;AAAA,YACT;AAAA,cACE;AAAA,cACA;AAAA,cACA,WAAW,IAAI;AAAA,YAAA;AAAA,YAEjB;AAAA,UAAA;AAAA,UAGD;AAAA,QAAA;AAAA,MAAA;AAAA,IACH,EAAA,GAlCc,cAmChB,GAEJ;AAAA,IACA;AAAA,EAAA;AAEJ;AAOO,MAAM,yBAAwD,CAAC,EAAE,UAAU,gBAChF;AAAA,EAAC;AAAA,EAAA;AAAA,IACC,WAAW;AAAA,MACT;AAAA,MACA;AAAA,IAAA;AAAA,IAGD;AAAA,EAAA;AACH;AAGK,MAAM,uBAAsD,CAAC,EAAE,UAAU,UAAA,MAC9E,oBAAC,OAAA,EAAI,WAAW,QAAQ,gFAAgF,SAAS,GAC9G,SAAA,CACH;AAGK,MAAM,yBAAwD,CAAC,EAAE,UAAU,UAAA,MAChF,oBAAC,OAAA,EAAI,WAAW,QAAQ,8EAA8E,SAAS,GAC5G,SAAA,CACH;"}
|
|
1
|
+
{"version":3,"file":"ControlledDialogPrimitives.js","sources":["../../../src/components/Form/ControlledDialogPrimitives.tsx"],"sourcesContent":["import React, { createContext, useContext, useEffect, useId, useMemo, useRef, useState } from 'react';\nimport classNames from 'classnames';\nimport { AnimatePresence, motion } from 'motion/react';\nimport { createPortal } from 'react-dom';\nimport { twMerge } from 'tailwind-merge';\n\nimport { getPortalHost } from '@/helpers/portalHost';\nimport { dialogVariants, scrimVariants } from '@/motion';\n\nexport type TDialogSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'xxl';\n\n/**\n * Where focus lands when the dialog opens. `auto` (default): an element that asked for it\n * (`autoFocus` / `data-autofocus`), else the first tabbable element outside a\n * `data-dialog-initial-focus=\"skip\"` zone, else the panel itself. `panel`: the panel, the\n * pre-0.13 behaviour. A ref: that element.\n */\nexport type TDialogInitialFocus = 'auto' | 'panel' | React.RefObject<HTMLElement | null>;\n\nexport interface IDialogDismissConfig {\n enabled?: boolean;\n escapeKey?: boolean;\n outsidePress?: boolean;\n}\n\nconst SIZE_CLASS: Record<TDialogSize, string> = {\n xs: 'w-full md:w-3/5 lg:w-2/5 2xl:w-1/4 min-w-[80%] md:min-w-[60%] lg:min-w-[40%] 2xl:min-w-[25%] max-w-[80%] md:max-w-[60%] lg:max-w-[40%] 2xl:max-w-[25%]',\n sm: 'w-full md:w-2/3 lg:w-2/4 2xl:w-1/3 min-w-[80%] md:min-w-[66.666667%] lg:min-w-[50%] 2xl:min-w-[33.333333%] max-w-[80%] md:max-w-[66.666667%] lg:max-w-[50%] 2xl:max-w-[33.333333%]',\n md: 'w-full md:w-3/4 lg:w-3/5 2xl:w-2/5 min-w-[90%] md:min-w-[75%] lg:min-w-[60%] 2xl:min-w-[40%] max-w-[90%] md:max-w-[75%] lg:max-w-[60%] 2xl:max-w-[40%]',\n lg: 'w-full md:w-5/6 lg:w-3/4 2xl:w-3/5 min-w-[90%] md:min-w-[83.333333%] lg:min-w-[75%] 2xl:min-w-[60%] max-w-[90%] md:max-w-[83.333333%] lg:max-w-[75%] 2xl:max-w-[60%]',\n xl: 'w-full md:w-5/6 2xl:w-3/4 min-w-[95%] md:min-w-[83.333333%] 2xl:min-w-[75%] max-w-[95%] md:max-w-[83.333333%] 2xl:max-w-[75%]',\n xxl: 'w-screen min-w-[100vw] max-w-[100vw]',\n};\n\ntype TDivPropsWithoutMotionConflicts = Omit<\n React.HTMLAttributes<HTMLDivElement>,\n | 'className'\n | 'onScroll'\n | 'onDrag'\n | 'onDragStart'\n | 'onDragEnd'\n | 'onAnimationStart'\n | 'onAnimationEnd'\n | 'onAnimationIteration'\n>;\n\ninterface IControlledDialogRootProps extends TDivPropsWithoutMotionConflicts {\n open: boolean;\n handler?: () => void;\n size?: TDialogSize;\n dismiss?: IDialogDismissConfig;\n className?: string;\n overlayClassName?: string;\n initialFocus?: TDialogInitialFocus;\n children: React.ReactNode;\n}\n\nconst TABBABLE_SELECTOR = [\n 'a[href]',\n 'area[href]',\n 'button:not([disabled])',\n 'input:not([disabled]):not([type=\"hidden\"])',\n 'select:not([disabled])',\n 'textarea:not([disabled])',\n 'iframe',\n 'audio[controls]',\n 'video[controls]',\n '[contenteditable]:not([contenteditable=\"false\"])',\n '[tabindex]',\n].join(',');\n\nconst isTabbable = (element: HTMLElement): boolean => {\n if (element.tabIndex < 0) return false;\n if (element.closest('[hidden], [inert]')) return false;\n if (typeof element.checkVisibility === 'function' && !element.checkVisibility()) return false;\n return true;\n};\n\nexport const getTabbableElements = (container: HTMLElement): Array<HTMLElement> =>\n Array.from(container.querySelectorAll<HTMLElement>(TABBABLE_SELECTOR)).filter(isTabbable);\n\n/* Open panels, top last: only the top one traps Tab and answers ESC. Ordered by React nesting\n depth (a dialog rendered inside another sits above it), then by document order, which is\n opening order for siblings because each one portals to the end of the host. */\nconst openDialogStack: Array<{ panel: HTMLElement; depth: number }> = [];\n\nconst pushOpenDialog = (panel: HTMLElement, depth: number) => {\n openDialogStack.push({ panel, depth });\n openDialogStack.sort((a, b) => {\n if (a.depth !== b.depth) return a.depth - b.depth;\n return a.panel.compareDocumentPosition(b.panel) & Node.DOCUMENT_POSITION_FOLLOWING ? -1 : 1;\n });\n};\n\nconst removeOpenDialog = (panel: HTMLElement) => {\n const index = openDialogStack.findIndex((entry) => entry.panel === panel);\n if (index >= 0) openDialogStack.splice(index, 1);\n};\n\nconst DialogDepthContext = createContext(0);\n\nconst isTopDialog = (panel: HTMLElement | null) =>\n panel !== null && openDialogStack[openDialogStack.length - 1]?.panel === panel;\n\nconst resolveInitialFocus = (panel: HTMLElement, initialFocus: TDialogInitialFocus): HTMLElement => {\n if (initialFocus === 'panel') return panel;\n if (initialFocus !== 'auto') return initialFocus.current ?? panel;\n\n const requested = panel.querySelector<HTMLElement>('[data-autofocus], [autofocus]');\n if (requested) return requested;\n\n const firstTabbable = getTabbableElements(panel).find(\n (element) => !element.closest('[data-dialog-initial-focus=\"skip\"]'),\n );\n return firstTabbable ?? panel;\n};\n\n/** Spread on a wrapper whose controls must not take the initial focus (the header close button). */\nexport const DIALOG_SKIP_INITIAL_FOCUS_PROPS = { 'data-dialog-initial-focus': 'skip' } as const;\n\ninterface IDialogTitleContext {\n titleId: string;\n registerTitle: (present: boolean) => void;\n}\n\nconst DialogTitleContext = createContext<IDialogTitleContext | null>(null);\n\n/**\n * Portals only while open (plus its exit transition), so a closed dialog inside a mapped\n * list renders nothing at all.\n */\nexport const ControlledDialogRoot: React.FC<IControlledDialogRootProps> = ({\n open,\n handler,\n size = 'md',\n dismiss,\n className,\n overlayClassName,\n initialFocus = 'auto',\n children,\n ...rest\n}) => {\n const panelRef = useRef<HTMLDivElement>(null);\n const [isLeaving, setIsLeaving] = useState(false);\n const previouslyFocusedRef = useRef<HTMLElement | null>(null);\n const initialFocusRef = useRef(initialFocus);\n initialFocusRef.current = initialFocus;\n\n const depth = useContext(DialogDepthContext) + 1;\n const titleId = useId();\n const [hasTitle, setHasTitle] = useState(false);\n const titleContext = useMemo(() => ({ titleId, registerTitle: setHasTitle }), [titleId]);\n const hasOwnName = rest['aria-label'] !== undefined || rest['aria-labelledby'] !== undefined;\n\n useEffect(() => {\n if (open) setIsLeaving(true);\n }, [open]);\n\n /* Held until the exit finishes, not until `open` flips: releasing it early brings the\n page scrollbar back while the panel is still leaving, and the content behind jumps. */\n useEffect(() => {\n if (!open && !isLeaving) return undefined;\n\n const { overflow } = document.body.style;\n document.body.style.overflow = 'hidden';\n return () => {\n document.body.style.overflow = overflow;\n };\n }, [open, isLeaving]);\n\n useEffect(() => {\n if (!open) return undefined;\n\n previouslyFocusedRef.current = document.activeElement as HTMLElement | null;\n const frame = requestAnimationFrame(() => {\n const panel = panelRef.current;\n if (!panel) return;\n // Something inside already took focus (a React `autoFocus` field): keep it.\n if (panel !== document.activeElement && panel.contains(document.activeElement)) return;\n resolveInitialFocus(panel, initialFocusRef.current).focus();\n });\n\n return () => {\n cancelAnimationFrame(frame);\n previouslyFocusedRef.current?.focus?.();\n };\n }, [open]);\n\n useEffect(() => {\n const panel = panelRef.current;\n if (!open || !panel) return undefined;\n\n pushOpenDialog(panel, depth);\n\n const handleTab = (event: KeyboardEvent) => {\n if (event.key !== 'Tab' || event.defaultPrevented || !isTopDialog(panel)) return;\n\n const active = document.activeElement as HTMLElement | null;\n const isInside = active !== null && panel.contains(active);\n // Focus in a layer opened from inside the dialog (a select list, a popover) belongs to\n // that layer; only focus that escaped back to the page behind is pulled in.\n const isInPageBehind =\n !active ||\n active === document.body ||\n Boolean(panel.compareDocumentPosition(active) & Node.DOCUMENT_POSITION_PRECEDING);\n if (!isInside && !isInPageBehind) return;\n\n const tabbables = getTabbableElements(panel);\n if (tabbables.length === 0) {\n event.preventDefault();\n panel.focus();\n return;\n }\n\n const first = tabbables[0];\n const last = tabbables[tabbables.length - 1];\n\n if (!isInside || active === panel) {\n event.preventDefault();\n (event.shiftKey ? last : first).focus();\n } else if (event.shiftKey && active === first) {\n event.preventDefault();\n last.focus();\n } else if (!event.shiftKey && active === last) {\n event.preventDefault();\n first.focus();\n }\n };\n\n document.addEventListener('keydown', handleTab);\n return () => {\n document.removeEventListener('keydown', handleTab);\n removeOpenDialog(panel);\n };\n }, [open]);\n\n useEffect(() => {\n if (!open || dismiss?.enabled === false || dismiss?.escapeKey === false) return undefined;\n\n const handleKeyDown = (event: KeyboardEvent) => {\n // A layer inside the dialog (a select list) that consumed the ESC marks it handled.\n if (event.key !== 'Escape' || event.defaultPrevented) return;\n if (panelRef.current && openDialogStack.length > 0 && !isTopDialog(panelRef.current)) return;\n handler?.();\n };\n\n document.addEventListener('keydown', handleKeyDown);\n return () => document.removeEventListener('keydown', handleKeyDown);\n }, [open, dismiss, handler]);\n\n const portalHost = getPortalHost();\n if (portalHost === null) return null;\n\n return createPortal(\n <AnimatePresence onExitComplete={() => setIsLeaving(false)}>\n {open && (\n <motion.div key=\"dialog-layer\" className=\"fixed inset-0 z-[9999] flex items-center justify-center\">\n <motion.div\n variants={scrimVariants}\n initial=\"hidden\"\n animate=\"visible\"\n exit=\"exit\"\n aria-hidden\n onClick={() => {\n if (dismiss?.enabled === false || !dismiss?.outsidePress) return;\n handler?.();\n }}\n className={classNames('absolute inset-0 glass-scrim', overlayClassName)}\n />\n\n <motion.div\n ref={panelRef}\n variants={dialogVariants}\n initial=\"hidden\"\n animate=\"visible\"\n exit=\"exit\"\n role=\"dialog\"\n aria-modal={true}\n aria-labelledby={hasTitle && !hasOwnName ? titleId : undefined}\n tabIndex={-1}\n {...rest}\n className={twMerge(\n classNames(\n 'relative m-4 flex max-h-[90vh] flex-col rounded-lg bg-surface text-base font-light leading-relaxed',\n 'text-foreground-muted shadow-xl outline-none',\n SIZE_CLASS[size],\n ),\n className,\n )}\n >\n <DialogDepthContext.Provider value={depth}>\n <DialogTitleContext.Provider value={titleContext}>{children}</DialogTitleContext.Provider>\n </DialogDepthContext.Provider>\n </motion.div>\n </motion.div>\n )}\n </AnimatePresence>,\n portalHost,\n );\n};\n\ninterface IDialogSectionProps {\n children: React.ReactNode;\n className?: string;\n}\n\nexport const ControlledDialogHeader: React.FC<IDialogSectionProps> = ({ children, className }) => (\n <div\n className={twMerge(\n 'flex shrink-0 items-center p-4 text-2xl font-semibold leading-snug text-foreground antialiased',\n className,\n )}\n >\n {children}\n </div>\n);\n\nexport const ControlledDialogBody: React.FC<IDialogSectionProps> = ({ children, className }) => (\n <div className={twMerge('relative min-h-0 flex-1 p-4 text-base font-light leading-relaxed antialiased', className)}>\n {children}\n </div>\n);\n\nexport const ControlledDialogFooter: React.FC<IDialogSectionProps> = ({ children, className }) => (\n <div className={twMerge('flex shrink-0 flex-wrap items-center justify-end p-4 text-foreground-muted', className)}>\n {children}\n </div>\n);\n\ninterface IControlledDialogTitleProps {\n children: React.ReactNode;\n className?: string;\n}\n\n/**\n * The dialog's name: a level-2 heading whose id the root wires into `aria-labelledby`, unless\n * the root received its own `aria-label` / `aria-labelledby`. A `div` with the heading role, so\n * any title content (blocks included) stays valid markup.\n */\nexport const ControlledDialogTitle: React.FC<IControlledDialogTitleProps> = ({ children, className }) => {\n const context = useContext(DialogTitleContext);\n const registerTitle = context?.registerTitle;\n\n useEffect(() => {\n if (!registerTitle) return undefined;\n registerTitle(true);\n return () => registerTitle(false);\n }, [registerTitle]);\n\n return (\n <div id={context?.titleId} role=\"heading\" aria-level={2} className={className}>\n {children}\n </div>\n );\n};\n"],"names":[],"mappings":"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"}
|
|
@@ -10,7 +10,16 @@ export interface IControlledSelectFetchParams {
|
|
|
10
10
|
search: string;
|
|
11
11
|
reset: boolean;
|
|
12
12
|
}
|
|
13
|
-
type
|
|
13
|
+
type TDataAttributes = {
|
|
14
|
+
[key: `data-${string}`]: string | number | boolean | undefined;
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* Native and ARIA attributes forwarded to the combobox trigger: `id` (so a `<label htmlFor>`
|
|
18
|
+
* names it), `aria-labelledby` / `aria-describedby` / `aria-invalid`, `data-*`, `onBlur`.
|
|
19
|
+
* `name` is submitted through hidden inputs carrying the selected value(s), never the label.
|
|
20
|
+
*/
|
|
21
|
+
type TControlledSelectTriggerProps = Pick<React.InputHTMLAttributes<HTMLInputElement>, 'id' | 'name' | 'required' | 'onBlur' | 'onFocus' | 'autoFocus'> & React.AriaAttributes & TDataAttributes;
|
|
22
|
+
type TControlledSelectProps = TControlledSelectTriggerProps & {
|
|
14
23
|
label?: string;
|
|
15
24
|
persistentLabel?: boolean;
|
|
16
25
|
options: Array<IFilterOption>;
|
|
@@ -40,5 +49,5 @@ type TControlledSelectProps = {
|
|
|
40
49
|
matchTriggerWidth?: boolean;
|
|
41
50
|
compact?: boolean;
|
|
42
51
|
};
|
|
43
|
-
export declare function ControlledSelect({ label, persistentLabel, options: optionsProp, selectedValue, error, multiple, disabled, searchable, clearable, className, listClassName, onChangeHook, onClearSearch, infiniteScroll, fetchMoreOptions, hasMoreOptions, loading, onOpen, selectedItemsPosition, showSelectedItems, inFieldChips, preserveLabelCase, swatchShape, ariaLabel, ariaRequired, inputClassName, matchTriggerWidth, compact, }: TControlledSelectProps): import("react").JSX.Element;
|
|
52
|
+
export declare function ControlledSelect({ label, persistentLabel, options: optionsProp, selectedValue, error, multiple, disabled, searchable, clearable, className, listClassName, onChangeHook, onClearSearch, infiniteScroll, fetchMoreOptions, hasMoreOptions, loading, onOpen, selectedItemsPosition, showSelectedItems, inFieldChips, preserveLabelCase, swatchShape, ariaLabel, ariaRequired, inputClassName, matchTriggerWidth, compact, id, name, required, onBlur, onFocus, autoFocus, ...triggerAttributes }: TControlledSelectProps): import("react").JSX.Element;
|
|
44
53
|
export {};
|