diff --git a/frontend/documentation/components/FieldError.stories.tsx b/frontend/documentation/components/FieldError.stories.tsx new file mode 100644 index 000000000000..bb22af48a9a1 --- /dev/null +++ b/frontend/documentation/components/FieldError.stories.tsx @@ -0,0 +1,29 @@ +import React from 'react' +import type { Meta, StoryObj } from 'storybook' + +import FieldError from 'components/base/forms/FieldError' + +const meta: Meta = { + component: FieldError, + parameters: { layout: 'padded' }, + title: 'Components/Forms/FieldError', +} +export default meta + +type Story = StoryObj + +export const Default: Story = { + render: () => , +} + +export const RichMessage: Story = { + render: () => ( + + Must be a valid email address. + + } + /> + ), +} diff --git a/frontend/documentation/components/FieldLabel.stories.tsx b/frontend/documentation/components/FieldLabel.stories.tsx new file mode 100644 index 000000000000..acfacaa19bd2 --- /dev/null +++ b/frontend/documentation/components/FieldLabel.stories.tsx @@ -0,0 +1,33 @@ +import React from 'react' +import type { Meta, StoryObj } from 'storybook' + +import FieldLabel from 'components/base/forms/FieldLabel' + +const meta: Meta = { + component: FieldLabel, + parameters: { layout: 'padded' }, + title: 'Components/Forms/FieldLabel', +} +export default meta + +type Story = StoryObj + +export const Default: Story = { + render: () => Email, +} + +export const Required: Story = { + render: () => ( + + Email + + ), +} + +export const WithTooltip: Story = { + render: () => ( + + Email + + ), +} diff --git a/frontend/web/components/BreadcrumbSeparator.tsx b/frontend/web/components/BreadcrumbSeparator.tsx index 1bee94c62a4e..8162ad01e72e 100644 --- a/frontend/web/components/BreadcrumbSeparator.tsx +++ b/frontend/web/components/BreadcrumbSeparator.tsx @@ -1,4 +1,11 @@ -import React, { FC, ReactNode, useEffect, useRef, useState } from 'react' +import React, { + FC, + KeyboardEvent, + ReactNode, + useEffect, + useRef, + useState, +} from 'react' import { IonIcon } from '@ionic/react' import { checkmarkCircle, chevronDown, chevronUp } from 'ionicons/icons' import InlineModal from './InlineModal' @@ -334,10 +341,8 @@ const BreadcrumbSeparator: FC = ({ > - navigateOrganisations(e, organisations) - } - onChange={(e: KeyboardEvent) => { + onKeyDown={(e) => navigateOrganisations(e, organisations)} + onChange={(e) => { setOrganisationSearch(Utils.safeParseEventValue(e)) }} search @@ -390,11 +395,11 @@ const BreadcrumbSeparator: FC = ({ )} > { + onChange={(e) => { setProjectSearch(Utils.safeParseEventValue(e)) }} autoFocus={focus === 'project'} - onKeyDown={(e: KeyboardEvent) => navigateProjects(e)} + onKeyDown={(e) => navigateProjects(e)} search className='full-width' inputClassName='border-0 bg-transparent border-bottom-1' diff --git a/frontend/web/components/ChangeRequestsSetting.tsx b/frontend/web/components/ChangeRequestsSetting.tsx index fe886247c38e..a4c7141a66a5 100644 --- a/frontend/web/components/ChangeRequestsSetting.tsx +++ b/frontend/web/components/ChangeRequestsSetting.tsx @@ -46,7 +46,7 @@ const ChangeRequestsSetting: FC = ({ name='env-name' disabled={isLoading} min={0} - onChange={(e: InputEvent) => { + onChange={(e) => { if (!Utils.safeParseEventValue(e)) return onChange(parseInt(Utils.safeParseEventValue(e))) }} diff --git a/frontend/web/components/GroupSelect.tsx b/frontend/web/components/GroupSelect.tsx index c2df9148f6fa..87f18bba4607 100644 --- a/frontend/web/components/GroupSelect.tsx +++ b/frontend/web/components/GroupSelect.tsx @@ -44,7 +44,7 @@ const GroupSelect: FC = ({ setFilter(Utils.safeParseEventValue(e))} + onChange={(e) => setFilter(Utils.safeParseEventValue(e))} className='full-width mb-2' placeholder='Type or choose a Group' search diff --git a/frontend/web/components/PermissionsTabs.tsx b/frontend/web/components/PermissionsTabs.tsx index 3857234bb415..4748cace9933 100644 --- a/frontend/web/components/PermissionsTabs.tsx +++ b/frontend/web/components/PermissionsTabs.tsx @@ -110,9 +110,7 @@ const PermissionsTabs: FC = ({ type='text' className='ml-3' value={searchProject} - onChange={(e: InputEvent) => - setSearchProject(Utils.safeParseEventValue(e)) - } + onChange={(e) => setSearchProject(Utils.safeParseEventValue(e))} size='small' placeholder='Search Projects' search @@ -142,9 +140,7 @@ const PermissionsTabs: FC = ({ type='text' className='ml-3' value={searchEnv} - onChange={(e: InputEvent) => - setSearchEnv(Utils.safeParseEventValue(e)) - } + onChange={(e) => setSearchEnv(Utils.safeParseEventValue(e))} size='small' placeholder='Search Environments' search diff --git a/frontend/web/components/RolesSelect.tsx b/frontend/web/components/RolesSelect.tsx index 737fd3abee58..f98b2327bae4 100644 --- a/frontend/web/components/RolesSelect.tsx +++ b/frontend/web/components/RolesSelect.tsx @@ -44,7 +44,7 @@ const RoleSelect: FC = ({ setFilter(Utils.safeParseEventValue(e))} + onChange={(e) => setFilter(Utils.safeParseEventValue(e))} className='full-width mb-2' placeholder='Type or choose a Role' search diff --git a/frontend/web/components/base/LabelWithTooltip.tsx b/frontend/web/components/base/LabelWithTooltip.tsx index 9b4fefa6b4a4..d15e289c826b 100644 --- a/frontend/web/components/base/LabelWithTooltip.tsx +++ b/frontend/web/components/base/LabelWithTooltip.tsx @@ -1,8 +1,8 @@ import Icon from 'components/icons/Icon' -import { FC } from 'react' +import { FC, ReactNode } from 'react' interface LabelWithTooltipProps { - label: string + label: ReactNode tooltip?: string } diff --git a/frontend/web/components/base/forms/FieldError.tsx b/frontend/web/components/base/forms/FieldError.tsx new file mode 100644 index 000000000000..932013109660 --- /dev/null +++ b/frontend/web/components/base/forms/FieldError.tsx @@ -0,0 +1,30 @@ +import React, { FC, ReactNode } from 'react' +import cn from 'classnames' + +interface FieldErrorProps { + // The field-level error to show; renders nothing when falsy. + error?: ReactNode + // Set so the control can reference it via aria-describedby. + id?: string + className?: string +} + +// Inline, per-field validation message shown beneath a control. The small-text +// counterpart to ErrorMessage (which is a banner alert for API/form-level +// errors) — use FieldError for a single field's validation. +const FieldError: FC = ({ className, error, id }) => { + if (!error) { + return null + } + return ( + + {error} + + ) +} + +export default FieldError diff --git a/frontend/web/components/base/forms/FieldLabel.tsx b/frontend/web/components/base/forms/FieldLabel.tsx new file mode 100644 index 000000000000..caff45cba86e --- /dev/null +++ b/frontend/web/components/base/forms/FieldLabel.tsx @@ -0,0 +1,46 @@ +import React, { FC, ReactNode } from 'react' +import cn from 'classnames' +import LabelWithTooltip from 'components/base/LabelWithTooltip' + +interface FieldLabelProps { + // Associates the label with its control; required for accessibility. + htmlFor?: string + children: ReactNode + // Shows a danger asterisk after the label. + required?: boolean + // When set, an info icon follows the label and reveals this text on hover. + tooltip?: string + className?: string +} + +// The label for a form field — wires `htmlFor` to the control, with an optional +// required indicator and an info-icon tooltip. The tooltip is rendered by the +// shared LabelWithTooltip (which uses the DS Tooltip), not hand-rolled here. +const FieldLabel: FC = ({ + children, + className, + htmlFor, + required, + tooltip, +}) => ( + +) + +export default FieldLabel diff --git a/frontend/web/components/base/forms/Input.js b/frontend/web/components/base/forms/Input.js deleted file mode 100644 index f00c39f82520..000000000000 --- a/frontend/web/components/base/forms/Input.js +++ /dev/null @@ -1,237 +0,0 @@ -/** - * Created by kylejohnson on 30/07/2016. - */ -import MaskedInput from 'react-maskedinput' -import cn from 'classnames' -import Icon from 'components/icons/Icon' -import Radio from './Radio' -import Checkbox from './Checkbox' - -const maskedCharacters = { - 'a': { - validate(char) { - return /[ap]/.test(char) - }, - }, - 'm': { - transform() { - return 'm' - }, - validate(char) { - return /\w/.test(char) - }, - }, -} - -const sizeClassNames = { - default: '', - large: 'input-lg', - small: 'input-sm', - xSmall: 'input-xsm', -} - -const Input = class extends React.Component { - static displayName = 'Input' - - constructor(props, context) { - super(props, context) - this.state = { - shouldValidate: !!this.props.value || this.props.autoValidate, - type: this.props.type, - } - } - onFocus = (e) => { - this.setState({ - isFocused: true, - }) - this.props.onFocus && this.props.onFocus(e) - } - - focus = () => { - if (E2E) return - this.input.focus() - } - - onKeyDown = (e) => { - if (Utils.keys.isEscape(e)) { - this.input.blur() - } - this.props.onKeyDown && this.props.onKeyDown(e) - } - - validate = () => { - this.setState({ - shouldValidate: true, - }) - } - - onBlur = (e) => { - this.setState({ - isFocused: false, - shouldValidate: true, - }) - this.props.onBlur && this.props.onBlur(e) - } - - render() { - const { - centered, - disabled, - inputClassName, - isValid, - mask, - placeholderChar, - showSuccess, - size, - underline, - ...rest - } = this.props - - const invalid = this.state.shouldValidate && !isValid - const success = isValid && showSuccess - const className = cn( - { - 'focused': this.state.isFocused, - 'input-container': true, - 'input-underline': underline, - invalid, - 'password': this.props.type === 'password', - 'search': this.props.search, - success, - }, - this.props.className, - ) - - const innerClassName = cn( - { - 'input': true, - 'text-center': centered, - }, - inputClassName, - sizeClassNames[size], - ) - - if (this.props.type === 'checkbox') { - return ( - - ) - } else if (this.props.type === 'radio') { - return ( - - ) - } - - return ( -
- {mask ? ( - (this.input = c)} - {...rest} - mask={this.props.mask} - type={this.state.type} - formatCharacters={maskedCharacters} - onKeyDown={this.onKeyDown} - onFocus={this.onFocus} - onBlur={this.onBlur} - className={innerClassName} - placeholderChar={placeholderChar} - /> - ) : ( - (this.input = c)} - {...rest} - onFocus={this.onFocus} - onKeyDown={this.onKeyDown} - type={this.state.type} - onBlur={this.onBlur} - value={this.props.value} - className={innerClassName} - disabled={disabled} - autoComplete={ - this.props.enableAutoComplete ?? this.props.autocomplete - } - /> - )} - {this.props.type === 'password' && ( - { - if (!disabled) { - this.setState({ - type: this.state.type === 'password' ? 'text' : 'password', - }) - } - }} - > - - - )} - {this.props.search && ( - - - - )} -
- ) - } -} - -Input.defaultProps = { - className: '', - isValid: true, - placeholderChar: ' ', -} - -Input.propTypes = { - autocomplete: propTypes.string, - centered: propTypes.bool, - className: propTypes.any, - inputClassName: OptionalString, - isValid: propTypes.any, - mask: OptionalString, - onBlur: OptionalFunc, - onFocus: OptionalFunc, - onKeyDown: OptionalFunc, - onSearchChange: OptionalFunc, - placeholderChar: OptionalString, - search: propTypes.Boolean, - size: OptionalString, - underline: propTypes.bool, -} - -export default Input diff --git a/frontend/web/components/base/forms/Input.tsx b/frontend/web/components/base/forms/Input.tsx new file mode 100644 index 000000000000..5b63ec342081 --- /dev/null +++ b/frontend/web/components/base/forms/Input.tsx @@ -0,0 +1,176 @@ +import React, { + FocusEvent, + KeyboardEvent, + Ref, + useImperativeHandle, + useRef, + useState, +} from 'react' +import cn from 'classnames' +import Icon from 'components/icons/Icon' +import Utils from 'common/utils/utils' +import { colorIconDanger } from 'common/theme/tokens' + +type InputSize = 'default' | 'large' | 'small' | 'xSmall' + +// Imperative API exposed via ref (React 19 ref-as-prop, no forwardRef). +// focus() is a no-op under E2E, matching the original behaviour. +export interface InputMethods { + focus: () => void +} + +export interface InputProps + extends Omit, 'size'> { + autoValidate?: boolean + // Custom lowercase alias for the standard `autoComplete`, kept for back-compat. + autocomplete?: string + centered?: boolean + enableAutoComplete?: string + inputClassName?: string + isValid?: boolean + ref?: Ref + search?: boolean + showSuccess?: boolean + size?: InputSize + underline?: boolean +} + +const sizeClassNames: Record = { + default: '', + large: 'input-lg', + small: 'input-sm', + xSmall: 'input-xsm', +} + +// Width of the password reveal / search icons per input size. +const iconWidthBySize: Record = { + default: undefined, + large: 24, + small: 20, + xSmall: 18, +} + +const Input: React.FC = ({ + autoValidate, + autocomplete, + centered, + className = '', + disabled, + enableAutoComplete, + inputClassName, + isValid = true, + onBlur: onBlurProp, + onChange, + onFocus: onFocusProp, + onKeyDown: onKeyDownProp, + ref, + search, + showSuccess, + size, + type: typeProp, + underline, + value, + ...rest +}) => { + const inputRef = useRef(null) + const [isFocused, setIsFocused] = useState(false) + const [shouldValidate, setShouldValidate] = useState( + !!value || !!autoValidate, + ) + const [type, setType] = useState(typeProp) + + // No-op under E2E to avoid programmatic focus stealing during tests; native + // autoFocus is unaffected. Matches the original Input.focus() behaviour. + useImperativeHandle(ref, () => ({ + focus: () => { + if (E2E) return + inputRef.current?.focus() + }, + })) + + const onFocus = (e: FocusEvent) => { + setIsFocused(true) + onFocusProp?.(e) + } + + const onBlur = (e: FocusEvent) => { + setIsFocused(false) + setShouldValidate(true) + onBlurProp?.(e) + } + + const onKeyDown = (e: KeyboardEvent) => { + if (Utils.keys.isEscape(e)) { + e.currentTarget.blur() + } + onKeyDownProp?.(e) + } + + const invalid = shouldValidate && !isValid + const success = isValid && showSuccess + const sizeClassName = size ? sizeClassNames[size] : '' + const containerClassName = cn( + { + 'focused': isFocused, + 'input-container': true, + 'input-underline': underline, + invalid, + 'password': typeProp === 'password', + 'search': search, + success, + }, + className, + ) + const innerClassName = cn( + { 'input': true, 'text-center': centered }, + inputClassName, + sizeClassName, + ) + const iconWidth = size ? iconWidthBySize[size] : undefined + + return ( +
+ + {typeProp === 'password' && ( + { + if (!disabled) { + setType(type === 'password' ? 'text' : 'password') + } + }} + > + + + )} + {search && ( + + + + )} +
+ ) +} + +Input.displayName = 'Input' + +export default Input diff --git a/frontend/web/components/import-export/ImportPage.tsx b/frontend/web/components/import-export/ImportPage.tsx index 415182fc7621..f4271b284248 100644 --- a/frontend/web/components/import-export/ImportPage.tsx +++ b/frontend/web/components/import-export/ImportPage.tsx @@ -130,9 +130,7 @@ const ImportPage: FC = ({ projectId, projectName }) => { - setLDKey(Utils.safeParseEventValue(e)) - } + onChange={(e) => setLDKey(Utils.safeParseEventValue(e))} type='text' placeholder='My LaunchDarkly key' /> diff --git a/frontend/web/components/inspect-permissions/InspectPermissions.tsx b/frontend/web/components/inspect-permissions/InspectPermissions.tsx index ecb8e7f31117..09052fb06e6a 100644 --- a/frontend/web/components/inspect-permissions/InspectPermissions.tsx +++ b/frontend/web/components/inspect-permissions/InspectPermissions.tsx @@ -81,9 +81,7 @@ const InspectPermissions: FC = ({ type='text' className='ml-3' value={searchEnv} - onChange={(e: InputEvent) => - setSearchEnv(Utils.safeParseEventValue(e)) - } + onChange={(e) => setSearchEnv(Utils.safeParseEventValue(e))} size='small' placeholder='Search Environments' search diff --git a/frontend/web/components/inspect-permissions/ProjectPermissions.tsx b/frontend/web/components/inspect-permissions/ProjectPermissions.tsx index 1b9c4f40d274..89ab477fabe4 100644 --- a/frontend/web/components/inspect-permissions/ProjectPermissions.tsx +++ b/frontend/web/components/inspect-permissions/ProjectPermissions.tsx @@ -26,7 +26,7 @@ const ProjectPermissions = ({ userId }: { userId?: number }) => { type='text' className='ml-3=' value={searchProject} - onChange={(e: InputEvent) => { + onChange={(e) => { setSearchProject(Utils.safeParseEventValue(e)) }} size='small' diff --git a/frontend/web/components/modals/CreateEditIntegrationModal.tsx b/frontend/web/components/modals/CreateEditIntegrationModal.tsx index feec008ca93a..538a521f2990 100644 --- a/frontend/web/components/modals/CreateEditIntegrationModal.tsx +++ b/frontend/web/components/modals/CreateEditIntegrationModal.tsx @@ -11,6 +11,7 @@ import Project from 'common/project' import AccountStore from 'common/stores/account-store' import Utils from 'common/utils/utils' import Input from 'components/base/forms/Input' +import Checkbox from 'components/base/forms/Checkbox' import { IntegrationData, IntegrationField, @@ -397,12 +398,11 @@ const CreateEditIntegration: FC = (props) => { if (field.inputType === 'checkbox') { return (
- update(field.key, e)} - type='checkbox' + checked={!!(formData[field.key] ?? field.default)} + onChange={(value) => update(field.key, value)} />
) diff --git a/frontend/web/components/modals/CreateSegmentRulesTabForm.tsx b/frontend/web/components/modals/CreateSegmentRulesTabForm.tsx index aa0f8bc174ce..bf48efcb810f 100644 --- a/frontend/web/components/modals/CreateSegmentRulesTabForm.tsx +++ b/frontend/web/components/modals/CreateSegmentRulesTabForm.tsx @@ -139,7 +139,7 @@ const CreateSegmentRulesTabForm: React.FC = ({ id='segmentID' maxLength={SEGMENT_ID_MAXLENGTH} value={name} - onChange={(e: InputEvent) => { + onChange={(e) => { setValueChanged(true) setName( Format.enumeration @@ -147,7 +147,7 @@ const CreateSegmentRulesTabForm: React.FC = ({ .toLowerCase(), ) }} - isValid={name && name.length} + isValid={!!(name && name.length)} type='text' placeholder='E.g. power_users' /> @@ -162,7 +162,7 @@ const CreateSegmentRulesTabForm: React.FC = ({ name: 'featureDesc', readOnly: !!identity || readOnly, }} - onChange={(e: InputEvent) => { + onChange={(e: React.ChangeEvent) => { setValueChanged(true) setDescription(Utils.safeParseEventValue(e)) }} diff --git a/frontend/web/components/pages/GitHubSetupPage.tsx b/frontend/web/components/pages/GitHubSetupPage.tsx index faa6974a3559..299c6a17134a 100644 --- a/frontend/web/components/pages/GitHubSetupPage.tsx +++ b/frontend/web/components/pages/GitHubSetupPage.tsx @@ -159,12 +159,8 @@ const GitHubSetupPage: FC = ({ location }) => { - setRepositoryOwner(Utils.safeParseEventValue(e)) - } + name='repositoryOwner' + onChange={(e) => setRepositoryOwner(Utils.safeParseEventValue(e))} disabled type='text' title={'Repository Owner'} diff --git a/frontend/web/components/pages/UsersAndPermissionsPage.tsx b/frontend/web/components/pages/UsersAndPermissionsPage.tsx index e958bd489964..064c6b3327d3 100644 --- a/frontend/web/components/pages/UsersAndPermissionsPage.tsx +++ b/frontend/web/components/pages/UsersAndPermissionsPage.tsx @@ -234,6 +234,7 @@ const UsersAndPermissionsInner: FC = ({ for your plan.{' '} {usedSeats && ( <> + {/* eslint-disable-next-line no-nested-ternary */} {overSeats && (!verifySeatsLimit || !autoSeats) ? ( @@ -347,8 +348,7 @@ const UsersAndPermissionsInner: FC = ({ data-test='invite-link' inputClassName='input input--wide' type='text' - readonly='readonly' - title={

Link

} + readOnly placeholder='Link' size='small' /> diff --git a/frontend/web/components/pages/sdk-keys/SDKKeysPage.tsx b/frontend/web/components/pages/sdk-keys/SDKKeysPage.tsx index 604857c6b1ef..74d94ceee9a6 100644 --- a/frontend/web/components/pages/sdk-keys/SDKKeysPage.tsx +++ b/frontend/web/components/pages/sdk-keys/SDKKeysPage.tsx @@ -53,7 +53,6 @@ const SDKKeysPage: FC = () => { value={environmentId} inputClassName='input input--wide' type='text' - title={

Client-side Environment Key

} placeholder='Client-side Environment Key' /> diff --git a/frontend/web/components/segments/Rule/components/RuleConditionValueInput.tsx b/frontend/web/components/segments/Rule/components/RuleConditionValueInput.tsx index 884aedaca4a4..418b46074a90 100644 --- a/frontend/web/components/segments/Rule/components/RuleConditionValueInput.tsx +++ b/frontend/web/components/segments/Rule/components/RuleConditionValueInput.tsx @@ -112,13 +112,13 @@ const RuleConditionValueInput: React.FC = ({ data-test={props['data-test']} name='rule-condition-value-input' aria-label='Rule condition value input' - value={value} + value={typeof value === 'boolean' ? String(value) : value} className='w-100' inputClassName={ showIcon ? `pr-5 ${hasWarning ? 'border-warning' : ''}` : '' } style={{ width: '100%' }} - onChange={(e: InputEvent) => { + onChange={(e) => { const value = Utils.safeParseEventValue(e) onChange?.(value) }} diff --git a/frontend/web/components/tables/TableFilterOptions.tsx b/frontend/web/components/tables/TableFilterOptions.tsx index bbce031fdd37..86aa64310584 100644 --- a/frontend/web/components/tables/TableFilterOptions.tsx +++ b/frontend/web/components/tables/TableFilterOptions.tsx @@ -1,7 +1,7 @@ import React, { FC, ReactNode, useMemo, useState } from 'react' import InlineModal from 'components/InlineModal' import { IonIcon } from '@ionic/react' -import { caretDown, search } from 'ionicons/icons' +import { caretDown } from 'ionicons/icons' import classNames from 'classnames' import TableFilterItem from './TableFilterItem' import Input from 'components/base/forms/Input' @@ -82,7 +82,7 @@ const TableFilter: FC = ({
{ + onChange={(e) => { setFilter(Utils.safeParseEventValue(e)) }} className='full-width' diff --git a/frontend/web/components/tables/TableSearchFilter.tsx b/frontend/web/components/tables/TableSearchFilter.tsx index e58833b3f663..31981fba4ba5 100644 --- a/frontend/web/components/tables/TableSearchFilter.tsx +++ b/frontend/web/components/tables/TableSearchFilter.tsx @@ -17,7 +17,7 @@ const TableSearchFilter: FC = ({ onChange, value }) => { return ( { + onChange={(e) => { const v = Utils.safeParseEventValue(e) setLocalValue(v) debouncedOnChange(v) diff --git a/frontend/web/components/tables/TableTagFilter.tsx b/frontend/web/components/tables/TableTagFilter.tsx index 387c8f110290..8458ec01083f 100644 --- a/frontend/web/components/tables/TableTagFilter.tsx +++ b/frontend/web/components/tables/TableTagFilter.tsx @@ -104,7 +104,7 @@ const TableTagFilter: FC = ({
{ + onChange={(e) => { setFilter(Utils.safeParseEventValue(e)) }} className='full-width' diff --git a/frontend/web/components/tags/AddEditTags.tsx b/frontend/web/components/tags/AddEditTags.tsx index 774a048e491f..48a32447b7c5 100644 --- a/frontend/web/components/tags/AddEditTags.tsx +++ b/frontend/web/components/tags/AddEditTags.tsx @@ -169,9 +169,7 @@ const AddEditTags: FC = ({ submit() } }} - onChange={(e: InputEvent) => - setFilter(Utils.safeParseEventValue(e)) - } + onChange={(e) => setFilter(Utils.safeParseEventValue(e))} size='xSmall' className='full-width' placeholder='Search tags...' diff --git a/frontend/web/styles/components/_input.scss b/frontend/web/styles/components/_input.scss index 104bf6c6f7ce..8df5a011e6b0 100644 --- a/frontend/web/styles/components/_input.scss +++ b/frontend/web/styles/components/_input.scss @@ -4,21 +4,21 @@ textarea { width: 100%; height: $textarea-height; outline: none; - border: 1px solid $input-border-color; + border: 1px solid var(--color-border-default); border-radius: $border-radius; - color: $body-color; + color: var(--color-text-default); line-height: $line-height-lg; padding: $input-padding; font-weight: $input-font-weight; &::placeholder { - color: $input-placeholder-color !important; + color: var(--color-text-tertiary) !important; font-weight: normal; } &:hover { - border-color: $basic-alpha-48; + border-color: var(--color-border-strong); } &:focus { - border-color: $primary; + border-color: var(--color-border-action); } &.textarea-lg { padding: $input-padding-lg; @@ -33,7 +33,7 @@ textarea { .input-container.input-underline { input.input { border: none; - border-bottom: 1px solid $input-border-color; + border-bottom: 1px solid var(--color-border-default); border-radius: 0; background-color: transparent; padding-left: 8px; @@ -41,11 +41,11 @@ textarea { // _forms.scss re-applies a full border shorthand on hover/focus. &:hover { border: none; - border-bottom: 1px solid $basic-alpha-48; + border-bottom: 1px solid var(--color-border-strong); } &:focus { border: none; - border-bottom: 1px solid $primary; + border-bottom: 1px solid var(--color-border-action); } } input[type='number'].input { @@ -65,15 +65,15 @@ textarea { .dark .input-container.input-underline { input.input { border: none; - border-bottom: 1px solid $white-alpha-16; + border-bottom: 1px solid var(--color-border-default); background-color: transparent; &:hover { border: none; - border-bottom: 1px solid $white-alpha-48; + border-bottom: 1px solid var(--color-border-strong); } &:focus { border: none; - border-bottom: 1px solid $primary; + border-bottom: 1px solid var(--color-border-action); } } } @@ -88,7 +88,7 @@ textarea { background-color: transparent !important; &.invalid hr, &.invalid hr.highlight { - border-color: $alert-danger-border-color; + border-color: var(--color-border-danger); } label { @@ -108,7 +108,7 @@ textarea { input[type='text'], input[type='password'] { width: 100%; - border: 1px solid $input-border-color; + border: 1px solid var(--color-border-default); outline: none; box-shadow: none; background-image: none; @@ -118,14 +118,14 @@ textarea { border-radius: $border-radius; &:read-only { - color: #777; + color: var(--color-text-secondary); } &:disabled { - border: 1px solid $basic-alpha-8; - color: $text-icon-light-grey; + border: 1px solid var(--color-border-disabled); + color: var(--color-text-disabled); & + .input-icon-right { path { - fill: $text-icon-light-grey; + fill: var(--color-text-disabled); opacity: $btn-disabled-opacity; } } @@ -147,9 +147,9 @@ textarea { background-image: none; -webkit-appearance: none; background-image: none; - color: rgba(0, 0, 0, 0.870588); + color: var(--color-text-default); height: 100%; - background-color: $input-bg; + background-color: var(--color-surface-default); &::-webkit-input-placeholder { font-family: $font-family; font-weight: 400; @@ -159,7 +159,7 @@ textarea { hr { border-bottom-width: 1px; border-style: none none solid; - border-color: $input-border-color; + border-color: var(--color-border-default); bottom: 8px; box-sizing: content-box; margin: 0px; @@ -167,14 +167,14 @@ textarea { width: 100%; &.highlight { border-bottom-width: 2px; - border-color: $input-border-highlight-color; + border-color: var(--color-border-action); transform: scaleX(0); transition: all 450ms cubic-bezier(0.23, 1, 0.32, 1) 0ms; } } &.error hr { - border-color: $alert-danger-border-color; + border-color: var(--color-border-danger); } &.error, @@ -191,22 +191,22 @@ textarea { input:-webkit-autofill, textarea:-webkit-autofill, select:-webkit-autofill { - background-color: $input-bg-dark; - color: $body-color-dark; + background-color: var(--color-surface-default); + color: var(--color-text-default); } } .dark textarea { - background-color: $input-bg-dark; - color: $body-color-dark; - border-color: $input-bg-dark; + background-color: var(--color-surface-default); + color: var(--color-text-default); + border-color: var(--color-surface-default); &::placeholder { - color: $input-placeholder-color-dark !important; + color: var(--color-text-tertiary) !important; } &:hover { - border-color: $white-alpha-8; + border-color: var(--color-border-strong); } &:focus { - border-color: $primary; + border-color: var(--color-border-action); } } @@ -247,7 +247,7 @@ textarea { } } .react-datepicker-time__input input { - border: 1px solid $input-border-color; + border: 1px solid var(--color-border-default); padding: 2px; border-radius: $border-radius; color: white; @@ -259,7 +259,7 @@ textarea { } .dark { .react-datepicker-time__input input { - border: 1px solid $input-border-color; + border: 1px solid var(--color-border-default); padding: 2px; border-radius: $border-radius; color: white; diff --git a/frontend/web/styles/project/_forms.scss b/frontend/web/styles/project/_forms.scss index a304f2d521d1..c619243d7577 100644 --- a/frontend/web/styles/project/_forms.scss +++ b/frontend/web/styles/project/_forms.scss @@ -27,24 +27,27 @@ .dark { .input-container { input.input { - border: 1px solid $input-hover-border-color-dark; - background-color: $input-bg-dark; - color: $text-icon-light; + border: 1px solid var(--color-border-default); + background-color: var(--color-surface-default); + color: var(--color-text-default); + // Dark hover/focus stay action-coloured (was $primary) — the light + // rule uses --color-border-strong on hover, so this override keeps + // the existing dark behaviour rather than inheriting it. &:hover { - border: 1px solid $input-focus-border-color-dark; + border: 1px solid var(--color-border-action); } &:focus { - border: 1px solid $input-focus-border-color-dark; + border: 1px solid var(--color-border-action); } &::placeholder { - color: $input-placeholder-color-dark; + color: var(--color-text-tertiary); } &:disabled { - border: 1px solid $black-alpha-32; - color: $text-icon-light-grey; + border: 1px solid var(--color-border-disabled); + color: var(--color-text-disabled); & + .input-icon-right { path { - fill: $text-icon-light-grey; + fill: var(--color-text-disabled); opacity: $btn-disabled-opacity; } } @@ -60,27 +63,27 @@ .react-datepicker-wrapper .react-datepicker__input-container { &.invalid { input.input { - border-color: $danger !important; + border-color: var(--color-border-danger) !important; } } &.success { input.input { - border-color: $success !important; + border-color: var(--color-border-success) !important; } } input.input { - border: 1px solid $input-border-color; - background-color: $input-bg; + border: 1px solid var(--color-border-default); + background-color: var(--color-surface-default); @include transition(all 200ms); &:hover { - border: 1px solid $basic-alpha-48; + border: 1px solid var(--color-border-strong); } &:focus { - border: 1px solid $primary; + border: 1px solid var(--color-border-action); } border-radius: $border-radius; - color: $body-color; + color: var(--color-text-default); height: $input-height; line-height: $line-height-lg; padding: 12px 12px 12px 16px; @@ -89,7 +92,7 @@ } &::placeholder { - color: $input-placeholder-color; + color: var(--color-text-tertiary); font-weight: normal; }