mirror of https://github.com/zulip/zulip.git
typeahead: Use tippy to position typeaheads without a specified parent.
Except for search typeaheads which have a specific parent container, we position typeaheads using tippy.
This commit is contained in:
parent
689489573a
commit
4e87f35c7d
|
@ -46,7 +46,11 @@ async function create_stream_message_draft(page: Page): Promise<void> {
|
|||
async function test_restore_stream_message_draft_by_opening_compose_box(page: Page): Promise<void> {
|
||||
await page.click(".search_icon");
|
||||
await page.waitForSelector("#search_query", {visible: true});
|
||||
await common.select_item_via_typeahead(page, "#search_query", "stream:Denmark topic:tests", "");
|
||||
await common.clear_and_type(page, "#search_query", "stream:Denmark topic:tests");
|
||||
// Wait for narrow to complete.
|
||||
const wait_for_change = true;
|
||||
await common.get_current_msg_list_id(page, wait_for_change);
|
||||
await page.keyboard.press("Enter");
|
||||
|
||||
await page.click("#left_bar_compose_reply_button_big");
|
||||
await page.waitForSelector("#send_message_form", {visible: true});
|
||||
|
|
|
@ -34,14 +34,12 @@ export const pm_recipient = {
|
|||
// a flake where the typeahead doesn't show up.
|
||||
await page.type("#private_message_recipient", recipient, {delay: 100});
|
||||
|
||||
// We use [style*="display: block"] here to distinguish
|
||||
// the visible typeahead menu from the invisible ones
|
||||
// meant for something else; e.g., the direct message
|
||||
// input typeahead is different from the topic input
|
||||
// typeahead but both can be present in the DOM.
|
||||
const entry = await page.waitForSelector('.typeahead[style*="display: block"] .active a', {
|
||||
// PM typeaheads always have an image. This ensures we are waiting for the right typeahead to appear.
|
||||
const entry = await page.waitForSelector(".typeahead .active a .typeahead-image", {
|
||||
visible: false,
|
||||
});
|
||||
// log entry in puppeteer logs
|
||||
console.log(await entry!.evaluate((el) => el.textContent));
|
||||
await entry!.click();
|
||||
},
|
||||
|
||||
|
@ -584,9 +582,7 @@ export async function select_item_via_typeahead(
|
|||
console.log(`Looking in ${field_selector} to select ${str}, ${item}`);
|
||||
await clear_and_type(page, field_selector, str);
|
||||
const entry = await page.waitForSelector(
|
||||
`xpath///*[${has_class_x(
|
||||
"typeahead",
|
||||
)} and contains(@style, "display: block")]//li[contains(normalize-space(), "${item}")]//a`,
|
||||
`xpath///*[${has_class_x("typeahead")}]//li[contains(normalize-space(), "${item}")]//a`,
|
||||
{visible: true},
|
||||
);
|
||||
assert.ok(entry);
|
||||
|
|
|
@ -16,8 +16,18 @@ async function _add_playground_and_return_status(page: Page, payload: Playground
|
|||
const admin_playground_status_selector = "div#admin-playground-status";
|
||||
await page.waitForSelector(admin_playground_status_selector, {hidden: true});
|
||||
|
||||
await common.select_item_via_typeahead(
|
||||
page,
|
||||
"#playground_pygments_language",
|
||||
payload.pygments_language,
|
||||
payload.pygments_language,
|
||||
);
|
||||
|
||||
// Now we can fill and click the submit button.
|
||||
await common.fill_form(page, "form.admin-playground-form", payload);
|
||||
await common.fill_form(page, "form.admin-playground-form", {
|
||||
playground_name: payload.playground_name,
|
||||
url_template: payload.url_template,
|
||||
});
|
||||
// Not sure why, but page.click() doesn't seem to always click the submit button.
|
||||
// So we resort to using eval with the button ID instead.
|
||||
await page.$eval("button#submit_playground_button", (el) => {
|
||||
|
|
|
@ -130,11 +130,15 @@
|
|||
*
|
||||
* This allows us to have things like a close button, and be able
|
||||
* to move focus there without the typeahead closing.
|
||||
*
|
||||
* 15. To position typeaheads, we use Tippyjs.
|
||||
* ============================================================ */
|
||||
|
||||
import $ from "jquery";
|
||||
import assert from "minimalistic-assert";
|
||||
import {insertTextIntoField} from "text-field-edit";
|
||||
import type {Instance} from "tippy.js";
|
||||
import tippy from "tippy.js";
|
||||
|
||||
import {get_string_diff} from "./util";
|
||||
|
||||
|
@ -229,6 +233,7 @@ export class Typeahead<ItemType extends string | object> {
|
|||
advanceKeyCodes: number[];
|
||||
parentElement?: string;
|
||||
values: WeakMap<HTMLElement, ItemType>;
|
||||
instance?: Instance;
|
||||
|
||||
constructor(input_element: TypeaheadInputElement, options: TypeaheadOptions<ItemType>) {
|
||||
this.input_element = input_element;
|
||||
|
@ -242,7 +247,10 @@ export class Typeahead<ItemType extends string | object> {
|
|||
this.sorter = options.sorter;
|
||||
this.highlighter_html = options.highlighter_html;
|
||||
this.updater = options.updater ?? ((items) => this.defaultUpdater(items));
|
||||
this.$container = $(CONTAINER_HTML).appendTo($(options.parentElement ?? "body"));
|
||||
this.$container = $(CONTAINER_HTML);
|
||||
if (options.parentElement) {
|
||||
$(options.parentElement).append(this.$container);
|
||||
}
|
||||
this.$menu = $(MENU_HTML).appendTo(this.$container);
|
||||
this.$header = $(HEADER_ELEMENT_HTML).appendTo(this.$container);
|
||||
this.source = options.source;
|
||||
|
@ -311,6 +319,13 @@ export class Typeahead<ItemType extends string | object> {
|
|||
}
|
||||
|
||||
show(): this {
|
||||
if (this.shown) {
|
||||
return this;
|
||||
}
|
||||
|
||||
// Call this early to avoid duplicate calls.
|
||||
this.shown = true;
|
||||
|
||||
const header_text_html = this.header_html();
|
||||
if (header_text_html) {
|
||||
this.$header.find("span#typeahead-header-text").html(header_text_html);
|
||||
|
@ -318,45 +333,48 @@ export class Typeahead<ItemType extends string | object> {
|
|||
} else {
|
||||
this.$header.hide();
|
||||
}
|
||||
|
||||
// If a parent element was specified, we shouldn't manually
|
||||
// position the element, since it's already in the right place.
|
||||
if (this.parentElement === undefined) {
|
||||
let pos;
|
||||
|
||||
pos = this.input_element.$element[0].getBoundingClientRect();
|
||||
|
||||
pos = $.extend({}, pos, {
|
||||
height: this.input_element.$element[0].offsetHeight,
|
||||
// Zulip patch: Workaround for iOS safari problems
|
||||
top: this.input_element.$element.get_offset_to_window().top,
|
||||
});
|
||||
|
||||
let top_pos = pos.top + pos.height;
|
||||
if (this.dropup) {
|
||||
top_pos = pos.top - this.$container.outerHeight()!;
|
||||
}
|
||||
|
||||
// Zulip patch: Avoid typeahead going off top of screen.
|
||||
if (top_pos < 0) {
|
||||
top_pos = 0;
|
||||
}
|
||||
|
||||
this.$container.css({
|
||||
top: top_pos,
|
||||
left: pos.left,
|
||||
});
|
||||
}
|
||||
|
||||
this.$container.show();
|
||||
this.shown = true;
|
||||
this.mouse_moved_since_typeahead = false;
|
||||
|
||||
if (this.parentElement) {
|
||||
this.$container.show();
|
||||
// We don't need tippy to position typeaheads which already know where they should be.
|
||||
return this;
|
||||
}
|
||||
this.instance = tippy(this.input_element.$element[0], {
|
||||
// Lets typeahead take the width needed to fit the content
|
||||
// and wraps it if it overflows the visible container.
|
||||
maxWidth: "none",
|
||||
theme: "popover-menu",
|
||||
placement: this.dropup ? "top-start" : "bottom-start",
|
||||
interactive: true,
|
||||
appendTo: () => document.body,
|
||||
showOnCreate: true,
|
||||
content: this.$container[0],
|
||||
// We expect the typeahead creator to handle when to hide / show the typeahead.
|
||||
trigger: "manual",
|
||||
arrow: false,
|
||||
offset: [0, 0],
|
||||
// We have event handlers to hide the typeahead, so we
|
||||
// don't want tippy to hide it for us.
|
||||
hideOnClick: false,
|
||||
onHidden: () => {
|
||||
assert(this.instance !== undefined);
|
||||
this.instance.destroy();
|
||||
this.instance = undefined;
|
||||
},
|
||||
});
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
hide(): this {
|
||||
this.$container.hide();
|
||||
this.shown = false;
|
||||
if (this.parentElement) {
|
||||
this.$container.hide();
|
||||
} else {
|
||||
this.instance?.hide();
|
||||
}
|
||||
|
||||
if (this.closeInputFieldOnHide !== undefined) {
|
||||
this.closeInputFieldOnHide();
|
||||
}
|
||||
|
@ -378,10 +396,10 @@ export class Typeahead<ItemType extends string | object> {
|
|||
|
||||
const items = this.source(this.query, this.input_element);
|
||||
|
||||
if (!items && this.shown) {
|
||||
if (!items.length && this.shown) {
|
||||
this.hide();
|
||||
}
|
||||
return items ? this.process(items) : this;
|
||||
return items.length ? this.process(items) : this;
|
||||
}
|
||||
|
||||
process(items: ItemType[]): this {
|
||||
|
@ -396,7 +414,13 @@ export class Typeahead<ItemType extends string | object> {
|
|||
this.select();
|
||||
return this;
|
||||
}
|
||||
return this.render(final_items.slice(0, this.items), matching_items).show();
|
||||
this.render(final_items.slice(0, this.items), matching_items);
|
||||
|
||||
if (!this.shown) {
|
||||
return this.show();
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
defaultMatcher(item: ItemType, query: string): boolean {
|
||||
|
@ -471,10 +495,11 @@ export class Typeahead<ItemType extends string | object> {
|
|||
}
|
||||
|
||||
unlisten(): void {
|
||||
this.hide();
|
||||
this.$container.remove();
|
||||
const events = ["blur", "keydown", "keyup", "keypress", "mousemove"];
|
||||
for (const event_ of events) {
|
||||
$(this.input_element.$element).off(event_);
|
||||
const events = ["blur", "keydown", "keyup", "keypress", "click"];
|
||||
for (const event of events) {
|
||||
$(this.input_element.$element).off(event);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -558,6 +583,11 @@ export class Typeahead<ItemType extends string | object> {
|
|||
}
|
||||
|
||||
keyup(e: JQuery.KeyUpEvent): void {
|
||||
// NOTE: Ideally we can ignore meta keyup calls here but
|
||||
// it's better to just trigger the lookup call to update the list in case
|
||||
// it did modify the query. For example, `Command + delete` on Mac
|
||||
// doesn't trigger a keyup event but when `Command` is released, it
|
||||
// triggers a keyup event which correctly updates the list.
|
||||
const pseudo_keycode = get_pseudo_keycode(e);
|
||||
|
||||
switch (pseudo_keycode) {
|
||||
|
|
|
@ -89,6 +89,7 @@
|
|||
|
||||
> .tippy-content {
|
||||
padding: 0;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
> .tippy-arrow {
|
||||
|
|
|
@ -1360,11 +1360,14 @@ div.focused-message-list {
|
|||
font-size: 0;
|
||||
}
|
||||
|
||||
.typeahead.dropdown-menu a {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.typeahead.dropdown-menu {
|
||||
/* Use default color until some other typeahead overrides it. */
|
||||
color: var(--color-text-default);
|
||||
|
||||
a {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.active {
|
||||
color: hsl(0deg 0% 100%);
|
||||
|
||||
|
|
|
@ -1,25 +1,10 @@
|
|||
/* CSS for Bootstrap typeahead */
|
||||
|
||||
.dropdown-menu {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
z-index: 1000;
|
||||
display: none;
|
||||
float: left;
|
||||
min-width: 160px;
|
||||
padding: 5px 0;
|
||||
margin: 2px 0 0;
|
||||
min-width: 160px;
|
||||
list-style: none;
|
||||
background-color: #ffffff;
|
||||
border: 1px solid #ccc;
|
||||
border: 1px solid rgba(0, 0, 0, 0.2);
|
||||
-webkit-border-radius: 6px;
|
||||
-moz-border-radius: 6px;
|
||||
border-radius: 6px;
|
||||
-webkit-box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2);
|
||||
-moz-box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2);
|
||||
box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2);
|
||||
-webkit-background-clip: padding-box;
|
||||
-moz-background-clip: padding;
|
||||
background-clip: padding-box;
|
||||
|
@ -83,8 +68,4 @@
|
|||
}
|
||||
.typeahead {
|
||||
z-index: 1051;
|
||||
margin-top: 2px;
|
||||
-webkit-border-radius: 4px;
|
||||
-moz-border-radius: 4px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue