feat: migrate form-controls to css modules (#2041)

* feat: migrate form-controls to css modules

* fix: checkbox field styles

* fix: remove old field styles

* fix: styles bugs

* CB-3773 fix: styles

* CB-3773 fix: field description styles

* CB-3773 fix: radio button styles

---------

Co-authored-by: Alexey <wrouds@gmail.com>
Co-authored-by: mr-anton-t <42037741+mr-anton-t@users.noreply.github.com>
This commit is contained in:
Dmitriy Osipov
2023-10-10 20:19:40 +03:00
committed by GitHub
co-authored by Alexey mr-anton-t
parent 4344dce700
commit fe2c7b76be
47 changed files with 644 additions and 744 deletions
@@ -1,2 +1,66 @@
.checkbox {
composes: theme-checkbox from global;
box-sizing: content-box !important;
}
.checkboxInput {
composes: theme-checkbox_native-control from global;
}
.checkboxBackground {
composes: theme-checkbox__background from global;
box-sizing: border-box !important;
}
.checkboxCheckmark {
composes: theme-checkbox__checkmark from global;
}
.checkboxCheckmarkPath {
composes: theme-checkbox__checkmark-path from global;
}
.checkboxMixedmark {
composes: theme-checkbox__mixedmark from global;
}
.checkboxRipple {
composes: theme-checkbox__ripple from global;
}
.checkboxContainer {
display: flex;
align-items: center;
height: 32px;
}
.checkboxLabel {
composes: theme-typography--body2 from global;
cursor: pointer;
}
.checkboxCaption {
composes: theme-text-text-hint-on-light theme-typography--caption from global;
}
.primary {
composes: theme-checkbox_primary from global;
}
.surface {
composes: theme-checkbox_surface from global;
}
.small {
composes: theme-checkbox_small from global;
}
.small {
&.checkboxContainer {
& .checkbox {
width: 14px;
height: 14px;
}
& .checkboxBackground {
width: 14px;
height: 14px;
}
}
}
.disabled {
composes: theme-checkbox--disabled from global;
}
.checked {
composes: theme-checkbox--checked from global;
}
@@ -6,103 +6,19 @@
* you may not use this file except in compliance with the License.
*/
import { useLayoutEffect, useRef } from 'react';
import styled, { css } from 'reshadow';
import type { ComponentStyle } from '@cloudbeaver/core-theming';
import { s } from '../../s';
import { useS } from '../../useS';
import { useStyles } from '../../useStyles';
import CheckboxMarkupStyles from './CheckboxMarkup.m.css';
export type CheckboxMod = 'primary' | 'surface' | 'small';
const checkboxStyles = css`
checkbox {
composes: theme-checkbox from global;
box-sizing: content-box !important;
}
checkbox-input {
composes: theme-checkbox_native-control from global;
}
checkbox-background {
composes: theme-checkbox__background from global;
box-sizing: border-box !important;
}
checkbox-checkmark {
composes: theme-checkbox__checkmark from global;
}
checkbox-checkmark-path {
composes: theme-checkbox__checkmark-path from global;
}
checkbox-mixedmark {
composes: theme-checkbox__mixedmark from global;
}
checkbox-ripple {
composes: theme-checkbox__ripple from global;
}
checkbox-container {
display: flex;
align-items: center;
height: 32px;
}
checkbox-label {
composes: theme-typography--body2 from global;
cursor: pointer;
}
checkbox-caption {
composes: theme-text-text-hint-on-light theme-typography--caption from global;
}
`;
const checkboxMod: Record<CheckboxMod, any> = {
primary: css`
checkbox {
composes: theme-checkbox_primary from global;
}
`,
surface: css`
checkbox {
composes: theme-checkbox_surface from global;
}
`,
small: css`
checkbox {
composes: theme-checkbox_small from global;
}
checkbox-container {
& checkbox {
width: 14px;
height: 14px;
}
& checkbox-background {
width: 14px;
height: 14px;
}
}
`,
};
const checkboxState = {
disabled: css`
checkbox {
composes: theme-checkbox--disabled from global;
}
`,
checked: css`
checkbox {
composes: theme-checkbox--checked from global;
}
`,
};
interface ICheckboxMarkupProps extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'style'> {
label?: string;
caption?: string;
indeterminate?: boolean;
ripple?: boolean;
mod?: CheckboxMod[];
style?: ComponentStyle;
}
export { CheckboxMarkupStyles };
@@ -115,7 +31,6 @@ export const CheckboxMarkup: React.FC<ICheckboxMarkupProps> = function CheckboxM
title,
mod = ['primary'],
ripple = true,
style,
readOnly,
caption,
...rest
@@ -129,32 +44,40 @@ export const CheckboxMarkup: React.FC<ICheckboxMarkupProps> = function CheckboxM
}
});
return styled(
useStyles(
checkboxStyles,
...(mod || []).map(mod => checkboxMod[mod]),
rest.disabled && checkboxState.disabled,
rest.checked && checkboxState.checked,
style,
),
)(
<checkbox-container className={className} title={title}>
<checkbox className={s(styles, { checkbox: true })}>
<checkbox-input ref={checkboxRef} as="input" type="checkbox" {...rest} disabled={rest.disabled || readOnly} id={id || rest.name} />
<checkbox-background>
<checkbox-checkmark as="svg" viewBox="0 0 24 24">
<checkbox-checkmark-path as="path" fill="none" d="M1.73,12.91 8.1,19.28 22.79,4.59" />
</checkbox-checkmark>
<checkbox-mixedmark />
</checkbox-background>
{ripple && <checkbox-ripple />}
</checkbox>
return (
<div className={s(styles, { checkboxContainer: true, small: mod.includes('small') }, className)} title={title}>
<div
className={s(styles, {
checkbox: true,
disabled: rest.disabled,
checked: rest.checked,
primary: mod.includes('primary'),
surface: mod.includes('surface'),
small: mod.includes('small'),
})}
>
<input
ref={checkboxRef}
className={s(styles, { checkboxInput: true })}
type="checkbox"
{...rest}
disabled={rest.disabled || readOnly}
id={id || rest.name}
/>
<div className={s(styles, { checkboxBackground: true })}>
<svg className={s(styles, { checkboxCheckmark: true })} viewBox="0 0 24 24">
<path className={s(styles, { checkboxCheckmarkPath: true })} fill="none" d="M1.73,12.91 8.1,19.28 22.79,4.59" />
</svg>
<div className={s(styles, { checkboxMixedmark: true })} />
</div>
{ripple && <div className={s(styles, { checkboxRipple: true })} />}
</div>
{label && (id || rest.name) && (
<checkbox-label as="label" htmlFor={id || rest.name}>
<label className={s(styles, { checkboxLabel: true })} htmlFor={id || rest.name}>
{label}
{caption && <checkbox-caption>{caption}</checkbox-caption>}
</checkbox-label>
{caption && <div className={s(styles, { checkboxCaption: true })}>{caption}</div>}
</label>
)}
</checkbox-container>,
</div>
);
};
@@ -5,36 +5,39 @@
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
*/
import { observer } from 'mobx-react-lite';
import { filterLayoutFakeProps, getLayoutProps } from '../../Containers/filterLayoutFakeProps';
import elementsSizeStyles from '../../Containers/shared/ElementsSize.m.css';
import { s } from '../../s';
import { useS } from '../../useS';
import formControlStyles from '../FormControl.m.css';
import { Field } from '../Field';
import { FieldLabel } from '../FieldLabel';
import { isControlPresented } from '../isControlPresented';
import { Checkbox, CheckboxBaseProps, CheckboxType, ICheckboxControlledProps, ICheckboxObjectProps } from './Checkbox';
import fieldCheckboxStyles from './FieldCheckbox.m.css';
export const FieldCheckbox: CheckboxType = function FieldCheckbox({
export const FieldCheckbox: CheckboxType = observer(function FieldCheckbox({
children,
className,
...rest
}: CheckboxBaseProps & (ICheckboxControlledProps | ICheckboxObjectProps<any>)) {
const layoutProps = getLayoutProps(rest);
const checkboxProps = filterLayoutFakeProps(rest);
const styles = useS(elementsSizeStyles, formControlStyles, fieldCheckboxStyles);
const styles = useS(fieldCheckboxStyles);
if (checkboxProps.autoHide && !isControlPresented(checkboxProps.name, checkboxProps.state)) {
return null;
}
return (
<div data-testid="field" className={s(styles, { field: true, ...layoutProps }, className)}>
<Checkbox {...(checkboxProps as CheckboxBaseProps & ICheckboxControlledProps)} className={styles.checkbox} />
<Field {...layoutProps} className={s(styles, { field: true }, className)}>
<Checkbox {...(checkboxProps as CheckboxBaseProps & ICheckboxControlledProps)} className={s(styles, { checkbox: true })} />
{children && (
<label data-testid="field-label" htmlFor={checkboxProps.id || checkboxProps.name} title={checkboxProps.title} className={styles.fieldLabel}>
<FieldLabel htmlFor={checkboxProps.id || checkboxProps.name} title={checkboxProps.title} className={s(styles, { fieldLabel: true })}>
{children}
</label>
</FieldLabel>
)}
</div>
</Field>
);
};
});
@@ -8,10 +8,11 @@
import { observer } from 'mobx-react-lite';
import { filterLayoutFakeProps } from '../../Containers/filterLayoutFakeProps';
import elementsSizeStyles from '../../Containers/shared/ElementsSize.m.css';
import { s } from '../../s';
import { useS } from '../../useS';
import formControlStyles from '../FormControl.m.css';
import { Field } from '../Field';
import { FieldDescription } from '../FieldDescription';
import { FieldLabel } from '../FieldLabel';
import { isControlPresented } from '../isControlPresented';
import type { ICheckboxControlledProps, ICheckboxObjectProps } from './Checkbox';
import switchStyles from './Switch.m.css';
@@ -64,14 +65,14 @@ export const Switch: SwitchType = observer(function Switch({
onChange,
});
rest = filterLayoutFakeProps(rest);
const styles = useS(elementsSizeStyles, formControlStyles, switchStyles, ...mod.map(mod => switchMod[mod]));
const styles = useS(switchStyles, ...mod.map(mod => switchMod[mod]));
if (autoHide && !isControlPresented(name, state)) {
return null;
}
return (
<div data-testid="field" className={s(styles, { field: true }, className)} title={rest.title}>
<Field data-testid="field" title={rest.title}>
<div data-testid="switch-body" className={styles.switchBody}>
<div data-testid="switch-control" className={s(styles, { switchControl: true, disabled: disabled, checked: checkboxState.checked })}>
<div data-testid="switch-control-track" className={styles.switchControlTrack} />
@@ -91,15 +92,11 @@ export const Switch: SwitchType = observer(function Switch({
/>
</div>
</div>
<label htmlFor={id || value || name} data-testid="field-label" className={styles.fieldLabel}>
<FieldLabel htmlFor={id || value || name} data-testid="field-label" className={styles.fieldLabel}>
{children}
</label>
</FieldLabel>
</div>
{description && (
<div data-testid="field-description" className={styles.fieldDescription}>
{description}
</div>
)}
</div>
{description && <FieldDescription>{description}</FieldDescription>}
</Field>
);
});
@@ -23,6 +23,15 @@
padding-right: 24px !important;
}
.input {
font-size: 12px;
&.select {
cursor: pointer;
user-select: none;
}
}
.menuButton {
position: absolute;
right: 0;
@@ -11,7 +11,6 @@ import { Menu, MenuButton, MenuItem, useMenuState } from 'reakit/Menu';
import { filterLayoutFakeProps, getLayoutProps } from '../Containers/filterLayoutFakeProps';
import type { ILayoutSizeProps } from '../Containers/ILayoutSizeProps';
import elementsSizeStyles from '../Containers/shared/ElementsSize.m.css';
import { getComputed } from '../getComputed';
import { Icon } from '../Icon';
import { IconOrImage } from '../IconOrImage';
@@ -20,8 +19,10 @@ import { useTranslate } from '../localization/useTranslate';
import { s } from '../s';
import { useS } from '../useS';
import comboboxStyles from './Combobox.m.css';
import { Field } from './Field';
import { FieldDescription } from './FieldDescription';
import { FieldLabel } from './FieldLabel';
import { FormContext } from './FormContext';
import formControlStyles from './FormControl.m.css';
type BaseProps<TKey, TValue> = Omit<React.InputHTMLAttributes<HTMLInputElement>, 'onChange' | 'onSelect' | 'name' | 'value' | 'defaultValue'> &
ILayoutSizeProps & {
@@ -93,7 +94,7 @@ export const Combobox: ComboboxType = observer(function Combobox({
const context = useContext(FormContext);
const menuRef = useRef<HTMLDivElement>(null);
const [inputRef, setInputRef] = useState<HTMLInputElement | null>(null);
const styles = useS(elementsSizeStyles, formControlStyles, comboboxStyles);
const styles = useS(comboboxStyles);
const menu = useMenuState({
placement: 'bottom-end',
@@ -255,17 +256,22 @@ export const Combobox: ComboboxType = observer(function Combobox({
}
return (
<div data-testid="field" className={s(styles, { field: true, inline, ...layoutProps }, className)}>
<Field {...layoutProps} className={s(styles, { field: true, inline }, className)}>
{children && (
<div title={title} data-testid="field-label" className={styles.fieldLabel}>
<FieldLabel required={rest.required} title={title} className={s(styles, { fieldLabel: true })}>
{children}
{rest.required && ' *'}
</div>
</FieldLabel>
)}
<div data-testid="input-box" className={styles.inputBox}>
<div className={s(styles, { inputBox: true })}>
{(icon || loading) && (
<div data-testid="input-icon" className={styles.inputIcon}>
{loading ? <Loader small fullSize /> : typeof icon === 'string' ? <IconOrImage icon={icon} className={styles.iconOrImage} /> : icon}
<div className={s(styles, { inputIcon: true })}>
{loading ? (
<Loader small fullSize />
) : typeof icon === 'string' ? (
<IconOrImage icon={icon} className={s(styles, { iconOrImage: true })} />
) : (
icon
)}
</div>
)}
<input
@@ -291,13 +297,11 @@ export const Combobox: ComboboxType = observer(function Combobox({
{...menu}
ref={menuRef}
aria-label={propertyName}
// unstable_finalFocusRef={inputRef || undefined}
// unstable_initialFocusRef={ref}
className={styles.menu}
className={s(styles, { menu: true })}
modal
>
{!filteredItems.length ? (
<MenuItem id="placeholder" disabled {...menu} className={styles.menuItem}>
<MenuItem id="placeholder" disabled {...menu} className={s(styles, { menuItem: true })}>
{translate('combobox_no_results_placeholder')}
</MenuItem>
) : (
@@ -314,28 +318,22 @@ export const Combobox: ComboboxType = observer(function Combobox({
title={title}
{...menu}
disabled={disabled}
className={styles.menuItem}
className={s(styles, { menuItem: true })}
onClick={event => handleSelect(event.currentTarget.id)}
>
{iconSelector && (
<div data-testid="item-icon" className={styles.itemIcon}>
{icon && typeof icon === 'string' ? <IconOrImage icon={icon} className={styles.iconOrImage} /> : icon}
<div data-testid="item-icon" className={s(styles, { itemIcon: true })}>
{icon && typeof icon === 'string' ? <IconOrImage icon={icon} className={s(styles, { iconOrImage: true })} /> : icon}
</div>
)}
<div data-testid="item-value" className={styles.itemValue}>
{valueSelector(item)}
</div>
<div data-testid="item-value">{valueSelector(item)}</div>
</MenuItem>
);
})
)}
</Menu>
</div>
{description && (
<div data-testid="field-description" className={styles.fieldDescription}>
{description}
</div>
)}
</div>
{description && <FieldDescription>{description}</FieldDescription>}
</Field>
);
});
@@ -0,0 +1,10 @@
.field {
box-sizing: border-box;
max-width: 100%;
/* nested <Field> */
& .field {
height: 32px;
padding: 0;
}
}
@@ -0,0 +1,25 @@
import { observer } from 'mobx-react-lite';
import type { HTMLAttributes, PropsWithChildren } from 'react';
import { getLayoutProps } from '../Containers/filterLayoutFakeProps';
import type { ILayoutSizeProps } from '../Containers/ILayoutSizeProps';
import elementsSizeStyles from '../Containers/shared/ElementsSize.m.css';
import { s } from '../s';
import { useS } from '../useS';
import fieldStyles from './Field.m.css';
type Props = ILayoutSizeProps &
HTMLAttributes<HTMLDivElement> & {
className?: string;
};
export const Field: React.FC<PropsWithChildren<Props>> = observer(function Field({ children, className, ...rest }) {
const styles = useS(fieldStyles, elementsSizeStyles);
const layoutProps = getLayoutProps(rest);
return (
<div {...rest} className={s(styles, { ...layoutProps, field: true }, className)}>
{children}
</div>
);
});
@@ -0,0 +1,15 @@
.fieldDescription {
composes: theme-typography--caption from global;
/*color: var(--theme-text-hint-on-background);*/
box-sizing: border-box;
padding-top: 4px;
min-height: 24px;
&.invalid {
color: var(--theme-negative);
}
&:empty {
display: none;
}
}
@@ -0,0 +1,16 @@
import { observer } from 'mobx-react-lite';
import type { PropsWithChildren } from 'react';
import { s } from '../s';
import { useS } from '../useS';
import fieldDescriptionStyles from './FieldDescription.m.css';
interface Props {
className?: string;
invalid?: boolean;
}
export const FieldDescription: React.FC<PropsWithChildren<Props>> = observer(function FieldDescription({ children, className, invalid }) {
const styles = useS(fieldDescriptionStyles);
return <div className={s(styles, { fieldDescription: true, invalid }, className)}>{children}</div>;
});
@@ -0,0 +1,7 @@
.fieldLabel {
composes: theme-typography--body1 from global;
box-sizing: border-box;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
@@ -0,0 +1,22 @@
import { observer } from 'mobx-react-lite';
import type { LabelHTMLAttributes, PropsWithChildren } from 'react';
import { s } from '../s';
import { useS } from '../useS';
import fieldLabelStyles from './FieldLabel.m.css';
type Props = LabelHTMLAttributes<HTMLLabelElement> & {
className?: string;
title?: string;
required?: boolean;
};
export const FieldLabel: React.FC<PropsWithChildren<Props>> = observer(function FieldLabel({ children, className, required, ...rest }) {
const styles = useS(fieldLabelStyles);
return (
<label {...rest} className={s(styles, { fieldLabel: true }, className)}>
{children}
{required && ' *'}
</label>
);
});
@@ -0,0 +1,46 @@
.filterContainer {
position: relative;
min-width: 24px;
min-height: 24px;
}
.inputField {
display: none;
width: 300px;
&.max {
width: 100%;
}
&.toggled {
display: block;
}
& .input {
padding-right: 40px !important;
}
}
.iconButton {
position: absolute;
right: 0;
top: 0;
margin: 0;
width: 24px;
height: 24px;
border-radius: 2px;
cursor: auto;
&.toggled {
right: 4px;
top: 4px;
}
&.cross {
width: 16px;
height: 16px;
top: 8px;
right: 8px;
}
}
.toggleMode {
composes: theme-background-primary theme-text-on-primary from global;
cursor: pointer;
}
@@ -7,74 +7,20 @@
*/
import { observer } from 'mobx-react-lite';
import { useCallback, useEffect, useState } from 'react';
import styled, { css, use } from 'reshadow';
import type { ComponentStyle } from '@cloudbeaver/core-theming';
import { IconButton } from '../IconButton';
import { s } from '../s';
import { useFocus } from '../useFocus';
import { useStyles } from '../useStyles';
import { useS } from '../useS';
import filterStyle from './Filter.m.css';
import { InputField } from './InputField';
const filterStyles = css`
filter-container {
position: relative;
min-width: 24px;
min-height: 24px;
}
InputField {
display: none;
width: 300px;
&[|max] {
width: 100%;
}
&[|toggled] {
display: block;
}
}
IconButton {
position: absolute;
right: 0;
top: 0;
margin: 0;
width: 24px;
height: 24px;
border-radius: 2px;
cursor: auto;
&[|toggled] {
right: 4px;
top: 4px;
}
&[name='cross'] {
width: 16px;
height: 16px;
top: 8px;
right: 8px;
}
}
`;
const toggleModeButtonStyle = css`
IconButton {
composes: theme-background-primary theme-text-on-primary from global;
cursor: pointer;
}
`;
const innerInputStyle = css`
input {
padding-right: 40px !important;
}
`;
interface BaseProps {
toggleMode?: boolean;
placeholder?: string;
disabled?: boolean;
max?: boolean;
className?: string;
style?: ComponentStyle;
onToggle?: (status: boolean) => void;
onKeyDown?: (event: React.KeyboardEvent<HTMLInputElement>) => void;
onClick?: (event: React.MouseEvent<HTMLDivElement>) => void;
@@ -103,13 +49,12 @@ export const Filter = observer<ControlledProps | ObjectsProps<any, any>>(functio
disabled,
max,
className,
style,
onFilter,
onToggle,
onKeyDown,
onClick,
}) {
const styles = useStyles(filterStyles, style, toggleMode && toggleModeButtonStyle);
const styles = useS(filterStyle);
const [inputRef, ref] = useFocus<HTMLInputElement>({});
const [toggled, setToggled] = useState(!toggleMode);
@@ -156,24 +101,28 @@ export const Filter = observer<ControlledProps | ObjectsProps<any, any>>(functio
value = state[name];
}
return styled(styles)(
<filter-container className={className} onClick={onClick}>
return (
<div className={s(styles, { filterContainer: true }, className)} onClick={onClick}>
<InputField
ref={inputRef}
style={[innerInputStyle, style]}
className={s(styles, { inputField: true, max, toggled })}
placeholder={placeholder}
disabled={disabled}
name={name}
value={value}
onChange={filter}
onKeyDown={onKeyDown}
{...use({ toggled, max })}
/>
{String(value) ? (
<IconButton name="cross" disabled={disabled} onClick={() => filter('', name)} />
<IconButton
className={s(styles, { iconButton: true, cross: true, toggleMode })}
name="cross"
disabled={disabled}
onClick={() => filter('', name)}
/>
) : (
<IconButton name="search" disabled={disabled} onClick={toggle} {...use({ toggled })} />
<IconButton className={s(styles, { iconButton: true, toggled, toggleMode })} name="search" disabled={disabled} onClick={toggle} />
)}
</filter-container>,
</div>
);
});
@@ -1,24 +0,0 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2023 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
*/
import styled, { css } from 'reshadow';
const styles = css`
box {
flex: 1;
display: flex;
flex-wrap: wrap;
}
`;
interface Props {
className?: string;
}
export const FormBox: React.FC<React.PropsWithChildren<Props>> = function FormBox({ children, className }) {
return styled(styles)(<box className={className}>{children}</box>);
};
@@ -1,32 +0,0 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2023 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
*/
import styled, { css, use } from 'reshadow';
const styles = css`
box-element {
flex-basis: 550px;
flex-grow: 1;
&[|max] {
width: 100%;
}
}
`;
interface Props {
className?: string;
max?: boolean;
}
export const FormBoxElement: React.FC<React.PropsWithChildren<Props>> = function FormBoxElement({ children, className, max }) {
return styled(styles)(
<box-element as="div" className={className} {...use({ max })}>
{children}
</box-element>,
);
};
@@ -0,0 +1,16 @@
.fieldLabel {
composes: theme-typography--body1 from global;
font-weight: 500;
margin-bottom: 10px;
box-sizing: border-box;
overflow: hidden;
white-space: initial;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
.fieldDescription {
padding: 0;
}
@@ -5,33 +5,14 @@
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
*/
import styled, { css } from 'reshadow';
import { filterLayoutFakeProps, getLayoutProps } from '../Containers/filterLayoutFakeProps';
import type { ILayoutSizeProps } from '../Containers/ILayoutSizeProps';
import elementsSizeStyles from '../Containers/shared/ElementsSize.m.css';
import { s } from '../s';
import { useS } from '../useS';
import { useStyles } from '../useStyles';
import { baseFormControlStyles, baseValidFormControlStyles } from './baseFormControlStyles';
const style = css`
field-label {
composes: theme-typography--body1 from global;
font-weight: 500;
margin-bottom: 10px;
box-sizing: border-box;
overflow: hidden;
white-space: initial;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
field-description {
padding: 0;
}
`;
import { Field } from './Field';
import { FieldDescription } from './FieldDescription';
import { FieldLabel } from './FieldLabel';
import style from './FormFieldDescription.m.css';
interface Props extends ILayoutSizeProps {
label?: string;
@@ -48,13 +29,12 @@ export const FormFieldDescription: React.FC<React.PropsWithChildren<Props>> = fu
}) {
const layoutProps = getLayoutProps(rest);
rest = filterLayoutFakeProps(rest);
const styles = useStyles(baseFormControlStyles, baseValidFormControlStyles, style);
const sizeStyles = useS(elementsSizeStyles);
const styles = useS(style);
return styled(styles)(
<field title={title} className={s(sizeStyles, { ...layoutProps }, className)} {...rest}>
{label && <field-label as="label">{label}</field-label>}
<field-description>{children}</field-description>
</field>,
return (
<Field title={title} className={className} {...rest} {...layoutProps}>
{label && <FieldLabel className={s(styles, { fieldLabel: true })}>{label}</FieldLabel>}
<FieldDescription className={s(styles, { fieldDescription: true })}>{children}</FieldDescription>
</Field>
);
};
@@ -1,23 +0,0 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2023 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
*/
import styled, { css } from 'reshadow';
const styles = css`
group {
box-sizing: border-box;
display: flex;
}
`;
interface Props {
className?: string;
}
export const FormGroup: React.FC<React.PropsWithChildren<Props>> = function FormGroup({ children, className }) {
return styled(styles)(<group className={className}>{children}</group>);
};
@@ -45,3 +45,7 @@
.input:not(:only-child) {
padding-right: 32px !important;
}
.input {
font-size: 12px;
}
@@ -7,14 +7,11 @@
*/
import { observer } from 'mobx-react-lite';
import React, { forwardRef, useCallback, useContext, useLayoutEffect, useRef, useState } from 'react';
import styled, { use } from 'reshadow';
import type { ComponentStyle } from '@cloudbeaver/core-theming';
import { isNotNullDefined } from '@cloudbeaver/core-utils';
import { filterLayoutFakeProps, getLayoutProps } from '../Containers/filterLayoutFakeProps';
import type { ILayoutSizeProps } from '../Containers/ILayoutSizeProps';
import elementsSizeStyles from '../Containers/shared/ElementsSize.m.css';
import { Icon } from '../Icon';
import { Loader } from '../Loader/Loader';
import { useTranslate } from '../localization/useTranslate';
@@ -23,9 +20,10 @@ import { useCombinedHandler } from '../useCombinedHandler';
import { useCombinedRef } from '../useCombinedRef';
import { useS } from '../useS';
import { useStateDelay } from '../useStateDelay';
import { useStyles } from '../useStyles';
import { Field } from './Field';
import { FieldDescription } from './FieldDescription';
import { FieldLabel } from './FieldLabel';
import { FormContext } from './FormContext';
import formControlStyles from './FormControl.m.css';
import inputFieldStyle from './InputField.m.css';
import { isControlPresented } from './isControlPresented';
import { useCapsLockTracker } from './useCapsLockTracker';
@@ -36,9 +34,7 @@ type BaseProps = Omit<React.InputHTMLAttributes<HTMLInputElement>, 'onChange' |
loading?: boolean;
description?: string;
labelTooltip?: string;
mod?: 'surface';
ref?: React.ForwardedRef<HTMLInputElement>;
style?: ComponentStyle;
canShowPassword?: boolean;
onCustomCopy?: () => void;
icon?: React.ReactElement;
@@ -73,7 +69,6 @@ export const InputField: InputFieldType = observer<ControlledProps | ObjectProps
forwardRef(function InputField(
{
name,
style,
value: valueControlled,
defaultValue,
required,
@@ -86,7 +81,6 @@ export const InputField: InputFieldType = observer<ControlledProps | ObjectProps
loading,
description,
labelTooltip,
mod,
autoHide,
canShowPassword = true,
onChange,
@@ -103,8 +97,7 @@ export const InputField: InputFieldType = observer<ControlledProps | ObjectProps
const translate = useTranslate();
const layoutProps = getLayoutProps(rest);
rest = filterLayoutFakeProps(rest);
const propStyles = useStyles(style);
const styles = useS(inputFieldStyle, formControlStyles, elementsSizeStyles);
const styles = useS(inputFieldStyle);
const context = useContext(FormContext);
loading = useStateDelay(loading ?? false, 300);
@@ -165,13 +158,12 @@ export const InputField: InputFieldType = observer<ControlledProps | ObjectProps
return null;
}
return styled(propStyles)(
<div data-testid="field" className={s(styles, { ...layoutProps, field: true }, className)}>
<div data-testid="field-label" title={labelTooltip || rest.title} className={styles.fieldLabel}>
return (
<Field {...layoutProps} className={s(styles, {}, className)}>
<FieldLabel title={labelTooltip || rest.title} className={s(styles, { fieldLabel: true })} required={required}>
{children}
{required && ' *'}
</div>
<div data-testid="input-container" className={styles.inputContainer}>
</FieldLabel>
<div className={s(styles, { inputContainer: true })}>
<input
ref={mergedRef}
{...rest}
@@ -179,40 +171,35 @@ export const InputField: InputFieldType = observer<ControlledProps | ObjectProps
name={name}
value={uncontrolled ? undefined : value ?? ''}
defaultValue={defaultValue}
className={styles.input}
className={s(styles, { input: true })}
required={required}
onChange={handleChange}
onBlur={handleBlur}
onKeyDown={handleKeyDown}
{...use({ mod })}
required={required}
/>
{loading && (
<div data-testid="loader-container" title={translate('ui_processing_loading')} className={styles.loaderContainer}>
<div title={translate('ui_processing_loading')} className={s(styles, { loaderContainer: true })}>
<Loader small />
</div>
)}
{passwordType && canShowPassword && (
<div data-testid="icon-container" title={translate('ui_reveal_password')} className={styles.iconContainer} onClick={revealPassword}>
<div title={translate('ui_reveal_password')} className={styles.iconContainer} onClick={revealPassword}>
<Icon name={passwordRevealed ? 'password-hide' : 'password-show'} viewBox="0 0 16 16" className={styles.icon} />
</div>
)}
{onCustomCopy && (
<div data-testid="icon-container" title={translate('ui_copy_to_clipboard')} className={styles.iconContainer} onClick={onCustomCopy}>
<div title={translate('ui_copy_to_clipboard')} className={styles.iconContainer} onClick={onCustomCopy}>
<Icon name="copy" viewBox="0 0 32 32" className={styles.icon} />
</div>
)}
{icon && (
<div data-testid="icon-container" className={styles.customIconContainer}>
<div data-testid="icon-container" className={s(styles, { customIconContainer: true })}>
{icon}
</div>
)}
</div>
{(description || passwordType) && (
<div data-testid="field-description" className={s(styles, { fieldDescription: true, valid: !error, invalid: error })}>
{description}
</div>
)}
</div>,
{(description || passwordType) && <FieldDescription invalid={error}>{description}</FieldDescription>}
</Field>
);
}),
) as InputFieldType;
@@ -0,0 +1,22 @@
.fieldLabel {
display: block;
composes: theme-typography--body1 from global;
font-weight: 500;
}
.fieldLabel:not(:empty) {
padding-bottom: 10px;
}
.fieldDescription {
display: flex;
align-items: center;
gap: 2px;
}
.iconButton {
width: 12px;
height: 12px;
flex-shrink: 0;
cursor: pointer;
&:hover {
opacity: 0.5;
}
}
@@ -7,51 +7,25 @@
*/
import { observer } from 'mobx-react-lite';
import { ReactNode, useContext, useState } from 'react';
import styled, { css } from 'reshadow';
import type { ComponentStyle } from '@cloudbeaver/core-theming';
import { blobToData, bytesToSize } from '@cloudbeaver/core-utils';
import { Button } from '../Button';
import { filterLayoutFakeProps, getLayoutProps } from '../Containers/filterLayoutFakeProps';
import type { ILayoutSizeProps } from '../Containers/ILayoutSizeProps';
import elementsSizeStyles from '../Containers/shared/ElementsSize.m.css';
import { IconButton } from '../IconButton';
import { useTranslate } from '../localization/useTranslate';
import { s } from '../s';
import { UploadArea } from '../UploadArea';
import { useS } from '../useS';
import { useStyles } from '../useStyles';
import { baseFormControlStyles, baseInvalidFormControlStyles, baseValidFormControlStyles } from './baseFormControlStyles';
import { Field } from './Field';
import { FieldDescription } from './FieldDescription';
import { FieldLabel } from './FieldLabel';
import { FormContext } from './FormContext';
import inputFileTextContentStyles from './InputFileTextContent.m.css';
const DEFAULT_MAX_FILE_SIZE = 2048;
const INPUT_FILE_FIELD_STYLES = css`
field-label {
display: block;
composes: theme-typography--body1 from global;
font-weight: 500;
}
field-label:not(:empty) {
padding-bottom: 10px;
}
field-description {
display: flex;
align-items: center;
gap: 2px;
}
IconButton {
width: 12px;
height: 12px;
flex-shrink: 0;
cursor: pointer;
&:hover {
opacity: 0.5;
}
}
`;
interface Props<TState> extends ILayoutSizeProps {
name: keyof TState;
state: TState;
@@ -80,14 +54,12 @@ export const InputFileTextContent: InputFileTextContentType = observer(function
tooltip,
required,
fileName,
style,
maxFileSize = DEFAULT_MAX_FILE_SIZE,
disabled,
className,
children,
onChange,
mapValue,
...rest
}: Props<Record<any, any>>) {
const translate = useTranslate();
const context = useContext(FormContext);
@@ -95,10 +67,7 @@ export const InputFileTextContent: InputFileTextContentType = observer(function
const [selected, setSelected] = useState<File | null>(null);
const [error, setError] = useState<Error | null>(null);
const layoutProps = getLayoutProps(rest);
rest = filterLayoutFakeProps(rest);
const styles = useStyles(INPUT_FILE_FIELD_STYLES, baseFormControlStyles, style, error ? baseInvalidFormControlStyles : baseValidFormControlStyles);
const sizeStyles = useS(elementsSizeStyles);
const styles = useS(inputFileTextContentStyles);
const savedExternally = !!fileName && state[name] !== '';
const saved = savedExternally || !!state[name];
@@ -166,21 +135,20 @@ export const InputFileTextContent: InputFileTextContentType = observer(function
}
}
return styled(styles)(
<field className={s(sizeStyles, { ...layoutProps }, className)}>
<field-label title={labelTooltip}>
return (
<Field className={className}>
<FieldLabel title={labelTooltip} required={required} className={s(styles, { fieldLabel: true })}>
{children}
{required && ' *'}
</field-label>
</FieldLabel>
<UploadArea title={tooltip} disabled={disabled} accept={accept} reset onChange={handleChange}>
<Button icon="/icons/import.svg" tag="div" mod={['outlined']} disabled={disabled}>
{translate('ui_upload_file')}
</Button>
</UploadArea>
<field-description>
<FieldDescription className={s(styles, { fieldDescription: true })}>
{description}
{(selected || saved) && <IconButton disabled={disabled} name="cross" onClick={removeFile} />}
</field-description>
</field>,
{(selected || saved) && <IconButton className={s(styles, { iconButton: true })} disabled={disabled} name="cross" onClick={removeFile} />}
</FieldDescription>
</Field>
);
});
@@ -0,0 +1,18 @@
.fieldLabel {
display: block;
composes: theme-typography--body1 from global;
font-weight: 500;
}
.fieldLabel:not(:empty) {
padding-bottom: 10px;
}
.inputContainer {
position: relative;
}
.tags {
padding-top: 8px;
&:empty {
display: none;
}
}
@@ -7,14 +7,12 @@
*/
import { observer } from 'mobx-react-lite';
import { forwardRef, useContext, useEffect, useState } from 'react';
import styled, { css, use } from 'reshadow';
import type { ComponentStyle } from '@cloudbeaver/core-theming';
import { Button } from '../Button';
import { filterLayoutFakeProps, getLayoutProps } from '../Containers/filterLayoutFakeProps';
import type { ILayoutSizeProps } from '../Containers/ILayoutSizeProps';
import elementsSizeStyles from '../Containers/shared/ElementsSize.m.css';
import { useTranslate } from '../localization/useTranslate';
import { s } from '../s';
import { Tag } from '../Tags/Tag';
@@ -24,32 +22,13 @@ import { useCombinedHandler } from '../useCombinedHandler';
import { useRefInherit } from '../useRefInherit';
import { useS } from '../useS';
import { useStateDelay } from '../useStateDelay';
import { useStyles } from '../useStyles';
import { baseFormControlStyles, baseInvalidFormControlStyles, baseValidFormControlStyles } from './baseFormControlStyles';
import { Field } from './Field';
import { FieldDescription } from './FieldDescription';
import { FieldLabel } from './FieldLabel';
import { FormContext } from './FormContext';
import InputFilesStyles from './InputFiles.m.css';
import { isControlPresented } from './isControlPresented';
const INPUT_FIELD_STYLES = css`
field-label {
display: block;
composes: theme-typography--body1 from global;
font-weight: 500;
}
field-label:not(:empty) {
padding-bottom: 10px;
}
input-container {
position: relative;
}
Tags {
padding-top: 8px;
&:empty {
display: none;
}
}
`;
type BaseProps = Omit<React.InputHTMLAttributes<HTMLInputElement>, 'onChange' | 'name' | 'value' | 'style'> &
ILayoutSizeProps & {
error?: boolean;
@@ -57,9 +36,7 @@ type BaseProps = Omit<React.InputHTMLAttributes<HTMLInputElement>, 'onChange' |
description?: string;
labelTooltip?: string;
hideTags?: boolean;
mod?: 'surface';
ref?: React.Ref<HTMLInputElement>;
style?: ComponentStyle;
aggregate?: boolean;
onDuplicate?: (files: File[]) => void;
};
@@ -89,7 +66,6 @@ export const InputFiles: InputFilesType = observer(
forwardRef(function InputFiles(
{
name,
style,
value: valueControlled,
required,
state,
@@ -100,7 +76,6 @@ export const InputFiles: InputFilesType = observer(
description,
labelTooltip,
hideTags,
mod,
autoHide,
aggregate,
onChange,
@@ -114,8 +89,7 @@ export const InputFiles: InputFilesType = observer(
const ref = useRefInherit<HTMLInputElement>(refInherit);
const [innerState, setInnerState] = useState<FileList | null>(null);
const translate = useTranslate();
const styles = useStyles(baseFormControlStyles, error ? baseInvalidFormControlStyles : baseValidFormControlStyles, INPUT_FIELD_STYLES, style);
const sizeStyles = useS(elementsSizeStyles);
const styles = useS(InputFilesStyles);
const context = useContext(FormContext);
loading = useStateDelay(loading ?? false, 300);
@@ -195,28 +169,27 @@ export const InputFiles: InputFilesType = observer(
const files = Array.from(value ?? []);
return styled(styles)(
<field className={s(sizeStyles, { ...layoutProps }, className)}>
<field-label title={labelTooltip || rest.title}>
return (
<Field {...layoutProps} className={className}>
<FieldLabel title={labelTooltip || rest.title} required={required} className={s(styles, { fieldLabel: true })}>
{children}
{required && ' *'}
</field-label>
<input-container>
<UploadArea ref={ref} {...rest} name={name} value={value} {...use({ mod })} required={required} onChange={handleChange}>
</FieldLabel>
<div className={s(styles, { inputContainer: true })}>
<UploadArea ref={ref} {...rest} name={name} value={value} required={required} onChange={handleChange}>
<Button icon="/icons/import.svg" tag="div" loading={loading} mod={['outlined']}>
{translate(rest.multiple ? 'ui_upload_files' : 'ui_upload_file')}
</Button>
</UploadArea>
{!hideTags && (
<Tags>
<Tags className={s(styles, { tags: true })}>
{files.map((file, i) => (
<Tag key={file.name} id={i} label={file.name} onRemove={removeFile} />
))}
</Tags>
)}
</input-container>
{description && <field-description>{description}</field-description>}
</field>,
</div>
{description && <FieldDescription invalid={error}>{description}</FieldDescription>}
</Field>
);
}),
) as InputFilesType;
@@ -0,0 +1,66 @@
.radio {
composes: theme-radio from global;
}
.radioNoRipple {
composes: theme-radio_no-ripple from global;
}
.radioBackground {
composes: theme-radio_background from global;
}
.input {
composes: theme-radio_native-control from global;
}
.radioOuterCircle {
composes: theme-radio_outer-circle from global;
}
.radioInnerCircle {
composes: theme-radio_inner-circle from global;
}
.radioRipple {
composes: theme-radio_ripple from global;
}
.field {
display: inline-flex;
align-items: center;
vertical-align: middle;
}
.label {
cursor: pointer;
.disabled {
cursor: auto;
}
}
.primary {
composes: theme-radio_primary from global;
}
.small {
composes: theme-radio_small from global;
}
.field.menu {
padding: 0;
& .radio {
width: 14px;
height: 14px;
}
& .radioBackground {
width: 14px;
height: 14px;
}
& .radioInnerCircle {
border-width: 7px;
}
}
.disabledRadio {
composes: theme-radio_disabled from global;
}
.disabledInput {
opacity: 0 !important;
}
@@ -7,105 +7,19 @@
*/
import { observer } from 'mobx-react-lite';
import { useCallback, useContext } from 'react';
import styled, { css, use } from 'reshadow';
import { filterLayoutFakeProps, getLayoutProps } from '../Containers/filterLayoutFakeProps';
import type { ILayoutSizeProps } from '../Containers/ILayoutSizeProps';
import elementsSizeStyles from '../Containers/shared/ElementsSize.m.css';
import { s } from '../s';
import { useS } from '../useS';
import { useStyles } from '../useStyles';
import { baseFormControlStyles } from './baseFormControlStyles';
import { Field } from './Field';
import { FormContext } from './FormContext';
import style from './Radio.m.css';
import { RadioGroupContext } from './RadioGroupContext';
const radioStyles = css`
radio {
composes: theme-radio from global;
}
radio-background {
composes: theme-radio_background from global;
}
input {
composes: theme-radio_native-control from global;
}
radio-outer-circle {
composes: theme-radio_outer-circle from global;
}
radio-inner-circle {
composes: theme-radio_inner-circle from global;
}
radio-ripple {
composes: theme-radio_ripple from global;
}
field {
display: inline-flex;
align-items: center;
font-weight: 500;
padding: 7px 12px;
vertical-align: middle;
}
label {
cursor: pointer;
&[|disabled] {
cursor: auto;
}
}
`;
const radioMod = {
primary: css`
radio {
composes: theme-radio_primary from global;
}
`,
small: css`
radio {
composes: theme-radio_small from global;
}
`,
menu: css`
radio {
composes: theme-radio_small from global;
}
field {
padding: 0;
& radio {
width: 14px;
height: 14px;
}
& radio-background {
width: 14px;
height: 14px;
}
& radio-inner-circle {
border-width: 7px;
}
}
`,
};
const noRippleStyles = css`
radio {
composes: theme-radio_no-ripple from global;
}
`;
const radioState = {
disabled: css`
radio {
composes: theme-radio--disabled from global;
}
input {
opacity: 0 !important;
}
`,
};
type BaseProps = Omit<React.InputHTMLAttributes<HTMLInputElement>, 'onChange' | 'value' | 'checked'> &
ILayoutSizeProps & {
mod?: Array<keyof typeof radioMod>;
mod?: Array<'primary' | 'small' | 'menu'>;
ripple?: boolean;
};
@@ -142,9 +56,10 @@ export const Radio: RadioType = observer(function Radio({
children,
...rest
}: ControlledProps | ObjectProps<any, any>) {
const styles = useS(style);
const layoutProps = getLayoutProps(rest);
rest = filterLayoutFakeProps(rest);
const sizeStyles = useS(elementsSizeStyles);
const formContext = useContext(FormContext);
const context = useContext(RadioGroupContext);
@@ -184,27 +99,36 @@ export const Radio: RadioType = observer(function Radio({
checked = state[name] === value;
}
return styled(
useStyles(
baseFormControlStyles,
radioStyles,
...(mod || []).map(mod => radioMod[mod]),
!ripple && noRippleStyles,
rest.disabled && radioState.disabled,
),
)(
<field className={s(sizeStyles, { ...layoutProps }, className)}>
<radio>
<input {...rest} type="radio" id={id} name={name} value={value ?? ''} checked={checked} onChange={handleChange} />
<radio-background>
<radio-outer-circle />
<radio-inner-circle />
</radio-background>
{ripple && <radio-ripple />}
</radio>
<label {...use({ disabled: rest.disabled })} htmlFor={id}>
return (
<Field {...layoutProps} className={s(styles, { field: true, menu: mod?.includes('menu') }, className)}>
<div
className={s(styles, {
radio: true,
primary: mod?.includes('primary'),
small: mod?.includes('small') || mod?.includes('menu'),
disabledRadio: rest.disabled,
radioNoRipple: !ripple,
})}
>
<input
{...rest}
className={s(styles, { input: true, disabledInput: rest.disabled })}
type="radio"
id={id}
name={name}
value={value ?? ''}
checked={checked}
onChange={handleChange}
/>
<div className={s(styles, { radioBackground: true })}>
<div className={s(styles, { radioOuterCircle: true })} />
<div className={s(styles, { radioInnerCircle: true })} />
</div>
{ripple && <div className={s(styles, { radioRipple: true })} />}
</div>
<label className={s(styles, { label: true, disabled: rest.disabled })} htmlFor={id}>
{children}
</label>
</field>,
</Field>
);
});
@@ -0,0 +1,25 @@
.textarea {
line-height: 19px;
font-size: 12px;
}
.field.embedded {
height: 100%;
display: flex;
flex-direction: column;
& .textarea {
border-radius: 0 !important;
height: 100%;
resize: none !important;
}
}
.fieldLabel {
display: block;
padding-bottom: 10px;
composes: theme-typography--body1 from global;
font-weight: 500;
&:empty {
display: none;
}
}
@@ -7,52 +7,21 @@
*/
import { observer } from 'mobx-react-lite';
import { useCallback, useContext } from 'react';
import styled, { css, use } from 'reshadow';
import type { ComponentStyle } from '@cloudbeaver/core-theming';
import { filterLayoutFakeProps, getLayoutProps } from '../Containers/filterLayoutFakeProps';
import type { ILayoutSizeProps } from '../Containers/ILayoutSizeProps';
import elementsSizeStyles from '../Containers/shared/ElementsSize.m.css';
import { s } from '../s';
import { useS } from '../useS';
import { useStyles } from '../useStyles';
import { baseFormControlStyles, baseValidFormControlStyles } from './baseFormControlStyles';
import { Field } from './Field';
import { FieldDescription } from './FieldDescription';
import { FieldLabel } from './FieldLabel';
import { FormContext } from './FormContext';
const styles = css`
textarea {
line-height: 19px;
}
field[|embedded] {
height: 100%;
display: flex;
flex-direction: column;
& textarea {
border-radius: 0 !important;
height: 100%;
resize: none !important;
}
}
field-label {
display: block;
padding-bottom: 10px;
composes: theme-typography--body1 from global;
font-weight: 500;
&:empty {
display: none;
}
}
`;
import textareaStyle from './Textarea.m.css';
type BaseProps = Omit<React.TextareaHTMLAttributes<HTMLTextAreaElement>, 'onChange' | 'style'> &
ILayoutSizeProps & {
description?: string;
labelTooltip?: string;
mod?: 'surface';
style?: ComponentStyle;
embedded?: boolean;
};
@@ -77,7 +46,6 @@ interface TextareaType {
export const Textarea: TextareaType = observer(function Textarea({
name,
style,
value: controlledValue,
state,
required,
@@ -85,14 +53,13 @@ export const Textarea: TextareaType = observer(function Textarea({
className,
description,
labelTooltip,
mod,
embedded,
onChange = () => {},
...rest
}: ControlledProps | ObjectProps<any, any>) {
const layoutProps = getLayoutProps(rest);
rest = filterLayoutFakeProps(rest);
const sizeStyles = useS(elementsSizeStyles);
const styles = useS(textareaStyle);
const context = useContext(FormContext);
const handleChange = useCallback(
@@ -112,14 +79,20 @@ export const Textarea: TextareaType = observer(function Textarea({
const value = state ? state[name] : controlledValue;
return styled(useStyles(baseFormControlStyles, baseValidFormControlStyles, styles, style))(
<field className={s(sizeStyles, { ...layoutProps }, className)} {...use({ embedded })}>
<field-label title={labelTooltip || rest.title}>
return (
<Field {...layoutProps} className={s(styles, { field: true, embedded }, className)}>
<FieldLabel className={s(styles, { fieldLabel: true })} title={labelTooltip || rest.title} required={required}>
{children}
{required && ' *'}
</field-label>
<textarea {...rest} value={value ?? ''} name={name} data-embedded={embedded} onChange={handleChange} {...use({ mod })} />
{description && <field-description>{description}</field-description>}
</field>,
</FieldLabel>
<textarea
{...rest}
className={s(styles, { textarea: true })}
value={value ?? ''}
name={name}
data-embedded={embedded}
onChange={handleChange}
/>
{description && <FieldDescription>{description}</FieldDescription>}
</Field>
);
});
@@ -1,57 +0,0 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2023 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
*/
import { css } from 'reshadow';
export const baseFormControlStyles = css`
field {
box-sizing: border-box;
max-width: 100%;
& field {
height: 32px;
padding: 0;
}
}
field-label {
composes: theme-typography--body1 from global;
box-sizing: border-box;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
field-description {
composes: theme-typography--caption from global;
box-sizing: border-box;
padding-top: 4px;
min-height: 24px;
}
input,
textarea {
font-size: 12px;
&[|select] {
cursor: pointer;
user-select: none;
}
}
`;
export const baseValidFormControlStyles = css`
field-description {
composes: theme-text-text-hint-on-light from global;
}
`;
export const baseInvalidFormControlStyles = css`
field-description {
composes: theme-text-negative from global;
}
`;
@@ -193,7 +193,6 @@ export const RenderField = observer<RenderFieldProps>(function RenderField({
state={state}
disabled={disabled}
readOnly={readOnly}
mod="surface"
className={className}
>
{property.displayName ?? ''}
@@ -210,7 +209,6 @@ export const RenderField = observer<RenderFieldProps>(function RenderField({
value={value}
disabled={disabled}
readOnly={readOnly}
mod="surface"
className={className}
>
{property.displayName ?? ''}
@@ -232,7 +230,6 @@ export const RenderField = observer<RenderFieldProps>(function RenderField({
readOnly={readOnly}
autoHide={autoHide}
autoComplete={RESERVED_KEYWORDS.includes(autofillToken) ? autofillToken : `${autofillToken} ${property.id}`}
mod="surface"
className={className}
canShowPassword={canShowPassword}
onFocus={onFocus}
@@ -254,7 +251,6 @@ export const RenderField = observer<RenderFieldProps>(function RenderField({
disabled={disabled}
readOnly={readOnly}
autoComplete={RESERVED_KEYWORDS.includes(autofillToken) ? autofillToken : `${autofillToken} ${property.id}`}
mod="surface"
className={className}
canShowPassword={canShowPassword}
onFocus={onFocus}
@@ -13,13 +13,17 @@ import styled from 'reshadow';
import { Button } from '../Button';
import ButtonStyles from '../Button.m.css';
import { Filter } from '../FormControls/Filter';
import FilterStyles from '../FormControls/Filter.m.css';
import InputFieldStyles from '../FormControls/InputField.m.css';
import { useTranslate } from '../localization/useTranslate';
import { SContext, StyleRegistry } from '../SContext';
import { useObjectRef } from '../useObjectRef';
import type { IProperty } from './IProperty';
import PropertiesTableFilterStyles from './PropertiesTableFilterStyles.m.css';
import PropertiesTableInputStyles from './PropertiesTableInputStyles.m.css';
import PropertiesTableAddButtonStyles from './PropertiesTableAddButtonStyles.m.css';
import { PropertyItem } from './PropertyItem';
import { PROPERTIES_FILTER_STYLES, PROPERTIES_TABLE_STYLES } from './styles';
import { PROPERTIES_TABLE_STYLES } from './styles';
type PropertiesState = Record<string, string | null>;
@@ -36,6 +40,20 @@ interface Props {
}
const registry: StyleRegistry = [
[
InputFieldStyles,
{
mode: 'append',
styles: [PropertiesTableInputStyles],
},
],
[
FilterStyles,
{
mode: 'append',
styles: [PropertiesTableFilterStyles],
},
],
[
ButtonStyles,
{
@@ -134,12 +152,13 @@ export const PropertiesTable = observer<Props>(function PropertiesTable(props) {
<properties-header-name>
<div>{translate('core_block_properties_table_name')}</div>
{props.filterable ? (
<Filter
value={filterValue}
placeholder={translate('core_block_properties_table_filter_name')}
style={PROPERTIES_FILTER_STYLES}
onFilter={setFilterValue}
/>
<SContext registry={registry}>
<Filter
value={filterValue}
placeholder={translate('core_block_properties_table_filter_name')}
onFilter={setFilterValue}
/>
</SContext>
) : null}
</properties-header-name>
<properties-header-value>{translate('core_block_properties_table_value')}</properties-header-value>
@@ -0,0 +1,18 @@
.iconButton {
right: 0px;
top: 0px;
width: 20px;
height: 20px;
&.toggled {
right: 0px;
top: 0px;
}
&.cross {
width: 14px;
height: 14px;
top: 5px;
right: 0;
}
}
@@ -0,0 +1,11 @@
.input {
composes: theme-background-surface from global;
box-sizing: border-box;
height: 24px;
flex: 1;
margin-left: 8px;
}
.inputField {
max-width: 100%;
}
@@ -58,36 +58,3 @@ export const PROPERTIES_TABLE_STYLES = css`
pointer-events: none;
}
`;
export const PROPERTIES_FILTER_STYLES = css`
input {
composes: theme-background-surface from global;
box-sizing: border-box;
height: 24px;
flex: 1;
margin-left: 8px;
}
InputField {
max-width: 100%;
}
IconButton {
right: 0px;
top: 0px;
width: 20px;
height: 20px;
&[|toggled] {
right: 0px;
top: 0px;
}
&[name='cross'] {
width: 14px;
height: 14px;
top: 5px;
right: 0;
}
}
`;
@@ -0,0 +1,4 @@
.tableSelect {
margin-left: -10px;
margin-right: -10px;
}
@@ -7,11 +7,13 @@
*/
import { observer } from 'mobx-react-lite';
import { useContext } from 'react';
import styled, { css } from 'reshadow';
import { Checkbox } from '../FormControls/Checkboxes/Checkbox';
import { useTranslate } from '../localization/useTranslate';
import { s } from '../s';
import { useS } from '../useS';
import { TableContext } from './TableContext';
import style from './TableSelect.m.css';
interface Props {
id?: string;
@@ -20,14 +22,8 @@ interface Props {
className?: string;
}
const styles = css`
Checkbox {
margin-left: -10px;
margin-right: -10px;
}
`;
export const TableSelect = observer<Props>(function TableSelect({ id, disabled, tooltip, className }) {
const styles = useS(style);
const tableContext = useContext(TableContext);
const translate = useTranslate();
@@ -35,14 +31,14 @@ export const TableSelect = observer<Props>(function TableSelect({ id, disabled,
throw new Error('Context must be provided');
}
return styled(styles)(
return (
<Checkbox
id={id}
className={className}
className={s(styles, { tableSelect: true }, className)}
title={tooltip || translate('ui_select_all')}
disabled={disabled || !tableContext.state.selectableItems.length}
checked={tableContext.state.tableSelected}
onClick={tableContext.state.selectTable}
/>,
/>
);
});
+2 -3
View File
@@ -131,8 +131,8 @@ export * from './FormControls/Checkboxes/CheckboxMarkup';
export * from './FormControls/Checkboxes/Switch';
export * from './FormControls/Checkboxes/useCheckboxState';
export * from './FormControls/BASE_DROPDOWN_STYLES';
export * from './FormControls/FormBox';
export * from './FormControls/Filter';
export { default as FilterStyles } from './FormControls/Filter.m.css';
export * from './Fill';
export * from './Containers/Container';
@@ -154,12 +154,11 @@ export * from './FolderExplorer/useFolderExplorer';
export * from './Tags/Tag';
export * from './Tags/Tags';
export * from './FormControls/FormBoxElement';
export * from './FormControls/Combobox';
export * from './FormControls/FormContext';
export * from './FormControls/FormFieldDescription';
export * from './FormControls/FormGroup';
export * from './FormControls/InputField';
export { default as InputFieldStyles } from './FormControls/InputField.m.css';
export * from './FormControls/InputFiles';
export * from './FormControls/InputFileTextContent';
export * from './FormControls/Radio';
@@ -22,7 +22,7 @@ export const ServerConfigurationInfoForm = observer<Props>(function ServerConfig
return (
<Group form gap>
<GroupTitle>{translate('administration_configuration_wizard_configuration_server_info')}</GroupTitle>
<InputField type="text" name="serverName" state={state.serverConfig} mod="surface" required medium>
<InputField type="text" name="serverName" state={state.serverConfig} required medium>
{translate('administration_configuration_wizard_configuration_server_name')}
</InputField>
<InputField
@@ -31,7 +31,6 @@ export const ServerConfigurationInfoForm = observer<Props>(function ServerConfig
name="serverURL"
state={state.serverConfig}
readOnly={serverConfigLoader.resource.distributed}
mod="surface"
required
medium
>
@@ -42,7 +41,6 @@ export const ServerConfigurationInfoForm = observer<Props>(function ServerConfig
type="number"
name="sessionExpireTime"
state={state.serverConfig}
mod="surface"
min={1}
mapState={v => (v === 0 ? 60000 : v ?? 1800000) / 1000 / 60}
mapValue={v => (v === undefined ? 30 : Number(v) || 1) * 1000 * 60}
@@ -20,10 +20,10 @@ export const ServerConfigurationAdminForm = observer<Props>(function ServerConfi
return (
<Group form gap medium>
<GroupTitle>{translate('administration_configuration_wizard_configuration_admin')}</GroupTitle>
<InputField type="text" name="adminName" state={serverConfig} minLength={6} mod="surface" required tiny>
<InputField type="text" name="adminName" state={serverConfig} minLength={6} required tiny>
{translate('administration_configuration_wizard_configuration_admin_name')}
</InputField>
<InputField type="password" name="adminPassword" state={serverConfig} autoComplete="new-password" mod="surface" required tiny>
<InputField type="password" name="adminPassword" state={serverConfig} autoComplete="new-password" required tiny>
{translate('administration_configuration_wizard_configuration_admin_password')}
</InputField>
</Group>
@@ -44,7 +44,7 @@ export const UserFormInfoCredentials = observer<Props>(function UserFormInfoCred
return (
<Container gap vertical>
<GroupTitle keepSize>{translate('authentication_user_credentials')}</GroupTitle>
<InputField type="text" name="userId" state={tabState.state} readOnly={editing} disabled={disabled} mod="surface" keepSize tiny required>
<InputField type="text" name="userId" state={tabState.state} readOnly={editing} disabled={disabled} keepSize tiny required>
{translate('authentication_user_name')}
</InputField>
{local && (
@@ -57,7 +57,6 @@ export const UserFormInfoCredentials = observer<Props>(function UserFormInfoCred
placeholder={editing ? PASSWORD_PLACEHOLDER : ''}
canShowPassword={tabState.state['password'] !== ''}
disabled={disabled}
mod="surface"
required={!editing}
keepSize
tiny
@@ -70,7 +69,6 @@ export const UserFormInfoCredentials = observer<Props>(function UserFormInfoCred
name="passwordRepeat"
placeholder={editing ? PASSWORD_PLACEHOLDER : ''}
disabled={disabled}
mod="surface"
required={!editing}
canShowPassword
keepSize
@@ -53,7 +53,6 @@ export const AuthProviderForm = observer<Props>(function AuthProviderForm({ prov
disabled={authenticate}
canShowPassword={false}
autoComplete={`section-authentication section-${provider.id} ${parameter.id}`}
mod="surface"
>
{parameter.displayName}
</InputField>
@@ -53,10 +53,10 @@ export const NetworkHandlerAuthForm = observer<Props>(function NetworkHandlerAut
</GroupTitle>
{ssh && (
<>
<InputField type="text" name="userName" state={state} disabled={disabled} mod="surface">
<InputField type="text" name="userName" state={state} disabled={disabled}>
{translate(`connections_network_handler_${id}_user`, 'connections_network_handler_default_user')}
</InputField>
<InputField type="password" name="password" canShowPassword={false} state={state} disabled={disabled} mod="surface">
<InputField type="password" name="password" canShowPassword={false} state={state} disabled={disabled}>
{passwordLabel}
</InputField>
</>
@@ -236,7 +236,6 @@ export const Options: TabContainerPanelComponent<IConnectionFormProps> = observe
disabled={disabled}
readOnly={readonly}
autoComplete={`section-${config.driverId || 'driver'} section-jdbc`}
mod="surface"
>
{translate('customConnection_url_JDBC')}
</InputField>
@@ -253,7 +252,7 @@ export const Options: TabContainerPanelComponent<IConnectionFormProps> = observe
</Group>
<Group form gap>
<Container wrap gap>
<InputField type="text" name="name" minLength={1} state={config} disabled={disabled} readOnly={readonly} mod="surface" required fill>
<InputField type="text" name="name" minLength={1} state={config} disabled={disabled} readOnly={readonly} required fill>
{translate('connections_connection_name')}
</InputField>
{!config.template && (
@@ -272,7 +271,6 @@ export const Options: TabContainerPanelComponent<IConnectionFormProps> = observe
state={config}
disabled={disabled}
autoComplete={`section-${config.driverId || 'driver'} section-folder`}
mod="surface"
autoHide
readOnly
tiny
@@ -328,6 +326,7 @@ export const Options: TabContainerPanelComponent<IConnectionFormProps> = observe
name="saveCredentials"
state={config}
disabled={disabled || readonly || config.sharedCredentials}
mod={['primary']}
keepSize
>
{translate('connections_connection_edit_save_credentials')}
@@ -117,7 +117,6 @@ export const SSH: TabContainerPanelComponent<Props> = observer(function SSH({ st
state={handlerState.properties}
disabled={disabled || !enabled}
readOnly={readonly}
mod="surface"
required
small
>
@@ -129,7 +128,6 @@ export const SSH: TabContainerPanelComponent<Props> = observer(function SSH({ st
state={handlerState.properties}
disabled={disabled || !enabled}
readOnly={readonly}
mod="surface"
required
tiny
>
@@ -143,7 +141,6 @@ export const SSH: TabContainerPanelComponent<Props> = observer(function SSH({ st
state={handlerState}
disabled={disabled || !enabled}
readOnly={readonly}
mod="surface"
required={handlerState.savePassword}
tiny
fill
@@ -157,7 +154,6 @@ export const SSH: TabContainerPanelComponent<Props> = observer(function SSH({ st
state={handlerState}
disabled={disabled || !enabled}
readOnly={readonly}
mod="surface"
required={!keyAuth && handlerState.savePassword}
description={passwordSaved ? translate('ui_processing_saved') : undefined}
tiny
@@ -187,7 +183,6 @@ export const SSH: TabContainerPanelComponent<Props> = observer(function SSH({ st
disabled={disabled || !enabled}
readOnly={readonly}
labelTooltip={aliveIntervalLabel}
mod="surface"
tiny
>
{aliveIntervalLabel}
@@ -199,7 +194,6 @@ export const SSH: TabContainerPanelComponent<Props> = observer(function SSH({ st
disabled={disabled || !enabled}
readOnly={readonly}
labelTooltip={connectTimeoutLabel}
mod="surface"
tiny
>
{connectTimeoutLabel}
@@ -74,7 +74,6 @@ export const SettingsInfoFormField = observer<SettingsInfoFormFieldProps>(functi
value={value}
disabled={disabled}
readOnly={readOnly}
mod="surface"
className={className}
onChange={onChange}
>
@@ -93,7 +92,6 @@ export const SettingsInfoFormField = observer<SettingsInfoFormFieldProps>(functi
description={description}
disabled={disabled}
readOnly={readOnly}
mod="surface"
className={className}
small
onChange={onChange}
@@ -45,13 +45,13 @@ export const UserInfoPanel = observer<Props>(function UserInfoPanel({ user, clas
<Group form gap>
<GroupTitle>{translate('plugin_user_profile_info')}</GroupTitle>
<Container wrap gap>
<InputField type="text" name="userId" minLength={1} state={user} mod="surface" readOnly required tiny fill>
<InputField type="text" name="userId" minLength={1} state={user} readOnly required tiny fill>
{translate('plugin_user_profile_info_id')}
</InputField>
<InputField type="text" name="displayName" minLength={1} state={user} mod="surface" readOnly required tiny fill>
<InputField type="text" name="displayName" minLength={1} state={user} readOnly required tiny fill>
{translate('plugin_user_profile_info_displayName')}
</InputField>
<InputField type="text" name="authRole" mod="surface" state={user} autoHide readOnly tiny fill>
<InputField type="text" name="authRole" state={user} autoHide readOnly tiny fill>
{translate('authentication_user_role')}
</InputField>
</Container>