@descope-ui/common 0.0.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.
- package/.eslintrc.json +18 -0
- package/README.md +7 -0
- package/package.json +36 -0
- package/project.json +7 -0
- package/src/baseClasses/baseClasses/createBaseClass.js +66 -0
- package/src/baseClasses/baseClasses/createBaseInputClass.js +14 -0
- package/src/baseClasses/baseClasses/createCssVarImageClass.js +55 -0
- package/src/baseClasses/index.js +3 -0
- package/src/componentsHelpers/index.js +95 -0
- package/src/componentsMixins/helpers/mixinsHelpers.js +10 -0
- package/src/componentsMixins/index.js +1 -0
- package/src/componentsMixins/mixins/activableMixin.js +14 -0
- package/src/componentsMixins/mixins/changeMixin.js +22 -0
- package/src/componentsMixins/mixins/componentNameValidationMixin.js +23 -0
- package/src/componentsMixins/mixins/componentsContextMixin.js +12 -0
- package/src/componentsMixins/mixins/createDynamicDataMixin.js +100 -0
- package/src/componentsMixins/mixins/createProxy.js +58 -0
- package/src/componentsMixins/mixins/createStyleMixin/helpers.js +106 -0
- package/src/componentsMixins/mixins/createStyleMixin/index.js +167 -0
- package/src/componentsMixins/mixins/draggableMixin.js +62 -0
- package/src/componentsMixins/mixins/externalInputHelpers.js +93 -0
- package/src/componentsMixins/mixins/externalInputMixin.js +170 -0
- package/src/componentsMixins/mixins/hoverableMixin.js +13 -0
- package/src/componentsMixins/mixins/index.js +14 -0
- package/src/componentsMixins/mixins/inputEventsDispatchingMixin.js +76 -0
- package/src/componentsMixins/mixins/inputValidationMixin.js +210 -0
- package/src/componentsMixins/mixins/lifecycleEventsMixin.js +23 -0
- package/src/componentsMixins/mixins/normalizeBooleanAttributesMixin.js +59 -0
- package/src/componentsMixins/mixins/portalMixin.js +112 -0
- package/src/componentsMixins/mixins/proxyInputMixin.js +242 -0
- package/src/constants.js +4 -0
- package/src/icons/errorMessageIconBase64.js +1 -0
- package/src/sbControls.js +302 -0
- package/src/sbHelpers.js +53 -0
- package/src/themeHelpers/colorsHelpers.js +94 -0
- package/src/themeHelpers/componentsThemeManager.js +45 -0
- package/src/themeHelpers/index.js +191 -0
- package/src/themeHelpers/resetHelpers.js +144 -0
- package/src/utils/index.js +68 -0
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { BASE_THEME_SECTION, CSS_SELECTOR_SPECIFIER_MULTIPLY } from '../../../constants';
|
|
2
|
+
import { kebabCaseJoin } from '../../../utils';
|
|
3
|
+
import { getCssVarName, observeAttributes } from '../../../componentsHelpers';
|
|
4
|
+
import { componentsThemeManager } from '../../../themeHelpers';
|
|
5
|
+
import { createStyle, createCssVarsList, createClassSelectorSpecifier } from './helpers';
|
|
6
|
+
|
|
7
|
+
const STYLE_OVERRIDE_ATTR_PREFIX = 'st';
|
|
8
|
+
|
|
9
|
+
export const createStyleMixin =
|
|
10
|
+
({ mappings = {}, componentNameOverride = '' }) =>
|
|
11
|
+
(superclass) => {
|
|
12
|
+
const componentName = componentNameOverride || superclass.componentName;
|
|
13
|
+
|
|
14
|
+
return class CustomStyleMixinClass extends superclass {
|
|
15
|
+
static get cssVarList() {
|
|
16
|
+
return {
|
|
17
|
+
...superclass.cssVarList,
|
|
18
|
+
...createCssVarsList(componentName, {
|
|
19
|
+
...mappings,
|
|
20
|
+
}),
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
#overrideStyleEle;
|
|
25
|
+
|
|
26
|
+
#themeStyleEle;
|
|
27
|
+
|
|
28
|
+
#disconnectThemeManager;
|
|
29
|
+
|
|
30
|
+
#componentNameSuffix;
|
|
31
|
+
|
|
32
|
+
#themeSection;
|
|
33
|
+
|
|
34
|
+
#rootElement;
|
|
35
|
+
|
|
36
|
+
#baseSelector;
|
|
37
|
+
|
|
38
|
+
#styleAttributes;
|
|
39
|
+
|
|
40
|
+
#getRootElement;
|
|
41
|
+
|
|
42
|
+
// we are using this mixin also for portalMixin
|
|
43
|
+
// so we should be able to inject styles to other DOM elements
|
|
44
|
+
// this is why we need to support these overrides
|
|
45
|
+
constructor({
|
|
46
|
+
getRootElement,
|
|
47
|
+
componentNameSuffix = '',
|
|
48
|
+
themeSection = BASE_THEME_SECTION,
|
|
49
|
+
baseSelector,
|
|
50
|
+
} = {}) {
|
|
51
|
+
super();
|
|
52
|
+
this.#componentNameSuffix = componentNameSuffix;
|
|
53
|
+
this.#themeSection = themeSection;
|
|
54
|
+
this.#baseSelector = baseSelector ?? this.baseSelector;
|
|
55
|
+
this.#getRootElement = getRootElement;
|
|
56
|
+
|
|
57
|
+
this.#styleAttributes = Object.keys(CustomStyleMixinClass.cssVarList).map((key) =>
|
|
58
|
+
kebabCaseJoin(STYLE_OVERRIDE_ATTR_PREFIX, componentNameSuffix, key)
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// eslint-disable-next-line class-methods-use-this
|
|
63
|
+
get #componentTheme() {
|
|
64
|
+
return componentsThemeManager.currentTheme?.[componentName] || '';
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
#onComponentThemeChange() {
|
|
68
|
+
this.#themeStyleEle.innerHTML = this.#componentTheme[this.#themeSection];
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
#createComponentTheme() {
|
|
72
|
+
this.#themeStyleEle = document.createElement('style');
|
|
73
|
+
this.#themeStyleEle.id = `style-mixin-theme__${componentName}`;
|
|
74
|
+
this.#rootElement.prepend(this.#themeStyleEle);
|
|
75
|
+
this.#disconnectThemeManager = componentsThemeManager.onCurrentThemeChange(
|
|
76
|
+
this.#onComponentThemeChange.bind(this)
|
|
77
|
+
);
|
|
78
|
+
this.#onComponentThemeChange();
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
#createOverridesStyle() {
|
|
82
|
+
if (this.#styleAttributes.length) {
|
|
83
|
+
this.#overrideStyleEle = document.createElement('style');
|
|
84
|
+
this.#overrideStyleEle.id = `style-mixin-overrides__${componentName}`;
|
|
85
|
+
|
|
86
|
+
const classSpecifier = createClassSelectorSpecifier(
|
|
87
|
+
componentName,
|
|
88
|
+
CSS_SELECTOR_SPECIFIER_MULTIPLY
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
this.#overrideStyleEle.innerText = `:host(${classSpecifier}) {}`;
|
|
92
|
+
this.#rootElement.append(this.#overrideStyleEle);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
#setAttrOverride(attrName, value) {
|
|
97
|
+
const style = this.#overrideStyleEle?.sheet?.cssRules[0].style;
|
|
98
|
+
if (!style) return;
|
|
99
|
+
|
|
100
|
+
const varName = getCssVarName(
|
|
101
|
+
componentName,
|
|
102
|
+
attrName.replace(new RegExp(`^${STYLE_OVERRIDE_ATTR_PREFIX}-`), '')
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
if (value) style?.setProperty(varName, value);
|
|
106
|
+
else {
|
|
107
|
+
style?.removeProperty(varName);
|
|
108
|
+
this.removeAttribute(attrName);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
#updateOverridesStyle(attrs = []) {
|
|
113
|
+
let shouldUpdate = false;
|
|
114
|
+
|
|
115
|
+
attrs.forEach((attr) => {
|
|
116
|
+
if (this.#styleAttributes.includes(attr)) {
|
|
117
|
+
this.#setAttrOverride(attr, this.getAttribute(attr));
|
|
118
|
+
shouldUpdate = true;
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
if (shouldUpdate) {
|
|
123
|
+
// we are rewriting the style back to the style tag
|
|
124
|
+
this.#overrideStyleEle.innerHTML = this.#overrideStyleEle?.sheet?.cssRules[0].cssText;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
#createMappingStyle() {
|
|
129
|
+
if (Object.keys(mappings).length) {
|
|
130
|
+
const themeStyle = document.createElement('style');
|
|
131
|
+
themeStyle.id = `style-mixin-mappings__${componentName}`;
|
|
132
|
+
themeStyle.innerHTML = createStyle(
|
|
133
|
+
kebabCaseJoin(componentName, this.#componentNameSuffix),
|
|
134
|
+
this.#baseSelector,
|
|
135
|
+
mappings
|
|
136
|
+
);
|
|
137
|
+
this.#rootElement.prepend(themeStyle);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
#addClassName(className) {
|
|
142
|
+
(this.#rootElement.classList || this.#rootElement.host.classList).add(className);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
async init() {
|
|
146
|
+
super.init?.();
|
|
147
|
+
if (this.shadowRoot.isConnected) {
|
|
148
|
+
this.#rootElement = (await this.#getRootElement?.(this)) || this.shadowRoot;
|
|
149
|
+
|
|
150
|
+
this.#addClassName(componentName);
|
|
151
|
+
|
|
152
|
+
this.#createMappingStyle();
|
|
153
|
+
this.#createComponentTheme();
|
|
154
|
+
this.#createOverridesStyle();
|
|
155
|
+
|
|
156
|
+
// this is instead attributeChangedCallback because we cannot use static methods in this case
|
|
157
|
+
observeAttributes(this, this.#updateOverridesStyle.bind(this), {});
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
disconnectedCallback() {
|
|
162
|
+
super.disconnectedCallback?.();
|
|
163
|
+
|
|
164
|
+
this.#disconnectThemeManager?.();
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
};
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
export const draggableMixin = (superclass) =>
|
|
2
|
+
class DraggableMixinClass extends superclass {
|
|
3
|
+
#styleEle = null;
|
|
4
|
+
|
|
5
|
+
static get observedAttributes() {
|
|
6
|
+
const superAttrs = superclass.observedAttributes || [];
|
|
7
|
+
return [...superAttrs, 'draggable'];
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
constructor() {
|
|
11
|
+
super();
|
|
12
|
+
|
|
13
|
+
this.#styleEle = document.createElement('style');
|
|
14
|
+
this.#styleEle.innerText = `* { cursor: inherit!important }`;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
#handleDraggableStyle(isDraggable) {
|
|
18
|
+
if (isDraggable) {
|
|
19
|
+
this.shadowRoot.appendChild(this.#styleEle);
|
|
20
|
+
} else {
|
|
21
|
+
this.#styleEle.remove();
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
get isDraggable() {
|
|
26
|
+
return this.hasAttribute('draggable') && this.getAttribute('draggable') !== 'false';
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
init() {
|
|
30
|
+
// because we are delegating the focus from the outer component,
|
|
31
|
+
// the D&D is not working well in the page editor
|
|
32
|
+
// in order to solve it we are making the inner component focusable on mouse down
|
|
33
|
+
// and removing it on complete
|
|
34
|
+
this.addEventListener('mousedown', (e) => {
|
|
35
|
+
if (this.isDraggable) {
|
|
36
|
+
const prevTabIndex = this.baseElement.getAttribute('tabindex');
|
|
37
|
+
this.baseElement.setAttribute('tabindex', '-1');
|
|
38
|
+
|
|
39
|
+
const onComplete = () => {
|
|
40
|
+
prevTabIndex
|
|
41
|
+
? this.baseElement.setAttribute('tabindex', prevTabIndex)
|
|
42
|
+
: this.baseElement.removeAttribute('tabindex');
|
|
43
|
+
|
|
44
|
+
e.target.removeEventListener('mouseup', onComplete);
|
|
45
|
+
e.target.removeEventListener('dragend', onComplete);
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
e.target.addEventListener('mouseup', onComplete, { once: true });
|
|
49
|
+
e.target.addEventListener('dragend', onComplete, { once: true });
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
super.init?.();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
attributeChangedCallback(attrName, oldValue, newValue) {
|
|
57
|
+
super.attributeChangedCallback?.(attrName, oldValue, newValue);
|
|
58
|
+
if (attrName === 'draggable') {
|
|
59
|
+
this.#handleDraggableStyle(newValue === 'true');
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
};
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
// since on load we can only sample the color of the placeholder,
|
|
2
|
+
// we need to temporarily populate the input in order to sample the value color
|
|
3
|
+
const getValueColor = (ele, computedStyle) => {
|
|
4
|
+
// to support setting dynamic values, we have to store the existing value
|
|
5
|
+
// and re-set it if we are returning from this hack
|
|
6
|
+
const origVal = ele.value;
|
|
7
|
+
|
|
8
|
+
// eslint-disable-next-line no-param-reassign
|
|
9
|
+
ele.value = '_';
|
|
10
|
+
|
|
11
|
+
const valueColor = computedStyle.getPropertyValue('color');
|
|
12
|
+
|
|
13
|
+
if (ele.value === '_') {
|
|
14
|
+
// eslint-disable-next-line no-param-reassign
|
|
15
|
+
ele.value = origVal;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return valueColor;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export const createExternalInputSlot = (slotName, targetSlotName) => {
|
|
22
|
+
const slotEle = document.createElement('slot');
|
|
23
|
+
|
|
24
|
+
slotEle.setAttribute('name', slotName);
|
|
25
|
+
slotEle.setAttribute('slot', targetSlotName);
|
|
26
|
+
|
|
27
|
+
return slotEle;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export const createExternalInputEle = (targetSlotName, type, autocompleteType, inputName) => {
|
|
31
|
+
const inputEle = document.createElement('input');
|
|
32
|
+
|
|
33
|
+
inputEle.setAttribute('slot', targetSlotName);
|
|
34
|
+
inputEle.setAttribute('type', type);
|
|
35
|
+
inputEle.setAttribute('name', inputName);
|
|
36
|
+
inputEle.setAttribute('data-hidden-input', 'true');
|
|
37
|
+
inputEle.setAttribute('autocomplete', autocompleteType);
|
|
38
|
+
|
|
39
|
+
return inputEle;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
// We apply the original input's style to the external-input.
|
|
43
|
+
// Eventually, the user should interact directly with the external input.
|
|
44
|
+
// We keep the original input
|
|
45
|
+
export const applyExternalInputStyles = (sourceInputEle, targetInputEle, labelType) => {
|
|
46
|
+
// We set a timeout here to avoid "Double Print" cases,
|
|
47
|
+
// caused by sampling the computed style before it's ready.
|
|
48
|
+
setTimeout(() => {
|
|
49
|
+
const computedStyle = getComputedStyle(sourceInputEle);
|
|
50
|
+
// Get minimal set of computed theme properties to set on external input
|
|
51
|
+
const height = computedStyle.getPropertyValue('height');
|
|
52
|
+
const paddingLeft = computedStyle.getPropertyValue('padding-left');
|
|
53
|
+
const paddingRight = computedStyle.getPropertyValue('padding-right');
|
|
54
|
+
const fontSize = computedStyle.getPropertyValue('font-size');
|
|
55
|
+
const fontFamily = computedStyle.getPropertyValue('font-family');
|
|
56
|
+
const letterSpacing = computedStyle.getPropertyValue('letter-spacing');
|
|
57
|
+
const caretColor = computedStyle.getPropertyValue('caret-color');
|
|
58
|
+
|
|
59
|
+
const valueColor = getValueColor(sourceInputEle, computedStyle);
|
|
60
|
+
|
|
61
|
+
const commonThemeStyles = [
|
|
62
|
+
['all', 'unset'],
|
|
63
|
+
['position', 'absolute'],
|
|
64
|
+
['background-color', 'transparent'],
|
|
65
|
+
['height', height],
|
|
66
|
+
['left', paddingLeft],
|
|
67
|
+
['right', paddingRight],
|
|
68
|
+
['font-size', fontSize],
|
|
69
|
+
['font-family', fontFamily],
|
|
70
|
+
['letter-spacing', letterSpacing],
|
|
71
|
+
['caret-color', caretColor], // this is for seeing caret when focusing on external input
|
|
72
|
+
['color', valueColor],
|
|
73
|
+
];
|
|
74
|
+
|
|
75
|
+
commonThemeStyles.forEach(([key, val]) =>
|
|
76
|
+
targetInputEle.style.setProperty(key, val, 'important')
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
// Handle floating label theme properties
|
|
80
|
+
if (labelType === 'floating') {
|
|
81
|
+
const marginBottom = computedStyle.getPropertyValue('margin-bottom');
|
|
82
|
+
targetInputEle.style.setProperty('margin-bottom', marginBottom, 'important');
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// sample and apply width only after original input is ready and fully rendered
|
|
86
|
+
const width = computedStyle.getPropertyValue('width');
|
|
87
|
+
targetInputEle.style.setProperty(
|
|
88
|
+
'width',
|
|
89
|
+
`calc(${width} - ${paddingLeft} - ${paddingRight}`,
|
|
90
|
+
'important'
|
|
91
|
+
);
|
|
92
|
+
}, 0);
|
|
93
|
+
};
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { syncAttrs } from '../../componentsHelpers';
|
|
2
|
+
import {
|
|
3
|
+
applyExternalInputStyles,
|
|
4
|
+
createExternalInputEle,
|
|
5
|
+
createExternalInputSlot,
|
|
6
|
+
} from './externalInputHelpers';
|
|
7
|
+
|
|
8
|
+
export const externalInputMixin =
|
|
9
|
+
({ inputType, inputName, autocompleteType, includeAttrs = [], noBlurDispatch = false }) =>
|
|
10
|
+
(superclass) =>
|
|
11
|
+
class ExternalInputMixinClass extends superclass {
|
|
12
|
+
#timers = [];
|
|
13
|
+
|
|
14
|
+
get isExternalInput() {
|
|
15
|
+
return this.getAttribute('external-input') === 'true';
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
createExternalInput() {
|
|
19
|
+
if (!this.isExternalInput || this.isReadOnly || this.isDisabled) {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// use original input element as reference
|
|
24
|
+
const origInput = this.baseElement.querySelector('input');
|
|
25
|
+
|
|
26
|
+
if (!origInput) {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// to avoid focus loop between external-input and origInput
|
|
31
|
+
// we set origInput's tabindex to -1
|
|
32
|
+
// otherwise, shift-tab will never leave the component focus
|
|
33
|
+
origInput.setAttribute('tabindex', '-1');
|
|
34
|
+
|
|
35
|
+
// create external slot
|
|
36
|
+
const externalInputSlot = createExternalInputSlot('external-input', 'suffix');
|
|
37
|
+
|
|
38
|
+
// append external slot to base element
|
|
39
|
+
this.baseElement.appendChild(externalInputSlot);
|
|
40
|
+
|
|
41
|
+
this.externalInput = createExternalInputEle(
|
|
42
|
+
'external-input',
|
|
43
|
+
inputType,
|
|
44
|
+
this.getAutocompleteType(),
|
|
45
|
+
inputName
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
// apply original input's styles to external input
|
|
49
|
+
setTimeout(() => {
|
|
50
|
+
applyExternalInputStyles(origInput, this.externalInput, this.getAttribute('label-type'));
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
// 1Password catches the internal input, so we forward the value to the external input
|
|
54
|
+
this.forwardInputValue(origInput, this.externalInput);
|
|
55
|
+
|
|
56
|
+
syncAttrs(origInput, this.externalInput, {
|
|
57
|
+
includeAttrs,
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
// We disable Vaadin's original `_setFocused` function, and use handleFocusEvents
|
|
61
|
+
// and handleBlurEvents functions
|
|
62
|
+
this.baseElement
|
|
63
|
+
.querySelector('input')
|
|
64
|
+
.addEventListener('focusout', (e) => e.stopImmediatePropagation(), true);
|
|
65
|
+
|
|
66
|
+
// In order to manage focus/blur events when moving between parts of the component
|
|
67
|
+
// we're managing the event dispatching by ourselves, with the following strategy:
|
|
68
|
+
// - If one of the component parts is focused - it means that the component is still focused - so we stop the blur events.
|
|
69
|
+
// - When none of the component parts is focused, we dispatch blur event.
|
|
70
|
+
this.handleFocusEvents();
|
|
71
|
+
this.handleBlurEvents();
|
|
72
|
+
|
|
73
|
+
// sync input value
|
|
74
|
+
this.handlelInputEvents(this.externalInput);
|
|
75
|
+
|
|
76
|
+
// append external input to component's DOM
|
|
77
|
+
this.appendChild(this.externalInput);
|
|
78
|
+
|
|
79
|
+
return this.externalInput;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
clearBlurTimers() {
|
|
83
|
+
this.#timers.forEach((timer) => clearTimeout(timer));
|
|
84
|
+
this.#timers.length = 0;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
dispatchBlur() {
|
|
88
|
+
return setTimeout(() => {
|
|
89
|
+
this.dispatchEvent(new Event('blur', { bubbles: true, composed: true }));
|
|
90
|
+
this.removeAttribute('focused');
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
handleFocusEvents() {
|
|
95
|
+
// on baseElement `focus` we forward the focus to the external input.
|
|
96
|
+
// also, in order to avoid any blur within the component, we clear the blur timers.
|
|
97
|
+
this.baseElement.addEventListener('focus', () => {
|
|
98
|
+
this.externalInput.focus();
|
|
99
|
+
this.clearBlurTimers();
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
// on `focus` of the external input, we manually set the `focused` attribute
|
|
103
|
+
this.externalInput.addEventListener('focus', () => {
|
|
104
|
+
this.clearBlurTimers();
|
|
105
|
+
setTimeout(() => this.baseElement.setAttribute('focused', 'true'));
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
handleBlurEvents() {
|
|
110
|
+
this.baseElement.addEventListener(
|
|
111
|
+
'blur',
|
|
112
|
+
(e) => {
|
|
113
|
+
e.stopImmediatePropagation();
|
|
114
|
+
// some components do not require this synthetic blur dispatch (e.g. Password)
|
|
115
|
+
// so we allow them to override this dispatch
|
|
116
|
+
if (noBlurDispatch) return;
|
|
117
|
+
this.#timers.push(this.dispatchBlur());
|
|
118
|
+
},
|
|
119
|
+
true
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
this.externalInput.addEventListener(
|
|
123
|
+
'blur',
|
|
124
|
+
(e) => {
|
|
125
|
+
e.stopImmediatePropagation();
|
|
126
|
+
this.#timers.push(this.dispatchBlur());
|
|
127
|
+
},
|
|
128
|
+
true
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
handlelInputEvents(externalInput) {
|
|
133
|
+
// sync value of insible input back to original input
|
|
134
|
+
externalInput.addEventListener('input', (e) => {
|
|
135
|
+
this.value = e.target.value;
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
// handle has-value attr
|
|
139
|
+
externalInput.addEventListener('input', (e) => {
|
|
140
|
+
if (!e.target.value) {
|
|
141
|
+
this.removeAttribute('has-value');
|
|
142
|
+
} else {
|
|
143
|
+
this.setAttribute('has-value', 'true');
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
getAutocompleteType() {
|
|
149
|
+
return this.getAttribute('autocomplete') || autocompleteType;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
forwardInputValue(source, target) {
|
|
153
|
+
// set internal sync events
|
|
154
|
+
const valueDescriptor = Object.getOwnPropertyDescriptor(
|
|
155
|
+
this.inputElement.constructor.prototype,
|
|
156
|
+
'value'
|
|
157
|
+
);
|
|
158
|
+
|
|
159
|
+
Object.defineProperty(source, 'value', {
|
|
160
|
+
...valueDescriptor,
|
|
161
|
+
|
|
162
|
+
set(v) {
|
|
163
|
+
valueDescriptor.set.call(this, v);
|
|
164
|
+
// eslint-disable-next-line no-param-reassign
|
|
165
|
+
target.value = v;
|
|
166
|
+
},
|
|
167
|
+
configurable: true,
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export const hoverableMixin = (superclass) =>
|
|
2
|
+
class HoverableMixinClass extends superclass {
|
|
3
|
+
init() {
|
|
4
|
+
super.init?.();
|
|
5
|
+
|
|
6
|
+
this.baseElement.addEventListener('mouseover', (e) => {
|
|
7
|
+
this.setAttribute('hover', 'true');
|
|
8
|
+
e.target.addEventListener('mouseleave', () => this.removeAttribute('hover'), {
|
|
9
|
+
once: true,
|
|
10
|
+
});
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export { createStyleMixin } from './createStyleMixin';
|
|
2
|
+
export { draggableMixin } from './draggableMixin';
|
|
3
|
+
export { createProxy } from './createProxy';
|
|
4
|
+
export { proxyInputMixin } from './proxyInputMixin';
|
|
5
|
+
export { componentNameValidationMixin } from './componentNameValidationMixin';
|
|
6
|
+
export { hoverableMixin } from './hoverableMixin';
|
|
7
|
+
export { inputValidationMixin } from './inputValidationMixin';
|
|
8
|
+
export { portalMixin } from './portalMixin';
|
|
9
|
+
export { changeMixin } from './changeMixin';
|
|
10
|
+
export { normalizeBooleanAttributesMixin } from './normalizeBooleanAttributesMixin';
|
|
11
|
+
export { lifecycleEventsMixin } from './lifecycleEventsMixin';
|
|
12
|
+
export { inputEventsDispatchingMixin } from './inputEventsDispatchingMixin';
|
|
13
|
+
export { externalInputMixin } from './externalInputMixin';
|
|
14
|
+
export {componentsContextMixin} from './componentsContextMixin'
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { createDispatchEvent } from '../helpers/mixinsHelpers';
|
|
2
|
+
|
|
3
|
+
export const inputEventsDispatchingMixin = (superclass) =>
|
|
4
|
+
class InputEventsDispatchingMixinClass extends superclass {
|
|
5
|
+
init() {
|
|
6
|
+
this.#blockNativeEvents();
|
|
7
|
+
|
|
8
|
+
super.init?.();
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// we want to block the native (trusted) events and control when these events are being dispatched
|
|
12
|
+
#blockNativeEvents() {
|
|
13
|
+
['blur', 'focus', 'focusin', 'focusout'].forEach((event) => {
|
|
14
|
+
this.addEventListener(event, (e) => {
|
|
15
|
+
e.isTrusted && e.target === this && e.stopImmediatePropagation();
|
|
16
|
+
});
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
handleFocusEventsDispatching(inputs) {
|
|
21
|
+
let timerId;
|
|
22
|
+
|
|
23
|
+
// in order to simulate blur & focusout on root input element
|
|
24
|
+
// we are checking if focus on one of the inner elements happened immediately after blur
|
|
25
|
+
// if not, the component is no longer focused and we should dispatch blur & focusout
|
|
26
|
+
inputs?.forEach((input) => {
|
|
27
|
+
input?.addEventListener('focusout', (e) => {
|
|
28
|
+
e.stopImmediatePropagation();
|
|
29
|
+
timerId = setTimeout(() => {
|
|
30
|
+
timerId = null;
|
|
31
|
+
|
|
32
|
+
createDispatchEvent.call(this, 'blur');
|
|
33
|
+
createDispatchEvent.call(this, 'focusout', { bubbles: true });
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
// in order to simulate focus & focusin on the root input element
|
|
38
|
+
// we are holding a timer id and clearing it on focusin
|
|
39
|
+
// if there is a timer id, it means that the root input is still focused
|
|
40
|
+
// otherwise, it was not focused before, and we should dispatch focus & focusin
|
|
41
|
+
const onFocus = (e) => {
|
|
42
|
+
e.stopImmediatePropagation();
|
|
43
|
+
clearTimeout(timerId);
|
|
44
|
+
if (!timerId) {
|
|
45
|
+
createDispatchEvent.call(this, 'focus');
|
|
46
|
+
createDispatchEvent.call(this, 'focusin', { bubbles: true });
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
// some components are not dispatching focusin but only focus
|
|
51
|
+
input?.addEventListener('focusin', onFocus);
|
|
52
|
+
input?.addEventListener('focus', onFocus);
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// we want to block the input events from propagating in case the value of the root input wasn't change
|
|
57
|
+
// this can happen if we are sanitizing characters on the internal inputs and do not want it to affect the root input element value
|
|
58
|
+
// in this case, on each input event, we are comparing the root input value to the previous one, and only if it does not match, we are allowing the input event to propagate
|
|
59
|
+
handleInputEventDispatching() {
|
|
60
|
+
let previousRootComponentValue = this.value;
|
|
61
|
+
|
|
62
|
+
// we are comparing the previous value to the new one,
|
|
63
|
+
// and if they have the same value, we are blocking the input event
|
|
64
|
+
this.addEventListener('input', (e) => {
|
|
65
|
+
// We don't want to block our own event that we fire from handleInputEventDispatching
|
|
66
|
+
if (this !== e.target) {
|
|
67
|
+
e.stopImmediatePropagation();
|
|
68
|
+
|
|
69
|
+
if (previousRootComponentValue !== this.value) {
|
|
70
|
+
previousRootComponentValue = this.value;
|
|
71
|
+
createDispatchEvent.call(this, 'input', { bubbles: true, composed: true });
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
};
|