@mittwald/flow-react-components 1.3.4 → 1.4.0-next.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.
Files changed (79) hide show
  1. package/MIGRATION.md +25 -2
  2. package/dist/assets/component-index.json +1 -1
  3. package/dist/assets/doc-properties.json +1 -1
  4. package/dist/css/all-layered.css +1 -1
  5. package/dist/css/all.css +1 -1
  6. package/dist/js/_virtual/_24.locale.mjs +10 -2
  7. package/dist/js/default.mjs +2 -1
  8. package/dist/js/flr-universal.mjs +2 -1
  9. package/dist/js/packages/components/src/components/Action/models/ActionModel.mjs +25 -8
  10. package/dist/js/packages/components/src/components/Action/models/ActionModel.mjs.map +1 -1
  11. package/dist/js/packages/components/src/components/CheckboxButton/CheckboxButton.mjs +0 -1
  12. package/dist/js/packages/components/src/components/CheckboxButton/CheckboxButton.mjs.map +1 -1
  13. package/dist/js/packages/components/src/components/CheckboxButton/CheckboxButton.module.mjs +2 -4
  14. package/dist/js/packages/components/src/components/CheckboxButton/CheckboxButton.module.mjs.map +1 -1
  15. package/dist/js/packages/components/src/components/ContextMenu/ContextMenu.mjs +2 -2
  16. package/dist/js/packages/components/src/components/ContextMenu/ContextMenu.mjs.map +1 -1
  17. package/dist/js/packages/components/src/components/ContextMenu/components/ContextMenuSection/ContextMenuSection.mjs +2 -2
  18. package/dist/js/packages/components/src/components/ContextMenu/components/ContextMenuSection/ContextMenuSection.mjs.map +1 -1
  19. package/dist/js/packages/components/src/components/ContextMenu/lib.mjs +7 -3
  20. package/dist/js/packages/components/src/components/ContextMenu/lib.mjs.map +1 -1
  21. package/dist/js/packages/components/src/components/CopyButton/CopyButton.mjs +4 -3
  22. package/dist/js/packages/components/src/components/CopyButton/CopyButton.mjs.map +1 -1
  23. package/dist/js/packages/components/src/components/Icon/components/icons/IconAIHosting.mjs +19 -0
  24. package/dist/js/packages/components/src/components/Icon/components/icons/IconAIHosting.mjs.map +1 -0
  25. package/dist/js/packages/components/src/components/Image/Image.mjs +2 -2
  26. package/dist/js/packages/components/src/components/Image/Image.mjs.map +1 -1
  27. package/dist/js/packages/components/src/components/Image/Image.module.mjs +3 -1
  28. package/dist/js/packages/components/src/components/Image/Image.module.mjs.map +1 -1
  29. package/dist/js/packages/components/src/components/MarkdownEditor/MarkdownEditor.mjs +16 -3
  30. package/dist/js/packages/components/src/components/MarkdownEditor/MarkdownEditor.mjs.map +1 -1
  31. package/dist/js/packages/components/src/components/MarkdownEditor/MarkdownEditor.module.mjs +4 -2
  32. package/dist/js/packages/components/src/components/MarkdownEditor/MarkdownEditor.module.mjs.map +1 -1
  33. package/dist/js/packages/components/src/components/MarkdownEditor/components/AttachmentButton.mjs +47 -0
  34. package/dist/js/packages/components/src/components/MarkdownEditor/components/AttachmentButton.mjs.map +1 -0
  35. package/dist/js/packages/components/src/components/MarkdownEditor/components/Toolbar.mjs +21 -20
  36. package/dist/js/packages/components/src/components/MarkdownEditor/components/Toolbar.mjs.map +1 -1
  37. package/dist/js/packages/components/src/components/MarkdownEditor/components/ToolbarButton.mjs +1 -1
  38. package/dist/js/packages/components/src/components/MarkdownEditor/components/ToolbarButton.mjs.map +1 -1
  39. package/dist/js/packages/components/src/components/MarkdownEditor/lib/fileUpload.mjs +73 -0
  40. package/dist/js/packages/components/src/components/MarkdownEditor/lib/fileUpload.mjs.map +1 -0
  41. package/dist/js/packages/components/src/components/MarkdownEditor/lib/useFileUpload.mjs +141 -0
  42. package/dist/js/packages/components/src/components/MarkdownEditor/lib/useFileUpload.mjs.map +1 -0
  43. package/dist/js/packages/components/src/components/Modal/Modal.mjs +6 -4
  44. package/dist/js/packages/components/src/components/Modal/Modal.mjs.map +1 -1
  45. package/dist/js/packages/components/src/components/OverlayTrigger/OverlayTrigger.mjs +1 -0
  46. package/dist/js/packages/components/src/components/OverlayTrigger/OverlayTrigger.mjs.map +1 -1
  47. package/dist/js/packages/components/src/components/TabNavigation/lib/useCollapsingItems.mjs +1 -2
  48. package/dist/js/packages/components/src/components/TabNavigation/lib/useCollapsingItems.mjs.map +1 -1
  49. package/dist/js/packages/components/src/components/Tabs/lib/useTabIndicator.mjs +2 -3
  50. package/dist/js/packages/components/src/components/Tabs/lib/useTabIndicator.mjs.map +1 -1
  51. package/dist/js/packages/components/src/lib/controller/overlay/OverlayContextProvider.mjs +6 -3
  52. package/dist/js/packages/components/src/lib/controller/overlay/OverlayContextProvider.mjs.map +1 -1
  53. package/dist/js/packages/components/src/lib/controller/overlay/OverlayController.mjs.map +1 -1
  54. package/dist/js/packages/components/src/lib/controller/overlay/context.mjs +1 -1
  55. package/dist/js/packages/components/src/lib/controller/overlay/context.mjs.map +1 -1
  56. package/dist/js/packages/components/src/lib/controller/overlay/useOverlayController.mjs +1 -1
  57. package/dist/js/packages/components/src/lib/controller/overlay/useOverlayController.mjs.map +1 -1
  58. package/dist/js/packages/design-tokens/dist/json-runtime/all-dark.mjs +1 -1
  59. package/dist/js/packages/design-tokens/dist/json-runtime/all-dark.mjs.map +1 -1
  60. package/dist/js/packages/design-tokens/dist/json-runtime/all-light.mjs +1 -1
  61. package/dist/js/packages/design-tokens/dist/json-runtime/all-light.mjs.map +1 -1
  62. package/dist/types/components/Action/models/ActionModel.d.ts +12 -4
  63. package/dist/types/components/Action/types.d.ts +9 -2
  64. package/dist/types/components/ContextMenu/lib.d.ts +5 -1
  65. package/dist/types/components/CopyButton/CopyButton.d.ts +5 -0
  66. package/dist/types/components/Icon/components/icons/IconAIHosting.d.ts +4 -0
  67. package/dist/types/components/Icon/components/icons/index.d.ts +1 -0
  68. package/dist/types/components/Image/Image.d.ts +7 -1
  69. package/dist/types/components/MarkdownEditor/MarkdownEditor.d.ts +20 -0
  70. package/dist/types/components/MarkdownEditor/components/AttachmentButton.d.ts +1 -0
  71. package/dist/types/components/MarkdownEditor/components/Toolbar.d.ts +11 -2
  72. package/dist/types/components/MarkdownEditor/components/ToolbarButton.d.ts +8 -2
  73. package/dist/types/components/MarkdownEditor/index.d.ts +1 -1
  74. package/dist/types/components/MarkdownEditor/lib/fileUpload.d.ts +19 -0
  75. package/dist/types/components/MarkdownEditor/lib/useFileUpload.d.ts +1 -0
  76. package/dist/types/lib/controller/overlay/OverlayContextProvider.d.ts +9 -0
  77. package/dist/types/lib/controller/overlay/OverlayController.d.ts +11 -1
  78. package/dist/types/lib/controller/overlay/context.d.ts +19 -2
  79. package/package.json +9 -9
@@ -0,0 +1,47 @@
1
+ "use client"
2
+ /* */
3
+ import { IconAttachment } from "../../Icon/components/icons/IconAttachment.mjs";
4
+ import { ToolbarButton } from "./ToolbarButton.mjs";
5
+ import { useRef } from "react";
6
+ import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
7
+ import { useVisuallyHidden } from "react-aria";
8
+ //#region src/components/MarkdownEditor/components/AttachmentButton.tsx
9
+ /**
10
+ * The toolbar's file picker — the keyboard and pointer equivalent of dropping a
11
+ * file onto the editor. A `ToolbarButton` like every other tool, plus the
12
+ * hidden input it opens.
13
+ *
14
+ * @internal
15
+ */
16
+ var AttachmentButton = (props) => {
17
+ const { accept, isDisabled, onSelect } = props;
18
+ const inputRef = useRef(null);
19
+ const { visuallyHiddenProps } = useVisuallyHidden();
20
+ const handlePress = () => {
21
+ const input = inputRef.current;
22
+ if (!input) return;
23
+ input.value = "";
24
+ input.click();
25
+ };
26
+ return /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx(ToolbarButton, {
27
+ type: "attachment",
28
+ isDisabled,
29
+ onPress: handlePress,
30
+ children: /* @__PURE__ */ jsx(IconAttachment, {})
31
+ }), /* @__PURE__ */ jsx("input", {
32
+ ...visuallyHiddenProps,
33
+ ref: inputRef,
34
+ type: "file",
35
+ accept,
36
+ multiple: true,
37
+ tabIndex: -1,
38
+ onChange: (event) => {
39
+ const files = event.target.files;
40
+ if (files?.length) onSelect(Array.from(files));
41
+ }
42
+ })] });
43
+ };
44
+ //#endregion
45
+ export { AttachmentButton };
46
+
47
+ //# sourceMappingURL=AttachmentButton.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"AttachmentButton.mjs","names":[],"sources":["../../../../../../../../src/components/MarkdownEditor/components/AttachmentButton.tsx"],"sourcesContent":["import { type FC, useRef } from \"react\";\nimport { useVisuallyHidden } from \"react-aria\";\nimport { IconAttachment } from \"@/components/Icon/components/icons\";\nimport { ToolbarButton } from \"@/components/MarkdownEditor/components/ToolbarButton\";\n\n/** @internal */\nexport interface AttachmentButtonProps {\n /** Which file types to offer, as an `accept` attribute value. */\n accept?: string;\n isDisabled?: boolean;\n /** Called with the files the user picked. */\n onSelect: (files: File[]) => void;\n}\n\n/**\n * The toolbar's file picker — the keyboard and pointer equivalent of dropping a\n * file onto the editor. A `ToolbarButton` like every other tool, plus the\n * hidden input it opens.\n *\n * @internal\n */\nexport const AttachmentButton: FC<AttachmentButtonProps> = (props) => {\n const { accept, isDisabled, onSelect } = props;\n\n const inputRef = useRef<HTMLInputElement>(null);\n const { visuallyHiddenProps } = useVisuallyHidden();\n\n const handlePress = () => {\n const input = inputRef.current;\n\n if (!input) {\n return;\n }\n\n /*\n * Cleared first: an input that still holds the previous file fires no\n * `change` when the same file is picked again.\n */\n input.value = \"\";\n input.click();\n };\n\n return (\n <>\n <ToolbarButton\n type=\"attachment\"\n isDisabled={isDisabled}\n onPress={handlePress}\n >\n <IconAttachment />\n </ToolbarButton>\n\n {/*\n * A plain input rather than `FileField`'s `FileInput`: that one renders an\n * `Aria.Input`, which reads the `InputContext` the surrounding TextArea's\n * `Aria.TextField` provides and would be handed the editor's text as its\n * value — which a file input rejects outright.\n *\n * `tabIndex={-1}` keeps the hidden input out of the tab order; the button\n * in front of it is the focusable affordance.\n */}\n <input\n {...visuallyHiddenProps}\n ref={inputRef}\n type=\"file\"\n accept={accept}\n multiple\n tabIndex={-1}\n onChange={(event) => {\n const files = event.target.files;\n\n if (files?.length) {\n onSelect(Array.from(files));\n }\n }}\n />\n </>\n );\n};\n"],"mappings":";;;;;;;;;;;;;;;AAqBA,IAAa,oBAA+C,UAAU;CACpE,MAAM,EAAE,QAAQ,YAAY,aAAa;CAEzC,MAAM,WAAW,OAAyB,IAAI;CAC9C,MAAM,EAAE,wBAAwB,kBAAkB;CAElD,MAAM,oBAAoB;EACxB,MAAM,QAAQ,SAAS;EAEvB,IAAI,CAAC,OACH;EAOF,MAAM,QAAQ;EACd,MAAM,MAAM;CACd;CAEA,OACE,qBAAA,YAAA,EAAA,UAAA,CACE,oBAAC,eAAD;EACE,MAAK;EACO;EACZ,SAAS;EAET,UAAA,oBAAC,gBAAD,CAAiB,CAAA;CACJ,CAAA,GAWf,oBAAC,SAAD;EACE,GAAI;EACJ,KAAK;EACL,MAAK;EACG;EACR,UAAA;EACA,UAAU;EACV,WAAW,UAAU;GACnB,MAAM,QAAQ,MAAM,OAAO;GAE3B,IAAI,OAAO,QACT,SAAS,MAAM,KAAK,KAAK,CAAC;EAE9B;CACD,CAAA,CACD,EAAA,CAAA;AAEN"}
@@ -12,14 +12,18 @@ import { UiComponentTunnelExit } from "../../UiComponentTunnel/UiComponentTunnel
12
12
  import MarkdownEditor_module_default from "../MarkdownEditor.module.mjs";
13
13
  import { ToolbarButton } from "./ToolbarButton.mjs";
14
14
  import { ModeButton } from "./ModeButton.mjs";
15
+ import { AttachmentButton } from "./AttachmentButton.mjs";
15
16
  import "react";
16
17
  import { jsx, jsxs } from "react/jsx-runtime";
17
18
  //#region src/components/MarkdownEditor/components/Toolbar.tsx
18
19
  var Toolbar = (props) => {
19
- const sharedToolButtonProps = {
20
- onPress: props.onToolPressed,
21
- isDisabled: props.isDisabled || props.currentMode === "preview"
22
- };
20
+ const isDisabled = props.isDisabled || props.currentMode === "preview";
21
+ /** The props of a tool that inserts markdown — all of them but the attachment. */
22
+ const insertTool = (type) => ({
23
+ type,
24
+ isDisabled,
25
+ onPress: () => props.onToolPressed(type)
26
+ });
23
27
  return /* @__PURE__ */ jsxs("div", {
24
28
  className: MarkdownEditor_module_default.toolbar,
25
29
  role: "toolbar",
@@ -27,45 +31,42 @@ var Toolbar = (props) => {
27
31
  className: MarkdownEditor_module_default.toolbarButtons,
28
32
  children: [
29
33
  /* @__PURE__ */ jsx(ToolbarButton, {
30
- ...sharedToolButtonProps,
31
- type: "bold",
34
+ ...insertTool("bold"),
32
35
  children: /* @__PURE__ */ jsx(IconBold, {})
33
36
  }),
34
37
  /* @__PURE__ */ jsx(ToolbarButton, {
35
- ...sharedToolButtonProps,
36
- type: "italic",
38
+ ...insertTool("italic"),
37
39
  children: /* @__PURE__ */ jsx(IconItalic, {})
38
40
  }),
39
41
  /* @__PURE__ */ jsx(ToolbarButton, {
40
- ...sharedToolButtonProps,
41
- type: "strikeThrough",
42
+ ...insertTool("strikeThrough"),
42
43
  children: /* @__PURE__ */ jsx(IconStrikeThrough, {})
43
44
  }),
44
45
  /* @__PURE__ */ jsx(ToolbarButton, {
45
- ...sharedToolButtonProps,
46
- type: "quote",
46
+ ...insertTool("quote"),
47
47
  children: /* @__PURE__ */ jsx(IconQuote, {})
48
48
  }),
49
49
  /* @__PURE__ */ jsx(ToolbarButton, {
50
- ...sharedToolButtonProps,
51
- type: "code",
50
+ ...insertTool("code"),
52
51
  children: /* @__PURE__ */ jsx(IconCode, {})
53
52
  }),
54
53
  /* @__PURE__ */ jsx(ToolbarButton, {
55
- ...sharedToolButtonProps,
56
- type: "link",
54
+ ...insertTool("link"),
57
55
  children: /* @__PURE__ */ jsx(IconLink, {})
58
56
  }),
59
57
  /* @__PURE__ */ jsx(ToolbarButton, {
60
- ...sharedToolButtonProps,
61
- type: "unorderedList",
58
+ ...insertTool("unorderedList"),
62
59
  children: /* @__PURE__ */ jsx(IconUnorderedList, {})
63
60
  }),
64
61
  /* @__PURE__ */ jsx(ToolbarButton, {
65
- ...sharedToolButtonProps,
66
- type: "orderedList",
62
+ ...insertTool("orderedList"),
67
63
  children: /* @__PURE__ */ jsx(IconOrderedList, {})
68
64
  }),
65
+ props.onFilesSelected && /* @__PURE__ */ jsx(AttachmentButton, {
66
+ accept: props.accept,
67
+ isDisabled: isDisabled || props.isReadOnly,
68
+ onSelect: props.onFilesSelected
69
+ }),
69
70
  /* @__PURE__ */ jsx(UiComponentTunnelExit, {
70
71
  id: "toolbarActions",
71
72
  component: "MarkdownEditor"
@@ -1 +1 @@
1
- {"version":3,"file":"Toolbar.mjs","names":[],"sources":["../../../../../../../../src/components/MarkdownEditor/components/Toolbar.tsx"],"sourcesContent":["import { type FC } from \"react\";\nimport styles from \"@/components/MarkdownEditor/MarkdownEditor.module.scss\";\nimport {\n IconBold,\n IconCode,\n IconItalic,\n IconLink,\n IconOrderedList,\n IconQuote,\n IconStrikeThrough,\n IconUnorderedList,\n} from \"@/components/Icon/components/icons\";\nimport {\n ToolbarButton,\n type ToolBarButtonProps,\n} from \"@/components/MarkdownEditor/components/ToolbarButton\";\nimport {\n ModeButton,\n type ModeButtonProps,\n} from \"@/components/MarkdownEditor/components/ModeButton\";\nimport { type ButtonProps } from \"@/components/Button\";\nimport { UiComponentTunnelExit } from \"@/components/UiComponentTunnel/UiComponentTunnelExit\";\n\ninterface ToolbarProps extends Pick<ButtonProps, \"isDisabled\"> {\n currentMode: ModeButtonProps[\"currentMode\"];\n onModeChange: ModeButtonProps[\"onChange\"];\n onToolPressed: ToolBarButtonProps[\"onPress\"];\n}\n\nexport const Toolbar: FC<ToolbarProps> = (props) => {\n const sharedToolButtonProps: Partial<ToolBarButtonProps> = {\n onPress: props.onToolPressed,\n isDisabled: props.isDisabled || props.currentMode === \"preview\",\n };\n\n return (\n <div className={styles.toolbar} role=\"toolbar\">\n <div className={styles.toolbarButtons}>\n <ToolbarButton {...sharedToolButtonProps} type=\"bold\">\n <IconBold />\n </ToolbarButton>\n\n <ToolbarButton {...sharedToolButtonProps} type=\"italic\">\n <IconItalic />\n </ToolbarButton>\n\n <ToolbarButton {...sharedToolButtonProps} type=\"strikeThrough\">\n <IconStrikeThrough />\n </ToolbarButton>\n\n <ToolbarButton {...sharedToolButtonProps} type=\"quote\">\n <IconQuote />\n </ToolbarButton>\n\n <ToolbarButton {...sharedToolButtonProps} type=\"code\">\n <IconCode />\n </ToolbarButton>\n\n <ToolbarButton {...sharedToolButtonProps} type=\"link\">\n <IconLink />\n </ToolbarButton>\n\n <ToolbarButton {...sharedToolButtonProps} type=\"unorderedList\">\n <IconUnorderedList />\n </ToolbarButton>\n\n <ToolbarButton {...sharedToolButtonProps} type=\"orderedList\">\n <IconOrderedList />\n </ToolbarButton>\n\n <UiComponentTunnelExit id=\"toolbarActions\" component=\"MarkdownEditor\" />\n </div>\n\n <ModeButton\n onChange={props.onModeChange}\n currentMode={props.currentMode}\n isDisabled={props.isDisabled ?? false}\n />\n </div>\n );\n};\n"],"mappings":";;;;;;;;;;;;;;;;;AA6BA,IAAa,WAA6B,UAAU;CAClD,MAAM,wBAAqD;EACzD,SAAS,MAAM;EACf,YAAY,MAAM,cAAc,MAAM,gBAAgB;CACxD;CAEA,OACE,qBAAC,OAAD;EAAK,WAAW,8BAAO;EAAS,MAAK;EAArC,UAAA,CACE,qBAAC,OAAD;GAAK,WAAW,8BAAO;GAAvB,UAAA;IACE,oBAAC,eAAD;KAAe,GAAI;KAAuB,MAAK;KAC7C,UAAA,oBAAC,UAAD,CAAW,CAAA;IACE,CAAA;IAEf,oBAAC,eAAD;KAAe,GAAI;KAAuB,MAAK;KAC7C,UAAA,oBAAC,YAAD,CAAa,CAAA;IACA,CAAA;IAEf,oBAAC,eAAD;KAAe,GAAI;KAAuB,MAAK;KAC7C,UAAA,oBAAC,mBAAD,CAAoB,CAAA;IACP,CAAA;IAEf,oBAAC,eAAD;KAAe,GAAI;KAAuB,MAAK;KAC7C,UAAA,oBAAC,WAAD,CAAY,CAAA;IACC,CAAA;IAEf,oBAAC,eAAD;KAAe,GAAI;KAAuB,MAAK;KAC7C,UAAA,oBAAC,UAAD,CAAW,CAAA;IACE,CAAA;IAEf,oBAAC,eAAD;KAAe,GAAI;KAAuB,MAAK;KAC7C,UAAA,oBAAC,UAAD,CAAW,CAAA;IACE,CAAA;IAEf,oBAAC,eAAD;KAAe,GAAI;KAAuB,MAAK;KAC7C,UAAA,oBAAC,mBAAD,CAAoB,CAAA;IACP,CAAA;IAEf,oBAAC,eAAD;KAAe,GAAI;KAAuB,MAAK;KAC7C,UAAA,oBAAC,iBAAD,CAAkB,CAAA;IACL,CAAA;IAEf,oBAAC,uBAAD;KAAuB,IAAG;KAAiB,WAAU;IAAkB,CAAA;GACpE;EAEL,CAAA,GAAA,oBAAC,YAAD;GACE,UAAU,MAAM;GAChB,aAAa,MAAM;GACnB,YAAY,MAAM,cAAc;EACjC,CAAA,CACE;;AAET"}
1
+ {"version":3,"file":"Toolbar.mjs","names":[],"sources":["../../../../../../../../src/components/MarkdownEditor/components/Toolbar.tsx"],"sourcesContent":["import { type FC } from \"react\";\nimport styles from \"@/components/MarkdownEditor/MarkdownEditor.module.scss\";\nimport {\n IconBold,\n IconCode,\n IconItalic,\n IconLink,\n IconOrderedList,\n IconQuote,\n IconStrikeThrough,\n IconUnorderedList,\n} from \"@/components/Icon/components/icons\";\nimport {\n ToolbarButton,\n type ToolBarButtonProps,\n} from \"@/components/MarkdownEditor/components/ToolbarButton\";\nimport {\n ModeButton,\n type ModeButtonProps,\n} from \"@/components/MarkdownEditor/components/ModeButton\";\nimport { type ButtonProps } from \"@/components/Button\";\nimport { type InsertType } from \"@/components/MarkdownEditor/lib/modifyValueByType\";\nimport { UiComponentTunnelExit } from \"@/components/UiComponentTunnel/UiComponentTunnelExit\";\nimport { AttachmentButton } from \"@/components/MarkdownEditor/components/AttachmentButton\";\n\ninterface ToolbarProps extends Pick<ButtonProps, \"isDisabled\"> {\n currentMode: ModeButtonProps[\"currentMode\"];\n onModeChange: ModeButtonProps[\"onChange\"];\n onToolPressed: (type: InsertType) => void;\n /** Which file types the attachment button offers. */\n accept?: string;\n /** Renders the attachment button when set. */\n onFilesSelected?: (files: File[]) => void;\n /**\n * Disables the attachment button, which a read-only editor takes no files\n * for.\n */\n isReadOnly?: boolean;\n}\n\nexport const Toolbar: FC<ToolbarProps> = (props) => {\n const isDisabled = props.isDisabled || props.currentMode === \"preview\";\n\n /** The props of a tool that inserts markdown — all of them but the attachment. */\n const insertTool = (type: InsertType): ToolBarButtonProps => ({\n type,\n isDisabled,\n onPress: () => props.onToolPressed(type),\n });\n\n return (\n <div className={styles.toolbar} role=\"toolbar\">\n <div className={styles.toolbarButtons}>\n <ToolbarButton {...insertTool(\"bold\")}>\n <IconBold />\n </ToolbarButton>\n\n <ToolbarButton {...insertTool(\"italic\")}>\n <IconItalic />\n </ToolbarButton>\n\n <ToolbarButton {...insertTool(\"strikeThrough\")}>\n <IconStrikeThrough />\n </ToolbarButton>\n\n <ToolbarButton {...insertTool(\"quote\")}>\n <IconQuote />\n </ToolbarButton>\n\n <ToolbarButton {...insertTool(\"code\")}>\n <IconCode />\n </ToolbarButton>\n\n <ToolbarButton {...insertTool(\"link\")}>\n <IconLink />\n </ToolbarButton>\n\n <ToolbarButton {...insertTool(\"unorderedList\")}>\n <IconUnorderedList />\n </ToolbarButton>\n\n <ToolbarButton {...insertTool(\"orderedList\")}>\n <IconOrderedList />\n </ToolbarButton>\n\n {props.onFilesSelected && (\n <AttachmentButton\n accept={props.accept}\n isDisabled={isDisabled || props.isReadOnly}\n onSelect={props.onFilesSelected}\n />\n )}\n\n <UiComponentTunnelExit id=\"toolbarActions\" component=\"MarkdownEditor\" />\n </div>\n\n <ModeButton\n onChange={props.onModeChange}\n currentMode={props.currentMode}\n isDisabled={props.isDisabled ?? false}\n />\n </div>\n );\n};\n"],"mappings":";;;;;;;;;;;;;;;;;;AAwCA,IAAa,WAA6B,UAAU;CAClD,MAAM,aAAa,MAAM,cAAc,MAAM,gBAAgB;;CAG7D,MAAM,cAAc,UAA0C;EAC5D;EACA;EACA,eAAe,MAAM,cAAc,IAAI;CACzC;CAEA,OACE,qBAAC,OAAD;EAAK,WAAW,8BAAO;EAAS,MAAK;EAArC,UAAA,CACE,qBAAC,OAAD;GAAK,WAAW,8BAAO;GAAvB,UAAA;IACE,oBAAC,eAAD;KAAe,GAAI,WAAW,MAAM;KAClC,UAAA,oBAAC,UAAD,CAAW,CAAA;IACE,CAAA;IAEf,oBAAC,eAAD;KAAe,GAAI,WAAW,QAAQ;KACpC,UAAA,oBAAC,YAAD,CAAa,CAAA;IACA,CAAA;IAEf,oBAAC,eAAD;KAAe,GAAI,WAAW,eAAe;KAC3C,UAAA,oBAAC,mBAAD,CAAoB,CAAA;IACP,CAAA;IAEf,oBAAC,eAAD;KAAe,GAAI,WAAW,OAAO;KACnC,UAAA,oBAAC,WAAD,CAAY,CAAA;IACC,CAAA;IAEf,oBAAC,eAAD;KAAe,GAAI,WAAW,MAAM;KAClC,UAAA,oBAAC,UAAD,CAAW,CAAA;IACE,CAAA;IAEf,oBAAC,eAAD;KAAe,GAAI,WAAW,MAAM;KAClC,UAAA,oBAAC,UAAD,CAAW,CAAA;IACE,CAAA;IAEf,oBAAC,eAAD;KAAe,GAAI,WAAW,eAAe;KAC3C,UAAA,oBAAC,mBAAD,CAAoB,CAAA;IACP,CAAA;IAEf,oBAAC,eAAD;KAAe,GAAI,WAAW,aAAa;KACzC,UAAA,oBAAC,iBAAD,CAAkB,CAAA;IACL,CAAA;IAEd,MAAM,mBACL,oBAAC,kBAAD;KACE,QAAQ,MAAM;KACd,YAAY,cAAc,MAAM;KAChC,UAAU,MAAM;IACjB,CAAA;IAGH,oBAAC,uBAAD;KAAuB,IAAG;KAAiB,WAAU;IAAkB,CAAA;GACpE;EAEL,CAAA,GAAA,oBAAC,YAAD;GACE,UAAU,MAAM;GAChB,aAAa,MAAM;GACnB,YAAY,MAAM,cAAc;EACjC,CAAA,CACE;;AAET"}
@@ -17,7 +17,7 @@ var ToolbarButton = (props) => {
17
17
  size: "s",
18
18
  variant: "plain",
19
19
  color: "dark",
20
- onPress: () => onPress?.(type),
20
+ onPress,
21
21
  children
22
22
  });
23
23
  };
@@ -1 +1 @@
1
- {"version":3,"file":"ToolbarButton.mjs","names":[],"sources":["../../../../../../../../src/components/MarkdownEditor/components/ToolbarButton.tsx"],"sourcesContent":["import { type FC } from \"react\";\nimport { type InsertType } from \"@/components/MarkdownEditor/lib/modifyValueByType\";\nimport { Button, type ButtonProps } from \"@/components/Button\";\nimport { useLocalizedStringFormatter } from \"@/components/TranslationProvider/useLocalizedStringFormatter\";\nimport locales from \"../locales/*.locale.json\";\n\nexport interface ToolBarButtonProps extends Pick<\n ButtonProps,\n \"isDisabled\" | \"children\"\n> {\n type: InsertType;\n onPress?: (type: InsertType) => void;\n}\n\nexport const ToolbarButton: FC<ToolBarButtonProps> = (props) => {\n const { children, type, onPress, ...rest } = props;\n\n const stringFormatter = useLocalizedStringFormatter(\n locales,\n \"MarkdownEditor\",\n );\n\n return (\n <Button\n {...rest}\n tunnel={null}\n data-button-type={type}\n aria-label={stringFormatter.format(`toolbar.${type}`)}\n size=\"s\"\n variant=\"plain\"\n color=\"dark\"\n onPress={() => onPress?.(type)}\n >\n {children}\n </Button>\n );\n};\n"],"mappings":";;;;;;;;AAcA,IAAa,iBAAyC,UAAU;CAC9D,MAAM,EAAE,UAAU,MAAM,SAAS,GAAG,SAAS;CAE7C,MAAM,kBAAkB,4BACtB,kBACA,gBACF;CAEA,OACE,oBAAC,QAAD;EACE,GAAI;EACJ,QAAQ;EACR,oBAAkB;EAClB,cAAY,gBAAgB,OAAO,WAAW,MAAM;EACpD,MAAK;EACL,SAAQ;EACR,OAAM;EACN,eAAe,UAAU,IAAI;EAE5B;CACK,CAAA;AAEZ"}
1
+ {"version":3,"file":"ToolbarButton.mjs","names":[],"sources":["../../../../../../../../src/components/MarkdownEditor/components/ToolbarButton.tsx"],"sourcesContent":["import { type FC } from \"react\";\nimport { type InsertType } from \"@/components/MarkdownEditor/lib/modifyValueByType\";\nimport { Button, type ButtonProps } from \"@/components/Button\";\nimport { useLocalizedStringFormatter } from \"@/components/TranslationProvider/useLocalizedStringFormatter\";\nimport locales from \"../locales/*.locale.json\";\n\n/**\n * What a toolbar button is: every markdown syntax it inserts, plus the one tool\n * that does something else. The name is also the `toolbar.<type>` locale key of\n * the button's label.\n */\nexport type ToolbarButtonType = InsertType | \"attachment\";\n\nexport interface ToolBarButtonProps extends Pick<\n ButtonProps,\n \"isDisabled\" | \"children\"\n> {\n type: ToolbarButtonType;\n onPress?: () => void;\n}\n\nexport const ToolbarButton: FC<ToolBarButtonProps> = (props) => {\n const { children, type, onPress, ...rest } = props;\n\n const stringFormatter = useLocalizedStringFormatter(\n locales,\n \"MarkdownEditor\",\n );\n\n return (\n <Button\n {...rest}\n tunnel={null}\n data-button-type={type}\n aria-label={stringFormatter.format(`toolbar.${type}`)}\n size=\"s\"\n variant=\"plain\"\n color=\"dark\"\n onPress={onPress}\n >\n {children}\n </Button>\n );\n};\n"],"mappings":";;;;;;;;AAqBA,IAAa,iBAAyC,UAAU;CAC9D,MAAM,EAAE,UAAU,MAAM,SAAS,GAAG,SAAS;CAE7C,MAAM,kBAAkB,4BACtB,kBACA,gBACF;CAEA,OACE,oBAAC,QAAD;EACE,GAAI;EACJ,QAAQ;EACR,oBAAkB;EAClB,cAAY,gBAAgB,OAAO,WAAW,MAAM;EACpD,MAAK;EACL,SAAQ;EACR,OAAM;EACG;EAER;CACK,CAAA;AAEZ"}
@@ -0,0 +1,73 @@
1
+ "use client"
2
+ /* */
3
+ //#region src/components/MarkdownEditor/lib/fileUpload.ts
4
+ var escapeComment = (fileName) => fileName.replace(/-(?=-)/g, "- ");
5
+ var escapeLinkLabel = (label) => label.replace(/[\\[\]]/g, "\\$&").replace(/\s+/g, " ").trim();
6
+ var escapeLinkTarget = (url) => /[\s()]/.test(url) ? `<${url.replace(/[<>]/g, encodeURIComponent)}>` : url;
7
+ /**
8
+ * What sits at the cursor while a file uploads. An HTML comment, so it stays
9
+ * out of the rendered preview and out of a message sent before the upload
10
+ * finished — the same marker GitHub leaves behind.
11
+ *
12
+ * @internal
13
+ */
14
+ var uploadPlaceholder = (fileName) => `<!-- Uploading "${escapeComment(fileName)}"... -->`;
15
+ /**
16
+ * The markdown a finished upload leaves behind: an embed for an image, a link
17
+ * for everything else.
18
+ *
19
+ * @internal
20
+ */
21
+ var uploadedFileMarkdown = (file, upload) => {
22
+ const link = `[${escapeLinkLabel(upload.name ?? file.name)}](${escapeLinkTarget(upload.url)})`;
23
+ return file.type.startsWith("image/") ? `!${link}` : link;
24
+ };
25
+ /**
26
+ * Splices `insert` into `value` at `caret`, on a line of its own, and reports
27
+ * where the cursor ends up.
28
+ *
29
+ * @internal
30
+ */
31
+ var insertAtCaret = (value, insert, caret) => {
32
+ const before = value.slice(0, caret);
33
+ const after = value.slice(caret);
34
+ const inserted = `${before.length > 0 && !before.endsWith("\n") ? "\n" : ""}${insert}${after.startsWith("\n") ? "" : "\n"}`;
35
+ return {
36
+ value: `${before}${inserted}${after}`,
37
+ selectionStart: caret + inserted.length,
38
+ selectionEnd: caret + inserted.length
39
+ };
40
+ };
41
+ /**
42
+ * Replaces the first occurrence of `search`, carrying the cursor along. Returns
43
+ * `null` when the editor no longer contains it — the user deleted the
44
+ * placeholder while the upload was running, and the result is dropped.
45
+ *
46
+ * @internal
47
+ */
48
+ var replacePlaceholder = (value, search, replacement, selectionStart, selectionEnd) => {
49
+ const index = value.indexOf(search);
50
+ if (index === -1) return null;
51
+ const end = index + search.length;
52
+ const delta = replacement.length - search.length;
53
+ const shift = (position) => {
54
+ if (position <= index) return position;
55
+ return position >= end ? position + delta : index + replacement.length;
56
+ };
57
+ return {
58
+ value: value.slice(0, index) + replacement + value.slice(end),
59
+ selectionStart: shift(selectionStart),
60
+ selectionEnd: shift(selectionEnd)
61
+ };
62
+ };
63
+ /**
64
+ * Takes a placeholder back out after a failed upload, including the line break
65
+ * it was inserted with.
66
+ *
67
+ * @internal
68
+ */
69
+ var removePlaceholder = (value, placeholder, selectionStart, selectionEnd) => replacePlaceholder(value, `${placeholder}\n`, "", selectionStart, selectionEnd) ?? replacePlaceholder(value, placeholder, "", selectionStart, selectionEnd);
70
+ //#endregion
71
+ export { insertAtCaret, removePlaceholder, replacePlaceholder, uploadPlaceholder, uploadedFileMarkdown };
72
+
73
+ //# sourceMappingURL=fileUpload.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fileUpload.mjs","names":[],"sources":["../../../../../../../../src/components/MarkdownEditor/lib/fileUpload.ts"],"sourcesContent":["/**\n * The result of uploading a file dropped into, pasted into or selected in a\n * `MarkdownEditor`.\n */\nexport interface MarkdownEditorUpload {\n /** The URL the uploaded file is available at. */\n url: string;\n /**\n * The link text of the inserted markdown — the alt text for an image.\n * Defaults to the name of the uploaded file.\n */\n name?: string;\n}\n\n/** A new editor value together with where the cursor ends up in it. */\nexport interface ValueEdit {\n value: string;\n selectionStart: number;\n selectionEnd: number;\n}\n\n/*\n * A file name that closes the comment early would leave the rest of it as\n * visible text. Every way to close one starts with `--`: `-->` does, `--!>`\n * does too, and dropping only `-->` can forge a new one — `a-->>b` collapses to\n * `a-->b`. Breaking up every `--` is what holds in all three.\n */\nconst escapeComment = (fileName: string) => fileName.replace(/-(?=-)/g, \"- \");\n\n/*\n * Brackets are what ends a markdown link label, and a line break ends the link\n * altogether. A file name may legally contain both.\n *\n * The backslash is escaped in the same pass, and has to be: escaping only the\n * brackets turns `a\\]b` into `a\\\\]b`, which markdown reads as an escaped\n * backslash followed by a bare `]` — the label ends right there.\n */\nconst escapeLinkLabel = (label: string) =>\n label\n .replace(/[\\\\[\\]]/g, \"\\\\$&\")\n .replace(/\\s+/g, \" \")\n .trim();\n\n/*\n * A bare link target ends at the first whitespace or unbalanced parenthesis.\n * Pointy brackets take a target that contains either — and are themselves the\n * only thing that cannot appear inside them.\n */\nconst escapeLinkTarget = (url: string) =>\n /[\\s()]/.test(url) ? `<${url.replace(/[<>]/g, encodeURIComponent)}>` : url;\n\n/**\n * What sits at the cursor while a file uploads. An HTML comment, so it stays\n * out of the rendered preview and out of a message sent before the upload\n * finished — the same marker GitHub leaves behind.\n *\n * @internal\n */\nexport const uploadPlaceholder = (fileName: string): string =>\n `<!-- Uploading \"${escapeComment(fileName)}\"... -->`;\n\n/**\n * The markdown a finished upload leaves behind: an embed for an image, a link\n * for everything else.\n *\n * @internal\n */\nexport const uploadedFileMarkdown = (\n file: Pick<File, \"name\" | \"type\">,\n upload: MarkdownEditorUpload,\n): string => {\n const label = escapeLinkLabel(upload.name ?? file.name);\n const target = escapeLinkTarget(upload.url);\n const link = `[${label}](${target})`;\n\n return file.type.startsWith(\"image/\") ? `!${link}` : link;\n};\n\n/**\n * Splices `insert` into `value` at `caret`, on a line of its own, and reports\n * where the cursor ends up.\n *\n * @internal\n */\nexport const insertAtCaret = (\n value: string,\n insert: string,\n caret: number,\n): ValueEdit => {\n const before = value.slice(0, caret);\n const after = value.slice(caret);\n\n const prefix = before.length > 0 && !before.endsWith(\"\\n\") ? \"\\n\" : \"\";\n const suffix = after.startsWith(\"\\n\") ? \"\" : \"\\n\";\n const inserted = `${prefix}${insert}${suffix}`;\n\n return {\n value: `${before}${inserted}${after}`,\n selectionStart: caret + inserted.length,\n selectionEnd: caret + inserted.length,\n };\n};\n\n/**\n * Replaces the first occurrence of `search`, carrying the cursor along. Returns\n * `null` when the editor no longer contains it — the user deleted the\n * placeholder while the upload was running, and the result is dropped.\n *\n * @internal\n */\nexport const replacePlaceholder = (\n value: string,\n search: string,\n replacement: string,\n selectionStart: number,\n selectionEnd: number,\n): ValueEdit | null => {\n const index = value.indexOf(search);\n\n if (index === -1) {\n return null;\n }\n\n const end = index + search.length;\n const delta = replacement.length - search.length;\n\n /*\n * A cursor before the placeholder does not move, one after it moves by the\n * length difference, and one inside it — the user was typing where the file\n * landed — comes to rest behind the replacement.\n */\n const shift = (position: number) => {\n if (position <= index) {\n return position;\n }\n return position >= end ? position + delta : index + replacement.length;\n };\n\n return {\n value: value.slice(0, index) + replacement + value.slice(end),\n selectionStart: shift(selectionStart),\n selectionEnd: shift(selectionEnd),\n };\n};\n\n/**\n * Takes a placeholder back out after a failed upload, including the line break\n * it was inserted with.\n *\n * @internal\n */\nexport const removePlaceholder = (\n value: string,\n placeholder: string,\n selectionStart: number,\n selectionEnd: number,\n): ValueEdit | null =>\n replacePlaceholder(\n value,\n `${placeholder}\\n`,\n \"\",\n selectionStart,\n selectionEnd,\n ) ?? replacePlaceholder(value, placeholder, \"\", selectionStart, selectionEnd);\n"],"mappings":";;;AA2BA,IAAM,iBAAiB,aAAqB,SAAS,QAAQ,WAAW,IAAI;AAU5E,IAAM,mBAAmB,UACvB,MACG,QAAQ,YAAY,MAAM,CAAC,CAC3B,QAAQ,QAAQ,GAAG,CAAC,CACpB,KAAK;AAOV,IAAM,oBAAoB,QACxB,SAAS,KAAK,GAAG,IAAI,IAAI,IAAI,QAAQ,SAAS,kBAAkB,EAAE,KAAK;;;;;;;;AASzE,IAAa,qBAAqB,aAChC,mBAAmB,cAAc,QAAQ,EAAE;;;;;;;AAQ7C,IAAa,wBACX,MACA,WACW;CAGX,MAAM,OAAO,IAFC,gBAAgB,OAAO,QAAQ,KAAK,IAEjC,EAAM,IADR,iBAAiB,OAAO,GACZ,EAAO;CAElC,OAAO,KAAK,KAAK,WAAW,QAAQ,IAAI,IAAI,SAAS;AACvD;;;;;;;AAQA,IAAa,iBACX,OACA,QACA,UACc;CACd,MAAM,SAAS,MAAM,MAAM,GAAG,KAAK;CACnC,MAAM,QAAQ,MAAM,MAAM,KAAK;CAI/B,MAAM,WAAW,GAFF,OAAO,SAAS,KAAK,CAAC,OAAO,SAAS,IAAI,IAAI,OAAO,KAEvC,SADd,MAAM,WAAW,IAAI,IAAI,KAAK;CAG7C,OAAO;EACL,OAAO,GAAG,SAAS,WAAW;EAC9B,gBAAgB,QAAQ,SAAS;EACjC,cAAc,QAAQ,SAAS;CACjC;AACF;;;;;;;;AASA,IAAa,sBACX,OACA,QACA,aACA,gBACA,iBACqB;CACrB,MAAM,QAAQ,MAAM,QAAQ,MAAM;CAElC,IAAI,UAAU,IACZ,OAAO;CAGT,MAAM,MAAM,QAAQ,OAAO;CAC3B,MAAM,QAAQ,YAAY,SAAS,OAAO;CAO1C,MAAM,SAAS,aAAqB;EAClC,IAAI,YAAY,OACd,OAAO;EAET,OAAO,YAAY,MAAM,WAAW,QAAQ,QAAQ,YAAY;CAClE;CAEA,OAAO;EACL,OAAO,MAAM,MAAM,GAAG,KAAK,IAAI,cAAc,MAAM,MAAM,GAAG;EAC5D,gBAAgB,MAAM,cAAc;EACpC,cAAc,MAAM,YAAY;CAClC;AACF;;;;;;;AAQA,IAAa,qBACX,OACA,aACA,gBACA,iBAEA,mBACE,OACA,GAAG,YAAY,KACf,IACA,gBACA,YACF,KAAK,mBAAmB,OAAO,aAAa,IAAI,gBAAgB,YAAY"}
@@ -0,0 +1,141 @@
1
+ "use client"
2
+ /* */
3
+ import { useLocalizedStringFormatter } from "../../TranslationProvider/useLocalizedStringFormatter.mjs";
4
+ import { matchesAccept } from "../../../lib/files/acceptedFiles.mjs";
5
+ import __locale_default from "../../../../../../_virtual/_24.locale.mjs";
6
+ import { insertAtCaret, removePlaceholder, replacePlaceholder, uploadPlaceholder, uploadedFileMarkdown } from "./fileUpload.mjs";
7
+ import { useEffect, useRef, useState } from "react";
8
+ import { announce } from "@react-aria/live-announcer";
9
+ //#region src/components/MarkdownEditor/lib/useFileUpload.ts
10
+ /** The files of a paste or a drop that `accept` covers. */
11
+ var acceptedFrom = (files, accept) => Array.from(files ?? []).filter((file) => matchesAccept(file, accept));
12
+ /**
13
+ * Drop, paste and toolbar uploads for the `MarkdownEditor`: a placeholder
14
+ * comment goes to the cursor right away and is replaced by a markdown link once
15
+ * the consumer's upload resolves.
16
+ *
17
+ * @internal
18
+ */
19
+ var useFileUpload = (options) => {
20
+ const { accept, uploadFile, isDisabled, value, onChange, textAreaRef } = options;
21
+ const stringFormatter = useLocalizedStringFormatter(__locale_default, "MarkdownEditor");
22
+ const isEnabled = !!uploadFile && !isDisabled;
23
+ const valueRef = useRef(value);
24
+ useEffect(() => {
25
+ valueRef.current = value;
26
+ }, [value]);
27
+ const readValue = () => textAreaRef.current?.value ?? valueRef.current;
28
+ const wasFocusedRef = useRef(false);
29
+ useEffect(() => {
30
+ const textArea = textAreaRef.current;
31
+ if (!textArea) return;
32
+ const rememberFocus = () => {
33
+ wasFocusedRef.current = true;
34
+ };
35
+ textArea.addEventListener("focus", rememberFocus);
36
+ return () => textArea.removeEventListener("focus", rememberFocus);
37
+ }, [textAreaRef]);
38
+ const readSelection = () => {
39
+ const textArea = textAreaRef.current;
40
+ const fallback = readValue().length;
41
+ return {
42
+ start: textArea?.selectionStart ?? fallback,
43
+ end: textArea?.selectionEnd ?? fallback
44
+ };
45
+ };
46
+ const applyEdit = (edit) => {
47
+ valueRef.current = edit.value;
48
+ const textArea = textAreaRef.current;
49
+ if (textArea) {
50
+ textArea.value = edit.value;
51
+ requestAnimationFrame(() => {
52
+ textArea.setSelectionRange(edit.selectionStart, edit.selectionEnd);
53
+ });
54
+ }
55
+ onChange(edit.value);
56
+ };
57
+ const runUpload = async (upload, file, placeholder) => {
58
+ announce(stringFormatter.format("upload.pending", { name: file.name }), "polite");
59
+ try {
60
+ const result = await upload(file);
61
+ const selection = readSelection();
62
+ const edit = replacePlaceholder(readValue(), placeholder, uploadedFileMarkdown(file, result), selection.start, selection.end);
63
+ if (edit) applyEdit(edit);
64
+ announce(stringFormatter.format("upload.succeeded", { name: file.name }), "polite");
65
+ } catch {
66
+ const selection = readSelection();
67
+ const edit = removePlaceholder(readValue(), placeholder, selection.start, selection.end);
68
+ if (edit) applyEdit(edit);
69
+ announce(stringFormatter.format("upload.failed", { name: file.name }), "assertive");
70
+ }
71
+ };
72
+ const uploadFiles = (files) => {
73
+ if (!uploadFile || files.length === 0) return;
74
+ const textArea = textAreaRef.current;
75
+ const caret = textArea && wasFocusedRef.current ? textArea.selectionStart : readValue().length;
76
+ const uploads = files.map((file) => ({
77
+ file,
78
+ placeholder: uploadPlaceholder(file.name)
79
+ }));
80
+ applyEdit(insertAtCaret(readValue(), uploads.map((upload) => upload.placeholder).join("\n"), caret));
81
+ textArea?.focus();
82
+ for (const upload of uploads) runUpload(uploadFile, upload.file, upload.placeholder);
83
+ };
84
+ const uploadFilesRef = useRef(uploadFiles);
85
+ uploadFilesRef.current = uploadFiles;
86
+ useEffect(() => {
87
+ const textArea = textAreaRef.current;
88
+ if (!textArea || !isEnabled) return;
89
+ const handlePaste = (event) => {
90
+ const files = acceptedFrom(event.clipboardData?.files, accept);
91
+ if (files.length === 0) return;
92
+ event.preventDefault();
93
+ uploadFilesRef.current(files);
94
+ };
95
+ textArea.addEventListener("paste", handlePaste);
96
+ return () => textArea.removeEventListener("paste", handlePaste);
97
+ }, [
98
+ isEnabled,
99
+ accept,
100
+ textAreaRef
101
+ ]);
102
+ const dragHasFiles = (event) => event.dataTransfer.types.includes("Files");
103
+ const dragDepth = useRef(0);
104
+ const [isDropTarget, setIsDropTarget] = useState(false);
105
+ const endDrag = () => {
106
+ dragDepth.current = 0;
107
+ setIsDropTarget(false);
108
+ };
109
+ return {
110
+ dropProps: isEnabled ? {
111
+ onDragEnter: (event) => {
112
+ if (!dragHasFiles(event)) return;
113
+ event.preventDefault();
114
+ dragDepth.current += 1;
115
+ setIsDropTarget(true);
116
+ },
117
+ onDragOver: (event) => {
118
+ if (!dragHasFiles(event)) return;
119
+ event.preventDefault();
120
+ event.dataTransfer.dropEffect = "copy";
121
+ },
122
+ onDragLeave: (event) => {
123
+ if (!dragHasFiles(event)) return;
124
+ dragDepth.current = Math.max(0, dragDepth.current - 1);
125
+ if (dragDepth.current === 0) setIsDropTarget(false);
126
+ },
127
+ onDrop: (event) => {
128
+ if (!dragHasFiles(event)) return;
129
+ event.preventDefault();
130
+ endDrag();
131
+ uploadFilesRef.current(acceptedFrom(event.dataTransfer.files, accept));
132
+ }
133
+ } : {},
134
+ isDropTarget,
135
+ uploadFiles: uploadFile ? uploadFiles : void 0
136
+ };
137
+ };
138
+ //#endregion
139
+ export { useFileUpload };
140
+
141
+ //# sourceMappingURL=useFileUpload.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useFileUpload.mjs","names":[],"sources":["../../../../../../../../src/components/MarkdownEditor/lib/useFileUpload.ts"],"sourcesContent":["import {\n type DragEvent as ReactDragEvent,\n type DragEventHandler,\n type RefObject,\n useEffect,\n useRef,\n useState,\n} from \"react\";\nimport { announce } from \"@react-aria/live-announcer\";\nimport { matchesAccept } from \"@/lib/files/acceptedFiles\";\nimport {\n insertAtCaret,\n type MarkdownEditorUpload,\n removePlaceholder,\n replacePlaceholder,\n uploadedFileMarkdown,\n uploadPlaceholder,\n type ValueEdit,\n} from \"@/components/MarkdownEditor/lib/fileUpload\";\nimport { useLocalizedStringFormatter } from \"@/components/TranslationProvider/useLocalizedStringFormatter\";\nimport locales from \"../locales/*.locale.json\";\n\n/** The files of a paste or a drop that `accept` covers. */\nconst acceptedFrom = (files: FileList | null | undefined, accept?: string) =>\n Array.from(files ?? []).filter((file) => matchesAccept(file, accept));\n\n/** @internal */\nexport type MarkdownEditorFileUploadHandler = (\n file: File,\n) => Promise<MarkdownEditorUpload>;\n\ninterface Options {\n /** Which file types to take, as an `accept` attribute value. */\n accept?: string;\n /** Uploads one file. Uploading is off entirely while this is undefined. */\n uploadFile?: MarkdownEditorFileUploadHandler;\n /** Turns dropping, pasting and the toolbar button off. */\n isDisabled?: boolean;\n value: string;\n onChange: (value: string) => void;\n textAreaRef: RefObject<HTMLTextAreaElement | null>;\n}\n\ninterface DropProps {\n onDragEnter?: DragEventHandler;\n onDragOver?: DragEventHandler;\n onDragLeave?: DragEventHandler;\n onDrop?: DragEventHandler;\n}\n\ninterface FileUpload {\n /** Drop handling for the editor's root element. */\n dropProps: DropProps;\n /** Whether a drag is currently over the editor. */\n isDropTarget: boolean;\n /**\n * Uploads the files the toolbar button picked. `undefined` while uploading is\n * off.\n */\n uploadFiles?: (files: File[]) => void;\n}\n\n/**\n * Drop, paste and toolbar uploads for the `MarkdownEditor`: a placeholder\n * comment goes to the cursor right away and is replaced by a markdown link once\n * the consumer's upload resolves.\n *\n * @internal\n */\nexport const useFileUpload = (options: Options): FileUpload => {\n const { accept, uploadFile, isDisabled, value, onChange, textAreaRef } =\n options;\n\n const stringFormatter = useLocalizedStringFormatter(\n locales,\n \"MarkdownEditor\",\n );\n\n const isEnabled = !!uploadFile && !isDisabled;\n\n /*\n * An upload resolves long after the drop, and the editor has kept taking\n * input in the meantime. The textarea's own value is the only one that is\n * current by then — the `value` prop is a render old whenever two uploads\n * resolve in the same tick.\n */\n const valueRef = useRef(value);\n useEffect(() => {\n valueRef.current = value;\n }, [value]);\n\n const readValue = () => textAreaRef.current?.value ?? valueRef.current;\n\n /*\n * A textarea keeps its `selectionStart` after losing focus, so the caret is\n * still readable once the attachment button has taken the focus away. What it\n * cannot tell apart is a caret at position 0 from one that was never placed —\n * both read 0, and appending is the better answer for the second.\n */\n const wasFocusedRef = useRef(false);\n useEffect(() => {\n const textArea = textAreaRef.current;\n\n if (!textArea) {\n return;\n }\n\n const rememberFocus = () => {\n wasFocusedRef.current = true;\n };\n\n textArea.addEventListener(\"focus\", rememberFocus);\n return () => textArea.removeEventListener(\"focus\", rememberFocus);\n }, [textAreaRef]);\n\n const readSelection = () => {\n const textArea = textAreaRef.current;\n const fallback = readValue().length;\n return {\n start: textArea?.selectionStart ?? fallback,\n end: textArea?.selectionEnd ?? fallback,\n };\n };\n\n const applyEdit = (edit: ValueEdit) => {\n valueRef.current = edit.value;\n\n const textArea = textAreaRef.current;\n if (textArea) {\n /*\n * Written before the re-render so an upload resolving in the same tick\n * reads this one's result, and the selection restored after it, once\n * React has rendered the controlled value.\n */\n textArea.value = edit.value;\n requestAnimationFrame(() => {\n textArea.setSelectionRange(edit.selectionStart, edit.selectionEnd);\n });\n }\n\n onChange(edit.value);\n };\n\n const runUpload = async (\n upload: MarkdownEditorFileUploadHandler,\n file: File,\n placeholder: string,\n ) => {\n announce(\n stringFormatter.format(\"upload.pending\", { name: file.name }),\n \"polite\",\n );\n\n try {\n const result = await upload(file);\n const selection = readSelection();\n const edit = replacePlaceholder(\n readValue(),\n placeholder,\n uploadedFileMarkdown(file, result),\n selection.start,\n selection.end,\n );\n\n if (edit) {\n applyEdit(edit);\n }\n\n announce(\n stringFormatter.format(\"upload.succeeded\", { name: file.name }),\n \"polite\",\n );\n } catch {\n const selection = readSelection();\n const edit = removePlaceholder(\n readValue(),\n placeholder,\n selection.start,\n selection.end,\n );\n\n if (edit) {\n applyEdit(edit);\n }\n\n announce(\n stringFormatter.format(\"upload.failed\", { name: file.name }),\n \"assertive\",\n );\n }\n };\n\n const uploadFiles = (files: File[]) => {\n if (!uploadFile || files.length === 0) {\n return;\n }\n\n const textArea = textAreaRef.current;\n const caret =\n textArea && wasFocusedRef.current\n ? textArea.selectionStart\n : readValue().length;\n\n const uploads = files.map((file) => ({\n file,\n placeholder: uploadPlaceholder(file.name),\n }));\n\n applyEdit(\n insertAtCaret(\n readValue(),\n uploads.map((upload) => upload.placeholder).join(\"\\n\"),\n caret,\n ),\n );\n textArea?.focus();\n\n for (const upload of uploads) {\n void runUpload(uploadFile, upload.file, upload.placeholder);\n }\n };\n\n const uploadFilesRef = useRef(uploadFiles);\n uploadFilesRef.current = uploadFiles;\n\n /*\n * The paste listener is attached imperatively rather than through react-aria's\n * `useClipboard`, which calls `preventDefault` on every paste it sees and\n * would take plain text pasting with it.\n */\n useEffect(() => {\n const textArea = textAreaRef.current;\n\n if (!textArea || !isEnabled) {\n return;\n }\n\n const handlePaste = (event: ClipboardEvent) => {\n const files = acceptedFrom(event.clipboardData?.files, accept);\n\n if (files.length === 0) {\n return;\n }\n\n event.preventDefault();\n uploadFilesRef.current(files);\n };\n\n textArea.addEventListener(\"paste\", handlePaste);\n return () => textArea.removeEventListener(\"paste\", handlePaste);\n }, [isEnabled, accept, textAreaRef]);\n\n /*\n * Native handlers rather than react-aria's `useDrop`: that one claims every\n * drag it sees — its `onDrop` calls `preventDefault` and `stopPropagation`\n * unconditionally — which is right for a dedicated drop surface like\n * `FileDropZone`, but here it would swallow a plain text drag into the\n * textarea. A drag is claimed only when it carries files, and\n * `dataTransfer.types` is the only place that says so: the public\n * `DragTypes` of `getDropOperation` offers just `has(type)`, which cannot\n * express it — the wildcard type is always true, and WebKit reports every\n * file drag as an unknown type that answers true to anything.\n */\n const dragHasFiles = (event: ReactDragEvent) =>\n event.dataTransfer.types.includes(\"Files\");\n\n /*\n * `dragenter` and `dragleave` fire again for every child the pointer\n * crosses, so the highlight follows their balance rather than the last one.\n */\n const dragDepth = useRef(0);\n const [isDropTarget, setIsDropTarget] = useState(false);\n\n const endDrag = () => {\n dragDepth.current = 0;\n setIsDropTarget(false);\n };\n\n const dropProps: DropProps = isEnabled\n ? {\n onDragEnter: (event) => {\n if (!dragHasFiles(event)) {\n return;\n }\n event.preventDefault();\n dragDepth.current += 1;\n setIsDropTarget(true);\n },\n onDragOver: (event) => {\n if (!dragHasFiles(event)) {\n return;\n }\n // Without this the browser refuses the drop and opens the file instead.\n event.preventDefault();\n event.dataTransfer.dropEffect = \"copy\";\n },\n onDragLeave: (event) => {\n if (!dragHasFiles(event)) {\n return;\n }\n dragDepth.current = Math.max(0, dragDepth.current - 1);\n if (dragDepth.current === 0) {\n setIsDropTarget(false);\n }\n },\n onDrop: (event) => {\n if (!dragHasFiles(event)) {\n return;\n }\n event.preventDefault();\n endDrag();\n uploadFilesRef.current(\n acceptedFrom(event.dataTransfer.files, accept),\n );\n },\n }\n : {};\n\n return {\n dropProps,\n isDropTarget,\n uploadFiles: uploadFile ? uploadFiles : undefined,\n };\n};\n"],"mappings":"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"}
@@ -86,14 +86,16 @@ var Modal = flowComponent("Modal", (props) => {
86
86
  };
87
87
  }),
88
88
  closeOverlay: dynamic((props) => {
89
- if (props.closeOverlay === void 0) return;
90
- if (props.closeOverlay instanceof OverlayController || typeof props.closeOverlay === "string") return {
89
+ const { closeOverlay } = props;
90
+ if (closeOverlay === void 0 || closeOverlay === false) return closeOverlay;
91
+ if (closeOverlay === true) return { bypassConfirmation: true };
92
+ if (closeOverlay instanceof OverlayController || typeof closeOverlay === "string") return {
91
93
  bypassConfirmation: true,
92
- overlay: props.closeOverlay
94
+ overlay: closeOverlay
93
95
  };
94
96
  return {
95
97
  bypassConfirmation: true,
96
- ...props.closeOverlay
98
+ ...closeOverlay
97
99
  };
98
100
  })
99
101
  }
@@ -1 +1 @@
1
- {"version":3,"file":"Modal.mjs","names":[],"sources":["../../../../../../../src/components/Modal/Modal.tsx"],"sourcesContent":["import { type PropsWithChildren, type ReactNode, Suspense, useId } from \"react\";\nimport styles from \"./Modal.module.scss\";\nimport clsx from \"clsx\";\nimport {\n dynamic,\n type PropsContext,\n PropsContextProvider,\n} from \"@/lib/propsContext\";\nimport { OverlayController } from \"@/lib/controller/overlay\";\nimport type { FlowComponentProps } from \"@/lib/componentFactory/flowComponent\";\nimport { flowComponent } from \"@/lib/componentFactory/flowComponent\";\nimport { Overlay, type OverlayProps } from \"@/components/Overlay/Overlay\";\nimport { Action } from \"@/components/Action\";\nimport { IconClose } from \"@/components/Icon/components/icons\";\nimport type { PropsWithClassName } from \"@/lib/types/props\";\nimport ButtonView from \"@/views/ButtonView\";\nimport DivView from \"@/views/DivView\";\nimport { OffCanvasSuspenseFallback } from \"@/components/Modal/components/OffCanvasSuspenseFallback\";\nimport { ConfirmUnsavedChangesModal } from \"@/components/Modal/components/ConfirmUnsavedChangesModal/ConfirmUnsavedChangesModal\";\nimport Wrap from \"@/components/Wrap\";\nimport { ClearPropsContext } from \"@/components/ClearPropsContext/ClearPropsContext\";\nimport ClearPropsContextView from \"@/views/ClearPropsContextView\";\nimport { useLocalizedStringFormatter } from \"@/components/TranslationProvider\";\nimport { useVirtualKeyboardVisible } from \"@/lib/hooks/dom/useVirtualKeyboardVisible\";\nimport locales from \"./locales/*.locale.json\";\n\ntype SupportedOverlayProps = Pick<\n OverlayProps,\n \"isOpen\" | \"isDefaultOpen\" | \"onOpen\" | \"onClose\" | \"onOpenChange\"\n>;\n\nexport interface ModalProps\n extends\n PropsWithChildren,\n FlowComponentProps,\n PropsWithClassName,\n SupportedOverlayProps {\n /** The size of the modal. @default \"s\" */\n size?: \"s\" | \"m\" | \"l\";\n /** Whether the modal should be displayed as an off canvas. */\n offCanvas?: boolean;\n /**\n * Whether the off canvas should be displayed on the right or left side of the\n * screen. @default \"right\"\n */\n offCanvasOrientation?: \"left\" | \"right\";\n /** An overlay controller to control the modal state. */\n controller?: OverlayController;\n /**\n * Accepts \"actionConfirm\" to use the modal as a confirmation modal for an\n * action.\n */\n slot?: string;\n /** Whether the modal can be closed by clicking outside of it. */\n isDismissable?: boolean;\n /** Whether the close button should be visible */\n showCloseButton?: boolean;\n /**\n * Whether closing the modal must be confirmed – use it to protect unsaved\n * changes. `false` does not switch off the confirmation of a dirty `Form`\n * inside the modal; set `confirmModalCloseOnUnsavedChanges={false}` on the\n * `Form` for that.\n */\n confirmOnClose?: boolean;\n}\n\nexport const Modal = flowComponent(\"Modal\", (props) => {\n const {\n size = \"s\",\n offCanvas,\n controller,\n children,\n ref,\n className,\n offCanvasOrientation = \"right\",\n showCloseButton,\n ...overlayProps\n } = props;\n\n const stringFormatter = useLocalizedStringFormatter(locales, \"Modal\");\n\n // On mobile the header/footer are docked (sticky) while at rest, but flow\n // normally while the on-screen keyboard is open so the content gets the full\n // available space (see Modal.module.scss). #2423\n const keyboardVisible = useVirtualKeyboardVisible();\n\n const rootClassName = clsx(\n offCanvas ? styles.offCanvas : styles.modal,\n styles[`size-${size}`],\n offCanvasOrientation === \"left\" && styles[\"left\"],\n keyboardVisible && styles.keyboardVisible,\n className,\n );\n\n const generatedId = useId();\n\n const header = (children: ReactNode) => (\n <>\n <DivView className={styles.headerTitle}>{children}</DivView>\n <ClearPropsContext>\n <ClearPropsContextView>\n <Action closeModal={{ bypassConfirmation: true }}>\n <ButtonView\n variant=\"plain\"\n color=\"secondary\"\n aria-label={stringFormatter.format(\"close\")}\n className={clsx(\n styles.closeButton,\n showCloseButton === true\n ? styles.alwaysVisible\n : showCloseButton === false\n ? styles.alwaysHidden\n : undefined,\n )}\n >\n <IconClose />\n </ButtonView>\n </Action>\n </ClearPropsContextView>\n </ClearPropsContext>\n </>\n );\n\n const nestedHeadingLevel = 3;\n\n const nestedHeadingProps: PropsContext = {\n Heading: { level: nestedHeadingLevel },\n Section: {\n Header: { Heading: { level: nestedHeadingLevel } },\n Heading: { level: nestedHeadingLevel },\n hideSeparator: true,\n },\n Header: { Heading: { level: nestedHeadingLevel } },\n };\n\n const propsContext: PropsContext = {\n Content: {\n ...nestedHeadingProps,\n className: styles.content,\n },\n ColumnLayout: {\n ...nestedHeadingProps,\n l: [2, 1],\n m: [1],\n className: styles.columnLayout,\n AccentBox: { className: styles.accentBox, backgroundColor: \"neutral\" },\n wrapWith: <ClearPropsContext />,\n },\n Heading: {\n className: styles.header,\n level: 2,\n children: dynamic((props) => header(props.children)),\n id: generatedId,\n },\n ActionGroup: {\n className: styles.actionGroup,\n spacing: \"m\",\n Action: {\n closeModal: dynamic((props) => {\n if (props.closeModal === undefined) {\n return;\n }\n if (props.closeModal === true) {\n return { bypassConfirmation: true };\n }\n return {\n bypassConfirmation: true,\n ...props.closeModal,\n };\n }),\n closeOverlay: dynamic((props) => {\n if (props.closeOverlay === undefined) {\n return;\n }\n if (\n props.closeOverlay instanceof OverlayController ||\n typeof props.closeOverlay === \"string\"\n ) {\n return {\n bypassConfirmation: true,\n overlay: props.closeOverlay,\n };\n }\n return {\n bypassConfirmation: true,\n ...props.closeOverlay,\n };\n }),\n },\n },\n };\n\n return (\n <Overlay\n className={rootClassName}\n controller={controller}\n ref={ref}\n aria-labelledby={generatedId}\n {...overlayProps}\n >\n <PropsContextProvider props={propsContext}>\n <Wrap if={offCanvas}>\n <Suspense fallback={<OffCanvasSuspenseFallback />}>\n {children}\n </Suspense>\n </Wrap>\n </PropsContextProvider>\n <ConfirmUnsavedChangesModal />\n </Overlay>\n );\n});\n\nexport default Modal;\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;AAkEA,IAAa,QAAQ,cAAc,UAAU,UAAU;CACrD,MAAM,EACJ,OAAO,KACP,WACA,YACA,UACA,KACA,WACA,uBAAuB,SACvB,iBACA,GAAG,iBACD;CAEJ,MAAM,kBAAkB,4BAA4B,kBAAS,OAAO;CAKpE,MAAM,kBAAkB,0BAA0B;CAElD,MAAM,gBAAgB,KACpB,YAAY,qBAAO,YAAY,qBAAO,OACtC,qBAAO,QAAQ,SACf,yBAAyB,UAAU,qBAAO,SAC1C,mBAAmB,qBAAO,iBAC1B,SACF;CAEA,MAAM,cAAc,MAAM;CAE1B,MAAM,UAAU,aACd,qBAAA,YAAA,EAAA,UAAA,CACE,oBAAC,SAAD;EAAS,WAAW,qBAAO;EAAc;CAAkB,CAAA,GAC3D,oBAAC,mBAAD,EAAA,UACE,oBAAC,uBAAD,EAAA,UACE,oBAAC,QAAD;EAAQ,YAAY,EAAE,oBAAoB,KAAK;EAC7C,UAAA,oBAAC,YAAD;GACE,SAAQ;GACR,OAAM;GACN,cAAY,gBAAgB,OAAO,OAAO;GAC1C,WAAW,KACT,qBAAO,aACP,oBAAoB,OAChB,qBAAO,gBACP,oBAAoB,QAClB,qBAAO,eACP,KAAA,CACR;GAEA,UAAA,oBAAC,WAAD,CAAY,CAAA;EACF,CAAA;CACN,CAAA,EACa,CAAA,EACN,CAAA,CACnB,EAAA,CAAA;CAGJ,MAAM,qBAAqB;CAE3B,MAAM,qBAAmC;EACvC,SAAS,EAAE,OAAO,mBAAmB;EACrC,SAAS;GACP,QAAQ,EAAE,SAAS,EAAE,OAAO,mBAAmB,EAAE;GACjD,SAAS,EAAE,OAAO,mBAAmB;GACrC,eAAe;EACjB;EACA,QAAQ,EAAE,SAAS,EAAE,OAAO,mBAAmB,EAAE;CACnD;CAEA,MAAM,eAA6B;EACjC,SAAS;GACP,GAAG;GACH,WAAW,qBAAO;EACpB;EACA,cAAc;GACZ,GAAG;GACH,GAAG,CAAC,GAAG,CAAC;GACR,GAAG,CAAC,CAAC;GACL,WAAW,qBAAO;GAClB,WAAW;IAAE,WAAW,qBAAO;IAAW,iBAAiB;GAAU;GACrE,UAAU,oBAAC,mBAAD,CAAoB,CAAA;EAChC;EACA,SAAS;GACP,WAAW,qBAAO;GAClB,OAAO;GACP,UAAU,SAAS,UAAU,OAAO,MAAM,QAAQ,CAAC;GACnD,IAAI;EACN;EACA,aAAa;GACX,WAAW,qBAAO;GAClB,SAAS;GACT,QAAQ;IACN,YAAY,SAAS,UAAU;KAC7B,IAAI,MAAM,eAAe,KAAA,GACvB;KAEF,IAAI,MAAM,eAAe,MACvB,OAAO,EAAE,oBAAoB,KAAK;KAEpC,OAAO;MACL,oBAAoB;MACpB,GAAG,MAAM;KACX;IACF,CAAC;IACD,cAAc,SAAS,UAAU;KAC/B,IAAI,MAAM,iBAAiB,KAAA,GACzB;KAEF,IACE,MAAM,wBAAwB,qBAC9B,OAAO,MAAM,iBAAiB,UAE9B,OAAO;MACL,oBAAoB;MACpB,SAAS,MAAM;KACjB;KAEF,OAAO;MACL,oBAAoB;MACpB,GAAG,MAAM;KACX;IACF,CAAC;GACH;EACF;CACF;CAEA,OACE,qBAAC,SAAD;EACE,WAAW;EACC;EACP;EACL,mBAAiB;EACjB,GAAI;EALN,UAAA,CAOE,oBAAC,sBAAD;GAAsB,OAAO;GAC3B,UAAA,oBAAC,MAAD;IAAM,IAAI;IACR,UAAA,oBAAC,UAAD;KAAU,UAAU,oBAAC,2BAAD,CAA4B,CAAA;KAC7C;IACO,CAAA;GACN,CAAA;EACc,CAAA,GACtB,oBAAC,4BAAD,CAA6B,CAAA,CACtB;;AAEb,CAAC"}
1
+ {"version":3,"file":"Modal.mjs","names":[],"sources":["../../../../../../../src/components/Modal/Modal.tsx"],"sourcesContent":["import { type PropsWithChildren, type ReactNode, Suspense, useId } from \"react\";\nimport styles from \"./Modal.module.scss\";\nimport clsx from \"clsx\";\nimport {\n dynamic,\n type PropsContext,\n PropsContextProvider,\n} from \"@/lib/propsContext\";\nimport { OverlayController } from \"@/lib/controller/overlay\";\nimport type { FlowComponentProps } from \"@/lib/componentFactory/flowComponent\";\nimport { flowComponent } from \"@/lib/componentFactory/flowComponent\";\nimport { Overlay, type OverlayProps } from \"@/components/Overlay/Overlay\";\nimport { Action } from \"@/components/Action\";\nimport { IconClose } from \"@/components/Icon/components/icons\";\nimport type { PropsWithClassName } from \"@/lib/types/props\";\nimport ButtonView from \"@/views/ButtonView\";\nimport DivView from \"@/views/DivView\";\nimport { OffCanvasSuspenseFallback } from \"@/components/Modal/components/OffCanvasSuspenseFallback\";\nimport { ConfirmUnsavedChangesModal } from \"@/components/Modal/components/ConfirmUnsavedChangesModal/ConfirmUnsavedChangesModal\";\nimport Wrap from \"@/components/Wrap\";\nimport { ClearPropsContext } from \"@/components/ClearPropsContext/ClearPropsContext\";\nimport ClearPropsContextView from \"@/views/ClearPropsContextView\";\nimport { useLocalizedStringFormatter } from \"@/components/TranslationProvider\";\nimport { useVirtualKeyboardVisible } from \"@/lib/hooks/dom/useVirtualKeyboardVisible\";\nimport locales from \"./locales/*.locale.json\";\n\ntype SupportedOverlayProps = Pick<\n OverlayProps,\n \"isOpen\" | \"isDefaultOpen\" | \"onOpen\" | \"onClose\" | \"onOpenChange\"\n>;\n\nexport interface ModalProps\n extends\n PropsWithChildren,\n FlowComponentProps,\n PropsWithClassName,\n SupportedOverlayProps {\n /** The size of the modal. @default \"s\" */\n size?: \"s\" | \"m\" | \"l\";\n /** Whether the modal should be displayed as an off canvas. */\n offCanvas?: boolean;\n /**\n * Whether the off canvas should be displayed on the right or left side of the\n * screen. @default \"right\"\n */\n offCanvasOrientation?: \"left\" | \"right\";\n /** An overlay controller to control the modal state. */\n controller?: OverlayController;\n /**\n * Accepts \"actionConfirm\" to use the modal as a confirmation modal for an\n * action.\n */\n slot?: string;\n /** Whether the modal can be closed by clicking outside of it. */\n isDismissable?: boolean;\n /** Whether the close button should be visible */\n showCloseButton?: boolean;\n /**\n * Whether closing the modal must be confirmed – use it to protect unsaved\n * changes. `false` does not switch off the confirmation of a dirty `Form`\n * inside the modal; set `confirmModalCloseOnUnsavedChanges={false}` on the\n * `Form` for that.\n */\n confirmOnClose?: boolean;\n}\n\nexport const Modal = flowComponent(\"Modal\", (props) => {\n const {\n size = \"s\",\n offCanvas,\n controller,\n children,\n ref,\n className,\n offCanvasOrientation = \"right\",\n showCloseButton,\n ...overlayProps\n } = props;\n\n const stringFormatter = useLocalizedStringFormatter(locales, \"Modal\");\n\n // On mobile the header/footer are docked (sticky) while at rest, but flow\n // normally while the on-screen keyboard is open so the content gets the full\n // available space (see Modal.module.scss). #2423\n const keyboardVisible = useVirtualKeyboardVisible();\n\n const rootClassName = clsx(\n offCanvas ? styles.offCanvas : styles.modal,\n styles[`size-${size}`],\n offCanvasOrientation === \"left\" && styles[\"left\"],\n keyboardVisible && styles.keyboardVisible,\n className,\n );\n\n const generatedId = useId();\n\n const header = (children: ReactNode) => (\n <>\n <DivView className={styles.headerTitle}>{children}</DivView>\n <ClearPropsContext>\n <ClearPropsContextView>\n <Action closeModal={{ bypassConfirmation: true }}>\n <ButtonView\n variant=\"plain\"\n color=\"secondary\"\n aria-label={stringFormatter.format(\"close\")}\n className={clsx(\n styles.closeButton,\n showCloseButton === true\n ? styles.alwaysVisible\n : showCloseButton === false\n ? styles.alwaysHidden\n : undefined,\n )}\n >\n <IconClose />\n </ButtonView>\n </Action>\n </ClearPropsContextView>\n </ClearPropsContext>\n </>\n );\n\n const nestedHeadingLevel = 3;\n\n const nestedHeadingProps: PropsContext = {\n Heading: { level: nestedHeadingLevel },\n Section: {\n Header: { Heading: { level: nestedHeadingLevel } },\n Heading: { level: nestedHeadingLevel },\n hideSeparator: true,\n },\n Header: { Heading: { level: nestedHeadingLevel } },\n };\n\n const propsContext: PropsContext = {\n Content: {\n ...nestedHeadingProps,\n className: styles.content,\n },\n ColumnLayout: {\n ...nestedHeadingProps,\n l: [2, 1],\n m: [1],\n className: styles.columnLayout,\n AccentBox: { className: styles.accentBox, backgroundColor: \"neutral\" },\n wrapWith: <ClearPropsContext />,\n },\n Heading: {\n className: styles.header,\n level: 2,\n children: dynamic((props) => header(props.children)),\n id: generatedId,\n },\n ActionGroup: {\n className: styles.actionGroup,\n spacing: \"m\",\n Action: {\n closeModal: dynamic((props) => {\n if (props.closeModal === undefined) {\n return;\n }\n if (props.closeModal === true) {\n return { bypassConfirmation: true };\n }\n return {\n bypassConfirmation: true,\n ...props.closeModal,\n };\n }),\n closeOverlay: dynamic((props) => {\n const { closeOverlay } = props;\n if (closeOverlay === undefined || closeOverlay === false) {\n return closeOverlay;\n }\n // `true` keeps its \"the nearest overlay\" meaning by leaving `overlay`\n // unset – the options object means the same thing.\n if (closeOverlay === true) {\n return { bypassConfirmation: true };\n }\n if (\n closeOverlay instanceof OverlayController ||\n typeof closeOverlay === \"string\"\n ) {\n return {\n bypassConfirmation: true,\n overlay: closeOverlay,\n };\n }\n return {\n bypassConfirmation: true,\n ...closeOverlay,\n };\n }),\n },\n },\n };\n\n return (\n <Overlay\n className={rootClassName}\n controller={controller}\n ref={ref}\n aria-labelledby={generatedId}\n {...overlayProps}\n >\n <PropsContextProvider props={propsContext}>\n <Wrap if={offCanvas}>\n <Suspense fallback={<OffCanvasSuspenseFallback />}>\n {children}\n </Suspense>\n </Wrap>\n </PropsContextProvider>\n <ConfirmUnsavedChangesModal />\n </Overlay>\n );\n});\n\nexport default Modal;\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;AAkEA,IAAa,QAAQ,cAAc,UAAU,UAAU;CACrD,MAAM,EACJ,OAAO,KACP,WACA,YACA,UACA,KACA,WACA,uBAAuB,SACvB,iBACA,GAAG,iBACD;CAEJ,MAAM,kBAAkB,4BAA4B,kBAAS,OAAO;CAKpE,MAAM,kBAAkB,0BAA0B;CAElD,MAAM,gBAAgB,KACpB,YAAY,qBAAO,YAAY,qBAAO,OACtC,qBAAO,QAAQ,SACf,yBAAyB,UAAU,qBAAO,SAC1C,mBAAmB,qBAAO,iBAC1B,SACF;CAEA,MAAM,cAAc,MAAM;CAE1B,MAAM,UAAU,aACd,qBAAA,YAAA,EAAA,UAAA,CACE,oBAAC,SAAD;EAAS,WAAW,qBAAO;EAAc;CAAkB,CAAA,GAC3D,oBAAC,mBAAD,EAAA,UACE,oBAAC,uBAAD,EAAA,UACE,oBAAC,QAAD;EAAQ,YAAY,EAAE,oBAAoB,KAAK;EAC7C,UAAA,oBAAC,YAAD;GACE,SAAQ;GACR,OAAM;GACN,cAAY,gBAAgB,OAAO,OAAO;GAC1C,WAAW,KACT,qBAAO,aACP,oBAAoB,OAChB,qBAAO,gBACP,oBAAoB,QAClB,qBAAO,eACP,KAAA,CACR;GAEA,UAAA,oBAAC,WAAD,CAAY,CAAA;EACF,CAAA;CACN,CAAA,EACa,CAAA,EACN,CAAA,CACnB,EAAA,CAAA;CAGJ,MAAM,qBAAqB;CAE3B,MAAM,qBAAmC;EACvC,SAAS,EAAE,OAAO,mBAAmB;EACrC,SAAS;GACP,QAAQ,EAAE,SAAS,EAAE,OAAO,mBAAmB,EAAE;GACjD,SAAS,EAAE,OAAO,mBAAmB;GACrC,eAAe;EACjB;EACA,QAAQ,EAAE,SAAS,EAAE,OAAO,mBAAmB,EAAE;CACnD;CAEA,MAAM,eAA6B;EACjC,SAAS;GACP,GAAG;GACH,WAAW,qBAAO;EACpB;EACA,cAAc;GACZ,GAAG;GACH,GAAG,CAAC,GAAG,CAAC;GACR,GAAG,CAAC,CAAC;GACL,WAAW,qBAAO;GAClB,WAAW;IAAE,WAAW,qBAAO;IAAW,iBAAiB;GAAU;GACrE,UAAU,oBAAC,mBAAD,CAAoB,CAAA;EAChC;EACA,SAAS;GACP,WAAW,qBAAO;GAClB,OAAO;GACP,UAAU,SAAS,UAAU,OAAO,MAAM,QAAQ,CAAC;GACnD,IAAI;EACN;EACA,aAAa;GACX,WAAW,qBAAO;GAClB,SAAS;GACT,QAAQ;IACN,YAAY,SAAS,UAAU;KAC7B,IAAI,MAAM,eAAe,KAAA,GACvB;KAEF,IAAI,MAAM,eAAe,MACvB,OAAO,EAAE,oBAAoB,KAAK;KAEpC,OAAO;MACL,oBAAoB;MACpB,GAAG,MAAM;KACX;IACF,CAAC;IACD,cAAc,SAAS,UAAU;KAC/B,MAAM,EAAE,iBAAiB;KACzB,IAAI,iBAAiB,KAAA,KAAa,iBAAiB,OACjD,OAAO;KAIT,IAAI,iBAAiB,MACnB,OAAO,EAAE,oBAAoB,KAAK;KAEpC,IACE,wBAAwB,qBACxB,OAAO,iBAAiB,UAExB,OAAO;MACL,oBAAoB;MACpB,SAAS;KACX;KAEF,OAAO;MACL,oBAAoB;MACpB,GAAG;KACL;IACF,CAAC;GACH;EACF;CACF;CAEA,OACE,qBAAC,SAAD;EACE,WAAW;EACC;EACP;EACL,mBAAiB;EACjB,GAAI;EALN,UAAA,CAOE,oBAAC,sBAAD;GAAsB,OAAO;GAC3B,UAAA,oBAAC,MAAD;IAAM,IAAI;IACR,UAAA,oBAAC,UAAD;KAAU,UAAU,oBAAC,2BAAD,CAA4B,CAAA;KAC7C;IACO,CAAA;GACN,CAAA;EACc,CAAA,GACtB,oBAAC,4BAAD,CAA6B,CAAA,CACtB;;AAEb,CAAC"}
@@ -31,6 +31,7 @@ var OverlayTrigger = (props) => {
31
31
  return /* @__PURE__ */ jsx(OverlayContextProvider, {
32
32
  type: overlayType,
33
33
  controller: overlayController,
34
+ isOverlayContent: false,
34
35
  children: /* @__PURE__ */ jsx(PropsContextProvider, {
35
36
  props: propsContext,
36
37
  dependencies: [isPending, overlayController],
@@ -1 +1 @@
1
- {"version":3,"file":"OverlayTrigger.mjs","names":[],"sources":["../../../../../../../src/components/OverlayTrigger/OverlayTrigger.tsx"],"sourcesContent":["import type { ComponentType, FC, PropsWithChildren, ReactNode } from \"react\";\nimport { OverlayController } from \"@/lib/controller\";\nimport type { PropsContext } from \"@/lib/propsContext\";\nimport { PropsContextProvider } from \"@/lib/propsContext\";\nimport { useComponentPropsContext } from \"@/lib/propsContext/propsContext\";\nimport type { FlowComponentName } from \"@/components/propTypes\";\nimport OverlayContextProvider from \"@/lib/controller/overlay/OverlayContextProvider\";\nimport type { FlowComponentProps } from \"@/lib/componentFactory/flowComponent\";\nimport { useIsPendingWithWait } from \"@/components/Action/hooks/useIsPendingWithWait\";\n\ntype AriaComponentType = ComponentType<{\n isOpen?: boolean;\n children: ReactNode;\n}>;\n\nexport interface OverlayTriggerProps\n extends FlowComponentProps, PropsWithChildren {\n /** Whether the overlay should be open initially. */\n isDefaultOpen?: boolean;\n /** A controller to control the state of the overlay. */\n controller?: OverlayController;\n}\n\ninterface Props extends OverlayTriggerProps {\n overlayType: FlowComponentName;\n component: AriaComponentType;\n}\n\nexport const OverlayTrigger: FC<Props> = (props) => {\n const {\n overlayType,\n isDefaultOpen = false,\n component: AriaOverlayTrigger,\n children,\n controller: controllerFromProps,\n } = props;\n const buttonPropsContext = useComponentPropsContext(\"Button\");\n\n const newOverlayController = OverlayController.useNew({ isDefaultOpen });\n const overlayController = controllerFromProps ?? newOverlayController;\n const isOpen = overlayController.useIsOpen();\n const isContentSuspended = overlayController.useIsContentSuspended();\n const isPending = useIsPendingWithWait(isContentSuspended);\n\n const propsContext: PropsContext = {\n Button: {\n onPress: overlayController.open,\n isPending,\n isDisabled: Boolean(buttonPropsContext?.isDisabled) || isContentSuspended,\n /*\n * The button is the react-aria trigger. react-aria hands the press\n * handling, the trigger ref and `aria-haspopup`/`aria-expanded`/\n * `aria-controls` down through a `PressResponder`, which only reaches its\n * own subtree — so a surrounding props context must not tunnel the button\n * out of the trigger. Such a context has to tunnel the trigger as a whole\n * instead (`overlayTriggersTunneledTo`).\n */\n tunnel: null,\n },\n /*\n * The aria trigger this renders is the host-side identity of every trigger\n * that is not `@flr-generate` (see `overlayTriggersTunneledTo`), so a\n * tunneling props context targets it as well. Here it is already inside the\n * trigger the author wrote — tunneling it again would send the same subtree\n * into the exit twice.\n */\n DialogTrigger: {\n tunnel: null,\n },\n /*\n * The overlay is already placed — this trigger owns it. A surrounding\n * props context that moves or wraps the bare overlay, so that it can be\n * written without a trigger (`Badge`), must not reach this one: it would\n * pull the overlay out of its trigger and add a second trigger button\n * around it.\n */\n [overlayType]: {\n tunnel: null,\n wrapWith: null,\n },\n };\n\n return (\n <OverlayContextProvider type={overlayType} controller={overlayController}>\n <PropsContextProvider\n props={propsContext}\n dependencies={[isPending, overlayController]}\n >\n <AriaOverlayTrigger isOpen={isOpen}>{children}</AriaOverlayTrigger>\n </PropsContextProvider>\n </OverlayContextProvider>\n );\n};\n\nexport default OverlayTrigger;\n"],"mappings":";;;;;;;;;AA4BA,IAAa,kBAA6B,UAAU;CAClD,MAAM,EACJ,aACA,gBAAgB,OAChB,WAAW,oBACX,UACA,YAAY,wBACV;CACJ,MAAM,qBAAqB,yBAAyB,QAAQ;CAE5D,MAAM,uBAAuB,kBAAkB,OAAO,EAAE,cAAc,CAAC;CACvE,MAAM,oBAAoB,uBAAuB;CACjD,MAAM,SAAS,kBAAkB,UAAU;CAC3C,MAAM,qBAAqB,kBAAkB,sBAAsB;CACnE,MAAM,YAAY,qBAAqB,kBAAkB;CAEzD,MAAM,eAA6B;EACjC,QAAQ;GACN,SAAS,kBAAkB;GAC3B;GACA,YAAY,QAAQ,oBAAoB,UAAU,KAAK;GASvD,QAAQ;EACV;EAQA,eAAe,EACb,QAAQ,KACV;GAQC,cAAc;GACb,QAAQ;GACR,UAAU;EACZ;CACF;CAEA,OACE,oBAAC,wBAAD;EAAwB,MAAM;EAAa,YAAY;EACrD,UAAA,oBAAC,sBAAD;GACE,OAAO;GACP,cAAc,CAAC,WAAW,iBAAiB;GAE3C,UAAA,oBAAC,oBAAD;IAA4B;IAAS;GAA6B,CAAA;EAC9C,CAAA;CACA,CAAA;AAE5B"}
1
+ {"version":3,"file":"OverlayTrigger.mjs","names":[],"sources":["../../../../../../../src/components/OverlayTrigger/OverlayTrigger.tsx"],"sourcesContent":["import type { ComponentType, FC, PropsWithChildren, ReactNode } from \"react\";\nimport { OverlayController } from \"@/lib/controller\";\nimport type { PropsContext } from \"@/lib/propsContext\";\nimport { PropsContextProvider } from \"@/lib/propsContext\";\nimport { useComponentPropsContext } from \"@/lib/propsContext/propsContext\";\nimport type { FlowComponentName } from \"@/components/propTypes\";\nimport OverlayContextProvider from \"@/lib/controller/overlay/OverlayContextProvider\";\nimport type { FlowComponentProps } from \"@/lib/componentFactory/flowComponent\";\nimport { useIsPendingWithWait } from \"@/components/Action/hooks/useIsPendingWithWait\";\n\ntype AriaComponentType = ComponentType<{\n isOpen?: boolean;\n children: ReactNode;\n}>;\n\nexport interface OverlayTriggerProps\n extends FlowComponentProps, PropsWithChildren {\n /** Whether the overlay should be open initially. */\n isDefaultOpen?: boolean;\n /** A controller to control the state of the overlay. */\n controller?: OverlayController;\n}\n\ninterface Props extends OverlayTriggerProps {\n overlayType: FlowComponentName;\n component: AriaComponentType;\n}\n\nexport const OverlayTrigger: FC<Props> = (props) => {\n const {\n overlayType,\n isDefaultOpen = false,\n component: AriaOverlayTrigger,\n children,\n controller: controllerFromProps,\n } = props;\n const buttonPropsContext = useComponentPropsContext(\"Button\");\n\n const newOverlayController = OverlayController.useNew({ isDefaultOpen });\n const overlayController = controllerFromProps ?? newOverlayController;\n const isOpen = overlayController.useIsOpen();\n const isContentSuspended = overlayController.useIsContentSuspended();\n const isPending = useIsPendingWithWait(isContentSuspended);\n\n const propsContext: PropsContext = {\n Button: {\n onPress: overlayController.open,\n isPending,\n isDisabled: Boolean(buttonPropsContext?.isDisabled) || isContentSuspended,\n /*\n * The button is the react-aria trigger. react-aria hands the press\n * handling, the trigger ref and `aria-haspopup`/`aria-expanded`/\n * `aria-controls` down through a `PressResponder`, which only reaches its\n * own subtree — so a surrounding props context must not tunnel the button\n * out of the trigger. Such a context has to tunnel the trigger as a whole\n * instead (`overlayTriggersTunneledTo`).\n */\n tunnel: null,\n },\n /*\n * The aria trigger this renders is the host-side identity of every trigger\n * that is not `@flr-generate` (see `overlayTriggersTunneledTo`), so a\n * tunneling props context targets it as well. Here it is already inside the\n * trigger the author wrote — tunneling it again would send the same subtree\n * into the exit twice.\n */\n DialogTrigger: {\n tunnel: null,\n },\n /*\n * The overlay is already placed — this trigger owns it. A surrounding\n * props context that moves or wraps the bare overlay, so that it can be\n * written without a trigger (`Badge`), must not reach this one: it would\n * pull the overlay out of its trigger and add a second trigger button\n * around it.\n */\n [overlayType]: {\n tunnel: null,\n wrapWith: null,\n },\n };\n\n return (\n <OverlayContextProvider\n type={overlayType}\n controller={overlayController}\n isOverlayContent={false}\n >\n <PropsContextProvider\n props={propsContext}\n dependencies={[isPending, overlayController]}\n >\n <AriaOverlayTrigger isOpen={isOpen}>{children}</AriaOverlayTrigger>\n </PropsContextProvider>\n </OverlayContextProvider>\n );\n};\n\nexport default OverlayTrigger;\n"],"mappings":";;;;;;;;;AA4BA,IAAa,kBAA6B,UAAU;CAClD,MAAM,EACJ,aACA,gBAAgB,OAChB,WAAW,oBACX,UACA,YAAY,wBACV;CACJ,MAAM,qBAAqB,yBAAyB,QAAQ;CAE5D,MAAM,uBAAuB,kBAAkB,OAAO,EAAE,cAAc,CAAC;CACvE,MAAM,oBAAoB,uBAAuB;CACjD,MAAM,SAAS,kBAAkB,UAAU;CAC3C,MAAM,qBAAqB,kBAAkB,sBAAsB;CACnE,MAAM,YAAY,qBAAqB,kBAAkB;CAEzD,MAAM,eAA6B;EACjC,QAAQ;GACN,SAAS,kBAAkB;GAC3B;GACA,YAAY,QAAQ,oBAAoB,UAAU,KAAK;GASvD,QAAQ;EACV;EAQA,eAAe,EACb,QAAQ,KACV;GAQC,cAAc;GACb,QAAQ;GACR,UAAU;EACZ;CACF;CAEA,OACE,oBAAC,wBAAD;EACE,MAAM;EACN,YAAY;EACZ,kBAAkB;EAElB,UAAA,oBAAC,sBAAD;GACE,OAAO;GACP,cAAc,CAAC,WAAW,iBAAiB;GAE3C,UAAA,oBAAC,oBAAD;IAA4B;IAAS;GAA6B,CAAA;EAC9C,CAAA;CACA,CAAA;AAE5B"}
@@ -28,9 +28,8 @@ var useCollapsingItems = () => {
28
28
  if (!list || !more || !container) return;
29
29
  const items = Array.from(list.children).filter((child) => child instanceof HTMLElement);
30
30
  const containerStyle = window.getComputedStyle(container);
31
- const scale = container.offsetWidth > 0 ? container.getBoundingClientRect().width / container.offsetWidth : 1;
32
31
  const availableWidth = container.clientWidth - (parseFloat(containerStyle.paddingInlineStart) || 0) - (parseFloat(containerStyle.paddingInlineEnd) || 0);
33
- const visibleCount = getVisibleItemCount(items.map((item) => item.getBoundingClientRect().width / scale), availableWidth, more.getBoundingClientRect().width / scale);
32
+ const visibleCount = getVisibleItemCount(items.map((item) => item.getBoundingClientRect().width), availableWidth, more.getBoundingClientRect().width);
34
33
  items.forEach((item, index) => {
35
34
  item.style.visibility = index < visibleCount ? "" : "hidden";
36
35
  });
@@ -1 +1 @@
1
- {"version":3,"file":"useCollapsingItems.mjs","names":[],"sources":["../../../../../../../../src/components/TabNavigation/lib/useCollapsingItems.ts"],"sourcesContent":["import type { RefObject } from \"react\";\nimport { useCallback, useLayoutEffect, useRef, useState } from \"react\";\nimport { createFrameResizeObserver } from \"@/lib/dom/createFrameResizeObserver\";\nimport { getVisibleItemCount } from \"./getVisibleItemCount\";\n\ninterface CollapsingItemsState {\n /** Number of visible items. `null` until the first measurement was done. */\n visibleCount: number | null;\n /** Whether at least one item does not fit and is collapsed. */\n hasOverflow: boolean;\n /** Whether one of the collapsed items contains the current page link. */\n menuHasCurrentItem: boolean;\n}\n\ninterface UseCollapsingItemsReturn extends CollapsingItemsState {\n listRef: RefObject<HTMLUListElement | null>;\n moreRef: RefObject<HTMLDivElement | null>;\n}\n\nconst initialState: CollapsingItemsState = {\n visibleCount: null,\n hasOverflow: false,\n menuHasCurrentItem: false,\n};\n\n/**\n * Observes the items of the list and computes how many of them fit into the\n * available width of the list's parent element. Items that do not fit are\n * hidden via `visibility: hidden` (removes them from the accessibility tree and\n * the tab order while keeping them measurable). The width of the \"more\" element\n * is reserved whenever items are collapsed, so it must always be rendered\n * (visually hidden when there is no overflow).\n */\nexport const useCollapsingItems = (): UseCollapsingItemsReturn => {\n const listRef = useRef<HTMLUListElement>(null);\n const moreRef = useRef<HTMLDivElement>(null);\n\n const [state, setState] = useState(initialState);\n\n const update = useCallback(() => {\n const list = listRef.current;\n const more = moreRef.current;\n const container = list?.parentElement;\n\n if (!list || !more || !container) {\n return;\n }\n\n const items = Array.from(list.children).filter(\n (child): child is HTMLElement => child instanceof HTMLElement,\n );\n\n const containerStyle = window.getComputedStyle(container);\n /*\n * `clientWidth` and a computed padding are layout pixels, while\n * `getBoundingClientRect` below reports rendered ones. Where an ancestor\n * scales the subtree — a `transform`, a CSS `zoom` — comparing the two\n * overstates the room by the scale factor, and the items stay visible\n * instead of collapsing. The container's own two border-box widths give\n * the factor; `clientWidth` would be off by the border.\n */\n const scale =\n container.offsetWidth > 0\n ? container.getBoundingClientRect().width / container.offsetWidth\n : 1;\n\n const availableWidth =\n container.clientWidth -\n (parseFloat(containerStyle.paddingInlineStart) || 0) -\n (parseFloat(containerStyle.paddingInlineEnd) || 0);\n\n const visibleCount = getVisibleItemCount(\n items.map((item) => item.getBoundingClientRect().width / scale),\n availableWidth,\n more.getBoundingClientRect().width / scale,\n );\n\n items.forEach((item, index) => {\n item.style.visibility = index < visibleCount ? \"\" : \"hidden\";\n });\n\n const menuHasCurrentItem = items\n .slice(visibleCount)\n .some(\n (item) =>\n !!item.querySelector('[aria-current]:not([aria-current=\"false\"])'),\n );\n\n setState((previous) => {\n const next: CollapsingItemsState = {\n visibleCount,\n hasOverflow: visibleCount < items.length,\n menuHasCurrentItem,\n };\n\n const isUnchanged =\n previous.visibleCount === next.visibleCount &&\n previous.hasOverflow === next.hasOverflow &&\n previous.menuHasCurrentItem === next.menuHasCurrentItem;\n\n return isUnchanged ? previous : next;\n });\n }, []);\n\n // Runs after every render (no dependency array): the rendered items may\n // have changed, so the observed elements are re-collected and re-measured.\n // `update()` only triggers a re-render when the computed state changed.\n useLayoutEffect(() => {\n const list = listRef.current;\n const more = moreRef.current;\n const container = list?.parentElement;\n\n if (!list || !more || !container) {\n return;\n }\n\n const observer = createFrameResizeObserver(update);\n observer.observe([container, more, ...list.children]);\n\n update();\n\n return () => observer.disconnect();\n });\n\n return { ...state, listRef, moreRef };\n};\n\nexport default useCollapsingItems;\n"],"mappings":";;;;;;AAmBA,IAAM,eAAqC;CACzC,cAAc;CACd,aAAa;CACb,oBAAoB;AACtB;;;;;;;;;AAUA,IAAa,2BAAqD;CAChE,MAAM,UAAU,OAAyB,IAAI;CAC7C,MAAM,UAAU,OAAuB,IAAI;CAE3C,MAAM,CAAC,OAAO,YAAY,SAAS,YAAY;CAE/C,MAAM,SAAS,kBAAkB;EAC/B,MAAM,OAAO,QAAQ;EACrB,MAAM,OAAO,QAAQ;EACrB,MAAM,YAAY,MAAM;EAExB,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC,WACrB;EAGF,MAAM,QAAQ,MAAM,KAAK,KAAK,QAAQ,CAAC,CAAC,QACrC,UAAgC,iBAAiB,WACpD;EAEA,MAAM,iBAAiB,OAAO,iBAAiB,SAAS;EASxD,MAAM,QACJ,UAAU,cAAc,IACpB,UAAU,sBAAsB,CAAC,CAAC,QAAQ,UAAU,cACpD;EAEN,MAAM,iBACJ,UAAU,eACT,WAAW,eAAe,kBAAkB,KAAK,MACjD,WAAW,eAAe,gBAAgB,KAAK;EAElD,MAAM,eAAe,oBACnB,MAAM,KAAK,SAAS,KAAK,sBAAsB,CAAC,CAAC,QAAQ,KAAK,GAC9D,gBACA,KAAK,sBAAsB,CAAC,CAAC,QAAQ,KACvC;EAEA,MAAM,SAAS,MAAM,UAAU;GAC7B,KAAK,MAAM,aAAa,QAAQ,eAAe,KAAK;EACtD,CAAC;EAED,MAAM,qBAAqB,MACxB,MAAM,YAAY,CAAC,CACnB,MACE,SACC,CAAC,CAAC,KAAK,cAAc,8CAA4C,CACrE;EAEF,UAAU,aAAa;GACrB,MAAM,OAA6B;IACjC;IACA,aAAa,eAAe,MAAM;IAClC;GACF;GAOA,OAJE,SAAS,iBAAiB,KAAK,gBAC/B,SAAS,gBAAgB,KAAK,eAC9B,SAAS,uBAAuB,KAAK,qBAElB,WAAW;EAClC,CAAC;CACH,GAAG,CAAC,CAAC;CAKL,sBAAsB;EACpB,MAAM,OAAO,QAAQ;EACrB,MAAM,OAAO,QAAQ;EACrB,MAAM,YAAY,MAAM;EAExB,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC,WACrB;EAGF,MAAM,WAAW,0BAA0B,MAAM;EACjD,SAAS,QAAQ;GAAC;GAAW;GAAM,GAAG,KAAK;EAAQ,CAAC;EAEpD,OAAO;EAEP,aAAa,SAAS,WAAW;CACnC,CAAC;CAED,OAAO;EAAE,GAAG;EAAO;EAAS;CAAQ;AACtC"}
1
+ {"version":3,"file":"useCollapsingItems.mjs","names":[],"sources":["../../../../../../../../src/components/TabNavigation/lib/useCollapsingItems.ts"],"sourcesContent":["import type { RefObject } from \"react\";\nimport { useCallback, useLayoutEffect, useRef, useState } from \"react\";\nimport { createFrameResizeObserver } from \"@/lib/dom/createFrameResizeObserver\";\nimport { getVisibleItemCount } from \"./getVisibleItemCount\";\n\ninterface CollapsingItemsState {\n /** Number of visible items. `null` until the first measurement was done. */\n visibleCount: number | null;\n /** Whether at least one item does not fit and is collapsed. */\n hasOverflow: boolean;\n /** Whether one of the collapsed items contains the current page link. */\n menuHasCurrentItem: boolean;\n}\n\ninterface UseCollapsingItemsReturn extends CollapsingItemsState {\n listRef: RefObject<HTMLUListElement | null>;\n moreRef: RefObject<HTMLDivElement | null>;\n}\n\nconst initialState: CollapsingItemsState = {\n visibleCount: null,\n hasOverflow: false,\n menuHasCurrentItem: false,\n};\n\n/**\n * Observes the items of the list and computes how many of them fit into the\n * available width of the list's parent element. Items that do not fit are\n * hidden via `visibility: hidden` (removes them from the accessibility tree and\n * the tab order while keeping them measurable). The width of the \"more\" element\n * is reserved whenever items are collapsed, so it must always be rendered\n * (visually hidden when there is no overflow).\n */\nexport const useCollapsingItems = (): UseCollapsingItemsReturn => {\n const listRef = useRef<HTMLUListElement>(null);\n const moreRef = useRef<HTMLDivElement>(null);\n\n const [state, setState] = useState(initialState);\n\n const update = useCallback(() => {\n const list = listRef.current;\n const more = moreRef.current;\n const container = list?.parentElement;\n\n if (!list || !more || !container) {\n return;\n }\n\n const items = Array.from(list.children).filter(\n (child): child is HTMLElement => child instanceof HTMLElement,\n );\n\n const containerStyle = window.getComputedStyle(container);\n const availableWidth =\n container.clientWidth -\n (parseFloat(containerStyle.paddingInlineStart) || 0) -\n (parseFloat(containerStyle.paddingInlineEnd) || 0);\n\n const visibleCount = getVisibleItemCount(\n items.map((item) => item.getBoundingClientRect().width),\n availableWidth,\n more.getBoundingClientRect().width,\n );\n\n items.forEach((item, index) => {\n item.style.visibility = index < visibleCount ? \"\" : \"hidden\";\n });\n\n const menuHasCurrentItem = items\n .slice(visibleCount)\n .some(\n (item) =>\n !!item.querySelector('[aria-current]:not([aria-current=\"false\"])'),\n );\n\n setState((previous) => {\n const next: CollapsingItemsState = {\n visibleCount,\n hasOverflow: visibleCount < items.length,\n menuHasCurrentItem,\n };\n\n const isUnchanged =\n previous.visibleCount === next.visibleCount &&\n previous.hasOverflow === next.hasOverflow &&\n previous.menuHasCurrentItem === next.menuHasCurrentItem;\n\n return isUnchanged ? previous : next;\n });\n }, []);\n\n // Runs after every render (no dependency array): the rendered items may\n // have changed, so the observed elements are re-collected and re-measured.\n // `update()` only triggers a re-render when the computed state changed.\n useLayoutEffect(() => {\n const list = listRef.current;\n const more = moreRef.current;\n const container = list?.parentElement;\n\n if (!list || !more || !container) {\n return;\n }\n\n const observer = createFrameResizeObserver(update);\n observer.observe([container, more, ...list.children]);\n\n update();\n\n return () => observer.disconnect();\n });\n\n return { ...state, listRef, moreRef };\n};\n\nexport default useCollapsingItems;\n"],"mappings":";;;;;;AAmBA,IAAM,eAAqC;CACzC,cAAc;CACd,aAAa;CACb,oBAAoB;AACtB;;;;;;;;;AAUA,IAAa,2BAAqD;CAChE,MAAM,UAAU,OAAyB,IAAI;CAC7C,MAAM,UAAU,OAAuB,IAAI;CAE3C,MAAM,CAAC,OAAO,YAAY,SAAS,YAAY;CAE/C,MAAM,SAAS,kBAAkB;EAC/B,MAAM,OAAO,QAAQ;EACrB,MAAM,OAAO,QAAQ;EACrB,MAAM,YAAY,MAAM;EAExB,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC,WACrB;EAGF,MAAM,QAAQ,MAAM,KAAK,KAAK,QAAQ,CAAC,CAAC,QACrC,UAAgC,iBAAiB,WACpD;EAEA,MAAM,iBAAiB,OAAO,iBAAiB,SAAS;EACxD,MAAM,iBACJ,UAAU,eACT,WAAW,eAAe,kBAAkB,KAAK,MACjD,WAAW,eAAe,gBAAgB,KAAK;EAElD,MAAM,eAAe,oBACnB,MAAM,KAAK,SAAS,KAAK,sBAAsB,CAAC,CAAC,KAAK,GACtD,gBACA,KAAK,sBAAsB,CAAC,CAAC,KAC/B;EAEA,MAAM,SAAS,MAAM,UAAU;GAC7B,KAAK,MAAM,aAAa,QAAQ,eAAe,KAAK;EACtD,CAAC;EAED,MAAM,qBAAqB,MACxB,MAAM,YAAY,CAAC,CACnB,MACE,SACC,CAAC,CAAC,KAAK,cAAc,8CAA4C,CACrE;EAEF,UAAU,aAAa;GACrB,MAAM,OAA6B;IACjC;IACA,aAAa,eAAe,MAAM;IAClC;GACF;GAOA,OAJE,SAAS,iBAAiB,KAAK,gBAC/B,SAAS,gBAAgB,KAAK,eAC9B,SAAS,uBAAuB,KAAK,qBAElB,WAAW;EAClC,CAAC;CACH,GAAG,CAAC,CAAC;CAKL,sBAAsB;EACpB,MAAM,OAAO,QAAQ;EACrB,MAAM,OAAO,QAAQ;EACrB,MAAM,YAAY,MAAM;EAExB,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC,WACrB;EAGF,MAAM,WAAW,0BAA0B,MAAM;EACjD,SAAS,QAAQ;GAAC;GAAW;GAAM,GAAG,KAAK;EAAQ,CAAC;EAEpD,OAAO;EAEP,aAAa,SAAS,WAAW;CACnC,CAAC;CAED,OAAO;EAAE,GAAG;EAAO;EAAS;CAAQ;AACtC"}
@@ -31,10 +31,9 @@ var useTabIndicator = (isCollapsed) => {
31
31
  }
32
32
  const rootRect = root.getBoundingClientRect();
33
33
  const selectedTabRect = selectedTab.getBoundingClientRect();
34
- const scale = root.offsetWidth > 0 ? rootRect.width / root.offsetWidth : 1;
35
34
  const nextIndicator = {
36
- x: (selectedTabRect.left - rootRect.left) / scale,
37
- width: selectedTabRect.width / scale,
35
+ x: selectedTabRect.left - rootRect.left,
36
+ width: selectedTabRect.width,
38
37
  isAnimated: hasMeasuredRef.current
39
38
  };
40
39
  setIndicator((previousIndicator) => {