Skip to content

Commit

Permalink
limit number of suggestions in BubbleTextField suggestions dropdown, …
Browse files Browse the repository at this point in the history
…fix scrolling, fixes #2949
  • Loading branch information
vaf-hub committed May 17, 2021
1 parent 8df059c commit 30a9309
Show file tree
Hide file tree
Showing 6 changed files with 83 additions and 55 deletions.
27 changes: 1 addition & 26 deletions src/gui/ScrollSelectList.js
Original file line number Diff line number Diff line change
Expand Up @@ -108,29 +108,4 @@ export class ScrollSelectList<T> implements MComponent<ScrollSelectListAttrs<T>>
this.scrollDom.scrollTop = selectedBottom - scrollWindowHeight
}
}
}

export function makeListSelectionChangedScrollHandler(scrollDom: HTMLElement, entryHeight: number, getSelectedEntryIndex: lazy<number>): () => void {
return function () {
const selectedIndex = getSelectedEntryIndex()

const scrollWindowHeight = scrollDom.getBoundingClientRect().height
const scrollOffset = scrollDom.scrollTop

// Actual position in the list
const selectedTop = entryHeight * selectedIndex
const selectedBottom = selectedTop + entryHeight

// Relative to the top of the scroll window
const selectedRelativeTop = selectedTop - scrollOffset
const selectedRelativeBottom = selectedBottom - scrollOffset

// clamp the selected item to stay between the top and bottom of the scroll window
if (selectedRelativeTop < 0) {
scrollDom.scrollTop = selectedTop
} else if (selectedRelativeBottom > scrollWindowHeight) {
scrollDom.scrollTop = selectedBottom - scrollWindowHeight
}
}
}

}
69 changes: 46 additions & 23 deletions src/gui/base/BubbleTextField.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ import {ease} from "../animation/Easing"
import type {TextFieldTypeEnum} from "./TextFieldN"
import {Type} from "./TextFieldN"
import {windowFacade} from "../../misc/WindowFacade"
import {neverNull} from "../../api/common/utils/Utils"
import {makeListSelectionChangedScrollHandler} from "./GuiUtils"

assertMainOrNode()

Expand Down Expand Up @@ -54,6 +56,11 @@ export interface BubbleHandler<T, S:Suggestion> {
* Height of a suggestion in pixels
*/
suggestionHeight: number;

/**
* Limit the number of suggestions to show if defined.
*/
maximumSuggestions: ?number;
}

/**
Expand All @@ -75,7 +82,7 @@ export class BubbleTextField<T> {
previousQuery: string;
originalIsEmpty: Function;
suggestions: Suggestion[];
selectedSuggestion: ?Suggestion;
selectedSuggestion: Stream<?Suggestion>;
suggestionAnimation: Promise<void>;
bubbleHandler: BubbleHandler<T, Suggestion>;
view: (Vnode<T>) => Children;
Expand All @@ -84,11 +91,12 @@ export class BubbleTextField<T> {
_textField: TextField;
_domSuggestions: HTMLElement;
_keyboardHeight: number;
_selectedSuggestionChangedListener: Stream<void>

constructor(labelIdOrLabelTextFunction: TranslationKey | lazy<string>, bubbleHandler: BubbleHandler<T, any>, injectionsRight: ?lazy<Children> = () => null, disabled: ?boolean = false) {
this.loading = null
this.suggestions = []
this.selectedSuggestion = null
this.selectedSuggestion = stream(null)
this.suggestionAnimation = Promise.resolve()
this.previousQuery = ""
this._textField = new TextField(labelIdOrLabelTextFunction)
Expand Down Expand Up @@ -150,21 +158,33 @@ export class BubbleTextField<T> {
}
}),
m(`.suggestions.abs.z4.full-width.elevated-bg.scroll.text-ellipsis${this.suggestions.length ? ".dropdown-shadow" : ""}`, {
oncreate: vnode => this._domSuggestions = vnode.dom,
oncreate: vnode => {
this._domSuggestions = vnode.dom
this._selectedSuggestionChangedListener = this.selectedSuggestion.map(
makeListSelectionChangedScrollHandler(this._domSuggestions, this.bubbleHandler.suggestionHeight, this._getSelectedSuggestionIndex.bind(this))
)
},
onbeforeremove: () => {
this._selectedSuggestionChangedListener.end(true)
},
onmousedown: e => this._textField.skipNextBlur = true,
style: {
transition: "height 0.2s"
},
}, this.suggestions.map(s => m(s, {
mouseDownHandler: e => {
this.selectedSuggestion = s
this.selectedSuggestion(s)
this.createBubbles()
}
})))
])
}
}

_getSelectedSuggestionIndex(): number {
return this.suggestions.indexOf(this.selectedSuggestion())
}

_updateSuggestions() {
let value = this._textField.value()
if (this._textField._domInput) {
Expand All @@ -182,10 +202,10 @@ export class BubbleTextField<T> {
this.animateSuggestionsHeight(this.suggestions.length, newSuggestions.length)
this.suggestions = newSuggestions
if (this.suggestions.length > 0) {
this.selectedSuggestion = this.suggestions[0]
this.selectedSuggestion.selected = true
this.suggestions[0].selected = true
this.selectedSuggestion(this.suggestions[0])
} else {
this.selectedSuggestion = null
this.selectedSuggestion(null)
}
this.previousQuery = query
let input = this._textField._domInput
Expand All @@ -202,7 +222,7 @@ export class BubbleTextField<T> {
} else if (query.length === 0 && query !== this.previousQuery) {
this.animateSuggestionsHeight(this.suggestions.length, 0)
this.suggestions = []
this.selectedSuggestion = null
this.selectedSuggestion(null)
this.previousQuery = query
}
}
Expand All @@ -221,7 +241,10 @@ export class BubbleTextField<T> {
// We need to calculate how much space can be actually used for the dropdown. We cannot just add margin like we do with dialog
// because the suggestions dropdown is absolutely positioned.
if (this._domSuggestions) {
const desiredHeight = this.bubbleHandler.suggestionHeight * newCount
const showableSuggestionsNum = this.bubbleHandler.maximumSuggestions
? Math.min(newCount, this.bubbleHandler.maximumSuggestions)
: newCount
const desiredHeight = this.bubbleHandler.suggestionHeight * showableSuggestionsNum
const top = this._domSuggestions.getBoundingClientRect().top
const availableHeight = window.innerHeight - top - this._keyboardHeight - size.vpad
const finalHeight = Math.min(availableHeight, desiredHeight)
Expand Down Expand Up @@ -266,8 +289,8 @@ export class BubbleTextField<T> {
if (value === "") return

// if there is a selected suggestion, we shall create a bubble from that suggestions instead of the entered text
if (this.selectedSuggestion != null) {
let bubble = this.bubbleHandler.createBubbleFromSuggestion(this.selectedSuggestion)
if (this.selectedSuggestion() != null) {
let bubble = this.bubbleHandler.createBubbleFromSuggestion(neverNull(this.selectedSuggestion()))
if (bubble) {
this.bubbles.push(bubble)
this._textField.value("")
Expand Down Expand Up @@ -337,30 +360,30 @@ export class BubbleTextField<T> {
}

handleUpArrow(): boolean {
if (this.selectedSuggestion != null) {
this.selectedSuggestion.selected = false
let next = (this.suggestions.indexOf(this.selectedSuggestion) - 1) % this.suggestions.length
if (this.selectedSuggestion() != null) {
neverNull(this.selectedSuggestion()).selected = false
let next = (this.suggestions.indexOf(this.selectedSuggestion()) - 1) % this.suggestions.length
if (next === -1) {
next = this.suggestions.length - 1
}
this.selectedSuggestion = this.suggestions[next]
this.selectedSuggestion.selected = true
this.suggestions[next].selected = true
this.selectedSuggestion(this.suggestions[next])
}
return false
}

handleDownArrow(): boolean {
if (this.selectedSuggestion != null) {
this.selectedSuggestion.selected = false
let next = (this.suggestions.indexOf(this.selectedSuggestion) + 1)
if (this.selectedSuggestion() != null) {
neverNull(this.selectedSuggestion()).selected = false
let next = (this.suggestions.indexOf(this.selectedSuggestion()) + 1)
if (next === this.suggestions.length) {
next = 0
}
this.selectedSuggestion = this.suggestions[next]
this.selectedSuggestion.selected = true
this.suggestions[next].selected = true
this.selectedSuggestion(this.suggestions[next])
} else if (this.suggestions.length > 0) {
this.selectedSuggestion = this.suggestions[0]
this.selectedSuggestion.selected = true
this.suggestions[0].selected = true
this.selectedSuggestion(this.suggestions[0])
}
return false
}
Expand Down
24 changes: 24 additions & 0 deletions src/gui/base/GuiUtils.js
Original file line number Diff line number Diff line change
Expand Up @@ -101,4 +101,28 @@ export function getCoordsOfMouseOrTouchEvent(event: MouseEvent | TouchEvent): {x
x: assertNotNull(event.touches.item(0)).clientX,
y: assertNotNull(event.touches.item(0)).clientY
}
}

export function makeListSelectionChangedScrollHandler(scrollDom: HTMLElement, entryHeight: number, getSelectedEntryIndex: lazy<number>): () => void {
return function () {
const selectedIndex = getSelectedEntryIndex()

const scrollWindowHeight = scrollDom.getBoundingClientRect().height
const scrollOffset = scrollDom.scrollTop

// Actual position in the list
const selectedTop = entryHeight * selectedIndex
const selectedBottom = selectedTop + entryHeight

// Relative to the top of the scroll window
const selectedRelativeTop = selectedTop - scrollOffset
const selectedRelativeBottom = selectedBottom - scrollOffset

// clamp the selected item to stay between the top and bottom of the scroll window
if (selectedRelativeTop < 0) {
scrollDom.scrollTop = selectedTop
} else if (selectedRelativeBottom > scrollWindowHeight) {
scrollDom.scrollTop = selectedBottom - scrollWindowHeight
}
}
}
2 changes: 1 addition & 1 deletion src/knowledgebase/view/KnowledgeBaseDialogContent.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {NotFoundError} from "../../api/common/error/RestError"
import {Dialog} from "../../gui/base/Dialog"
import type {TextFieldAttrs} from "../../gui/base/TextFieldN"
import {TextFieldN} from "../../gui/base/TextFieldN"
import {makeListSelectionChangedScrollHandler} from "../../gui/ScrollSelectList"
import {makeListSelectionChangedScrollHandler} from "../../gui/base/GuiUtils"

export type KnowledgebaseDialogContentAttrs = {|
+onTemplateSelect: (EmailTemplate,) => void,
Expand Down
4 changes: 3 additions & 1 deletion src/misc/RecipientInfoBubbleHandler.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,15 @@ export interface RecipientInfoBubbleFactory {
export class RecipientInfoBubbleHandler implements BubbleHandler<RecipientInfo, ContactSuggestion> {

suggestionHeight: number;
maximumSuggestions: ?number;
_bubbleFactory: RecipientInfoBubbleFactory;
_contactModel: ContactModel

constructor(bubbleFactory: RecipientInfoBubbleFactory, contactModel: ContactModel) {
constructor(bubbleFactory: RecipientInfoBubbleFactory, contactModel: ContactModel, maximumSuggestions?: number) {
this._bubbleFactory = bubbleFactory
this._contactModel = contactModel
this.suggestionHeight = ContactSuggestionHeight
this.maximumSuggestions = maximumSuggestions
}

async getSuggestions(text: string): Promise<ContactSuggestion[]> {
Expand Down
12 changes: 8 additions & 4 deletions src/sharing/view/GroupSharingDialog.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,9 @@ import {getConfirmation} from "../../gui/base/GuiUtils"
import type {GroupSharingTexts} from "../GroupGuiUtils"
import {getTextsForGroupType} from "../GroupGuiUtils"

// the maximum number of BTF suggestions so the suggestions dropdown does not overflow the dialog
const SHOW_CONTACT_SUGGESTIONS_MAX = 3

export function showGroupSharingDialog(groupInfo: GroupInfo,
allowGroupNameOverride: boolean) {

Expand Down Expand Up @@ -151,7 +154,7 @@ class GroupSharingDialogContent implements MComponent<GroupSharingDialogAttrs> {
}

function showAddParticipantDialog(model: GroupSharingModel, texts: GroupSharingTexts) {
const invitePeopleValueTextField: BubbleTextField<RecipientInfo> = new BubbleTextField("shareWithEmailRecipient_label", new RecipientInfoBubbleHandler({
const bubbleHandler = new RecipientInfoBubbleHandler({
createBubble(name: ? string, mailAddress: string, contact: ? Contact): Bubble<RecipientInfo> {
let recipientInfo = createRecipientInfo(mailAddress, name, contact)
recipientInfo.resolveContactPromise =
Expand Down Expand Up @@ -182,7 +185,8 @@ function showAddParticipantDialog(model: GroupSharingModel, texts: GroupSharingT
})
return buttonAttrs
}
}, locator.contactModel))
}, locator.contactModel, SHOW_CONTACT_SUGGESTIONS_MAX)
const invitePeopleValueTextField: BubbleTextField<RecipientInfo> = new BubbleTextField("shareWithEmailRecipient_label", bubbleHandler)
const capability: Stream<ShareCapabilityEnum> = stream(ShareCapability.Read)
const realGroupName = getSharedGroupName(model.info, false)
const customGroupName = getSharedGroupName(model.info, true)
Expand All @@ -192,7 +196,6 @@ function showAddParticipantDialog(model: GroupSharingModel, texts: GroupSharingT
type: DialogType.EditMedium,
title: () => lang.get("addParticipant_action"),
child: () => [
m(".pt", texts.addMemberMessage(customGroupName || realGroupName)),
m(".rel", m(invitePeopleValueTextField)),
m(DropDownSelectorN, {
label: "permissions_label",
Expand All @@ -214,7 +217,8 @@ function showAddParticipantDialog(model: GroupSharingModel, texts: GroupSharingT
? null
: texts.yourCustomNameLabel(customGroupName))
}
})
}),
m(".pt", texts.addMemberMessage(customGroupName || realGroupName))
],
okAction: () => {
invitePeopleValueTextField.createBubbles()
Expand Down

0 comments on commit 30a9309

Please sign in to comment.