Improve budget joining view and add screen reader compatibility
This commit is contained in:
parent
1d10c9d9f7
commit
242fef400d
4 changed files with 77 additions and 23 deletions
|
@ -1,12 +1,15 @@
|
|||
import { LitElement, html } from 'lit-element'
|
||||
import { InputField } from './input';
|
||||
import { QRReadEvent } from './budget-qr-reader';
|
||||
import { InputField } from './input'
|
||||
import './budget-qr-reader'
|
||||
import { QRReadEvent } from './budget-qr-reader'
|
||||
import { ERROR_TIMER } from '../config'
|
||||
|
||||
export class JoinBudgetEvent extends CustomEvent<{ uuid: string }> { }
|
||||
|
||||
class BudgetJoinComponent extends LitElement {
|
||||
hasError: boolean = false
|
||||
showQRReader: boolean = false
|
||||
errorTimer: number = null
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
|
@ -15,9 +18,30 @@ class BudgetJoinComponent extends LitElement {
|
|||
}
|
||||
}
|
||||
|
||||
firstUpdated() {
|
||||
const secretInputEl = <InputField>this.shadowRoot.getElementById('uuid')
|
||||
secretInputEl.focusInput()
|
||||
}
|
||||
|
||||
updated(changedProperties: Map<string | number | symbol, any>) {
|
||||
// If error was turned on, turn it off after a moment
|
||||
if (changedProperties.has('hasError')) {
|
||||
if (this.errorTimer != null) {
|
||||
window.clearTimeout(this.errorTimer)
|
||||
}
|
||||
|
||||
this.errorTimer = window.setTimeout(() => {
|
||||
this.hasError = false
|
||||
this.errorTimer = null
|
||||
}, ERROR_TIMER * 1000)
|
||||
}
|
||||
|
||||
return super.updated(changedProperties)
|
||||
}
|
||||
|
||||
onJoin(e: Event) {
|
||||
e.preventDefault()
|
||||
const inputEl = <InputField>this.shadowRoot.getElementById('join-budget-uuid')
|
||||
const inputEl = <InputField>this.shadowRoot.getElementById('uuid')
|
||||
|
||||
this.dispatchEvent(new JoinBudgetEvent(
|
||||
'joinBudget',
|
||||
|
@ -42,12 +66,10 @@ class BudgetJoinComponent extends LitElement {
|
|||
|
||||
render() {
|
||||
return html`
|
||||
<p>Add below the budget secret that you can get from the person who created the budget.</p>
|
||||
<input-field
|
||||
id="join-budget-uuid"
|
||||
id="uuid"
|
||||
type="text"
|
||||
label="Budget secret"
|
||||
placeholder="aaaaaaaa-bbbb-4ccc-9ddd-ffffffffffff"
|
||||
label="Budget secret (ask someone to share theirs)"
|
||||
maxlength="36"
|
||||
pattern="[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-4[0-9A-Fa-f]{3}-[89ABab][0-9A-Fa-f]{3}-[0-9A-Fa-f]{12}"
|
||||
required
|
||||
|
@ -56,14 +78,17 @@ class BudgetJoinComponent extends LitElement {
|
|||
<fa-icon name="plus" alt="Join budget"></fa-icon>
|
||||
</fa-button>
|
||||
<fa-button type="button" @click=${this.toggleShowQRReader}>
|
||||
<fa-icon name="qrcode" alt="Read QR using camera"></fa-icon>
|
||||
<fa-icon
|
||||
name="qrcode"
|
||||
alt="${(!this.showQRReader) ? 'Read QR using camera' : 'Stop reading QR'}"
|
||||
></fa-icon>
|
||||
</fa-button>
|
||||
|
||||
<div>
|
||||
${this.showQRReader ? html`<budget-qr-reader @qrRead=${this.onQRRead}></budget-qr-reader>` : null}
|
||||
</div>
|
||||
|
||||
${this.hasError ? html`<p>Could not add given budget, please check it is correctly typed.</p>` : null}
|
||||
${this.hasError ? html`<p role="alert">Could not add given budget, please check it is correctly typed.</p>` : null}
|
||||
`
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { LitElement, html } from 'lit-element'
|
||||
import { LitElement, html, css } from 'lit-element'
|
||||
import { BrowserQRCodeReader, VideoInputDevice } from '../vendor/zxing-typescript/src/browser/BrowserQRCodeReader'
|
||||
import { SR_ONLY } from './styles'
|
||||
|
||||
export class QRReadEvent extends CustomEvent<{ text: string }> { }
|
||||
|
||||
|
@ -9,16 +10,26 @@ class BudgetQRReaderComponent extends LitElement {
|
|||
reader: BrowserQRCodeReader
|
||||
inputs: VideoInputDevice[]
|
||||
currentInput: string
|
||||
currentInputLabel: string
|
||||
test: string
|
||||
scanActive: boolean
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
inputs: { type: Array },
|
||||
currentInput: { type: String },
|
||||
test: { type: String }
|
||||
currentInputLabel: { type: String },
|
||||
test: { type: String },
|
||||
scanActive: { type: Boolean }
|
||||
}
|
||||
}
|
||||
|
||||
static get styles() {
|
||||
return css`
|
||||
${SR_ONLY}
|
||||
`
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super()
|
||||
|
||||
|
@ -31,6 +42,7 @@ class BudgetQRReaderComponent extends LitElement {
|
|||
this.inputs = await this.reader.getVideoInputDevices()
|
||||
if (this.inputs.length > 0) {
|
||||
this.currentInput = this.inputs[0].deviceId
|
||||
this.currentInputLabel = this.inputs[0].label
|
||||
this.startRead()
|
||||
}
|
||||
}
|
||||
|
@ -54,6 +66,7 @@ class BudgetQRReaderComponent extends LitElement {
|
|||
|
||||
let found = false
|
||||
do {
|
||||
this.scanActive = true
|
||||
const result = await this.reader.decodeFromInputVideoDevice(this.currentInput, videoEl)
|
||||
const text = result.getText()
|
||||
if (this._isUUID(text)) {
|
||||
|
@ -67,13 +80,23 @@ class BudgetQRReaderComponent extends LitElement {
|
|||
}
|
||||
|
||||
stopRead() {
|
||||
this.scanActive = false
|
||||
this.reader.reset()
|
||||
}
|
||||
|
||||
inputChanged() {
|
||||
const inputEl = <HTMLSelectElement>this.shadowRoot.getElementById('input')
|
||||
this.currentInput = inputEl.value
|
||||
this.currentInputLabel = inputEl.options[inputEl.selectedIndex].label
|
||||
this.stopRead()
|
||||
this.startRead()
|
||||
}
|
||||
|
||||
render() {
|
||||
return html`
|
||||
${this.scanActive ? html`<p role="alert" class="sr-only">Now scanning for QR codes with device ${this.currentInputLabel}.</p>` : null}
|
||||
<video id="video" width="300" height="200" style="border: 1px solid gray"></video>
|
||||
<select id="qr-reader-input">
|
||||
<select id="input" @change="${this.inputChanged}">
|
||||
${this.inputs.map(input => this.renderVideoInput(input))}
|
||||
</select>
|
||||
`
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { LitElement, html, css, unsafeCSS } from 'lit-element'
|
||||
import { SR_ONLY } from './styles'
|
||||
|
||||
/**
|
||||
* An accessible Font Awesome icon. Normally FA icons are not read by screen readers, leading to bad
|
||||
|
@ -54,17 +55,7 @@ function icon2css(name: string, code: string) {
|
|||
}
|
||||
|
||||
const FA_CSS = css`
|
||||
/* From https://webaim.org/techniques/css/invisiblecontent/ */
|
||||
.sr-only {
|
||||
clip: rect(1px, 1px, 1px, 1px);
|
||||
clip-path: inset(50%);
|
||||
height: 1px;
|
||||
width: 1px;
|
||||
margin: -1px;
|
||||
overflow: hidden;
|
||||
padding: 0;
|
||||
position: absolute;
|
||||
}
|
||||
${SR_ONLY}
|
||||
|
||||
.fa,
|
||||
.fas,
|
||||
|
|
|
@ -4,3 +4,18 @@ import { css } from 'lit-element'
|
|||
* Breakpoint media query to use for mobile layouts.
|
||||
*/
|
||||
export const MOB_BP = css`@media (max-width: 768px)`
|
||||
|
||||
/** Show something only for screen readers. */
|
||||
export const SR_ONLY = css`
|
||||
/* From https://webaim.org/techniques/css/invisiblecontent/ */
|
||||
.sr-only {
|
||||
clip: rect(1px, 1px, 1px, 1px);
|
||||
clip-path: inset(50%);
|
||||
height: 1px;
|
||||
width: 1px;
|
||||
margin: -1px;
|
||||
overflow: hidden;
|
||||
padding: 0;
|
||||
position: absolute;
|
||||
}
|
||||
`
|
||||
|
|
Reference in a new issue