mirror of
https://github.com/zulip/zulip.git
synced 2025-10-23 04:52:12 +00:00
inputs: Implement redesigned input component.
This commit serves as the base for the ongoing effort to standardize redesigned input elements throughout the Zulip Web UI. It introduces a new Handlebars partial block for inputs, located at web/templates/components/input.hbs. The partial can be used with the partial block syntax: {{#> input}}, allowing contributors to pass in the input element as a template. This approach wraps the input with a consistent structure that includes support for an icon and an action button. It also applies the necessary styling to ensure visual and functional consistency across the web UI. This commit also implements the filter input component at /devtools/inputs/ showroom page for design discussions and prototyping.
This commit is contained in:
@@ -125,6 +125,7 @@ EXEMPT_FILES = make_set(
|
||||
"web/src/info_overlay.ts",
|
||||
"web/src/information_density.ts",
|
||||
"web/src/input_pill.ts",
|
||||
"web/src/inputs.ts",
|
||||
"web/src/integration_branch_pill.ts",
|
||||
"web/src/integration_url_modal.ts",
|
||||
"web/src/invite.ts",
|
||||
|
@@ -10,6 +10,7 @@ import "../setup.ts";
|
||||
import "../reload.ts";
|
||||
import "../templates.ts";
|
||||
import "../zulip_test.ts";
|
||||
import "../inputs.ts";
|
||||
|
||||
// Import styles
|
||||
import "tippy.js/dist/tippy.css";
|
||||
@@ -26,6 +27,7 @@ import "../../styles/typeahead.css";
|
||||
import "../../styles/app_variables.css";
|
||||
import "../../styles/tooltips.css";
|
||||
import "../../styles/buttons.css";
|
||||
import "../../styles/inputs.css";
|
||||
import "../../styles/banners.css";
|
||||
import "../../styles/components.css";
|
||||
import "../../styles/app_components.css";
|
||||
|
@@ -1,9 +1,12 @@
|
||||
import "./portico.ts";
|
||||
import "../templates.ts";
|
||||
import "../inputs.ts";
|
||||
import "../portico/showroom.ts";
|
||||
|
||||
// Import styles in the required order
|
||||
import "../../styles/portico/showroom.css";
|
||||
import "../../styles/app_variables.css";
|
||||
import "../../styles/buttons.css";
|
||||
import "../../styles/inputs.css";
|
||||
import "../../styles/banners.css";
|
||||
import "../../styles/app_components.css";
|
||||
|
19
web/src/inputs.ts
Normal file
19
web/src/inputs.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import $ from "jquery";
|
||||
|
||||
$("body").on("input", ".input-element", function (this: HTMLInputElement, _e: JQuery.Event) {
|
||||
if (this.value.length === 0) {
|
||||
$(this).removeClass("input-element-nonempty");
|
||||
} else {
|
||||
$(this).addClass("input-element-nonempty");
|
||||
}
|
||||
});
|
||||
|
||||
$("body").on(
|
||||
"click",
|
||||
".filter-input .input-button",
|
||||
function (this: HTMLElement, _e: JQuery.Event) {
|
||||
const $input = $(this).prev(".input-element");
|
||||
$input.val("").trigger("input");
|
||||
$input.trigger("blur");
|
||||
},
|
||||
);
|
@@ -3,6 +3,7 @@ import $ from "jquery";
|
||||
import assert from "minimalistic-assert";
|
||||
|
||||
import render_banner from "../../templates/components/banner.hbs";
|
||||
import render_filter_input from "../../templates/components/showroom/filter_input.hbs";
|
||||
import {$t, $t_html} from "../i18n.ts";
|
||||
import type {HTMLSelectOneElement} from "../types.ts";
|
||||
|
||||
@@ -675,4 +676,9 @@ $(window).on("load", () => {
|
||||
$("#showroom_component_banner_default_wrapper").html(banner_html(custom_normal_banner));
|
||||
}
|
||||
});
|
||||
|
||||
if (window.location.pathname === "/devtools/inputs/") {
|
||||
const $filter_input_container = $<HTMLInputElement>(".showroom-filter-input-container");
|
||||
$filter_input_container.html(render_filter_input());
|
||||
}
|
||||
});
|
||||
|
@@ -1,5 +1,7 @@
|
||||
import Handlebars from "handlebars/runtime.js";
|
||||
|
||||
import render_input from "../templates/components/input.hbs";
|
||||
|
||||
import * as common from "./common.ts";
|
||||
import {default_html_elements, intl} from "./i18n.ts";
|
||||
import {postprocess_content} from "./postprocess_content.ts";
|
||||
@@ -173,3 +175,13 @@ Handlebars.registerHelper("popover_hotkey_hints", (...args) => {
|
||||
`<span class="popover-menu-hotkey-hints">${hotkey_hints}</span>`,
|
||||
);
|
||||
});
|
||||
|
||||
// The below section is for registering global Handlebar partials.
|
||||
|
||||
// The "input" partial block located at web/templates/components/input.hbs
|
||||
// is used to wrap any input element that needs to be styled as a Zulip input.
|
||||
// Usage example:
|
||||
// {{#> input . input_type="filter-input" custom_classes="inbox-search-wrapper" icon="search" input_button_icon="close"}}
|
||||
// <input type="text" id="{{INBOX_SEARCH_ID}}" class="input-element" value="{{search_val}}" autocomplete="off" placeholder="{{t 'Filter' }}" />
|
||||
// {{/input}}
|
||||
Handlebars.registerPartial("input", render_input);
|
||||
|
@@ -680,6 +680,14 @@
|
||||
/* Popup banner related variables */
|
||||
--popup-banner-translate-y-distance: 100px;
|
||||
|
||||
/* Input grid layout variables */
|
||||
--input-icon-starting-offset: 0.5em; /* 8px at 16px/1em */
|
||||
--input-icon-width: 1em; /* 16px at 16px/1em */
|
||||
--input-icon-content-gap: 0.4375em; /* 7px at 16px/1em */
|
||||
--input-button-content-gap: 0.25em; /* 4px at 16px/1em */
|
||||
--input-button-width: 1.5em; /* 24px at 16px/1em */
|
||||
--input-button-ending-offset: 0.125em; /* 2px at 16px/1em */
|
||||
|
||||
/* Colors used across the app */
|
||||
--color-date: light-dark(hsl(0deg 0% 15% / 75%), hsl(0deg 0% 100% / 75%));
|
||||
--color-background-private-message-header: light-dark(
|
||||
@@ -1407,15 +1415,6 @@
|
||||
hsl(0deg 0% 80%),
|
||||
hsl(0deg 0% 0% / 60%)
|
||||
);
|
||||
/* TODO: Light mode uses browser-default white
|
||||
backgrounds; we should extend the use of this
|
||||
color variable to 1) explicitly set the
|
||||
background color of inputs, and 2) clean up a
|
||||
lingering stack of selectors in dark_theme.css. */
|
||||
--color-background-input: light-dark(
|
||||
hsl(0deg 0% 100%),
|
||||
hsl(0deg 0% 0% / 20%)
|
||||
);
|
||||
/* Link colors */
|
||||
--color-text-link: light-dark(hsl(210deg 94% 42%), hsl(200deg 100% 50%));
|
||||
--color-text-link-decoration: light-dark(
|
||||
@@ -2626,6 +2625,39 @@
|
||||
hsl(0deg 52% 18%)
|
||||
);
|
||||
|
||||
/* Inputs */
|
||||
--color-text-input: light-dark(
|
||||
color-mix(in oklch, hsl(0deg 0% 15%) 50%, transparent),
|
||||
color-mix(in oklch, hsl(0deg 0% 83%) 50%, transparent)
|
||||
);
|
||||
--color-text-input-focus: light-dark(hsl(0deg 0% 15%), hsl(0deg 0% 83%));
|
||||
/* TODO: Light mode uses browser-default white
|
||||
backgrounds; we should extend the use of this
|
||||
color variable to 1) explicitly set the
|
||||
background color of inputs, and 2) clean up a
|
||||
lingering stack of selectors in dark_theme.css. */
|
||||
--color-background-input: light-dark(hsl(0deg 0% 100%), hsl(220deg 6% 10%));
|
||||
--color-background-input-focus: light-dark(
|
||||
hsl(0deg 0% 100%),
|
||||
hsl(240deg 6% 7%)
|
||||
);
|
||||
--color-border-input: light-dark(
|
||||
color-mix(in oklch, hsl(229deg 22% 10%) 30%, transparent),
|
||||
color-mix(in oklch, hsl(0deg 0% 100%) 20%, transparent)
|
||||
);
|
||||
--color-border-input-hover: light-dark(
|
||||
color-mix(in oklch, hsl(229deg 22% 10%) 30%, transparent),
|
||||
color-mix(in oklch, hsl(0deg 0% 100%) 20%, transparent)
|
||||
);
|
||||
--color-border-input-focus: light-dark(
|
||||
color-mix(in oklch, hsl(229deg 22% 10%) 80%, transparent),
|
||||
color-mix(in oklch, hsl(0deg 0% 100%) 50%, transparent)
|
||||
);
|
||||
--color-box-shadow-input-focus: light-dark(
|
||||
color-mix(in oklch, hsl(228deg 10% 20%) 30%, transparent),
|
||||
color-mix(in oklch, hsl(0deg 0% 100%) 40%, transparent)
|
||||
);
|
||||
|
||||
/* .main-view-banner colors */
|
||||
--color-success-main-view-banner: light-dark(
|
||||
hsl(147deg 57% 25%),
|
||||
|
@@ -105,12 +105,6 @@
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
& input[type="text"],
|
||||
input[type="email"],
|
||||
input[type="password"],
|
||||
input[type="number"],
|
||||
input[type="url"],
|
||||
input[type="date"],
|
||||
textarea,
|
||||
select,
|
||||
.pill-container,
|
||||
@@ -122,6 +116,22 @@
|
||||
border-color: hsl(0deg 0% 0% / 60%);
|
||||
}
|
||||
|
||||
input:not(.input-element) {
|
||||
&[type="text"],
|
||||
&[type="email"],
|
||||
&[type="password"],
|
||||
&[type="number"],
|
||||
&[type="url"],
|
||||
&[type="date"] {
|
||||
background-color: hsl(0deg 0% 0% / 20%);
|
||||
border-color: hsl(0deg 0% 0% / 60%);
|
||||
|
||||
&:focus {
|
||||
border-color: hsl(0deg 0% 0% / 90%);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.popover-filter-input-wrapper .popover-filter-input:focus {
|
||||
background-color: hsl(225deg 6% 7%);
|
||||
border: 1px solid hsl(0deg 0% 100% / 50%);
|
||||
@@ -148,9 +158,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
& input[type="text"]:focus,
|
||||
input[type="email"]:focus,
|
||||
input[type="number"]:focus,
|
||||
textarea:focus,
|
||||
textarea.new_message_textarea:focus,
|
||||
#compose_recipient_box:focus {
|
||||
|
95
web/styles/inputs.css
Normal file
95
web/styles/inputs.css
Normal file
@@ -0,0 +1,95 @@
|
||||
.input-active-styles {
|
||||
color: var(--color-text-input-focus);
|
||||
background-color: var(--color-background-input-focus);
|
||||
border-color: var(--color-border-input-focus);
|
||||
}
|
||||
|
||||
.input-element-wrapper {
|
||||
display: grid;
|
||||
grid-template:
|
||||
[input-element-start] "icon-starting-offset input-icon icon-content-gap content button-content-gap input-button button-ending-offset" auto [input-element-end] / [input-element-start] var(
|
||||
--input-icon-starting-offset
|
||||
)
|
||||
var(--input-icon-width) var(--input-icon-content-gap) minmax(0, 1fr)
|
||||
var(--input-button-content-gap) var(--input-button-width) var(
|
||||
--input-button-ending-offset
|
||||
)
|
||||
[input-element-end];
|
||||
align-items: center;
|
||||
|
||||
.input-element {
|
||||
grid-area: input-element;
|
||||
box-sizing: border-box;
|
||||
padding: 0.1875em 0.5em; /* 3px at 16px/1em and 8px at 16px/1em */
|
||||
font-size: var(--base-font-size-px);
|
||||
font-family: "Source Sans 3 VF", sans-serif;
|
||||
line-height: 1.25;
|
||||
text-overflow: ellipsis;
|
||||
color: var(--color-text-input);
|
||||
background: var(--color-background-input);
|
||||
border: 1px solid var(--color-border-input);
|
||||
border-radius: 4px;
|
||||
outline: none;
|
||||
transition: 0.1s linear;
|
||||
transition-property: border-color, box-shadow;
|
||||
|
||||
&:hover {
|
||||
border-color: var(--color-border-input-hover);
|
||||
}
|
||||
|
||||
&:focus {
|
||||
box-shadow: 0 0 5px var(--color-box-shadow-input-focus);
|
||||
}
|
||||
|
||||
&:focus,
|
||||
&.input-element-nonempty {
|
||||
@extend .input-active-styles;
|
||||
}
|
||||
}
|
||||
|
||||
&.has-input-icon .input-element {
|
||||
padding-left: calc(
|
||||
var(--input-icon-starting-offset) + var(--input-icon-width) +
|
||||
var(--input-icon-content-gap)
|
||||
);
|
||||
}
|
||||
|
||||
&.has-input-button .input-element {
|
||||
padding-right: calc(
|
||||
var(--input-button-content-gap) + var(--input-button-width) +
|
||||
var(--input-button-ending-offset)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
.input-icon {
|
||||
grid-area: input-icon;
|
||||
color: var(--color-text-input);
|
||||
/* We need to set the z-index, since the input icon
|
||||
comes before the input element in the DOM, but we
|
||||
want to display it over the input element in the UI. */
|
||||
z-index: 1;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.input-button {
|
||||
grid-area: input-button;
|
||||
padding: 0.25em; /* 4px at 16px/1em */
|
||||
}
|
||||
|
||||
.filter-input .input-element {
|
||||
&:placeholder-shown {
|
||||
/* In case of filter inputs, when the input field
|
||||
is empty, we hide the input button and adjust
|
||||
the right padding to compensate for the same. */
|
||||
padding-right: 0.5em;
|
||||
|
||||
~ .input-button {
|
||||
visibility: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
&:not(:placeholder-shown) {
|
||||
@extend .input-active-styles;
|
||||
}
|
||||
}
|
@@ -137,3 +137,7 @@ body {
|
||||
width: min(100%, 800px);
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.showroom-filter-input-container {
|
||||
width: clamp(200px, 100%, 400px);
|
||||
}
|
||||
|
@@ -702,7 +702,7 @@ i.zulip-icon:focus-visible,
|
||||
}
|
||||
|
||||
/* List of text-like input types taken from Bootstrap */
|
||||
input {
|
||||
input:not(.input-element) {
|
||||
&[type="text"],
|
||||
&[type="password"],
|
||||
&[type="datetime"],
|
||||
|
9
web/templates/components/input.hbs
Normal file
9
web/templates/components/input.hbs
Normal file
@@ -0,0 +1,9 @@
|
||||
<div {{#if id}}id="{{id}}"{{/if}} class="input-element-wrapper{{#if input_type}} {{input_type}}{{/if}}{{#if custom_classes}} {{custom_classes}}{{/if}}{{#if icon}} has-input-icon{{/if}}{{#if input_button_icon}} has-input-button{{/if}}">
|
||||
{{#if icon}}
|
||||
<i class="input-icon zulip-icon zulip-icon-{{icon}}" aria-hidden="true"></i>
|
||||
{{/if}}
|
||||
{{> @partial-block .}}
|
||||
{{#if input_button_icon}}
|
||||
{{> icon_button custom_classes="input-button" squared=true icon=input_button_icon intent="neutral" }}
|
||||
{{/if}}
|
||||
</div>
|
3
web/templates/components/showroom/filter_input.hbs
Normal file
3
web/templates/components/showroom/filter_input.hbs
Normal file
@@ -0,0 +1,3 @@
|
||||
{{#> input input_type="filter-input" icon="search" input_button_icon="close"}}
|
||||
<input class="input-element" type="text" placeholder="{{t 'Filter component' }}" />
|
||||
{{/input}}
|
Reference in New Issue
Block a user