diff --git a/jsapp/js/account/accountSettings.scss b/jsapp/js/account/accountSettings.scss index 3a5f050469..dfa689d56e 100644 --- a/jsapp/js/account/accountSettings.scss +++ b/jsapp/js/account/accountSettings.scss @@ -68,10 +68,6 @@ } } - .account-settings-link { - font-size: 0.85em; - } - .form-modal__item { &.form-modal__item--primary-sector { width: 46%; @@ -97,28 +93,9 @@ width: 46%; float: left; } - - &.form-modal__item--social { - clear: both; - - > label { - position: relative; - - &:not(:first-child) { - margin-top: sizes.$x5; - } - - > i { - position: absolute; - font-size: 24px; - top: 4px; - left: 4px; - } - - .text-box .text-box__input { - padding-left: 40px; - } - } - } } } + +.account-settings-social-row:not(:first-child) { + margin-top: sizes.$x5; +} diff --git a/jsapp/js/account/accountSettingsRoute.tsx b/jsapp/js/account/accountSettingsRoute.tsx index db608bef8d..b8892af046 100644 --- a/jsapp/js/account/accountSettingsRoute.tsx +++ b/jsapp/js/account/accountSettingsRoute.tsx @@ -282,7 +282,6 @@ const AccountSettings = observer(() => { {/* Full name */} {metadata.name && { {/* Organization */} {metadata.organization && { {/* Organization Website */} {metadata.organization_website && { {/* Bio */} {metadata.bio && { {/* City */} {metadata.city && { } {/* Social */} - {(metadata.twitter || metadata.linkedin || metadata.instagram) && + {(metadata.twitter || metadata.linkedin || metadata.instagram) && {/* Twitter */} - {metadata.twitter && } )} diff --git a/jsapp/js/account/security/apiToken/apiTokenSection.component.tsx b/jsapp/js/account/security/apiToken/apiTokenSection.component.tsx index 23a0cc1d15..975fa349bf 100644 --- a/jsapp/js/account/security/apiToken/apiTokenSection.component.tsx +++ b/jsapp/js/account/security/apiToken/apiTokenSection.component.tsx @@ -49,7 +49,6 @@ export default function ApiTokenDisplay() {
{/*TODO: Move TextBox into a modal--it messes up the flow of the row right now*/} {t('Forgot Password?')} @@ -150,7 +149,6 @@ export default function UpdatePasswordForm(props: UpdatePasswordFormProps) {
{ }); dataShareActions.attachToSource.failed.listen((response) => { notify.error( - response?.responseJSON?.filename[0] || + response?.responseJSON?.filename?.[0] || response?.responseJSON || t('Failed to attach to source') ); diff --git a/jsapp/js/bemComponents.ts b/jsapp/js/bemComponents.ts index ede63ce6d0..4357622e39 100644 --- a/jsapp/js/bemComponents.ts +++ b/jsapp/js/bemComponents.ts @@ -50,11 +50,6 @@ bem.FormBuilderMessageBox = makeBem(null, 'form-builder-message-box'); bem.FormBuilderMessageBox__toggle = makeBem(bem.FormBuilderMessageBox, 'toggle', 'button'); bem.FormBuilderMessageBox__details = makeBem(bem.FormBuilderMessageBox, 'details', 'section'); -bem.FormBuilderMeta = makeBem(null, 'form-builder-meta'); -bem.FormBuilderMeta__columns = makeBem(bem.FormBuilderMeta, 'columns'); -bem.FormBuilderMeta__column = makeBem(bem.FormBuilderMeta, 'column'); -bem.FormBuilderMeta__row = makeBem(bem.FormBuilderMeta, 'row'); - bem.FormBuilderAside = makeBem(null, 'form-builder-aside'); bem.FormBuilderAside__content = makeBem(bem.FormBuilderAside, 'content'); bem.FormBuilderAside__header = makeBem(bem.FormBuilderAside, 'header', 'h2'); @@ -74,10 +69,6 @@ bem.FormMedia__list = makeBem(bem.FormMedia, 'list'); bem.FormMedia__label = makeBem(bem.FormMedia, 'label', 'label'); bem.FormMedia__listItem = makeBem(bem.FormMedia, 'list-item', 'li'); -bem.FormMediaUploadUrl = makeBem(null, 'form-media-upload-url'); -bem.FormMediaUploadUrl__label = makeBem(bem.FormMediaUploadUrl, 'label', 'label'); -bem.FormMediaUploadUrl__form = makeBem(bem.FormMediaUploadUrl, 'form'); - bem.SearchInput = makeBem(null, 'search-input', 'input'); bem.Search = makeBem(null, 'search'); @@ -241,13 +232,6 @@ bem.SimpleTable__cell = makeBem(bem.SimpleTable, 'cell', 'td'); bem.tagSelect = makeBem(null, 'tag-select'); bem.collectionFilter = makeBem(null, 'collection-filter'); -bem.TextBox = makeBem(null, 'text-box', 'label'); -bem.TextBox__label = makeBem(bem.TextBox, 'label'); -bem.TextBox__labelLink = makeBem(bem.TextBox, 'label-link', 'a'); -bem.TextBox__input = makeBem(bem.TextBox, 'input', 'input'); -bem.TextBox__description = makeBem(bem.TextBox, 'description'); -bem.TextBox__error = makeBem(bem.TextBox, 'error'); - bem.ToggleSwitch = makeBem(null, 'toggle-switch'); bem.ToggleSwitch__wrapper = makeBem(bem.ToggleSwitch, 'wrapper', 'label'); bem.ToggleSwitch__input = makeBem(bem.ToggleSwitch, 'input', 'input'); diff --git a/jsapp/js/components/RESTServices/RESTServicesForm.es6 b/jsapp/js/components/RESTServices/RESTServicesForm.es6 index 0dac214d45..85c3ed4505 100644 --- a/jsapp/js/components/RESTServices/RESTServicesForm.es6 +++ b/jsapp/js/components/RESTServices/RESTServicesForm.es6 @@ -423,7 +423,6 @@ export default class RESTServicesForm extends React.Component { @@ -375,10 +375,10 @@ const MFAModals = class MFAModals extends React.Component< @@ -429,10 +429,10 @@ const MFAModals = class MFAModals extends React.Component< diff --git a/jsapp/js/components/common/button.scss b/jsapp/js/components/common/button.scss index 63f8208560..2669b1dcf2 100644 --- a/jsapp/js/components/common/button.scss +++ b/jsapp/js/components/common/button.scss @@ -1,5 +1,6 @@ @use '~kobo-common/src/styles/colors'; @use 'scss/sizes'; +@use 'scss/mixins'; @use 'js/components/common/icon'; @use 'sass:color'; @@ -164,6 +165,14 @@ $button-border-radius: sizes.$x6; transform: translateY(sizes.$x1); } +.k-button:focus { + outline: none; +} + +.k-button:focus-visible { + @include mixins.default-ui-focus; +} + .k-button__label { cursor: inherit; line-height: 1; diff --git a/jsapp/js/components/common/checkbox.scss b/jsapp/js/components/common/checkbox.scss index cb5086388d..6e1493cae6 100644 --- a/jsapp/js/components/common/checkbox.scss +++ b/jsapp/js/components/common/checkbox.scss @@ -1,6 +1,7 @@ @use '~kobo-common/src/styles/colors'; @use 'scss/_variables'; @use 'scss/sizes'; +@use 'scss/mixins'; // Note: we can't change this into a CSS Module, because some Data Table and // `utils.ts` code relies on `.checkbox__input` being available. @@ -106,8 +107,7 @@ // Keyboard focus styles &:focus-visible { - outline: 3px solid colors.$kobo-alt-blue; - outline-offset: sizes.$x1; + @include mixins.default-ui-focus; } } } diff --git a/jsapp/js/components/common/koboSelect.scss b/jsapp/js/components/common/koboSelect.scss index 5665f2567a..6f70102c81 100644 --- a/jsapp/js/components/common/koboSelect.scss +++ b/jsapp/js/components/common/koboSelect.scss @@ -1,13 +1,13 @@ @use '~kobo-common/src/styles/colors'; -@use "scss/mixins"; -@use "scss/sizes"; -@use "js/components/common/button"; +@use 'scss/mixins'; +@use 'scss/sizes'; +@use 'js/components/common/button'; $k-select-option-height: sizes.$x36; $k-select-menu-padding: sizes.$x6; .k-select { - font-size: sizes.$x13; + font-size: sizes.$x12; .kobo-dropdown__menu { width: 100%; @@ -123,6 +123,11 @@ $k-select-menu-padding: sizes.$x6; padding: 0; font-size: inherit; + &:focus-visible { + // wrapper element is handling that + outline: none; + } + &::placeholder { @include mixins.textEllipsis; color: inherit; @@ -191,7 +196,10 @@ $k-select-menu-padding: sizes.$x6; &.k-select--is-menu-visible { .k-select__trigger { border-color: colors.$kobo-alt-blue; - box-shadow: 0 0 0 sizes.$x2 rgba(colors.$kobo-alt-blue, 0.5); + + &:focus-within { + @include mixins.default-ui-focus; + } .k-icon.k-icon-caret-down, .k-icon.k-icon-caret-up { @@ -243,7 +251,10 @@ $k-select-menu-padding: sizes.$x6; &.k-select--is-menu-visible { .k-select__trigger { border-color: colors.$kobo-blue; - box-shadow: 0 0 0 sizes.$x2 rgba(colors.$kobo-alt-blue, 0.5); + + &:focus-within { + @include mixins.default-ui-focus; + } .k-icon.k-icon-caret-down, .k-icon.k-icon-caret-up { diff --git a/jsapp/js/components/common/modal.scss b/jsapp/js/components/common/modal.scss index f2c7aa085f..7feb2a56d2 100644 --- a/jsapp/js/components/common/modal.scss +++ b/jsapp/js/components/common/modal.scss @@ -890,9 +890,7 @@ $modal-custom-header-height: sizes.$x60; .form-media__upload-url { width: 100%; padding-left: 0px; - label.text-box { - width: 100%; - } + button { width: 100%; margin-left: 0px !important; diff --git a/jsapp/js/components/common/radio.scss b/jsapp/js/components/common/radio.scss index 9ea343c0fa..c97e214f38 100644 --- a/jsapp/js/components/common/radio.scss +++ b/jsapp/js/components/common/radio.scss @@ -1,6 +1,7 @@ @use '~kobo-common/src/styles/colors'; @use 'scss/_variables'; @use 'scss/sizes'; +@use 'scss/mixins'; // Note: we can't change this into a CSS Module, because some Form Builder code // relies on `.radio__input` being available. @@ -104,8 +105,7 @@ // Keyboard focus styles &:focus-visible { - outline: 3px solid colors.$kobo-alt-blue; - outline-offset: sizes.$x1; + @include mixins.default-ui-focus; } } } diff --git a/jsapp/js/components/common/textBox.module.scss b/jsapp/js/components/common/textBox.module.scss new file mode 100644 index 0000000000..640715d343 --- /dev/null +++ b/jsapp/js/components/common/textBox.module.scss @@ -0,0 +1,143 @@ +@use 'scss/sizes'; +@use 'scss/mixins'; +@use '~kobo-common/src/styles/colors'; +@use 'scss/_variables'; + +// Note: this needs to override a lot of styles defined in `_kobo.bem.ui.scss`, +// for the context of `.form-modal__item`. Plus we fight the specificity battle +// because of too general styles of Form Builder. +// See: https://github.com/kobotoolbox/kpi/issues/3914 + +$input-color: colors.$kobo-gray-24; + +.root { + width: 100%; + + &.hasValue { + .inputWrapper { + border-color: colors.$kobo-gray-85; + } + } + + &.isDisabled { + .inputWrapper { + color: colors.$kobo-gray-65; + background-color: colors.$kobo-gray-98; + border-color: colors.$kobo-gray-65; + } + + .startIcon, + .endIcon { + color: colors.$kobo-gray-65; + } + } + + &.hasError { + .inputWrapper { + border-color: colors.$kobo-red; + } + + .input { + // Don't type red if there is an error + color: $input-color; + } + + .startIcon, + .endIcon { + color: colors.$kobo-red; + } + } +} + +.label { + color: colors.$kobo-gray-24; + font-size: sizes.$x12; + line-height: 1.6; + margin-bottom: sizes.$x3; +} + +.requiredMark { + // Smaller than the design, because there is also a single whitespace + // character between the label and this mark + margin-left: sizes.$x2; + color: colors.$kobo-red; + font-size: sizes.$x14; + // Magic number to align it similarly to Figma designs + line-height: sizes.$x16; + display: inline-block; + vertical-align: bottom; +} + +.inputWrapper { + display: flex; + flex-direction: row; + align-content: flex-start; + color: $input-color; + background-color: colors.$kobo-white; + border: sizes.$x1 solid colors.$kobo-gray-92; + padding: sizes.$x10 sizes.$x16; + border-radius: sizes.$x6; + + &:focus-within { + @include mixins.default-ui-focus; + } +} + +// We need this crazy selector here to increse the specificity +// TODO: use a normal selector in far future when we no longer have bad CSS code +textarea[class].input.input, +input[class].input.input { + font-size: sizes.$x14; + width: 100%; + margin: 0; + padding: 0; + border: 0; + background-color: transparent; + color: $input-color; + + // The wrapper component is handling focus styles + &:focus { + outline: none; + } + + &:disabled { + color: colors.$kobo-gray-65; + pointer-events: none; + + &::placeholder { + color: colors.$kobo-gray-55; + } + } + + &::placeholder { + color: colors.$kobo-gray-55; + opacity: 1; + } +} + +.startIcon, +.endIcon { + color: colors.$kobo-gray-40; +} + +.startIcon { + margin-right: sizes.$x8; +} + +.endIcon, +.errorIcon { + margin-left: sizes.$x8; +} + +.errorIcon { + color: colors.$kobo-red; +} + +.errorMessages { + font-size: sizes.$x12; + line-height: 1.6; + font-weight: 400; + font-style: normal; + color: colors.$kobo-red; + margin-top: sizes.$x6; +} diff --git a/jsapp/js/components/common/textBox.scss b/jsapp/js/components/common/textBox.scss deleted file mode 100644 index 5f4c10e3af..0000000000 --- a/jsapp/js/components/common/textBox.scss +++ /dev/null @@ -1,98 +0,0 @@ -@use "scss/sizes"; -@use '~kobo-common/src/styles/colors'; -@use "scss/_variables"; - -// TODO: cleanup the other places -// this copies a lot of global styles defined in _kobo.bem.ui.scss -// we copy it because we want to use this component outside of .form-modal__item -// plus we fight the specificity battle because of too general styles of Form Builder -// See: https://github.com/kobotoolbox/kpi/issues/3914 - -label.text-box { - &.text-box--error { - color: colors.$kobo-red; - - textarea.text-box__input, - input.text-box__input { - // Don't type red if there is an error - color: colors.$kobo-gray-24; - border-bottom-color: colors.$kobo-red; - } - } - - &.text-box--on-white input.text-box__input, - &.text-box--on-white textarea.text-box__input { - border-color: colors.$kobo-gray-92; - padding-left: 10px; - padding-right: 10px; - border-radius: 6px; - - &:focus { - border-color: colors.$kobo-blue; - } - } - - &.text-box--on-white.text-box--error input.text-box__input, - &.text-box--on-white.text-box--error textarea.text-box__input { - border-color: colors.$kobo-red; - } - - .text-box__label .text-box__label-link { - display: inline-block; - vertical-align: bottom; - - .k-icon { - font-size: 18px; - display: block; - margin: 1px; - } - } - - .text-box__label { - font-size: sizes.$x13; - line-height: sizes.$x16; - } - - .text-box__label + textarea.text-box__input, - .text-box__label + input.text-box__input { - margin-top: 5px; - } - - textarea.text-box__input, - input.text-box__input { - width: 100%; - padding: 5px 0px; - margin: 0; - font-size: variables.$base-font-size; - color: colors.$kobo-gray-24; - background-color: transparent; - border: 1px solid transparent; - border-bottom-color: colors.$kobo-gray-85; - transition: border-color 0.3s; - - &:focus { - transition: border-color 0.3s; - border-bottom-color: colors.$kobo-blue; - } - - &:disabled { - color: colors.$kobo-gray-65; - pointer-events: none; - - &::placeholder { - color: colors.$kobo-gray-65; - } - } - - &::placeholder { - color: colors.$kobo-gray-65; - opacity: 1; - } - } - - .text-box__error { - font-size: 13px; - font-weight: 400; - font-style: normal; - } -} diff --git a/jsapp/js/components/common/textBox.stories.tsx b/jsapp/js/components/common/textBox.stories.tsx index f70ff01fd2..c2369da463 100644 --- a/jsapp/js/components/common/textBox.stories.tsx +++ b/jsapp/js/components/common/textBox.stories.tsx @@ -1,11 +1,82 @@ import React from 'react'; import type {ComponentStory, ComponentMeta} from '@storybook/react'; import TextBox from './textBox'; +import type {TextBoxType} from './textBox'; +import {IconNames} from 'jsapp/fonts/k-icons'; + +const textBoxTypes: TextBoxType[] = [ + 'email', + 'number', + 'password', + 'text-multiline', + 'text', + 'url', +]; export default { title: 'common/TextBox', component: TextBox, - argTypes: {}, + description: + 'This is a component that displays an input. It uses most of the browser built-in functionalities.', + argTypes: { + type: { + description: + 'Type of the HTML `input`, choosing "text-multiline" will render a `textarea`.', + defaultValue: {summary: 'text'}, + options: textBoxTypes, + control: 'select', + }, + startIcon: { + description: 'Appears inside the input, on the beginning.', + options: IconNames, + control: 'select', + }, + endIcon: { + description: + 'Appears inside the input, on the end. Is replaced by "alert" icon if there are any errors.', + options: IconNames, + control: 'select', + }, + value: {control: 'text'}, + errors: { + description: + 'Displays errors underneath the input and changes the component color to red.', + options: [ + undefined, + true, + 'This is the only error', + ['This is first error', 'This is second error'], + ], + control: { + type: 'radio', + labels: { + undefined: 'No error', + true: 'Error without message (boolean)', + 'This is the only error': 'One error (string)', + // Note: we need to provide literal array-to-string conversion here, + // can't simply use the output. + [String(['This is first error', 'This is second error'])]: + 'Multiple errors (array of strings)', + }, + }, + }, + label: { + control: 'text', + description: + 'Appears above the input. Required for the "required" mark to appear', + }, + placeholder: {control: 'text'}, + readOnly: {control: 'boolean'}, + disabled: {control: 'boolean'}, + required: {control: 'boolean'}, + onChange: {description: 'Input value change callback'}, + onBlur: {description: 'Input blur callback'}, + onKeyPress: {description: 'Input typing callback'}, + customClassNames: { + description: 'Adds custom class name to topmost wrapper', + }, + 'data-cy': {description: 'Cypress identifier'}, + }, } as ComponentMeta; const Template: ComponentStory = (args) => ( @@ -14,22 +85,31 @@ const Template: ComponentStory = (args) => ( export const Primary = Template.bind({}); Primary.args = { - type: 'text', - errors: '', label: 'Your real name', - description: 'We need your first and last name only.', placeholder: 'Type your name...', - readOnly: false, - disabled: false, +}; + +export const Disabled = Template.bind({}); +Disabled.args = { + label: "You can't remove this value", + value: "I'm here to stay!", + disabled: true, }; export const WithErrors = Template.bind({}); WithErrors.args = { label: 'Well done.', - description: 'Here are the test results', placeholder: "We weren't even testing for that", errors: [ 'Horrible person', "I'm serious, that's what it says: 'A horrible person.'", ], }; + +export const WithIcon = Template.bind({}); +WithIcon.args = { + label: 'Your nice and funny username', + placeholder: 'It really needs to be funny, sorry!', + required: true, + startIcon: 'user', +}; diff --git a/jsapp/js/components/common/textBox.tsx b/jsapp/js/components/common/textBox.tsx index ef1dcbf857..6145f9c141 100644 --- a/jsapp/js/components/common/textBox.tsx +++ b/jsapp/js/components/common/textBox.tsx @@ -1,14 +1,31 @@ import React from 'react'; -import bem from 'js/bem'; import TextareaAutosize from 'react-autosize-textarea'; -import './textBox.scss'; - -export type AvailableType = 'email' | 'number' | 'password' | 'text-multiline' | 'text' | 'url'; - -const DefaultType: AvailableType = 'text'; +import styles from './textBox.module.scss'; +import classnames from 'classnames'; +import {ButtonToIconMap} from 'js/components/common/button'; +import type {IconName} from 'jsapp/fonts/k-icons'; +import Icon from './icon'; + +export type TextBoxType = + | 'email' + | 'number' + | 'password' + | 'text-multiline' + | 'text' + | 'url'; + +const DefaultType: TextBoxType = 'text'; interface TextBoxProps { - type?: AvailableType; + type?: TextBoxType; + /** Displays an icon inside the input, on the beginning. */ + startIcon?: IconName; + /** + * Displays an icon inside the input, on the end. + * Note: Displayed only if there are no errors (in such case "alert" icon is + * displayed instead). + */ + endIcon?: IconName; value: string; /** Not needed if `readOnly` */ onChange?: Function; @@ -22,115 +39,152 @@ interface TextBoxProps { errors?: string[] | boolean | string; label?: string; placeholder?: string; - description?: string; readOnly?: boolean; + /** + * Makes the component visually disabled and uses the browser built-in + * functionality + */ disabled?: boolean; - customModifiers?: string[]|string; + /** + * Adds required mark ("*") to the label (if label is provided). + * Note: this adds the built-in browser required input handling, but most + * probably there is a need for additional safety checks within the code that + * uses this component. + */ + required?: boolean; + customClassNames?: string[]; 'data-cy'?: string; } /** - * A text box generic component. + * A generic text box component. It relies on parent to handle all the data + * updates. */ -class TextBox extends React.Component { +export default function TextBox(props: TextBoxProps) { /** * NOTE: I needed to set `| any` for `onChange`, `onBlur` and `onKeyPress` * types to stop TextareaAutosize complaining. */ - onChange(evt: React.ChangeEvent | any) { - if (this.props.readOnly || !this.props.onChange) { + function onChange(evt: React.ChangeEvent | any) { + if (props.readOnly || !props.onChange) { return; } - this.props.onChange(evt.currentTarget.value); + props.onChange(evt.currentTarget.value); } - onBlur(evt: React.FocusEvent | any) { - if (typeof this.props.onBlur === 'function') { - this.props.onBlur(evt.currentTarget.value); + function onBlur(evt: React.FocusEvent | any) { + if (typeof props.onBlur === 'function') { + props.onBlur(evt.currentTarget.value); } } - onKeyPress(evt: React.KeyboardEvent | any) { - if (typeof this.props.onKeyPress === 'function') { - this.props.onKeyPress(evt.key, evt); + function onKeyPress(evt: React.KeyboardEvent | any) { + if (typeof props.onKeyPress === 'function') { + props.onKeyPress(evt.key, evt); } } - render() { - let modifiers = []; - if ( - Array.isArray(this.props.customModifiers) && - typeof this.props.customModifiers[0] === 'string' - ) { - modifiers = this.props.customModifiers; - } else if (typeof this.props.customModifiers === 'string') { - modifiers.push(this.props.customModifiers); - } + const rootClassNames = props.customClassNames || []; + rootClassNames.push(styles.root); - let errors = []; - if (Array.isArray(this.props.errors)) { - errors = this.props.errors; - } else if (typeof this.props.errors === 'string' && this.props.errors.length > 0) { - errors.push(this.props.errors); - } - if (errors.length > 0 || this.props.errors === true) { - modifiers.push('error'); - } + let errors = []; + if (Array.isArray(props.errors)) { + errors = props.errors; + } else if (typeof props.errors === 'string' && props.errors.length > 0) { + errors.push(props.errors); + } + if (errors.length > 0 || props.errors === true) { + rootClassNames.push(styles.hasError); + } - let type = DefaultType; - if (this.props.type) { - type = this.props.type; - } + if (props.disabled) { + rootClassNames.push(styles.isDisabled); + } + + if (props.value) { + rootClassNames.push(styles.hasValue); + } - const inputProps = { - value: this.props.value, - placeholder: this.props.placeholder, - onChange: this.onChange.bind(this), - onBlur: this.onBlur.bind(this), - onKeyPress: this.onKeyPress.bind(this), - readOnly: this.props.readOnly, - disabled: this.props.disabled, - 'data-cy': this.props['data-cy'], - }; - - return ( - - {this.props.label && - - {this.props.label} - - } - - {this.props.type === 'text-multiline' && - + {/* The label over the input */} + {props.label && ( +
+ {props.label}{' '} + {props.required && *} +
+ )} + +
+ {/* The custom icon on the left */} + {props.startIcon && ( + - } - {this.props.type !== 'text-multiline' && - + )} + {props.type !== 'text-multiline' && ( + + )} + + {/* + The custom icon on the right. It is being displayed only if there are + no errors. For TextBox with error, we display always an alert icon. + */} + {errors.length === 0 && props.endIcon && ( + - } - - {this.props.description && - - {this.props.description} - - } - - {errors.length > 0 && - - {errors.map((message: string, index: number) => ( -
{message}
- ))} -
- } - - ); - } + )} + {errors.length > 0 && ( + + )} +
+ + {errors.length > 0 && ( +
+ {errors.map((message: string, index: number) => ( +
{message}
+ ))} +
+ )} + + ); } - -export default TextBox; diff --git a/jsapp/js/components/dataAttachments/connect-projects.scss b/jsapp/js/components/dataAttachments/connect-projects.scss index edaba6d01e..c516e6f88e 100644 --- a/jsapp/js/components/dataAttachments/connect-projects.scss +++ b/jsapp/js/components/dataAttachments/connect-projects.scss @@ -1,29 +1,30 @@ @use '~kobo-common/src/styles/colors'; -@use "scss/_variables"; +@use 'scss/mixins'; +@use 'scss/breakpoints'; +@use 'scss/sizes'; +@use 'scss/_variables'; .connect-projects { - i.k-icon { - font-size: 32px; - margin-right: 5px; - } - .form-view__cell--page-title { font-size: variables.$base-font-size !important; display: flex; - margin-top: 20px !important; - i { - margin-top: 10px; + margin-top: sizes.$x20 !important; + + i.k-icon { + margin-top: sizes.$x10; + font-size: sizes.$x32; + margin-right: sizes.$x5; } } .connect-projects__export { display: block; - margin-top: 20px; + margin-top: sizes.$x20; .connect-projects__export-options { display: flex; justify-content: space-between; - padding-bottom: 12px; + padding-bottom: sizes.$x12; .toggle-switch { .toggle-switch__label { @@ -43,8 +44,8 @@ display: flex; justify-content: space-between; position: relative; - padding-top: 12px; - border-top: 1px solid; + padding-top: sizes.$x12; + border-top: sizes.$x1 solid; border-color: colors.$kobo-gray-92; .connect-projects__export-hint { @@ -52,7 +53,7 @@ } .multi-checkbox { - height: 200px; + height: sizes.$x200; width: 50%; } } @@ -65,33 +66,29 @@ flex-direction: row; align-items: center; align-content: center; - margin-top: 10px; + margin-top: sizes.$x10; .kobo-select__wrapper { width: 50%; - margin-right: 50px; + margin-right: sizes.$x50; + .kobo-select__placeholder { color: colors.$kobo-gray-24; } } - .text-box { + .connect-projects-textbox { width: 35%; - margin-top: 0px; - margin-right: 24px; - .text-box__input { - padding: 9px 10px; - background-color: colors.$kobo-white; - color: colors.$kobo-gray-24; - } + margin-right: sizes.$x24; } } } + .connect-projects__import-list { - margin-top: 20px; + margin-top: sizes.$x20; label { - margin-top: 20px; + margin-top: sizes.$x20; font-size: variables.$base-font-size; font-weight: bold; color: colors.$kobo-gray-40; @@ -102,75 +99,54 @@ position: relative; display: flex; justify-content: space-between; - margin-top: 7px; - margin-bottom: 10px; - border-bottom: 1px solid; + margin-top: sizes.$x8; + margin-bottom: sizes.$x10; + border-bottom: sizes.$x1 solid; border-color: colors.$kobo-gray-92; } .connect-projects__import-list-item--no-imports { font-style: italic; color: colors.$kobo-gray-65; - // Match vertial height of a regular list item + // Match vertcial height of a regular list item padding: 11px 0 11px 11px; } .connect-projects__import-list-item { - padding-bottom: 10px; + padding-bottom: sizes.$x10; i.k-icon-check { + font-size: sizes.$x32; + margin-right: sizes.$x5; color: colors.$kobo-blue; } .connect-projects__import-labels { position: absolute; - top: 6px; - left: 32px; + top: sizes.$x6; + left: sizes.$x32; font-weight: 500; - i.k-icon-check { - color: colors.$kobo-blue; - font-weight: bold; - } - .connect-projects__import-labels-source { - margin-left: 24px; + margin-left: sizes.$x24; font-weight: 400; color: colors.$kobo-gray-40; } } .connect-projects__import-options { - position: relative; - .kobo-light-button { - position: absolute; - right: 0; - padding-bottom: 25px; - - i.k-icon { - font-size: 24px; - } - - &:not(:first-child) { - margin-right: 45px; - } - } - - } - } - .loading__inner { - i { - vertical-align: text-bottom; + @include mixins.centerRowFlex; + gap: sizes.$x10; } } } } .form-modal__form.form-modal__form--data-attachment-columns { - color: colors.$kobo-gray-55; + color: colors.$kobo-gray-55; .bulk-options { - margin-top: 14px; + margin-top: sizes.$x14; display: flex; justify-content: space-between; @@ -180,7 +156,7 @@ .bulk-options__buttons { span { - margin: 12px; + margin: sizes.$x12; } a { @@ -191,39 +167,37 @@ } .multi-checkbox { - margin-top: 12px; - height: 200px; + margin-top: sizes.$x12; + height: sizes.$x200; } .loading { - margin-top: 12px; + margin-top: sizes.$x12; } .modal__footer { text-align: center; button { - padding-left: 64px; - padding-right: 64px; + padding-left: sizes.$x60; + padding-right: sizes.$x60; } } - } - // Compensate for when sidebar(s) messes up modal a bit -//TODO: Clean this up via PR changes +// TODO: Clean this up via PR changes // See: https://github.com/kobotoolbox/kpi/issues/3912 @media - (min-width: 1000px) and (max-width: 1140px), - (min-width: 770px) and (max-width: 860px), - (max-width: 700px) { + (min-width: breakpoints.$b1000) and (max-width: breakpoints.$b1140), + (min-width: breakpoints.$b768) and (max-width: breakpoints.$b860), + (max-width: breakpoints.$b700) { .connect-projects__export-multicheckbox { display: block !important; .multi-checkbox { - margin-top: 12px; + margin-top: sizes.$x12; width: 100% !important; overflow-x: scroll; } @@ -234,27 +208,23 @@ .kobo-select__wrapper { width: 100% !important; - margin-bottom: 12px; - } - - .text-box { - width: 100%; + margin-bottom: sizes.$x12; } .kobo-button { display: block; - margin-top: 12px auto 0; + margin-top: sizes.$x12 auto 0; width: 70%; } } } -@media screen and (max-width: 530px) { +@media screen and (max-width: breakpoints.$b530) { .connect-projects__export-options { display: block !important; .checkbox { - margin-top: 20px; + margin-top: sizes.$x20; width: 100% !important; } } diff --git a/jsapp/js/components/dataAttachments/connectProjects.es6 b/jsapp/js/components/dataAttachments/connectProjects.es6 index 71ea64015a..e2810bb37e 100644 --- a/jsapp/js/components/dataAttachments/connectProjects.es6 +++ b/jsapp/js/components/dataAttachments/connectProjects.es6 @@ -6,6 +6,7 @@ import Select from 'react-select'; import ToggleSwitch from 'js/components/common/toggleSwitch'; import Checkbox from 'js/components/common/checkbox'; import TextBox from 'js/components/common/textBox'; +import Button from 'js/components/common/button'; import MultiCheckbox from 'js/components/common/multiCheckbox'; import {actions} from 'js/actions'; import {stores} from 'js/stores'; @@ -443,6 +444,7 @@ class ConnectProjects extends React.Component { {this.renderSelect()}
- this.onRemoveAttachment(item.attachmentUrl)} - > - - + /> - this.showColumnFilterModal( this.props.asset, { @@ -510,9 +516,7 @@ class ConnectProjects extends React.Component { item.linkedFields, item.attachmentUrl, )} - > - - + />
); diff --git a/jsapp/js/components/metadataEditor.es6 b/jsapp/js/components/metadataEditor.es6 index 6606a3d3be..212ab8e820 100644 --- a/jsapp/js/components/metadataEditor.es6 +++ b/jsapp/js/components/metadataEditor.es6 @@ -2,6 +2,7 @@ import React from 'react'; import autoBind from 'react-autobind'; import Checkbox from 'js/components/common/checkbox'; import TextBox from 'js/components/common/textBox'; +import Icon from 'js/components/common/icon'; import ToggleSwitch from 'js/components/common/toggleSwitch'; import Select from 'react-select'; import {assign} from 'utils'; @@ -10,8 +11,15 @@ import { SURVEY_DETAIL_ATTRIBUTES, FUNCTION_TYPE, } from 'js/constants'; -import bem from 'js/bem'; +import bem, {makeBem} from 'js/bem'; import envStore from 'js/envStore'; +import './metadataEditor.scss'; + +bem.FormBuilderMeta = makeBem(null, 'form-builder-meta'); +bem.FormBuilderMeta__columns = makeBem(bem.FormBuilderMeta, 'columns'); +bem.FormBuilderMeta__column = makeBem(bem.FormBuilderMeta, 'column'); +bem.FormBuilderMeta__row = makeBem(bem.FormBuilderMeta, 'row'); +bem.FormBuilderMeta__labelLink = makeBem(bem.FormBuilderMeta, 'label-link', 'a'); const AUDIT_SUPPORT_URL = 'audit_logging.html'; const RECORDING_SUPPORT_URL = 'recording-interviews.html'; @@ -143,14 +151,12 @@ export default class MetadataEditor extends React.Component { {envStore.isReady && envStore.data.support_url && ( - - - + + )} ); @@ -163,12 +169,12 @@ export default class MetadataEditor extends React.Component { {envStore.isReady && envStore.data.support_url && ( - - - + + )} ); @@ -232,7 +238,6 @@ export default class MetadataEditor extends React.Component { {this.isAuditEnabled() && ( {this.props.question.label} - {/* Icon gets populated via CSS like formbuilder, see: kpi#3133 */} - - {t('Add')} - - ); - } - renderFileName(item) { // Check if current item is uploaded via URL. `redirect_url` is the indicator var fileName = item.metadata.filename; @@ -295,7 +275,15 @@ class FormMedia extends React.Component { onChange={this.onInputURLChange} /> - {this.renderButton()} +