Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Align behavior with Primer guidance behind only-validate-on-blur toggle #76

Merged
merged 9 commits into from
Nov 26, 2024
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
16 changes: 14 additions & 2 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</h2>
<h2 tabindex="-1" id="success3" class="success" hidden>Your submission was successful</h2>
<form>
<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 @@ -97,6 +109,6 @@ <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="../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)
) {
checker()
setLoadingState(event)
}
}

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'))
}