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 { LitElement, html } from 'lit-element'
|
||||||
import { InputField } from './input';
|
import { InputField } from './input'
|
||||||
import { QRReadEvent } from './budget-qr-reader';
|
import './budget-qr-reader'
|
||||||
|
import { QRReadEvent } from './budget-qr-reader'
|
||||||
|
import { ERROR_TIMER } from '../config'
|
||||||
|
|
||||||
export class JoinBudgetEvent extends CustomEvent<{ uuid: string }> { }
|
export class JoinBudgetEvent extends CustomEvent<{ uuid: string }> { }
|
||||||
|
|
||||||
class BudgetJoinComponent extends LitElement {
|
class BudgetJoinComponent extends LitElement {
|
||||||
hasError: boolean = false
|
hasError: boolean = false
|
||||||
showQRReader: boolean = false
|
showQRReader: boolean = false
|
||||||
|
errorTimer: number = null
|
||||||
|
|
||||||
static get properties() {
|
static get properties() {
|
||||||
return {
|
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) {
|
onJoin(e: Event) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
const inputEl = <InputField>this.shadowRoot.getElementById('join-budget-uuid')
|
const inputEl = <InputField>this.shadowRoot.getElementById('uuid')
|
||||||
|
|
||||||
this.dispatchEvent(new JoinBudgetEvent(
|
this.dispatchEvent(new JoinBudgetEvent(
|
||||||
'joinBudget',
|
'joinBudget',
|
||||||
|
@ -42,12 +66,10 @@ class BudgetJoinComponent extends LitElement {
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return html`
|
return html`
|
||||||
<p>Add below the budget secret that you can get from the person who created the budget.</p>
|
|
||||||
<input-field
|
<input-field
|
||||||
id="join-budget-uuid"
|
id="uuid"
|
||||||
type="text"
|
type="text"
|
||||||
label="Budget secret"
|
label="Budget secret (ask someone to share theirs)"
|
||||||
placeholder="aaaaaaaa-bbbb-4ccc-9ddd-ffffffffffff"
|
|
||||||
maxlength="36"
|
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}"
|
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
|
required
|
||||||
|
@ -56,14 +78,17 @@ class BudgetJoinComponent extends LitElement {
|
||||||
<fa-icon name="plus" alt="Join budget"></fa-icon>
|
<fa-icon name="plus" alt="Join budget"></fa-icon>
|
||||||
</fa-button>
|
</fa-button>
|
||||||
<fa-button type="button" @click=${this.toggleShowQRReader}>
|
<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>
|
</fa-button>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
${this.showQRReader ? html`<budget-qr-reader @qrRead=${this.onQRRead}></budget-qr-reader>` : null}
|
${this.showQRReader ? html`<budget-qr-reader @qrRead=${this.onQRRead}></budget-qr-reader>` : null}
|
||||||
</div>
|
</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 { BrowserQRCodeReader, VideoInputDevice } from '../vendor/zxing-typescript/src/browser/BrowserQRCodeReader'
|
||||||
|
import { SR_ONLY } from './styles'
|
||||||
|
|
||||||
export class QRReadEvent extends CustomEvent<{ text: string }> { }
|
export class QRReadEvent extends CustomEvent<{ text: string }> { }
|
||||||
|
|
||||||
|
@ -9,16 +10,26 @@ class BudgetQRReaderComponent extends LitElement {
|
||||||
reader: BrowserQRCodeReader
|
reader: BrowserQRCodeReader
|
||||||
inputs: VideoInputDevice[]
|
inputs: VideoInputDevice[]
|
||||||
currentInput: string
|
currentInput: string
|
||||||
|
currentInputLabel: string
|
||||||
test: string
|
test: string
|
||||||
|
scanActive: boolean
|
||||||
|
|
||||||
static get properties() {
|
static get properties() {
|
||||||
return {
|
return {
|
||||||
inputs: { type: Array },
|
inputs: { type: Array },
|
||||||
currentInput: { type: String },
|
currentInput: { type: String },
|
||||||
test: { type: String }
|
currentInputLabel: { type: String },
|
||||||
|
test: { type: String },
|
||||||
|
scanActive: { type: Boolean }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static get styles() {
|
||||||
|
return css`
|
||||||
|
${SR_ONLY}
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super()
|
super()
|
||||||
|
|
||||||
|
@ -31,6 +42,7 @@ class BudgetQRReaderComponent extends LitElement {
|
||||||
this.inputs = await this.reader.getVideoInputDevices()
|
this.inputs = await this.reader.getVideoInputDevices()
|
||||||
if (this.inputs.length > 0) {
|
if (this.inputs.length > 0) {
|
||||||
this.currentInput = this.inputs[0].deviceId
|
this.currentInput = this.inputs[0].deviceId
|
||||||
|
this.currentInputLabel = this.inputs[0].label
|
||||||
this.startRead()
|
this.startRead()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -54,6 +66,7 @@ class BudgetQRReaderComponent extends LitElement {
|
||||||
|
|
||||||
let found = false
|
let found = false
|
||||||
do {
|
do {
|
||||||
|
this.scanActive = true
|
||||||
const result = await this.reader.decodeFromInputVideoDevice(this.currentInput, videoEl)
|
const result = await this.reader.decodeFromInputVideoDevice(this.currentInput, videoEl)
|
||||||
const text = result.getText()
|
const text = result.getText()
|
||||||
if (this._isUUID(text)) {
|
if (this._isUUID(text)) {
|
||||||
|
@ -67,13 +80,23 @@ class BudgetQRReaderComponent extends LitElement {
|
||||||
}
|
}
|
||||||
|
|
||||||
stopRead() {
|
stopRead() {
|
||||||
|
this.scanActive = false
|
||||||
this.reader.reset()
|
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() {
|
render() {
|
||||||
return html`
|
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>
|
<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))}
|
${this.inputs.map(input => this.renderVideoInput(input))}
|
||||||
</select>
|
</select>
|
||||||
`
|
`
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import { LitElement, html, css, unsafeCSS } from 'lit-element'
|
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
|
* 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`
|
const FA_CSS = css`
|
||||||
/* From https://webaim.org/techniques/css/invisiblecontent/ */
|
${SR_ONLY}
|
||||||
.sr-only {
|
|
||||||
clip: rect(1px, 1px, 1px, 1px);
|
|
||||||
clip-path: inset(50%);
|
|
||||||
height: 1px;
|
|
||||||
width: 1px;
|
|
||||||
margin: -1px;
|
|
||||||
overflow: hidden;
|
|
||||||
padding: 0;
|
|
||||||
position: absolute;
|
|
||||||
}
|
|
||||||
|
|
||||||
.fa,
|
.fa,
|
||||||
.fas,
|
.fas,
|
||||||
|
|
|
@ -4,3 +4,18 @@ import { css } from 'lit-element'
|
||||||
* Breakpoint media query to use for mobile layouts.
|
* Breakpoint media query to use for mobile layouts.
|
||||||
*/
|
*/
|
||||||
export const MOB_BP = css`@media (max-width: 768px)`
|
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