Socket
Socket
Sign inDemoInstall

@lion/form-core

Package Overview
Dependencies
Maintainers
1
Versions
73
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@lion/form-core - npm Package Compare versions

Comparing version 0.12.0 to 0.13.0

10

CHANGELOG.md
# 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 @@

2

docs/overview.md

@@ -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

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc