@jsenv/navi 0.1.1 → 0.2.1

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.
@@ -3,29 +3,43 @@ import { forwardRef } from "preact/compat";
3
3
  import { useImperativeHandle, useRef, useState } from "preact/hooks";
4
4
 
5
5
  import.meta.css = /* css */ `
6
- label[data-readonly] {
7
- color: rgba(0, 0, 0, 0.5);
6
+ @layer navi {
7
+ label {
8
+ cursor: pointer;
9
+ }
10
+
11
+ label[data-readonly],
12
+ label[data-disabled] {
13
+ color: rgba(0, 0, 0, 0.5);
14
+ cursor: default;
15
+ }
8
16
  }
9
17
  `;
10
18
 
11
19
  export const ReportReadOnlyOnLabelContext = createContext();
20
+ export const ReportDisabledOnLabelContext = createContext();
12
21
 
13
22
  export const Label = forwardRef((props, ref) => {
14
- const { readOnly, children, ...rest } = props;
23
+ const { readOnly, disabled, children, ...rest } = props;
15
24
  const innerRef = useRef();
16
25
  useImperativeHandle(ref, () => innerRef.current);
17
26
 
18
27
  const [inputReadOnly, setInputReadOnly] = useState(false);
19
28
  const innerReadOnly = readOnly || inputReadOnly;
29
+ const [inputDisabled, setInputDisabled] = useState(false);
30
+ const innerDisabled = disabled || inputDisabled;
20
31
 
21
32
  return (
22
33
  <label
23
34
  ref={innerRef}
24
35
  data-readonly={innerReadOnly ? "" : undefined}
36
+ data-disabled={innerDisabled ? "" : undefined}
25
37
  {...rest}
26
38
  >
27
39
  <ReportReadOnlyOnLabelContext.Provider value={setInputReadOnly}>
28
- {children}
40
+ <ReportDisabledOnLabelContext.Provider value={setInputDisabled}>
41
+ {children}
42
+ </ReportDisabledOnLabelContext.Provider>
29
43
  </ReportReadOnlyOnLabelContext.Provider>
30
44
  </label>
31
45
  );
@@ -0,0 +1,10 @@
1
+ import.meta.css = /* css */ `
2
+ @layer navi {
3
+ :root {
4
+ --navi-background-color-readonly: #d3d3d3;
5
+ --navi-color-readonly: grey;
6
+ --navi-background-color-disabled: #d3d3d3;
7
+ --navi-color-disabled: #eeeeee;
8
+ }
9
+ }
10
+ `;
@@ -28,9 +28,11 @@ import {
28
28
  } from "./use_ui_state_controller.js";
29
29
 
30
30
  import.meta.css = /* css */ `
31
- .navi_radio_list {
32
- display: flex;
33
- flex-direction: column;
31
+ @layer navi {
32
+ .navi_radio_list {
33
+ display: flex;
34
+ flex-direction: column;
35
+ }
34
36
  }
35
37
  `;
36
38
 
@@ -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,126 +25,149 @@ import {
23
25
  * @returns {Function} - Function to hide and remove the validation message
24
26
  */
25
27
 
26
- import.meta.css = /* css */ `
27
- /* Ensure the validation message CANNOT cause overflow */
28
- /* might be important to ensure it cannot create scrollbars in the document */
29
- /* When measuring the size it should take */
30
- .jsenv_validation_message_container {
31
- position: fixed;
32
- inset: 0;
33
- overflow: hidden;
34
- }
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;
35
34
 
36
- .jsenv_validation_message {
37
- display: block;
38
- overflow: visible;
39
- height: auto;
40
- position: absolute;
41
- z-index: 1;
42
- opacity: 0;
43
- left: 0;
44
- top: 0;
45
- /* will be positioned with transform: translate */
46
- transition: opacity 0.2s ease-in-out;
47
- }
35
+ import.meta.css = /* css */ `
36
+ @layer navi {
37
+ :root {
38
+ --navi-info-color: #2196f3;
39
+ --navi-warning-color: #ff9800;
40
+ --navi-error-color: #f44336;
41
+ --navi-validation-message-background-color: white;
42
+ }
48
43
 
49
- .jsenv_validation_message_border {
50
- position: absolute;
51
- pointer-events: none;
52
- filter: drop-shadow(4px 4px 3px rgba(0, 0, 0, 0.2));
53
- }
44
+ /* Ensure the validation message CANNOT cause overflow */
45
+ /* might be important to ensure it cannot create scrollbars in the document */
46
+ /* When measuring the size it should take */
47
+ .jsenv_validation_message_container {
48
+ position: fixed;
49
+ inset: 0;
50
+ overflow: hidden;
51
+ }
54
52
 
55
- .jsenv_validation_message_body_wrapper {
56
- border-style: solid;
57
- border-color: transparent;
58
- position: relative;
59
- }
53
+ .jsenv_validation_message {
54
+ position: absolute;
55
+ top: 0;
56
+ left: 0;
57
+ z-index: 1;
58
+ display: block;
59
+ height: auto;
60
+ opacity: 0;
61
+ /* will be positioned with transform: translate */
62
+ transition: opacity 0.2s ease-in-out;
63
+ overflow: visible;
64
+ }
60
65
 
61
- .jsenv_validation_message_body {
62
- padding: 8px;
63
- position: relative;
64
- max-width: 47vw;
65
- display: flex;
66
- flex-direction: row;
67
- gap: 10px;
68
- }
66
+ .jsenv_validation_message_border {
67
+ position: absolute;
68
+ filter: drop-shadow(4px 4px 3px rgba(0, 0, 0, 0.2));
69
+ pointer-events: none;
70
+ }
69
71
 
70
- .jsenv_validation_message_icon {
71
- display: flex;
72
- align-self: flex-start;
73
- align-items: center;
74
- justify-content: center;
75
- width: 22px;
76
- height: 22px;
77
- border-radius: 2px;
78
- flex-shrink: 0;
79
- }
72
+ .jsenv_validation_message_body_wrapper {
73
+ position: relative;
74
+ border-style: solid;
75
+ border-color: transparent;
76
+ }
80
77
 
81
- .jsenv_validation_message_exclamation_svg {
82
- width: 16px;
83
- height: 12px;
84
- color: white;
85
- }
78
+ .jsenv_validation_message_body {
79
+ position: relative;
80
+ display: flex;
81
+ max-width: 47vw;
82
+ padding: 8px;
83
+ flex-direction: row;
84
+ gap: 10px;
85
+ }
86
86
 
87
- .jsenv_validation_message[data-level="info"] .jsenv_validation_message_icon {
88
- background-color: #2196f3;
89
- }
90
- .jsenv_validation_message[data-level="warning"]
91
87
  .jsenv_validation_message_icon {
92
- background-color: #ff9800;
93
- }
94
- .jsenv_validation_message[data-level="error"] .jsenv_validation_message_icon {
95
- background-color: #f44336;
96
- }
88
+ display: flex;
89
+ width: 22px;
90
+ height: 22px;
91
+ flex-shrink: 0;
92
+ align-items: center;
93
+ align-self: flex-start;
94
+ justify-content: center;
95
+ border-radius: 2px;
96
+ }
97
97
 
98
- .jsenv_validation_message_content {
99
- align-self: center;
100
- word-break: break-word;
101
- min-width: 0;
102
- overflow-wrap: anywhere;
103
- }
98
+ .jsenv_validation_message_exclamation_svg {
99
+ width: 16px;
100
+ height: 12px;
101
+ color: white;
102
+ }
104
103
 
105
- .jsenv_validation_message_border svg {
106
- position: absolute;
107
- inset: 0;
108
- overflow: visible;
109
- }
104
+ .jsenv_validation_message[data-level="info"] .border_path {
105
+ fill: var(--navi-info-color);
106
+ }
107
+ .jsenv_validation_message[data-level="info"]
108
+ .jsenv_validation_message_icon {
109
+ background-color: var(--navi-info-color);
110
+ }
111
+ .jsenv_validation_message[data-level="warning"] .border_path {
112
+ fill: var(--navi-warning-color);
113
+ }
114
+ .jsenv_validation_message[data-level="warning"]
115
+ .jsenv_validation_message_icon {
116
+ background-color: var(--navi-warning-color);
117
+ }
118
+ .jsenv_validation_message[data-level="error"] .border_path {
119
+ fill: var(--navi-error-color);
120
+ }
121
+ .jsenv_validation_message[data-level="error"]
122
+ .jsenv_validation_message_icon {
123
+ background-color: var(--navi-error-color);
124
+ }
110
125
 
111
- .border_path {
112
- fill: var(--border-color);
113
- }
126
+ .jsenv_validation_message_content {
127
+ min-width: 0;
128
+ align-self: center;
129
+ word-break: break-word;
130
+ overflow-wrap: anywhere;
131
+ }
114
132
 
115
- .background_path {
116
- fill: var(--background-color);
117
- }
133
+ .jsenv_validation_message_border svg {
134
+ position: absolute;
135
+ inset: 0;
136
+ overflow: visible;
137
+ }
118
138
 
119
- .jsenv_validation_message_close_button_column {
120
- display: flex;
121
- height: 22px;
122
- }
123
- .jsenv_validation_message_close_button {
124
- border: none;
125
- background: none;
126
- padding: 0;
127
- width: 1em;
128
- height: 1em;
129
- font-size: inherit;
130
- cursor: pointer;
131
- border-radius: 0.2em;
132
- align-self: center;
133
- color: currentColor;
134
- }
135
- .jsenv_validation_message_close_button:hover {
136
- background: rgba(0, 0, 0, 0.1);
137
- }
138
- .close_svg {
139
- width: 100%;
140
- height: 100%;
141
- }
139
+ .background_path {
140
+ fill: var(--navi-validation-message-background-color);
141
+ }
142
+
143
+ .jsenv_validation_message_close_button_column {
144
+ display: flex;
145
+ height: 22px;
146
+ }
147
+ .jsenv_validation_message_close_button {
148
+ width: 1em;
149
+ height: 1em;
150
+ padding: 0;
151
+ align-self: center;
152
+ color: currentColor;
153
+ font-size: inherit;
154
+ background: none;
155
+ border: none;
156
+ border-radius: 0.2em;
157
+ cursor: pointer;
158
+ }
159
+ .jsenv_validation_message_close_button:hover {
160
+ background: rgba(0, 0, 0, 0.1);
161
+ }
162
+ .close_svg {
163
+ width: 100%;
164
+ height: 100%;
165
+ }
142
166
 
143
- .error_stack {
144
- overflow: auto;
145
- max-height: 200px;
167
+ .error_stack {
168
+ max-height: 200px;
169
+ overflow: auto;
170
+ }
146
171
  }
147
172
  `;
148
173
 
@@ -191,6 +216,9 @@ const validationMessageTemplate = /* html */ `
191
216
  </div>
192
217
  `;
193
218
 
219
+ const validationMessageStyleController =
220
+ createStyleController("validation_message");
221
+
194
222
  export const openValidationMessage = (
195
223
  targetElement,
196
224
  message,
@@ -210,8 +238,8 @@ export const openValidationMessage = (
210
238
  });
211
239
  }
212
240
 
241
+ const [teardown, addTeardown] = createPubSub(true);
213
242
  let opened = true;
214
- const closeCallbackSet = new Set();
215
243
  const close = (reason) => {
216
244
  if (!opened) {
217
245
  return;
@@ -220,10 +248,7 @@ export const openValidationMessage = (
220
248
  console.debug(`validation message closed (reason: ${reason})`);
221
249
  }
222
250
  opened = false;
223
- for (const closeCallback of closeCallbackSet) {
224
- closeCallback();
225
- }
226
- closeCallbackSet.clear();
251
+ teardown(reason);
227
252
  };
228
253
 
229
254
  // Create and add validation message to document
@@ -244,15 +269,6 @@ export const openValidationMessage = (
244
269
  { level = "warning", closeOnClickOutside = level === "info" } = {},
245
270
  ) => {
246
271
  _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
272
 
257
273
  if (Error.isError(newMessage)) {
258
274
  const error = newMessage;
@@ -265,7 +281,7 @@ export const openValidationMessage = (
265
281
  };
266
282
  update(message, { level });
267
283
 
268
- jsenvValidationMessage.style.opacity = "0";
284
+ validationMessageStyleController.set(jsenvValidationMessage, { opacity: 0 });
269
285
 
270
286
  allowWheelThrough(jsenvValidationMessage, targetElement);
271
287
 
@@ -274,13 +290,18 @@ export const openValidationMessage = (
274
290
  jsenvValidationMessage.id = validationMessageId;
275
291
  targetElement.setAttribute("aria-invalid", "true");
276
292
  targetElement.setAttribute("aria-errormessage", validationMessageId);
277
- closeCallbackSet.add(() => {
293
+ targetElement.style.setProperty(
294
+ "--invalid-color",
295
+ `var(--navi-${level}-color)`,
296
+ );
297
+ addTeardown(() => {
278
298
  targetElement.removeAttribute("aria-invalid");
279
299
  targetElement.removeAttribute("aria-errormessage");
300
+ targetElement.style.removeProperty("--invalid-color");
280
301
  });
281
302
 
282
303
  document.body.appendChild(jsenvValidationMessage);
283
- closeCallbackSet.add(() => {
304
+ addTeardown(() => {
284
305
  jsenvValidationMessage.remove();
285
306
  });
286
307
 
@@ -291,12 +312,12 @@ export const openValidationMessage = (
291
312
  debug,
292
313
  },
293
314
  );
294
- closeCallbackSet.add(() => {
315
+ addTeardown(() => {
295
316
  positionFollower.stop();
296
317
  });
297
318
 
298
319
  if (onClose) {
299
- closeCallbackSet.add(onClose);
320
+ addTeardown(onClose);
300
321
  }
301
322
  close_on_target_focus: {
302
323
  const onfocus = () => {
@@ -310,7 +331,7 @@ export const openValidationMessage = (
310
331
  close("target_element_focus");
311
332
  };
312
333
  targetElement.addEventListener("focus", onfocus);
313
- closeCallbackSet.add(() => {
334
+ addTeardown(() => {
314
335
  targetElement.removeEventListener("focus", onfocus);
315
336
  });
316
337
  }
@@ -337,7 +358,7 @@ export const openValidationMessage = (
337
358
  close("click_outside");
338
359
  };
339
360
  document.addEventListener("click", handleClickOutside, true);
340
- closeCallbackSet.add(() => {
361
+ addTeardown(() => {
341
362
  document.removeEventListener("click", handleClickOutside, true);
342
363
  });
343
364
  }
@@ -349,19 +370,12 @@ export const openValidationMessage = (
349
370
  updatePosition: positionFollower.updatePosition,
350
371
  };
351
372
  targetElement.jsenvValidationMessage = validationMessage;
352
- closeCallbackSet.add(() => {
373
+ addTeardown(() => {
353
374
  delete targetElement.jsenvValidationMessage;
354
375
  });
355
376
  return validationMessage;
356
377
  };
357
378
 
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
379
  /**
366
380
  * Generates SVG path for validation message with arrow on top
367
381
  * @param {number} width - Validation message width
@@ -567,6 +581,8 @@ const stickValidationMessageToTarget = (validationMessage, targetElement) => {
567
581
  spaceBelowTarget,
568
582
  } = pickPositionRelativeTo(validationMessageClone, targetElement, {
569
583
  alignToViewportEdgeWhenTargetNearEdge: 20,
584
+ // when fully to the left, the border color is collé to the browser window making it hard to see
585
+ minLeft: 1,
570
586
  });
571
587
 
572
588
  // Get element padding and border to properly position arrow
@@ -581,7 +597,7 @@ const stickValidationMessageToTarget = (validationMessage, targetElement) => {
581
597
  let arrowTargetLeft;
582
598
  if (arrowPositionAttribute === "center") {
583
599
  // Target the center of the element
584
- arrowTargetLeft = targetRight / 2;
600
+ arrowTargetLeft = (targetLeft + targetRight) / 2;
585
601
  } else {
586
602
  // Default behavior: target the left edge of the element (after borders)
587
603
  arrowTargetLeft = targetLeft + targetBorderSizes.left;
@@ -626,14 +642,6 @@ const stickValidationMessageToTarget = (validationMessage, targetElement) => {
626
642
  contentHeight -= 16; // padding * 2
627
643
  const spaceRemainingAfterContent =
628
644
  spaceAvailableForContent - contentHeight;
629
- console.log({
630
- position,
631
- spaceBelowTarget,
632
- validationMessageHeight,
633
- spaceAvailableForContent,
634
- contentHeight,
635
- spaceRemainingAfterContent,
636
- });
637
645
  if (spaceRemainingAfterContent < 2) {
638
646
  const maxHeight = spaceAvailableForContent;
639
647
  validationMessageContent.style.maxHeight = `${maxHeight}px`;
@@ -667,10 +675,14 @@ const stickValidationMessageToTarget = (validationMessage, targetElement) => {
667
675
  );
668
676
  }
669
677
 
670
- validationMessage.style.opacity = visibilityRatio ? "1" : "0";
671
678
  validationMessage.setAttribute("data-position", position);
672
- validationMessage.style.transform = `translateX(${validationMessageLeft}px) translateY(${validationMessageTop}px)`;
673
-
679
+ validationMessageStyleController.set(validationMessage, {
680
+ opacity: visibilityRatio ? 1 : 0,
681
+ transform: {
682
+ translateX: validationMessageLeft,
683
+ translateY: validationMessageTop,
684
+ },
685
+ });
674
686
  validationMessageClone.remove();
675
687
  },
676
688
  );