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> {
|
async function test_restore_stream_message_draft_by_opening_compose_box(page: Page): Promise<void> {
|
||||||
await page.click(".search_icon");
|
await page.click(".search_icon");
|
||||||
await page.waitForSelector("#search_query", {visible: true});
|
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.click("#left_bar_compose_reply_button_big");
|
||||||
await page.waitForSelector("#send_message_form", {visible: true});
|
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.
|
// a flake where the typeahead doesn't show up.
|
||||||
await page.type("#private_message_recipient", recipient, {delay: 100});
|
await page.type("#private_message_recipient", recipient, {delay: 100});
|
||||||
|
|
||||||
// We use [style*="display: block"] here to distinguish
|
// PM typeaheads always have an image. This ensures we are waiting for the right typeahead to appear.
|
||||||
// the visible typeahead menu from the invisible ones
|
const entry = await page.waitForSelector(".typeahead .active a .typeahead-image", {
|
||||||
// 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', {
|
|
||||||
visible: false,
|
visible: false,
|
||||||
});
|
});
|
||||||
|
// log entry in puppeteer logs
|
||||||
|
console.log(await entry!.evaluate((el) => el.textContent));
|
||||||
await entry!.click();
|
await entry!.click();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -584,9 +582,7 @@ export async function select_item_via_typeahead(
|
||||||
console.log(`Looking in ${field_selector} to select ${str}, ${item}`);
|
console.log(`Looking in ${field_selector} to select ${str}, ${item}`);
|
||||||
await clear_and_type(page, field_selector, str);
|
await clear_and_type(page, field_selector, str);
|
||||||
const entry = await page.waitForSelector(
|
const entry = await page.waitForSelector(
|
||||||
`xpath///*[${has_class_x(
|
`xpath///*[${has_class_x("typeahead")}]//li[contains(normalize-space(), "${item}")]//a`,
|
||||||
"typeahead",
|
|
||||||
)} and contains(@style, "display: block")]//li[contains(normalize-space(), "${item}")]//a`,
|
|
||||||
{visible: true},
|
{visible: true},
|
||||||
);
|
);
|
||||||
assert.ok(entry);
|
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";
|
const admin_playground_status_selector = "div#admin-playground-status";
|
||||||
await page.waitForSelector(admin_playground_status_selector, {hidden: true});
|
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.
|
// 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.
|
// 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.
|
// So we resort to using eval with the button ID instead.
|
||||||
await page.$eval("button#submit_playground_button", (el) => {
|
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
|
* This allows us to have things like a close button, and be able
|
||||||
* to move focus there without the typeahead closing.
|
* to move focus there without the typeahead closing.
|
||||||
|
*
|
||||||
|
* 15. To position typeaheads, we use Tippyjs.
|
||||||
* ============================================================ */
|
* ============================================================ */
|
||||||
|
|
||||||
import $ from "jquery";
|
import $ from "jquery";
|
||||||
import assert from "minimalistic-assert";
|
import assert from "minimalistic-assert";
|
||||||
import {insertTextIntoField} from "text-field-edit";
|
import {insertTextIntoField} from "text-field-edit";
|
||||||
|
import type {Instance} from "tippy.js";
|
||||||
|
import tippy from "tippy.js";
|
||||||
|
|
||||||
import {get_string_diff} from "./util";
|
import {get_string_diff} from "./util";
|
||||||
|
|
||||||
|
@ -229,6 +233,7 @@ export class Typeahead<ItemType extends string | object> {
|
||||||
advanceKeyCodes: number[];
|
advanceKeyCodes: number[];
|
||||||
parentElement?: string;
|
parentElement?: string;
|
||||||
values: WeakMap<HTMLElement, ItemType>;
|
values: WeakMap<HTMLElement, ItemType>;
|
||||||
|
instance?: Instance;
|
||||||
|
|
||||||
constructor(input_element: TypeaheadInputElement, options: TypeaheadOptions<ItemType>) {
|
constructor(input_element: TypeaheadInputElement, options: TypeaheadOptions<ItemType>) {
|
||||||
this.input_element = input_element;
|
this.input_element = input_element;
|
||||||
|
@ -242,7 +247,10 @@ export class Typeahead<ItemType extends string | object> {
|
||||||
this.sorter = options.sorter;
|
this.sorter = options.sorter;
|
||||||
this.highlighter_html = options.highlighter_html;
|
this.highlighter_html = options.highlighter_html;
|
||||||
this.updater = options.updater ?? ((items) => this.defaultUpdater(items));
|
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.$menu = $(MENU_HTML).appendTo(this.$container);
|
||||||
this.$header = $(HEADER_ELEMENT_HTML).appendTo(this.$container);
|
this.$header = $(HEADER_ELEMENT_HTML).appendTo(this.$container);
|
||||||
this.source = options.source;
|
this.source = options.source;
|
||||||
|
@ -311,6 +319,13 @@ export class Typeahead<ItemType extends string | object> {
|
||||||
}
|
}
|
||||||
|
|
||||||
show(): this {
|
show(): this {
|
||||||
|
if (this.shown) {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call this early to avoid duplicate calls.
|
||||||
|
this.shown = true;
|
||||||
|
|
||||||
const header_text_html = this.header_html();
|
const header_text_html = this.header_html();
|
||||||
if (header_text_html) {
|
if (header_text_html) {
|
||||||
this.$header.find("span#typeahead-header-text").html(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 {
|
} else {
|
||||||
this.$header.hide();
|
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;
|
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;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
hide(): this {
|
hide(): this {
|
||||||
this.$container.hide();
|
|
||||||
this.shown = false;
|
this.shown = false;
|
||||||
|
if (this.parentElement) {
|
||||||
|
this.$container.hide();
|
||||||
|
} else {
|
||||||
|
this.instance?.hide();
|
||||||
|
}
|
||||||
|
|
||||||
if (this.closeInputFieldOnHide !== undefined) {
|
if (this.closeInputFieldOnHide !== undefined) {
|
||||||
this.closeInputFieldOnHide();
|
this.closeInputFieldOnHide();
|
||||||
}
|
}
|
||||||
|
@ -378,10 +396,10 @@ export class Typeahead<ItemType extends string | object> {
|
||||||
|
|
||||||
const items = this.source(this.query, this.input_element);
|
const items = this.source(this.query, this.input_element);
|
||||||
|
|
||||||
if (!items && this.shown) {
|
if (!items.length && this.shown) {
|
||||||
this.hide();
|
this.hide();
|
||||||
}
|
}
|
||||||
return items ? this.process(items) : this;
|
return items.length ? this.process(items) : this;
|
||||||
}
|
}
|
||||||
|
|
||||||
process(items: ItemType[]): this {
|
process(items: ItemType[]): this {
|
||||||
|
@ -396,7 +414,13 @@ export class Typeahead<ItemType extends string | object> {
|
||||||
this.select();
|
this.select();
|
||||||
return this;
|
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 {
|
defaultMatcher(item: ItemType, query: string): boolean {
|
||||||
|
@ -471,10 +495,11 @@ export class Typeahead<ItemType extends string | object> {
|
||||||
}
|
}
|
||||||
|
|
||||||
unlisten(): void {
|
unlisten(): void {
|
||||||
|
this.hide();
|
||||||
this.$container.remove();
|
this.$container.remove();
|
||||||
const events = ["blur", "keydown", "keyup", "keypress", "mousemove"];
|
const events = ["blur", "keydown", "keyup", "keypress", "click"];
|
||||||
for (const event_ of events) {
|
for (const event of events) {
|
||||||
$(this.input_element.$element).off(event_);
|
$(this.input_element.$element).off(event);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -558,6 +583,11 @@ export class Typeahead<ItemType extends string | object> {
|
||||||
}
|
}
|
||||||
|
|
||||||
keyup(e: JQuery.KeyUpEvent): void {
|
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);
|
const pseudo_keycode = get_pseudo_keycode(e);
|
||||||
|
|
||||||
switch (pseudo_keycode) {
|
switch (pseudo_keycode) {
|
||||||
|
|
|
@ -89,6 +89,7 @@
|
||||||
|
|
||||||
> .tippy-content {
|
> .tippy-content {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
> .tippy-arrow {
|
> .tippy-arrow {
|
||||||
|
|
|
@ -1360,11 +1360,14 @@ div.focused-message-list {
|
||||||
font-size: 0;
|
font-size: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.typeahead.dropdown-menu a {
|
.typeahead.dropdown-menu {
|
||||||
|
/* Use default color until some other typeahead overrides it. */
|
||||||
|
color: var(--color-text-default);
|
||||||
|
|
||||||
|
a {
|
||||||
color: inherit;
|
color: inherit;
|
||||||
}
|
}
|
||||||
|
|
||||||
.typeahead.dropdown-menu {
|
|
||||||
.active {
|
.active {
|
||||||
color: hsl(0deg 0% 100%);
|
color: hsl(0deg 0% 100%);
|
||||||
|
|
||||||
|
|
|
@ -1,25 +1,10 @@
|
||||||
/* CSS for Bootstrap typeahead */
|
/* CSS for Bootstrap typeahead */
|
||||||
|
|
||||||
.dropdown-menu {
|
.dropdown-menu {
|
||||||
position: absolute;
|
|
||||||
top: 100%;
|
|
||||||
left: 0;
|
|
||||||
z-index: 1000;
|
|
||||||
display: none;
|
|
||||||
float: left;
|
|
||||||
min-width: 160px;
|
|
||||||
padding: 5px 0;
|
padding: 5px 0;
|
||||||
margin: 2px 0 0;
|
min-width: 160px;
|
||||||
list-style: none;
|
list-style: none;
|
||||||
background-color: #ffffff;
|
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;
|
-webkit-background-clip: padding-box;
|
||||||
-moz-background-clip: padding;
|
-moz-background-clip: padding;
|
||||||
background-clip: padding-box;
|
background-clip: padding-box;
|
||||||
|
@ -83,8 +68,4 @@
|
||||||
}
|
}
|
||||||
.typeahead {
|
.typeahead {
|
||||||
z-index: 1051;
|
z-index: 1051;
|
||||||
margin-top: 2px;
|
|
||||||
-webkit-border-radius: 4px;
|
|
||||||
-moz-border-radius: 4px;
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue