import { html, css, nothing, dedupeMixin, SlotMixin } from '@lion/core';
import { ObserverMixin } from '@lion/core/src/ObserverMixin.js';
import { FormRegisteringMixin } from './FormRegisteringMixin.js';
/**
* #FormControlMixin :
*
* This Mixin is a shared fundament for all form components, it's applied on:
* - LionField (which is extended to LionInput, LionTextarea, LionSelect etc. etc.)
* - LionFieldset (which is extended to LionRadioGroup, LionCheckboxGroup, LionForm)
*
* @polymerMixin
* @mixinFunction
*/
export const FormControlMixin = dedupeMixin(
superclass =>
// eslint-disable-next-line no-shadow, no-unused-vars
class FormControlMixin extends FormRegisteringMixin(ObserverMixin(SlotMixin(superclass))) {
static get properties() {
return {
/**
* A list of ids that will be put on the inputElement as a serialized string
*/
_ariaDescribedby: {
type: String,
},
/**
* A list of ids that will be put on the inputElement as a serialized string
*/
_ariaLabelledby: {
type: String,
},
/**
* When no light dom defined and prop set
*/
label: {
type: String,
},
/**
* When no light dom defined and prop set
*/
helpText: {
type: String,
attribute: 'help-text',
},
};
}
get slots() {
return {
...super.slots,
label: () => {
const label = document.createElement('label');
label.textContent = this.label;
return label;
},
'help-text': () => {
const helpText = document.createElement('div');
helpText.textContent = this.helpText;
return helpText;
},
};
}
static get asyncObservers() {
return {
...super.asyncObservers,
_onAriaLabelledbyChanged: ['_ariaLabelledby'],
_onAriaDescribedbyChanged: ['_ariaDescribedby'],
_onLabelChanged: ['label'],
_onHelpTextChanged: ['helpText'],
};
}
/** @deprecated will be this._inputNode in next breaking release */
get inputElement() {
return this.__getDirectSlotChild('input');
}
get _labelNode() {
return this.__getDirectSlotChild('label');
}
get _helpTextNode() {
return this.__getDirectSlotChild('help-text');
}
get _feedbackNode() {
return this.__getDirectSlotChild('feedback');
}
constructor() {
super();
this._inputId = `${this.localName}-${Math.random()
.toString(36)
.substr(2, 10)}`;
this._ariaLabelledby = '';
this._ariaDescribedby = '';
}
connectedCallback() {
super.connectedCallback();
this._enhanceLightDomClasses();
this._enhanceLightDomA11y();
}
/**
* Public methods
*/
_enhanceLightDomClasses() {
if (this.inputElement) {
this.inputElement.classList.add('form-control');
}
}
_enhanceLightDomA11y() {
const { inputElement, _labelNode, _helpTextNode, _feedbackNode } = this;
if (inputElement) {
inputElement.id = inputElement.id || this._inputId;
}
if (_labelNode) {
_labelNode.setAttribute('for', this._inputId);
_labelNode.id = _labelNode.id || `label-${this._inputId}`;
const labelledById = ` ${_labelNode.id}`;
if (this._ariaLabelledby.indexOf(labelledById) === -1) {
this._ariaLabelledby += ` ${_labelNode.id}`;
}
}
if (_helpTextNode) {
_helpTextNode.id = _helpTextNode.id || `help-text-${this._inputId}`;
const describeIdHelpText = ` ${_helpTextNode.id}`;
if (this._ariaDescribedby.indexOf(describeIdHelpText) === -1) {
this._ariaDescribedby += ` ${_helpTextNode.id}`;
}
}
if (_feedbackNode) {
_feedbackNode.id = _feedbackNode.id || `feedback-${this._inputId}`;
const describeIdFeedback = ` ${_feedbackNode.id}`;
if (this._ariaDescribedby.indexOf(describeIdFeedback) === -1) {
this._ariaDescribedby += ` ${_feedbackNode.id}`;
}
}
this._enhanceLightDomA11yForAdditionalSlots();
}
/**
* Enhances additional slots(prefix, suffix, before, after) defined by developer.
*
* When boolean attribute data-label or data-description is found,
* the slot element will be connected to the input via aria-labelledby or aria-describedby
*/
_enhanceLightDomA11yForAdditionalSlots(
additionalSlots = ['prefix', 'suffix', 'before', 'after'],
) {
additionalSlots.forEach(additionalSlot => {
const element = this.__getDirectSlotChild(additionalSlot);
if (element) {
element.id = element.id || `${additionalSlot}-${this._inputId}`;
if (element.hasAttribute('data-label') === true) {
this._ariaLabelledby += ` ${element.id}`;
}
if (element.hasAttribute('data-description') === true) {
this._ariaDescribedby += ` ${element.id}`;
}
}
});
}
/**
* Will handle label, prefix/suffix/before/after (if they contain data-label flag attr).
* Also, contents of id references that will be put in the ._ariaLabelledby property
* from an external context, will be read by a screen reader.
*/
_onAriaLabelledbyChanged({ _ariaLabelledby }) {
if (this.inputElement) {
this.inputElement.setAttribute('aria-labelledby', _ariaLabelledby);
}
}
/**
* Will handle help text, validation feedback and character counter,
* prefix/suffix/before/after (if they contain data-description flag attr).
* Also, contents of id references that will be put in the ._ariaDescribedby property
* from an external context, will be read by a screen reader.
*/
_onAriaDescribedbyChanged({ _ariaDescribedby }) {
if (this.inputElement) {
this.inputElement.setAttribute('aria-describedby', _ariaDescribedby);
}
}
_onLabelChanged({ label }) {
if (this._labelNode) {
this._labelNode.textContent = label;
}
}
_onHelpTextChanged({ helpText }) {
if (this._helpTextNode) {
this._helpTextNode.textContent = helpText;
}
}
/**
*
* Default Render Result:
*
`;
}
/**
* All CSS below is written from a generic mindset, following BEM conventions:
* https://en.bem.info/methodology/
* Although the CSS and HTML are implemented by the component, they should be regarded as
* totally decoupled.
*
* Not only does this force us to write better structured css, it also allows for future
* reusability in many different ways like:
* - disabling shadow DOM for a component (for water proof encapsulation can be combined with
* a build step)
* - easier translation to more flexible, WebComponents agnostic solutions like JSS
* (allowing extends, mixins, reasoning, IDE integration, tree shaking etc.)
* - export to a CSS module for reuse in an outer context
*
*
* Please note that the HTML structure is purposely 'loose', allowing multiple design systems
* to be compatible
* with the CSS component.
* Note that every occurence of '::slotted(*)' can be rewritten to '> *' for use in an other
* context
*
* TODO: find best naming convention: https://en.bem.info/methodology/naming-convention/
* (react style would align better with JSS)
*/
/**
* {block} .form-field
*
* Structure:
* - {element} .form-field__label : a wrapper element around the projected label
* - {element} .form-field__help-text (optional) : a wrapper element around the projected
* help-text
* - {block} .input-group : a container around the input element, including prefixes and
* suffixes
* - {element} .form-field__feedback (optional) : a wrapper element around the projected
* (validation) feedback message
*
* Modifiers:
* - {state} .state-disabled : when .form-control (,