1
0
Fork 0
mirror of https://github.com/anyproto/anytype-kotlin.git synced 2025-06-08 13:57:10 +09:00

Focus the last empty block when tapping on empty space (#936)

This commit is contained in:
Evgenii Kozlov 2020-09-29 19:55:19 +03:00 committed by GitHub
parent 8a2cd1eacc
commit 770cca74d1
Signed by: github
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 349 additions and 159 deletions

View file

@ -12,6 +12,7 @@
### Fixes & tech 🚒
* Should focus the last empty text block when clicking under document's blocks (#935)
* Clicking on empty space before document is loaded should not crash application (#930)
* Focusing on start may crash application by some users (#931)

View file

@ -4,5 +4,5 @@ package com.anytypeio.anytype.core_ui.common
* Defines a view that can be focused.
*/
interface Focusable {
val isFocused: Boolean
var isFocused: Boolean
}

View file

@ -65,7 +65,10 @@ abstract class Text(
}
content.apply {
setOnFocusChangeListener { _, hasFocus -> onFocusChanged(item.id, hasFocus) }
setOnFocusChangeListener { _, hasFocus ->
item.isFocused = hasFocus
onFocusChanged(item.id, hasFocus)
}
setOnClickListener { onTextInputClicked(item.id) }
enableEnterKeyDetector(
onSplitLineEnterClicked = { range ->

View file

@ -377,7 +377,7 @@ sealed class BlockView : ViewType, Parcelable {
@Parcelize
data class Document(
override val id: String,
override val isFocused: Boolean,
override var isFocused: Boolean,
override var text: String?,
val emoji: String? = null,
override val image: String? = null,
@ -396,7 +396,7 @@ sealed class BlockView : ViewType, Parcelable {
@Parcelize
data class Profile(
override val id: String,
override val isFocused: Boolean,
override var isFocused: Boolean,
override var text: String?,
override val image: String? = null,
override val mode: Mode = Mode.EDIT,
@ -414,7 +414,7 @@ sealed class BlockView : ViewType, Parcelable {
@Parcelize
data class Archive(
override val id: String,
override val isFocused: Boolean = false,
override var isFocused: Boolean = false,
override var text: String?,
override val image: String? = null,
override val mode: Mode = Mode.READ,

View file

@ -59,8 +59,11 @@ class BlockViewDiffUtil(
}
if (newBlock is Focusable && oldBlock is Focusable) {
if (newBlock.isFocused != oldBlock.isFocused)
if (newBlock.isFocused != oldBlock.isFocused) {
changes.add(FOCUS_CHANGED)
Timber.d("Focus changed!")
} else
Timber.d("Focus hasn't changed")
}
if (newBlock is BlockView.Cursor && oldBlock is BlockView.Cursor) {

View file

@ -1840,6 +1840,16 @@ class PageViewModel(
when {
content.style == Content.Text.Style.TITLE -> addNewBlockAtTheEnd()
content.text.isNotEmpty() -> addNewBlockAtTheEnd()
content.text.isEmpty() -> {
val stores = orchestrator.stores
if (stores.focus.current().isEmpty) {
val focus = Editor.Focus(id = last.id, cursor = null)
viewModelScope.launch { orchestrator.stores.focus.update(focus) }
viewModelScope.launch { refresh() }
} else {
Timber.d("Outside click is ignored because focus is not empty")
}
}
else -> Timber.d("Outside-click has been ignored.")
}
}
@ -1865,7 +1875,7 @@ class PageViewModel(
fun onHideKeyboardClicked() {
controlPanelInteractor.onEvent(ControlPanelMachine.Event.OnClearFocusClicked)
viewModelScope.launch { orchestrator.stores.focus.update(Editor.Focus.empty()) }
viewModelScope.launch { renderCommand.send(Unit) }
viewModelScope.launch { refresh() }
}
private fun proceedWithClearingFocus() {

View file

@ -1868,57 +1868,6 @@ open class PageViewModelTest {
}
}
@Test
fun `should create a new paragraph on outside-clicked event if page contains only title and icon`() {
val root = MockDataFactory.randomUuid()
val child = MockDataFactory.randomUuid()
val page = MockBlockFactory.makeOnePageWithOneTextBlock(
root = root,
child = child,
style = Block.Content.Text.Style.TITLE
)
val flow: Flow<List<Event.Command>> = flow {
delay(100)
emit(
listOf(
Event.Command.ShowBlock(
root = root,
blocks = page,
context = root
)
)
)
}
stubObserveEvents(flow)
stubOpenPage()
stubCreateBlock(root)
buildViewModel()
vm.onStart(root)
coroutineTestRule.advanceTime(100)
vm.onOutsideClicked()
runBlockingTest {
verify(createBlock, times(1)).invoke(
params = eq(
CreateBlock.Params(
context = root,
target = "",
position = Position.INNER,
prototype = Block.Prototype.Text(
style = Block.Content.Text.Style.P
)
)
)
)
}
}
@Test
fun `should start updating text style of the focused block on turn-into-option-clicked event`() {
@ -1978,52 +1927,6 @@ open class PageViewModelTest {
}
}
@Test
fun `should clear focus internally and re-render on hide-keyboard event`() {
val root = MockDataFactory.randomUuid()
val child = MockDataFactory.randomUuid()
val page = MockBlockFactory.makeOnePageWithOneTextBlock(
root = root,
child = child,
style = Block.Content.Text.Style.TITLE
)
val flow: Flow<List<Event.Command>> = flow {
delay(100)
emit(
listOf(
Event.Command.ShowBlock(
root = root,
blocks = page,
context = root
)
)
)
}
stubObserveEvents(flow)
stubOpenPage()
buildViewModel()
vm.onStart(root)
coroutineTestRule.advanceTime(100)
val testObserver = vm.focus.test()
vm.onBlockFocusChanged(
id = child,
hasFocus = true
)
testObserver.assertValue(child)
vm.onHideKeyboardClicked()
testObserver.assertValue(PageViewModel.EMPTY_FOCUS_ID)
}
@Test
fun `should start updating the target block's color on color-toolbar-option-selected event`() {
@ -2212,61 +2115,6 @@ open class PageViewModelTest {
}
}
@Test
fun `should create a new paragraph on outside-clicked event if the last block is a link block`() {
val root = MockDataFactory.randomUuid()
val firstChild = MockDataFactory.randomUuid()
val secondChild = MockDataFactory.randomUuid()
val page = MockBlockFactory.makeOnePageWithTitleAndOnePageLinkBlock(
rootId = root,
titleBlockId = firstChild,
pageBlockId = secondChild
)
val startDelay = 100L
val flow: Flow<List<Event.Command>> = flow {
delay(startDelay)
emit(
listOf(
Event.Command.ShowBlock(
root = root,
blocks = page,
context = root
)
)
)
}
stubObserveEvents(flow)
stubOpenPage()
stubCreateBlock(root)
buildViewModel()
vm.onStart(root)
coroutineTestRule.advanceTime(startDelay)
vm.onOutsideClicked()
runBlockingTest {
verify(createBlock, times(1)).invoke(
params = eq(
CreateBlock.Params(
target = "",
context = root,
position = Position.INNER,
prototype = Block.Prototype.Text(
style = Block.Content.Text.Style.P
)
)
)
)
}
}
@Test
fun `should send update text style intent when is list and empty`() {
// SETUP

View file

@ -1,7 +1,17 @@
package com.anytypeio.anytype.presentation.page.editor
import MockDataFactory
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import com.anytypeio.anytype.core_ui.features.page.BlockView
import com.anytypeio.anytype.domain.block.interactor.CreateBlock
import com.anytypeio.anytype.domain.block.model.Block
import com.anytypeio.anytype.domain.block.model.Position
import com.anytypeio.anytype.presentation.MockBlockFactory
import com.anytypeio.anytype.presentation.util.CoroutinesTestRule
import com.jraska.livedata.test
import com.nhaarman.mockitokotlin2.eq
import com.nhaarman.mockitokotlin2.times
import com.nhaarman.mockitokotlin2.verifyBlocking
import com.nhaarman.mockitokotlin2.verifyZeroInteractions
import org.junit.Before
import org.junit.Rule
@ -27,4 +37,155 @@ class EditorEmptySpaceInteractionTest : EditorPresentationTestSetup() {
vm.onOutsideClicked()
verifyZeroInteractions(createBlock)
}
@Test
fun `should create a new paragraph on outside-clicked event if page contains only title and icon`() {
// SETUP
val child = MockDataFactory.randomUuid()
val page = MockBlockFactory.makeOnePageWithOneTextBlock(
root = root,
child = child,
style = Block.Content.Text.Style.TITLE
)
stubInterceptEvents()
stubOpenDocument(page)
stubCreateBlock(root)
val vm = buildViewModel()
vm.onStart(root)
// TESTING
vm.onOutsideClicked()
verifyBlocking(createBlock, times(1)) {
invoke(
params = eq(
CreateBlock.Params(
context = root,
target = "",
position = Position.INNER,
prototype = Block.Prototype.Text(
style = Block.Content.Text.Style.P
)
)
)
)
}
}
@Test
fun `should create a new paragraph on outside-clicked event if the last block is a link block`() {
// SETUP
val firstChild = MockDataFactory.randomUuid()
val secondChild = MockDataFactory.randomUuid()
val page = MockBlockFactory.makeOnePageWithTitleAndOnePageLinkBlock(
rootId = root,
titleBlockId = firstChild,
pageBlockId = secondChild
)
stubInterceptEvents()
stubOpenDocument(page)
stubCreateBlock(root)
val vm = buildViewModel()
vm.onStart(root)
// TESTING
vm.onOutsideClicked()
verifyBlocking(createBlock, times(1)) {
invoke(
params = eq(
CreateBlock.Params(
target = "",
context = root,
position = Position.INNER,
prototype = Block.Prototype.Text(
style = Block.Content.Text.Style.P
)
)
)
)
}
}
@Test
fun `should not create a new paragraph but focus the last empty block`() {
// SETUP
val pic = Block(
id = MockDataFactory.randomUuid(),
content = Block.Content.File(
type = Block.Content.File.Type.IMAGE,
state = Block.Content.File.State.DONE
),
fields = Block.Fields.empty(),
children = emptyList()
)
val txt = Block(
id = MockDataFactory.randomUuid(),
fields = Block.Fields(emptyMap()),
content = Block.Content.Text(
text = "",
marks = emptyList(),
style = Block.Content.Text.Style.values().random()
),
children = emptyList()
)
val doc = listOf(
Block(
id = root,
fields = Block.Fields(emptyMap()),
content = Block.Content.Page(
style = Block.Content.Page.Style.SET
),
children = listOf(pic.id, txt.id)
),
pic,
txt
)
stubInterceptEvents()
stubOpenDocument(document = doc)
val vm = buildViewModel()
// TESTING
vm.onStart(root)
// Checking that no text block is focused
vm.state.test().assertValue { value ->
check(value is ViewState.Success)
value.blocks.none { it is BlockView.Text && it.isFocused }
}
vm.onOutsideClicked()
verifyZeroInteractions(createBlock)
// Checking that the last text block is focused and has empty text
vm.state.test().assertValue { value ->
check(value is ViewState.Success)
val last = value.blocks.last()
check(last is BlockView.Text)
last.text.isEmpty() && last.isFocused
}
}
}

View file

@ -0,0 +1,164 @@
package com.anytypeio.anytype.presentation.page.editor
import MockDataFactory
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import com.anytypeio.anytype.core_ui.common.Focusable
import com.anytypeio.anytype.domain.block.model.Block
import com.anytypeio.anytype.presentation.page.PageViewModel
import com.anytypeio.anytype.presentation.util.CoroutinesTestRule
import com.jraska.livedata.test
import com.nhaarman.mockitokotlin2.verifyZeroInteractions
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.mockito.MockitoAnnotations
class EditorFocusTest : EditorPresentationTestSetup() {
@get:Rule
val rule = InstantTaskExecutorRule()
@get:Rule
val coroutineTestRule = CoroutinesTestRule()
@Before
fun setup() {
MockitoAnnotations.initMocks(this)
}
@Test
fun `should clear focus internally and re-render on hide-keyboard event`() {
// SETUP
val block = Block(
id = MockDataFactory.randomUuid(),
fields = Block.Fields(emptyMap()),
content = Block.Content.Text(
text = MockDataFactory.randomString(),
marks = emptyList(),
style = Block.Content.Text.Style.values().random()
),
children = emptyList()
)
val page = listOf(
Block(
id = root,
fields = Block.Fields(emptyMap()),
content = Block.Content.Page(
style = Block.Content.Page.Style.SET
),
children = listOf(block.id)
),
block
)
stubInterceptEvents()
stubOpenDocument(page)
val vm = buildViewModel()
// TESTING
vm.onStart(root)
val testViewStateObserver = vm.state.test()
val testFocusObserver = vm.focus.test()
testViewStateObserver.assertValue { value ->
check(value is ViewState.Success)
val last = value.blocks.last()
check(last is Focusable)
!last.isFocused
}
vm.onBlockFocusChanged(
id = block.id,
hasFocus = true
)
testFocusObserver.assertValue(block.id)
vm.onHideKeyboardClicked()
testFocusObserver.assertValue(PageViewModel.EMPTY_FOCUS_ID)
}
@Test
fun `should update views on hide-keyboard event`() {
// SETUP
val block = Block(
id = MockDataFactory.randomUuid(),
fields = Block.Fields(emptyMap()),
content = Block.Content.Text(
text = "",
marks = emptyList(),
style = Block.Content.Text.Style.values().random()
),
children = emptyList()
)
val page = listOf(
Block(
id = root,
fields = Block.Fields(emptyMap()),
content = Block.Content.Page(
style = Block.Content.Page.Style.SET
),
children = listOf(block.id)
),
block
)
stubInterceptEvents()
stubOpenDocument(page)
val vm = buildViewModel()
// TESTING
vm.onStart(root)
vm.state.test().apply {
assertValue { value ->
check(value is ViewState.Success)
val last = value.blocks.last()
check(last is Focusable)
!last.isFocused
}
}
vm.onBlockFocusChanged(
id = block.id,
hasFocus = true
)
vm.onHideKeyboardClicked()
vm.state.test().apply {
assertValue { value ->
check(value is ViewState.Success)
val last = value.blocks.last()
check(last is Focusable)
!last.isFocused
}
}
vm.onOutsideClicked()
vm.state.test().apply {
assertValue { value ->
check(value is ViewState.Success)
val last = value.blocks.last()
check(last is Focusable)
last.isFocused
}
}
verifyZeroInteractions(createBlock)
}
}