@jsenv/navi 0.1.0 → 0.2.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.
@@ -1,6 +1,7 @@
1
1
  import { resolveCSSSize } from "@jsenv/dom";
2
- import { createPortal } from "preact/compat";
2
+ import { createPortal, forwardRef } from "preact/compat";
3
3
  import { useLayoutEffect, useRef, useState } from "preact/hooks";
4
+
4
5
  import { useDebounceTrue } from "../use_debounce_true.js";
5
6
  import { RectangleLoading } from "./rectangle_loading.jsx";
6
7
 
@@ -10,6 +11,8 @@ import.meta.css = /* css */ `
10
11
  width: fit-content;
11
12
  display: inline-flex;
12
13
  height: fit-content;
14
+ border-radius: inherit;
15
+ cursor: inherit;
13
16
  }
14
17
 
15
18
  .navi_loading_rectangle_wrapper {
@@ -27,31 +30,53 @@ import.meta.css = /* css */ `
27
30
  }
28
31
  `;
29
32
 
30
- export const LoadableInlineElement = ({
31
- children,
32
- width,
33
- height,
34
- ...props
35
- }) => {
36
- const actionName = props["data-action"];
37
- if (actionName) {
38
- delete props["data-action"];
39
- }
33
+ export const LoadableInlineElement = forwardRef((props, ref) => {
34
+ const {
35
+ // background props
36
+ loading,
37
+ containerRef,
38
+ targetSelector,
39
+ color,
40
+ inset,
41
+ spacingTop,
42
+ spacingLeft,
43
+ spacingBottom,
44
+ spacingRight,
45
+ // other props
46
+ width,
47
+ height,
48
+ children,
49
+ ...rest
50
+ } = props;
40
51
 
41
52
  return (
42
53
  <span
54
+ {...rest}
55
+ ref={ref}
43
56
  className="navi_inline_wrapper"
44
57
  style={{
58
+ ...rest.style,
45
59
  ...(width ? { width } : {}),
46
60
  ...(height ? { height } : {}),
47
61
  }}
48
- data-action={actionName}
49
62
  >
50
- <LoaderBackground {...props} />
63
+ <LoaderBackground
64
+ {...{
65
+ loading,
66
+ containerRef,
67
+ targetSelector,
68
+ color,
69
+ inset,
70
+ spacingTop,
71
+ spacingLeft,
72
+ spacingBottom,
73
+ spacingRight,
74
+ }}
75
+ />
51
76
  {children}
52
77
  </span>
53
78
  );
54
- };
79
+ });
55
80
 
56
81
  export const LoaderBackground = ({
57
82
  loading,
@@ -249,6 +274,8 @@ const LoaderBackgroundBasic = ({
249
274
  setPaddingBottom(paddingBottom);
250
275
 
251
276
  if (color) {
277
+ // const resolvedColor = resolveCSSColor(color, rectangle, "css");
278
+ // console.log(resolvedColor);
252
279
  setCurrentColor(color);
253
280
  } else if (
254
281
  newOutlineColor &&
@@ -7,6 +7,9 @@ export const READONLY_CONSTRAINT = {
7
7
  if (!element.readonly && !element.hasAttribute("data-readonly")) {
8
8
  return null;
9
9
  }
10
+ if (element.type === "hidden") {
11
+ return null;
12
+ }
10
13
  const readonlySilent = element.hasAttribute("data-readonly-silent");
11
14
  if (readonlySilent) {
12
15
  return { silent: true };
@@ -21,11 +24,13 @@ export const READONLY_CONSTRAINT = {
21
24
  const isBusy = element.getAttribute("aria-busy") === "true";
22
25
  if (isBusy) {
23
26
  return {
27
+ target: element,
24
28
  message: `Cette action est en cours. Veuillez patienter.`,
25
29
  level: "info",
26
30
  };
27
31
  }
28
32
  return {
33
+ target: element,
29
34
  message:
30
35
  element.tagName === "BUTTON"
31
36
  ? `Cet action n'est pas disponible pour l'instant.`
@@ -1,5 +1,7 @@
1
1
  import {
2
2
  allowWheelThrough,
3
+ createPubSub,
4
+ createStyleController,
3
5
  getBorderSizes,
4
6
  pickPositionRelativeTo,
5
7
  visibleRectEffect,
@@ -23,7 +25,21 @@ import {
23
25
  * @returns {Function} - Function to hide and remove the validation message
24
26
  */
25
27
 
28
+ // Configuration parameters for validation message appearance
29
+ const BORDER_WIDTH = 1;
30
+ const CORNER_RADIUS = 3;
31
+ const ARROW_WIDTH = 16;
32
+ const ARROW_HEIGHT = 8;
33
+ const ARROW_SPACING = 8;
34
+
26
35
  import.meta.css = /* css */ `
36
+ :root {
37
+ --navi-info-color: #2196f3;
38
+ --navi-warning-color: #ff9800;
39
+ --navi-error-color: #f44336;
40
+ --navi-validation-message-background-color: white;
41
+ }
42
+
27
43
  /* Ensure the validation message CANNOT cause overflow */
28
44
  /* might be important to ensure it cannot create scrollbars in the document */
29
45
  /* When measuring the size it should take */
@@ -34,48 +50,48 @@ import.meta.css = /* css */ `
34
50
  }
35
51
 
36
52
  .jsenv_validation_message {
37
- display: block;
38
- overflow: visible;
39
- height: auto;
40
53
  position: absolute;
54
+ top: 0;
55
+ left: 0;
41
56
  z-index: 1;
57
+ display: block;
58
+ height: auto;
42
59
  opacity: 0;
43
- left: 0;
44
- top: 0;
45
60
  /* will be positioned with transform: translate */
46
61
  transition: opacity 0.2s ease-in-out;
62
+ overflow: visible;
47
63
  }
48
64
 
49
65
  .jsenv_validation_message_border {
50
66
  position: absolute;
51
- pointer-events: none;
52
67
  filter: drop-shadow(4px 4px 3px rgba(0, 0, 0, 0.2));
68
+ pointer-events: none;
53
69
  }
54
70
 
55
71
  .jsenv_validation_message_body_wrapper {
72
+ position: relative;
56
73
  border-style: solid;
57
74
  border-color: transparent;
58
- position: relative;
59
75
  }
60
76
 
61
77
  .jsenv_validation_message_body {
62
- padding: 8px;
63
78
  position: relative;
64
- max-width: 47vw;
65
79
  display: flex;
80
+ max-width: 47vw;
81
+ padding: 8px;
66
82
  flex-direction: row;
67
83
  gap: 10px;
68
84
  }
69
85
 
70
86
  .jsenv_validation_message_icon {
71
87
  display: flex;
72
- align-self: flex-start;
73
- align-items: center;
74
- justify-content: center;
75
88
  width: 22px;
76
89
  height: 22px;
77
- border-radius: 2px;
78
90
  flex-shrink: 0;
91
+ align-items: center;
92
+ align-self: flex-start;
93
+ justify-content: center;
94
+ border-radius: 2px;
79
95
  }
80
96
 
81
97
  .jsenv_validation_message_exclamation_svg {
@@ -84,21 +100,30 @@ import.meta.css = /* css */ `
84
100
  color: white;
85
101
  }
86
102
 
103
+ .jsenv_validation_message[data-level="info"] .border_path {
104
+ fill: var(--navi-info-color);
105
+ }
87
106
  .jsenv_validation_message[data-level="info"] .jsenv_validation_message_icon {
88
- background-color: #2196f3;
107
+ background-color: var(--navi-info-color);
108
+ }
109
+ .jsenv_validation_message[data-level="warning"] .border_path {
110
+ fill: var(--navi-warning-color);
89
111
  }
90
112
  .jsenv_validation_message[data-level="warning"]
91
113
  .jsenv_validation_message_icon {
92
- background-color: #ff9800;
114
+ background-color: var(--navi-warning-color);
115
+ }
116
+ .jsenv_validation_message[data-level="error"] .border_path {
117
+ fill: var(--navi-error-color);
93
118
  }
94
119
  .jsenv_validation_message[data-level="error"] .jsenv_validation_message_icon {
95
- background-color: #f44336;
120
+ background-color: var(--navi-error-color);
96
121
  }
97
122
 
98
123
  .jsenv_validation_message_content {
124
+ min-width: 0;
99
125
  align-self: center;
100
126
  word-break: break-word;
101
- min-width: 0;
102
127
  overflow-wrap: anywhere;
103
128
  }
104
129
 
@@ -108,12 +133,8 @@ import.meta.css = /* css */ `
108
133
  overflow: visible;
109
134
  }
110
135
 
111
- .border_path {
112
- fill: var(--border-color);
113
- }
114
-
115
136
  .background_path {
116
- fill: var(--background-color);
137
+ fill: var(--navi-validation-message-background-color);
117
138
  }
118
139
 
119
140
  .jsenv_validation_message_close_button_column {
@@ -121,16 +142,16 @@ import.meta.css = /* css */ `
121
142
  height: 22px;
122
143
  }
123
144
  .jsenv_validation_message_close_button {
124
- border: none;
125
- background: none;
126
- padding: 0;
127
145
  width: 1em;
128
146
  height: 1em;
129
- font-size: inherit;
130
- cursor: pointer;
131
- border-radius: 0.2em;
147
+ padding: 0;
132
148
  align-self: center;
133
149
  color: currentColor;
150
+ font-size: inherit;
151
+ background: none;
152
+ border: none;
153
+ border-radius: 0.2em;
154
+ cursor: pointer;
134
155
  }
135
156
  .jsenv_validation_message_close_button:hover {
136
157
  background: rgba(0, 0, 0, 0.1);
@@ -141,8 +162,8 @@ import.meta.css = /* css */ `
141
162
  }
142
163
 
143
164
  .error_stack {
144
- overflow: auto;
145
165
  max-height: 200px;
166
+ overflow: auto;
146
167
  }
147
168
  `;
148
169
 
@@ -191,6 +212,9 @@ const validationMessageTemplate = /* html */ `
191
212
  </div>
192
213
  `;
193
214
 
215
+ const validationMessageStyleController =
216
+ createStyleController("validation_message");
217
+
194
218
  export const openValidationMessage = (
195
219
  targetElement,
196
220
  message,
@@ -210,8 +234,8 @@ export const openValidationMessage = (
210
234
  });
211
235
  }
212
236
 
237
+ const [teardown, addTeardown] = createPubSub(true);
213
238
  let opened = true;
214
- const closeCallbackSet = new Set();
215
239
  const close = (reason) => {
216
240
  if (!opened) {
217
241
  return;
@@ -220,10 +244,7 @@ export const openValidationMessage = (
220
244
  console.debug(`validation message closed (reason: ${reason})`);
221
245
  }
222
246
  opened = false;
223
- for (const closeCallback of closeCallbackSet) {
224
- closeCallback();
225
- }
226
- closeCallbackSet.clear();
247
+ teardown(reason);
227
248
  };
228
249
 
229
250
  // Create and add validation message to document
@@ -244,15 +265,6 @@ export const openValidationMessage = (
244
265
  { level = "warning", closeOnClickOutside = level === "info" } = {},
245
266
  ) => {
246
267
  _closeOnClickOutside = closeOnClickOutside;
247
- const borderColor =
248
- level === "info" ? "blue" : level === "warning" ? "grey" : "red";
249
- const backgroundColor = "white";
250
-
251
- jsenvValidationMessage.style.setProperty("--border-color", borderColor);
252
- jsenvValidationMessage.style.setProperty(
253
- "--background-color",
254
- backgroundColor,
255
- );
256
268
 
257
269
  if (Error.isError(newMessage)) {
258
270
  const error = newMessage;
@@ -265,7 +277,7 @@ export const openValidationMessage = (
265
277
  };
266
278
  update(message, { level });
267
279
 
268
- jsenvValidationMessage.style.opacity = "0";
280
+ validationMessageStyleController.set(jsenvValidationMessage, { opacity: 0 });
269
281
 
270
282
  allowWheelThrough(jsenvValidationMessage, targetElement);
271
283
 
@@ -274,13 +286,18 @@ export const openValidationMessage = (
274
286
  jsenvValidationMessage.id = validationMessageId;
275
287
  targetElement.setAttribute("aria-invalid", "true");
276
288
  targetElement.setAttribute("aria-errormessage", validationMessageId);
277
- closeCallbackSet.add(() => {
289
+ targetElement.style.setProperty(
290
+ "--invalid-color",
291
+ `var(--navi-${level}-color)`,
292
+ );
293
+ addTeardown(() => {
278
294
  targetElement.removeAttribute("aria-invalid");
279
295
  targetElement.removeAttribute("aria-errormessage");
296
+ targetElement.style.removeProperty("--invalid-color");
280
297
  });
281
298
 
282
299
  document.body.appendChild(jsenvValidationMessage);
283
- closeCallbackSet.add(() => {
300
+ addTeardown(() => {
284
301
  jsenvValidationMessage.remove();
285
302
  });
286
303
 
@@ -291,12 +308,12 @@ export const openValidationMessage = (
291
308
  debug,
292
309
  },
293
310
  );
294
- closeCallbackSet.add(() => {
311
+ addTeardown(() => {
295
312
  positionFollower.stop();
296
313
  });
297
314
 
298
315
  if (onClose) {
299
- closeCallbackSet.add(onClose);
316
+ addTeardown(onClose);
300
317
  }
301
318
  close_on_target_focus: {
302
319
  const onfocus = () => {
@@ -310,7 +327,7 @@ export const openValidationMessage = (
310
327
  close("target_element_focus");
311
328
  };
312
329
  targetElement.addEventListener("focus", onfocus);
313
- closeCallbackSet.add(() => {
330
+ addTeardown(() => {
314
331
  targetElement.removeEventListener("focus", onfocus);
315
332
  });
316
333
  }
@@ -337,7 +354,7 @@ export const openValidationMessage = (
337
354
  close("click_outside");
338
355
  };
339
356
  document.addEventListener("click", handleClickOutside, true);
340
- closeCallbackSet.add(() => {
357
+ addTeardown(() => {
341
358
  document.removeEventListener("click", handleClickOutside, true);
342
359
  });
343
360
  }
@@ -349,19 +366,12 @@ export const openValidationMessage = (
349
366
  updatePosition: positionFollower.updatePosition,
350
367
  };
351
368
  targetElement.jsenvValidationMessage = validationMessage;
352
- closeCallbackSet.add(() => {
369
+ addTeardown(() => {
353
370
  delete targetElement.jsenvValidationMessage;
354
371
  });
355
372
  return validationMessage;
356
373
  };
357
374
 
358
- // Configuration parameters for validation message appearance
359
- const ARROW_WIDTH = 16;
360
- const ARROW_HEIGHT = 8;
361
- const CORNER_RADIUS = 3;
362
- const BORDER_WIDTH = 1;
363
- const ARROW_SPACING = 8;
364
-
365
375
  /**
366
376
  * Generates SVG path for validation message with arrow on top
367
377
  * @param {number} width - Validation message width
@@ -567,6 +577,8 @@ const stickValidationMessageToTarget = (validationMessage, targetElement) => {
567
577
  spaceBelowTarget,
568
578
  } = pickPositionRelativeTo(validationMessageClone, targetElement, {
569
579
  alignToViewportEdgeWhenTargetNearEdge: 20,
580
+ // when fully to the left, the border color is collé to the browser window making it hard to see
581
+ minLeft: 1,
570
582
  });
571
583
 
572
584
  // Get element padding and border to properly position arrow
@@ -581,7 +593,7 @@ const stickValidationMessageToTarget = (validationMessage, targetElement) => {
581
593
  let arrowTargetLeft;
582
594
  if (arrowPositionAttribute === "center") {
583
595
  // Target the center of the element
584
- arrowTargetLeft = targetRight / 2;
596
+ arrowTargetLeft = (targetLeft + targetRight) / 2;
585
597
  } else {
586
598
  // Default behavior: target the left edge of the element (after borders)
587
599
  arrowTargetLeft = targetLeft + targetBorderSizes.left;
@@ -626,14 +638,6 @@ const stickValidationMessageToTarget = (validationMessage, targetElement) => {
626
638
  contentHeight -= 16; // padding * 2
627
639
  const spaceRemainingAfterContent =
628
640
  spaceAvailableForContent - contentHeight;
629
- console.log({
630
- position,
631
- spaceBelowTarget,
632
- validationMessageHeight,
633
- spaceAvailableForContent,
634
- contentHeight,
635
- spaceRemainingAfterContent,
636
- });
637
641
  if (spaceRemainingAfterContent < 2) {
638
642
  const maxHeight = spaceAvailableForContent;
639
643
  validationMessageContent.style.maxHeight = `${maxHeight}px`;
@@ -667,10 +671,14 @@ const stickValidationMessageToTarget = (validationMessage, targetElement) => {
667
671
  );
668
672
  }
669
673
 
670
- validationMessage.style.opacity = visibilityRatio ? "1" : "0";
671
674
  validationMessage.setAttribute("data-position", position);
672
- validationMessage.style.transform = `translateX(${validationMessageLeft}px) translateY(${validationMessageTop}px)`;
673
-
675
+ validationMessageStyleController.set(validationMessage, {
676
+ opacity: visibilityRatio ? 1 : 0,
677
+ transform: {
678
+ translateX: validationMessageLeft,
679
+ translateY: validationMessageTop,
680
+ },
681
+ });
674
682
  validationMessageClone.remove();
675
683
  },
676
684
  );