From 75a5de5fb8f28c2c377178940f140a816075bb82 Mon Sep 17 00:00:00 2001 From: Bartosz Leper Date: Thu, 3 Oct 2024 16:17:31 +0200 Subject: [PATCH] Revert "Implement new inputs and select components (#46812)" This reverts commit 4543d0f8bec4588ef2f85f25a4c370f4db9feded. --- web/packages/design/src/Checkbox/Checkbox.tsx | 2 +- web/packages/design/src/Icon/Icon.tsx | 20 +- .../src/Input/Input.story.jsx} | 29 +- web/packages/design/src/Input/Input.story.tsx | 91 ------ .../Input/{Input.test.tsx => Input.test.js} | 17 +- web/packages/design/src/Input/Input.tsx | 274 ++--------------- .../design/src/Input/{index.ts => index.js} | 1 - .../design/src/LabelInput/LabelInput.tsx | 1 + .../design/src/ThemeProvider/globals.js | 2 +- web/packages/design/src/theme/typography.ts | 6 +- .../NewRequest/ResourceList/Apps.tsx | 70 +++-- .../FieldInput/FieldInput.story.tsx | 31 +- .../components/FieldInput/FieldInput.test.tsx | 14 +- .../components/FieldInput/FieldInput.tsx | 184 +++--------- .../FieldSelect/FieldSelect.story.tsx | 74 +---- .../components/FieldSelect/FieldSelect.tsx | 175 +++++++++-- .../FieldSelect/FieldSelectCreatable.tsx | 252 +++++++++++++--- .../shared/components/FieldSelect/index.ts | 8 +- .../shared/components/FieldSelect/shared.tsx | 256 ---------------- .../FileTransferStateless/CommonElements.tsx | 10 +- .../DownloadForm/DownloadForm.tsx | 13 +- .../components/FormPassword/FormPassword.tsx | 2 +- .../shared/components/Select/Select.story.tsx | 178 ++--------- .../shared/components/Select/Select.tsx | 283 +++++------------- .../shared/components/Select/index.ts | 4 +- .../shared/components/Select/types.ts | 5 +- .../ChangePasswordWizard.test.tsx | 77 ++--- .../ChangePasswordWizard.tsx | 2 +- .../wizards/AddAuthDeviceWizard.tsx | 2 +- .../src/Apps/AddApp/Automatically.tsx | 2 +- .../Add/GitHubActions/ConnectGitHub.test.tsx | 2 +- .../Bots/Add/GitHubActions/ConnectGitHub.tsx | 34 ++- .../ClusterSelector/ClusterSelector.tsx | 2 +- .../Kubernetes/HelmChart/HelmChart.tsx | 2 +- .../TestConnection/TestConnection.tsx | 2 +- .../Shared/CustomInputFieldForAsterisks.tsx | 2 +- .../Shared/LabelsCreater/LabelsCreater.tsx | 45 +-- .../SelectCreatable/SelectCreatable.tsx | 2 - .../EditAwsOidcIntegrationDialog.tsx | 2 +- .../src/JoinTokens/JoinTokenForms.tsx | 2 +- .../EventRangePicker/EventRangePicker.tsx | 11 +- .../src/components/FormLogin/FormLogin.tsx | 43 ++- .../components/LabelsInput/LabelsInput.tsx | 47 +-- .../ReAuthenticate/ReAuthenticate.tsx | 2 +- .../FormLogin/FormLocal/FormLocal.tsx | 7 +- .../modals/ReAuthenticate/ReAuthenticate.tsx | 3 +- .../src/ui/components/FieldInputs.tsx | 10 +- 47 files changed, 762 insertions(+), 1541 deletions(-) rename web/packages/{shared/@types/react-select.d.ts => design/src/Input/Input.story.jsx} (58%) delete mode 100644 web/packages/design/src/Input/Input.story.tsx rename web/packages/design/src/Input/{Input.test.tsx => Input.test.js} (62%) rename web/packages/design/src/Input/{index.ts => index.js} (91%) diff --git a/web/packages/design/src/Checkbox/Checkbox.tsx b/web/packages/design/src/Checkbox/Checkbox.tsx index 9444e4f96922..39b00d42ba09 100644 --- a/web/packages/design/src/Checkbox/Checkbox.tsx +++ b/web/packages/design/src/Checkbox/Checkbox.tsx @@ -34,7 +34,7 @@ interface CheckboxInputProps { disabled?: boolean; id?: string; name?: string; - readOnly?: boolean; + readonly?: boolean; role?: string; type?: 'checkbox' | 'radio'; value?: string; diff --git a/web/packages/design/src/Icon/Icon.tsx b/web/packages/design/src/Icon/Icon.tsx index 84c49bc8d3ce..fea9c16ef24b 100644 --- a/web/packages/design/src/Icon/Icon.tsx +++ b/web/packages/design/src/Icon/Icon.tsx @@ -19,7 +19,7 @@ import React, { PropsWithChildren } from 'react'; import styled from 'styled-components'; -import { space, color, borderRadius, SpaceProps } from 'design/system'; +import { space, color, borderRadius } from 'design/system'; export function Icon({ size = 'medium', @@ -65,14 +65,28 @@ const StyledIcon = styled.span` export type IconSize = 'small' | 'medium' | 'large' | 'extraLarge' | number; -export type IconProps = SpaceProps & { +export type IconProps = { size?: IconSize; color?: string; title?: string; + m?: number | string; + mr?: number | string; + ml?: number | string; + mb?: number | string; + mt?: number | string; + my?: number | string; + mx?: number | string; + p?: number | string; + pr?: number | string; + pl?: number | string; + pb?: number | string; + pt?: number | string; + py?: number | string; + px?: number | string; role?: string; style?: React.CSSProperties; borderRadius?: number; - onClick?: React.MouseEventHandler; + onClick?: () => void; disabled?: boolean; as?: any; to?: string; diff --git a/web/packages/shared/@types/react-select.d.ts b/web/packages/design/src/Input/Input.story.jsx similarity index 58% rename from web/packages/shared/@types/react-select.d.ts rename to web/packages/design/src/Input/Input.story.jsx index 79b65eb4457f..cb799697b5d3 100644 --- a/web/packages/shared/@types/react-select.d.ts +++ b/web/packages/design/src/Input/Input.story.jsx @@ -1,6 +1,6 @@ -/** +/* * Teleport - * Copyright (C) 2024 Gravitational, Inc. + * Copyright (C) 2023 Gravitational, Inc. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by @@ -16,16 +16,19 @@ * along with this program. If not, see . */ -// Declare custom props via module augmentation, per -// https://react-select.com/typescript#custom-select-props +import React from 'react'; -import type {} from 'react-select/base'; -// This import is necessary for module augmentation. -// It allows us to extend the 'Props' interface in the 'react-select/base' module -// and add our custom property 'myCustomProp' to it. +import Input from '.'; -declare module 'react-select/base' { - export interface Props { - customProps?: Record; - } -} +export default { + title: 'Design/Inputs', +}; + +export const Inputs = () => ( + <> + + + + + +); diff --git a/web/packages/design/src/Input/Input.story.tsx b/web/packages/design/src/Input/Input.story.tsx deleted file mode 100644 index 3d214c6f4677..000000000000 --- a/web/packages/design/src/Input/Input.story.tsx +++ /dev/null @@ -1,91 +0,0 @@ -/* - * Teleport - * Copyright (C) 2023 Gravitational, Inc. - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -import React from 'react'; - -import * as Icon from 'design/Icon'; -import { H2 } from 'design/Text'; -import Flex from 'design/Flex'; -import Box from 'design/Box'; - -import Input from '.'; - -export default { - title: 'Design/Inputs', -}; - -export const Inputs = () => ( - <> - - - - - - - - -

Sizes

- - - - - - - - - - - - - - - - - - - - - - - -); diff --git a/web/packages/design/src/Input/Input.test.tsx b/web/packages/design/src/Input/Input.test.js similarity index 62% rename from web/packages/design/src/Input/Input.test.tsx rename to web/packages/design/src/Input/Input.test.js index 5a7b4c19c990..a07cec89a13a 100644 --- a/web/packages/design/src/Input/Input.test.tsx +++ b/web/packages/design/src/Input/Input.test.js @@ -18,24 +18,15 @@ import React from 'react'; -import { render, screen, theme } from 'design/utils/testing'; +import { render, theme } from 'design/utils/testing'; import Input from './Input'; describe('design/Input', () => { - it('forwards a ref', () => { - const ref = jest.fn(); - render(); - expect(ref).toHaveBeenCalledWith(expect.objectContaining({ value: 'foo' })); - }); it('respects hasError prop', () => { - render(); - expect(screen.getByRole('textbox')).toHaveStyle({ - 'border-color': theme.colors.interactive.solid.danger.default.background, + let { container } = render(); + expect(container.firstChild).toHaveStyle({ + 'border-color': theme.colors.error.main, }); - expect(screen.getByRole('graphics-symbol')).toHaveAttribute( - 'aria-label', - 'Error' - ); }); }); diff --git a/web/packages/design/src/Input/Input.tsx b/web/packages/design/src/Input/Input.tsx index cff92abac571..c92564a4f2e2 100644 --- a/web/packages/design/src/Input/Input.tsx +++ b/web/packages/design/src/Input/Input.tsx @@ -16,7 +16,8 @@ * along with this program. If not, see . */ -import styled, { css, useTheme } from 'styled-components'; +import styled from 'styled-components'; +import PropTypes from 'prop-types'; import { space, width, @@ -28,257 +29,43 @@ import { HeightProps, } from 'styled-system'; -import React, { - forwardRef, - HTMLAttributes, - HTMLInputAutoCompleteAttribute, -} from 'react'; - -import { Theme } from 'design/theme/themes/types'; -import * as Icon from 'design/Icon'; -import { IconProps } from 'design/Icon/Icon'; -import Box from 'design/Box'; - -export type InputSize = 'large' | 'medium' | 'small'; -export type InputType = - | 'email' - | 'text' - | 'password' - | 'number' - | 'date' - | 'week'; -export type InputMode = HTMLAttributes<'input'>['inputMode']; - -interface InputProps extends ColorProps, SpaceProps, WidthProps, HeightProps { - size?: InputSize; - hasError?: boolean; - icon?: React.ComponentType; - - // Input properties - autoFocus?: boolean; - disabled?: boolean; - id?: string; - name?: string; - readOnly?: boolean; - role?: string; - type?: InputType; - value?: string; - defaultValue?: string; - placeholder?: string; - min?: number; - max?: number; - autoComplete?: HTMLInputAutoCompleteAttribute; - inputMode?: InputMode; - spellCheck?: boolean; - style?: React.CSSProperties; - - 'aria-invalid'?: HTMLAttributes<'input'>['aria-invalid']; - 'aria-describedby'?: HTMLAttributes<'input'>['aria-describedby']; - - onChange?: React.ChangeEventHandler; - onKeyPress?: React.KeyboardEventHandler; - onKeyDown?: React.KeyboardEventHandler; - onFocus?: React.FocusEventHandler; - onBlur?: React.FocusEventHandler; - onClick?: React.MouseEventHandler; -} - -export const inputGeometry: { - [s in InputSize]: { - height: number; - iconSize: number; - horizontalGap: number; - typography: keyof Theme['typography']; - }; -} = { - large: { - height: 48, - iconSize: 20, - horizontalGap: 12, - typography: 'body1', - }, - medium: { - height: 40, - iconSize: 18, - horizontalGap: 8, - typography: 'body2', - }, - small: { - height: 32, - iconSize: 16, - horizontalGap: 8, - typography: 'body3', - }, -}; - -const borderSize = 1; -const baseHorizontalPadding = 16; -const errorIconHorizontalPadding = 8; - -function error({ hasError, theme }: { hasError?: boolean; theme: Theme }) { +function error({ hasError, theme }) { if (!hasError) { return; } return { - borderColor: theme.colors.interactive.solid.danger.default.background, - '&:hover': { - borderColor: theme.colors.interactive.solid.danger.default.background, + border: `2px solid ${theme.colors.error.main}`, + '&:hover, &:focus': { + border: `2px solid ${theme.colors.error.main}`, }, + padding: '10px 14px', }; } -function padding({ - hasError, - hasIcon, - inputSize, -}: { +interface InputProps extends ColorProps, SpaceProps, WidthProps, HeightProps { hasError?: boolean; - hasIcon: boolean; - inputSize: InputSize; -}) { - const { iconSize, horizontalGap } = inputGeometry[inputSize]; - const paddingRight = hasError - ? errorIconHorizontalPadding + horizontalGap + iconSize - : baseHorizontalPadding; - const paddingLeft = hasIcon - ? baseHorizontalPadding + horizontalGap + iconSize - : baseHorizontalPadding; - return css` - padding: 0 ${paddingRight}px 0 ${paddingLeft}px; - `; } -const Input = forwardRef((props, ref) => { - const { - size = 'medium', - hasError, - icon: IconComponent, - - autoFocus, - disabled, - id, - name, - readOnly, - role, - type, - value, - defaultValue, - placeholder, - min, - max, - autoComplete, - inputMode, - spellCheck, - style, - - 'aria-invalid': ariaInvalid, - 'aria-describedby': ariaDescribedBy, - - onChange, - onKeyPress, - onKeyDown, - onFocus, - onBlur, - onClick, - ...wrapperProps - } = props; - const theme = useTheme(); - const { iconSize } = inputGeometry[size]; - return ( - - {IconComponent && ( - - - - )} - - {hasError && ( - - )} - - ); -}); - -const InputWrapper = styled(Box)<{ inputSize: InputSize }>` - position: relative; - height: ${props => inputGeometry[props.inputSize].height}px; -`; - -const IconWrapper = styled.div` - position: absolute; - left: ${borderSize + baseHorizontalPadding}px; - top: 0; - bottom: 0; - display: flex; // For vertical centering. -`; - -const StyledInput = styled.input<{ hasIcon: boolean; inputSize: InputSize }>` +const Input = styled.input` appearance: none; - border: ${borderSize}px solid; - border-color: ${props => - props.theme.colors.interactive.tonal.neutral[2].background}; + border: 1px solid ${props => props.theme.colors.text.muted}; border-radius: 4px; box-sizing: border-box; display: block; - height: 100%; + height: 40px; + font-size: 16px; + font-weight: 300; + padding: 0 16px; outline: none; width: 100%; - background-color: transparent; + background: ${props => props.theme.colors.levels.surface}; color: ${props => props.theme.colors.text.main}; - ${props => props.theme.typography[inputGeometry[props.inputSize].typography]} - ${padding} - - &:hover { - border: 1px solid ${props => props.theme.colors.text.muted}; - } - - &:focus-visible { - border-color: ${props => - props.theme.colors.interactive.solid.primary.default.background}; + &:hover, + &:focus, + &:active { + border: 1px solid ${props => props.theme.colors.text.slightlyMuted}; } &::-ms-clear { @@ -290,20 +77,13 @@ const StyledInput = styled.input<{ hasIcon: boolean; inputSize: InputSize }>` opacity: 1; } - &:disabled::placeholder { - color: ${props => props.theme.colors.text.disabled}; - opacity: 1; - } - &:read-only { cursor: not-allowed; } &:disabled { - background-color: ${props => - props.theme.colors.interactive.tonal.neutral[0].background}; color: ${props => props.theme.colors.text.disabled}; - border-color: transparent; + border-color: ${props => props.theme.colors.text.disabled}; } ${color} @@ -313,13 +93,11 @@ const StyledInput = styled.input<{ hasIcon: boolean; inputSize: InputSize }>` ${error} `; -const ErrorIcon = styled(Icon.WarningCircle)` - position: absolute; - right: ${errorIconHorizontalPadding + borderSize}px; - color: ${props => - props.theme.colors.interactive.solid.danger.default.background}; - top: 0; - bottom: 0; -`; +Input.displayName = 'Input'; + +Input.propTypes = { + placeholder: PropTypes.string, + hasError: PropTypes.bool, +}; export default Input; diff --git a/web/packages/design/src/Input/index.ts b/web/packages/design/src/Input/index.js similarity index 91% rename from web/packages/design/src/Input/index.ts rename to web/packages/design/src/Input/index.js index 19d91f13072f..97c71f0a0d65 100644 --- a/web/packages/design/src/Input/index.ts +++ b/web/packages/design/src/Input/index.js @@ -17,5 +17,4 @@ */ import Input from './Input'; -export { type InputSize, type InputType, type InputMode } from './Input'; export default Input; diff --git a/web/packages/design/src/LabelInput/LabelInput.tsx b/web/packages/design/src/LabelInput/LabelInput.tsx index 1e564882a884..5151f4d4df4f 100644 --- a/web/packages/design/src/LabelInput/LabelInput.tsx +++ b/web/packages/design/src/LabelInput/LabelInput.tsx @@ -31,6 +31,7 @@ const LabelInput = styled.label` ? props.theme.colors.error.main : props.theme.colors.text.main}; display: block; + font-size: ${p => p.theme.fontSizes[1]}px; width: 100%; margin-bottom: ${props => props.theme.space[1]}px; ${props => props.theme.typography.body3} diff --git a/web/packages/design/src/ThemeProvider/globals.js b/web/packages/design/src/ThemeProvider/globals.js index bc62231a3d4a..e2bb9f0fe0a6 100644 --- a/web/packages/design/src/ThemeProvider/globals.js +++ b/web/packages/design/src/ThemeProvider/globals.js @@ -40,7 +40,7 @@ const GlobalStyle = createGlobalStyle` input { accent-color: ${props => props.theme.colors.brand}; - &::placeholder { + ::placeholder { color: ${props => props.theme.colors.text.muted}; } } diff --git a/web/packages/design/src/theme/typography.ts b/web/packages/design/src/theme/typography.ts index adb0452a2ac8..6a882e7c52b1 100644 --- a/web/packages/design/src/theme/typography.ts +++ b/web/packages/design/src/theme/typography.ts @@ -16,8 +16,6 @@ * along with this program. If not, see . */ -import React from 'react'; - const light = 300; const regular = 400; const medium = 500; @@ -116,8 +114,6 @@ const typography = { fontSize: '14px', lineHeight: '20px', }, - - // Declare value type, while the key type is narrowed down automatically. -} satisfies Record; +}; export default typography; diff --git a/web/packages/shared/components/AccessRequests/NewRequest/ResourceList/Apps.tsx b/web/packages/shared/components/AccessRequests/NewRequest/ResourceList/Apps.tsx index 13cce44eb8e4..7bd7cc29a541 100644 --- a/web/packages/shared/components/AccessRequests/NewRequest/ResourceList/Apps.tsx +++ b/web/packages/shared/components/AccessRequests/NewRequest/ResourceList/Apps.tsx @@ -28,6 +28,7 @@ import Select, { Option as BaseOption, CustomSelectComponentProps, } from 'shared/components/Select'; +import { StyledSelect as BaseStyledSelect } from 'shared/components/Select/Select'; import { ToolTipInfo } from 'shared/components/ToolTip'; import { ResourceMap, ResourceKind } from '../resource'; @@ -235,33 +236,33 @@ function ActionCell({ members of user groups. You can alternatively select user groups instead to access this application. - + + ( autoComplete={autoComplete} onChange={onChange} onKeyPress={onKeyPress} - onKeyDown={onKeyDown} - onFocus={onFocus} - onBlur={onBlur} readOnly={readonly} inputMode={inputMode} - spellCheck={spellCheck} defaultValue={defaultValue} disabled={disabled} - icon={icon} - size={size} - aria-invalid={hasError || markAsError} - aria-describedby={helperTextId} /> ); return ( - + {label ? ( - - - {toolTipContent ? ( - <> - - {label} - - - - ) : ( - <>{label} - )} - + + {toolTipContent ? ( + <> + + {labelText} + {labelTip && } + + + + ) : ( + <> + {labelText} + {labelTip && } + + )} {$inputElement} ) : ( $inputElement )} - ); } @@ -137,105 +108,24 @@ const FieldInput = forwardRef( const defaultRule = () => () => ({ valid: true }); -/** - * Renders a line that, depending on situation, shows either a helper text or - * an error message. Since we want the text line to appear dynamically in - * response to input validation, we introduce height animation here and - * constrain the amount of text to a single line. This limitation can be lifted - * after the `calc-size` CSS function gets widely adopted. - */ -export const HelperTextLine = ({ - hasError, - helperTextId, - helperText, - errorMessage, -}: { - hasError: boolean; - /** - * ID of the helper text element, used to connect it to the input control for - * better accessibility. - */ - helperTextId: string; - helperText?: React.ReactNode; - errorMessage?: string; -}) => { - const theme = useTheme(); - return ( - - {!hasError && ( - - {helperText} - - )} - {/* For the live region to work and announce validation errors in screen - readers, it needs to be rendered prior to the validation error itself. - The screen reader will only announce changes to the live region - content, and not its initial value. We therefore use separate regions - for showing helper text and error messages. */} -
- {hasError && ( - - {errorMessage} - - )} -
-
- ); -}; - -const HelperTextContainer = styled.div<{ - expanded?: boolean; -}>` - // Constrain the height to be able to animate it. - height: ${props => - props.expanded - ? `calc(${props.theme.space[1]}px + ${props.theme.typography.body3.lineHeight})` - : '0'}; - opacity: ${props => (props.expanded ? 1 : 0)}; - transition: - height 200ms ease-in, - opacity 200ms ease-in; -`; - -const HelperText = styled(Text)` - white-space: nowrap; -`; +const LabelTip = ({ text }) => ( + {` - ${text}`} +); export default FieldInput; -export type FieldInputProps = BoxProps & { - id?: string; - name?: string; +export type FieldInputProps = { value?: string; label?: string; - helperText?: React.ReactNode; - icon?: React.ComponentType; - size?: InputSize; + labelTip?: string; placeholder?: string; autoFocus?: boolean; - autoComplete?: HTMLInputAutoCompleteAttribute; - type?: InputType; - inputMode?: InputMode; - spellCheck?: boolean; + autoComplete?: 'off' | 'on' | 'one-time-code'; + type?: 'email' | 'text' | 'password' | 'number' | 'date' | 'week'; + inputMode?: 'text' | 'numeric'; rule?: (options: unknown) => () => unknown; - onChange?: React.ChangeEventHandler; - onKeyPress?: React.KeyboardEventHandler; - onKeyDown?: React.KeyboardEventHandler; - onFocus?: React.FocusEventHandler; - onBlur?: React.FocusEventHandler; + onChange: (e: React.ChangeEvent) => void; + onKeyPress?: (e: React.KeyboardEvent) => void; readonly?: boolean; defaultValue?: string; min?: number; @@ -246,4 +136,6 @@ export type FieldInputProps = BoxProps & { // input box as error color before validator // runs (which marks it as error) markAsError?: boolean; + // TS: temporary handles ...styles + [key: string]: any; }; diff --git a/web/packages/shared/components/FieldSelect/FieldSelect.story.tsx b/web/packages/shared/components/FieldSelect/FieldSelect.story.tsx index cecfa4e84dac..6ef4e1b85c12 100644 --- a/web/packages/shared/components/FieldSelect/FieldSelect.story.tsx +++ b/web/packages/shared/components/FieldSelect/FieldSelect.story.tsx @@ -24,29 +24,11 @@ import Validation from 'shared/components/Validation'; import { Option } from 'shared/components/Select'; import { FieldSelect, FieldSelectAsync } from './FieldSelect'; -import { - FieldSelectCreatable, - FieldSelectCreatableAsync, -} from './FieldSelectCreatable'; export default { title: 'Shared/FieldSelect', }; -function noPenguinsAllowed(opt: Option) { - return () => - opt.value !== 'linux' - ? { valid: true } - : { valid: false, message: 'No penguins allowed' }; -} - -function noPenguinsAllowedInArray(opt: Option[]) { - return () => - opt.every(o => o.value !== 'linux') - ? { valid: true } - : { valid: false, message: 'No penguins allowed' }; -} - export function Default() { const [selectedOption, setSelectedOption] = useState
); } + +type FieldProps = BoxProps & { + autoFocus?: boolean; + label?: string; + labelTip?: string; + rule?: (options: OnChangeValue) => () => unknown; +}; + +/** + * Returns an option loader that wraps given function and returns a promise to + * an empty array if the wrapped function returns `undefined`. This wrapper is + * useful for using the `loadingOptions` callback in context where a promise is + * strictly required, while the declaration of the `loadingOptions` attribute + * allows a `void` return type. + */ +export const resolveUndefinedOptions = + >( + loadOptions: AsyncSelectProps['loadOptions'] + ) => + ( + value: string, + callback?: (options: OptionsOrGroups) => void + ) => { + const result = loadOptions(value, callback); + if (!result) { + return Promise.resolve([] as Opt[]); + } + return result; + }; diff --git a/web/packages/shared/components/FieldSelect/FieldSelectCreatable.tsx b/web/packages/shared/components/FieldSelect/FieldSelectCreatable.tsx index 25cebbf4b013..004185f0e6ab 100644 --- a/web/packages/shared/components/FieldSelect/FieldSelectCreatable.tsx +++ b/web/packages/shared/components/FieldSelect/FieldSelectCreatable.tsx @@ -18,11 +18,15 @@ import React from 'react'; +import { Box, Flex, LabelInput } from 'design'; + import { GroupBase, OnChangeValue } from 'react-select'; import { BoxProps } from 'design/Box'; import { useAsync } from 'shared/hooks/useAsync'; +import { ToolTipInfo } from 'shared/components/ToolTip'; +import { useRule } from 'shared/components/Validation'; import { AsyncProps, @@ -32,9 +36,9 @@ import { } from '../Select'; import { SelectCreatableAsync } from '../Select/Select'; -import { FieldProps, FieldSelectWrapper, splitSelectProps } from './shared'; +import { LabelTip, defaultRule } from './shared'; -import { resolveUndefinedOptions } from './shared'; +import { resolveUndefinedOptions } from './FieldSelect'; /** * Returns a styled SelectCreatable with label, input validation rule and error handling. @@ -49,21 +53,97 @@ export function FieldSelectCreatable< Opt = Option, IsMulti extends boolean = false, Group extends GroupBase = GroupBase, ->(props: SelectCreatableProps & FieldProps) { - const { base, wrapper, others } = splitSelectProps< - Opt, - IsMulti, - Group, - typeof props - >(props, {}); - const { formatCreateLabel, ...styles } = others; +>({ + components, + toolTipContent = null, + label, + labelTip, + value, + name, + onChange, + placeholder, + maxMenuHeight, + isClearable, + isMulti, + menuIsOpen, + menuPosition, + inputValue, + onKeyDown, + onInputChange, + onBlur, + options, + formatCreateLabel, + ariaLabel, + rule = defaultRule, + stylesConfig, + isSearchable = false, + autoFocus = false, + isDisabled = false, + elevated = false, + inputId = 'select', + markAsError = false, + customProps, + defaultValue, + ...styles +}: CreatableProps) { + const { valid, message } = useRule(rule(value)); + const hasError = Boolean(!valid); + const labelText = hasError ? message : label; + const $inputElement = ( + + components={components} + inputId={inputId} + name={name} + menuPosition={menuPosition} + hasError={hasError || markAsError} + isSearchable={isSearchable} + isClearable={isClearable} + value={value} + onChange={onChange} + onKeyDown={onKeyDown} + onInputChange={onInputChange} + onBlur={onBlur} + inputValue={inputValue} + maxMenuHeight={maxMenuHeight} + placeholder={placeholder} + isMulti={isMulti} + autoFocus={autoFocus} + isDisabled={isDisabled} + elevated={elevated} + menuIsOpen={menuIsOpen} + stylesConfig={stylesConfig} + options={options} + formatCreateLabel={formatCreateLabel} + aria-label={ariaLabel} + customProps={customProps} + defaultValue={defaultValue} + /> + ); + return ( - - - {...base} - formatCreateLabel={formatCreateLabel} - /> - + + {label ? ( + <> + + {toolTipContent ? ( + + {labelText} + {labelTip && } + + + ) : ( + <> + {labelText} + {labelTip && } + + )} + + {$inputElement} + + ) : ( + $inputElement + )} + ); } @@ -81,40 +161,115 @@ export function FieldSelectCreatableAsync< Opt = Option, IsMulti extends boolean = false, Group extends GroupBase = GroupBase, ->( - props: AsyncProps & CreatableProps -) { - const { base, wrapper, others } = splitSelectProps< - Opt, - IsMulti, - Group, - typeof props - >(props, { - defaultOptions: true, - }); - const { defaultOptions, loadOptions, formatCreateLabel, ...styles } = others; +>({ + components, + toolTipContent = null, + label, + labelTip, + value, + name, + onChange, + placeholder, + maxMenuHeight, + isClearable, + isMulti, + menuIsOpen, + menuPosition, + inputValue, + onKeyDown, + onInputChange, + onBlur, + options, + formatCreateLabel, + ariaLabel, + rule = defaultRule, + stylesConfig, + isSearchable = false, + autoFocus = false, + isDisabled = false, + elevated = false, + inputId = 'select', + markAsError = false, + customProps, + loadOptions, + noOptionsMessage, + defaultOptions, + defaultValue, + ...styles +}: AsyncProps & CreatableProps) { const [attempt, runAttempt] = useAsync(resolveUndefinedOptions(loadOptions)); + const { valid, message } = useRule(rule(value)); + const hasError = Boolean(!valid); + const labelText = hasError ? message : label; + const $inputElement = ( + { + const [options, error] = await runAttempt(input, option); + if (error) { + return []; + } + return options; + }} + noOptionsMessage={obj => { + if (attempt.status === 'error') { + return `Could not load options: ${attempt.error}`; + } + return noOptionsMessage?.(obj) ?? 'No options'; + }} + /> + ); + return ( - - { - const [options, error] = await runAttempt(input, option); - if (error) { - return []; - } - return options; - }} - noOptionsMessage={obj => { - if (attempt.status === 'error') { - return `Could not load options: ${attempt.error}`; - } - return base.noOptionsMessage?.(obj) ?? 'No options'; - }} - /> - + + {label ? ( + <> + + {toolTipContent ? ( + + {labelText} + {labelTip && } + + + ) : ( + <> + {labelText} + {labelTip && } + + )} + + {$inputElement} + + ) : ( + $inputElement + )} + ); } @@ -126,6 +281,7 @@ type CreatableProps< BoxProps & { autoFocus?: boolean; label?: string; + labelTip?: string; toolTipContent?: React.ReactNode; rule?: (options: OnChangeValue) => () => unknown; markAsError?: boolean; diff --git a/web/packages/shared/components/FieldSelect/index.ts b/web/packages/shared/components/FieldSelect/index.ts index 8c03c141d32c..7c8a50a36d62 100644 --- a/web/packages/shared/components/FieldSelect/index.ts +++ b/web/packages/shared/components/FieldSelect/index.ts @@ -18,10 +18,6 @@ import FieldSelect from './FieldSelect'; export default FieldSelect; -export { FieldSelect, FieldSelectAsync } from './FieldSelect'; -export { resolveUndefinedOptions } from './shared'; +export * from './FieldSelect'; -export { - FieldSelectCreatable, - FieldSelectCreatableAsync, -} from './FieldSelectCreatable'; +export { FieldSelectCreatable } from './FieldSelectCreatable'; diff --git a/web/packages/shared/components/FieldSelect/shared.tsx b/web/packages/shared/components/FieldSelect/shared.tsx index a25768f5c2df..2c4535b64461 100644 --- a/web/packages/shared/components/FieldSelect/shared.tsx +++ b/web/packages/shared/components/FieldSelect/shared.tsx @@ -16,23 +16,6 @@ * along with this program. If not, see . */ -import { GroupBase, OnChangeValue, OptionsOrGroups } from 'react-select'; - -import Box, { BoxProps } from 'design/Box'; - -import React, { useId } from 'react'; -import LabelInput from 'design/LabelInput'; - -import Flex from 'design/Flex'; - -import { HelperTextLine } from '../FieldInput/FieldInput'; -import { useRule } from '../Validation'; -import { - AsyncProps as AsyncSelectProps, - Props as SelectProps, -} from '../Select'; -import { ToolTipInfo } from '../ToolTip'; - export const defaultRule = () => () => ({ valid: true }); export const LabelTip = ({ text }) => ( @@ -40,242 +23,3 @@ export const LabelTip = ({ text }) => ( css={{ fontWeight: 'normal', textTransform: 'none' }} >{` - ${text}`} ); - -type FieldSelectWrapperPropsBase = { - label?: string; - toolTipContent?: React.ReactNode; - helperText?: React.ReactNode; - value?: OnChangeValue; - rule?: (options: OnChangeValue) => () => unknown; - inputId?: string; - markAsError?: boolean; -}; - -type FieldSelectWrapperProps< - Opt, - IsMulti extends boolean, -> = FieldSelectWrapperPropsBase & { - children: React.ReactElement< - // Note: I have no idea why `aria-invalid` is mentioned in the types, but - // `aria-describedby` is not. As this attribute actually gets applied, I - // suppose it's a type system bug. - SelectProps & { 'aria-describedby'?: string } - >; -} & BoxProps; - -/** - * This component contains common validation and ID wrangling logic for all the - * select fields. - */ -export const FieldSelectWrapper = ({ - label, - toolTipContent, - helperText, - value, - rule, - inputId, - markAsError, - children, - ...styles -}: FieldSelectWrapperProps) => { - const { valid, message } = useRule((rule ?? defaultRule)(value)); - // We can't generate a random ID only when it's needed; this would break the - // expectation that hooks need to be called exactly the same way on every - // rendering pass. - const randomFieldId = useId(); - const helperId = useId(); - - const id = inputId || randomFieldId; - const hasError = !valid; - - return ( - - {label && ( - - {toolTipContent ? ( - - {label} - - - ) : ( - label - )} - - )} - {React.cloneElement(children, { - inputId: id, - hasError, - 'aria-invalid': markAsError || hasError, - 'aria-describedby': helperId, - })} - - - ); -}; - -/** - * Returns an option loader that wraps given function and returns a promise to - * an empty array if the wrapped function returns `undefined`. This wrapper is - * useful for using the `loadingOptions` callback in context where a promise is - * strictly required, while the declaration of the `loadingOptions` attribute - * allows a `void` return type. - */ -export const resolveUndefinedOptions = - >( - loadOptions: AsyncSelectProps['loadOptions'] - ) => - ( - value: string, - callback?: (options: OptionsOrGroups) => void - ) => { - const result = loadOptions(value, callback); - if (!result) { - return Promise.resolve([] as Opt[]); - } - return result; - }; - -export type FieldProps = BoxProps & { - autoFocus?: boolean; - label?: string; - toolTipContent?: React.ReactNode; - helperText?: React.ReactNode; - rule?: (options: OnChangeValue) => () => unknown; - markAsError?: boolean; - ariaLabel?: string; -}; - -/** - * Select fields have a metric ton of props, all of which need to be properly - * forwarded to the underlying components. The existing interface is a flat list - * of props, which makes this task quite tricky. Therefore, we offload it to this - * separate function for consistency. - */ -export function splitSelectProps< - Opt, - IsMulti extends boolean, - Group extends GroupBase, - Props extends SelectProps & FieldProps, ->( - props: Props, - defaults: Partial -): { - /** Props that should go to the underlying select component. */ - base: SelectProps; - /** Props that should go to `FieldSelectWrapper`. */ - wrapper: FieldSelectWrapperPropsBase; - /** Rest of the props. It's up to the caller to decide what to do with these. */ - others: Omit; -} { - const propsWithDefaults = { ...defaults, ...props }; - const { - ariaLabel, - autoFocus, - components, - customProps, - defaultValue, - elevated, - helperText, - inputId, - inputValue, - isClearable, - isDisabled, - isMulti, - isSearchable, - label, - markAsError, - maxMenuHeight, - menuIsOpen, - menuPosition, - name, - noOptionsMessage, - onBlur, - onChange, - onInputChange, - onKeyDown, - options, - placeholder, - rule, - stylesConfig, - toolTipContent, - value, - ...others - } = propsWithDefaults; - return { - // hasError and inputId are deliberately excluded from the base, since they - // are set by the wrapper component. - base: { - 'aria-label': ariaLabel, - autoFocus, - components, - customProps, - defaultValue, - elevated, - inputValue, - isClearable, - isDisabled, - isMulti, - isSearchable, - maxMenuHeight, - menuIsOpen, - menuPosition, - name, - noOptionsMessage, - onBlur, - onChange, - onInputChange, - onKeyDown, - options, - placeholder, - stylesConfig, - value, - }, - wrapper: { - helperText, - inputId, - label, - markAsError, - rule, - toolTipContent, - value, - }, - others, - }; -} - -type KeysRemovedFromOthers = - | 'ariaLabel' - | 'autoFocus' - | 'components' - | 'customProps' - | 'defaultValue' - | 'elevated' - | 'helperText' - | 'inputId' - | 'inputValue' - | 'isClearable' - | 'isDisabled' - | 'isMulti' - | 'isSearchable' - | 'label' - | 'markAsError' - | 'maxMenuHeight' - | 'menuIsOpen' - | 'menuPosition' - | 'name' - | 'noOptionsMessage' - | 'onBlur' - | 'onChange' - | 'onInputChange' - | 'onKeyDown' - | 'options' - | 'placeholder' - | 'rule' - | 'stylesConfig' - | 'toolTipContent' - | 'value'; diff --git a/web/packages/shared/components/FileTransfer/FileTransferStateless/CommonElements.tsx b/web/packages/shared/components/FileTransfer/FileTransferStateless/CommonElements.tsx index e42788caf499..b3ed7919609d 100644 --- a/web/packages/shared/components/FileTransfer/FileTransferStateless/CommonElements.tsx +++ b/web/packages/shared/components/FileTransfer/FileTransferStateless/CommonElements.tsx @@ -38,9 +38,8 @@ export const PathInput = forwardRef( return ( {({ validator }) => ( - ( ); } ); + +const StyledFieldInput = styled(FieldInput)` + input { + font-size: 14px; + height: 32px; + } +`; diff --git a/web/packages/shared/components/FileTransfer/FileTransferStateless/DownloadForm/DownloadForm.tsx b/web/packages/shared/components/FileTransfer/FileTransferStateless/DownloadForm/DownloadForm.tsx index f0b457dbf444..460c66affdf0 100644 --- a/web/packages/shared/components/FileTransfer/FileTransferStateless/DownloadForm/DownloadForm.tsx +++ b/web/packages/shared/components/FileTransfer/FileTransferStateless/DownloadForm/DownloadForm.tsx @@ -16,8 +16,8 @@ * along with this program. If not, see . */ -import React, { useId, useState } from 'react'; -import { Flex, LabelInput } from 'design'; +import React, { useState } from 'react'; +import { Flex } from 'design'; import { ButtonPrimary } from 'design/Button'; import { Form, PathInput } from '../CommonElements'; @@ -28,7 +28,6 @@ interface DownloadFormProps { export function DownloadForm(props: DownloadFormProps) { const [sourcePath, setSourcePath] = useState('~/'); - const inputId = useId(); const isSourcePathValid = !sourcePath.endsWith('/'); function download(): void { @@ -42,13 +41,9 @@ export function DownloadForm(props: DownloadFormProps) { download(); }} > - {/* Instead of using the built-in label, we supply our own, because it's - the only way to reliably align the download button with the input - control. */} - File Path - + setSourcePath(e.target.value)} value={sourcePath} diff --git a/web/packages/shared/components/FormPassword/FormPassword.tsx b/web/packages/shared/components/FormPassword/FormPassword.tsx index 7f33de0f5c4c..a31a3ef171af 100644 --- a/web/packages/shared/components/FormPassword/FormPassword.tsx +++ b/web/packages/shared/components/FormPassword/FormPassword.tsx @@ -122,7 +122,7 @@ function FormPassword(props: Props) { placeholder="Password" /> {mfaEnabled && ( - + - - -

Multi

- setSelectedMulti(options)} - options={options} - placeholder="Click to select a role" - isMulti={true} - isClearable - /> -
- -

Multi, empty

- setSelectedMulti(options)} - options={options} - placeholder="Click to select a role" - isMulti={true} - isDisabled={true} - /> -
- -

Single

- -
- -

Single, disabled

- -
- -

Error

- setSelectedSingle(option)} - options={options} - placeholder="Click to select a role" - /> - setSelectedSingle(option)} - options={options} - placeholder="Click to select a role" - /> - setSelectedSingle(option)} - options={options} - placeholder="Click to select a role" - /> - setSelectedMulti(options)} + options={options} + placeholder="Click to select a role" + isMulti={true} + /> + setSelectedSingle(option)} + options={options} + placeholder="Click to select a role" + /> + )` +const RefTypeSelect = styled(StyledSelect)` .react-select__control { border-radius: 0 4px 4px 0; - border-left-color: transparent; + border-left: none; } - .react-select__control--is-focused { - border-left-color: ${props => - props.theme.colors.interactive.solid.primary.default.background}; + .react-select__control:hover { + border-left: none; } `; diff --git a/web/packages/teleport/src/Console/DocumentNodes/ClusterSelector/ClusterSelector.tsx b/web/packages/teleport/src/Console/DocumentNodes/ClusterSelector/ClusterSelector.tsx index 6d007c4d23c9..20b3501fb543 100644 --- a/web/packages/teleport/src/Console/DocumentNodes/ClusterSelector/ClusterSelector.tsx +++ b/web/packages/teleport/src/Console/DocumentNodes/ClusterSelector/ClusterSelector.tsx @@ -20,7 +20,7 @@ import React, { useState } from 'react'; import { Box, LabelInput } from 'design'; import { SelectAsync } from 'shared/components/Select'; -import { resolveUndefinedOptions } from 'shared/components/FieldSelect/shared'; +import { resolveUndefinedOptions } from 'shared/components/FieldSelect'; import { useConsoleContext } from 'teleport/Console/consoleContextProvider'; diff --git a/web/packages/teleport/src/Discover/Kubernetes/HelmChart/HelmChart.tsx b/web/packages/teleport/src/Discover/Kubernetes/HelmChart/HelmChart.tsx index 53a94e1b17cb..51b72ea3f6e8 100644 --- a/web/packages/teleport/src/Discover/Kubernetes/HelmChart/HelmChart.tsx +++ b/web/packages/teleport/src/Discover/Kubernetes/HelmChart/HelmChart.tsx @@ -284,7 +284,7 @@ const StepTwo = ({ disabled={disabled} rule={requiredField('Kubernetes Cluster Name is required')} label="Kubernetes Cluster Name" - helperText="Name shown to Teleport users connecting to the cluster" + labelTip="Name shown to Teleport users connecting to the cluster" value={clusterName} placeholder="my-cluster" width="100%" diff --git a/web/packages/teleport/src/Discover/Kubernetes/TestConnection/TestConnection.tsx b/web/packages/teleport/src/Discover/Kubernetes/TestConnection/TestConnection.tsx index 6933383168cd..054133f7a577 100644 --- a/web/packages/teleport/src/Discover/Kubernetes/TestConnection/TestConnection.tsx +++ b/web/packages/teleport/src/Discover/Kubernetes/TestConnection/TestConnection.tsx @@ -154,7 +154,7 @@ export function TestConnection({ onValueChange(e.target.value)} - disabled={disabled} + isDisabled={disabled} placeholder={`custom-${nameKind.replace(' ', '-')}-name`} rule={requiredField( `${capitalizeFirstLetter(nameKind)} name is required` diff --git a/web/packages/teleport/src/Discover/Shared/LabelsCreater/LabelsCreater.tsx b/web/packages/teleport/src/Discover/Shared/LabelsCreater/LabelsCreater.tsx index da86d0701cf0..f156a558cbad 100644 --- a/web/packages/teleport/src/Discover/Shared/LabelsCreater/LabelsCreater.tsx +++ b/web/packages/teleport/src/Discover/Shared/LabelsCreater/LabelsCreater.tsx @@ -24,8 +24,6 @@ import { useValidation, Validator } from 'shared/components/Validation'; import { requiredField } from 'shared/components/Validation/rules'; import { ButtonTextWithAddIcon } from 'shared/components/ButtonTextWithAddIcon'; -import { inputGeometry } from 'design/Input/Input'; - import { ResourceLabel } from 'teleport/services/agents'; export function LabelsCreater({ @@ -101,11 +99,10 @@ export function LabelsCreater({ } return { valid: !notValid, - message: 'required', + message: '', // err msg doesn't matter as it isn't diaplsyed. }; }; - const inputSize = 'medium'; return ( <> {labels.length > 0 && ( @@ -128,9 +125,9 @@ export function LabelsCreater({ {labels.map((label, index) => { return ( - + {!label.isFixed && ( - // Force the trash button container to be the same height as - // an input. We can't just set `alignItems="center"` on the - // parent flex container above, because the field can expand - // when showing a validation error. - removeLabel(index)} + css={` + &:disabled { + opacity: 0.65; + pointer-events: none; + } + `} + disabled={disableBtns} > - removeLabel(index)} - css={` - &:disabled { - opacity: 0.65; - pointer-events: none; - } - `} - disabled={disableBtns} - > - - - + + )} {label.isDupKey && ( diff --git a/web/packages/teleport/src/Discover/Shared/SelectCreatable/SelectCreatable.tsx b/web/packages/teleport/src/Discover/Shared/SelectCreatable/SelectCreatable.tsx index 01991a1afd1d..5dba2dbabbdd 100644 --- a/web/packages/teleport/src/Discover/Shared/SelectCreatable/SelectCreatable.tsx +++ b/web/packages/teleport/src/Discover/Shared/SelectCreatable/SelectCreatable.tsx @@ -99,8 +99,6 @@ export type SelectCreatableProps = { autoFocus?: boolean; }; -// TODO(bl-nero): There's no need for this to be a separate component. Migrate -// it to the shared component. export const SelectCreatable = ({ isMulti = true, isClearable = true, diff --git a/web/packages/teleport/src/Integrations/EditAwsOidcIntegrationDialog.tsx b/web/packages/teleport/src/Integrations/EditAwsOidcIntegrationDialog.tsx index f38592a62cc9..3813f7381ef4 100644 --- a/web/packages/teleport/src/Integrations/EditAwsOidcIntegrationDialog.tsx +++ b/web/packages/teleport/src/Integrations/EditAwsOidcIntegrationDialog.tsx @@ -137,7 +137,7 @@ export function EditAwsOidcIntegrationDialog(props: Props) { {`arn:aws:iam:::role/`} } - disabled={!!scriptUrl} + disabled={scriptUrl} /> {showReadonlyS3Fields && !scriptUrl && ( <> diff --git a/web/packages/teleport/src/JoinTokens/JoinTokenForms.tsx b/web/packages/teleport/src/JoinTokens/JoinTokenForms.tsx index f00394756448..35ede06cea2b 100644 --- a/web/packages/teleport/src/JoinTokens/JoinTokenForms.tsx +++ b/web/packages/teleport/src/JoinTokens/JoinTokenForms.tsx @@ -206,7 +206,7 @@ export const JoinTokenGCPForm = ({ } value={rule.locations} label="Add Locations" - helperText="Allows regions and/or zones." + labelTip="Allows regions and/or zones." /> - + {`${displayDate(from)} - ${displayDate(to)}`} - + {children} ); @@ -112,11 +110,6 @@ const ValueContainer = ({ ); }; -/** Positions the value text on the internal react-select grid. */ -const ValueText = styled(Text)` - grid-area: 1/1/2/3; -`; - type Props = { ml?: string | number; range: State['range']; diff --git a/web/packages/teleport/src/components/FormLogin/FormLogin.tsx b/web/packages/teleport/src/components/FormLogin/FormLogin.tsx index d6b284b0c74e..e9599690391c 100644 --- a/web/packages/teleport/src/components/FormLogin/FormLogin.tsx +++ b/web/packages/teleport/src/components/FormLogin/FormLogin.tsx @@ -39,7 +39,7 @@ import { import { useAttempt, useRefAutoFocus } from 'shared/hooks'; import Validation, { Validator } from 'shared/components/Validation'; import FieldInput from 'shared/components/FieldInput'; -import { FieldSelect } from 'shared/components/FieldSelect'; +import FieldSelect from 'shared/components/FieldSelect'; import { requiredToken, requiredField, @@ -259,16 +259,6 @@ const LocalForm = ({ onRecover(true)} - > - Forgot Password? - - ) - } value={pass} onChange={e => setPass(e.target.value)} type="password" @@ -277,31 +267,32 @@ const LocalForm = ({ mb={0} width="100%" /> + {isRecoveryEnabled && ( + + onRecover(true)} + > + Forgot Password? + + + )} {auth2faType !== 'off' && ( - + onRecover(false)} - > - Lost Two-Factor Device? - - ) - } value={mfaType} options={mfaOptions} onChange={opt => onSetMfaOption(opt as MfaOption, validator)} mr={3} mb={0} isDisabled={isProcessing} + menuIsOpen={true} // Needed to prevent the menu from causing scroll bars to // appear. menuPosition="fixed" @@ -321,6 +312,14 @@ const LocalForm = ({ /> )} + {isRecoveryEnabled && ( + onRecover(false)} + > + Lost Two-Factor Device? + + )} )} () => { // Check for empty length and duplicate key. - // TODO(bl-nero): This function doesn't really check for uniqueness; it - // needs to be fixed. This control should probably be merged with - // `LabelsCreater`, which has this feature working correctly. let notValid = !value || value.length === 0; return { valid: !notValid, - message: 'required', + message: '', // err msg doesn't matter as it isn't diaplsyed. }; }; const width = `${inputWidth}px`; - const inputSize = 'medium'; return ( <> {labels.length > 0 && ( @@ -123,9 +118,9 @@ export function LabelsInput({ {labels.map((label, index) => { return ( - + handleChange(e, index, 'value')} readonly={disableBtns} /> - {/* Force the trash button container to be the same height as an - input. We can't just set `alignItems="center"` on the parent - flex container above, because the field can expand when - showing a validation error. */} - removeLabel(index)} + css={` + &:disabled { + opacity: 0.65; + pointer-events: none; + } + `} + disabled={disableBtns} > - removeLabel(index)} - css={` - &:disabled { - opacity: 0.65; - pointer-events: none; - } - `} - disabled={disableBtns} - > - - - + + ); diff --git a/web/packages/teleport/src/components/ReAuthenticate/ReAuthenticate.tsx b/web/packages/teleport/src/components/ReAuthenticate/ReAuthenticate.tsx index c344262c7fd6..e6d363d94c8c 100644 --- a/web/packages/teleport/src/components/ReAuthenticate/ReAuthenticate.tsx +++ b/web/packages/teleport/src/components/ReAuthenticate/ReAuthenticate.tsx @@ -92,7 +92,7 @@ export function ReAuthenticate({ )} - + {({ validator }) => ( - + {secondFactor !== 'off' && ( - + {mfaType.value === 'otp' && ( ).value as MfaType ) } + menuIsOpen={true} /> )} diff --git a/web/packages/teleterm/src/ui/components/FieldInputs.tsx b/web/packages/teleterm/src/ui/components/FieldInputs.tsx index 10233ff889a2..6c0d933de3c2 100644 --- a/web/packages/teleterm/src/ui/components/FieldInputs.tsx +++ b/web/packages/teleterm/src/ui/components/FieldInputs.tsx @@ -21,9 +21,13 @@ import styled from 'styled-components'; import React, { forwardRef } from 'react'; import { FieldInputProps } from 'shared/components/FieldInput'; -export const ConfigFieldInput = forwardRef( - (props, ref) => -); +export const ConfigFieldInput = styled(FieldInput)` + input { + background: inherit; + font-size: 14px; + height: 34px; + } +`; const ConfigFieldInputWithoutStepper = styled(ConfigFieldInput)` input {