@microsoft/atlas-js
Advanced tools
Comparing version 1.3.0 to 1.4.0
@@ -0,1 +1,10 @@ | ||
export declare const defaultMessageStrings: { | ||
contentHasChanged: string; | ||
inputMaxLength: string; | ||
inputMinLength: string; | ||
inputRequired: string; | ||
pleaseFixTheFollowingIssues: string; | ||
thereAreNoEditsToSubmit: string; | ||
weEncounteredAnUnexpectedError: string; | ||
}; | ||
declare class FormBehaviorElement extends HTMLElement { | ||
@@ -15,3 +24,2 @@ submitting: boolean; | ||
weEncounteredAnUnexpectedError: string; | ||
youMustSelectBetweenMinAndMaxTags: string; | ||
} & { | ||
@@ -46,7 +54,6 @@ [key: string]: string; | ||
validateMaxLength(input: HTMLValueElement, label: string): string | null; | ||
validateTagSelector(input: HTMLInputElement | HTMLValueElement, label: string): string | null; | ||
validateForm(form: HTMLFormElement, displayValidity?: boolean, scope?: Element): Promise<FormValidationResult>; | ||
clearValidationErrors(target: EventTarget | null): void; | ||
showNoChangesMessage(form: HTMLFormElement): void; | ||
runBasicValidation(input: Element, displayValidity: boolean | undefined, errors: FormValidationError[], errorList: HTMLElement, isTagSelector: boolean, isCustomElement: boolean): void; | ||
runBasicValidation(input: Element, displayValidity: boolean | undefined, errors: FormValidationError[], errorList: HTMLElement, isCustomElement: boolean): void; | ||
} | ||
@@ -69,3 +76,3 @@ declare global { | ||
} | ||
declare type NavigationSteps = 'follow' | 'hash-reload' | 'replace' | 'reload' | null; | ||
export declare type NavigationSteps = 'follow' | 'hash-reload' | 'replace' | 'reload' | null; | ||
export interface FormValidationError { | ||
@@ -72,0 +79,0 @@ message: string; |
import { generateElementId, kebabToCamelCase } from '../utilities/util'; | ||
const defaultMessageStrings = { | ||
export const defaultMessageStrings = { | ||
contentHasChanged: 'Content has changed, please reload the page to get the latest changes.', | ||
@@ -9,4 +9,3 @@ inputMaxLength: '{inputLabel} cannot be longer than {maxLength} characters.', | ||
thereAreNoEditsToSubmit: 'There are no edits to submit.', | ||
weEncounteredAnUnexpectedError: 'We encountered an unexpected error. Please try again later. If this issue continues, please contact site support.', | ||
youMustSelectBetweenMinAndMaxTags: 'You must select between {min} and {max} {tagLabel}.' | ||
weEncounteredAnUnexpectedError: 'We encountered an unexpected error. Please try again later. If this issue continues, please contact site support.' | ||
}; | ||
@@ -29,4 +28,3 @@ // <form-behavior> | ||
this.validateRequired.bind(this), | ||
this.validateMaxLength.bind(this), | ||
this.validateTagSelector.bind(this) | ||
this.validateMaxLength.bind(this) | ||
]; | ||
@@ -54,3 +52,3 @@ constructor() { | ||
const errorSummaryContainer = document.createElement('div'); | ||
errorSummaryContainer.classList.add('form-error-container', 'margin-bottom-sm'); | ||
errorSummaryContainer.classList.add('form-error-container'); | ||
this.insertAdjacentElement('afterend', errorSummaryContainer); | ||
@@ -138,10 +136,5 @@ this.initialData = new FormData(form); | ||
const form = event.currentTarget; | ||
const validationErrorEvent = new CustomEvent('validationerror', { | ||
bubbles: true, | ||
cancelable: true | ||
}); | ||
// reject the submit if no edits have been made (overridable with the new attribute) | ||
if (!this.canSave) { | ||
this.showNoChangesMessage(form); | ||
this.dispatchEvent(validationErrorEvent); | ||
return; | ||
@@ -152,5 +145,4 @@ } | ||
setBusySubmitButton(form, this.submitting); | ||
const valid = await this.validateForm(form); | ||
if (!valid.valid) { | ||
this.dispatchEvent(validationErrorEvent); | ||
const result = await this.validateForm(form); | ||
if (!result.valid) { | ||
return; | ||
@@ -212,2 +204,9 @@ } | ||
} | ||
this.dispatchEvent(new CustomEvent('submission-error', { | ||
detail: { | ||
request, | ||
response | ||
}, | ||
bubbles: true | ||
})); | ||
errorList.appendChild(errorText); | ||
@@ -225,3 +224,2 @@ errorAlert.hidden = false; | ||
const formLayout = form.querySelector('.form-error-container') || form; | ||
formLayout.setAttribute('role', 'alert'); | ||
const alertId = generateElementId(); | ||
@@ -282,20 +280,2 @@ const errorAlert = document.createElement('div'); | ||
} | ||
validateTagSelector(input, label) { | ||
if (input instanceof HTMLInputElement && input.classList.contains('tag-input')) { | ||
const min = input.getAttribute('minTags'); | ||
const max = input.getAttribute('maxTags'); | ||
const tagCount = input.value === '' ? 0 : input.value.split(',').length; | ||
// if no min or max, no need to validate | ||
if (!min || !max) { | ||
return null; | ||
} | ||
if (!tagCount || tagCount < Number(min) || tagCount > Number(max)) { | ||
return `${this.locStrings.youMustSelectBetweenMinAndMaxTags | ||
.replace('{min}', min) | ||
.replace('{max}', max) | ||
.replace('{tagLabel}', label.toLocaleLowerCase())}`; | ||
} | ||
} | ||
return null; | ||
} | ||
async validateForm(form, displayValidity = true, scope = form) { | ||
@@ -313,4 +293,3 @@ const errors = []; | ||
} | ||
const isTagSelector = input.classList.contains('tag-input'); | ||
if (input.hasAttribute('aria-hidden') && !isTagSelector) { | ||
if (input.hasAttribute('aria-hidden')) { | ||
continue; | ||
@@ -327,3 +306,11 @@ } | ||
const isCustomElement = !!customElements.find(el => el === input); | ||
this.runBasicValidation(input, displayValidity, errors, errorList, isTagSelector, isCustomElement); | ||
this.runBasicValidation(input, displayValidity, errors, errorList, isCustomElement); | ||
const validationErrorEvent = new CustomEvent('form-validating', { | ||
detail: { | ||
errors, | ||
form | ||
}, | ||
bubbles: true | ||
}); | ||
this.dispatchEvent(validationErrorEvent); | ||
} | ||
@@ -334,2 +321,3 @@ if (errors.length === 0) { | ||
if (displayValidity) { | ||
errorAlert.classList.add('margin-bottom-sm'); | ||
errorAlert.hidden = false; | ||
@@ -356,2 +344,6 @@ errorAlert.focus(); | ||
} | ||
const clearValidationEvent = new CustomEvent('clear-validation-errors', { | ||
bubbles: true | ||
}); | ||
this.dispatchEvent(clearValidationEvent); | ||
} | ||
@@ -374,3 +366,3 @@ showNoChangesMessage(form) { | ||
} | ||
runBasicValidation(input, displayValidity = true, errors, errorList, isTagSelector, isCustomElement) { | ||
runBasicValidation(input, displayValidity = true, errors, errorList, isCustomElement) { | ||
if (!canValidate(input, this.form)) { | ||
@@ -395,6 +387,3 @@ return; | ||
if (displayValidity) { | ||
const inputId = isTagSelector | ||
? input.parentElement?.querySelector('input.autocomplete-input')?.id | ||
: input.id; | ||
if (!inputId) { | ||
if (!input.id) { | ||
continue; | ||
@@ -407,11 +396,17 @@ } | ||
const a = document.createElement('a'); | ||
a.href = `#${inputId}`; | ||
a.href = `#${input.id}`; | ||
a.textContent = message; | ||
a.classList.add('help', 'help-danger'); | ||
// ensure focus is set on the custom element | ||
a.addEventListener('click', e => { | ||
if (isCustomElement) { | ||
const target = e.target.getAttribute('href'); | ||
if (target) { | ||
document.querySelector(target).focus(); | ||
} | ||
} | ||
}); | ||
child.appendChild(a); | ||
errorList.appendChild(child); | ||
if (!isCustomElement) { | ||
if (isTagSelector) { | ||
input.nextElementSibling?.classList.add('border-color-danger'); | ||
} | ||
input.classList.add(`${input.localName}-danger`); | ||
@@ -536,5 +531,6 @@ } | ||
const formData = Object.fromEntries(new FormData(form)); | ||
const formElements = Object.keys(form.elements); | ||
const customElementList = []; | ||
const customElements = Object.keys(formData).filter(el => formElements.indexOf(el) === -1); | ||
const customElements = Object.keys(formData).filter(key => { | ||
return !form.elements.namedItem(key); | ||
}); | ||
customElements.forEach(name => { | ||
@@ -549,6 +545,2 @@ const element = form.querySelector(`[name="${name}"]`); | ||
function clearInputErrorBorder(input) { | ||
const isTagSelector = input.classList.contains('tag-input'); | ||
if (isTagSelector) { | ||
input.nextElementSibling?.classList.remove('border-color-danger'); | ||
} | ||
input.classList.remove(`${input.localName}-danger`); | ||
@@ -555,0 +547,0 @@ } |
@@ -24,2 +24,3 @@ export declare class StarRatingElement extends HTMLElement { | ||
updateStarFill(newValue: number): void; | ||
focus(): void; | ||
} | ||
@@ -26,0 +27,0 @@ declare global { |
@@ -84,3 +84,4 @@ const starRatingTemplate = document.createElement('template'); | ||
input:focus-visible + label { | ||
input:focus-visible + label, | ||
input:focus + label { | ||
border-radius: 0.5em; | ||
@@ -186,3 +187,3 @@ outline: 3px dashed; | ||
static get observedAttributes() { | ||
return ['disabled', 'name', 'required', 'value']; | ||
return ['disabled', 'name', 'required', 'value', 'focus', 'blur']; | ||
} | ||
@@ -250,2 +251,4 @@ coercedValue = ''; | ||
: 'star rating'); | ||
this.addEventListener('focus', this); | ||
this.addEventListener('blur', this); | ||
} | ||
@@ -286,2 +289,5 @@ disconnectedCallback() { | ||
switch (event.type) { | ||
case 'blur': | ||
this.removeAttribute('tabindex'); | ||
break; | ||
case 'change': | ||
@@ -292,2 +298,7 @@ const target = event.target; | ||
break; | ||
case 'focus': | ||
if (!this.shadowRoot?.activeElement) { | ||
this.shadowRoot?.querySelectorAll('input')[0].focus(); | ||
} | ||
break; | ||
case 'slotchange': | ||
@@ -333,2 +344,5 @@ const slot = event.target; | ||
} | ||
focus() { | ||
this.setAttribute('tabindex', '-1'); | ||
} | ||
} | ||
@@ -335,0 +349,0 @@ if (!window.customElements.get('star-rating')) { |
export * from './behaviors/popover'; | ||
export * from './behaviors/tag-inputs'; | ||
export * from './elements/form-behavior'; | ||
export * from './elements/star-rating'; | ||
//# sourceMappingURL=index.d.ts.map |
export * from './behaviors/popover'; | ||
export * from './behaviors/tag-inputs'; | ||
export * from './elements/form-behavior'; | ||
export * from './elements/star-rating'; |
{ | ||
"name": "@microsoft/atlas-js", | ||
"version": "1.3.0", | ||
"version": "1.4.0", | ||
"public": true, | ||
@@ -5,0 +5,0 @@ "description": "Scripts backing the Atlas Design System used by Microsoft's Developer Relations.", |
import { generateElementId, kebabToCamelCase } from '../utilities/util'; | ||
const defaultMessageStrings = { | ||
export const defaultMessageStrings = { | ||
contentHasChanged: 'Content has changed, please reload the page to get the latest changes.', | ||
@@ -11,4 +11,3 @@ inputMaxLength: '{inputLabel} cannot be longer than {maxLength} characters.', | ||
weEncounteredAnUnexpectedError: | ||
'We encountered an unexpected error. Please try again later. If this issue continues, please contact site support.', | ||
youMustSelectBetweenMinAndMaxTags: 'You must select between {min} and {max} {tagLabel}.' | ||
'We encountered an unexpected error. Please try again later. If this issue continues, please contact site support.' | ||
}; | ||
@@ -36,4 +35,3 @@ // <form-behavior> | ||
this.validateRequired.bind(this), | ||
this.validateMaxLength.bind(this), | ||
this.validateTagSelector.bind(this) | ||
this.validateMaxLength.bind(this) | ||
]; | ||
@@ -67,3 +65,3 @@ | ||
const errorSummaryContainer = document.createElement('div'); | ||
errorSummaryContainer.classList.add('form-error-container', 'margin-bottom-sm'); | ||
errorSummaryContainer.classList.add('form-error-container'); | ||
this.insertAdjacentElement('afterend', errorSummaryContainer); | ||
@@ -174,6 +172,2 @@ | ||
const form = event.currentTarget as HTMLFormElement; | ||
const validationErrorEvent = new CustomEvent('validationerror', { | ||
bubbles: true, | ||
cancelable: true | ||
}); | ||
@@ -183,3 +177,2 @@ // reject the submit if no edits have been made (overridable with the new attribute) | ||
this.showNoChangesMessage(form); | ||
this.dispatchEvent(validationErrorEvent); | ||
return; | ||
@@ -191,5 +184,4 @@ } | ||
setBusySubmitButton(form, this.submitting); | ||
const valid = await this.validateForm(form); | ||
if (!valid.valid) { | ||
this.dispatchEvent(validationErrorEvent); | ||
const result = await this.validateForm(form); | ||
if (!result.valid) { | ||
return; | ||
@@ -259,2 +251,11 @@ } | ||
} | ||
this.dispatchEvent( | ||
new CustomEvent('submission-error', { | ||
detail: { | ||
request, | ||
response | ||
}, | ||
bubbles: true | ||
}) | ||
); | ||
@@ -276,3 +277,2 @@ errorList.appendChild(errorText); | ||
const formLayout = form.querySelector('.form-error-container') || form; | ||
formLayout.setAttribute('role', 'alert'); | ||
const alertId = generateElementId(); | ||
@@ -350,23 +350,2 @@ | ||
validateTagSelector(input: HTMLInputElement | HTMLValueElement, label: string): string | null { | ||
if (input instanceof HTMLInputElement && input.classList.contains('tag-input')) { | ||
const min = input.getAttribute('minTags'); | ||
const max = input.getAttribute('maxTags'); | ||
const tagCount = input.value === '' ? 0 : input.value.split(',').length; | ||
// if no min or max, no need to validate | ||
if (!min || !max) { | ||
return null; | ||
} | ||
if (!tagCount || tagCount < Number(min) || tagCount > Number(max)) { | ||
return `${this.locStrings.youMustSelectBetweenMinAndMaxTags | ||
.replace('{min}', min) | ||
.replace('{max}', max) | ||
.replace('{tagLabel}', label.toLocaleLowerCase())}`; | ||
} | ||
} | ||
return null; | ||
} | ||
async validateForm( | ||
@@ -391,4 +370,3 @@ form: HTMLFormElement, | ||
const isTagSelector = input.classList.contains('tag-input'); | ||
if (input.hasAttribute('aria-hidden') && !isTagSelector) { | ||
if (input.hasAttribute('aria-hidden')) { | ||
continue; | ||
@@ -409,10 +387,11 @@ } | ||
this.runBasicValidation( | ||
input, | ||
displayValidity, | ||
errors, | ||
errorList, | ||
isTagSelector, | ||
isCustomElement | ||
); | ||
this.runBasicValidation(input, displayValidity, errors, errorList, isCustomElement); | ||
const validationErrorEvent = new CustomEvent('form-validating', { | ||
detail: { | ||
errors, | ||
form | ||
}, | ||
bubbles: true | ||
}); | ||
this.dispatchEvent(validationErrorEvent); | ||
} | ||
@@ -425,2 +404,3 @@ | ||
if (displayValidity) { | ||
errorAlert.classList.add('margin-bottom-sm'); | ||
errorAlert.hidden = false; | ||
@@ -453,2 +433,7 @@ errorAlert.focus(); | ||
} | ||
const clearValidationEvent = new CustomEvent('clear-validation-errors', { | ||
bubbles: true | ||
}); | ||
this.dispatchEvent(clearValidationEvent); | ||
} | ||
@@ -479,3 +464,2 @@ | ||
errorList: HTMLElement, | ||
isTagSelector: boolean, | ||
isCustomElement: boolean | ||
@@ -506,7 +490,3 @@ ) { | ||
if (displayValidity) { | ||
const inputId = isTagSelector | ||
? input.parentElement?.querySelector('input.autocomplete-input')?.id | ||
: input.id; | ||
if (!inputId) { | ||
if (!input.id) { | ||
continue; | ||
@@ -521,6 +501,16 @@ } | ||
const a = document.createElement('a'); | ||
a.href = `#${inputId}`; | ||
a.href = `#${input.id}`; | ||
a.textContent = message; | ||
a.classList.add('help', 'help-danger'); | ||
// ensure focus is set on the custom element | ||
a.addEventListener('click', e => { | ||
if (isCustomElement) { | ||
const target = (e.target as HTMLAnchorElement).getAttribute('href'); | ||
if (target) { | ||
(document.querySelector(target) as HTMLElement).focus(); | ||
} | ||
} | ||
}); | ||
child.appendChild(a); | ||
@@ -530,5 +520,2 @@ errorList.appendChild(child); | ||
if (!isCustomElement) { | ||
if (isTagSelector) { | ||
input.nextElementSibling?.classList.add('border-color-danger'); | ||
} | ||
input.classList.add(`${input.localName}-danger`); | ||
@@ -575,6 +562,5 @@ } | ||
weEncounteredAnUnexpectedError: string; | ||
youMustSelectBetweenMinAndMaxTags: string; | ||
} | ||
type NavigationSteps = 'follow' | 'hash-reload' | 'replace' | 'reload' | null; | ||
export type NavigationSteps = 'follow' | 'hash-reload' | 'replace' | 'reload' | null; | ||
@@ -730,5 +716,7 @@ export interface FormValidationError { | ||
const formData = Object.fromEntries(new FormData(form)); | ||
const formElements = Object.keys(form.elements); | ||
const customElementList: Element[] = []; | ||
const customElements = Object.keys(formData).filter(el => formElements.indexOf(el) === -1); | ||
const customElements = Object.keys(formData).filter(key => { | ||
return !form.elements.namedItem(key); | ||
}); | ||
customElements.forEach(name => { | ||
@@ -744,6 +732,2 @@ const element = form.querySelector(`[name="${name}"]`); | ||
function clearInputErrorBorder(input: HTMLValueElement) { | ||
const isTagSelector = input.classList.contains('tag-input'); | ||
if (isTagSelector) { | ||
input.nextElementSibling?.classList.remove('border-color-danger'); | ||
} | ||
input.classList.remove(`${input.localName}-danger`); | ||
@@ -750,0 +734,0 @@ } |
@@ -86,3 +86,4 @@ const starRatingTemplate = document.createElement('template'); | ||
input:focus-visible + label { | ||
input:focus-visible + label, | ||
input:focus + label { | ||
border-radius: 0.5em; | ||
@@ -190,3 +191,3 @@ outline: 3px dashed; | ||
static get observedAttributes() { | ||
return ['disabled', 'name', 'required', 'value']; | ||
return ['disabled', 'name', 'required', 'value', 'focus', 'blur']; | ||
} | ||
@@ -273,2 +274,4 @@ | ||
); | ||
this.addEventListener('focus', this); | ||
this.addEventListener('blur', this); | ||
} | ||
@@ -315,2 +318,5 @@ | ||
switch (event.type) { | ||
case 'blur': | ||
this.removeAttribute('tabindex'); | ||
break; | ||
case 'change': | ||
@@ -321,2 +327,7 @@ const target = event.target as HTMLInputElement; | ||
break; | ||
case 'focus': | ||
if (!this.shadowRoot?.activeElement) { | ||
this.shadowRoot?.querySelectorAll('input')[0].focus(); | ||
} | ||
break; | ||
case 'slotchange': | ||
@@ -365,2 +376,6 @@ const slot = event.target as HTMLSlotElement; | ||
} | ||
focus() { | ||
this.setAttribute('tabindex', '-1'); | ||
} | ||
} | ||
@@ -367,0 +382,0 @@ |
export * from './behaviors/popover'; | ||
export * from './behaviors/tag-inputs'; | ||
export * from './elements/form-behavior'; | ||
export * from './elements/star-rating'; |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
78843
22
2057