@lion/form-core
Advanced tools
Comparing version 0.12.0 to 0.13.0
# Change Log | ||
## 0.13.0 | ||
### Minor Changes | ||
- 0ddd38c0: member descriptions for editors and api tables | ||
### Patch Changes | ||
- 0ddd38c0: support [focused-visible] when focusable node within matches :focus-visible | ||
## 0.12.0 | ||
@@ -4,0 +14,0 @@ |
@@ -7,4 +7,4 @@ # Systems >> Form >> Overview ||10 | ||
- Built in [validate](https://github.com/ing-bank/lion/blob/4bd5e4fc4dcadd802d0856c0e73e3af559f40537/docs/docs/systems/form/validate.md) for error/warning/info/success | ||
- Built in [validate](https://github.com/ing-bank/lion/blob/6589d4cc235d8b2ddfe0d695262dc4bcc910cdd1/docs/docs/systems/form/validate.md) for error/warning/info/success | ||
- Formatting of values | ||
- Accessible |
{ | ||
"name": "@lion/form-core", | ||
"version": "0.12.0", | ||
"version": "0.13.0", | ||
"description": "Form-core contains all essential building blocks for creating form fields and fieldsets", | ||
@@ -5,0 +5,0 @@ "license": "MIT", |
@@ -7,4 +7,4 @@ # Systems >> Form >> Overview ||10 | ||
- Built in [validate](https://github.com/ing-bank/lion/blob/4bd5e4fc4dcadd802d0856c0e73e3af559f40537/docs/docs/systems/form/validate.md) for error/warning/info/success | ||
- Built in [validate](https://github.com/ing-bank/lion/blob/6589d4cc235d8b2ddfe0d695262dc4bcc910cdd1/docs/docs/systems/form/validate.md) for error/warning/info/success | ||
- Formatting of values | ||
- Accessible |
@@ -28,11 +28,3 @@ import { dedupeMixin } from '@lion/core'; | ||
return { | ||
/** | ||
* @desc When false (default), modelValue and serializedValue will reflect the | ||
* currently selected choice (usually a string). When true, modelValue will and | ||
* serializedValue will be an array of strings. | ||
*/ | ||
multipleChoice: { | ||
type: Boolean, | ||
attribute: 'multiple-choice', | ||
}, | ||
multipleChoice: { type: Boolean, attribute: 'multiple-choice' }, | ||
}; | ||
@@ -136,7 +128,17 @@ } | ||
super(); | ||
/** | ||
* When false (default), modelValue and serializedValue will reflect the | ||
* currently selected choice (usually a string). When true, modelValue will and | ||
* serializedValue will be an array of strings. | ||
* @type {boolean} | ||
*/ | ||
this.multipleChoice = false; | ||
/** @type {'child'|'choice-group'|'fieldset'} | ||
/** | ||
* @type {'child'|'choice-group'|'fieldset'} | ||
* @configure FormControlMixin event propagation | ||
* @protected | ||
*/ | ||
this._repropagationRole = 'choice-group'; // configures event propagation logic of FormControlMixin | ||
this._repropagationRole = 'choice-group'; | ||
/** @private */ | ||
@@ -161,3 +163,3 @@ this.__isInitialModelValue = true; | ||
/** | ||
* @enhance FormRegistrarMixin | ||
* @enhance FormRegistrarMixin: we need one extra microtask to complete | ||
*/ | ||
@@ -181,3 +183,3 @@ _completeRegistration() { | ||
/** | ||
* @override from FormRegistrarMixin | ||
* @enhance FormRegistrarMixin | ||
* @param {FormControl} child | ||
@@ -360,6 +362,5 @@ * @param {number} indexToInsertAt | ||
/** | ||
* Don't repropagate unchecked single choice choiceInputs | ||
* @param {FormControlHost & ChoiceInputHost} target | ||
* @protected | ||
* @overridable | ||
* @configure FormControlMixin: don't repropagate unchecked single choice choiceInputs | ||
*/ | ||
@@ -366,0 +367,0 @@ _repropagationCondition(target) { |
@@ -28,38 +28,14 @@ /* eslint-disable class-methods-use-this */ | ||
return { | ||
/** | ||
* Boolean indicating whether or not this element is checked by the end user. | ||
*/ | ||
checked: { | ||
type: Boolean, | ||
reflect: true, | ||
}, | ||
/** | ||
* Boolean indicating whether or not this element is disabled. | ||
*/ | ||
disabled: { | ||
type: Boolean, | ||
reflect: true, | ||
}, | ||
/** | ||
* Whereas 'normal' `.modelValue`s usually store a complex/typed version | ||
* of a view value, choice inputs have a slightly different approach. | ||
* In order to remain their Single Source of Truth characteristic, choice inputs | ||
* store both the value and 'checkedness', in the format { value: 'x', checked: true } | ||
* Different from the platform, this also allows to serialize the 'non checkedness', | ||
* allowing to restore form state easily and inform the server about unchecked options. | ||
*/ | ||
modelValue: { | ||
type: Object, | ||
hasChanged, | ||
}, | ||
/** | ||
* The value property of the modelValue. It provides an easy interface for storing | ||
* (complex) values in the modelValue | ||
*/ | ||
choiceValue: { | ||
type: Object, | ||
}, | ||
checked: { type: Boolean, reflect: true }, | ||
disabled: { type: Boolean, reflect: true }, | ||
modelValue: { type: Object, hasChanged }, | ||
choiceValue: { type: Object }, | ||
}; | ||
} | ||
/** | ||
* The value that will be registered to the modelValue of the parent ChoiceGroup. Recommended | ||
* to be a string | ||
* @type {string|any} | ||
*/ | ||
get choiceValue() { | ||
@@ -127,4 +103,29 @@ return this.modelValue.value; | ||
super(); | ||
/** | ||
* Boolean indicating whether or not this element is checked by the end user. | ||
*/ | ||
// TODO: [v1] this can be solved when property effects are scheduled until firstUpdated | ||
// this.checked = false; | ||
/** | ||
* Whereas 'normal' `.modelValue`s usually store a complex/typed version | ||
* of a view value, choice inputs have a slightly different approach. | ||
* In order to remain their Single Source of Truth characteristic, choice inputs | ||
* store both the value and 'checkedness', in the format { value: 'x', checked: true } | ||
* Different from the platform, this also allows to serialize the 'non checkedness', | ||
* allowing to restore form state easily and inform the server about unchecked options. | ||
* @type {{value:string|any,checked:boolean}} | ||
*/ | ||
this.modelValue = { value: '', checked: false }; | ||
// TODO: maybe disabled is more a concern of FormControl/Field? | ||
/** | ||
* Boolean indicating whether or not this element is disabled. | ||
* @type {boolean} | ||
*/ | ||
this.disabled = false; | ||
/** | ||
* The value property of the modelValue. It provides an easy interface for storing | ||
* (complex) values in the modelValue | ||
*/ | ||
/** @protected */ | ||
@@ -131,0 +132,0 @@ this._preventDuplicateLabelClick = this._preventDuplicateLabelClick.bind(this); |
@@ -1,2 +0,7 @@ | ||
export type FocusMixin = typeof import("../types/FocusMixinTypes.js").FocusImplementation; | ||
export const FocusMixin: typeof import("../types/FocusMixinTypes.js").FocusImplementation; | ||
export type FocusMixin = typeof import("../types/FocusMixinTypes").FocusImplementation; | ||
/** | ||
* For browsers that not support the [spec](https://developer.mozilla.org/en-US/docs/Web/CSS/:focus-visible), | ||
* be sure to load the polyfill into your application https://github.com/WICG/focus-visible | ||
* (or go for progressive enhancement). | ||
*/ | ||
export const FocusMixin: typeof import("../types/FocusMixinTypes").FocusImplementation; |
import { dedupeMixin } from '@lion/core'; | ||
import { FormControlMixin } from './FormControlMixin.js'; | ||
const windowWithOptionalPolyfill = /** @type {Window & typeof globalThis & {applyFocusVisiblePolyfill?: function}} */ (window); | ||
const polyfilledNodes = new WeakMap(); | ||
/** | ||
* @param {Node} node | ||
*/ | ||
function applyFocusVisiblePolyfillWhenNeeded(node) { | ||
if (windowWithOptionalPolyfill.applyFocusVisiblePolyfill && !polyfilledNodes.has(node)) { | ||
windowWithOptionalPolyfill.applyFocusVisiblePolyfill(node); | ||
polyfilledNodes.set(node, undefined); | ||
} | ||
} | ||
/** | ||
* @typedef {import('../types/FocusMixinTypes').FocusMixin} FocusMixin | ||
@@ -9,10 +22,8 @@ * @type {FocusMixin} | ||
const FocusMixinImplementation = superclass => | ||
class FocusMixin extends FormControlMixin(superclass) { | ||
class FocusMixin extends superclass { | ||
/** @type {any} */ | ||
static get properties() { | ||
return { | ||
focused: { | ||
type: Boolean, | ||
reflect: true, | ||
}, | ||
focused: { type: Boolean, reflect: true }, | ||
focusedVisible: { type: Boolean, reflect: true, attribute: 'focused-visible' }, | ||
}; | ||
@@ -23,3 +34,17 @@ } | ||
super(); | ||
/** | ||
* Whether the focusable element within (`._focusableNode`) is focused. | ||
* Reflects to attribute '[focused]' as a styling hook | ||
* @type {boolean} | ||
*/ | ||
this.focused = false; | ||
/** | ||
* Whether the focusable element within (`._focusableNode`) matches ':focus-visible' | ||
* Reflects to attribute '[focused-visible]' as a styling hook | ||
* See: https://developer.mozilla.org/en-US/docs/Web/CSS/:focus-visible | ||
* @type {boolean} | ||
*/ | ||
this.focusedVisible = false; | ||
} | ||
@@ -37,17 +62,31 @@ | ||
/** | ||
* Calls `focus()` on focusable element within | ||
*/ | ||
focus() { | ||
const native = this._inputNode; | ||
if (native) { | ||
native.focus(); | ||
} | ||
this._focusableNode?.focus(); | ||
} | ||
/** | ||
* Calls `blur()` on focusable element within | ||
*/ | ||
blur() { | ||
const native = this._inputNode; | ||
if (native) { | ||
native.blur(); | ||
} | ||
this._focusableNode?.blur(); | ||
} | ||
/** | ||
* The focusable element: | ||
* could be an input, textarea, select, button or any other element with tabindex > -1 | ||
* @protected | ||
* @type {HTMLElement} | ||
*/ | ||
// @ts-ignore it's up to Subclassers to return the right element. This is needed for docs/types | ||
// eslint-disable-next-line class-methods-use-this, getter-return, no-empty-function | ||
get _focusableNode() { | ||
// TODO: [v1]: remove return of _inputNode (it's now here for backwards compatibility) | ||
// @ts-expect-error see above | ||
return /** @type {HTMLElement} */ (this._inputNode || document.createElement('input')); | ||
} | ||
/** | ||
* @private | ||
@@ -57,2 +96,12 @@ */ | ||
this.focused = true; | ||
if (typeof windowWithOptionalPolyfill.applyFocusVisiblePolyfill === 'function') { | ||
this.focusedVisible = this._focusableNode.hasAttribute('data-focus-visible-added'); | ||
} else | ||
try { | ||
// Safari throws when matches is called | ||
this.focusedVisible = this._focusableNode.matches(':focus-visible'); | ||
} catch (_) { | ||
this.focusedVisible = false; | ||
} | ||
} | ||
@@ -65,2 +114,3 @@ | ||
this.focused = false; | ||
this.focusedVisible = false; | ||
} | ||
@@ -72,2 +122,4 @@ | ||
__registerEventsForFocusMixin() { | ||
applyFocusVisiblePolyfillWhenNeeded(this.getRootNode()); | ||
/** | ||
@@ -81,3 +133,3 @@ * focus | ||
}; | ||
this._inputNode.addEventListener('focus', this.__redispatchFocus); | ||
this._focusableNode.addEventListener('focus', this.__redispatchFocus); | ||
@@ -92,3 +144,3 @@ /** | ||
}; | ||
this._inputNode.addEventListener('blur', this.__redispatchBlur); | ||
this._focusableNode.addEventListener('blur', this.__redispatchBlur); | ||
@@ -104,3 +156,3 @@ /** | ||
}; | ||
this._inputNode.addEventListener('focusin', this.__redispatchFocusin); | ||
this._focusableNode.addEventListener('focusin', this.__redispatchFocusin); | ||
@@ -116,3 +168,3 @@ /** | ||
}; | ||
this._inputNode.addEventListener('focusout', this.__redispatchFocusout); | ||
this._focusableNode.addEventListener('focusout', this.__redispatchFocusout); | ||
} | ||
@@ -124,15 +176,15 @@ | ||
__teardownEventsForFocusMixin() { | ||
this._inputNode.removeEventListener( | ||
this._focusableNode.removeEventListener( | ||
'focus', | ||
/** @type {EventListenerOrEventListenerObject} */ (this.__redispatchFocus), | ||
); | ||
this._inputNode.removeEventListener( | ||
this._focusableNode.removeEventListener( | ||
'blur', | ||
/** @type {EventListenerOrEventListenerObject} */ (this.__redispatchBlur), | ||
); | ||
this._inputNode.removeEventListener( | ||
this._focusableNode.removeEventListener( | ||
'focusin', | ||
/** @type {EventListenerOrEventListenerObject} */ (this.__redispatchFocusin), | ||
); | ||
this._inputNode.removeEventListener( | ||
this._focusableNode.removeEventListener( | ||
'focusout', | ||
@@ -144,2 +196,7 @@ /** @type {EventListenerOrEventListenerObject} */ (this.__redispatchFocusout), | ||
/** | ||
* For browsers that not support the [spec](https://developer.mozilla.org/en-US/docs/Web/CSS/:focus-visible), | ||
* be sure to load the polyfill into your application https://github.com/WICG/focus-visible | ||
* (or go for progressive enhancement). | ||
*/ | ||
export const FocusMixin = dedupeMixin(FocusMixinImplementation); |
@@ -37,46 +37,14 @@ import { dedupeMixin, html, SlotMixin, DisabledMixin } from '@lion/core'; | ||
return { | ||
/** | ||
* Interaction state that can be used to compute the visibility of | ||
* feedback messages | ||
*/ | ||
submitted: { | ||
type: Boolean, | ||
reflect: true, | ||
}, | ||
/** | ||
* Interaction state that will be active when any of the children | ||
* is focused. | ||
*/ | ||
focused: { | ||
type: Boolean, | ||
reflect: true, | ||
}, | ||
/** | ||
* Interaction state that will be active when any of the children | ||
* is dirty (see InteractionStateMixin for more details.) | ||
*/ | ||
dirty: { | ||
type: Boolean, | ||
reflect: true, | ||
}, | ||
/** | ||
* Interaction state that will be active when the group as a whole is | ||
* blurred | ||
*/ | ||
touched: { | ||
type: Boolean, | ||
reflect: true, | ||
}, | ||
/** | ||
* Interaction state that will be active when all of the children | ||
* are prefilled (see InteractionStateMixin for more details.) | ||
*/ | ||
prefilled: { | ||
type: Boolean, | ||
reflect: true, | ||
}, | ||
submitted: { type: Boolean, reflect: true }, | ||
focused: { type: Boolean, reflect: true }, | ||
dirty: { type: Boolean, reflect: true }, | ||
touched: { type: Boolean, reflect: true }, | ||
prefilled: { type: Boolean, reflect: true }, | ||
}; | ||
} | ||
/** @protected */ | ||
/** | ||
* The host element with role group (or radigroup or form) containing neccessary aria attributes | ||
* @protected | ||
*/ | ||
get _inputNode() { | ||
@@ -86,2 +54,5 @@ return this; | ||
/** | ||
* Object keyed by formElements names, containing formElements' modelValues | ||
*/ | ||
get modelValue() { | ||
@@ -102,2 +73,5 @@ return this._getFromAllFormElements('modelValue'); | ||
/** | ||
* Object keyed by formElements names, containing formElements' serializedValues | ||
*/ | ||
get serializedValue() { | ||
@@ -118,2 +92,5 @@ return this._getFromAllFormElements('serializedValue'); | ||
/** | ||
* Object keyed by formElements names, containing formElements' formattedValues | ||
*/ | ||
get formattedValue() { | ||
@@ -127,2 +104,5 @@ return this._getFromAllFormElements('formattedValue'); | ||
/** | ||
* True when all of the children are prefilled (see InteractionStateMixin for more details.) | ||
*/ | ||
get prefilled() { | ||
@@ -134,14 +114,38 @@ return this._everyFormElementHas('prefilled'); | ||
super(); | ||
// ._inputNode = this, which always requires a value prop | ||
// ._inputNode === this, which always requires a value prop | ||
this.value = ''; | ||
/** | ||
* Disables all formElements in group | ||
*/ | ||
this.disabled = false; | ||
/** | ||
* True when parent form is submitted | ||
*/ | ||
this.submitted = false; | ||
/** | ||
* True when any of the children is dirty (see InteractionStateMixin for more details.) | ||
*/ | ||
this.dirty = false; | ||
/** | ||
* True when the group as a whole is blurred (see InteractionStateMixin for more details.) | ||
*/ | ||
this.touched = false; | ||
/** | ||
* True when any of the children is focused. | ||
*/ | ||
this.focused = false; | ||
/** @private */ | ||
this.__addedSubValidators = false; | ||
/** @private */ | ||
this.__isInitialModelValue = true; | ||
/** @private */ | ||
this.__isInitialSerializedValue = true; | ||
/** @private */ | ||
this._checkForOutsideClick = this._checkForOutsideClick.bind(this); | ||
@@ -264,3 +268,3 @@ | ||
/** | ||
* @desc Handles interaction state 'submitted'. | ||
* Handles interaction state 'submitted'. | ||
* This allows children to enable visibility of validation feedback | ||
@@ -279,2 +283,5 @@ */ | ||
/** | ||
* Resets to initial/prefilled values and interaction states of all FormControls in group, | ||
*/ | ||
resetGroup() { | ||
@@ -292,2 +299,5 @@ this.formElements.forEach(child => { | ||
/** | ||
* Clears all values and resets all interaction states of all FormControls in group, | ||
*/ | ||
clearGroup() { | ||
@@ -305,2 +315,5 @@ this.formElements.forEach(child => { | ||
/** | ||
* Resets all interaction states for all formElements | ||
*/ | ||
resetInteractionState() { | ||
@@ -318,3 +331,5 @@ this.submitted = false; | ||
/** | ||
* Gets a keyed be name object for requested property (like modelValue/serializedValue) | ||
* @param {string} property | ||
* @returns {{[name:string]: any}} | ||
*/ | ||
@@ -340,2 +355,3 @@ _getFromAllFormElements(property, filterFn = (/** @type {FormControl} */ el) => !el.disabled) { | ||
/** | ||
* Sets the same value for requested property in all formElements | ||
* @param {string | number} property | ||
@@ -351,2 +367,3 @@ * @param {any} value | ||
/** | ||
* Allows to set formElements values via a keyed object structure | ||
* @param {string} property | ||
@@ -376,2 +393,3 @@ * @param {{ [x: string]: any; }} values | ||
/** | ||
* Returns true when one of the formElements has requested | ||
* @param {string} property | ||
@@ -406,2 +424,3 @@ */ | ||
/** | ||
* Returns true when all of the formElements have requested property | ||
* @param {string} property | ||
@@ -418,7 +437,9 @@ */ | ||
// TODO: the same functionality has been implemented with model-value-changed event, which | ||
// covers the same and works with FormRegistrarPortalMixin | ||
/** | ||
* Gets triggered by event 'validate-performed' which enabled us to handle 2 different situations | ||
* - react on modelValue change, which says something about the validity as a whole | ||
* (at least two checkboxes for instance) and nothing about the children's values | ||
* - children validity states have changed, so fieldset needs to update itself based on that | ||
* - react on modelValue change, which says something about the validity as a whole | ||
* (at least two checkboxes for instance) and nothing about the children's values | ||
* - children validity states have changed, so fieldset needs to update itself based on that | ||
* @param {Event} ev | ||
@@ -460,2 +481,3 @@ */ | ||
* @example | ||
* ```html | ||
* <lion-fieldset name="address"> | ||
@@ -468,2 +490,3 @@ * <lion-input name="street" label="Street" .modelValue="${'Park Avenue'}"></lion-input> | ||
* </lion-fieldset> | ||
* ``` | ||
*/ | ||
@@ -508,4 +531,3 @@ __storeAllDescriptionElementsInParentChain() { | ||
/** | ||
* @override of FormRegistrarMixin. | ||
* @desc Connects ValidateMixin and DisabledMixin | ||
* @enhance FormRegistrarMixin: connects ValidateMixin and DisabledMixin. | ||
* On top of this, error messages of children are linked to their parents | ||
@@ -542,4 +564,3 @@ * @param {FormControl & {serializedValue:string|object}} child | ||
/** | ||
* Gathers initial model values of all children. Used | ||
* when resetGroup() is called. | ||
* Gathers initial model values of all children. Used when resetGroup() is called. | ||
*/ | ||
@@ -551,3 +572,3 @@ get _initialModelValue() { | ||
/** | ||
* @override of FormRegistrarMixin. Connects ValidateMixin | ||
* @override FormRegistrarMixin; Connects ValidateMixin | ||
* @param {FormRegisteringHost & FormControl} el | ||
@@ -554,0 +575,0 @@ */ |
@@ -65,42 +65,4 @@ /* eslint-disable class-methods-use-this */ | ||
return { | ||
/** | ||
* The view value is the result of the formatter function (when available). | ||
* The result will be stored in the native _inputNode (usually an input[type=text]). | ||
* | ||
* Examples: | ||
* - For a date input, this would be '20/01/1999' (dependent on locale). | ||
* - For a number input, this could be '1,234.56' (a String representation of modelValue | ||
* 1234.56) | ||
* | ||
* @private | ||
*/ | ||
formattedValue: { attribute: false }, | ||
/** | ||
* The serialized version of the model value. | ||
* This value exists for maximal compatibility with the platform API. | ||
* The serialized value can be an interface in context where data binding is not | ||
* supported and a serialized string needs to be set. | ||
* | ||
* Examples: | ||
* - For a date input, this would be the iso format of a date, e.g. '1999-01-20'. | ||
* - For a number input this would be the String representation of a float ('1234.56' | ||
* instead of 1234.56) | ||
* | ||
* When no parser is available, the value is usually the same as the formattedValue | ||
* (being _inputNode.value) | ||
* | ||
*/ | ||
serializedValue: { attribute: false }, | ||
/** | ||
* Event that will trigger formatting (more precise, visual update of the view, so the | ||
* user sees the formatted value) | ||
* Default: 'change' | ||
*/ | ||
formatOn: { attribute: false }, | ||
/** | ||
* Configuration object that will be available inside the formatter function | ||
*/ | ||
formatOptions: { attribute: false }, | ||
@@ -128,2 +90,5 @@ }; | ||
/** | ||
* The view value. Will be delegated to `._inputNode.value` | ||
*/ | ||
get value() { | ||
@@ -133,3 +98,2 @@ return (this._inputNode && this._inputNode.value) || this.__value || ''; | ||
// We don't delegate, because we want to preserve caret position via _setValueAndPreserveCaret | ||
/** @param {string} value */ | ||
@@ -148,2 +112,11 @@ set value(value) { | ||
/** | ||
* Preprocesses the viewValue before it's parsed to a modelValue. Can be used to filter | ||
* invalid input amongst others. | ||
* @example | ||
* ```js | ||
* preprocessor(viewValue) { | ||
* // only use digits | ||
* return viewValue.replace(/\D/g, ''); | ||
* } | ||
* ``` | ||
* @param {string} v - the raw value from the <input> after keyUp/Down event | ||
@@ -157,5 +130,5 @@ * @returns {string} preprocessedValue: the result of preprocessing for invalid input | ||
/** | ||
* Converts formattedValue to modelValue | ||
* Converts viewValue to modelValue | ||
* For instance, a localized date to a Date Object | ||
* @param {string} v - formattedValue: the formatted value inside <input> | ||
* @param {string} v - viewValue: the formatted value inside <input> | ||
* @param {FormatOptions} opts | ||
@@ -195,3 +168,3 @@ * @returns {*} modelValue | ||
/** | ||
* Converts `LionField.value` to `.modelValue` | ||
* Converts `.serializedValue` to `.modelValue` | ||
* For instance, an iso formatted date string to a Date object | ||
@@ -232,7 +205,5 @@ * @param {?} v - modelValue: can be an Object, Number, String depending on the | ||
if (source !== 'formatted') { | ||
/** @type {string} */ | ||
this.formattedValue = this._callFormatter(); | ||
} | ||
if (source !== 'serialized') { | ||
/** @type {string} */ | ||
this.serializedValue = this.serializer(this.modelValue); | ||
@@ -320,3 +291,2 @@ } | ||
/** | ||
* Observer Handlers | ||
* @param {{ modelValue: unknown; }[]} args | ||
@@ -331,5 +301,5 @@ * @protected | ||
/** | ||
* @param {{ modelValue: unknown; }[]} args | ||
* This is wrapped in a distinct method, so that parents can control when the changed event | ||
* is fired. For objects, a deep comparison might be needed. | ||
* @param {{ modelValue: unknown; }[]} args | ||
* @protected | ||
@@ -341,2 +311,3 @@ */ | ||
this.dispatchEvent( | ||
/** @privateEvent model-value-changed: FormControl redispatches it as public event */ | ||
new CustomEvent('model-value-changed', { | ||
@@ -409,8 +380,5 @@ bubbles: true, | ||
_proxyInputEvent() { | ||
this.dispatchEvent( | ||
new CustomEvent('user-input-changed', { | ||
bubbles: true, | ||
composed: true, | ||
}), | ||
); | ||
// TODO: [v1] remove composed (and bubbles as well if possible) | ||
/** @protectedEvent user-input-changed meant for usage by Subclassers only */ | ||
this.dispatchEvent(new Event('user-input-changed', { bubbles: true })); | ||
} | ||
@@ -442,5 +410,49 @@ | ||
super(); | ||
// TODO: [v1] delete; use 'change' event directly within this file | ||
/** | ||
* Event that will trigger formatting (more precise, visual update of the view, so the | ||
* user sees the formatted value) | ||
* Default: 'change' | ||
* @deprecated use _reflectBackOn() | ||
* @protected | ||
*/ | ||
this.formatOn = 'change'; | ||
/** | ||
* Configuration object that will be available inside the formatter function | ||
*/ | ||
this.formatOptions = /** @type {FormatOptions} */ ({}); | ||
/** | ||
* The view value is the result of the formatter function (when available). | ||
* The result will be stored in the native _inputNode (usually an input[type=text]). | ||
* | ||
* Examples: | ||
* - For a date input, this would be '20/01/1999' (dependent on locale). | ||
* - For a number input, this could be '1,234.56' (a String representation of modelValue | ||
* 1234.56) | ||
* @type {string|undefined} | ||
* @readOnly | ||
*/ | ||
this.formattedValue = undefined; | ||
/** | ||
* The serialized version of the model value. | ||
* This value exists for maximal compatibility with the platform API. | ||
* The serialized value can be an interface in context where data binding is not | ||
* supported and a serialized string needs to be set. | ||
* | ||
* Examples: | ||
* - For a date input, this would be the iso format of a date, e.g. '1999-01-20'. | ||
* - For a number input this would be the String representation of a float ('1234.56' | ||
* instead of 1234.56) | ||
* | ||
* When no parser is available, the value is usually the same as the formattedValue | ||
* (being _inputNode.value) | ||
* @type {string|undefined} | ||
*/ | ||
this.serializedValue = undefined; | ||
/** | ||
* Whether the user is pasting content. Allows Subclassers to do this in their subclass: | ||
@@ -454,5 +466,15 @@ * @example | ||
* @protected | ||
* @type {boolean} | ||
*/ | ||
this._isPasting = false; | ||
/** | ||
* Flag that will be set when user interaction takes place (for instance after an 'input' | ||
* event). Will be added as meta info to the `model-value-changed` event. Depending on | ||
* whether a user is interacting, formatting logic will be handled differently. | ||
* @protected | ||
* @type {boolean} | ||
*/ | ||
this._isHandlingUserInput = false; | ||
/** | ||
* @private | ||
@@ -467,2 +489,16 @@ * @type {string} | ||
this.addEventListener('paste', this.__onPaste); | ||
/** | ||
* @protected | ||
*/ | ||
this._reflectBackFormattedValueToUser = this._reflectBackFormattedValueToUser.bind(this); | ||
/** | ||
* @private | ||
*/ | ||
this._reflectBackFormattedValueDebounced = () => { | ||
// Make sure this is fired after the change event of _inputNode, so that formattedValue | ||
// is guaranteed to be calculated | ||
setTimeout(this._reflectBackFormattedValueToUser); | ||
}; | ||
} | ||
@@ -481,10 +517,3 @@ | ||
super.connectedCallback(); | ||
this._reflectBackFormattedValueToUser = this._reflectBackFormattedValueToUser.bind(this); | ||
this._reflectBackFormattedValueDebounced = () => { | ||
// Make sure this is fired after the change event of _inputNode, so that formattedValue | ||
// is guaranteed to be calculated | ||
setTimeout(this._reflectBackFormattedValueToUser); | ||
}; | ||
// Connect the value found in <input> to the formatting/parsing/serializing loop as a | ||
@@ -491,0 +520,0 @@ // fallback mechanism. Assume the user uses the value property of the |
@@ -43,73 +43,10 @@ import { css, dedupeMixin, html, nothing, SlotMixin, DisabledMixin } from '@lion/core'; | ||
return { | ||
/** | ||
* The name the element will be registered on to the .formElements collection | ||
* of the parent. | ||
*/ | ||
name: { | ||
type: String, | ||
reflect: true, | ||
}, | ||
/** | ||
* A Boolean attribute which, if present, indicates that the user should not be able to edit | ||
* the value of the input. The difference between disabled and readonly is that read-only | ||
* controls can still function, whereas disabled controls generally do not function as | ||
* controls until they are enabled. | ||
* | ||
* (From: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#attr-readonly) | ||
*/ | ||
readOnly: { | ||
type: Boolean, | ||
attribute: 'readonly', | ||
reflect: true, | ||
}, | ||
/** | ||
* The label text for the input node. | ||
* When no light dom defined via [slot=label], this value will be used | ||
*/ | ||
name: { type: String, reflect: true }, | ||
readOnly: { type: Boolean, attribute: 'readonly', reflect: true }, | ||
label: String, // FIXME: { attribute: false } breaks a bunch of tests, but shouldn't... | ||
/** | ||
* The helpt text for the input node. | ||
* When no light dom defined via [slot=help-text], this value will be used | ||
*/ | ||
helpText: { | ||
type: String, | ||
attribute: 'help-text', | ||
}, | ||
/** | ||
* The model value is the result of the parser function(when available). | ||
* It should be considered as the internal value used for validation and reasoning/logic. | ||
* The model value is 'ready for consumption' by the outside world (think of a Date | ||
* object or a float). The modelValue can(and is recommended to) be used as both input | ||
* value and output value of the `LionField`. | ||
* | ||
* Examples: | ||
* - For a date input: a String '20/01/1999' will be converted to new Date('1999/01/20') | ||
* - For a number input: a formatted String '1.234,56' will be converted to a Number: | ||
* 1234.56 | ||
*/ | ||
helpText: { type: String, attribute: 'help-text' }, | ||
modelValue: { attribute: false }, | ||
/** | ||
* Contains all elements that should end up in aria-labelledby of `._inputNode` | ||
*/ | ||
_ariaLabelledNodes: { attribute: false }, | ||
/** | ||
* Contains all elements that should end up in aria-describedby of `._inputNode` | ||
*/ | ||
_ariaDescribedNodes: { attribute: false }, | ||
/** | ||
* Based on the role, details of handling model-value-changed repropagation differ. | ||
*/ | ||
_repropagationRole: { attribute: false }, | ||
/** | ||
* By default, a field with _repropagationRole 'choice-group' will act as an | ||
* 'endpoint'. This means it will be considered as an individual field: for | ||
* a select, individual options will not be part of the formPath. They | ||
* will. | ||
* Similarly, components that (a11y wise) need to be fieldsets, but 'interaction wise' | ||
* (from Application Developer perspective) need to be more like fields | ||
* (think of an amount-input with a currency select box next to it), can set this | ||
* to true to hide private internals in the formPath. | ||
*/ | ||
_isRepropagationEndpoint: { attribute: false }, | ||
@@ -120,3 +57,5 @@ }; | ||
/** | ||
* @return {string} | ||
* The label text for the input node. | ||
* When no light dom defined via [slot=label], this value will be used. | ||
* @type {string} | ||
*/ | ||
@@ -138,3 +77,5 @@ get label() { | ||
/** | ||
* @return {string} | ||
* The helpt text for the input node. | ||
* When no light dom defined via [slot=help-text], this value will be used | ||
* @type {string} | ||
*/ | ||
@@ -156,3 +97,4 @@ get helpText() { | ||
/** | ||
* @return {string} | ||
* Will be used in validation messages to refer to the current field | ||
* @type {string} | ||
*/ | ||
@@ -172,3 +114,3 @@ get fieldName() { | ||
/** | ||
* @type {SlotsMap} | ||
* @configure SlotMixin | ||
*/ | ||
@@ -191,3 +133,7 @@ get slots() { | ||
/** @protected */ | ||
/** | ||
* The interactive (form) element. Can be a native element like input/textarea/select or | ||
* an element with tabindex > -1 | ||
* @protected | ||
*/ | ||
get _inputNode() { | ||
@@ -197,2 +143,6 @@ return /** @type {HTMLElementWithValue} */ (this.__getDirectSlotChild('input')); | ||
/** | ||
* Element where label will be rendered to | ||
* @protected | ||
*/ | ||
get _labelNode() { | ||
@@ -202,2 +152,6 @@ return /** @type {HTMLElement} */ (this.__getDirectSlotChild('label')); | ||
/** | ||
* Element where help text will be rendered to | ||
* @protected | ||
*/ | ||
get _helpTextNode() { | ||
@@ -208,2 +162,3 @@ return /** @type {HTMLElement} */ (this.__getDirectSlotChild('help-text')); | ||
/** | ||
* Element where validation feedback will be rendered to | ||
* @protected | ||
@@ -217,15 +172,87 @@ */ | ||
super(); | ||
/** @type {string} */ | ||
/** | ||
* The name the element will be registered with to the .formElements collection | ||
* of the parent. Also, it serves as the key of key/value pairs in | ||
* modelValue/serializedValue objects | ||
* @type {string} | ||
*/ | ||
this.name = ''; | ||
/** @type {string} */ | ||
/** | ||
* A Boolean attribute which, if present, indicates that the user should not be able to edit | ||
* the value of the input. The difference between disabled and readonly is that read-only | ||
* controls can still function, whereas disabled controls generally do not function as | ||
* controls until they are enabled. | ||
* (From: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#attr-readonly) | ||
* @type {boolean} | ||
*/ | ||
this.readOnly = false; | ||
/** | ||
* The label text for the input node. | ||
* When no value is defined, textContent of [slot=label] will be used | ||
* @type {string} | ||
*/ | ||
this.label = ''; | ||
/** | ||
* The helpt text for the input node. | ||
* When no value is defined, textContent of [slot=help-text] will be used | ||
* @type {string} | ||
*/ | ||
this.helpText = ''; | ||
/** | ||
* The model value is the result of the parser function(when available). | ||
* It should be considered as the internal value used for validation and reasoning/logic. | ||
* The model value is 'ready for consumption' by the outside world (think of a Date | ||
* object or a float). The modelValue can(and is recommended to) be used as both input | ||
* value and output value of the `LionField`. | ||
* | ||
* Examples: | ||
* - For a date input: a String '20/01/1999' will be converted to new Date('1999/01/20') | ||
* - For a number input: a formatted String '1.234,56' will be converted to a Number: | ||
* 1234.56 | ||
*/ | ||
// TODO: we can probably set this up properly once propert effects run from firstUpdated | ||
// this.modelValue = undefined; | ||
/** | ||
* Unique id that can be used in all light dom | ||
* @type {string} | ||
* @protected | ||
*/ | ||
this._inputId = uuid(this.localName); | ||
/** @type {HTMLElement[]} */ | ||
/** | ||
* Contains all elements that should end up in aria-labelledby of `._inputNode` | ||
* @type {HTMLElement[]} | ||
*/ | ||
this._ariaLabelledNodes = []; | ||
/** @type {HTMLElement[]} */ | ||
/** | ||
* Contains all elements that should end up in aria-describedby of `._inputNode` | ||
* @type {HTMLElement[]} | ||
*/ | ||
this._ariaDescribedNodes = []; | ||
/** @type {'child'|'choice-group'|'fieldset'} */ | ||
/** | ||
* Based on the role, details of handling model-value-changed repropagation differ. | ||
* @type {'child'|'choice-group'|'fieldset'} | ||
*/ | ||
this._repropagationRole = 'child'; | ||
/** | ||
* By default, a field with _repropagationRole 'choice-group' will act as an | ||
* 'endpoint'. This means it will be considered as an individual field: for | ||
* a select, individual options will not be part of the formPath. They | ||
* will. | ||
* Similarly, components that (a11y wise) need to be fieldsets, but 'interaction wise' | ||
* (from Application Developer perspective) need to be more like fields | ||
* (think of an amount-input with a currency select box next to it), can set this | ||
* to true to hide private internals in the formPath. | ||
* @type {boolean} | ||
*/ | ||
this._isRepropagationEndpoint = false; | ||
/** @private */ | ||
this.__label = ''; | ||
this.addEventListener( | ||
@@ -287,2 +314,3 @@ 'model-value-changed', | ||
this.dispatchEvent( | ||
/** @privateEvent */ | ||
new CustomEvent('form-element-name-changed', { | ||
@@ -565,2 +593,3 @@ detail: { oldName: changedProperties.get('name'), newName: this.name }, | ||
/** | ||
* Used for Required validation and computation of interaction states | ||
* @param {any} modelValue | ||
@@ -728,3 +757,3 @@ * @return {boolean} | ||
/** | ||
* Meant for Application Developers wanting to add to aria-labelledby attribute. | ||
* Allows to add extra element references to aria-labelledby attribute. | ||
* @param {HTMLElement} element | ||
@@ -745,3 +774,3 @@ * @param {{idPrefix?:string; reorder?: boolean}} customConfig | ||
/** | ||
* Meant for Application Developers wanting to delete from aria-labelledby attribute. | ||
* Allows to remove element references from aria-labelledby attribute. | ||
* @param {HTMLElement} element | ||
@@ -761,3 +790,3 @@ */ | ||
/** | ||
* Meant for Application Developers wanting to add to aria-describedby attribute. | ||
* Allows to add element references to aria-describedby attribute. | ||
* @param {HTMLElement} element | ||
@@ -778,3 +807,3 @@ * @param {{idPrefix?:string; reorder?: boolean}} customConfig | ||
/** | ||
* Meant for Application Developers wanting to delete from aria-describedby attribute. | ||
* Allows to remove element references from aria-describedby attribute. | ||
* @param {HTMLElement} element | ||
@@ -829,2 +858,4 @@ */ | ||
/** | ||
* Hook for Subclassers to add logic before repropagation | ||
* @configurable | ||
* @param {CustomEvent} ev | ||
@@ -831,0 +862,0 @@ * @protected |
@@ -26,38 +26,7 @@ import { dedupeMixin } from '@lion/core'; | ||
return { | ||
/** | ||
* True when user has focused and left(blurred) the field. | ||
*/ | ||
touched: { | ||
type: Boolean, | ||
reflect: true, | ||
}, | ||
/** | ||
* True when user has changed the value of the field. | ||
*/ | ||
dirty: { | ||
type: Boolean, | ||
reflect: true, | ||
}, | ||
/** | ||
* True when the modelValue is non-empty (see _isEmpty in FormControlMixin) | ||
*/ | ||
filled: { | ||
type: Boolean, | ||
reflect: true, | ||
}, | ||
/** | ||
* True when user has left non-empty field or input is prefilled. | ||
* The name must be seen from the point of view of the input field: | ||
* once the user enters the input field, the value is non-empty. | ||
*/ | ||
prefilled: { | ||
attribute: false, | ||
}, | ||
/** | ||
* True when user has attempted to submit the form, e.g. through a button | ||
* of type="submit" | ||
*/ | ||
submitted: { | ||
attribute: false, | ||
}, | ||
touched: { type: Boolean, reflect: true }, | ||
dirty: { type: Boolean, reflect: true }, | ||
filled: { type: Boolean, reflect: true }, | ||
prefilled: { attribute: false }, | ||
submitted: { attribute: false }, | ||
}; | ||
@@ -67,3 +36,2 @@ } | ||
/** | ||
* | ||
* @param {PropertyKey} name | ||
@@ -91,14 +59,61 @@ * @param {*} oldVal | ||
super(); | ||
/** | ||
* True when user has focused and left(blurred) the field. | ||
* @type {boolean} | ||
*/ | ||
this.touched = false; | ||
/** | ||
* True when user has changed the value of the field. | ||
* @type {boolean} | ||
*/ | ||
this.dirty = false; | ||
/** | ||
* True when user has left non-empty field or input is prefilled. | ||
* The name must be seen from the point of view of the input field: | ||
* once the user enters the input field, the value is non-empty. | ||
* @type {boolean} | ||
*/ | ||
this.prefilled = false; | ||
/** | ||
* True when the modelValue is non-empty (see _isEmpty in FormControlMixin) | ||
* @type {boolean} | ||
*/ | ||
this.filled = false; | ||
/** @type {string} */ | ||
/** | ||
* True when user has attempted to submit the form, e.g. through a button | ||
* of type="submit" | ||
* @type {boolean} | ||
*/ | ||
// TODO: [v1] this might be fixable by scheduling property effects till firstUpdated | ||
// this.submitted = false; | ||
/** | ||
* The event that triggers the touched state | ||
* @type {string} | ||
* @protected | ||
*/ | ||
this._leaveEvent = 'blur'; | ||
/** @type {string} */ | ||
/** | ||
* The event that triggers the dirty state | ||
* @type {string} | ||
* @protected | ||
*/ | ||
this._valueChangedEvent = 'model-value-changed'; | ||
/** @type {EventHandlerNonNull} */ | ||
/** | ||
* @type {EventHandlerNonNull} | ||
* @protected | ||
*/ | ||
this._iStateOnLeave = this._iStateOnLeave.bind(this); | ||
/** @type {EventHandlerNonNull} */ | ||
/** | ||
* @type {EventHandlerNonNull} | ||
* @protected | ||
*/ | ||
this._iStateOnValueChange = this._iStateOnValueChange.bind(this); | ||
@@ -124,6 +139,5 @@ } | ||
/** | ||
* Evaluations performed on connectedCallback. Since some components can be out of sync | ||
* (due to interdependence on light children that can only be processed | ||
* after connectedCallback and affect the initial value). | ||
* This method is exposed, so it can be called after they are initialized themselves. | ||
* Evaluations performed on connectedCallback. | ||
* This method is public, so it can be called at a later moment (when we need to wait for | ||
* registering children for instance) as well. | ||
* Since this method will be called twice in last mentioned scenario, it must stay idempotent. | ||
@@ -137,4 +151,3 @@ */ | ||
/** | ||
* Sets touched value to true | ||
* Reevaluates prefilled state. | ||
* Sets touched value to true and reevaluates prefilled state. | ||
* When false, on next interaction, user will start with a clean state. | ||
@@ -167,18 +180,21 @@ * @protected | ||
/** | ||
* Dispatches custom event on touched state change | ||
* Dispatches event on touched state change | ||
* @protected | ||
*/ | ||
_onTouchedChanged() { | ||
this.dispatchEvent(new CustomEvent('touched-changed', { bubbles: true, composed: true })); | ||
/** @protectedEvent touched-changed */ | ||
this.dispatchEvent(new Event('touched-changed', { bubbles: true, composed: true })); | ||
} | ||
/** | ||
* Dispatches custom event on touched state change | ||
* Dispatches event on touched state change | ||
* @protected | ||
*/ | ||
_onDirtyChanged() { | ||
this.dispatchEvent(new CustomEvent('dirty-changed', { bubbles: true, composed: true })); | ||
/** @protectedEvent dirty-changed */ | ||
this.dispatchEvent(new Event('dirty-changed', { bubbles: true, composed: true })); | ||
} | ||
/** | ||
* @override ValidateMixin | ||
* Show the validity feedback when one of the following conditions is met: | ||
@@ -208,2 +224,5 @@ * | ||
/** | ||
* @enhance ValidateMixin | ||
*/ | ||
get _feedbackConditionMeta() { | ||
@@ -210,0 +229,0 @@ return { |
@@ -24,6 +24,2 @@ declare const LionField_base: typeof LitElement & import("@open-wc/dedupe-mixin").Constructor<import("@lion/core/types/SlotMixinTypes").SlotHost> & Pick<typeof import("@lion/core/types/SlotMixinTypes").SlotHost, "prototype"> & Pick<{ | ||
/** @type {any} */ | ||
static get properties(): any; | ||
/** @type {string | undefined} */ | ||
autocomplete: string | undefined; | ||
/** @type {any} */ | ||
_initialModelValue: any; | ||
@@ -30,0 +26,0 @@ /** |
@@ -28,22 +28,2 @@ import { LitElement, SlotMixin } from '@lion/core'; | ||
) { | ||
/** @type {any} */ | ||
static get properties() { | ||
return { | ||
autocomplete: { | ||
type: String, | ||
reflect: true, | ||
}, | ||
value: { | ||
type: String, | ||
}, | ||
}; | ||
} | ||
constructor() { | ||
super(); | ||
this.name = ''; | ||
/** @type {string | undefined} */ | ||
this.autocomplete = undefined; | ||
} | ||
/** | ||
@@ -89,3 +69,4 @@ * @param {import('@lion/core').PropertyValues } changedProperties | ||
clear() { | ||
this.modelValue = ''; // can't set null here, because IE11 treats it as a string | ||
// TODO: [v1] set to undefined | ||
this.modelValue = ''; | ||
} | ||
@@ -98,7 +79,4 @@ | ||
_onChange() { | ||
this.dispatchEvent( | ||
new CustomEvent('user-input-changed', { | ||
bubbles: true, | ||
}), | ||
); | ||
/** @protectedEvent user-input-changed */ | ||
this.dispatchEvent(new Event('user-input-changed', { bubbles: true })); | ||
} | ||
@@ -112,2 +90,9 @@ | ||
} | ||
/** | ||
* @configure FocusMixin | ||
*/ | ||
get _focusableNode() { | ||
return this._inputNode; | ||
} | ||
} |
@@ -13,2 +13,19 @@ import { dedupeMixin } from '@lion/core'; | ||
class NativeTextFieldMixin extends FormatMixin(FocusMixin(FormControlMixin(superclass))) { | ||
/** @type {any} */ | ||
static get properties() { | ||
return { | ||
autocomplete: { type: String, reflect: true }, | ||
}; | ||
} | ||
constructor() { | ||
super(); | ||
/** | ||
* Delegates this property to input/textarea/select. | ||
* @type {string | undefined} | ||
*/ | ||
this.autocomplete = undefined; | ||
} | ||
/** | ||
@@ -114,4 +131,11 @@ * @protected | ||
} | ||
/** | ||
* @configure FocusMixin | ||
*/ | ||
get _focusableNode() { | ||
return this._inputNode; | ||
} | ||
}; | ||
export const NativeTextFieldMixin = dedupeMixin(NativeTextFieldMixinImplementation); |
/** | ||
* @desc This class closely mimics the natively | ||
* This class closely mimics the natively | ||
* supported HTMLFormControlsCollection. It can be accessed | ||
@@ -4,0 +4,0 @@ * both like an array and an object (based on control/element names). |
/* eslint-disable */ | ||
/** | ||
* @desc This class closely mimics the natively | ||
* This class closely mimics the natively | ||
* supported HTMLFormControlsCollection. It can be accessed | ||
@@ -6,0 +6,0 @@ * both like an array and an object (based on control/element names). |
@@ -24,3 +24,7 @@ import { dedupeMixin } from '@lion/core'; | ||
super(); | ||
/** @type {FormRegistrarHost | undefined} */ | ||
/** | ||
* The registrar this FormControl registers to, Usually a descendant of FormGroup or | ||
* ChoiceGroup | ||
* @type {FormRegistrarHost | undefined} | ||
*/ | ||
this._parentFormGroup = undefined; | ||
@@ -27,0 +31,0 @@ } |
@@ -32,11 +32,2 @@ // eslint-disable-next-line max-classes-per-file | ||
return { | ||
/** | ||
* @desc Flag that determines how ".formElements" should behave. | ||
* For a regular fieldset (see LionFieldset) we expect ".formElements" | ||
* to be accessible as an object. | ||
* In case of a radio-group, a checkbox-group or a select/listbox, | ||
* it should act like an array (see ChoiceGroupMixin). | ||
* Usually, when false, we deal with a choice-group (radio-group, checkbox-group, | ||
* (multi)select) | ||
*/ | ||
_isFormOrFieldset: { type: Boolean }, | ||
@@ -48,4 +39,21 @@ }; | ||
super(); | ||
/** | ||
* Closely mimics the natively supported HTMLFormControlsCollection. It can be accessed | ||
* both like an array and an object (based on control/element names). | ||
* @type {FormControlsCollection} | ||
*/ | ||
this.formElements = new FormControlsCollection(); | ||
/** | ||
* Flag that determines how ".formElements" should behave. | ||
* For a regular fieldset (see LionFieldset) we expect ".formElements" | ||
* to be accessible as an object. | ||
* In case of a radio-group, a checkbox-group or a select/listbox, | ||
* it should act like an array (see ChoiceGroupMixin). | ||
* Usually, when false, we deal with a choice-group (radio-group, checkbox-group, | ||
* (multi)select) | ||
* @type {boolean} | ||
* @protected | ||
*/ | ||
this._isFormOrFieldset = false; | ||
@@ -105,2 +113,6 @@ | ||
/** | ||
* Resolves the registrationComplete promise. Subclassers can delay if needed | ||
* @overridable | ||
*/ | ||
_completeRegistration() { | ||
@@ -203,2 +215,3 @@ Promise.resolve().then(() => this.__resolveRegistrationComplete(undefined)); | ||
/** | ||
* Hook for Subclassers to perform logic before an element is added | ||
* @param {CustomEvent} ev | ||
@@ -205,0 +218,0 @@ * @protected |
@@ -27,3 +27,8 @@ import { dedupeMixin } from '@lion/core'; | ||
super(); | ||
/** @type {(FormRegistrarPortalHost & HTMLElement) | undefined} */ | ||
/** | ||
* Registration target: an element, usually in the body of the dom, that captures events | ||
* and redispatches them on host | ||
* @type {(FormRegistrarPortalHost & HTMLElement) | undefined} | ||
*/ | ||
this.registrationTarget = undefined; | ||
@@ -30,0 +35,0 @@ this.__redispatchEventForFormRegistrarPortalMixin = this.__redispatchEventForFormRegistrarPortalMixin.bind( |
@@ -11,3 +11,3 @@ import { dedupeMixin } from '@lion/core'; | ||
/** | ||
* @desc Why this mixin? | ||
* Why this mixin? | ||
* - it adheres to the "Member Order Independence" web components standard: | ||
@@ -31,4 +31,5 @@ * https://github.com/webcomponents/gold-standard/wiki/Member-Order-Independence | ||
super(); | ||
// Namespace for this mixin that guarantees naming clashes will not occur... | ||
/** | ||
* Namespace for this mixin that guarantees naming clashes will not occur... | ||
* @type {SyncUpdatableNamespace} | ||
@@ -118,3 +119,10 @@ */ | ||
/** | ||
* @desc A public abstraction that has the exact same api as `requestUpdateInternal`. | ||
* An abstraction that has the exact same api as `requestUpdateInternal`, but taking | ||
* into account: | ||
* - [member order independence](https://github.com/webcomponents/gold-standard/wiki/Member-Order-Independence) | ||
* - property effects start when all (light) dom has initialized (on firstUpdated) | ||
* - property effects don't interrupt the first meaningful paint | ||
* - compatible with propertyAccessor.`hasChanged`: no manual checks needed or accidentally | ||
* run property effects / events when no change happened | ||
* effects when values didn't change | ||
* All code previously present in requestUpdateInternal can be placed in this method. | ||
@@ -121,0 +129,0 @@ * @param {string} name |
@@ -45,2 +45,4 @@ import { html, LitElement } from '@lion/core'; | ||
window.clearTimeout(this.removeMessage); | ||
// TODO: this logic should be in ValidateMixin, so that [show-feedback-for] is in sync, | ||
// plus duration should be configurable | ||
if (this.currentType === 'success') { | ||
@@ -47,0 +49,0 @@ this.removeMessage = window.setTimeout(() => { |
@@ -19,5 +19,14 @@ /** | ||
constructor(value: string); | ||
type: string; | ||
/** | ||
* Meta info for restoring serialized Unparseable values | ||
* @type {'unparseable'} | ||
*/ | ||
type: 'unparseable'; | ||
/** | ||
* Stores current view value. For instance, value '09-' is an unparseable Date. | ||
* This info can be used to restore previous form states. | ||
* @type {string} | ||
*/ | ||
viewValue: string; | ||
toString(): string; | ||
} |
@@ -19,3 +19,12 @@ /** | ||
constructor(value) { | ||
/** | ||
* Meta info for restoring serialized Unparseable values | ||
* @type {'unparseable'} | ||
*/ | ||
this.type = 'unparseable'; | ||
/** | ||
* Stores current view value. For instance, value '09-' is an unparseable Date. | ||
* This info can be used to restore previous form states. | ||
* @type {string} | ||
*/ | ||
this.viewValue = value; | ||
@@ -22,0 +31,0 @@ } |
/** | ||
* @desc Handles all validation, based on modelValue changes. It has no knowledge about dom and | ||
* Handles all validation, based on modelValue changes. It has no knowledge about dom and | ||
* UI. All error visibility, dom interaction and accessibility are handled in FeedbackMixin. | ||
@@ -11,1 +11,2 @@ * | ||
export const ValidateMixin: typeof import("../../types/validate/ValidateMixinTypes.js").ValidateImplementation; | ||
export type ValidationType = string; |
@@ -15,4 +15,7 @@ /* eslint-disable class-methods-use-this, camelcase, no-param-reassign, max-classes-per-file */ | ||
// TODO: [v1] make all @readOnly => @readonly and actually make sure those values cannot be set | ||
/** | ||
* @typedef {import('../../types/validate/ValidateMixinTypes').ValidateMixin} ValidateMixin | ||
* @typedef {import('../../types/validate/ValidateMixinTypes').ValidationType} ValidationType | ||
*/ | ||
@@ -29,3 +32,3 @@ | ||
/** | ||
* @desc Handles all validation, based on modelValue changes. It has no knowledge about dom and | ||
* Handles all validation, based on modelValue changes. It has no knowledge about dom and | ||
* UI. All error visibility, dom interaction and accessibility are handled in FeedbackMixin. | ||
@@ -53,7 +56,4 @@ * | ||
validators: { attribute: false }, | ||
hasFeedbackFor: { attribute: false }, | ||
shouldShowFeedbackFor: { attribute: false }, | ||
showsFeedbackFor: { | ||
@@ -68,8 +68,3 @@ type: Array, | ||
}, | ||
validationStates: { attribute: false }, | ||
/** | ||
* @desc flag that indicates whether async validation is pending | ||
*/ | ||
isPending: { | ||
@@ -80,15 +75,4 @@ type: Boolean, | ||
}, | ||
/** | ||
* @desc specialized fields (think of input-date and input-email) can have preconfigured | ||
* validators. | ||
*/ | ||
defaultValidators: { attribute: false }, | ||
/** | ||
* Subclassers can enable this to show multiple feedback messages at the same time | ||
* By default, just like the platform, only one message (with highest prio) is visible. | ||
*/ | ||
_visibleMessagesAmount: { attribute: false }, | ||
__childModelValueChanged: { attribute: false }, | ||
@@ -99,3 +83,5 @@ }; | ||
/** | ||
* Types of validation supported by this FormControl (for instance 'error'|'warning'|'info') | ||
* @overridable | ||
* @type {ValidationType[]} | ||
*/ | ||
@@ -133,2 +119,7 @@ static get validationTypes() { | ||
/** | ||
* Combination of validators provided by Application Developer and the default validators | ||
* @type {Validator[]} | ||
* @protected | ||
*/ | ||
get _allValidators() { | ||
@@ -141,25 +132,90 @@ return [...this.validators, ...this.defaultValidators]; | ||
/** @type {string[]} */ | ||
/** | ||
* As soon as validation happens (after modelValue/validators/validator param change), this | ||
* array is updated with the active ValidationTypes ('error'|'warning'|'success'|'info' etc.). | ||
* Notice the difference with `.showsFeedbackFor`, which filters `.hasFeedbackFor` based on | ||
* `.feedbackCondition()`. | ||
* | ||
* For styling purposes, will be reflected to [has-feedback-for="error warning"]. This can | ||
* be useful for subtle visual feedback on keyup, like a red/green border around an input. | ||
* | ||
* @example | ||
* ```css | ||
* :host([has-feedback-for~="error"]) .input-group__container { | ||
* border: 1px solid red; | ||
* } | ||
* ``` | ||
* @type {ValidationType[]} | ||
* @readOnly | ||
*/ | ||
this.hasFeedbackFor = []; | ||
/** @type {string[]} */ | ||
/** | ||
* Based on outcome of feedbackCondition, this array decides what ValidationTypes should be | ||
* shown in validationFeedback, based on meta data like interaction states. | ||
* | ||
* For styling purposes, it reflects it `[shows-feedback-for="error warning"]` | ||
* @type {ValidationType[]} | ||
* @readOnly | ||
* @example | ||
* ```css | ||
* :host([shows-feedback-for~="success"]) .form-field__feedback { | ||
* transform: scaleY(1); | ||
* } | ||
* ``` | ||
*/ | ||
this.showsFeedbackFor = []; | ||
// TODO: [v1] make this fully private (preifix __)? | ||
/** | ||
* A temporary storage to transition from hasFeedbackFor to showsFeedbackFor | ||
* @type {ValidationType[]} | ||
* @readOnly | ||
* @private | ||
*/ | ||
this.shouldShowFeedbackFor = []; | ||
/** @type {string[]} */ | ||
this.showsFeedbackFor = []; | ||
/** @type {Object.<string, Object.<string, boolean>>} */ | ||
/** | ||
* The outcome of a validation 'round'. Keyed by ValidationType and Validator name | ||
* @readOnly | ||
* @type {Object.<string, Object.<string, boolean>>} | ||
*/ | ||
this.validationStates = {}; | ||
/** @protected */ | ||
this._visibleMessagesAmount = 1; | ||
/** | ||
* Flag indicating whether async validation is pending. | ||
* Creates attribute [is-pending] as a styling hook | ||
* @type {boolean} | ||
*/ | ||
this.isPending = false; | ||
/** @type {Validator[]} */ | ||
/** | ||
* Used by Application Developers to add Validators to a FormControl. | ||
* @example | ||
* ```html | ||
* <form-control .validators="${[new Required(), new MinLength(4, {type: 'warning'})]}"> | ||
* </form-control> | ||
* ``` | ||
* @type {Validator[]} | ||
*/ | ||
this.validators = []; | ||
/** @type {Validator[]} */ | ||
/** | ||
* Used by Subclassers to add default Validators to a particular FormControl. | ||
* A date input for instance, always needs the isDate validator. | ||
* @example | ||
* ```js | ||
* this.defaultValidators.push(new IsDate()); | ||
* ``` | ||
* @type {Validator[]} | ||
*/ | ||
this.defaultValidators = []; | ||
/** | ||
* The amount of feedback messages that will visible in LionValidationFeedback | ||
* @protected | ||
*/ | ||
this._visibleMessagesAmount = 1; | ||
/** | ||
* @type {Validator[]} | ||
@@ -177,3 +233,3 @@ * @private | ||
/** | ||
* @desc contains results from sync Validators, async Validators and ResultValidators | ||
* Aggregated result from sync Validators, async Validators and ResultValidators | ||
* @type {Validator[]} | ||
@@ -183,2 +239,3 @@ * @private | ||
this.__validationResult = []; | ||
/** | ||
@@ -189,5 +246,16 @@ * @type {Validator[]} | ||
this.__prevValidationResult = []; | ||
/** @type {Validator[]} */ | ||
/** | ||
* @type {Validator[]} | ||
* @private | ||
*/ | ||
this.__prevShownValidationResult = []; | ||
/** | ||
* The updated children validity affects the validity of the parent. Helper to recompute | ||
* validatity of parent FormGroup | ||
* @private | ||
*/ | ||
this.__childModelValueChanged = false; | ||
/** @private */ | ||
@@ -197,8 +265,2 @@ this.__onValidatorUpdated = this.__onValidatorUpdated.bind(this); | ||
this._updateFeedbackComponent = this._updateFeedbackComponent.bind(this); | ||
/** | ||
* This will be used for FormGroups that listen for `model-value-changed` of children | ||
* @private | ||
*/ | ||
this.__childModelValueChanged = false; | ||
} | ||
@@ -286,24 +348,24 @@ | ||
/** | ||
* @desc The main function of this mixin. Triggered by: | ||
* - a modelValue change | ||
* - a change in the 'validators' array | ||
* - a change in the config of an individual Validator | ||
* Triggered by: | ||
* - modelValue change | ||
* - change in the 'validators' array | ||
* - change in the config of an individual Validator | ||
* | ||
* Three situations are handled: | ||
* - A.1 The FormControl is empty: further execution is halted. When the Required Validator | ||
* - a1) the FormControl is empty: further execution is halted. When the Required Validator | ||
* (being mutually exclusive to the other Validators) is applied, it will end up in the | ||
* validation result (as the only Validator, since further execution was halted). | ||
* - A.2 There are synchronous Validators: this is the most common flow. When modelValue hasn't | ||
* - a2) there are synchronous Validators: this is the most common flow. When modelValue hasn't | ||
* changed since last async results were generated, 'sync results' are merged with the | ||
* 'async results'. | ||
* - A.3 There are asynchronous Validators: for instance when server side evaluation is needed. | ||
* - a3) there are asynchronous Validators: for instance when server side evaluation is needed. | ||
* Executions are scheduled and awaited and the 'async results' are merged with the | ||
* 'sync results'. | ||
* | ||
* - B. There are ResultValidators. After steps A.1, A.2, or A.3 are finished, the holistic | ||
* ResultValidators (evaluating the total result of the 'regular' (A.1, A.2 and A.3) validators) | ||
* - b) there are ResultValidators. After steps a1, a2, or a3 are finished, the holistic | ||
* ResultValidators (evaluating the total result of the 'regular' (a1, a2 and a3) validators) | ||
* will be run... | ||
* | ||
* Situations A.2 and A.3 are not mutually exclusive and can be triggered within one validate() | ||
* call. Situation B will occur after every call. | ||
* Situations a2 and a3 are not mutually exclusive and can be triggered within one `validate()` | ||
* call. Situation b will occur after every call. | ||
* | ||
@@ -334,3 +396,3 @@ * @param {{ clearCurrentResult?: boolean }} [opts] | ||
/** | ||
* @desc step A1-3 + B (as explained in 'validate') | ||
* @desc step a1-3 + b (as explained in `validate()`) | ||
*/ | ||
@@ -397,3 +459,3 @@ async __executeValidators() { | ||
/** | ||
* @desc step A2, calls __finishValidation | ||
* step a2 (as explained in `validate()`): calls `__finishValidation` | ||
* @param {Validator[]} syncValidators | ||
@@ -414,3 +476,3 @@ * @param {unknown} value | ||
/** | ||
* @desc step A3, calls __finishValidation | ||
* step a3 (as explained in `validate()`), calls __finishValidation | ||
* @param {Validator[]} asyncValidators all Validators except required and ResultValidators | ||
@@ -434,3 +496,3 @@ * @param {?} value | ||
/** | ||
* @desc step B, called by __finishValidation | ||
* step b (as explained in `validate()`), called by __finishValidation | ||
* @param {Validator[]} regularValidationResult result of steps 1-3 | ||
@@ -561,2 +623,3 @@ * @private | ||
/** | ||
* Helper method for the mutually exclusive Required Validator | ||
* @param {?} v | ||
@@ -614,3 +677,3 @@ * @private | ||
/** | ||
* @desc Responsible for retrieving messages from Validators and | ||
* Responsible for retrieving messages from Validators and | ||
* (delegation of) rendering them. | ||
@@ -662,4 +725,4 @@ * | ||
/** | ||
* The default feedbackCondition condition that will be used when the | ||
* feedbackCondition is not overridden. | ||
* Default feedbackCondition condition, used by Subclassers, that will be used when | ||
* `feedbackCondition()` is not overridden by Application Developer. | ||
* Show the validity feedback when returning true, don't show when false | ||
@@ -677,3 +740,3 @@ * @param {string} type could be 'error', 'warning', 'info', 'success' or any other custom | ||
/** | ||
* Allows super classes to add meta info for feedbackCondition | ||
* Allows Subclassers to add meta info for feedbackCondition | ||
* @configurable | ||
@@ -688,2 +751,3 @@ */ | ||
* @example | ||
* ```js | ||
* feedbackCondition(type, meta, defaultCondition) { | ||
@@ -697,2 +761,3 @@ * if (type === 'info') { | ||
* } | ||
* ``` | ||
* @overridable | ||
@@ -716,2 +781,3 @@ * @param {string} type could be 'error', 'warning', 'info', 'success' or any other custom | ||
/** | ||
* Used to translate `.hasFeedbackFor` and `.shouldShowFeedbackFor` to `.showsFeedbackFor` | ||
* @param {string} type | ||
@@ -794,9 +860,8 @@ * @protected | ||
/** | ||
* Orders all active validators in this.__validationResult. Can | ||
* also filter out occurrences (based on interaction states) | ||
* @overridable | ||
* @desc Orders all active validators in this.__validationResult. Can | ||
* also filter out occurrences (based on interaction states) | ||
* @param {{ validationResult: Validator[] }} opts | ||
* @return {Validator[]} ordered list of Validators with feedback messages visible to the | ||
* @return {Validator[]} ordered list of Validators with feedback messages visible to the end user | ||
* @protected | ||
* end user | ||
*/ | ||
@@ -803,0 +868,0 @@ _prioritizeAndFilterFeedback({ validationResult }) { |
import { LitElement } from '@lion/core'; | ||
import { defineCE, expect, fixture, html, oneEvent, unsafeStatic } from '@open-wc/testing'; | ||
import { getFormControlMembers } from '@lion/form-core/test-helpers'; | ||
import sinon from 'sinon'; | ||
import { FocusMixin } from '../src/FocusMixin.js'; | ||
const windowWithOptionalPolyfill = /** @type {Window & typeof globalThis & {applyFocusVisiblePolyfill?: function}} */ (window); | ||
/** | ||
* Checks two things: | ||
* 1. whether focus-visible should apply (if focus and keyboard interaction present) | ||
* 2. whether the polyfill is used or not | ||
* When the polyfill is used, it mocks `.hasAttribute` method, otherwise `.matches` method | ||
* of focusable element. | ||
* @param {HTMLElement} focusableEl focusable element | ||
* @param {{phase: 'focusin'|'focusout', hasKeyboardInteraction: boolean }} options | ||
* @returns {function} restore function | ||
*/ | ||
function mockFocusVisible(focusableEl, { phase, hasKeyboardInteraction }) { | ||
const focusVisibleApplies = phase === 'focusin' && hasKeyboardInteraction; | ||
if (!focusVisibleApplies) { | ||
return () => {}; | ||
} | ||
/** @type {any} */ | ||
const originalMatches = focusableEl.matches; | ||
if (typeof windowWithOptionalPolyfill.applyFocusVisiblePolyfill !== 'function') { | ||
// eslint-disable-next-line no-param-reassign | ||
focusableEl.matches = selector => | ||
selector === ':focus-visible' || originalMatches.call(focusableEl, selector); | ||
return () => { | ||
// eslint-disable-next-line no-param-reassign | ||
focusableEl.matches = originalMatches; | ||
}; | ||
} | ||
const originalHasAttribute = focusableEl.hasAttribute; | ||
// eslint-disable-next-line no-param-reassign | ||
focusableEl.hasAttribute = attr => | ||
attr === 'data-focus-visible-added' || originalHasAttribute.call(focusableEl, attr); | ||
return () => { | ||
// eslint-disable-next-line no-param-reassign | ||
focusableEl.hasAttribute = originalHasAttribute; | ||
}; | ||
} | ||
/** | ||
* @returns {function} restore function | ||
*/ | ||
function mockPolyfill() { | ||
const originalApplyFocusVisiblePolyfill = windowWithOptionalPolyfill.applyFocusVisiblePolyfill; | ||
// @ts-ignore | ||
window.applyFocusVisiblePolyfill = () => {}; | ||
return () => { | ||
// @ts-ignore | ||
window.applyFocusVisiblePolyfill = originalApplyFocusVisiblePolyfill; | ||
}; | ||
} | ||
describe('FocusMixin', () => { | ||
@@ -11,2 +64,9 @@ class Focusable extends FocusMixin(LitElement) { | ||
} | ||
/** | ||
* @configure FocusMixin | ||
*/ | ||
get _focusableNode() { | ||
return /** @type {HTMLInputElement} */ (this.querySelector('input')); | ||
} | ||
} | ||
@@ -21,8 +81,9 @@ | ||
`)); | ||
const { _inputNode } = getFormControlMembers(el); | ||
// @ts-ignore [allow-protected] in test | ||
const { _focusableNode } = el; | ||
el.focus(); | ||
expect(document.activeElement === _inputNode).to.be.true; | ||
expect(document.activeElement === _focusableNode).to.be.true; | ||
el.blur(); | ||
expect(document.activeElement === _inputNode).to.be.false; | ||
expect(document.activeElement === _focusableNode).to.be.false; | ||
}); | ||
@@ -48,8 +109,9 @@ | ||
`)); | ||
const { _inputNode } = getFormControlMembers(el); | ||
// @ts-ignore [allow-protected] in test | ||
const { _focusableNode } = el; | ||
expect(el.focused).to.be.false; | ||
_inputNode?.focus(); | ||
_focusableNode?.focus(); | ||
expect(el.focused).to.be.true; | ||
_inputNode?.blur(); | ||
_focusableNode?.blur(); | ||
expect(el.focused).to.be.false; | ||
@@ -101,2 +163,153 @@ }); | ||
}); | ||
describe('Having :focus-visible within', () => { | ||
it('sets focusedVisible to true when focusable element matches :focus-visible', async () => { | ||
const el = /** @type {Focusable} */ (await fixture(html` | ||
<${tag}><input slot="input"></${tag}> | ||
`)); | ||
// @ts-ignore [allow-protected] in test | ||
const { _focusableNode } = el; | ||
const restoreMock1 = mockFocusVisible(_focusableNode, { | ||
phase: 'focusout', | ||
hasKeyboardInteraction: true, | ||
}); | ||
_focusableNode.dispatchEvent(new Event('focusout', { bubbles: true, composed: true })); | ||
await el.updateComplete; | ||
expect(el.focusedVisible).to.be.false; | ||
restoreMock1(); | ||
const restoreMock2 = mockFocusVisible(_focusableNode, { | ||
phase: 'focusin', | ||
hasKeyboardInteraction: false, | ||
}); | ||
_focusableNode.dispatchEvent(new Event('focusin', { bubbles: true, composed: true })); | ||
await el.updateComplete; | ||
expect(el.focusedVisible).to.be.false; | ||
restoreMock2(); | ||
const restoreMock3 = mockFocusVisible(_focusableNode, { | ||
phase: 'focusout', | ||
hasKeyboardInteraction: false, | ||
}); | ||
_focusableNode.dispatchEvent(new Event('focusout', { bubbles: true, composed: true })); | ||
await el.updateComplete; | ||
expect(el.focusedVisible).to.be.false; | ||
restoreMock3(); | ||
const restoreMock4 = mockFocusVisible(_focusableNode, { | ||
phase: 'focusin', | ||
hasKeyboardInteraction: true, | ||
}); | ||
_focusableNode.dispatchEvent(new Event('focusin', { bubbles: true, composed: true })); | ||
await el.updateComplete; | ||
expect(el.focusedVisible).to.be.true; | ||
restoreMock4(); | ||
}); | ||
it('has an attribute focused-visible when focusedVisible is true', async () => { | ||
const el = /** @type {Focusable} */ (await fixture(html` | ||
<${tag}><input slot="input"></${tag}> | ||
`)); | ||
// @ts-ignore [allow-protected] in test | ||
const { _focusableNode } = el; | ||
const restoreMock1 = mockFocusVisible(_focusableNode, { | ||
phase: 'focusout', | ||
hasKeyboardInteraction: true, | ||
}); | ||
_focusableNode.dispatchEvent(new Event('focusout', { bubbles: true, composed: true })); | ||
await el.updateComplete; | ||
expect(el.hasAttribute('focused-visible')).to.be.false; | ||
restoreMock1(); | ||
const restoreMock2 = mockFocusVisible(_focusableNode, { | ||
phase: 'focusin', | ||
hasKeyboardInteraction: true, | ||
}); | ||
_focusableNode.dispatchEvent(new Event('focusin', { bubbles: true, composed: true })); | ||
await el.updateComplete; | ||
expect(el.hasAttribute('focused-visible')).to.be.true; | ||
restoreMock2(); | ||
}); | ||
// For polyfill, see https://github.com/WICG/focus-visible | ||
describe('Using polyfill', () => { | ||
const restoreMockPolyfill = mockPolyfill(); | ||
after(() => { | ||
restoreMockPolyfill(); | ||
}); | ||
it('calls polyfill once per node', async () => { | ||
class UniqueHost extends LitElement { | ||
render() { | ||
return html`<${tag}><input slot="input"></${tag}><${tag}><input slot="input"></${tag}>`; | ||
} | ||
} | ||
const hostTagString = defineCE(UniqueHost); | ||
const hostTag = unsafeStatic(hostTagString); | ||
const polySpy = sinon.spy(windowWithOptionalPolyfill, 'applyFocusVisiblePolyfill'); | ||
await fixture(html`<${hostTag}></${hostTag}>`); | ||
expect(polySpy).to.have.been.calledOnce; | ||
}); | ||
it('sets focusedVisible to true when focusable element if :focus-visible polyfill is loaded', async () => { | ||
const el = /** @type {Focusable} */ (await fixture(html` | ||
<${tag}><input slot="input"></${tag}> | ||
`)); | ||
// @ts-ignore [allow-protected] in test | ||
const { _focusableNode } = el; | ||
const restoreMock1 = mockFocusVisible(_focusableNode, { | ||
phase: 'focusout', | ||
hasKeyboardInteraction: true, | ||
}); | ||
const spy1 = sinon.spy(_focusableNode, 'hasAttribute'); | ||
_focusableNode.dispatchEvent(new Event('focusout', { bubbles: true, composed: true })); | ||
await el.updateComplete; | ||
expect(el.focusedVisible).to.be.false; | ||
expect(spy1).to.not.have.been.calledWith('data-focus-visible-added'); | ||
spy1.restore(); | ||
restoreMock1(); | ||
const restoreMock2 = mockFocusVisible(_focusableNode, { | ||
phase: 'focusin', | ||
hasKeyboardInteraction: false, | ||
}); | ||
const spy2 = sinon.spy(_focusableNode, 'hasAttribute'); | ||
_focusableNode.dispatchEvent(new Event('focusin', { bubbles: true, composed: true })); | ||
await el.updateComplete; | ||
expect(el.focusedVisible).to.be.false; | ||
expect(spy2).to.have.been.calledWith('data-focus-visible-added'); | ||
spy2.restore(); | ||
restoreMock2(); | ||
const restoreMock3 = mockFocusVisible(_focusableNode, { | ||
phase: 'focusout', | ||
hasKeyboardInteraction: false, | ||
}); | ||
const spy3 = sinon.spy(_focusableNode, 'hasAttribute'); | ||
_focusableNode.dispatchEvent(new Event('focusout', { bubbles: true, composed: true })); | ||
await el.updateComplete; | ||
expect(el.focusedVisible).to.be.false; | ||
expect(spy3).to.not.have.been.calledWith('data-focus-visible-added'); | ||
spy3.restore(); | ||
restoreMock3(); | ||
const restoreMock4 = mockFocusVisible(_focusableNode, { | ||
phase: 'focusin', | ||
hasKeyboardInteraction: true, | ||
}); | ||
const spy4 = sinon.spy(_focusableNode, 'hasAttribute'); | ||
_focusableNode.dispatchEvent(new Event('focusin', { bubbles: true, composed: true })); | ||
await el.updateComplete; | ||
expect(el.focusedVisible).to.be.true; | ||
expect(spy4).to.have.been.called; | ||
spy4.restore(); | ||
restoreMock4(); | ||
}); | ||
}); | ||
}); | ||
}); |
@@ -164,3 +164,10 @@ import { expect, html, defineCE, unsafeStatic, fixture } from '@open-wc/testing'; | ||
const focusableTagString = defineCE( | ||
class extends FocusMixin(FormControlMixin(LitElement)) {}, | ||
class extends FocusMixin(FormControlMixin(LitElement)) { | ||
/** | ||
* @configure FocusMixin | ||
*/ | ||
get _focusableNode() { | ||
return this._inputNode; | ||
} | ||
}, | ||
); | ||
@@ -167,0 +174,0 @@ const focusableTag = unsafeStatic(focusableTagString); |
@@ -23,2 +23,6 @@ import { Constructor } from '@open-wc/dedupe-mixin'; | ||
set modelValue(value: ChoiceInputModelValue); | ||
/** | ||
* The value that will be registered to the modelValue of the parent ChoiceGroup. Recommended | ||
* to be a string | ||
*/ | ||
get choiceValue(): any; | ||
@@ -25,0 +29,0 @@ set choiceValue(value: any); |
import { Constructor } from '@open-wc/dedupe-mixin'; | ||
import { LitElement } from '@lion/core'; | ||
import { FormControlHost } from './FormControlMixinTypes'; | ||
export declare class FocusHost { | ||
/** | ||
* Whether the focusable element within (`._focusableNode`) is focused. | ||
* Reflects to attribute '[focused]' as a styling hook | ||
*/ | ||
focused: boolean; | ||
/** | ||
* Whether the focusable element within (`._focusableNode`) matches ':focus-visible' | ||
* Reflects to attribute '[focused-visible]' as a styling hook | ||
* @see https://developer.mozilla.org/en-US/docs/Web/CSS/:focus-visible | ||
*/ | ||
focusedVisible: boolean; | ||
/** | ||
* Calls `focus()` on focusable element within | ||
*/ | ||
focus(): void; | ||
/** | ||
* Calls `blur()` on focusable element within | ||
*/ | ||
blur(): void; | ||
/** | ||
* The focusable element: | ||
* could be an input, textarea, select, button or any other element with tabindex > -1 | ||
*/ | ||
protected get _focusableNode(): HTMLElement; | ||
private __onFocus(): void; | ||
@@ -21,6 +45,4 @@ private __onBlur(): void; | ||
Pick<typeof FocusHost, keyof typeof FocusHost> & | ||
Constructor<FormControlHost> & | ||
Pick<typeof FormControlHost, keyof typeof FormControlHost> & | ||
Pick<typeof LitElement, keyof typeof LitElement>; | ||
export type FocusMixin = typeof FocusImplementation; |
@@ -10,22 +10,128 @@ import { Constructor } from '@open-wc/dedupe-mixin'; | ||
export declare class FormGroupHost { | ||
/** | ||
* Disables all formElements in group | ||
*/ | ||
disabled: boolean; | ||
/** | ||
* True when all of the children are prefilled (see InteractionStateMixin for more details.) | ||
*/ | ||
prefilled: boolean; | ||
/** | ||
* True when the group as a whole is blurred (see InteractionStateMixin for more details.) | ||
*/ | ||
touched: boolean; | ||
/** | ||
* True when any of the children is dirty (see InteractionStateMixin for more details.) | ||
*/ | ||
dirty: boolean; | ||
/** | ||
* True when parent form is submitted | ||
*/ | ||
submitted: boolean; | ||
/** | ||
* Object keyed by formElements names, containing formElements' serializedValues | ||
*/ | ||
serializedValue: { [key: string]: any }; | ||
/** | ||
* Object keyed by formElements names, containing formElements' formattedValues | ||
*/ | ||
formattedValue: string; | ||
children: Array<HTMLElement & FormControlHost>; | ||
/** | ||
* Object keyed by formElements names, containing formElements' modelValues | ||
*/ | ||
get modelValue(): { [x: string]: any }; | ||
set modelValue(value: { [x: string]: any }); | ||
/** | ||
* Resets all interaction states for all formElements | ||
*/ | ||
resetInteractionState(): void; | ||
/** | ||
* Clears all values and resets all interaction states of all FormControls in group, | ||
*/ | ||
clearGroup(): void; | ||
/** | ||
* Handles interaction state 'submitted'. | ||
* This allows children to enable visibility of validation feedback | ||
*/ | ||
submitGroup(): void; | ||
/** | ||
* Resets to initial/prefilled values and interaction states of all FormControls in group, | ||
*/ | ||
resetGroup(): void; | ||
/** | ||
* Gathers initial model values of all children. Used when resetGroup() is called. | ||
*/ | ||
protected _initialModelValue: { [x: string]: any }; | ||
/** | ||
* The host element with role group (or radigroup or form) containing neccessary aria attributes | ||
*/ | ||
protected get _inputNode(): HTMLElement; | ||
protected static _addDescriptionElementIdsToField(): void; | ||
/** | ||
* Gets a keyed be name object for requested property (like modelValue/serializedValue) | ||
*/ | ||
protected _getFromAllFormElements( | ||
property: string, | ||
filterFn: (el: FormControlHost) => boolean, | ||
): { [name: string]: any }; | ||
/** | ||
* Allows to set formElements values via a keyed object structure | ||
*/ | ||
protected _setValueMapForAllFormElements(property: string, values: { [x: string]: any }): void; | ||
/** | ||
* Sets the same value for requested property in all formElements | ||
*/ | ||
protected _setValueForAllFormElements(property: string, value: any): void; | ||
/** | ||
* Returns true when one of the formElements has requested property | ||
*/ | ||
protected _anyFormElementHas(prop: string): boolean; | ||
/** | ||
* Returns true when all of the formElements have requested property | ||
*/ | ||
protected _everyFormElementHas(prop: string): boolean; | ||
/** | ||
* Returns true when all of the formElements have requested property | ||
*/ | ||
protected _anyFormElementHasFeedbackFor(prop: string): boolean; | ||
protected _checkForOutsideClick(): void; | ||
protected _triggerInitialModelValueChangedEvent(): void; | ||
protected _syncDirty(): void; | ||
protected _onFocusOut(): void; | ||
protected _syncFocused(): void; | ||
private __descriptionElementsInParentChain: Set<HTMLElement>; | ||
private __addedSubValidators: boolean; | ||
private __isInitialModelValue: boolean; | ||
private __isInitialSerializedValue: boolean; | ||
private __pendingValues: { | ||
modelValue?: { [key: string]: any }; | ||
serializedValue?: { [key: string]: any }; | ||
}; | ||
private __initInteractionStates(): void; | ||
private __setupOutsideClickHandling(): void; | ||
private __requestChildrenToBeDisabled(): void; | ||
private __retractRequestChildrenToBeDisabled(): void; | ||
private __linkParentMessages(): void; | ||
private __unlinkParentMessages(): void; | ||
private __storeAllDescriptionElementsInParentChain(): void; | ||
private __onChildValidatePerformed(e: Event): void; | ||
} | ||
@@ -32,0 +138,0 @@ |
@@ -8,15 +8,111 @@ import { Constructor } from '@open-wc/dedupe-mixin'; | ||
export declare class FormatHost { | ||
/** | ||
* Converts viewValue to modelValue | ||
* For instance, a localized date to a Date Object | ||
* @param {string} v - viewValue: the formatted value inside <input> | ||
* @param {FormatOptions} opts | ||
* @returns {*} modelValue | ||
*/ | ||
parser(v: string, opts: FormatNumberOptions): unknown; | ||
/** | ||
* Converts modelValue to formattedValue (formattedValue will be synced with | ||
* `._inputNode.value`) | ||
* For instance, a Date object to a localized date. | ||
* @param {*} v - modelValue: can be an Object, Number, String depending on the | ||
* input type(date, number, email etc) | ||
* @param {FormatOptions} opts | ||
* @returns {string} formattedValue | ||
*/ | ||
formatter(v: unknown, opts?: FormatNumberOptions): string; | ||
/** | ||
* Converts `.modelValue` to `.serializedValue` | ||
* For instance, a Date object to an iso formatted date string | ||
* @param {?} v - modelValue: can be an Object, Number, String depending on the | ||
* input type(date, number, email etc) | ||
* @returns {string} serializedValue | ||
*/ | ||
serializer(v: unknown): string; | ||
/** | ||
* Converts `.serializedValue` to `.modelValue` | ||
* For instance, an iso formatted date string to a Date object | ||
* @param {?} v - modelValue: can be an Object, Number, String depending on the | ||
* input type(date, number, email etc) | ||
* @returns {?} modelValue | ||
*/ | ||
deserializer(v: string): unknown; | ||
/** | ||
* Preprocesses the viewValue before it's parsed to a modelValue. Can be used to filter | ||
* invalid input amongst others. | ||
* @example | ||
* ```js | ||
* preprocessor(viewValue) { | ||
* // only use digits | ||
* return viewValue.replace(/\D/g, ''); | ||
* } | ||
* ``` | ||
* @param {string} v - the raw value from the <input> after keyUp/Down event | ||
* @returns {string} preprocessedValue: the result of preprocessing for invalid input | ||
*/ | ||
preprocessor(v: string): string; | ||
formattedValue: string; | ||
serializedValue: string; | ||
/** | ||
* The view value is the result of the formatter function (when available). | ||
* The result will be stored in the native _inputNode (usually an input[type=text]). | ||
* | ||
* Examples: | ||
* - For a date input, this would be '20/01/1999' (dependent on locale). | ||
* - For a number input, this could be '1,234.56' (a String representation of modelValue | ||
* 1234.56) | ||
* @type {string|undefined} | ||
* @readOnly | ||
*/ | ||
formattedValue: string | undefined; | ||
/** | ||
* The serialized version of the model value. | ||
* This value exists for maximal compatibility with the platform API. | ||
* The serialized value can be an interface in context where data binding is not | ||
* supported and a serialized string needs to be set. | ||
* | ||
* Examples: | ||
* - For a date input, this would be the iso format of a date, e.g. '1999-01-20'. | ||
* - For a number input this would be the String representation of a float ('1234.56' | ||
* instead of 1234.56) | ||
* | ||
* When no parser is available, the value is usually the same as the formattedValue | ||
* (being _inputNode.value) | ||
*/ | ||
serializedValue: string | undefined; | ||
/** | ||
* Event that will trigger formatting (more precise, visual update of the view, so the | ||
* user sees the formatted value) | ||
* Default: 'change' | ||
* @deprecated use _reflectBackOn() | ||
* @protected | ||
*/ | ||
formatOn: string; | ||
/** | ||
* Configuration object that will be available inside the formatter function | ||
*/ | ||
formatOptions: FormatNumberOptions; | ||
/** | ||
* The view value. Will be delegated to `._inputNode.value` | ||
*/ | ||
get value(): string; | ||
set value(value: string); | ||
/** | ||
* Flag that will be set when user interaction takes place (for instance after an 'input' | ||
* event). Will be added as meta info to the `model-value-changed` event. Depending on | ||
* whether a user is interacting, formatting logic will be handled differently. | ||
*/ | ||
protected _isHandlingUserInput: boolean; | ||
/** | ||
@@ -26,4 +122,4 @@ * Whether the user is pasting content. Allows Subclassers to do this in their subclass: | ||
* ```js | ||
* _reflectBackFormattedValueToUser() { | ||
* return super._reflectBackFormattedValueToUser() || this._isPasting; | ||
* _reflectBackOn() { | ||
* return super._reflectBackOn() || this._isPasting; | ||
* } | ||
@@ -33,9 +129,44 @@ * ``` | ||
protected _isPasting: boolean; | ||
/** | ||
* Responsible for storing all representations(modelValue, serializedValue, formattedValue | ||
* and value) of the input value. Prevents infinite loops, so all value observers can be | ||
* treated like they will only be called once, without indirectly calling other observers. | ||
* (in fact, some are called twice, but the __preventRecursiveTrigger lock prevents the | ||
* second call from having effect). | ||
* @param {{source:'model'|'serialized'|'formatted'|null}} config | ||
* the type of value that triggered this method. It should not be set again, so that its | ||
* observer won't be triggered. Can be: 'model'|'formatted'|'serialized'. | ||
*/ | ||
protected _calculateValues(opts: { source: 'model' | 'serialized' | 'formatted' | null }): void; | ||
protected _onModelValueChanged(arg: { modelValue: unknown }): void; | ||
protected _dispatchModelValueChangedEvent(): void; | ||
/** | ||
* Synchronization from `._inputNode.value` to `LionField` (flow [2]) | ||
* Downwards syncing should only happen for `LionField`.value changes from 'above'. | ||
* This triggers _onModelValueChanged and connects user input | ||
* to the parsing/formatting/serializing loop. | ||
*/ | ||
protected _syncValueUpwards(): void; | ||
protected _reflectBackFormattedValueToUser(): void; | ||
protected _reflectBackFormattedValueDebounced(): void; | ||
private _reflectBackFormattedValueDebounced(): void; | ||
/** | ||
* Every time .formattedValue is attempted to sync to the view value (on change/blur and on | ||
* modelValue change), this condition is checked. When enhancing it, it's recommended to | ||
* call `super._reflectBackOn()` | ||
* @overridable | ||
* @return {boolean} | ||
* @protected | ||
*/ | ||
protected _reflectBackOn(): boolean; | ||
/** | ||
* This can be called whenever the view value should be updated. Dependent on component type | ||
* ("input" for <input> or "change" for <select>(mainly for IE)) a different event should be | ||
* used as source for the "user-input-changed" event (which can be seen as an abstraction | ||
* layer on top of other events (input, change, whatever)) | ||
* @protected | ||
*/ | ||
protected _proxyInputEvent(): void; | ||
@@ -42,0 +173,0 @@ protected _onUserInputChanged(): void; |
@@ -18,2 +18,3 @@ import { LitElement, nothing, TemplateResult, CSSResultArray } from '@lion/core'; | ||
formPath: HTMLElement[]; | ||
/** | ||
@@ -28,2 +29,3 @@ * Sometimes it can be helpful to detect whether a value change was caused by a user or | ||
isTriggeredByUser: boolean; | ||
/** | ||
@@ -64,15 +66,19 @@ * Whether it is the first event sent on initialization of the form (other | ||
}; | ||
/** | ||
* A Boolean attribute which, if present, indicates that the user should not be able to edit | ||
* A boolean attribute which, if present, indicates that the user should not be able to edit | ||
* the value of the input. The difference between disabled and readonly is that read-only | ||
* controls can still function, whereas disabled controls generally do not function as | ||
* controls until they are enabled. | ||
* (From: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#attr-readonly) | ||
* See: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#attr-readonly | ||
*/ | ||
readOnly: boolean; | ||
/** | ||
* The name the element will be registered with to the .formElements collection | ||
* of the parent. | ||
* of the parent. Also, it serves as the key of key/value pairs in | ||
* modelValue/serializedValue objects | ||
*/ | ||
name: string; | ||
/** | ||
@@ -92,2 +98,3 @@ * The model value is the result of the parser function(when available). | ||
set modelValue(value: any | Unparseable); | ||
/** | ||
@@ -99,2 +106,3 @@ * The label text for the input node. | ||
set label(arg: string); | ||
/** | ||
@@ -107,5 +115,11 @@ * The helpt text for the input node. | ||
/** | ||
* Will be used in validation messages to refer to the current field | ||
*/ | ||
set fieldName(arg: string); | ||
get fieldName(): string; | ||
/** | ||
* Allows to add extra element references to aria-labelledby attribute. | ||
*/ | ||
addToAriaLabelledBy( | ||
@@ -118,2 +132,6 @@ element: HTMLElement, | ||
): void; | ||
/** | ||
* Allows to add extra element references to aria-describedby attribute. | ||
*/ | ||
addToAriaDescribedBy( | ||
@@ -126,2 +144,6 @@ element: HTMLElement, | ||
): void; | ||
/** | ||
* Allows to remove element references from aria-labelledby attribute. | ||
*/ | ||
removeFromAriaLabelledBy( | ||
@@ -133,2 +155,6 @@ element: HTMLElement, | ||
): void; | ||
/** | ||
* Allows to remove element references from aria-describedby attribute. | ||
*/ | ||
removeFromAriaDescribedBy( | ||
@@ -140,11 +166,42 @@ element: HTMLElement, | ||
): void; | ||
updated(changedProperties: import('@lion/core').PropertyValues): void; | ||
/** | ||
* The interactive (form) element. Can be a native element like input/textarea/select or | ||
* an element with tabindex > -1 | ||
*/ | ||
protected get _inputNode(): HTMLElementWithValue | HTMLInputElement | HTMLTextAreaElement; | ||
/** | ||
* Element where label will be rendered to | ||
*/ | ||
protected get _labelNode(): HTMLElement; | ||
/** | ||
* Element where help text will be rendered to | ||
*/ | ||
protected get _helpTextNode(): HTMLElement; | ||
/** | ||
* Element where validation feedback will be rendered to | ||
*/ | ||
protected get _feedbackNode(): LionValidationFeedback; | ||
/** | ||
* Unique id that can be used in all light dom | ||
*/ | ||
protected _inputId: string; | ||
/** | ||
* Contains all elements that should end up in aria-labelledby of `._inputNode` | ||
* @type {HTMLElement[]} | ||
*/ | ||
protected _ariaLabelledNodes: HTMLElement[]; | ||
/** | ||
* Contains all elements that should end up in aria-describedby of `._inputNode` | ||
*/ | ||
protected _ariaDescribedNodes: HTMLElement[]; | ||
/** | ||
@@ -154,2 +211,3 @@ * Based on the role, details of handling model-value-changed repropagation differ. | ||
protected _repropagationRole: 'child' | 'choice-group' | 'fieldset'; | ||
/** | ||
@@ -156,0 +214,0 @@ * By default, a field with _repropagationRole 'choice-group' will act as an |
@@ -17,18 +17,72 @@ import { Constructor } from '@open-wc/dedupe-mixin'; | ||
export declare class InteractionStateHost { | ||
/** | ||
* True when user has focused and left(blurred) the field. | ||
*/ | ||
touched: boolean; | ||
/** | ||
* True when user has changed the value of the field. | ||
*/ | ||
dirty: boolean; | ||
/** | ||
* True when user has left non-empty field or input is prefilled. | ||
* The name must be seen from the point of view of the input field: | ||
* once the user enters the input field, the value is non-empty. | ||
*/ | ||
prefilled: boolean; | ||
/** | ||
* True when the modelValue is non-empty (see _isEmpty in FormControlMixin) | ||
*/ | ||
filled: boolean; | ||
touched: boolean; | ||
dirty: boolean; | ||
/** | ||
* True when user has attempted to submit the form, e.g. through a button | ||
* of type="submit" | ||
*/ | ||
submitted: boolean; | ||
/** | ||
* Evaluations performed on connectedCallback. | ||
* This method is public, so it can be called at a later moment (when we need to wait for | ||
* registering children for instance) as well. | ||
* Since this method will be called twice in last mentioned scenario, it must stay idempotent. | ||
*/ | ||
initInteractionState(): void; | ||
/** | ||
* Resets touched and dirty, and recomputes prefilled | ||
*/ | ||
resetInteractionState(): void; | ||
connectedCallback(): void; | ||
disconnectedCallback(): void; | ||
/** | ||
* The event that triggers the touched state | ||
*/ | ||
protected _leaveEvent: string; | ||
protected _leaveEvent: string; | ||
/** | ||
* The event that triggers the dirty state | ||
*/ | ||
protected _valueChangedEvent: string; | ||
/** | ||
* Sets touched value to true and reevaluates prefilled state. | ||
* When false, on next interaction, user will start with a clean state. | ||
*/ | ||
protected _iStateOnLeave(): void; | ||
/** | ||
* Sets dirty value and validates when already touched or invalid | ||
*/ | ||
protected _iStateOnValueChange(): void; | ||
/** | ||
* Dispatches event on touched state change | ||
*/ | ||
protected _onTouchedChanged(): void; | ||
/** | ||
* Dispatches event on touched state change | ||
*/ | ||
protected _onDirtyChanged(): void; | ||
@@ -35,0 +89,0 @@ } |
@@ -8,6 +8,23 @@ import { Constructor } from '@open-wc/dedupe-mixin'; | ||
export declare class NativeTextFieldHost { | ||
/** | ||
* Delegates autocomplete to input/textarea | ||
*/ | ||
autocomplete: string; | ||
/** | ||
* Delegates selectionStart to input/textarea | ||
*/ | ||
get selectionStart(): number; | ||
set selectionStart(value: number); | ||
/** | ||
* Delegates selectionEnd to input/textarea | ||
*/ | ||
get selectionEnd(): number; | ||
set selectionEnd(value: number); | ||
/** | ||
* Restores the cursor to its original position after updating the value. | ||
*/ | ||
protected _setValueAndPreserveCaret(value: string): void; | ||
} | ||
@@ -14,0 +31,0 @@ |
@@ -7,3 +7,11 @@ import { Constructor } from '@open-wc/dedupe-mixin'; | ||
export declare class FormRegisteringHost { | ||
/** | ||
* The name the host is registered with to a parent | ||
*/ | ||
name: string; | ||
/** | ||
* The registrar this FormControl registers to, Usually a descendant of FormGroup or | ||
* ChoiceGroup | ||
*/ | ||
protected _parentFormGroup: FormRegistrarHost | undefined; | ||
@@ -10,0 +18,0 @@ } |
@@ -12,3 +12,11 @@ import { Constructor } from '@open-wc/dedupe-mixin'; | ||
export declare class FormRegistrarHost { | ||
/** | ||
* Closely mimics the natively supported HTMLFormControlsCollection. It can be accessed | ||
* both like an array and an object (based on control/element names). | ||
*/ | ||
formElements: FormControlsCollection & { [x: string]: any }; | ||
/** | ||
* Adds FormControl to `.formElements` | ||
*/ | ||
addFormElement( | ||
@@ -21,8 +29,49 @@ child: | ||
): void; | ||
/** | ||
* Removes FormControl from `.formElements` | ||
*/ | ||
removeFormElement(child: FormRegisteringHost): void; | ||
/** | ||
* Whether FormControl is part of `.formElements` | ||
*/ | ||
isRegisteredFormElement(el: FormControlHost): boolean; | ||
/** | ||
* Promise that is resolved by `._completeRegistration`. By default after one microtask, | ||
* so children get the chance to register themselves | ||
*/ | ||
registrationComplete: Promise<boolean>; | ||
/** | ||
* initComplete resolves after all pending initialization logic | ||
* (for instance `<form-group .serializedValue=${{ child1: 'a', child2: 'b' }}>`) | ||
* is executed. | ||
*/ | ||
initComplete: Promise<boolean>; | ||
/** | ||
* Flag that determines how ".formElements" should behave. | ||
* For a regular fieldset (see LionFieldset) we expect ".formElements" | ||
* to be accessible as an object. | ||
* In case of a radio-group, a checkbox-group or a select/listbox, | ||
* it should act like an array (see ChoiceGroupMixin). | ||
* Usually, when false, we deal with a choice-group (radio-group, checkbox-group, | ||
* (multi)select) | ||
*/ | ||
protected _isFormOrFieldset: boolean; | ||
/** | ||
* Hook for Subclassers to perform logic before an element is added | ||
*/ | ||
protected _onRequestToAddFormElement(e: CustomEvent): void; | ||
protected _onRequestToChangeFormElementName(e: CustomEvent): void; | ||
protected _onRequestToRemoveFormElement(e: CustomEvent): void; | ||
/** | ||
* Resolves the registrationComplete promise. Subclassers can delay if needed | ||
*/ | ||
protected _completeRegistration(): void; | ||
@@ -29,0 +78,0 @@ } |
@@ -5,2 +5,6 @@ import { Constructor } from '@open-wc/dedupe-mixin'; | ||
export declare class FormRegistrarPortalHost { | ||
/** | ||
* Registration target: an element, usually in the body of the dom, that captures events | ||
* and redispatches them on host | ||
*/ | ||
registrationTarget: HTMLElement; | ||
@@ -7,0 +11,0 @@ private __redispatchEventForFormRegistrarPortalMixin(ev: CustomEvent): void; |
@@ -12,2 +12,15 @@ import { LitElement } from '@lion/core'; | ||
export declare class SyncUpdatableHost { | ||
/** | ||
* An abstraction that has the exact same api as `requestUpdateInternal`, but taking | ||
* into account: | ||
* - [member order independence](https://github.com/webcomponents/gold-standard/wiki/Member-Order-Independence) | ||
* - property effects start when all (light) dom has initialized (on firstUpdated) | ||
* - property effects don't interrupt the first meaningful paint | ||
* - compatible with propertyAccessor.`hasChanged`: no manual checks needed or accidentally | ||
* run property effects / events when no change happened | ||
* effects when values didn't change | ||
* All code previously present in requestUpdateInternal can be placed in this method. | ||
* @param {string} name | ||
* @param {*} oldValue | ||
*/ | ||
protected updateSync(name: string, oldValue: any): void; | ||
@@ -14,0 +27,0 @@ private __syncUpdatableInitialize(): void; |
@@ -21,9 +21,77 @@ import { LitElement } from '@lion/core'; | ||
export type ValidationType = 'error' | 'warning' | 'info' | 'success' | string; | ||
export declare class ValidateHost { | ||
/** | ||
* Used by Application Developers to add Validators to a FormControl. | ||
* @example | ||
* ```html | ||
* <form-control .validators="${[new Required(), new MinLength(4, {type: 'warning'})]}"> | ||
* </form-control> | ||
* ``` | ||
*/ | ||
validators: Validator[]; | ||
hasFeedbackFor: string[]; | ||
shouldShowFeedbackFor: string[]; | ||
showsFeedbackFor: string[]; | ||
/** | ||
* As soon as validation happens (after modelValue/validators/validator param change), this | ||
* array is updated with the active ValidationTypes ('error'|'warning'|'success'|'info' etc.). | ||
* Notice the difference with `.showsFeedbackFor`, which filters `.hasFeedbackFor` based on | ||
* `.feedbackCondition()`. | ||
* | ||
* For styling purposes, will be reflected to [has-feedback-for="error warning"]. This can | ||
* be useful for subtle visual feedback on keyup, like a red/green border around an input. | ||
* | ||
* @readOnly | ||
* @example | ||
* ```css | ||
* :host([has-feedback-for~="error"]) .input-group__container { | ||
* border: 1px solid red; | ||
* } | ||
* ``` | ||
*/ | ||
hasFeedbackFor: ValidationType[]; | ||
/** | ||
* Based on outcome of feedbackCondition, this array decides what ValidationTypes should be | ||
* shown in validationFeedback, based on meta data like interaction states. | ||
* | ||
* For styling purposes, it reflects it `[shows-feedback-for="error warning"]` | ||
* @readOnly | ||
* @example | ||
* ```css | ||
* :host([shows-feedback-for~="success"]) .form-field__feedback { | ||
* transform: scaleY(1); | ||
* } | ||
* ``` | ||
*/ | ||
showsFeedbackFor: ValidationType[]; | ||
/** | ||
* A temporary storage to transition from hasFeedbackFor to showsFeedbackFor | ||
* @type {ValidationType[]} | ||
* @readOnly | ||
* @private | ||
*/ | ||
private shouldShowFeedbackFor: ValidationType[]; | ||
/** | ||
* The outcome of a validation 'round'. Keyed by ValidationType and Validator name | ||
* @readOnly | ||
*/ | ||
validationStates: { [key: string]: { [key: string]: Object } }; | ||
/** | ||
* Flag indicating whether async validation is pending. | ||
* Creates attribute [is-pending] as a styling hook | ||
*/ | ||
isPending: boolean; | ||
/** | ||
* Used by Subclassers to add default Validators to a particular FormControl. | ||
* A date input for instance, always needs the isDate validator. | ||
* @example | ||
* ```js | ||
* this.defaultValidators.push(new IsDate()); | ||
* ``` | ||
*/ | ||
defaultValidators: Validator[]; | ||
@@ -36,15 +104,93 @@ fieldName: string; | ||
/** | ||
* Triggered by: | ||
* - modelValue change | ||
* - change in the 'validators' array | ||
* - change in the config of an individual Validator | ||
* | ||
* Three situations are handled: | ||
* - a1) the FormControl is empty: further execution is halted. When the Required Validator | ||
* (being mutually exclusive to the other Validators) is applied, it will end up in the | ||
* validation result (as the only Validator, since further execution was halted). | ||
* - a2) there are synchronous Validators: this is the most common flow. When modelValue hasn't | ||
* changed since last async results were generated, 'sync results' are merged with the | ||
* 'async results'. | ||
* - a3) there are asynchronous Validators: for instance when server side evaluation is needed. | ||
* Executions are scheduled and awaited and the 'async results' are merged with the | ||
* 'sync results'. | ||
* | ||
* - b) there are ResultValidators. After steps a1, a2, or a3 are finished, the holistic | ||
* ResultValidators (evaluating the total result of the 'regular' (a1, a2 and a3) validators) | ||
* will be run... | ||
* | ||
* Situations a2 and a3 are not mutually exclusive and can be triggered within one `validate()` | ||
* call. Situation b will occur after every call. | ||
* | ||
* @param {{ clearCurrentResult?: boolean }} [opts] | ||
*/ | ||
validate(opts?: { clearCurrentResult?: boolean }): void; | ||
/** | ||
* The amount of feedback messages that will visible in LionValidationFeedback | ||
*/ | ||
protected _visibleMessagesAmount: number; | ||
/** | ||
* Combination of validators provided by Application Developer and the default validators | ||
*/ | ||
protected _allValidators: Validator[]; | ||
/** | ||
* Allows Subclassers to add meta info for feedbackCondition | ||
*/ | ||
protected get _feedbackConditionMeta(): object; | ||
protected get _feedbackNode(): LionValidationFeedback; | ||
/** | ||
* Responsible for retrieving messages from Validators and | ||
* (delegation of) rendering them. | ||
* | ||
* For `._feedbackNode` (extension of LionValidationFeedback): | ||
* - retrieve messages from highest prio Validators | ||
* - provide the result to custom feedback node and let the | ||
* custom node decide on their renderings | ||
* | ||
* In both cases: | ||
* - we compute the 'show' flag (like 'hasErrorVisible') for all types | ||
* - we set the customValidity message of the highest prio Validator | ||
* - we set aria-invalid="true" in case hasErrorVisible is true | ||
*/ | ||
protected _updateFeedbackComponent(): void; | ||
/** | ||
* Default feedbackCondition condition, used by Subclassers, that will be used when | ||
* `feedbackCondition()` is not overridden by Application Developer. | ||
* Show the validity feedback when returning true, don't show when false | ||
* @param {string} type could be 'error', 'warning', 'info', 'success' or any other custom | ||
* Validator type | ||
* @param {object} meta meta info (interaction states etc) | ||
*/ | ||
protected _showFeedbackConditionFor(type: string, meta: object): boolean; | ||
/** | ||
* Used to translate `.hasFeedbackFor` and `.shouldShowFeedbackFor` to `.showsFeedbackFor` | ||
*/ | ||
protected _hasFeedbackVisibleFor(type: string): boolean; | ||
protected _updateShouldShowFeedbackFor(): void; | ||
/** | ||
* Orders all active validators in this.__validationResult. Can also filter out occurrences | ||
* (based on interaction states). | ||
* | ||
* @example | ||
* ```js | ||
* _prioritizeAndFilterFeedback({ validationResult }) { | ||
* // Put info messages on top; no limitation by `._visibleMessagesAmount` | ||
* const meta = this._feedbackConditionMeta; | ||
* return validationResult.filter(v => | ||
* this.feedbackCondition(v.type, meta, this._showFeedbackConditionFor.bind(this)) | ||
* ).sort((a, b) => a.type === 'info' ? 1 : 0); | ||
* } | ||
* ``` | ||
*/ | ||
protected _prioritizeAndFilterFeedback(opts: { validationResult: Validator[] }): Validator[]; | ||
protected updateSync(name: string, oldValue: unknown): void; | ||
@@ -51,0 +197,0 @@ private __syncValidationResult: Validator[]; |
Sorry, the diff of this file is too big to display
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
703995
2120
20473