Skip to content

Commit 32051d9

Browse files
authored
Merge pull request #76 from github/validate-after-first-blur
Align behavior with Primer guidance behind only-validate-on-blur toggle
2 parents fde00ab + 8008229 commit 32051d9

6 files changed

+151
-10
lines changed

README.md

+2
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,8 @@ npm install
158158
npm test
159159
```
160160
161+
For local development, uncomment the line at the bottom of `examples/index` and serve the page using `npx serve`.
162+
161163
## License
162164
163165
Distributed under the MIT license. See LICENSE for details.

custom-elements.json

+15
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,21 @@
198198
"text": "string"
199199
},
200200
"readonly": true
201+
},
202+
{
203+
"kind": "field",
204+
"name": "validateOnKeystroke",
205+
"type": {
206+
"text": "boolean"
207+
}
208+
},
209+
{
210+
"kind": "field",
211+
"name": "onlyValidateOnBlur",
212+
"type": {
213+
"text": "boolean"
214+
},
215+
"readonly": true
201216
}
202217
],
203218
"attributes": [

examples/index.html

+17-5
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@
1212
<main>
1313
<h1>auto-check-element</h1>
1414
<h2>Simple form</h2>
15-
<p>Input 422 for an error response.</p>
1615
<h2 tabindex="-1" id="success1" class="success" hidden>Your submission was successful</h2>
1716
<form>
1817
<p>All fields marked with * are required</p>
@@ -38,6 +37,19 @@ <h2 tabindex="-1" id="success2" class="success" hidden>Your submission was succe
3837
</auto-check>
3938
<button value="2" name="form">submit</button>
4039
</form>
40+
41+
<h2>only-validate-on-blur with custom validity messages</h2>
42+
<h2 tabindex="-1" id="success3" class="success" hidden>Your submission was successful</h2>
43+
<form id="custom2">
44+
<p>All fields marked with * are required</p>
45+
46+
<label for="simple-field2">Desired username*:</label>
47+
<auto-check csrf="foo" src="/demo" required only-validate-on-blur>
48+
<input id="simple-field2" autofocus name="foo" required aria-describedby="state3" />
49+
<p id="state3" aria-atomic="true" aria-live="polite" class="state"></p>
50+
</auto-check>
51+
<button value="3" name="form">submit</button>
52+
</form>
4153
</main>
4254

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

7688
form.addEventListener('auto-check-start', () => {
77-
if (form.id === 'custom') {
89+
if (form.id.includes('custom')) {
7890
const {setValidity} = event.detail
7991
setValidity('🔍 Checking validity...')
8092
}
@@ -84,7 +96,7 @@ <h2 tabindex="-1" id="success2" class="success" hidden>Your submission was succe
8496
state.textContent = 'succeeded'
8597
})
8698
form.addEventListener('auto-check-error', event => {
87-
if (form.id === 'custom') {
99+
if (form.id.includes('custom')) {
88100
const {setValidity} = event.detail
89101
setValidity('🚫 Something went wrong. Please try again')
90102
}
@@ -96,7 +108,7 @@ <h2 tabindex="-1" id="success2" class="success" hidden>Your submission was succe
96108
}
97109
</script>
98110

99-
<script type="module" src="https://unpkg.com/@github/auto-check-element@latest"></script>
100-
<!-- <script type="module" src="../dist/index.js" defer></script> -->
111+
<!-- <script type="module" src="https://unpkg.com/@github/auto-check-element@latest"></script> -->
112+
<script type="module" src="../dist/bundle.js" defer></script>
101113
</body>
102114
</html>

package-lock.json

+3-3
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/auto-check-element.ts

+51-2
Original file line numberDiff line numberDiff line change
@@ -105,8 +105,10 @@ export class AutoCheckElement extends HTMLElement {
105105
const state = {check: checker, controller: null}
106106
states.set(this, state)
107107

108-
input.addEventListener('input', setLoadingState)
109-
input.addEventListener('input', checker)
108+
const changeHandler = handleChange.bind(null, checker)
109+
110+
input.addEventListener('blur', changeHandler)
111+
input.addEventListener('input', changeHandler)
110112
input.autocomplete = 'off'
111113
input.spellcheck = false
112114
}
@@ -185,6 +187,43 @@ export class AutoCheckElement extends HTMLElement {
185187
get httpMethod(): string {
186188
return AllowedHttpMethods[this.getAttribute('http-method') as keyof typeof AllowedHttpMethods] || 'POST'
187189
}
190+
191+
set validateOnKeystroke(enabled: boolean) {
192+
if (enabled) {
193+
this.setAttribute('validate-on-keystroke', '')
194+
} else {
195+
this.removeAttribute('validate-on-keystroke')
196+
}
197+
}
198+
199+
get validateOnKeystroke(): boolean {
200+
const value = this.getAttribute('validate-on-keystroke')
201+
return value === 'true' || value === ''
202+
}
203+
204+
get onlyValidateOnBlur(): boolean {
205+
const value = this.getAttribute('only-validate-on-blur')
206+
return value === 'true' || value === ''
207+
}
208+
}
209+
210+
function handleChange(checker: () => void, event: Event) {
211+
const input = event.currentTarget
212+
if (!(input instanceof HTMLInputElement)) return
213+
214+
const autoCheckElement = input.closest('auto-check')
215+
if (!(autoCheckElement instanceof AutoCheckElement)) return
216+
217+
if (input.value.length === 0) return
218+
219+
if (
220+
(event.type !== 'blur' && !autoCheckElement.onlyValidateOnBlur) || // Existing default behavior
221+
(event.type === 'blur' && autoCheckElement.onlyValidateOnBlur) || // Only validate on blur if only-validate-on-blur is set
222+
(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)
223+
) {
224+
setLoadingState(event)
225+
checker()
226+
}
188227
}
189228

190229
function setLoadingState(event: Event) {
@@ -298,8 +337,18 @@ async function check(autoCheckElement: AutoCheckElement) {
298337
if (autoCheckElement.required) {
299338
input.setCustomValidity('')
300339
}
340+
// We do not have good test coverage for this code path.
341+
// To test, ensure that the input only validates on blur
342+
// once it has been "healed" by a valid input after
343+
// previously being in an invalid state.
344+
if (autoCheckElement.onlyValidateOnBlur) {
345+
autoCheckElement.validateOnKeystroke = false
346+
}
301347
input.dispatchEvent(new AutoCheckSuccessEvent(response.clone()))
302348
} else {
349+
if (autoCheckElement.onlyValidateOnBlur) {
350+
autoCheckElement.validateOnKeystroke = true
351+
}
303352
const event = new AutoCheckErrorEvent(response.clone())
304353
input.dispatchEvent(event)
305354
if (autoCheckElement.required) {

test/auto-check.js

+63
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,65 @@ describe('auto-check element', function () {
2222
})
2323
})
2424

25+
describe('when only-validate-on-blur is true', function () {
26+
let checker
27+
let input
28+
29+
beforeEach(function () {
30+
const container = document.createElement('div')
31+
container.innerHTML = `
32+
<auto-check csrf="foo" src="/success" only-validate-on-blur>
33+
<input>
34+
</auto-check>`
35+
document.body.append(container)
36+
37+
checker = document.querySelector('auto-check')
38+
input = checker.querySelector('input')
39+
})
40+
41+
it('does not emit on initial input change', async function () {
42+
const events = []
43+
input.addEventListener('auto-check-start', event => events.push(event.type))
44+
triggerInput(input, 'hub')
45+
assert.deepEqual(events, [])
46+
})
47+
48+
it('does not emit on blur if input is blank', async function () {
49+
const events = []
50+
input.addEventListener('auto-check-start', event => events.push(event.type))
51+
triggerBlur(input)
52+
assert.deepEqual(events, [])
53+
})
54+
55+
it('emits on blur', async function () {
56+
const events = []
57+
input.addEventListener('auto-check-start', event => events.push(event.type))
58+
triggerInput(input, 'hub')
59+
triggerBlur(input)
60+
assert.deepEqual(events, ['auto-check-start'])
61+
})
62+
63+
it('emits on input change if input is invalid after blur', async function () {
64+
const events = []
65+
input.addEventListener('auto-check-start', event => events.push(event.type))
66+
67+
checker.src = '/fail'
68+
triggerInput(input, 'hub')
69+
triggerBlur(input)
70+
await once(input, 'auto-check-complete')
71+
triggerInput(input, 'hub2')
72+
triggerInput(input, 'hub3')
73+
74+
assert.deepEqual(events, ['auto-check-start', 'auto-check-start', 'auto-check-start'])
75+
})
76+
77+
afterEach(function () {
78+
document.body.innerHTML = ''
79+
checker = null
80+
input = null
81+
})
82+
})
83+
2584
describe('required attribute', function () {
2685
let checker
2786
let input
@@ -331,3 +390,7 @@ function triggerInput(input, value) {
331390
input.value = value
332391
return input.dispatchEvent(new InputEvent('input'))
333392
}
393+
394+
function triggerBlur(input) {
395+
return input.dispatchEvent(new FocusEvent('blur'))
396+
}

0 commit comments

Comments
 (0)