feat: add length gauge for media alt text editor (#1431)

* feat: add length gauge for media alt text editor

* fix test
This commit is contained in:
Nolan Lawson 2019-08-24 21:23:43 -07:00 committed by GitHub
parent 7f9195c2af
commit 59d26f1a09
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 163 additions and 103 deletions

View file

@ -0,0 +1,59 @@
<div class="length-gauge {shouldAnimate ? 'should-animate' : ''} {overLimit ? 'over-char-limit' : ''}"
style={computedStyle}
aria-hidden="true"
></div>
<style>
.length-gauge {
height: 2px;
background: var(--main-theme-color);
transform-origin: left;
}
.length-gauge.should-animate {
transition: transform 0.2s linear;
}
.length-gauge.over-char-limit {
background: var(--warning-color);
}
</style>
<script>
import { mark, stop } from '../_utils/marks'
import { store } from '../_store/store'
import { observe } from 'svelte-extras'
import { throttleTimer } from '../_utils/throttleTimer'
const updateDisplayedLength = process.browser && throttleTimer(requestAnimationFrame)
export default {
oncreate () {
const { lengthAsFraction } = this.get()
this.set({ lengthAsFractionDeferred: lengthAsFraction })
// perf improvement for keyboard input latency
this.observe('lengthAsFraction', () => {
updateDisplayedLength(() => {
mark('set lengthAsFractionDeferred')
const { lengthAsFraction } = this.get()
this.set({ lengthAsFractionDeferred: lengthAsFraction })
stop('set lengthAsFractionDeferred')
requestAnimationFrame(() => this.set({ shouldAnimate: true }))
})
}, { init: false })
},
data: () => ({
shouldAnimate: false,
lengthAsFractionDeferred: 0,
style: ''
}),
store: () => store,
computed: {
lengthAsFraction: ({ length, max }) => {
// We don't need to update the gauge for every decimal point, so round it to the nearest 0.02
const int = Math.round(Math.min(max, length) / max * 100)
return (int - (int % 2)) / 100
},
computedStyle: ({ style, lengthAsFractionDeferred }) => `transform: scaleX(${lengthAsFractionDeferred}); ${style}`
},
methods: {
observe
}
}
</script>

View file

@ -0,0 +1,56 @@
<span class="length-indicator {overLimit ? 'over-char-limit' : ''}"
aria-label={lengthLabel}
{style}
>{lengthToDisplayDeferred}</span>
<style>
.length-indicator {
color: var(--length-indicator-color);
font-size: 1.3em;
}
.length-indicator.over-char-limit {
color: var(--warning-color);
}
</style>
<script>
import { mark, stop } from '../_utils/marks'
import { store } from '../_store/store'
import { observe } from 'svelte-extras'
import { throttleTimer } from '../_utils/throttleTimer'
const updateDisplayedLength = process.browser && throttleTimer(requestAnimationFrame)
export default {
oncreate () {
const { lengthToDisplay } = this.get()
this.set({ lengthToDisplayDeferred: lengthToDisplay })
// perf improvement for keyboard input latency
this.observe('lengthToDisplay', () => {
updateDisplayedLength(() => {
mark('set lengthToDisplayDeferred')
const { lengthToDisplay } = this.get()
this.set({ lengthToDisplayDeferred: lengthToDisplay })
stop('set lengthToDisplayDeferred')
})
}, { init: false })
},
data: () => ({
lengthToDisplayDeferred: 0,
style: ''
}),
store: () => store,
computed: {
lengthToDisplay: ({ length, max }) => max - length,
lengthLabel: ({ overLimit, lengthToDisplayDeferred }) => {
if (overLimit) {
return `${lengthToDisplayDeferred} characters over limit`
} else {
return `${lengthToDisplayDeferred} characters remaining`
}
}
},
methods: {
observe
}
}
</script>

View file

@ -1,57 +1,16 @@
<div class="compose-box-length-gauge {shouldAnimate ? 'should-animate' : ''} {overLimit ? 'over-char-limit' : ''}" <LengthGauge style="grid-area: gauge; margin: 0 0 5px 5px;"
style="transform: scaleX({lengthAsFractionDeferred});" {length}
aria-hidden="true" {overLimit}
></div> max={$maxStatusChars}
<style> />
.compose-box-length-gauge {
grid-area: gauge;
margin: 0 0 5px 5px;
height: 2px;
background: var(--main-theme-color);
transform-origin: left;
}
.compose-box-length-gauge.should-animate {
transition: transform 0.2s linear;
}
.compose-box-length-gauge.over-char-limit {
background: var(--warning-color);
}
</style>
<script> <script>
import { mark, stop } from '../../_utils/marks' import LengthGauge from '../LengthGauge.html'
import { store } from '../../_store/store' import { store } from '../../_store/store'
import { scheduleIdleTask } from '../../_utils/scheduleIdleTask'
import { observe } from 'svelte-extras'
export default { export default {
oncreate () { components: {
const { lengthAsFraction } = this.get() LengthGauge
this.set({ lengthAsFractionDeferred: lengthAsFraction })
// perf improvement for keyboard input latency
this.observe('lengthAsFraction', () => {
scheduleIdleTask(() => {
mark('set lengthAsFractionDeferred')
const { lengthAsFraction } = this.get()
this.set({ lengthAsFractionDeferred: lengthAsFraction })
stop('set lengthAsFractionDeferred')
requestAnimationFrame(() => this.set({ shouldAnimate: true }))
})
}, { init: false })
}, },
data: () => ({ store: () => store
shouldAnimate: false,
lengthAsFractionDeferred: 0
}),
store: () => store,
computed: {
lengthAsFraction: ({ length, $maxStatusChars }) => {
// We don't need to update the gauge for every decimal point, so round it to the nearest 0.02
const int = Math.round(Math.min($maxStatusChars, length) / $maxStatusChars * 100)
return (int - (int % 2)) / 100
}
},
methods: {
observe
}
} }
</script> </script>

View file

@ -1,56 +1,17 @@
<span class="compose-box-length {overLimit ? 'over-char-limit' : ''}" <LengthIndicator
aria-label={lengthLabel}> {length}
{lengthToDisplayDeferred} {overLimit}
</span> max={$maxStatusChars}
<style> style="grid-area: length; justify-self: right; align-self: center;"
.compose-box-length { />
grid-area: length;
justify-self: right;
color: var(--length-indicator-color);
font-size: 1.3em;
align-self: center;
}
.compose-box-length.over-char-limit {
color: var(--warning-color);
}
</style>
<script> <script>
import { mark, stop } from '../../_utils/marks'
import { store } from '../../_store/store' import { store } from '../../_store/store'
import { scheduleIdleTask } from '../../_utils/scheduleIdleTask' import LengthIndicator from '../LengthIndicator.html'
import { observe } from 'svelte-extras'
export default { export default {
oncreate () {
const { lengthToDisplay } = this.get()
this.set({ lengthToDisplayDeferred: lengthToDisplay })
// perf improvement for keyboard input latency
this.observe('lengthToDisplay', () => {
scheduleIdleTask(() => {
mark('set lengthToDisplayDeferred')
const { lengthToDisplay } = this.get()
this.set({ lengthToDisplayDeferred: lengthToDisplay })
stop('set lengthToDisplayDeferred')
})
}, { init: false })
},
data: () => ({
lengthToDisplayDeferred: 0
}),
store: () => store, store: () => store,
computed: { components: {
lengthToDisplay: ({ length, $maxStatusChars }) => $maxStatusChars - length, LengthIndicator
lengthLabel: ({ overLimit, lengthToDisplayDeferred }) => {
if (overLimit) {
return `${lengthToDisplayDeferred} characters over limit`
} else {
return `${lengthToDisplayDeferred} characters remaining`
}
}
},
methods: {
observe
} }
} }
</script> </script>

View file

@ -26,7 +26,6 @@
placeholder="Description" placeholder="Description"
ref:textarea ref:textarea
bind:value=rawText bind:value=rawText
maxlength="420"
></textarea> ></textarea>
<label for="compose-media-input-{uuid}" class="sr-only"> <label for="compose-media-input-{uuid}" class="sr-only">
Describe for the visually impaired (image, video) or auditorily impaired (audio, video) Describe for the visually impaired (image, video) or auditorily impaired (audio, video)

View file

@ -5,15 +5,26 @@
placeholder="Description" placeholder="Description"
ref:textarea ref:textarea
bind:value=rawText bind:value=rawText
maxlength="420"
></textarea> ></textarea>
<label for="the-media-alt-input-{realm}-{index}" class="sr-only"> <label for="the-media-alt-input-{realm}-{index}" class="sr-only">
Describe for the visually impaired Describe for the visually impaired
</label> </label>
<LengthGauge
{length}
{overLimit}
max={mediaAltCharLimit}
/>
<LengthIndicator
{length}
{overLimit}
max={mediaAltCharLimit}
style="width: 100%; text-align: right;"
/>
</div> </div>
<style> <style>
.media-alt-editor { .media-alt-editor {
display: flex; display: flex;
flex-direction: column;
} }
.media-alt-input { .media-alt-input {
padding: 5px; padding: 5px;
@ -42,6 +53,10 @@
import { throttleTimer } from '../../../_utils/throttleTimer' import { throttleTimer } from '../../../_utils/throttleTimer'
import { get } from '../../../_utils/lodash-lite' import { get } from '../../../_utils/lodash-lite'
import { store } from '../../../_store/store' import { store } from '../../../_store/store'
import { MEDIA_ALT_CHAR_LIMIT } from '../../../_static/media'
import LengthGauge from '../../LengthGauge.html'
import LengthIndicator from '../../LengthIndicator.html'
import { length } from 'stringz'
const updateRawTextInStore = throttleTimer(requestPostAnimationFrame) const updateRawTextInStore = throttleTimer(requestPostAnimationFrame)
@ -56,8 +71,13 @@
}, },
store: () => store, store: () => store,
data: () => ({ data: () => ({
rawText: '' rawText: '',
mediaAltCharLimit: MEDIA_ALT_CHAR_LIMIT
}), }),
computed: {
length: ({ rawText }) => length(rawText || ''),
overLimit: ({ mediaAltCharLimit, length }) => length > mediaAltCharLimit
},
methods: { methods: {
observe, observe,
setupSyncFromStore () { setupSyncFromStore () {
@ -99,6 +119,10 @@
measure () { measure () {
autosize.update(this.refs.textarea) autosize.update(this.refs.textarea)
} }
},
components: {
LengthGauge,
LengthIndicator
} }
} }
</script> </script>

View file

@ -8,3 +8,5 @@ export const mediaAccept = '.jpg,.jpeg,.png,.gif,.webm,.mp4,.m4v,.mov,image/jpeg
'image/gif,video/webm,video/mp4,video/quicktime,' + 'image/gif,video/webm,video/mp4,video/quicktime,' +
// some instances allow audio uploads // some instances allow audio uploads
'audio/mpeg,audio/mp4,audio/vnd.wav,audio/wav,audio/x-wav,audio/x-wave,audio/ogg' 'audio/mpeg,audio/mp4,audio/vnd.wav,audio/wav,audio/x-wav,audio/x-wave,audio/ogg'
export const MEDIA_ALT_CHAR_LIMIT = 420

View file

@ -19,7 +19,7 @@ export const formError = $('.form-error-user-error')
export const composeInput = $('.compose-box-input') export const composeInput = $('.compose-box-input')
export const composeContentWarning = $('.content-warning-input') export const composeContentWarning = $('.content-warning-input')
export const composeButton = $('.compose-box-button') export const composeButton = $('.compose-box-button')
export const composeLengthIndicator = $('.compose-box-length') export const composeLengthIndicator = $('.length-indicator')
export const emojiButton = $('.compose-box-toolbar button:first-child') export const emojiButton = $('.compose-box-toolbar button:first-child')
export const mediaButton = $('.compose-box-toolbar button:nth-child(2)') export const mediaButton = $('.compose-box-toolbar button:nth-child(2)')
export const pollButton = $('.compose-box-toolbar button:nth-child(3)') export const pollButton = $('.compose-box-toolbar button:nth-child(3)')