mirror of
https://github.com/tutao/tutanota.git
synced 2025-12-07 13:49:47 +00:00
maintain separate implementation for LoginTextField
Co-authored-by: yoy <yoy@tutao.de> Co-authored-by: hak <hak@tutao.de>
This commit is contained in:
parent
afddfe11d1
commit
780a4dd3c9
5 changed files with 487 additions and 175 deletions
391
src/common/gui/base/LoginTextField.ts
Normal file
391
src/common/gui/base/LoginTextField.ts
Normal file
|
|
@ -0,0 +1,391 @@
|
|||
import m, { Children, ClassComponent, CVnode } from "mithril"
|
||||
import { font_size, px, size } from "../size"
|
||||
import { DefaultAnimationTime } from "../animation/Animations"
|
||||
import { theme } from "../theme"
|
||||
import type { MaybeTranslation } from "../../misc/LanguageViewModel"
|
||||
import { lang } from "../../misc/LanguageViewModel"
|
||||
import type { lazy } from "@tutao/tutanota-utils"
|
||||
import { isKeyPressed, keyHandler, useKeyHandler } from "../../misc/KeyManager"
|
||||
import { Keys, TabIndex } from "../../api/common/TutanotaConstants"
|
||||
import { ClickHandler, getOperatingClasses } from "./GuiUtils"
|
||||
import { AriaPopupType } from "../AriaUtils.js"
|
||||
import { AllIcons, Icon, IconSize } from "./Icon"
|
||||
import { Autocapitalize, Autocomplete, TextFieldType } from "./TextField"
|
||||
|
||||
export type LoginTextFieldAttrs = {
|
||||
id?: string
|
||||
label: MaybeTranslation
|
||||
value: string
|
||||
autocompleteAs?: Autocomplete
|
||||
autocapitalize?: Autocapitalize
|
||||
type?: TextFieldType
|
||||
hasPopup?: AriaPopupType
|
||||
helpLabel?: lazy<Children> | null
|
||||
alignRight?: boolean
|
||||
injectionsLeft?: lazy<Children>
|
||||
// only used by the BubbleTextField (-> uses old TextField) to display bubbles and out of office notification
|
||||
injectionsRight?: lazy<Children>
|
||||
keyHandler?: keyHandler
|
||||
onDomInputCreated?: (dom: HTMLInputElement) => void
|
||||
// interceptor used by the BubbleTextField to react on certain keys
|
||||
onfocus?: (dom: HTMLElement, input: HTMLInputElement) => unknown
|
||||
onblur?: (...args: Array<any>) => any
|
||||
maxWidth?: number
|
||||
class?: string
|
||||
style?: Record<string, any> //Temporary, Do not use
|
||||
disabled?: boolean
|
||||
// Creates a dummy TextField without interactively & disabled styling
|
||||
isReadOnly?: boolean
|
||||
oninput?: (value: string, input: HTMLInputElement) => unknown
|
||||
onclick?: ClickHandler
|
||||
doShowBorder?: boolean | null
|
||||
fontSize?: string
|
||||
min?: number
|
||||
max?: number
|
||||
leadingIcon?: {
|
||||
icon: AllIcons
|
||||
color: string
|
||||
}
|
||||
|
||||
/** This is called whenever the return key is pressed; overrides keyHandler */
|
||||
onReturnKeyPressed?: () => unknown
|
||||
}
|
||||
|
||||
const inputMarginTop = font_size.small + size.spacing_4 + 3
|
||||
|
||||
// this is not always correct because font size can be bigger/smaller, and we ideally should take that into account
|
||||
const baseLabelPosition = "-50%"
|
||||
// it should fit
|
||||
// compact button + 1 px border + 1 px padding to keep things centered = 32
|
||||
// 24px line-height + 12px label + some space between them = 36 + ?
|
||||
const minInputHeight = 56
|
||||
|
||||
export class LoginTextField implements ClassComponent<LoginTextFieldAttrs> {
|
||||
active: boolean
|
||||
onblur: EventListener | null = null
|
||||
domInput!: HTMLInputElement
|
||||
_domWrapper!: HTMLElement
|
||||
private _domLabel!: HTMLElement
|
||||
private _domInputWrapper!: HTMLElement
|
||||
private _didAutofill!: boolean
|
||||
|
||||
constructor() {
|
||||
this.active = false
|
||||
}
|
||||
|
||||
view(vnode: CVnode<LoginTextFieldAttrs>): Children {
|
||||
const a = vnode.attrs
|
||||
const maxWidth = a.maxWidth
|
||||
const labelBase = !this.active && a.value === "" && !a.isReadOnly && !this._didAutofill && !a.injectionsLeft
|
||||
const labelTransitionSpeed = DefaultAnimationTime / 2
|
||||
const doShowBorder = a.doShowBorder !== false
|
||||
const borderWidth = 3
|
||||
const borderColor = this.active ? theme.primary : "transparent"
|
||||
const borderBottomRadius = this.active ? "0px" : px(size.radius_8)
|
||||
|
||||
const borderRadius = `${px(size.radius_8)} ${px(size.radius_8)} ${borderBottomRadius} ${borderBottomRadius}`
|
||||
|
||||
return [
|
||||
m(
|
||||
".login-textfield.rel.overflow-hidden",
|
||||
{
|
||||
id: vnode.attrs.id,
|
||||
oncreate: (vnode) => (this._domWrapper = vnode.dom as HTMLElement),
|
||||
onclick: (e: MouseEvent) => (a.onclick ? a.onclick(e, this._domInputWrapper) : this.focus(e, a)),
|
||||
"aria-haspopup": a.hasPopup,
|
||||
"data-testid": `tf:${lang.getTestId(a.label)}`,
|
||||
class: a.class != null ? a.class : "mt-16" + " " + getOperatingClasses(a.disabled),
|
||||
style: maxWidth
|
||||
? {
|
||||
maxWidth: px(maxWidth),
|
||||
borderRadius,
|
||||
...a.style,
|
||||
}
|
||||
: { ...a.style, borderRadius },
|
||||
},
|
||||
[
|
||||
m(
|
||||
"label.abs.text-ellipsis.noselect.z1.pr-4",
|
||||
{
|
||||
"aria-hidden": "true",
|
||||
class: this.active ? "" : "" + " " + getOperatingClasses(a.disabled),
|
||||
oncreate: (vnode) => {
|
||||
this._domLabel = vnode.dom as HTMLElement
|
||||
},
|
||||
style: {
|
||||
fontSize: `${labelBase ? font_size.base : font_size.small}px`,
|
||||
transform: `translateY(${labelBase ? baseLabelPosition : 0})`,
|
||||
"transition-timing-function": "ease-out",
|
||||
"transition-duration": `${labelTransitionSpeed}ms`,
|
||||
"transition-property": "transform, font-size, top, color",
|
||||
top: labelBase ? "50%" : px(size.spacing_8),
|
||||
left: a.leadingIcon ? px(size.icon_24 + size.spacing_16) : 0,
|
||||
"padding-left": px(size.spacing_16),
|
||||
"padding-right": px(size.spacing_16),
|
||||
color: !this.active ? "inherit" : theme.primary,
|
||||
},
|
||||
},
|
||||
lang.getTranslationText(a.label),
|
||||
),
|
||||
m(".flex.flex-column", [
|
||||
// another wrapper to fix IE 11 min-height bug https://github.com/philipwalton/flexbugs#3-min-height-on-a-flex-container-wont-apply-to-its-flex-items
|
||||
m(
|
||||
".flex.items-end.flex-wrap",
|
||||
{
|
||||
// .flex-wrap
|
||||
style: {
|
||||
"min-height": px(minInputHeight),
|
||||
"border-bottom": doShowBorder ? `${px(borderWidth)} solid ${borderColor}` : "",
|
||||
transition: `border-bottom ${labelTransitionSpeed}ms ease-out`,
|
||||
},
|
||||
},
|
||||
[
|
||||
a.leadingIcon &&
|
||||
m(Icon, {
|
||||
size: IconSize.PX20,
|
||||
icon: a.leadingIcon.icon,
|
||||
style: {
|
||||
fill: a.leadingIcon.color,
|
||||
"align-self": "center",
|
||||
"padding-left": px(16),
|
||||
position: "relative",
|
||||
top: px(borderWidth / 2),
|
||||
},
|
||||
}),
|
||||
a.injectionsLeft ? a.injectionsLeft() : null, // additional wrapper element for bubble input field. input field should always be in one line with right injections
|
||||
m(
|
||||
".inputWrapper.flex-space-between.items-end",
|
||||
{
|
||||
style: {
|
||||
minHeight: px(minInputHeight - 2), // minus padding
|
||||
|
||||
"padding-left": px(size.spacing_16),
|
||||
"padding-right": px(size.spacing_16),
|
||||
},
|
||||
oncreate: (vnode) => (this._domInputWrapper = vnode.dom as HTMLElement),
|
||||
},
|
||||
[
|
||||
a.type !== TextFieldType.Area ? this._getInputField(a) : this._getTextArea(a),
|
||||
a.injectionsRight
|
||||
? m(
|
||||
".flex-end.items-center",
|
||||
{
|
||||
style: {
|
||||
minHeight: px(minInputHeight - 2),
|
||||
position: "relative",
|
||||
top: px(borderWidth / 2),
|
||||
},
|
||||
},
|
||||
a.injectionsRight(),
|
||||
)
|
||||
: null,
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
]),
|
||||
],
|
||||
),
|
||||
|
||||
a.helpLabel &&
|
||||
m(
|
||||
"small.noselect",
|
||||
{
|
||||
onclick: (e: MouseEvent) => {
|
||||
e.stopPropagation()
|
||||
},
|
||||
},
|
||||
a.helpLabel(),
|
||||
),
|
||||
]
|
||||
}
|
||||
|
||||
_getInputField(a: LoginTextFieldAttrs): Children {
|
||||
if (a.isReadOnly) {
|
||||
return m(
|
||||
".text-break.selectable",
|
||||
{
|
||||
style: {
|
||||
marginTop: px(inputMarginTop),
|
||||
lineHeight: px(font_size.line_height_input),
|
||||
},
|
||||
"data-testid": `tfi:${lang.getTestId(a.label)}`,
|
||||
},
|
||||
a.value,
|
||||
)
|
||||
} else {
|
||||
// Due to modern browser's 'smart' password managers that try to autofill everything
|
||||
// that remotely resembles a password field, we prepend invisible inputs to password fields
|
||||
// that shouldn't be autofilled.
|
||||
// since the autofill algorithm looks at inputs that come before and after the password field we need
|
||||
// three dummies.
|
||||
const autofillGuard: Children =
|
||||
a.autocompleteAs === Autocomplete.off
|
||||
? [
|
||||
m("input.abs", {
|
||||
style: {
|
||||
opacity: "0",
|
||||
height: "0",
|
||||
},
|
||||
tabIndex: TabIndex.Programmatic,
|
||||
type: TextFieldType.Text,
|
||||
}),
|
||||
m("input.abs", {
|
||||
style: {
|
||||
opacity: "0",
|
||||
height: "0",
|
||||
},
|
||||
tabIndex: TabIndex.Programmatic,
|
||||
type: TextFieldType.Password,
|
||||
}),
|
||||
m("input.abs", {
|
||||
style: {
|
||||
opacity: "0",
|
||||
height: "0",
|
||||
},
|
||||
tabIndex: TabIndex.Programmatic,
|
||||
type: TextFieldType.Text,
|
||||
}),
|
||||
]
|
||||
: []
|
||||
return m(
|
||||
".flex-grow.rel",
|
||||
autofillGuard.concat([
|
||||
m("input.input" + (a.alignRight ? ".right" : ""), {
|
||||
autocomplete: a.autocompleteAs ?? "",
|
||||
autocapitalize: a.autocapitalize,
|
||||
type: a.type,
|
||||
min: a.min,
|
||||
max: a.max,
|
||||
"aria-label": lang.getTranslationText(a.label),
|
||||
disabled: a.disabled,
|
||||
class: getOperatingClasses(a.disabled) + " text",
|
||||
oncreate: (vnode) => {
|
||||
this.domInput = vnode.dom as HTMLInputElement
|
||||
a.onDomInputCreated?.(this.domInput)
|
||||
this.domInput.value = a.value
|
||||
if (a.type !== TextFieldType.Area) {
|
||||
;(vnode.dom as HTMLElement).addEventListener("animationstart", (e: AnimationEvent) => {
|
||||
if (e.animationName === "onAutoFillStart") {
|
||||
this._didAutofill = true
|
||||
m.redraw()
|
||||
} else if (e.animationName === "onAutoFillCancel") {
|
||||
this._didAutofill = false
|
||||
m.redraw()
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
onfocus: (e: FocusEvent) => {
|
||||
this.focus(e, a)
|
||||
a.onfocus?.(this._domWrapper, this.domInput)
|
||||
},
|
||||
onblur: (e: FocusEvent) => this.blur(e, a),
|
||||
onkeydown: (e: KeyboardEvent) => {
|
||||
if (a.onReturnKeyPressed != null && e.key?.toLowerCase() === Keys.RETURN.code) {
|
||||
a.onReturnKeyPressed()
|
||||
return false
|
||||
}
|
||||
|
||||
const handled = useKeyHandler(e, a.keyHandler)
|
||||
if (!isKeyPressed(e.key, Keys.F1, Keys.TAB, Keys.ESC) && !(e.ctrlKey || e.metaKey)) {
|
||||
// When we are in a text field we don't want keys propagated up to act as hotkeys
|
||||
e.stopPropagation()
|
||||
}
|
||||
return handled
|
||||
},
|
||||
onupdate: () => {
|
||||
// only change the value if the value has changed otherwise the cursor in Safari and in the iOS App cannot be positioned.
|
||||
if (this.domInput.value !== a.value) {
|
||||
this.domInput.value = a.value
|
||||
}
|
||||
},
|
||||
oninput: () => {
|
||||
a.oninput?.(this.domInput.value, this.domInput)
|
||||
},
|
||||
onremove: () => {
|
||||
// We clean up any value that might still be in DOM e.g. password
|
||||
if (this.domInput) this.domInput.value = ""
|
||||
},
|
||||
style: {
|
||||
maxWidth: a.maxWidth,
|
||||
minWidth: px(20),
|
||||
// fix for edge browser. buttons are cut off in small windows otherwise
|
||||
lineHeight: px(font_size.line_height_input),
|
||||
fontSize: a.fontSize,
|
||||
position: "relative",
|
||||
bottom: px(size.spacing_4),
|
||||
},
|
||||
"data-testid": `tfi:${lang.getTestId(a.label)}`,
|
||||
}),
|
||||
]),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
_getTextArea(a: LoginTextFieldAttrs): Children {
|
||||
if (a.isReadOnly) {
|
||||
return m(
|
||||
".text-prewrap.text-break.selectable",
|
||||
{
|
||||
style: {
|
||||
marginTop: px(inputMarginTop),
|
||||
lineHeight: px(font_size.line_height_input),
|
||||
},
|
||||
},
|
||||
a.value,
|
||||
)
|
||||
} else {
|
||||
return m("textarea.input-area.text-pre", {
|
||||
"aria-label": lang.getTranslationText(a.label),
|
||||
disabled: a.disabled,
|
||||
autocapitalize: a.autocapitalize,
|
||||
class: getOperatingClasses(a.disabled) + " text",
|
||||
oncreate: (vnode) => {
|
||||
this.domInput = vnode.dom as HTMLInputElement
|
||||
this.domInput.value = a.value
|
||||
this.domInput.style.height = px(Math.max(a.value.split("\n").length, 1) * font_size.line_height_input) // display all lines on creation of text area
|
||||
},
|
||||
onfocus: (e: FocusEvent) => this.focus(e, a),
|
||||
onblur: (e: FocusEvent) => this.blur(e, a),
|
||||
onkeydown: (e: KeyboardEvent) => useKeyHandler(e, a.keyHandler),
|
||||
oninput: () => {
|
||||
this.domInput.style.height = "0px"
|
||||
this.domInput.style.height = px(this.domInput.scrollHeight)
|
||||
a.oninput?.(this.domInput.value, this.domInput)
|
||||
},
|
||||
onupdate: () => {
|
||||
// only change the value if the value has changed otherwise the cursor in Safari and in the iOS App cannot be positioned.
|
||||
if (this.domInput.value !== a.value) {
|
||||
this.domInput.value = a.value
|
||||
}
|
||||
},
|
||||
style: {
|
||||
marginTop: px(inputMarginTop),
|
||||
lineHeight: px(font_size.line_height_input),
|
||||
minWidth: px(20), // fix for edge browser. buttons are cut off in small windows otherwise
|
||||
fontSize: a.fontSize,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
focus(e: Event, a: LoginTextFieldAttrs) {
|
||||
if (!this.active && !a.disabled && !a.isReadOnly) {
|
||||
this.active = true
|
||||
this.domInput.focus()
|
||||
|
||||
this._domWrapper.classList.add("active")
|
||||
}
|
||||
}
|
||||
|
||||
blur(e: Event, a: LoginTextFieldAttrs) {
|
||||
this._domWrapper.classList.remove("active")
|
||||
this.active = false
|
||||
if (a.onblur instanceof Function) a.onblur(e)
|
||||
}
|
||||
|
||||
isEmpty(value: string): boolean {
|
||||
return value === ""
|
||||
}
|
||||
}
|
||||
|
|
@ -9,7 +9,6 @@ import { isKeyPressed, keyHandler, useKeyHandler } from "../../misc/KeyManager"
|
|||
import { Keys, TabIndex } from "../../api/common/TutanotaConstants"
|
||||
import { ClickHandler, getOperatingClasses } from "./GuiUtils"
|
||||
import { AriaPopupType } from "../AriaUtils.js"
|
||||
import { AllIcons, Icon, IconSize } from "./Icon"
|
||||
|
||||
export type TextFieldAttrs = {
|
||||
id?: string
|
||||
|
|
@ -41,10 +40,6 @@ export type TextFieldAttrs = {
|
|||
fontSize?: string
|
||||
min?: number
|
||||
max?: number
|
||||
leadingIcon?: {
|
||||
icon: AllIcons
|
||||
color: string
|
||||
}
|
||||
|
||||
/** This is called whenever the return key is pressed; overrides keyHandler */
|
||||
onReturnKeyPressed?: () => unknown
|
||||
|
|
@ -85,11 +80,11 @@ export const enum Autocapitalize {
|
|||
const inputMarginTop = font_size.small + size.spacing_4 + 3
|
||||
|
||||
// this is not always correct because font size can be bigger/smaller, and we ideally should take that into account
|
||||
const baseLabelPosition = "-50%"
|
||||
const baseLabelPosition = 21
|
||||
// it should fit
|
||||
// compact button + 1 px border + 1 px padding to keep things centered = 32
|
||||
// 24px line-height + 12px label + some space between them = 36 + ?
|
||||
const minInputHeight = 56
|
||||
const minInputHeight = 46
|
||||
|
||||
export class TextField implements ClassComponent<TextFieldAttrs> {
|
||||
active: boolean
|
||||
|
|
@ -110,129 +105,93 @@ export class TextField implements ClassComponent<TextFieldAttrs> {
|
|||
const labelBase = !this.active && a.value === "" && !a.isReadOnly && !this._didAutofill && !a.injectionsLeft
|
||||
const labelTransitionSpeed = DefaultAnimationTime / 2
|
||||
const doShowBorder = a.doShowBorder !== false
|
||||
const borderWidth = 3
|
||||
const borderColor = this.active ? theme.primary : "transparent"
|
||||
const borderBottomRadius = this.active ? "0px" : px(size.radius_8)
|
||||
|
||||
const baseStyling = {
|
||||
"background-color": theme.surface_container_high,
|
||||
"border-radius": `${px(size.radius_8)} ${px(size.radius_8)} ${borderBottomRadius} ${borderBottomRadius}`,
|
||||
transition: `border-radius ${labelTransitionSpeed}ms ease-out`,
|
||||
}
|
||||
|
||||
return [
|
||||
m(
|
||||
".text-field.rel.overflow-hidden",
|
||||
{
|
||||
id: vnode.attrs.id,
|
||||
oncreate: (vnode) => (this._domWrapper = vnode.dom as HTMLElement),
|
||||
onclick: (e: MouseEvent) => (a.onclick ? a.onclick(e, this._domInputWrapper) : this.focus(e, a)),
|
||||
"aria-haspopup": a.hasPopup,
|
||||
"data-testid": `tf:${lang.getTestId(a.label)}`,
|
||||
class: a.class != null ? a.class : "mt-16" + " " + getOperatingClasses(a.disabled),
|
||||
style: maxWidth
|
||||
? {
|
||||
maxWidth: px(maxWidth),
|
||||
...a.style,
|
||||
...baseStyling,
|
||||
}
|
||||
: { ...a.style, ...baseStyling },
|
||||
},
|
||||
[
|
||||
m(
|
||||
"label.abs.text-ellipsis.noselect.z1.pr-4",
|
||||
{
|
||||
"aria-hidden": "true",
|
||||
class: this.active ? "" : "" + " " + getOperatingClasses(a.disabled),
|
||||
oncreate: (vnode) => {
|
||||
this._domLabel = vnode.dom as HTMLElement
|
||||
},
|
||||
style: {
|
||||
fontSize: `${labelBase ? font_size.base : font_size.small}px`,
|
||||
transform: `translateY(${labelBase ? baseLabelPosition : 0})`,
|
||||
"transition-timing-function": "ease-out",
|
||||
"transition-duration": `${labelTransitionSpeed}ms`,
|
||||
"transition-property": "transform, font-size, top, color",
|
||||
top: labelBase ? "50%" : px(size.spacing_8),
|
||||
left: a.leadingIcon ? px(size.icon_24 + size.spacing_16) : 0,
|
||||
"padding-left": px(size.spacing_16),
|
||||
"padding-right": px(size.spacing_16),
|
||||
color: !this.active ? "inherit" : theme.primary,
|
||||
},
|
||||
},
|
||||
lang.getTranslationText(a.label),
|
||||
),
|
||||
m(".flex.flex-column", [
|
||||
// another wrapper to fix IE 11 min-height bug https://github.com/philipwalton/flexbugs#3-min-height-on-a-flex-container-wont-apply-to-its-flex-items
|
||||
m(
|
||||
".flex.items-end.flex-wrap",
|
||||
{
|
||||
// .flex-wrap
|
||||
style: {
|
||||
"min-height": px(minInputHeight),
|
||||
"border-bottom": doShowBorder ? `${px(borderWidth)} solid ${borderColor}` : "",
|
||||
transition: `border-bottom ${labelTransitionSpeed}ms ease-out`,
|
||||
},
|
||||
},
|
||||
[
|
||||
a.leadingIcon &&
|
||||
m(Icon, {
|
||||
size: IconSize.PX20,
|
||||
icon: a.leadingIcon.icon,
|
||||
style: {
|
||||
fill: a.leadingIcon.color,
|
||||
"align-self": "center",
|
||||
"padding-left": px(16),
|
||||
position: "relative",
|
||||
top: px(borderWidth / 2),
|
||||
},
|
||||
}),
|
||||
a.injectionsLeft ? a.injectionsLeft() : null, // additional wrapper element for bubble input field. input field should always be in one line with right injections
|
||||
m(
|
||||
".inputWrapper.flex-space-between.items-end",
|
||||
{
|
||||
style: {
|
||||
minHeight: px(minInputHeight - 2), // minus padding
|
||||
|
||||
"padding-left": px(size.spacing_16),
|
||||
"padding-right": px(size.spacing_16),
|
||||
},
|
||||
oncreate: (vnode) => (this._domInputWrapper = vnode.dom as HTMLElement),
|
||||
},
|
||||
[
|
||||
a.type !== TextFieldType.Area ? this._getInputField(a) : this._getTextArea(a),
|
||||
a.injectionsRight
|
||||
? m(
|
||||
".flex-end.items-center",
|
||||
{
|
||||
style: {
|
||||
minHeight: px(minInputHeight - 2),
|
||||
position: "relative",
|
||||
top: px(borderWidth / 2),
|
||||
},
|
||||
},
|
||||
a.injectionsRight(),
|
||||
)
|
||||
: null,
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
]),
|
||||
],
|
||||
),
|
||||
|
||||
a.helpLabel &&
|
||||
const borderWidth = this.active ? "2px" : "1px"
|
||||
const borderColor = this.active ? theme.primary : theme.outline_variant
|
||||
return m(
|
||||
".text-field.rel.overflow-hidden",
|
||||
{
|
||||
id: vnode.attrs.id,
|
||||
oncreate: (vnode) => (this._domWrapper = vnode.dom as HTMLElement),
|
||||
onclick: (e: MouseEvent) => (a.onclick ? a.onclick(e, this._domInputWrapper) : this.focus(e, a)),
|
||||
"aria-haspopup": a.hasPopup,
|
||||
"data-testid": `tf:${lang.getTestId(a.label)}`,
|
||||
class: a.class != null ? a.class : "pt-16" + " " + getOperatingClasses(a.disabled),
|
||||
style: maxWidth
|
||||
? {
|
||||
maxWidth: px(maxWidth),
|
||||
...a.style,
|
||||
}
|
||||
: { ...a.style },
|
||||
},
|
||||
[
|
||||
m(
|
||||
"small.noselect",
|
||||
"label.abs.text-ellipsis.noselect.z1.i.pr-4",
|
||||
{
|
||||
onclick: (e: MouseEvent) => {
|
||||
e.stopPropagation()
|
||||
"aria-hidden": "true",
|
||||
class: this.active ? "content-accent-fg" : "" + " " + getOperatingClasses(a.disabled),
|
||||
oncreate: (vnode) => {
|
||||
this._domLabel = vnode.dom as HTMLElement
|
||||
},
|
||||
style: {
|
||||
fontSize: `${labelBase ? font_size.base : font_size.small}px`,
|
||||
transform: `translateY(${labelBase ? baseLabelPosition : 0}px)`,
|
||||
transition: `transform ${labelTransitionSpeed}ms ease-out, font-size ${labelTransitionSpeed}ms ease-out`,
|
||||
},
|
||||
},
|
||||
a.helpLabel(),
|
||||
lang.getTranslationText(a.label),
|
||||
),
|
||||
]
|
||||
m(".flex.flex-column", [
|
||||
// another wrapper to fix IE 11 min-height bug https://github.com/philipwalton/flexbugs#3-min-height-on-a-flex-container-wont-apply-to-its-flex-items
|
||||
m(
|
||||
".flex.items-end.flex-wrap",
|
||||
{
|
||||
// .flex-wrap
|
||||
style: {
|
||||
"min-height": px(minInputHeight),
|
||||
// 2 px border
|
||||
"padding-bottom": this.active ? px(0) : px(1),
|
||||
"border-bottom": doShowBorder ? `${borderWidth} solid ${borderColor}` : "",
|
||||
},
|
||||
},
|
||||
[
|
||||
a.injectionsLeft ? a.injectionsLeft() : null, // additional wrapper element for bubble input field. input field should always be in one line with right injections
|
||||
m(
|
||||
".inputWrapper.flex-space-between.items-end",
|
||||
{
|
||||
style: {
|
||||
minHeight: px(minInputHeight - 2), // minus padding
|
||||
},
|
||||
oncreate: (vnode) => (this._domInputWrapper = vnode.dom as HTMLElement),
|
||||
},
|
||||
[
|
||||
a.type !== TextFieldType.Area ? this._getInputField(a) : this._getTextArea(a),
|
||||
a.injectionsRight
|
||||
? m(
|
||||
".flex-end.items-center",
|
||||
{
|
||||
style: { minHeight: px(minInputHeight - 2) },
|
||||
},
|
||||
a.injectionsRight(),
|
||||
)
|
||||
: null,
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
]),
|
||||
a.helpLabel
|
||||
? m(
|
||||
"small.noselect",
|
||||
{
|
||||
onclick: (e: MouseEvent) => {
|
||||
e.stopPropagation()
|
||||
},
|
||||
},
|
||||
a.helpLabel(),
|
||||
)
|
||||
: [],
|
||||
],
|
||||
)
|
||||
}
|
||||
|
||||
_getInputField(a: TextFieldAttrs): Children {
|
||||
|
|
@ -348,8 +307,6 @@ export class TextField implements ClassComponent<TextFieldAttrs> {
|
|||
// fix for edge browser. buttons are cut off in small windows otherwise
|
||||
lineHeight: px(font_size.line_height_input),
|
||||
fontSize: a.fontSize,
|
||||
position: "relative",
|
||||
bottom: px(size.spacing_4),
|
||||
},
|
||||
"data-testid": `tfi:${lang.getTestId(a.label)}`,
|
||||
}),
|
||||
|
|
|
|||
|
|
@ -3274,5 +3274,14 @@ styles.registerStyle("main", () => {
|
|||
".tutaui-button-ghost:active": {
|
||||
"background-color": theme.state_bg_active,
|
||||
},
|
||||
".login-textfield": {
|
||||
"background-color": theme.surface_container_high,
|
||||
"transition-property": "border-radius, background-color",
|
||||
"transition-duration": `${DefaultAnimationTime / 2}ms`,
|
||||
"transition-timing-function": "ease-out",
|
||||
},
|
||||
".login-textfield:hover:not(:has(input:focus))": {
|
||||
"background-color": theme.state_bg_focus,
|
||||
},
|
||||
}
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,45 +0,0 @@
|
|||
import m, { Component, Vnode } from "mithril"
|
||||
import { TextField, TextFieldAttrs, TextFieldType } from "../gui/base/TextField"
|
||||
import { font_size, px, size } from "../gui/size"
|
||||
import { theme } from "../gui/theme"
|
||||
|
||||
export type SignupTextFieldAttrs = TextFieldAttrs
|
||||
|
||||
export class SignupTextField implements Component<SignupTextFieldAttrs> {
|
||||
view({ attrs }: Vnode<SignupTextFieldAttrs>) {
|
||||
const customStyle = {
|
||||
backgroundColor: theme.surface_container_highest,
|
||||
color: theme.on_surface_variant,
|
||||
border: "none",
|
||||
borderRadius: px(size.radius_8),
|
||||
paddingTop: px(size.spacing_8),
|
||||
paddingBottom: px(size.spacing_8),
|
||||
paddingRight: px(size.spacing_8),
|
||||
paddingLeft: px(40),
|
||||
"font-size": px(font_size.base),
|
||||
height: px(56),
|
||||
|
||||
...attrs.style,
|
||||
}
|
||||
return m(TextField, {
|
||||
// 1. Pass through functional attributes
|
||||
value: attrs.value,
|
||||
oninput: attrs.oninput,
|
||||
type: attrs.type || TextFieldType.Text,
|
||||
label: attrs.label,
|
||||
|
||||
leadingIcon: attrs.leadingIcon
|
||||
? {
|
||||
icon: attrs.leadingIcon.icon,
|
||||
color: theme.on_surface_variant,
|
||||
}
|
||||
: undefined,
|
||||
|
||||
style: customStyle as any,
|
||||
|
||||
onblur: attrs.onblur,
|
||||
onfocus: attrs.onfocus,
|
||||
onclick: attrs.onclick,
|
||||
} satisfies TextFieldAttrs)
|
||||
}
|
||||
}
|
||||
|
|
@ -56,8 +56,8 @@ import { UpgradeCongratulationsPageNew } from "../subscription/UpgradeCongratula
|
|||
import { RadioSelector, type RadioSelectorAttrs } from "../gui/base/RadioSelector"
|
||||
import { type RadioSelectorOption } from "../gui/base/RadioSelectorItem"
|
||||
import { theme } from "../gui/theme"
|
||||
import { TextField, TextFieldAttrs } from "../gui/base/TextField"
|
||||
import { Icons } from "../gui/base/icons/Icons"
|
||||
import { LoginTextField } from "../gui/base/LoginTextField"
|
||||
|
||||
assertMainOrNode()
|
||||
|
||||
|
|
@ -279,7 +279,7 @@ export class SignupView extends BaseTopLevelView implements TopLevelView<SignupV
|
|||
},
|
||||
|
||||
[
|
||||
m(TextField, {
|
||||
m(LoginTextField, {
|
||||
oninput: (newValue) => (this.wizardViewModel.addressInputStore = newValue),
|
||||
label: lang.getTranslation("address_label"),
|
||||
value: this.wizardViewModel.addressInputStore ?? "",
|
||||
|
|
@ -287,7 +287,7 @@ export class SignupView extends BaseTopLevelView implements TopLevelView<SignupV
|
|||
icon: Icons.Eye,
|
||||
color: theme.on_surface_variant,
|
||||
},
|
||||
} satisfies TextFieldAttrs),
|
||||
}),
|
||||
m(
|
||||
".flex-center.full-width.pt-32",
|
||||
m(LoginButton, {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue