Skip to content

Commit

Permalink
Merge pull request #76 from github/validate-after-first-blur
Browse files Browse the repository at this point in the history
Align behavior with Primer guidance behind only-validate-on-blur toggle
  • Loading branch information
joelhawksley authored Nov 26, 2024
2 parents fde00ab + 8008229 commit 32051d9
Show file tree
Hide file tree
Showing 6 changed files with 151 additions and 10 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,8 @@ npm install
npm test
```
For local development, uncomment the line at the bottom of `examples/index` and serve the page using `npx serve`.
## License
Distributed under the MIT license. See LICENSE for details.
15 changes: 15 additions & 0 deletions custom-elements.json
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,21 @@
"text": "string"
},
"readonly": true
},
{
"kind": "field",
"name": "validateOnKeystroke",
"type": {
"text": "boolean"
}
},
{
"kind": "field",
"name": "onlyValidateOnBlur",
"type": {
"text": "boolean"
},
"readonly": true
}
],
"attributes": [
Expand Down
22 changes: 17 additions & 5 deletions examples/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@
<main>
<h1>auto-check-element</h1>
<h2>Simple form</h2>
<p>Input 422 for an error response.</p>
<h2 tabindex="-1" id="success1" class="success" hidden>Your submission was successful</h2>
<form>
<p>All fields marked with * are required</p>
Expand All @@ -38,6 +37,19 @@ <h2 tabindex="-1" id="success2" class="success" hidden>Your submission was succe
</auto-check>
<button value="2" name="form">submit</button>
</form>

<h2>only-validate-on-blur with custom validity messages</h2>
<h2 tabindex="-1" id="success3" class="success" hidden>Your submission was successful</h2>
<form id="custom2">
<p>All fields marked with * are required</p>

<label for="simple-field2">Desired username*:</label>
<auto-check csrf="foo" src="/demo" required only-validate-on-blur>
<input id="simple-field2" autofocus name="foo" required aria-describedby="state3" />
<p id="state3" aria-atomic="true" aria-live="polite" class="state"></p>
</auto-check>
<button value="3" name="form">submit</button>
</form>
</main>

<script>
Expand Down Expand Up @@ -74,7 +86,7 @@ <h2 tabindex="-1" id="success2" class="success" hidden>Your submission was succe
})

form.addEventListener('auto-check-start', () => {
if (form.id === 'custom') {
if (form.id.includes('custom')) {
const {setValidity} = event.detail
setValidity('🔍 Checking validity...')
}
Expand All @@ -84,7 +96,7 @@ <h2 tabindex="-1" id="success2" class="success" hidden>Your submission was succe
state.textContent = 'succeeded'
})
form.addEventListener('auto-check-error', event => {
if (form.id === 'custom') {
if (form.id.includes('custom')) {
const {setValidity} = event.detail
setValidity('🚫 Something went wrong. Please try again')
}
Expand All @@ -96,7 +108,7 @@ <h2 tabindex="-1" id="success2" class="success" hidden>Your submission was succe
}
</script>

<script type="module" src="https://unpkg.com/@github/auto-check-element@latest"></script>
<!-- <script type="module" src="../dist/index.js" defer></script> -->
<!-- <script type="module" src="https://unpkg.com/@github/auto-check-element@latest"></script> -->
<script type="module" src="../dist/bundle.js" defer></script>
</body>
</html>
6 changes: 3 additions & 3 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

53 changes: 51 additions & 2 deletions src/auto-check-element.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,8 +105,10 @@ export class AutoCheckElement extends HTMLElement {
const state = {check: checker, controller: null}
states.set(this, state)

input.addEventListener('input', setLoadingState)
input.addEventListener('input', checker)
const changeHandler = handleChange.bind(null, checker)

input.addEventListener('blur', changeHandler)
input.addEventListener('input', changeHandler)
input.autocomplete = 'off'
input.spellcheck = false
}
Expand Down Expand Up @@ -185,6 +187,43 @@ export class AutoCheckElement extends HTMLElement {
get httpMethod(): string {
return AllowedHttpMethods[this.getAttribute('http-method') as keyof typeof AllowedHttpMethods] || 'POST'
}

set validateOnKeystroke(enabled: boolean) {
if (enabled) {
this.setAttribute('validate-on-keystroke', '')
} else {
this.removeAttribute('validate-on-keystroke')
}
}

get validateOnKeystroke(): boolean {
const value = this.getAttribute('validate-on-keystroke')
return value === 'true' || value === ''
}

get onlyValidateOnBlur(): boolean {
const value = this.getAttribute('only-validate-on-blur')
return value === 'true' || value === ''
}
}

function handleChange(checker: () => void, event: Event) {
const input = event.currentTarget
if (!(input instanceof HTMLInputElement)) return

const autoCheckElement = input.closest('auto-check')
if (!(autoCheckElement instanceof AutoCheckElement)) return

if (input.value.length === 0) return

if (
(event.type !== 'blur' && !autoCheckElement.onlyValidateOnBlur) || // Existing default behavior
(event.type === 'blur' && autoCheckElement.onlyValidateOnBlur) || // Only validate on blur if only-validate-on-blur is set
(autoCheckElement.onlyValidateOnBlur && autoCheckElement.validateOnKeystroke) // Only validate on key inputs in only-validate-on-blur mode if validate-on-keystroke is set (when input is invalid)
) {
setLoadingState(event)
checker()
}
}

function setLoadingState(event: Event) {
Expand Down Expand Up @@ -298,8 +337,18 @@ async function check(autoCheckElement: AutoCheckElement) {
if (autoCheckElement.required) {
input.setCustomValidity('')
}
// We do not have good test coverage for this code path.
// To test, ensure that the input only validates on blur
// once it has been "healed" by a valid input after
// previously being in an invalid state.
if (autoCheckElement.onlyValidateOnBlur) {
autoCheckElement.validateOnKeystroke = false
}
input.dispatchEvent(new AutoCheckSuccessEvent(response.clone()))
} else {
if (autoCheckElement.onlyValidateOnBlur) {
autoCheckElement.validateOnKeystroke = true
}
const event = new AutoCheckErrorEvent(response.clone())
input.dispatchEvent(event)
if (autoCheckElement.required) {
Expand Down
63 changes: 63 additions & 0 deletions test/auto-check.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,65 @@ describe('auto-check element', function () {
})
})

describe('when only-validate-on-blur is true', function () {
let checker
let input

beforeEach(function () {
const container = document.createElement('div')
container.innerHTML = `
<auto-check csrf="foo" src="/success" only-validate-on-blur>
<input>
</auto-check>`
document.body.append(container)

checker = document.querySelector('auto-check')
input = checker.querySelector('input')
})

it('does not emit on initial input change', async function () {
const events = []
input.addEventListener('auto-check-start', event => events.push(event.type))
triggerInput(input, 'hub')
assert.deepEqual(events, [])
})

it('does not emit on blur if input is blank', async function () {
const events = []
input.addEventListener('auto-check-start', event => events.push(event.type))
triggerBlur(input)
assert.deepEqual(events, [])
})

it('emits on blur', async function () {
const events = []
input.addEventListener('auto-check-start', event => events.push(event.type))
triggerInput(input, 'hub')
triggerBlur(input)
assert.deepEqual(events, ['auto-check-start'])
})

it('emits on input change if input is invalid after blur', async function () {
const events = []
input.addEventListener('auto-check-start', event => events.push(event.type))

checker.src = '/fail'
triggerInput(input, 'hub')
triggerBlur(input)
await once(input, 'auto-check-complete')
triggerInput(input, 'hub2')
triggerInput(input, 'hub3')

assert.deepEqual(events, ['auto-check-start', 'auto-check-start', 'auto-check-start'])
})

afterEach(function () {
document.body.innerHTML = ''
checker = null
input = null
})
})

describe('required attribute', function () {
let checker
let input
Expand Down Expand Up @@ -331,3 +390,7 @@ function triggerInput(input, value) {
input.value = value
return input.dispatchEvent(new InputEvent('input'))
}

function triggerBlur(input) {
return input.dispatchEvent(new FocusEvent('blur'))
}

0 comments on commit 32051d9

Please sign in to comment.