@openeditor/ui 0.0.44 → 0.0.46

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/index.js CHANGED
@@ -12,7 +12,7 @@ import { Input } from '@base-ui/react/input';
12
12
  import { Popover } from '@base-ui/react/popover';
13
13
  import { Toggle } from '@base-ui/react/toggle';
14
14
  import { ToggleGroup } from '@base-ui/react/toggle-group';
15
- import { useState, useRef, useEffect, useId, useMemo, useCallback } from 'react';
15
+ import { useState, useRef, useEffect, useId, useMemo, useSyncExternalStore, Fragment as Fragment$1, useCallback } from 'react';
16
16
 
17
17
  // src/table-menu.tsx
18
18
  var runTableCommandOnMouseDown = (event, controller, command) => {
@@ -854,86 +854,181 @@ var OpenEditorSlashMenu = ({
854
854
  }
855
855
  ) }) });
856
856
  };
857
- var createOpenEditorBuiltInBlockMenuActions = (handlers) => [
858
- {
859
- id: "move-up",
860
- label: "Move up",
861
- group: "move",
862
- icon: openEditorIcons.moveUp,
863
- disabled: !handlers.canMove(-1),
864
- run: () => handlers.move(-1)
865
- },
866
- {
867
- id: "move-down",
868
- label: "Move down",
869
- group: "move",
870
- icon: openEditorIcons.moveDown,
871
- disabled: !handlers.canMove(1),
872
- run: () => handlers.move(1)
873
- },
874
- {
875
- id: "copy",
876
- label: "Copy block",
877
- group: "edit",
878
- icon: openEditorIcons.copy,
879
- run: handlers.copy
880
- },
881
- {
882
- id: "duplicate",
883
- label: "Duplicate",
884
- group: "edit",
885
- icon: openEditorIcons.duplicate,
886
- run: handlers.duplicate
887
- },
857
+ var BlockMenuIcon = ({ item }) => item.icon ? /* @__PURE__ */ jsx(HugeiconsIcon, { "aria-hidden": "true", icon: item.icon, size: 15, strokeWidth: 2 }) : /* @__PURE__ */ jsx("span", { "aria-hidden": "true", className: "oe-block-menu-icon-placeholder" });
858
+ var BlockMenuAction = ({
859
+ action,
860
+ session
861
+ }) => {
862
+ const snapshot = useSyncExternalStore(session.subscribe, session.getSnapshot, session.getSnapshot);
863
+ const pending = snapshot.pendingItemId === action.id;
864
+ return /* @__PURE__ */ jsxs(
865
+ Menu.Item,
866
+ {
867
+ "aria-current": action.active || void 0,
868
+ "aria-describedby": action.disabledReason ? `${action.id}-reason` : void 0,
869
+ className: "oe-block-menu-item",
870
+ "data-active": action.active ? "" : void 0,
871
+ "data-danger": action.intent === "danger" ? "" : void 0,
872
+ disabled: action.disabled || Boolean(snapshot.pendingItemId),
873
+ label: action.label,
874
+ onClick: () => {
875
+ void session.run(action);
876
+ },
877
+ children: [
878
+ /* @__PURE__ */ jsx(BlockMenuIcon, { item: action }),
879
+ /* @__PURE__ */ jsx("span", { children: pending ? `${action.label}\u2026` : action.label }),
880
+ action.disabledReason ? /* @__PURE__ */ jsx("span", { className: "oe-visually-hidden", id: `${action.id}-reason`, children: action.disabledReason }) : null
881
+ ]
882
+ }
883
+ );
884
+ };
885
+ var BlockMenuPanel = ({
886
+ container,
887
+ item,
888
+ target,
889
+ session,
890
+ style
891
+ }) => {
892
+ const snapshot = useSyncExternalStore(session.subscribe, session.getSnapshot, session.getSnapshot);
893
+ const open = snapshot.open && snapshot.surface.type === "panel" && snapshot.surface.id === item.id;
894
+ const Panel = item.panel;
895
+ return /* @__PURE__ */ jsxs(
896
+ Popover.Root,
897
+ {
898
+ modal: false,
899
+ onOpenChange: (nextOpen) => {
900
+ if (nextOpen) session.openPanel(item.id);
901
+ else if (open) session.closeSurface();
902
+ },
903
+ open,
904
+ children: [
905
+ /* @__PURE__ */ jsxs(
906
+ Popover.Trigger,
907
+ {
908
+ disabled: item.disabled,
909
+ nativeButton: false,
910
+ render: (triggerProps) => /* @__PURE__ */ jsx(
911
+ Menu.Item,
912
+ {
913
+ ...triggerProps,
914
+ "aria-describedby": item.disabledReason ? `${item.id}-reason` : void 0,
915
+ className: "oe-block-menu-item",
916
+ closeOnClick: false,
917
+ label: item.label,
918
+ role: "menuitem"
919
+ }
920
+ ),
921
+ children: [
922
+ /* @__PURE__ */ jsx(BlockMenuIcon, { item }),
923
+ /* @__PURE__ */ jsx("span", { children: item.label }),
924
+ item.disabledReason ? /* @__PURE__ */ jsx("span", { className: "oe-visually-hidden", id: `${item.id}-reason`, children: item.disabledReason }) : null
925
+ ]
926
+ }
927
+ ),
928
+ /* @__PURE__ */ jsx(Popover.Portal, { container, children: /* @__PURE__ */ jsx(
929
+ Popover.Positioner,
930
+ {
931
+ align: "start",
932
+ collisionAvoidance: { side: "flip", align: "shift", fallbackAxisSide: "none" },
933
+ collisionPadding: 12,
934
+ positionMethod: "fixed",
935
+ side: "right",
936
+ sideOffset: 8,
937
+ children: /* @__PURE__ */ jsx(Popover.Popup, { "aria-label": item.label, className: "oe-block-menu-panel", role: "dialog", style, children: /* @__PURE__ */ jsx(Panel, { target, close: () => session.closeSurface() }) })
938
+ }
939
+ ) })
940
+ ]
941
+ }
942
+ );
943
+ };
944
+ var BlockMenuSubmenu = ({
945
+ container,
946
+ item,
947
+ target,
948
+ session,
949
+ style
950
+ }) => /* @__PURE__ */ jsxs(
951
+ Menu.SubmenuRoot,
888
952
  {
889
- id: "delete",
890
- label: "Delete",
891
- group: "danger",
892
- icon: openEditorIcons.delete,
893
- run: handlers.delete
953
+ onOpenChange: (open) => {
954
+ if (open) session.openSubmenu(item.id);
955
+ else session.closeSurface();
956
+ },
957
+ children: [
958
+ /* @__PURE__ */ jsxs(Menu.SubmenuTrigger, { "aria-describedby": item.disabledReason ? `${item.id}-reason` : void 0, className: "oe-block-menu-item", disabled: item.disabled, label: item.label, children: [
959
+ /* @__PURE__ */ jsx(BlockMenuIcon, { item }),
960
+ /* @__PURE__ */ jsx("span", { children: item.label }),
961
+ item.disabledReason ? /* @__PURE__ */ jsx("span", { className: "oe-visually-hidden", id: `${item.id}-reason`, children: item.disabledReason }) : null,
962
+ /* @__PURE__ */ jsx(HugeiconsIcon, { "aria-hidden": "true", className: "oe-block-menu-chevron", icon: openEditorIcons.chevronRight, size: 13, strokeWidth: 2 })
963
+ ] }),
964
+ /* @__PURE__ */ jsx(Menu.Portal, { container, children: /* @__PURE__ */ jsx(Menu.Positioner, { align: "start", collisionPadding: 12, positionMethod: "fixed", side: "right", sideOffset: 4, children: /* @__PURE__ */ jsx(Menu.Popup, { "aria-label": item.label, className: "oe-block-menu", style, children: item.children.map((child) => /* @__PURE__ */ jsx(BlockMenuItem, { container, item: child, session, style, target }, child.id)) }) }) })
965
+ ]
894
966
  }
895
- ];
896
- var OpenEditorBlockMenuPopup = ({
897
- actions,
967
+ );
968
+ var BlockMenuItem = ({
969
+ container,
970
+ item,
971
+ target,
972
+ session,
973
+ style
974
+ }) => {
975
+ if (item.type === "action") return /* @__PURE__ */ jsx(BlockMenuAction, { action: item, session });
976
+ if (item.type === "panel") return /* @__PURE__ */ jsx(BlockMenuPanel, { container, item, session, style, target });
977
+ return /* @__PURE__ */ jsx(BlockMenuSubmenu, { container, item, session, style, target });
978
+ };
979
+ var OpenEditorResolvedBlockMenuView = ({
980
+ resolver,
981
+ session,
898
982
  anchor,
899
983
  ariaLabel,
900
984
  container,
901
- onOpenChange,
902
- open,
903
985
  style,
904
986
  trigger
905
- }) => /* @__PURE__ */ jsxs(Menu.Root, { modal: false, open, onOpenChange, children: [
906
- trigger,
907
- open ? /* @__PURE__ */ jsx(Menu.Portal, { container, children: /* @__PURE__ */ jsx(
908
- Menu.Positioner,
987
+ }) => {
988
+ const snapshot = useSyncExternalStore(session.subscribe, session.getSnapshot, session.getSnapshot);
989
+ const targetSnapshot = useSyncExternalStore(resolver.target.subscribe, resolver.target.getSnapshot, resolver.target.getSnapshot);
990
+ const menu = resolver.resolve();
991
+ useEffect(() => {
992
+ if (snapshot.open && !targetSnapshot) session.close();
993
+ }, [session, snapshot.open, targetSnapshot]);
994
+ return /* @__PURE__ */ jsxs(
995
+ Menu.Root,
909
996
  {
910
- align: "start",
911
- anchor,
912
- collisionAvoidance: { side: "flip", align: "shift", fallbackAxisSide: "none" },
913
- collisionPadding: 12,
914
- positionMethod: "fixed",
915
- side: "bottom",
916
- sideOffset: 8,
917
- children: /* @__PURE__ */ jsx(Menu.Popup, { "aria-label": ariaLabel, className: "oe-block-menu", style, children: actions.map((action) => /* @__PURE__ */ jsxs(
918
- Menu.Item,
919
- {
920
- className: "oe-block-menu-item",
921
- "data-danger": action.group === "danger" ? "" : void 0,
922
- disabled: action.disabled,
923
- onClick: () => {
924
- onOpenChange(false);
925
- void action.run();
926
- },
927
- children: [
928
- action.icon ? /* @__PURE__ */ jsx(HugeiconsIcon, { "aria-hidden": "true", icon: action.icon, size: 15, strokeWidth: 2 }) : /* @__PURE__ */ jsx("span", { "aria-hidden": "true", className: "oe-block-menu-icon-placeholder" }),
929
- /* @__PURE__ */ jsx("span", { children: action.label })
930
- ]
931
- },
932
- action.id
933
- )) })
997
+ modal: false,
998
+ onOpenChange: (open) => {
999
+ if (!open) session.close();
1000
+ },
1001
+ onOpenChangeComplete: (open) => {
1002
+ if (open) return;
1003
+ const anchorElement = anchor instanceof HTMLElement ? anchor : anchor && "current" in anchor ? anchor.current : null;
1004
+ anchorElement?.focus();
1005
+ },
1006
+ open: snapshot.open,
1007
+ children: [
1008
+ trigger,
1009
+ snapshot.open ? /* @__PURE__ */ jsx(Menu.Portal, { container, children: /* @__PURE__ */ jsx(
1010
+ Menu.Positioner,
1011
+ {
1012
+ align: "start",
1013
+ anchor,
1014
+ collisionAvoidance: { side: "flip", align: "shift", fallbackAxisSide: "none" },
1015
+ collisionPadding: 12,
1016
+ positionMethod: "fixed",
1017
+ side: "bottom",
1018
+ sideOffset: 8,
1019
+ children: /* @__PURE__ */ jsxs(Menu.Popup, { "aria-label": ariaLabel, className: "oe-block-menu", style, children: [
1020
+ menu.sections.map((section, sectionIndex) => /* @__PURE__ */ jsxs(Fragment$1, { children: [
1021
+ sectionIndex > 0 ? /* @__PURE__ */ jsx(Menu.Separator, { className: "oe-block-menu-separator" }) : null,
1022
+ section.items.map((item) => /* @__PURE__ */ jsx(BlockMenuItem, { container, item, session, style, target: menu.target }, item.id))
1023
+ ] }, section.id)),
1024
+ snapshot.error ? /* @__PURE__ */ jsx("div", { "aria-live": "polite", className: "oe-block-menu-error", role: "status", children: "The action failed. Try again." }) : null
1025
+ ] })
1026
+ }
1027
+ ) }) : null
1028
+ ]
934
1029
  }
935
- ) }) : null
936
- ] });
1030
+ );
1031
+ };
937
1032
  var OpenEditorBlockMenu = ({
938
1033
  controller,
939
1034
  className,
@@ -942,24 +1037,36 @@ var OpenEditorBlockMenu = ({
942
1037
  const interaction = useOpenEditorBlockInteraction(controller);
943
1038
  const block = interaction.activeBlock;
944
1039
  const themeStyle = useOpenEditorThemeStyle();
945
- const [open, setOpen] = useState(false);
946
1040
  const handleElementRef = useRef(null);
947
1041
  const lastBlockRef = useRef(null);
948
- const controllerRef = useRef(controller);
949
- controllerRef.current = controller;
950
- if (block) lastBlockRef.current = block;
951
- const activeBlock = block ?? (open ? lastBlockRef.current : null);
1042
+ const session = controller.blockMenu.session;
1043
+ const sessionState = useSyncExternalStore(session.subscribe, session.getSnapshot, session.getSnapshot);
1044
+ const previousOpenRef = useRef(sessionState.open);
1045
+ useEffect(() => {
1046
+ let focusTimer;
1047
+ if (previousOpenRef.current && !sessionState.open) {
1048
+ focusTimer = setTimeout(() => {
1049
+ if (handleElementRef.current?.isConnected) handleElementRef.current.focus();
1050
+ });
1051
+ }
1052
+ previousOpenRef.current = sessionState.open;
1053
+ return () => {
1054
+ if (focusTimer) clearTimeout(focusTimer);
1055
+ };
1056
+ }, [sessionState.open]);
1057
+ if (block && !sessionState.open) lastBlockRef.current = block;
1058
+ const activeBlock = sessionState.target ?? block;
952
1059
  const setMenuOpen = useCallback((nextOpen) => {
953
1060
  if (nextOpen) {
954
1061
  if (!lastBlockRef.current) {
955
- setOpen(false);
1062
+ session.close();
956
1063
  return;
957
1064
  }
958
- setOpen(true);
1065
+ session.open(lastBlockRef.current);
959
1066
  return;
960
1067
  }
961
- setOpen(false);
962
- }, []);
1068
+ session.close();
1069
+ }, [session]);
963
1070
  if (!controller.editable) return null;
964
1071
  const handle = /* @__PURE__ */ jsx(
965
1072
  OpenEditorBlockDragHandle,
@@ -968,9 +1075,9 @@ var OpenEditorBlockMenu = ({
968
1075
  className: "oe-block-handle-anchor",
969
1076
  controller,
970
1077
  elementRef: handleElementRef,
971
- expanded: open,
1078
+ expanded: sessionState.open,
972
1079
  onActivate: () => {
973
- setMenuOpen(!open);
1080
+ setMenuOpen(!sessionState.open);
974
1081
  },
975
1082
  onDraggingChange: (dragging) => {
976
1083
  if (dragging) setMenuOpen(false);
@@ -978,44 +1085,21 @@ var OpenEditorBlockMenu = ({
978
1085
  children: activeBlock ? /* @__PURE__ */ jsx("span", { "aria-hidden": "true", className: cx("oe-block-handle", className), children: /* @__PURE__ */ jsx(HugeiconsIcon, { "aria-hidden": "true", icon: openEditorIcons.dragHandle, size: 16, strokeWidth: 2 }) }) : null
979
1086
  }
980
1087
  );
981
- const actions = activeBlock ? [
982
- ...createOpenEditorBuiltInBlockMenuActions({
983
- canMove: (direction) => controller.canMoveBlock(direction, activeBlock),
984
- move: (direction) => {
985
- controller.moveBlock(direction, activeBlock);
986
- },
987
- copy: async () => {
988
- await controller.copyBlock(activeBlock);
989
- },
990
- duplicate: () => {
991
- controller.duplicateBlock(activeBlock);
992
- },
993
- delete: () => {
994
- controller.deleteBlock(activeBlock);
1088
+ const resolver = activeBlock ? controller.blockMenu.resolve(activeBlock) : null;
1089
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
1090
+ handle,
1091
+ resolver ? /* @__PURE__ */ jsx(
1092
+ OpenEditorResolvedBlockMenuView,
1093
+ {
1094
+ resolver,
1095
+ session,
1096
+ anchor: handleElementRef,
1097
+ ariaLabel: activeBlock ? `Actions for ${activeBlock.blockName ?? activeBlock.nodeType} block` : "Block actions",
1098
+ container,
1099
+ style: themeStyle
995
1100
  }
996
- }),
997
- ...controller.blockActions.map((action) => {
998
- const context = { controller, block: activeBlock };
999
- return {
1000
- ...action,
1001
- disabled: action.disabled?.(context),
1002
- run: () => action.run(context)
1003
- };
1004
- })
1005
- ] : [];
1006
- return /* @__PURE__ */ jsx(
1007
- OpenEditorBlockMenuPopup,
1008
- {
1009
- actions,
1010
- anchor: handleElementRef,
1011
- ariaLabel: activeBlock ? `Actions for ${activeBlock.blockName ?? activeBlock.nodeType} block` : "Block actions",
1012
- container,
1013
- onOpenChange: setMenuOpen,
1014
- open: open && Boolean(activeBlock),
1015
- style: themeStyle,
1016
- trigger: handle
1017
- }
1018
- );
1101
+ ) : null
1102
+ ] });
1019
1103
  };
1020
1104
  var OpenEditor = ({
1021
1105
  className = "oe-editor-surface",
@@ -1035,6 +1119,6 @@ var OpenEditor = ({
1035
1119
  return theme ? /* @__PURE__ */ jsx(OpenEditorThemeProvider, { theme, children: editor }) : editor;
1036
1120
  };
1037
1121
 
1038
- export { OpenEditor, OpenEditorBlockMenu, OpenEditorBlockMenuPopup, OpenEditorButton, OpenEditorInput, OpenEditorSelectionBubble, OpenEditorSlashMenu, OpenEditorTableMenu, OpenEditorToggle, OpenEditorToggleGroup, createOpenEditorBuiltInBlockMenuActions };
1122
+ export { OpenEditor, OpenEditorBlockMenu, OpenEditorButton, OpenEditorInput, OpenEditorResolvedBlockMenuView, OpenEditorSelectionBubble, OpenEditorSlashMenu, OpenEditorTableMenu, OpenEditorToggle, OpenEditorToggleGroup };
1039
1123
  //# sourceMappingURL=index.js.map
1040
1124
  //# sourceMappingURL=index.js.map