mirror of
https://github.com/zulip/zulip.git
synced 2025-11-10 17:07:07 +00:00
dropdown_widget: Add sticky_bottom_option element.
Adds `sticky_bottom_option` element to the `dropdown_widget` to support moving of "Add a new saved snippet" button to the bottom sticky area of the dropdown for better accessibility and UX.
This commit is contained in:
@@ -45,6 +45,7 @@ export type DropdownWidgetOptions = {
|
|||||||
event: JQuery.ClickEvent,
|
event: JQuery.ClickEvent,
|
||||||
instance: tippy.Instance,
|
instance: tippy.Instance,
|
||||||
widget: DropdownWidget,
|
widget: DropdownWidget,
|
||||||
|
is_sticky_bottom_option_clicked: boolean,
|
||||||
) => void;
|
) => void;
|
||||||
// Provide an parent element to widget which will be re-rendered if the widget is setup again.
|
// Provide an parent element to widget which will be re-rendered if the widget is setup again.
|
||||||
// It is important to not pass `$("body")` here for widgets that would be `setup()`
|
// It is important to not pass `$("body")` here for widgets that would be `setup()`
|
||||||
@@ -55,6 +56,8 @@ export type DropdownWidgetOptions = {
|
|||||||
on_hidden_callback?: (instance: tippy.Instance) => void;
|
on_hidden_callback?: (instance: tippy.Instance) => void;
|
||||||
on_exit_with_escape_callback?: () => void;
|
on_exit_with_escape_callback?: () => void;
|
||||||
render_selected_option?: () => void;
|
render_selected_option?: () => void;
|
||||||
|
// Used to add a sticky button at the bottom of the dropdown.
|
||||||
|
sticky_bottom_option?: string;
|
||||||
// Used to focus the `target` after dropdown is closed. This is important since the dropdown is
|
// Used to focus the `target` after dropdown is closed. This is important since the dropdown is
|
||||||
// appended to `body` and hence `body` is focused when the dropdown is closed, which makes
|
// appended to `body` and hence `body` is focused when the dropdown is closed, which makes
|
||||||
// it hard for the user to get focus back to the `target`.
|
// it hard for the user to get focus back to the `target`.
|
||||||
@@ -82,6 +85,7 @@ export class DropdownWidget {
|
|||||||
event: JQuery.ClickEvent,
|
event: JQuery.ClickEvent,
|
||||||
instance: tippy.Instance,
|
instance: tippy.Instance,
|
||||||
widget: DropdownWidget,
|
widget: DropdownWidget,
|
||||||
|
is_sticky_bottom_option_clicked: boolean,
|
||||||
) => void;
|
) => void;
|
||||||
focus_target_on_hidden: boolean;
|
focus_target_on_hidden: boolean;
|
||||||
on_show_callback: (instance: tippy.Instance) => void;
|
on_show_callback: (instance: tippy.Instance) => void;
|
||||||
@@ -89,6 +93,7 @@ export class DropdownWidget {
|
|||||||
on_hidden_callback: (instance: tippy.Instance) => void;
|
on_hidden_callback: (instance: tippy.Instance) => void;
|
||||||
on_exit_with_escape_callback: () => void;
|
on_exit_with_escape_callback: () => void;
|
||||||
render_selected_option: () => void;
|
render_selected_option: () => void;
|
||||||
|
sticky_bottom_option: string | undefined;
|
||||||
tippy_props: Partial<tippy.Props>;
|
tippy_props: Partial<tippy.Props>;
|
||||||
list_widget: ListWidgetType<Option, Option> | undefined;
|
list_widget: ListWidgetType<Option, Option> | undefined;
|
||||||
instance: tippy.Instance | undefined;
|
instance: tippy.Instance | undefined;
|
||||||
@@ -116,6 +121,7 @@ export class DropdownWidget {
|
|||||||
this.on_hidden_callback = options.on_hidden_callback ?? noop;
|
this.on_hidden_callback = options.on_hidden_callback ?? noop;
|
||||||
this.on_exit_with_escape_callback = options.on_exit_with_escape_callback ?? noop;
|
this.on_exit_with_escape_callback = options.on_exit_with_escape_callback ?? noop;
|
||||||
this.render_selected_option = options.render_selected_option ?? noop;
|
this.render_selected_option = options.render_selected_option ?? noop;
|
||||||
|
this.sticky_bottom_option = options.sticky_bottom_option;
|
||||||
// These properties can override any tippy props.
|
// These properties can override any tippy props.
|
||||||
this.tippy_props = options.tippy_props ?? {};
|
this.tippy_props = options.tippy_props ?? {};
|
||||||
this.list_widget = undefined;
|
this.list_widget = undefined;
|
||||||
@@ -256,6 +262,7 @@ export class DropdownWidget {
|
|||||||
render_dropdown_list_container({
|
render_dropdown_list_container({
|
||||||
widget_name: this.widget_name,
|
widget_name: this.widget_name,
|
||||||
hide_search_box: this.hide_search_box,
|
hide_search_box: this.hide_search_box,
|
||||||
|
sticky_bottom_option: this.sticky_bottom_option,
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -299,9 +306,14 @@ export class DropdownWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const $search_input = $popper.find(".dropdown-list-search-input");
|
const $search_input = $popper.find(".dropdown-list-search-input");
|
||||||
|
const $sticky_bottom_option = $popper.find(".sticky-bottom-option");
|
||||||
assert(this.list_widget !== undefined);
|
assert(this.list_widget !== undefined);
|
||||||
const list_items = this.list_widget.get_current_list();
|
const list_items = this.list_widget.get_current_list();
|
||||||
if (list_items.length === 0 && !(e.key === "Escape")) {
|
if (
|
||||||
|
list_items.length === 0 &&
|
||||||
|
!(e.key === "Escape") &&
|
||||||
|
!this.sticky_bottom_option
|
||||||
|
) {
|
||||||
// Let the browser handle it.
|
// Let the browser handle it.
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -328,6 +340,16 @@ export class DropdownWidget {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handle_arrow_down_on_last_item = (): void => {
|
const handle_arrow_down_on_last_item = (): void => {
|
||||||
|
if (this.sticky_bottom_option) {
|
||||||
|
trigger_element_focus($sticky_bottom_option);
|
||||||
|
} else if (this.hide_search_box) {
|
||||||
|
trigger_element_focus(first_item());
|
||||||
|
} else {
|
||||||
|
trigger_element_focus($search_input);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handle_arrow_down_on_sticky_bottom_option = (): void => {
|
||||||
if (this.hide_search_box) {
|
if (this.hide_search_box) {
|
||||||
trigger_element_focus(first_item());
|
trigger_element_focus(first_item());
|
||||||
} else {
|
} else {
|
||||||
@@ -335,6 +357,30 @@ export class DropdownWidget {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handle_arrow_up_on_sticky_bottom_option = (): void => {
|
||||||
|
if (list_items.length > 0) {
|
||||||
|
render_all_items_and_focus_last_item();
|
||||||
|
} else if (!this.hide_search_box) {
|
||||||
|
trigger_element_focus($search_input);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handle_arrow_down_on_search_input = (): void => {
|
||||||
|
if (list_items.length > 0) {
|
||||||
|
trigger_element_focus(first_item());
|
||||||
|
} else if (this.sticky_bottom_option) {
|
||||||
|
trigger_element_focus($sticky_bottom_option);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handle_arrow_up_on_search_input = (): void => {
|
||||||
|
if (this.sticky_bottom_option) {
|
||||||
|
trigger_element_focus($sticky_bottom_option);
|
||||||
|
} else {
|
||||||
|
render_all_items_and_focus_last_item();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handle_arrow_up_on_first_item = (): void => {
|
const handle_arrow_up_on_first_item = (): void => {
|
||||||
if (this.hide_search_box) {
|
if (this.hide_search_box) {
|
||||||
render_all_items_and_focus_last_item();
|
render_all_items_and_focus_last_item();
|
||||||
@@ -365,12 +411,15 @@ export class DropdownWidget {
|
|||||||
case "Tab":
|
case "Tab":
|
||||||
case "ArrowDown":
|
case "ArrowDown":
|
||||||
switch (e.target) {
|
switch (e.target) {
|
||||||
|
case $search_input.get(0):
|
||||||
|
handle_arrow_down_on_search_input();
|
||||||
|
break;
|
||||||
|
case $sticky_bottom_option.get(0):
|
||||||
|
handle_arrow_down_on_sticky_bottom_option();
|
||||||
|
break;
|
||||||
case last_item().get(0):
|
case last_item().get(0):
|
||||||
handle_arrow_down_on_last_item();
|
handle_arrow_down_on_last_item();
|
||||||
break;
|
break;
|
||||||
case $search_input.get(0):
|
|
||||||
trigger_element_focus(first_item());
|
|
||||||
break;
|
|
||||||
default:
|
default:
|
||||||
trigger_element_focus($(e.target).next());
|
trigger_element_focus($(e.target).next());
|
||||||
}
|
}
|
||||||
@@ -378,12 +427,15 @@ export class DropdownWidget {
|
|||||||
|
|
||||||
case "ArrowUp":
|
case "ArrowUp":
|
||||||
switch (e.target) {
|
switch (e.target) {
|
||||||
|
case $search_input.get(0):
|
||||||
|
handle_arrow_up_on_search_input();
|
||||||
|
break;
|
||||||
|
case $sticky_bottom_option.get(0):
|
||||||
|
handle_arrow_up_on_sticky_bottom_option();
|
||||||
|
break;
|
||||||
case first_item().get(0):
|
case first_item().get(0):
|
||||||
handle_arrow_up_on_first_item();
|
handle_arrow_up_on_first_item();
|
||||||
break;
|
break;
|
||||||
case $search_input.get(0):
|
|
||||||
render_all_items_and_focus_last_item();
|
|
||||||
break;
|
|
||||||
default:
|
default:
|
||||||
trigger_element_focus($(e.target).prev());
|
trigger_element_focus($(e.target).prev());
|
||||||
}
|
}
|
||||||
@@ -399,7 +451,12 @@ export class DropdownWidget {
|
|||||||
if (this.unique_id_type === DataTypes.NUMBER) {
|
if (this.unique_id_type === DataTypes.NUMBER) {
|
||||||
this.current_value = Number.parseInt(this.current_value, 10);
|
this.current_value = Number.parseInt(this.current_value, 10);
|
||||||
}
|
}
|
||||||
this.item_click_callback(event, instance, this);
|
this.item_click_callback(event, instance, this, false);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Click on $sticky_bottom_option.
|
||||||
|
$popper.on("click", ".sticky-bottom-option", (event) => {
|
||||||
|
this.item_click_callback(event, instance, this, true);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Set focus on first element when dropdown opens.
|
// Set focus on first element when dropdown opens.
|
||||||
|
|||||||
@@ -1381,3 +1381,24 @@ input.settings_text_input {
|
|||||||
.text-error {
|
.text-error {
|
||||||
color: hsl(1.06deg 44.66% 50.39%);
|
color: hsl(1.06deg 44.66% 50.39%);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.sticky-bottom-option {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-start;
|
||||||
|
color: var(--color-dropdown-item);
|
||||||
|
border: none;
|
||||||
|
border-top: 1px solid var(--color-border-zulip-button);
|
||||||
|
border-top-left-radius: 0 !important;
|
||||||
|
border-top-right-radius: 0 !important;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: var(--color-dropdown-item);
|
||||||
|
background-color: var(--background-color-active-dropdown-item);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
background-color: var(--background-color-active-dropdown-item);
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -8,4 +8,9 @@
|
|||||||
<div class="no-dropdown-items dropdown-list-item-common-styles">
|
<div class="no-dropdown-items dropdown-list-item-common-styles">
|
||||||
{{t 'No matching results'}}
|
{{t 'No matching results'}}
|
||||||
</div>
|
</div>
|
||||||
|
{{#if sticky_bottom_option}}
|
||||||
|
<button class="button rounded sticky-bottom-option">
|
||||||
|
{{sticky_bottom_option}}
|
||||||
|
</button>
|
||||||
|
{{/if}}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user