@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/dist/rettangoli-iife-ui.min.js +40 -40
- package/package.json +1 -1
- package/src/components/form/form.methods.js +9 -7
- package/src/components/form/form.store.js +2 -1
- package/src/components/form/form.view.yaml +2 -0
- package/src/components/segmentedControl/segmentedControl.handlers.js +172 -0
- package/src/components/segmentedControl/segmentedControl.schema.yaml +38 -0
- package/src/components/segmentedControl/segmentedControl.store.js +104 -0
- package/src/components/segmentedControl/segmentedControl.view.yaml +29 -0
package/package.json
CHANGED
|
@@ -58,7 +58,7 @@ const syncFieldValueAttribute = ({ ref, fieldType, value, forceRefresh = false }
|
|
|
58
58
|
ref.setAttribute("value", String(value));
|
|
59
59
|
};
|
|
60
60
|
|
|
61
|
-
const
|
|
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
|
|
92
|
+
const syncChoiceRefsFromValues = ({ root, values = {} }) => {
|
|
93
93
|
if (!root || typeof root.querySelectorAll !== "function") return;
|
|
94
94
|
|
|
95
|
-
const
|
|
96
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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}
|