fix: make autosuggest list appear over modal dialog (#1649)

fixes #1645
This commit is contained in:
Nolan Lawson 2019-11-23 13:21:21 -08:00 committed by GitHub
parent 4221ce1c72
commit fec0c282c9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 221 additions and 100 deletions

View file

@ -1,125 +1,120 @@
<div class="compose-autosuggest {shown ? '' : 'not-shown'} {realm === 'dialog' ? 'is-dialog' : ''}"> <div class="compose-autosuggest-anchor-point" ref:anchor></div>
<ComposeAutosuggestionList
items={autosuggestSearchResults}
on:click="onClick(event)"
type={autosuggestType}
selected={autosuggestSelected}
{realm}
/>
</div>
<style> <style>
.compose-autosuggest { .compose-autosuggest-anchor-point {
grid-area: autosuggest; grid-area: autosuggest;
position: absolute; width: 100%;
left: 5px; height: 0;
top: 0;
z-index: 7000;
} }
.compose-autosuggest.is-dialog {
z-index: 11000;
}
.compose-autosuggest.not-shown {
display: none;
}
@media (min-width: 480px) {
.compose-autosuggest {
min-width: 400px;
max-width: calc(100% - 20px);
}
}
@media (max-width: 479px) {
.compose-autosuggest {
/* on mobile, move it to the left and make it fill the viewport width */
transform: translateX(-58px); /* avatar size 48px + 10px padding */
width: calc(100vw - 20px);
}
.compose-autosuggest.is-dialog {
width: calc(100vw - 40px); /* extra padding when within the dialog */
}
}
@media (max-width: 240px) {
.compose-autosuggest {
width: calc(100vw - 10px);
transform: translateX(-29px); /* avatar size 24px + 5px padding */
}
.compose-autosuggest.is-dialog {
transform: translateX(-34px); /* avatar size 24px + 10px padding */
width: calc(100vw - 20px); /* extra padding when within the dialog */
}
}
</style> </style>
<script> <script>
import { store } from '../../_store/store' import ComposeAutosuggestContainer from './ComposeAutosuggestContainer.html'
import ComposeAutosuggestionList from './ComposeAutosuggestionList.html' import { requestPostAnimationFrame } from '../../_utils/requestPostAnimationFrame'
import { get } from '../../_utils/lodash-lite'
import { selectAutosuggestItem } from '../../_actions/autosuggest'
import { observe } from 'svelte-extras' import { observe } from 'svelte-extras'
import { once } from '../../_utils/once' import { throttleTimer } from '../../_utils/throttleTimer'
import { on } from '../../_utils/eventBus'
import { store } from '../../_store/store'
import { getScrollContainer } from '../../_utils/scrollContainer'
import { get } from '../../_utils/lodash-lite'
import { registerResizeListener, unregisterResizeListener } from '../../_utils/resize'
const updatePosition = process.browser && throttleTimer(requestAnimationFrame)
export default { export default {
oncreate () { oncreate () {
this._promiseChain = Promise.resolve() const { dialogId } = this.get()
this.observe('shouldBeShown', (shouldBeShown) => { this.onResize = () => updatePosition(() => this.doResize())
// TODO: hack so that when the user clicks the button, and the textarea blurs, let setupDone = false
// we don't immediately hide the dropdown which would cause the click to get lost if (this.get().realm === 'dialog') {
this._promiseChain = this._promiseChain.then(() => { // wait for dialog to render first
if (!shouldBeShown) { on('dialogDidRender', this, id => {
return Promise.race([ if (id === dialogId && !setupDone) {
new Promise(resolve => setTimeout(resolve, 200)), setupDone = true
new Promise(resolve => this.once('autosuggestItemSelected', resolve)) this.setup()
])
} }
}).then(() => {
this.set({ shown: shouldBeShown })
this.store.setForCurrentAutosuggest({ autosuggestSelecting: false })
})
}) })
} else {
setupDone = true
this.setup()
}
},
ondestroy () {
if (this._autosuggest) {
this._autosuggest.destroy()
this._autosuggest = null
}
if (this._element) {
this._element.remove()
}
unregisterResizeListener(this.onResize)
document.removeEventListener('scroll', this.onResize)
}, },
methods: { methods: {
observe, observe,
once, setup () {
onClick (item) { const { realm, text, dialogId } = this.get()
/* autosuggestSelecting prevents a flash of searched content */ requestAnimationFrame(() => {
this.store.setForCurrentAutosuggest({ autosuggestSelecting: true }) const id = `the-autosuggest-container-${realm}`
this.fire('autosuggestItemSelected') this._element = document.getElementById(id)
selectAutosuggestItem(item) if (!this._element) {
this._element = document.createElement('div')
this._element.id = id
const parent = realm === 'dialog' ? document.querySelector('.modal-dialog-contents') : document.body
parent.appendChild(this._element) // write
}
requestPostAnimationFrame(() => {
const { left, top } = this.calculateLeftAndTop()
this._autosuggest = new ComposeAutosuggestContainer({
target: this._element,
data: {
realm,
text,
dialogId,
left,
top
}
})
this.observe('text', text => {
this._autosuggest.set({ text })
}, { init: false })
this.observe('shouldBeShown', shouldBeShown => {
if (shouldBeShown) {
this.onResize() // just in case the window size changed while we weren't focused
}
})
registerResizeListener(this.onResize)
document.addEventListener('scroll', this.onResize)
})
})
},
doResize () {
const { shouldBeShown } = this.get()
if (this._autosuggest && shouldBeShown) {
const { left, top } = this.calculateLeftAndTop()
console.log('updating autosuggest position', { left, top })
this._autosuggest.set({ left, top })
} }
}, },
calculateLeftAndTop () {
const { anchor } = this.refs
const { realm } = this.get()
const { left, bottom } = anchor.getBoundingClientRect()
const yOffset = realm === 'dialog' ? 0 : getScrollContainer().scrollTop
return {
left,
top: bottom + yOffset
}
}
},
store: () => store,
computed: { computed: {
/* eslint-disable camelcase */ /* eslint-disable camelcase */
composeSelectionStart: ({ $autosuggestData_composeSelectionStart, $currentInstance, realm }) => (
get($autosuggestData_composeSelectionStart, [$currentInstance, realm], 0)
),
composeFocused: ({ $autosuggestData_composeFocused, $currentInstance, realm }) => ( composeFocused: ({ $autosuggestData_composeFocused, $currentInstance, realm }) => (
get($autosuggestData_composeFocused, [$currentInstance, realm], false) get($autosuggestData_composeFocused, [$currentInstance, realm], false)
), ),
autosuggestSearchResults: ({ $autosuggestData_autosuggestSearchResults, $currentInstance, realm }) => (
get($autosuggestData_autosuggestSearchResults, [$currentInstance, realm], [])
),
autosuggestType: ({ $autosuggestData_autosuggestType, $currentInstance, realm }) => (
get($autosuggestData_autosuggestType, [$currentInstance, realm])
),
autosuggestSelected: ({ $autosuggestData_autosuggestSelected, $currentInstance, realm }) => (
get($autosuggestData_autosuggestSelected, [$currentInstance, realm], 0)
),
autosuggestSearchText: ({ $autosuggestData_autosuggestSelected, $currentInstance, realm }) => (
get($autosuggestData_autosuggestSelected, [$currentInstance, realm])
),
/* eslint-enable camelcase */ /* eslint-enable camelcase */
shouldBeShown: ({ realm, $autosuggestShown, composeFocused }) => ( shouldBeShown: ({ $autosuggestShown, composeFocused }) => (
!!($autosuggestShown && composeFocused) !!($autosuggestShown && composeFocused)
) )
},
data: () => ({
shown: false
}),
store: () => store,
components: {
ComposeAutosuggestionList
} }
} }
</script> </script>

View file

@ -0,0 +1,125 @@
<div class="compose-autosuggest {shown ? '' : 'not-shown'} {realm === 'dialog' ? 'is-dialog' : ''}"
style="top: {top}px; --autosuggest-input-left: {left}px;"
>
<ComposeAutosuggestionList
items={autosuggestSearchResults}
on:click="onClick(event)"
type={autosuggestType}
selected={autosuggestSelected}
{realm}
/>
</div>
<style>
.compose-autosuggest {
position: absolute;
z-index: 90;
--autosuggest-input-left: 0; /* overridden by "left" prop passed in */
--autosuggest-left-offset: 5px;
/* In desktop mode, the autosuggest tracks the position of the input (the "left" prop passed in). */
left: calc(var(--autosuggest-input-left) + var(--autosuggest-left-offset));
}
.compose-autosuggest.not-shown {
display: none;
}
/* desktop styles */
@media (min-width: 480px) {
.compose-autosuggest {
min-width: 400px;
max-width: calc(100% - 20px);
}
}
/* mobile size */
@media (max-width: 479px) {
.compose-autosuggest {
/* on mobile, make it fill the viewport width */
--autosuggest-left-offset: 10px;
left: var(--autosuggest-left-offset);
width: calc(100vw - (2 * var(--autosuggest-left-offset)));
}
}
/* tiny mobile size */
@media (max-width: 240px) {
.compose-autosuggest {
--autosuggest-left-offset: 5px; /* make it bigger on tiny devices */
}
.compose-autosuggest.is-dialog {
--autosuggest-left-offset: 10px; /* more padding in dialogs */
}
}
</style>
<script>
import { store } from '../../_store/store'
import ComposeAutosuggestionList from './ComposeAutosuggestionList.html'
import { get } from '../../_utils/lodash-lite'
import { selectAutosuggestItem } from '../../_actions/autosuggest'
import { observe } from 'svelte-extras'
import { once } from '../../_utils/once'
export default {
oncreate () {
this._promiseChain = Promise.resolve()
this.observe('shouldBeShown', (shouldBeShown) => {
// TODO: hack so that when the user clicks the button, and the textarea blurs,
// we don't immediately hide the dropdown which would cause the click to get lost
this._promiseChain = this._promiseChain.then(() => {
if (!shouldBeShown) {
return Promise.race([
new Promise(resolve => setTimeout(resolve, 200)),
new Promise(resolve => this.once('autosuggestItemSelected', resolve))
])
}
}).then(() => {
this.set({ shown: shouldBeShown })
this.store.setForCurrentAutosuggest({ autosuggestSelecting: false })
})
})
},
methods: {
observe,
once,
onClick (item) {
/* autosuggestSelecting prevents a flash of searched content */
this.store.setForCurrentAutosuggest({ autosuggestSelecting: true })
this.fire('autosuggestItemSelected')
selectAutosuggestItem(item)
}
},
computed: {
/* eslint-disable camelcase */
composeSelectionStart: ({ $autosuggestData_composeSelectionStart, $currentInstance, realm }) => (
get($autosuggestData_composeSelectionStart, [$currentInstance, realm], 0)
),
composeFocused: ({ $autosuggestData_composeFocused, $currentInstance, realm }) => (
get($autosuggestData_composeFocused, [$currentInstance, realm], false)
),
autosuggestSearchResults: ({ $autosuggestData_autosuggestSearchResults, $currentInstance, realm }) => (
get($autosuggestData_autosuggestSearchResults, [$currentInstance, realm], [])
),
autosuggestType: ({ $autosuggestData_autosuggestType, $currentInstance, realm }) => (
get($autosuggestData_autosuggestType, [$currentInstance, realm])
),
autosuggestSelected: ({ $autosuggestData_autosuggestSelected, $currentInstance, realm }) => (
get($autosuggestData_autosuggestSelected, [$currentInstance, realm], 0)
),
autosuggestSearchText: ({ $autosuggestData_autosuggestSelected, $currentInstance, realm }) => (
get($autosuggestData_autosuggestSelected, [$currentInstance, realm])
),
/* eslint-enable camelcase */
shouldBeShown: ({ $autosuggestShown, composeFocused }) => (
!!($autosuggestShown && composeFocused)
)
},
data: () => ({
shown: false,
top: 0
}),
store: () => store,
components: {
ComposeAutosuggestionList
}
}
</script>

View file

@ -12,7 +12,7 @@
{/if} {/if}
<ComposeInput {realm} {text} {autoFocus} on:postAction="doPostStatus()" /> <ComposeInput {realm} {text} {autoFocus} on:postAction="doPostStatus()" />
<ComposeLengthGauge {length} {overLimit} /> <ComposeLengthGauge {length} {overLimit} />
<ComposeAutosuggest {realm} {text} /> <ComposeAutosuggest {realm} {text} {dialogId} />
{#if poll && poll.options && poll.options.length} {#if poll && poll.options && poll.options.length}
<div class="compose-poll-wrapper" <div class="compose-poll-wrapper"
transition:slide="{duration: 333}"> transition:slide="{duration: 333}">

View file

@ -9,8 +9,8 @@
aria-activedescendant={activeDescendant} aria-activedescendant={activeDescendant}
ref:textarea ref:textarea
bind:value=rawText bind:value=rawText
on:blur="onBlur()"
on:focus="onFocus()" on:focus="onFocus()"
on:blur="onBlur()"
on:selectionChange="onSelectionChange(event)" on:selectionChange="onSelectionChange(event)"
on:keydown="onKeydown(event)" on:keydown="onKeydown(event)"
></textarea> ></textarea>

View file

@ -235,6 +235,7 @@
this._a11yDialog.show() this._a11yDialog.show()
this.set({ fadedIn: true }) this.set({ fadedIn: true })
this.fire('show') this.fire('show')
emit('dialogDidRender', id)
}) })
}) })
}, },