@bpmn-io/form-js-editor 1.19.0 → 1.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.es.js CHANGED
@@ -5700,6 +5700,8 @@ function Tooltip(props) {
5700
5700
  hideDelay = 250
5701
5701
  } = props;
5702
5702
  const [visible, setVisible] = useState(false);
5703
+ const [tooltipPosition, setTooltipPosition] = useState(null);
5704
+ const [arrowOffset, setArrowOffset] = useState(null);
5703
5705
  const showTimeoutRef = useRef(null);
5704
5706
  const hideTimeoutRef = useRef(null);
5705
5707
  const wrapperRef = useRef(null);
@@ -5753,6 +5755,20 @@ function Tooltip(props) {
5753
5755
  document.removeEventListener('mousedown', handleClickOutside);
5754
5756
  };
5755
5757
  }, [visible, hide]);
5758
+ useLayoutEffect(() => {
5759
+ if (!visible || position) {
5760
+ setTooltipPosition(null);
5761
+ setArrowOffset(null);
5762
+ return;
5763
+ }
5764
+ if (!wrapperRef.current || !tooltipRef.current) return;
5765
+ const {
5766
+ tooltipPosition: newPosition,
5767
+ arrowOffset: newArrowOffset
5768
+ } = getTooltipPosition(wrapperRef.current, tooltipRef.current, direction);
5769
+ setTooltipPosition(newPosition);
5770
+ setArrowOffset(newArrowOffset);
5771
+ }, [visible, position]);
5756
5772
  const handleMouseLeave = ({
5757
5773
  relatedTarget
5758
5774
  }) => {
@@ -5788,12 +5804,14 @@ function Tooltip(props) {
5788
5804
  e.code === 'Escape' && hide(false);
5789
5805
  };
5790
5806
  const renderTooltip = () => {
5807
+ const tooltipStyle = position || (tooltipPosition ? `right: ${tooltipPosition.right}; top: ${tooltipPosition.top}px;` : undefined);
5808
+ const arrowStyle = arrowOffset != null ? `margin-top: ${arrowOffset}px;` : undefined;
5791
5809
  return jsxs("div", {
5792
5810
  class: `bio-properties-panel-tooltip ${direction}`,
5793
5811
  role: "tooltip",
5794
5812
  id: "bio-properties-panel-tooltip",
5795
5813
  "aria-labelledby": forId,
5796
- style: position || getTooltipPosition(wrapperRef.current),
5814
+ style: tooltipStyle,
5797
5815
  ref: tooltipRef,
5798
5816
  onClick: e => e.stopPropagation(),
5799
5817
  onMouseEnter: handleTooltipMouseEnter,
@@ -5802,7 +5820,8 @@ function Tooltip(props) {
5802
5820
  class: "bio-properties-panel-tooltip-content",
5803
5821
  children: value
5804
5822
  }), jsx("div", {
5805
- class: "bio-properties-panel-tooltip-arrow"
5823
+ class: "bio-properties-panel-tooltip-arrow",
5824
+ style: arrowStyle
5806
5825
  })]
5807
5826
  });
5808
5827
  };
@@ -5821,11 +5840,47 @@ function Tooltip(props) {
5821
5840
 
5822
5841
  // helper
5823
5842
 
5824
- function getTooltipPosition(refElement) {
5843
+ function getTooltipPosition(refElement, tooltipElement, direction) {
5844
+ if (!refElement) {
5845
+ return {
5846
+ tooltipPosition: null,
5847
+ arrowOffset: null
5848
+ };
5849
+ }
5825
5850
  const refPosition = refElement.getBoundingClientRect();
5826
5851
  const right = `calc(100% - ${refPosition.x}px)`;
5827
- const top = `${refPosition.top - 10}px`;
5828
- return `right: ${right}; top: ${top};`;
5852
+ let top = refPosition.top - 10;
5853
+ let arrowOffset = null;
5854
+
5855
+ // Ensure that the tooltip is within the viewport, adjust the top position if needed.
5856
+ // This is only relevant for the 'right' direction for now
5857
+ if (tooltipElement && direction === 'right') {
5858
+ const tooltipRect = tooltipElement.getBoundingClientRect();
5859
+ const viewportHeight = window.innerHeight;
5860
+ const minTop = 0;
5861
+ const maxTop = viewportHeight - tooltipRect.height;
5862
+ const originalTop = top;
5863
+ if (top > maxTop) {
5864
+ top = maxTop;
5865
+ }
5866
+ if (top < minTop) {
5867
+ top = minTop;
5868
+ }
5869
+
5870
+ // Adjust the arrow position if the tooltip had to be moved to stay within viewport
5871
+ if (top !== originalTop) {
5872
+ const defaultMarginTop = 16;
5873
+ const topDiff = top - originalTop;
5874
+ arrowOffset = defaultMarginTop - topDiff;
5875
+ }
5876
+ }
5877
+ return {
5878
+ tooltipPosition: {
5879
+ right,
5880
+ top
5881
+ },
5882
+ arrowOffset
5883
+ };
5829
5884
  }
5830
5885
 
5831
5886
  /**
@@ -6063,12 +6118,17 @@ function useStickyIntersectionObserver(ref, scrollContainerSelector, setSticky)
6063
6118
  * The `callback` reference is static and can be safely used in external
6064
6119
  * libraries or as a prop that does not cause rerendering of children.
6065
6120
  *
6121
+ * The ref update is deferred to useLayoutEffect to prevent stale-closure
6122
+ * bugs when Chrome fires blur on elements removed during re-render.
6123
+ *
6066
6124
  * @param {Function} callback function with changing reference
6067
6125
  * @returns {Function} static function reference
6068
6126
  */
6069
6127
  function useStaticCallback(callback) {
6070
6128
  const callbackRef = useRef(callback);
6071
- callbackRef.current = callback;
6129
+ useLayoutEffect(() => {
6130
+ callbackRef.current = callback;
6131
+ });
6072
6132
  return useCallback((...args) => callbackRef.current(...args), []);
6073
6133
  }
6074
6134
  function useElementVisible(element) {
@@ -7324,7 +7384,7 @@ function ToggleSwitchEntry(props) {
7324
7384
  inline: inline,
7325
7385
  tooltip: tooltip,
7326
7386
  element: element
7327
- }), jsx(Description$1, {
7387
+ }, element), jsx(Description$1, {
7328
7388
  forId: id,
7329
7389
  element: element,
7330
7390
  value: description
@@ -7497,7 +7557,7 @@ function prefixId$6(id) {
7497
7557
  const noop$2 = () => {};
7498
7558
 
7499
7559
  /**
7500
- * @typedef {'required'|'optional'|'static'} FeelType
7560
+ * @typedef {'required'|'optional'|'optional-default-enabled'|'static'} FeelType
7501
7561
  */
7502
7562
 
7503
7563
  /**
@@ -7540,7 +7600,7 @@ function FeelTextfield(props) {
7540
7600
  OptionalComponent = OptionalFeelInput,
7541
7601
  tooltip
7542
7602
  } = props;
7543
- const [localValue, setLocalValue] = useState(value);
7603
+ const [localValue, setLocalValue] = useState(getInitialFeelLocalValue(feel, value));
7544
7604
  const editorRef = useShowEntryEvent(id);
7545
7605
  const containerRef = useRef();
7546
7606
  const onInput = useCallback(newValue => {
@@ -7549,8 +7609,8 @@ function FeelTextfield(props) {
7549
7609
  const newModelValue = newValue === '' || newValue === '=' ? undefined : newValue;
7550
7610
  commitValue(newModelValue);
7551
7611
  }, [commitValue]);
7552
- const feelActive = isString(localValue) && localValue.startsWith('=') || feel === 'required';
7553
- const feelOnlyValue = isString(localValue) && localValue.startsWith('=') ? localValue.substring(1) : localValue;
7612
+ const feelActive = isFeelActive(feel, localValue);
7613
+ const feelOnlyValue = getFeelValue(localValue);
7554
7614
  const feelLanguageContext = useContext(FeelLanguageContext);
7555
7615
  const [focus, _setFocus] = useState(undefined);
7556
7616
  const {
@@ -7613,7 +7673,7 @@ function FeelTextfield(props) {
7613
7673
  };
7614
7674
  const handleOnKeyDown = e => {
7615
7675
  if (isCmdWithChar(e)) {
7616
- handleInput.flush();
7676
+ handleInput.flush?.();
7617
7677
  }
7618
7678
  };
7619
7679
  const handleLint = useStaticCallback((lint = []) => {
@@ -7737,7 +7797,7 @@ function FeelTextfield(props) {
7737
7797
  ref: containerRef,
7738
7798
  children: [jsx(FeelIndicator, {
7739
7799
  active: feelActive,
7740
- disabled: feel !== 'optional' || disabled,
7800
+ disabled: !isFeelOptional(feel) || disabled,
7741
7801
  onClick: handleFeelToggle
7742
7802
  }), feelActive ? jsx(FeelEditor, {
7743
7803
  name: id,
@@ -8195,6 +8255,68 @@ function prefixId$5(id) {
8195
8255
  return `bio-properties-panel-${id}`;
8196
8256
  }
8197
8257
 
8258
+ /**
8259
+ * Determine if FEEL is optional for the configured {@link FeelType}.
8260
+ *
8261
+ * @param {FeelType} feelType
8262
+ *
8263
+ * @return {boolean}
8264
+ */
8265
+ function isFeelOptional(feelType) {
8266
+ return feelType === 'optional' || feelType === 'optional-default-enabled';
8267
+ }
8268
+
8269
+ /**
8270
+ * Determine if FEEL editing is currently active.
8271
+ *
8272
+ * @param {FeelType} feelType
8273
+ * @param {string} localValue
8274
+ *
8275
+ * @return {boolean}
8276
+ */
8277
+ function isFeelActive(feelType, localValue) {
8278
+ if (feelType === 'required') {
8279
+ return true;
8280
+ }
8281
+ if (isString(localValue)) {
8282
+ if (localValue.startsWith('=')) {
8283
+ return true;
8284
+ }
8285
+ }
8286
+ return false;
8287
+ }
8288
+
8289
+ /**
8290
+ * @template T
8291
+ * @param {T} value
8292
+ *
8293
+ * @return {string|T}
8294
+ */
8295
+ function getFeelValue(value) {
8296
+ if (isString(value) && value.startsWith('=')) {
8297
+ return value.substring(1);
8298
+ }
8299
+ return value;
8300
+ }
8301
+
8302
+ /**
8303
+ * Initialize local FEEL value.
8304
+ *
8305
+ * `optional-default-enabled` starts in FEEL mode if no value or empty string is provided.
8306
+ *
8307
+ * @template T
8308
+ * @param {FeelType} feelType
8309
+ * @param {T} value
8310
+ *
8311
+ * @return {string|T}
8312
+ */
8313
+ function getInitialFeelLocalValue(feelType, value) {
8314
+ if (feelType === 'optional-default-enabled' && (value === undefined || value === '')) {
8315
+ return '=';
8316
+ }
8317
+ return value;
8318
+ }
8319
+
8198
8320
  /**
8199
8321
  * @typedef { { value: string, label: string, disabled: boolean, children: { value: string, label: string, disabled: boolean } } } Option
8200
8322
  */
@@ -8445,7 +8567,7 @@ function TextArea(props) {
8445
8567
  };
8446
8568
  const handleOnKeyDown = e => {
8447
8569
  if (isCmdWithChar(e)) {
8448
- handleInput.flush();
8570
+ handleInput.flush?.();
8449
8571
  }
8450
8572
  };
8451
8573
  useLayoutEffect(() => {
@@ -8660,7 +8782,7 @@ function Textfield(props) {
8660
8782
  }, [value]);
8661
8783
  const handleOnKeyDown = e => {
8662
8784
  if (isCmdWithChar(e)) {
8663
- handleInput.flush();
8785
+ handleInput.flush?.();
8664
8786
  }
8665
8787
  };
8666
8788
  return jsxs("div", {
@@ -9028,6 +9150,7 @@ function Title(props) {
9028
9150
  class: "bio-properties-panel-popup__title",
9029
9151
  children: title
9030
9152
  }), children, showCloseButton && jsx("button", {
9153
+ type: "button",
9031
9154
  title: closeButtonTooltip,
9032
9155
  class: "bio-properties-panel-popup__close",
9033
9156
  onClick: onClose,
@@ -13800,7 +13923,7 @@ function CustomPropertiesGroup(field, editField) {
13800
13923
  event.stopPropagation();
13801
13924
  return editField(field, ['properties'], removeKey(properties, key));
13802
13925
  };
13803
- const id = `property-${field.id}-${index}`;
13926
+ const id = `property-${index}`;
13804
13927
  return {
13805
13928
  autoFocusEntry: id + '-key',
13806
13929
  entries: CustomValueEntry({