@rettangoli/ui 1.1.0 → 1.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rettangoli/ui",
3
- "version": "1.1.0",
3
+ "version": "1.2.0",
4
4
  "description": "A UI component library for building web interfaces.",
5
5
  "main": "dist/rettangoli-esm.min.js",
6
6
  "type": "module",
@@ -58,7 +58,7 @@ const syncFieldValueAttribute = ({ ref, fieldType, value, forceRefresh = false }
58
58
  ref.setAttribute("value", String(value));
59
59
  };
60
60
 
61
- const syncSelectFieldState = ({ ref, value }) => {
61
+ const syncChoiceFieldState = ({ ref, value }) => {
62
62
  if (!ref) return;
63
63
  if (!ref?.store?.updateSelectedValue) return;
64
64
  ref.store.updateSelectedValue({ value });
@@ -89,16 +89,18 @@ const resolveRenderRoot = (instance) => {
89
89
  return instance?.shadow;
90
90
  };
91
91
 
92
- const syncSelectRefsFromValues = ({ root, values = {} }) => {
92
+ const syncChoiceRefsFromValues = ({ root, values = {} }) => {
93
93
  if (!root || typeof root.querySelectorAll !== "function") return;
94
94
 
95
- const selectRefs = root.querySelectorAll("rtgl-select[data-field-name]");
96
- selectRefs.forEach((ref) => {
95
+ const choiceRefs = root.querySelectorAll(
96
+ "rtgl-select[data-field-name], rtgl-segmented-control[data-field-name]",
97
+ );
98
+ choiceRefs.forEach((ref) => {
97
99
  const fieldName = ref.dataset?.fieldName;
98
100
  if (!fieldName) return;
99
101
 
100
102
  const value = get(values, fieldName);
101
- syncSelectFieldState({ ref, value });
103
+ syncChoiceFieldState({ ref, value });
102
104
  });
103
105
  };
104
106
 
@@ -141,7 +143,7 @@ export const setValues = function (payload = {}) {
141
143
  });
142
144
 
143
145
  if (typeof ref?.tagName === "string" && ref.tagName.toUpperCase() === "RTGL-SELECT") {
144
- syncSelectFieldState({ ref, value });
146
+ syncChoiceFieldState({ ref, value });
145
147
  }
146
148
 
147
149
  if (field.type === "checkbox") {
@@ -155,7 +157,7 @@ export const setValues = function (payload = {}) {
155
157
  this.render();
156
158
  const syncSelects = () => {
157
159
  const nextState = this.store.getState();
158
- syncSelectRefsFromValues({
160
+ syncChoiceRefsFromValues({
159
161
  root: resolveRenderRoot(this),
160
162
  values: nextState.formValues,
161
163
  });
@@ -402,6 +402,7 @@ export const getDefaultValue = (field) => {
402
402
  case "input-number":
403
403
  return null;
404
404
  case "select":
405
+ case "segmented-control":
405
406
  return null;
406
407
  case "checkbox":
407
408
  return false;
@@ -506,7 +507,7 @@ export const selectViewData = ({ state, props }) => {
506
507
  field._inputType = field.inputType || "text";
507
508
  }
508
509
 
509
- if (field.type === "select") {
510
+ if (field.type === "select" || field.type === "segmented-control") {
510
511
  const val = get(state.formValues, field.name);
511
512
  field._selectedValue = val !== undefined ? val : null;
512
513
  field.placeholder = field.placeholder || "";
@@ -68,6 +68,8 @@ template:
68
68
  - rtgl-textarea#field${field._idx} data-field-name=${field.name} w=f rows=${field.rows} placeholder=${field.placeholder} ?disabled=${field._disabled}: null
69
69
  - $if field.type == "select":
70
70
  - rtgl-select#field${field._idx} data-field-name=${field.name} w=f :options=${field.options} ?no-clear=${field.noClear} :selectedValue=${field._selectedValue} :placeholder=${field.placeholder} ?disabled=${field._disabled}: null
71
+ - $if field.type == "segmented-control":
72
+ - rtgl-segmented-control#field${field._idx} data-field-name=${field.name} w=f :options=${field.options} ?no-clear=${field.noClear} :selectedValue=${field._selectedValue} :placeholder=${field.placeholder} ?disabled=${field._disabled}: null
71
73
  - $if field.type == "color-picker":
72
74
  - rtgl-color-picker#field${field._idx} data-field-name=${field.name} ?disabled=${field._disabled}: null
73
75
  - $if field.type == "slider":
@@ -0,0 +1,172 @@
1
+ import { deepEqual } from "../../common.js";
2
+
3
+ const emitValueChange = ({ dispatchEvent, value, label, index, item }) => {
4
+ dispatchEvent(
5
+ new CustomEvent("value-change", {
6
+ detail: {
7
+ value,
8
+ label,
9
+ index,
10
+ item,
11
+ },
12
+ bubbles: true,
13
+ }),
14
+ );
15
+ };
16
+
17
+ export const handleBeforeMount = (deps) => {
18
+ const { store, props, render } = deps;
19
+
20
+ if (
21
+ props.selectedValue !== null
22
+ && props.selectedValue !== undefined
23
+ && props.options
24
+ ) {
25
+ const selectedOption = props.options.find((opt) =>
26
+ deepEqual(opt.value, props.selectedValue),
27
+ );
28
+ if (selectedOption) {
29
+ store.updateSelectedValue({
30
+ value: selectedOption.value,
31
+ });
32
+ render();
33
+ }
34
+ }
35
+ };
36
+
37
+ export const handleOnUpdate = (deps, payload) => {
38
+ const { oldProps, newProps } = payload;
39
+ const { store, render } = deps;
40
+ let shouldRender = false;
41
+
42
+ if (oldProps.selectedValue !== newProps.selectedValue) {
43
+ store.updateSelectedValue({ value: newProps.selectedValue });
44
+ shouldRender = true;
45
+ }
46
+
47
+ if (shouldRender) {
48
+ render();
49
+ }
50
+ };
51
+
52
+ export const handleOptionClick = (deps, payload) => {
53
+ const { render, dispatchEvent, props, store } = deps;
54
+ if (props.disabled) return;
55
+
56
+ const event = payload._event;
57
+ event.stopPropagation();
58
+
59
+ const id = event.currentTarget.id.slice("option".length);
60
+ const index = Number(id);
61
+ const option = props.options[index];
62
+ const hasControlledValue = Object.prototype.hasOwnProperty.call(
63
+ props || {},
64
+ "selectedValue",
65
+ );
66
+ const currentValue = store.selectSelectedValue();
67
+ const hasCurrentValue = hasControlledValue ? true : store.selectHasSelectedValue();
68
+ const isSelected = option
69
+ ? hasCurrentValue && deepEqual(option.value, currentValue)
70
+ : false;
71
+
72
+ if (!option) {
73
+ return;
74
+ }
75
+
76
+ if (isSelected && !props.noClear) {
77
+ store.clearSelectedValue({});
78
+
79
+ if (props.onChange && typeof props.onChange === "function") {
80
+ props.onChange(undefined);
81
+ }
82
+
83
+ emitValueChange({
84
+ dispatchEvent,
85
+ value: undefined,
86
+ label: undefined,
87
+ index: null,
88
+ item: undefined,
89
+ });
90
+
91
+ render();
92
+ return;
93
+ }
94
+
95
+ store.updateSelectedValue({ value: option.value });
96
+
97
+ if (props.onChange && typeof props.onChange === "function") {
98
+ props.onChange(option.value);
99
+ }
100
+
101
+ emitValueChange({
102
+ dispatchEvent,
103
+ value: option.value,
104
+ label: option.label,
105
+ index,
106
+ item: option,
107
+ });
108
+
109
+ render();
110
+ };
111
+
112
+ export const handleOptionKeyDown = (deps, payload) => {
113
+ const event = payload._event;
114
+ if (event.key !== "Enter" && event.key !== " ") {
115
+ return;
116
+ }
117
+
118
+ event.preventDefault();
119
+ handleOptionClick(deps, payload);
120
+ };
121
+
122
+ export const handleOptionMouseEnter = (deps, payload) => {
123
+ const { store, render } = deps;
124
+ const event = payload._event;
125
+ const id = parseInt(event.currentTarget.id.slice("option".length), 10);
126
+ store.setHoveredOption({ optionId: id });
127
+ render();
128
+ };
129
+
130
+ export const handleOptionMouseLeave = (deps) => {
131
+ const { store, render } = deps;
132
+ store.clearHoveredOption({});
133
+ render();
134
+ };
135
+
136
+ export const handleAddOptionClick = (deps, payload) => {
137
+ if (deps.props.disabled) return;
138
+
139
+ const { render, dispatchEvent } = deps;
140
+ const { _event: event } = payload;
141
+ event.stopPropagation();
142
+
143
+ dispatchEvent(
144
+ new CustomEvent("add-option-click", {
145
+ bubbles: true,
146
+ }),
147
+ );
148
+
149
+ render();
150
+ };
151
+
152
+ export const handleAddOptionKeyDown = (deps, payload) => {
153
+ const event = payload._event;
154
+ if (event.key !== "Enter" && event.key !== " ") {
155
+ return;
156
+ }
157
+
158
+ event.preventDefault();
159
+ handleAddOptionClick(deps, payload);
160
+ };
161
+
162
+ export const handleAddOptionMouseEnter = (deps) => {
163
+ const { store, render } = deps;
164
+ store.setHoveredAddOption({ isHovered: true });
165
+ render();
166
+ };
167
+
168
+ export const handleAddOptionMouseLeave = (deps) => {
169
+ const { store, render } = deps;
170
+ store.setHoveredAddOption({ isHovered: false });
171
+ render();
172
+ };
@@ -0,0 +1,38 @@
1
+ componentName: rtgl-segmented-control
2
+ propsSchema:
3
+ type: object
4
+ properties:
5
+ placeholder:
6
+ type: string
7
+ options:
8
+ type: array
9
+ items:
10
+ type: object
11
+ properties:
12
+ label:
13
+ type: string
14
+ value:
15
+ type: any
16
+ testId:
17
+ type: string
18
+ selectedValue:
19
+ type: any
20
+ onChange:
21
+ type: function
22
+ noClear:
23
+ type: boolean
24
+ addOption:
25
+ type: object
26
+ properties:
27
+ label:
28
+ type: string
29
+ disabled:
30
+ type: boolean
31
+ w:
32
+ type: string
33
+ events:
34
+ value-change: {}
35
+ add-option-click: {}
36
+ methods:
37
+ type: object
38
+ properties: {}
@@ -0,0 +1,104 @@
1
+ import { deepEqual } from "../../common.js";
2
+
3
+ const blacklistedProps = [
4
+ "id",
5
+ "class",
6
+ "style",
7
+ "slot",
8
+ "placeholder",
9
+ "selectedValue",
10
+ "onChange",
11
+ "options",
12
+ "noClear",
13
+ "addOption",
14
+ "disabled",
15
+ ];
16
+
17
+ const stringifyProps = (props = {}) => {
18
+ return Object.entries(props)
19
+ .filter(([key]) => !blacklistedProps.includes(key))
20
+ .map(([key, value]) => `${key}=${value}`)
21
+ .join(" ");
22
+ };
23
+
24
+ export const createInitialState = () =>
25
+ Object.freeze({
26
+ selectedValue: null,
27
+ hasSelectedValue: false,
28
+ hoveredOptionId: null,
29
+ hoveredAddOption: false,
30
+ });
31
+
32
+ export const selectViewData = ({ state, props }) => {
33
+ const containerAttrString = stringifyProps(props);
34
+ const isDisabled = !!props.disabled;
35
+ const hasControlledValue = Object.prototype.hasOwnProperty.call(
36
+ props || {},
37
+ "selectedValue",
38
+ );
39
+ const currentValue = hasControlledValue ? props.selectedValue : state.selectedValue;
40
+ const hasCurrentValue = hasControlledValue ? true : !!state.hasSelectedValue;
41
+ const options = props.options || [];
42
+
43
+ const optionsWithSelection = options.map((option, index) => {
44
+ const isSelected = hasCurrentValue && deepEqual(option.value, currentValue);
45
+ const isHovered = state.hoveredOptionId === index;
46
+
47
+ return {
48
+ ...option,
49
+ isSelected,
50
+ bgc: isSelected ? "ac" : (isHovered && !isDisabled ? "mu" : ""),
51
+ textColor: isSelected ? "ac-fg" : "fg",
52
+ borderLeftWidth: index === 0 ? "none" : "xs",
53
+ cursor: isDisabled ? "not-allowed" : "pointer",
54
+ tabIndex: isDisabled ? -1 : 0,
55
+ };
56
+ });
57
+
58
+ return {
59
+ containerAttrString,
60
+ isDisabled,
61
+ options: optionsWithSelection,
62
+ selectedValue: currentValue,
63
+ hasSelectedValue: hasCurrentValue,
64
+ ariaLabel: props.placeholder || "Segmented control",
65
+ showAddOption: !isDisabled && !!props.addOption,
66
+ addOptionLabel: props.addOption?.label ? `+ ${props.addOption.label}` : "+ Add",
67
+ addOptionBgc: state.hoveredAddOption ? "mu" : "",
68
+ addOptionBorderLeftWidth: options.length === 0 ? "none" : "xs",
69
+ };
70
+ };
71
+
72
+ export const selectState = ({ state }) => {
73
+ return state;
74
+ };
75
+
76
+ export const selectSelectedValue = ({ state }) => {
77
+ return state.selectedValue;
78
+ };
79
+
80
+ export const selectHasSelectedValue = ({ state }) => {
81
+ return !!state.hasSelectedValue;
82
+ };
83
+
84
+ export const updateSelectedValue = ({ state }, payload = {}) => {
85
+ state.selectedValue = payload.value;
86
+ state.hasSelectedValue = true;
87
+ };
88
+
89
+ export const clearSelectedValue = ({ state }) => {
90
+ state.selectedValue = undefined;
91
+ state.hasSelectedValue = false;
92
+ };
93
+
94
+ export const setHoveredOption = ({ state }, payload = {}) => {
95
+ state.hoveredOptionId = payload.optionId;
96
+ };
97
+
98
+ export const clearHoveredOption = ({ state }) => {
99
+ state.hoveredOptionId = null;
100
+ };
101
+
102
+ export const setHoveredAddOption = ({ state }, payload = {}) => {
103
+ state.hoveredAddOption = !!payload.isHovered;
104
+ };
@@ -0,0 +1,29 @@
1
+ refs:
2
+ option*:
3
+ eventListeners:
4
+ click:
5
+ handler: handleOptionClick
6
+ keydown:
7
+ handler: handleOptionKeyDown
8
+ mouseenter:
9
+ handler: handleOptionMouseEnter
10
+ mouseleave:
11
+ handler: handleOptionMouseLeave
12
+ optionAdd:
13
+ eventListeners:
14
+ click:
15
+ handler: handleAddOptionClick
16
+ keydown:
17
+ handler: handleAddOptionKeyDown
18
+ mouseenter:
19
+ handler: handleAddOptionMouseEnter
20
+ mouseleave:
21
+ handler: handleAddOptionMouseLeave
22
+ template:
23
+ - 'rtgl-view d=h bw=xs bc=bo br=md bgc=bg overflow=hidden ${containerAttrString} role="group" aria-label=${ariaLabel}':
24
+ - $for option, i in options:
25
+ - 'rtgl-view#option${i} d=h av=c ah=c w=1fg ph=lg pv=md cur=${option.cursor} bgc=${option.bgc} bwl=${option.borderLeftWidth} bc=bo data-testid=${option.testId} role="button" tabindex=${option.tabIndex} aria-pressed=${option.isSelected} aria-disabled=${isDisabled}':
26
+ - rtgl-text s=sm c=${option.textColor} ta=c: ${option.label}
27
+ - $if showAddOption:
28
+ - 'rtgl-view#optionAdd d=h av=c ah=c w=1fg ph=lg pv=md cur=pointer bgc=${addOptionBgc} bwl=${addOptionBorderLeftWidth} bc=bo data-testid="segmented-control-add-option" role="button" tabindex=0':
29
+ - rtgl-text s=sm c=ac ta=c: ${addOptionLabel}