@rettangoli/ui 1.7.1 → 1.7.3

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rettangoli/ui",
3
- "version": "1.7.1",
3
+ "version": "1.7.3",
4
4
  "description": "A UI component library for building web interfaces.",
5
5
  "main": "dist/rettangoli-esm.min.js",
6
6
  "type": "module",
@@ -18,6 +18,19 @@ const normalizeSelectedValues = (selectedValues) => {
18
18
  return [...selectedValues];
19
19
  };
20
20
 
21
+ const resolvePopoverPosition = (trigger) => {
22
+ if (!trigger || typeof trigger.getBoundingClientRect !== "function") {
23
+ return undefined;
24
+ }
25
+
26
+ const rect = trigger.getBoundingClientRect();
27
+ return {
28
+ x: Math.round(rect.left),
29
+ y: Math.round(rect.bottom + 12),
30
+ w: Math.max(Math.round(rect.width), 240),
31
+ };
32
+ };
33
+
21
34
  const resolveCurrentValues = ({ store, props }) => {
22
35
  if (store.selectHasSelectedValues()) {
23
36
  return store.selectSelectedValues();
@@ -26,6 +39,18 @@ const resolveCurrentValues = ({ store, props }) => {
26
39
  return normalizeSelectedValues(props.selectedValues);
27
40
  };
28
41
 
42
+ const resolveDraftValues = ({ store, props }) => {
43
+ if (Array.isArray(props?.draftSelectedValues)) {
44
+ return normalizeSelectedValues(props.draftSelectedValues);
45
+ }
46
+
47
+ if (store.getState().isOpen) {
48
+ return store.selectDraftSelectedValues();
49
+ }
50
+
51
+ return resolveCurrentValues({ store, props });
52
+ };
53
+
29
54
  const emitValueChange = ({
30
55
  dispatchEvent,
31
56
  value,
@@ -50,57 +75,180 @@ const emitValueChange = ({
50
75
  );
51
76
  };
52
77
 
78
+ const emitDraftValueChange = ({ dispatchEvent, value }) => {
79
+ dispatchEvent(
80
+ new CustomEvent("draft-value-change", {
81
+ detail: {
82
+ value,
83
+ },
84
+ bubbles: true,
85
+ }),
86
+ );
87
+ };
88
+
89
+ const emitOpenChange = ({ dispatchEvent, open }) => {
90
+ dispatchEvent(
91
+ new CustomEvent("open-change", {
92
+ detail: {
93
+ open,
94
+ },
95
+ bubbles: true,
96
+ }),
97
+ );
98
+ };
99
+
100
+ const openControlledPopover = ({ store, props, refs } = {}) => {
101
+ const position = resolvePopoverPosition(refs?.trigger);
102
+ if (!position) {
103
+ return false;
104
+ }
105
+
106
+ store.openOptionsPopover({
107
+ position,
108
+ values: resolveDraftValues({ store, props }),
109
+ });
110
+
111
+ return true;
112
+ };
113
+
53
114
  export const handleBeforeMount = (deps) => {
54
- const { store, props, render } = deps;
115
+ const { store, props, render, refs } = deps;
116
+ let shouldRender = false;
55
117
 
56
118
  if (props.selectedValues !== undefined) {
57
- store.updateSelectedValues({ values: props.selectedValues });
119
+ store.updateSelectedValues({
120
+ values: props.selectedValues,
121
+ syncDraft: props.draftSelectedValues === undefined,
122
+ preserveDraft: Array.isArray(props.draftSelectedValues),
123
+ });
124
+ shouldRender = true;
125
+ }
126
+
127
+ if (Array.isArray(props.draftSelectedValues)) {
128
+ store.updateDraftSelectedValues({
129
+ values: props.draftSelectedValues,
130
+ });
131
+ shouldRender = true;
132
+ }
133
+
134
+ if (props.open === true && !props.disabled && openControlledPopover(deps)) {
135
+ shouldRender = true;
136
+ }
137
+
138
+ if (shouldRender) {
58
139
  render();
59
140
  }
60
141
  };
61
142
 
143
+ export const handleAfterMount = (deps) => {
144
+ const { props, render, store, dispatchEvent } = deps;
145
+
146
+ if (props.disabled) {
147
+ if (props.open === true) {
148
+ emitOpenChange({
149
+ dispatchEvent,
150
+ open: false,
151
+ });
152
+ }
153
+ return;
154
+ }
155
+
156
+ if (props.open === true && !store.getState().isOpen) {
157
+ if (openControlledPopover(deps)) {
158
+ render();
159
+ }
160
+ }
161
+ };
162
+
62
163
  export const handleOnUpdate = (deps, payload) => {
63
164
  const { oldProps, newProps } = payload;
64
- const { store, render } = deps;
165
+ const { store, render, refs } = deps;
65
166
  let shouldRender = false;
66
167
 
67
168
  if (!!newProps?.disabled && !oldProps?.disabled) {
169
+ const wasOpen = store.getState().isOpen;
68
170
  store.closeOptionsPopover({});
171
+ if (wasOpen) {
172
+ emitOpenChange({
173
+ dispatchEvent: deps.dispatchEvent,
174
+ open: false,
175
+ });
176
+ }
69
177
  shouldRender = true;
70
178
  }
71
179
 
72
180
  if (oldProps.selectedValues !== newProps.selectedValues) {
73
- store.updateSelectedValues({ values: newProps.selectedValues, syncDraft: true });
181
+ store.updateSelectedValues({
182
+ values: newProps.selectedValues,
183
+ syncDraft: newProps.draftSelectedValues === undefined,
184
+ preserveDraft: Array.isArray(newProps.draftSelectedValues),
185
+ });
74
186
  shouldRender = true;
75
187
  }
76
188
 
77
- if (oldProps.options !== newProps.options) {
189
+ if (oldProps.draftSelectedValues !== newProps.draftSelectedValues) {
190
+ store.updateDraftSelectedValues({
191
+ values: Array.isArray(newProps?.draftSelectedValues)
192
+ ? newProps.draftSelectedValues
193
+ : resolveCurrentValues({ store, props: newProps }),
194
+ });
78
195
  shouldRender = true;
79
196
  }
80
197
 
198
+ if (oldProps.open !== newProps.open && newProps.open !== undefined) {
199
+ if (newProps.open) {
200
+ if (newProps.disabled) {
201
+ emitOpenChange({
202
+ dispatchEvent: deps.dispatchEvent,
203
+ open: false,
204
+ });
205
+ } else if (
206
+ openControlledPopover({
207
+ store,
208
+ props: newProps,
209
+ refs,
210
+ })
211
+ ) {
212
+ shouldRender = true;
213
+ }
214
+ } else {
215
+ store.closeOptionsPopover({});
216
+ shouldRender = true;
217
+ }
218
+ }
219
+
220
+ if (oldProps.options !== newProps.options) {
221
+ const hasCurrentValues = resolveCurrentValues({ store, props: newProps }).length > 0;
222
+
223
+ if (store.getState().isOpen || hasCurrentValues) {
224
+ shouldRender = true;
225
+ }
226
+ }
227
+
81
228
  if (shouldRender) {
82
229
  render();
83
230
  }
84
231
  };
85
232
 
86
233
  export const handleTriggerClick = (deps, payload) => {
87
- const { store, render, refs, props } = deps;
234
+ const { store, render, refs, props, dispatchEvent } = deps;
88
235
  if (props.disabled) return;
89
236
 
90
237
  const event = payload._event;
91
238
  event.stopPropagation();
92
239
 
93
- const trigger = refs.trigger;
94
- const rect = trigger.getBoundingClientRect();
95
- const currentValues = resolveCurrentValues({ store, props });
240
+ const position = resolvePopoverPosition(refs.trigger);
241
+ if (!position) {
242
+ return;
243
+ }
96
244
 
97
245
  store.openOptionsPopover({
98
- position: {
99
- x: Math.round(rect.left),
100
- y: Math.round(rect.bottom + 12),
101
- w: Math.max(Math.round(rect.width), 240),
102
- },
103
- values: currentValues,
246
+ position,
247
+ values: resolveDraftValues({ store, props }),
248
+ });
249
+ emitOpenChange({
250
+ dispatchEvent,
251
+ open: true,
104
252
  });
105
253
  render();
106
254
  };
@@ -116,13 +264,17 @@ export const handleTriggerKeyDown = (deps, payload) => {
116
264
  };
117
265
 
118
266
  export const handlePopoverClose = (deps) => {
119
- const { store, render } = deps;
267
+ const { store, render, dispatchEvent } = deps;
120
268
  store.closeOptionsPopover({});
269
+ emitOpenChange({
270
+ dispatchEvent,
271
+ open: false,
272
+ });
121
273
  render();
122
274
  };
123
275
 
124
276
  export const handleOptionClick = (deps, payload) => {
125
- const { render, props, store } = deps;
277
+ const { render, props, store, dispatchEvent } = deps;
126
278
  if (props.disabled) return;
127
279
 
128
280
  const event = payload._event;
@@ -137,6 +289,10 @@ export const handleOptionClick = (deps, payload) => {
137
289
  }
138
290
 
139
291
  store.toggleDraftSelectedValue({ value: option.value });
292
+ emitDraftValueChange({
293
+ dispatchEvent,
294
+ value: store.selectDraftSelectedValues(),
295
+ });
140
296
  render();
141
297
  };
142
298
 
@@ -175,6 +331,11 @@ export const handleSubmitClick = (deps, payload) => {
175
331
  item: undefined,
176
332
  });
177
333
 
334
+ emitOpenChange({
335
+ dispatchEvent,
336
+ open: false,
337
+ });
338
+
178
339
  render();
179
340
  };
180
341
 
@@ -1,3 +1,5 @@
1
+ import { deepEqual } from "../../common.js";
2
+
1
3
  const resolvePopoverPosition = (trigger) => {
2
4
  if (!trigger || typeof trigger.getBoundingClientRect !== "function") {
3
5
  return null;
@@ -11,12 +13,66 @@ const resolvePopoverPosition = (trigger) => {
11
13
  };
12
14
  };
13
15
 
14
- export const refreshPopover = function () {
16
+ const normalizeSelectedValues = (selectedValues) => {
17
+ if (!Array.isArray(selectedValues)) {
18
+ return [];
19
+ }
20
+
21
+ return [...selectedValues];
22
+ };
23
+
24
+ const renderInstance = (instance) => {
25
+ if (typeof instance.render === "function") {
26
+ instance.render();
27
+ }
28
+ };
29
+
30
+ const resolveCurrentValues = (instance) => {
31
+ if (instance.store?.selectHasSelectedValues?.()) {
32
+ return instance.store?.selectSelectedValues?.() || [];
33
+ }
34
+
35
+ return normalizeSelectedValues(instance.props?.selectedValues);
36
+ };
37
+
38
+ const openPopoverWithDraft = (instance, values = []) => {
39
+ const position = resolvePopoverPosition(instance.refs?.trigger);
40
+
41
+ if (!position || !instance.store?.openOptionsPopover) {
42
+ return false;
43
+ }
44
+
45
+ instance.store.openOptionsPopover({
46
+ position,
47
+ values,
48
+ });
49
+
50
+ return true;
51
+ };
52
+
53
+ const setDraftValues = (instance, values = [], keepOpen = false) => {
54
+ const state = instance.store?.getState?.();
55
+
56
+ if (state?.isOpen && instance.store?.updateDraftSelectedValues) {
57
+ instance.store.updateDraftSelectedValues({ values });
58
+ return true;
59
+ }
60
+
61
+ if (keepOpen) {
62
+ return openPopoverWithDraft(instance, values);
63
+ }
64
+
65
+ return false;
66
+ };
67
+
68
+ export const refreshPopover = function (payload = {}) {
15
69
  const state = this.store?.getState?.();
70
+ const draftValues = Array.isArray(payload.values)
71
+ ? normalizeSelectedValues(payload.values)
72
+ : (this.store?.selectDraftSelectedValues?.() || []);
16
73
 
17
74
  if (state?.isOpen) {
18
75
  const position = resolvePopoverPosition(this.refs?.trigger);
19
- const draftValues = this.store?.selectDraftSelectedValues?.() || [];
20
76
 
21
77
  if (position) {
22
78
  this.store.openOptionsPopover({
@@ -24,9 +80,46 @@ export const refreshPopover = function () {
24
80
  values: draftValues,
25
81
  });
26
82
  }
83
+ } else if (payload.keepOpen) {
84
+ openPopoverWithDraft(this, draftValues.length > 0 ? draftValues : resolveCurrentValues(this));
27
85
  }
28
86
 
29
- if (typeof this.render === "function") {
30
- this.render();
87
+ renderInstance(this);
88
+ };
89
+
90
+ export const setDraftSelectedValues = function (payload = {}) {
91
+ const values = normalizeSelectedValues(payload.values);
92
+
93
+ if (!setDraftValues(this, values, !!payload.keepOpen)) {
94
+ return;
95
+ }
96
+
97
+ renderInstance(this);
98
+ };
99
+
100
+ export const appendDraftSelectedValue = function (payload = {}) {
101
+ if (!Object.prototype.hasOwnProperty.call(payload || {}, "value")) {
102
+ return;
31
103
  }
104
+
105
+ const state = this.store?.getState?.();
106
+ const currentValues = state?.isOpen
107
+ ? (this.store?.selectDraftSelectedValues?.() || [])
108
+ : resolveCurrentValues(this);
109
+
110
+ if (currentValues.some((currentValue) => deepEqual(currentValue, payload.value))) {
111
+ if (!!payload.keepOpen && !state?.isOpen && openPopoverWithDraft(this, currentValues)) {
112
+ renderInstance(this);
113
+ }
114
+
115
+ return;
116
+ }
117
+
118
+ const nextValues = [...normalizeSelectedValues(currentValues), payload.value];
119
+
120
+ if (!setDraftValues(this, nextValues, !!payload.keepOpen)) {
121
+ return;
122
+ }
123
+
124
+ renderInstance(this);
32
125
  };
@@ -31,6 +31,12 @@ propsSchema:
31
31
  type: array
32
32
  items:
33
33
  type: any
34
+ draftSelectedValues:
35
+ type: array
36
+ items:
37
+ type: any
38
+ open:
39
+ type: boolean
34
40
  onChange:
35
41
  type: function
36
42
  addOption:
@@ -44,8 +50,27 @@ propsSchema:
44
50
  type: string
45
51
  events:
46
52
  value-change: {}
53
+ draft-value-change: {}
54
+ open-change: {}
47
55
  add-option-click: {}
48
56
  methods:
49
57
  type: object
50
58
  properties:
51
- refreshPopover: {}
59
+ refreshPopover:
60
+ description: Recomputes the open popover position and rerenders the current options and draft values.
61
+ params:
62
+ - values
63
+ - keepOpen
64
+ returns: void
65
+ setDraftSelectedValues:
66
+ description: Replaces the draft-only selection without committing `selectedValues`; optionally reopens the popover around the trigger when `keepOpen` is true.
67
+ params:
68
+ - values
69
+ - keepOpen
70
+ returns: void
71
+ appendDraftSelectedValue:
72
+ description: Adds a value to the draft-only selection without committing `selectedValues`; optionally reopens the popover around the trigger when `keepOpen` is true.
73
+ params:
74
+ - value
75
+ - keepOpen
76
+ returns: void
@@ -7,7 +7,9 @@ const blacklistedProps = [
7
7
  "slot",
8
8
  "placeholder",
9
9
  "options",
10
+ "open",
10
11
  "selectedValues",
12
+ "draftSelectedValues",
11
13
  "onChange",
12
14
  "addOption",
13
15
  "disabled",
@@ -68,26 +70,6 @@ const sameValueArray = (left = [], right = []) => {
68
70
  return left.every((value, index) => deepEqual(value, right[index]));
69
71
  };
70
72
 
71
- const stringifyKeyPart = (value) => {
72
- if (value === undefined) {
73
- return "undefined";
74
- }
75
-
76
- if (value === null) {
77
- return "null";
78
- }
79
-
80
- if (typeof value === "string") {
81
- return value;
82
- }
83
-
84
- try {
85
- return JSON.stringify(value);
86
- } catch {
87
- return String(value);
88
- }
89
- };
90
-
91
73
  const isSelectedValue = (selectedValues = [], candidate) => {
92
74
  return selectedValues.some((value) => deepEqual(value, candidate));
93
75
  };
@@ -133,37 +115,6 @@ const buildTagStyle = ({ isSelected = true, isAddChip = false } = {}) => {
133
115
  return `${baseStyle.join("; ")};`;
134
116
  };
135
117
 
136
- const buildPopoverSignature = (options = []) => {
137
- if (!Array.isArray(options) || options.length === 0) {
138
- return "empty";
139
- }
140
-
141
- return options.map((option, index) => {
142
- const type = getOptionType(option);
143
-
144
- if (type === "section") {
145
- return `section:${index}:${option.label || ""}`;
146
- }
147
-
148
- if (type === "separator") {
149
- return `separator:${index}`;
150
- }
151
-
152
- return `item:${index}:${option.label || ""}:${stringifyKeyPart(option.value)}`;
153
- }).join("|");
154
- };
155
-
156
- const buildPopoverKey = (options = []) => {
157
- const signature = buildPopoverSignature(options);
158
- let hash = 0;
159
-
160
- for (let index = 0; index < signature.length; index += 1) {
161
- hash = ((hash << 5) - hash + signature.charCodeAt(index)) >>> 0;
162
- }
163
-
164
- return `tagSelectPopover${hash}`;
165
- };
166
-
167
118
  const normalizeOption = ({
168
119
  option = {},
169
120
  index,
@@ -227,7 +178,8 @@ export const selectViewData = ({ state, props }) => {
227
178
  const isDisabled = !!props.disabled;
228
179
  const currentValues = getCurrentValues({ state, props });
229
180
  const draftValues = getDraftValues({ state, props });
230
- const options = Array.isArray(props.options) ? props.options : [];
181
+ const shouldResolveOptions = state.isOpen || currentValues.length > 0;
182
+ const options = shouldResolveOptions && Array.isArray(props.options) ? props.options : [];
231
183
  const hasIconColumn = options.some((option) => isSelectableOption(option) && hasOwnProp(option, "icon"));
232
184
  const normalizedOptions = options.map((option, index) =>
233
185
  normalizeOption({
@@ -254,6 +206,19 @@ export const selectViewData = ({ state, props }) => {
254
206
 
255
207
  const hasSelectableOptions = normalizedOptions.some((option) => option.isItem);
256
208
  const hasDraftChanges = !sameValueArray(currentValues, draftValues);
209
+ const triggerTags = selectedTags.length > 0
210
+ ? selectedTags.map((tag) => ({
211
+ ...tag,
212
+ tagStyle: buildTagStyle({ isSelected: true }),
213
+ }))
214
+ : [{
215
+ value: undefined,
216
+ selectionIndex: "",
217
+ label: props.placeholder || "Add tag",
218
+ icon: "",
219
+ testId: "",
220
+ tagStyle: buildTagStyle({ isAddChip: true }),
221
+ }];
257
222
 
258
223
  return {
259
224
  containerAttrString,
@@ -262,12 +227,8 @@ export const selectViewData = ({ state, props }) => {
262
227
  position: state.position,
263
228
  options: normalizedOptions,
264
229
  hasSelectableOptions,
265
- popoverKey: buildPopoverKey(options),
266
230
  placeholder: props.placeholder || "Add tag",
267
- selectedTags,
268
- hasSelectedTags: selectedTags.length > 0,
269
- triggerTagStyle: buildTagStyle({ isSelected: true }),
270
- placeholderTagStyle: buildTagStyle({ isAddChip: true }),
231
+ triggerTags,
271
232
  triggerCursor: isDisabled ? "not-allowed" : "pointer",
272
233
  triggerTabIndex: isDisabled ? -1 : 0,
273
234
  showAddOption: true,
@@ -305,11 +266,15 @@ export const closeOptionsPopover = ({ state }) => {
305
266
  state.draftSelectedValues = [];
306
267
  };
307
268
 
269
+ export const updateDraftSelectedValues = ({ state }, payload = {}) => {
270
+ state.draftSelectedValues = normalizeSelectedValues(payload.values);
271
+ };
272
+
308
273
  export const updateSelectedValues = ({ state }, payload = {}) => {
309
274
  const values = normalizeSelectedValues(payload.values);
310
275
  state.selectedValues = values;
311
276
  state.hasSelectedValues = true;
312
- if (payload.syncDraft || !state.isOpen) {
277
+ if ((payload.syncDraft || !state.isOpen) && !payload.preserveDraft) {
313
278
  state.draftSelectedValues = [...values];
314
279
  }
315
280
  };
@@ -31,13 +31,10 @@ styles:
31
31
  template:
32
32
  - 'rtgl-view#trigger d=h av=c g=sm cur=${triggerCursor} ${containerAttrString} data-testid="tag-select-trigger" role="button" tabindex=${triggerTabIndex} aria-disabled=${isDisabled} style="min-height: 24px;"':
33
33
  - rtgl-view d=h av=c wrap g=sm w=1fg:
34
- - $if !hasSelectedTags:
35
- - 'rtgl-tag v=mu style="${placeholderTagStyle}"':
36
- ${placeholder}
37
- - $for tag, i in selectedTags:
38
- - 'rtgl-tag#tag${tag.selectionIndex} data-selection-index=${tag.selectionIndex} pre=${tag.icon} data-testid=${tag.testId} v=mu style="${triggerTagStyle}"':
34
+ - $for tag, i in triggerTags:
35
+ - 'rtgl-tag data-selection-index=${tag.selectionIndex} pre=${tag.icon} data-testid=${tag.testId} v=mu style="${tag.tagStyle}"':
39
36
  ${tag.label}
40
- - 'div#${popoverKey} style="display: contents;"':
37
+ - $if isOpen:
41
38
  - rtgl-popover#popover ?open=${isOpen} x=${position.x} y=${position.y} place=bs content-w=${position.w} content-g=sm content-sv=true content-ph=md content-pv=md:
42
39
  - $if !hasSelectableOptions:
43
40
  - rtgl-text s=sm c=mu-fg: No tags available
@@ -51,7 +48,7 @@ template:
51
48
  - 'rtgl-tag v=mu pre=${option.icon} style="${option.tagStyle}"':
52
49
  ${option.label}
53
50
  $elif option.isSeparator:
54
- - rtgl-view w=f h=1 bgc=mu my=xs: null
51
+ - rtgl-view w=f h=1 bgc=mu mv=xs: null
55
52
  - $if showAddOption:
56
53
  - rtgl-button#addOptionButton v=gh s=sm pre=plus data-testid="tag-select-add-option":
57
54
  ${addOptionLabel}