From: Revanshu Paliwal Date: Tue, 27 Jun 2023 10:16:58 +0000 (+0200) Subject: [NO-JIRA] Design system refactor: inputs and buttons X-Git-Tag: 10.2.0.77647~465 X-Git-Url: https://source.dussan.org/?a=commitdiff_plain;h=a2124bcdd58dadee846cb87eaa77f4d9c2cd331a;p=sonarqube.git [NO-JIRA] Design system refactor: inputs and buttons --- diff --git a/server/sonar-web/design-system/src/components/Checkbox.tsx b/server/sonar-web/design-system/src/components/Checkbox.tsx deleted file mode 100644 index 238a52cba6e..00000000000 --- a/server/sonar-web/design-system/src/components/Checkbox.tsx +++ /dev/null @@ -1,178 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2023 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser 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 - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -import styled from '@emotion/styled'; -import React from 'react'; -import tw from 'twin.macro'; -import { themeBorder, themeColor, themeContrast } from '../helpers/theme'; -import { DeferredSpinner } from './DeferredSpinner'; -import { CheckIcon } from './icons/CheckIcon'; -import { CustomIcon } from './icons/Icon'; - -interface Props { - checked: boolean; - children?: React.ReactNode; - className?: string; - disabled?: boolean; - id?: string; - label?: string; - loading?: boolean; - onCheck: (checked: boolean, id?: string) => void; - onClick?: (event: React.MouseEvent) => void; - onFocus?: VoidFunction; - right?: boolean; - thirdState?: boolean; - title?: string; -} - -export function Checkbox({ - checked, - disabled, - children, - className, - id, - label, - loading = false, - onCheck, - onFocus, - onClick, - right, - thirdState = false, - title, -}: Props) { - const handleChange = () => { - if (!disabled) { - onCheck(!checked, id); - } - }; - - return ( - - {right && children} - - - - - - - {!right && children} - - ); -} - -interface CheckIconProps { - checked?: boolean; - thirdState?: boolean; -} - -function CheckboxIcon({ checked, thirdState }: CheckIconProps) { - if (checked && thirdState) { - return ( - - - - ); - } else if (checked) { - return ; - } - return null; -} - -const CheckboxContainer = styled.label<{ disabled?: boolean }>` - color: ${themeContrast('backgroundSecondary')}; - user-select: none; - - ${tw`sw-inline-flex sw-items-center`}; - - &:hover { - ${tw`sw-cursor-pointer`} - } - - &:disabled { - color: ${themeContrast('checkboxDisabled')}; - ${tw`sw-cursor-not-allowed`} - } -`; - -export const StyledCheckbox = styled.span` - border: ${themeBorder('default', 'primary')}; - color: ${themeContrast('primary')}; - - ${tw`sw-w-4 sw-h-4`}; - ${tw`sw-rounded-1/2`}; - ${tw`sw-box-border`} - ${tw`sw-inline-flex sw-items-center sw-justify-center`}; -`; - -export const AccessibleCheckbox = styled.input` - // Following css makes the checkbox accessible and invisible - border: 0; - clip: rect(0 0 0 0); - clip-path: inset(50%); - height: 1px; - overflow: hidden; - padding: 0; - white-space: nowrap; - width: 1px; - appearance: none; - - &:focus, - &:active { - &:not(:disabled) + ${StyledCheckbox} { - outline: ${themeBorder('focus', 'primary')}; - } - } - - &:checked { - & + ${StyledCheckbox} { - background: ${themeColor('primary')}; - } - &:disabled + ${StyledCheckbox} { - background: ${themeColor('checkboxDisabledChecked')}; - } - } - - &:hover { - &:not(:disabled) + ${StyledCheckbox} { - background: ${themeColor('checkboxHover')}; - border: ${themeBorder('default', 'primary')}; - } - - &:checked:not(:disabled) + ${StyledCheckbox} { - background: ${themeColor('checkboxCheckedHover')}; - border: ${themeBorder('default', 'checkboxCheckedHover')}; - } - } - - &:disabled + ${StyledCheckbox} { - background: ${themeColor('checkboxDisabled')}; - color: ${themeColor('checkboxDisabled')}; - border: ${themeBorder('default', 'checkboxDisabledChecked')}; - } -`; diff --git a/server/sonar-web/design-system/src/components/ColorsLegend.tsx b/server/sonar-web/design-system/src/components/ColorsLegend.tsx index c5f6e2f1082..5b5ed6d8555 100644 --- a/server/sonar-web/design-system/src/components/ColorsLegend.tsx +++ b/server/sonar-web/design-system/src/components/ColorsLegend.tsx @@ -20,11 +20,10 @@ import { useTheme } from '@emotion/react'; import styled from '@emotion/styled'; import tw from 'twin.macro'; +import { themeBorder, themeColor, themeContrast } from '../helpers'; import { BubbleColorVal } from '../types/charts'; -import { Checkbox } from './Checkbox'; import Tooltip from './Tooltip'; - -import { themeBorder, themeColor, themeContrast } from '../helpers'; +import { Checkbox } from './input/Checkbox'; export interface ColorFilterOption { ariaLabel?: string; diff --git a/server/sonar-web/design-system/src/components/DatePicker.tsx b/server/sonar-web/design-system/src/components/DatePicker.tsx deleted file mode 100644 index 838fbd04cc0..00000000000 --- a/server/sonar-web/design-system/src/components/DatePicker.tsx +++ /dev/null @@ -1,416 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2023 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser 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 - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -import styled from '@emotion/styled'; -import classNames from 'classnames'; -import { - format, - getYear, - isSameMonth, - isSameYear, - setMonth, - setYear, - startOfMonth, -} from 'date-fns'; -import { range } from 'lodash'; -import * as React from 'react'; -import { - ActiveModifiers, - CaptionProps, - Matcher, - DayPicker as OriginalDayPicker, - useNavigation as useCalendarNavigation, - useDayPicker, -} from 'react-day-picker'; -import tw from 'twin.macro'; -import { PopupPlacement, PopupZLevel, themeBorder, themeColor, themeContrast } from '../helpers'; -import { InputSizeKeys } from '../types/theme'; -import EscKeydownHandler from './EscKeydownHandler'; -import { FocusOutHandler } from './FocusOutHandler'; -import { InputField } from './InputField'; -import { InputSelect } from './InputSelect'; -import { InteractiveIcon } from './InteractiveIcon'; -import { OutsideClickHandler } from './OutsideClickHandler'; -import { CalendarIcon, ChevronLeftIcon, ChevronRightIcon } from './icons'; -import { CloseIcon } from './icons/CloseIcon'; -import { Popup } from './popups'; - -// When no minDate is given, year dropdown will show year options up to PAST_MAX_YEARS in the past -const YEARS_TO_DISPLAY = 10; -const MONTHS_IN_A_YEAR = 12; - -interface Props { - alignRight?: boolean; - ariaNextMonthLabel: string; - ariaPreviousMonthLabel: string; - className?: string; - clearButtonLabel: string; - currentMonth?: Date; - highlightFrom?: Date; - highlightTo?: Date; - id?: string; - inputClassName?: string; - inputRef?: React.Ref; - maxDate?: Date; - minDate?: Date; - name?: string; - onChange: (date: Date | undefined) => void; - placeholder: string; - showClearButton?: boolean; - size?: InputSizeKeys; - value?: Date; - valueFormatter?: (date?: Date) => string; - zLevel?: PopupZLevel; -} - -interface State { - currentMonth: Date; - lastHovered?: Date; - open: boolean; -} - -function formatWeekdayName(date: Date) { - return format(date, 'EEE'); // Short weekday name, e.g. Wed, Thu -} - -export class DatePicker extends React.PureComponent { - constructor(props: Props) { - super(props); - - this.state = { currentMonth: props.value ?? props.currentMonth ?? new Date(), open: false }; - } - - handleResetClick = () => { - this.closeCalendar(); - this.props.onChange(undefined); - }; - - openCalendar = () => { - this.setState({ - currentMonth: this.props.value ?? this.props.currentMonth ?? new Date(), - lastHovered: undefined, - open: true, - }); - }; - - closeCalendar = () => { - this.setState({ open: false }); - }; - - handleDayClick = (day: Date, modifiers: ActiveModifiers) => { - if (!modifiers.disabled) { - this.closeCalendar(); - this.props.onChange(day); - } - }; - - handleDayMouseEnter = (day: Date, modifiers: ActiveModifiers) => { - this.setState({ lastHovered: modifiers.disabled ? undefined : day }); - }; - - render() { - const { - alignRight, - ariaNextMonthLabel, - ariaPreviousMonthLabel, - clearButtonLabel, - highlightFrom, - highlightTo, - inputRef, - minDate, - maxDate = new Date(), - value: selectedDay, - name, - className, - inputClassName, - id, - placeholder, - showClearButton = true, - valueFormatter = (date?: Date) => (date ? format(date, 'MMM d, yyyy') : ''), - size, - zLevel = PopupZLevel.Global, - } = this.props; - const { lastHovered, currentMonth, open } = this.state; - - // Infer start and end dropdown year from min/max dates, if set - const fromYear = minDate ? minDate.getFullYear() : new Date().getFullYear() - YEARS_TO_DISPLAY; - const toYear = maxDate.getFullYear(); - - const selectedDays = selectedDay ? [selectedDay] : []; - let highlighted: Matcher = false; - const lastHoveredOrValue = lastHovered ?? selectedDay; - - if (highlightFrom && lastHoveredOrValue) { - highlighted = { from: highlightFrom, to: lastHoveredOrValue }; - selectedDays.push(highlightFrom); - } - - if (highlightTo && lastHoveredOrValue) { - highlighted = { from: lastHoveredOrValue, to: highlightTo }; - selectedDays.push(highlightTo); - } - - return ( - - - - - { - this.setState({ currentMonth }); - }} - selected={selectedDays} - toYear={toYear} - weekStartsOn={1} - /> - - ) : null - } - placement={alignRight ? PopupPlacement.BottomRight : PopupPlacement.BottomLeft} - zLevel={zLevel} - > - - - - - - {selectedDay !== undefined && showClearButton && ( - - )} - - - - - - ); - } -} - -const StyledCalendarIcon = styled(CalendarIcon)` - ${tw`sw-absolute`}; - ${tw`sw-top-[0.625rem] sw-left-2`}; -`; - -const StyledInteractiveIcon = styled(InteractiveIcon)` - ${tw`sw-absolute`}; - ${tw`sw-top-[0.375rem] sw-right-[0.375rem]`}; -`; - -const StyledInputField = styled(InputField)` - input[type='text']& { - ${tw`sw-pl-8`}; - ${tw`sw-cursor-pointer`}; - - &.is-filled { - ${tw`sw-pr-8`}; - } - } -`; - -const DayPicker = styled(OriginalDayPicker)` - --rdp-cell-size: auto; - /* Ensures the month/year dropdowns do not move on click, but rdp outline is not shown */ - --rdp-outline: 2px solid transparent; - --rdp-outline-selected: 2px solid transparent; - - margin: 0; - - .rdp-head { - color: ${themeContrast('datePicker')}; - } - - .rdp-day { - height: 28px; - width: 33px; - border-radius: 0; - color: ${themeContrast('datePickerDefault')}; - } - - /* Default modifiers */ - - .rdp-day_disabled { - cursor: not-allowed; - background: ${themeColor('datePickerDisabled')}; - color: ${themeContrast('datePickerDisabled')}; - } - - .rdp-day:hover:not(.rdp-day_outside):not(.rdp-day_disabled):not(.rdp-day_selected) { - background: ${themeColor('datePickerHover')}; - color: ${themeContrast('datePickerHover')}; - } - - .rdp-day:focus-visible { - outline: ${themeBorder('focus', 'inputFocus')}; - background: inherit; - z-index: 1; - } - - .rdp-day.rdp-highlighted:not(.rdp-day_selected) { - background: ${themeColor('datePickerRange')}; - color: ${themeContrast('datePickerRange')}; - } - - .rdp-day_selected, - .rdp-day_selected:focus-visible { - background: ${themeColor('datePickerSelected')}; - color: ${themeContrast('datePickerSelected')}; - } -`; - -function getCustomCalendarNavigation({ - ariaNextMonthLabel, - ariaPreviousMonthLabel, -}: { - ariaNextMonthLabel: string; - ariaPreviousMonthLabel: string; -}) { - return function CalendarNavigation(props: CaptionProps) { - const { displayMonth } = props; - const { fromYear, toYear } = useDayPicker(); - const { goToMonth, nextMonth, previousMonth } = useCalendarNavigation(); - - const baseDate = startOfMonth(displayMonth); // reference date - - const months = range(MONTHS_IN_A_YEAR).map((month) => { - const monthValue = setMonth(baseDate, month); - - return { - label: format(monthValue, 'MMM'), - value: monthValue, - }; - }); - - const startYear = fromYear ?? getYear(Date.now()) - YEARS_TO_DISPLAY; - - const years = range(startYear, toYear ? toYear + 1 : undefined).map((year) => { - const yearValue = setYear(baseDate, year); - - return { - label: String(year), - value: yearValue, - }; - }); - - return ( - - ); - }; -} diff --git a/server/sonar-web/design-system/src/components/DateRangePicker.tsx b/server/sonar-web/design-system/src/components/DateRangePicker.tsx deleted file mode 100644 index f44ca09e0ca..00000000000 --- a/server/sonar-web/design-system/src/components/DateRangePicker.tsx +++ /dev/null @@ -1,134 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2023 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser 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 - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ -import classNames from 'classnames'; -import { max, min } from 'date-fns'; -import * as React from 'react'; -import { PopupZLevel } from '../helpers'; -import { DatePicker } from './DatePicker'; -import { LightLabel } from './Text'; - -interface DateRange { - from?: Date; - to?: Date; -} - -interface Props { - alignEndDateCalandarRight?: boolean; - ariaNextMonthLabel: string; - ariaPreviousMonthLabel: string; - className?: string; - clearButtonLabel: string; - fromLabel: string; - maxDate?: Date; - minDate?: Date; - onChange: (date: DateRange) => void; - separatorText?: string; - toLabel: string; - value?: DateRange; - valueFormatter?: (date?: Date) => string; - zLevel?: PopupZLevel; -} - -export class DateRangePicker extends React.PureComponent { - toDateInput?: HTMLInputElement | null; - - get from() { - return this.props.value?.from; - } - - get to() { - return this.props.value?.to; - } - - handleFromChange = (from: Date | undefined) => { - this.props.onChange({ from, to: this.to }); - - // use `setTimeout` to work around the immediate closing of the `toDateInput` - setTimeout(() => { - if (from && !this.to && this.toDateInput) { - this.toDateInput.focus(); - } - }, 0); - }; - - handleToChange = (to: Date | undefined) => { - this.props.onChange({ from: this.from, to }); - }; - - render() { - const { - alignEndDateCalandarRight, - ariaNextMonthLabel, - ariaPreviousMonthLabel, - clearButtonLabel, - fromLabel, - minDate, - maxDate, - separatorText, - toLabel, - valueFormatter, - zLevel, - } = this.props; - - return ( -
- - {separatorText ?? '–'} - { - this.toDateInput = element; - }} - maxDate={maxDate} - minDate={minDate && this.from ? max([minDate, this.from]) : minDate ?? this.from} - onChange={this.handleToChange} - placeholder={toLabel} - size="full" - value={this.to} - valueFormatter={valueFormatter} - zLevel={zLevel} - /> -
- ); - } -} diff --git a/server/sonar-web/design-system/src/components/DiscreetSelect.tsx b/server/sonar-web/design-system/src/components/DiscreetSelect.tsx deleted file mode 100644 index 085caf1b12b..00000000000 --- a/server/sonar-web/design-system/src/components/DiscreetSelect.tsx +++ /dev/null @@ -1,125 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2023 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser 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 - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -import styled from '@emotion/styled'; -import tw from 'twin.macro'; -import { themeBorder, themeColor, themeContrast } from '../helpers/theme'; -import { InputSizeKeys } from '../types/theme'; -import { InputSelect, LabelValueSelectOption } from './InputSelect'; - -interface Props { - className?: string; - components?: Parameters[0]['components']; - customValue?: JSX.Element; - isDisabled?: boolean; - menuIsOpen?: boolean; - onMenuClose?: () => void; - onMenuOpen?: () => void; - options: Array>; - setValue: ({ value }: LabelValueSelectOption) => void; - size?: InputSizeKeys; - value: V; -} - -export function DiscreetSelect({ - className, - customValue, - onMenuOpen, - options, - size = 'small', - setValue, - value, - ...props -}: Props) { - return ( - item.value === value)} - {...props} - /> - ); -} - -const StyledSelect = styled(InputSelect)` - & { - width: inherit !important; - } - - & .react-select__dropdown-indicator { - ${tw`sw-p-0 sw-py-1`}; - } - - & .react-select__value-container { - ${tw`sw-p-0`}; - } - - & .react-select__menu { - margin: 0; - } - - & .react-select__control { - height: auto; - min-height: inherit; - color: ${themeContrast('discreetBackground')}; - background: none; - outline: inherit; - box-shadow: none; - - ${tw`sw-border-none`}; - ${tw`sw-p-0`}; - ${tw`sw-cursor-pointer`}; - ${tw`sw-flex sw-items-center`}; - ${tw`sw-body-sm`}; - ${tw`sw-select-none`}; - - &:hover { - ${tw`sw-border-none`}; - outline: none; - color: ${themeColor('discreetButtonHover')}; - border-color: inherit; - box-shadow: none; - - & .react-select__single-value, - & .react-select__dropdown-indicator, - & .react-select__placeholder { - color: ${themeColor('discreetButtonHover')}; - } - } - - &:focus { - ${tw`sw-rounded-1`}; - color: ${themeColor('discreetButtonHover')}; - background: ${themeColor('discreetBackground')}; - outline: ${themeBorder('focus', 'discreetFocusBorder')}; - border-color: inherit; - box-shadow: none; - } - } - - & .react-select__control--is-focused, - & .react-select__control--menu-is-open { - ${tw`sw-border-none`}; - } -`; diff --git a/server/sonar-web/design-system/src/components/DropdownMenu.tsx b/server/sonar-web/design-system/src/components/DropdownMenu.tsx index 74c41581703..d6cd692bac0 100644 --- a/server/sonar-web/design-system/src/components/DropdownMenu.tsx +++ b/server/sonar-web/design-system/src/components/DropdownMenu.tsx @@ -25,12 +25,12 @@ import tw from 'twin.macro'; import { INPUT_SIZES } from '../helpers/constants'; import { themeBorder, themeColor, themeContrast } from '../helpers/theme'; import { InputSizeKeys, ThemedProps } from '../types/theme'; -import { Checkbox } from './Checkbox'; import { BaseLink, LinkProps } from './Link'; import NavLink from './NavLink'; -import { RadioButton } from './RadioButton'; import Tooltip from './Tooltip'; import { ClipboardBase } from './clipboard'; +import { Checkbox } from './input/Checkbox'; +import { RadioButton } from './input/RadioButton'; interface Props extends React.HtmlHTMLAttributes { children?: React.ReactNode; diff --git a/server/sonar-web/design-system/src/components/FormField.tsx b/server/sonar-web/design-system/src/components/FormField.tsx deleted file mode 100644 index a6bac58d5e8..00000000000 --- a/server/sonar-web/design-system/src/components/FormField.tsx +++ /dev/null @@ -1,74 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2023 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser 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 - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ -import styled from '@emotion/styled'; -import { ReactNode } from 'react'; -import tw from 'twin.macro'; -import { Highlight, Note } from './Text'; -import { RequiredIcon } from './icons'; - -interface Props { - ariaLabel?: string; - children: ReactNode; - className?: string; - description?: string | ReactNode; - help?: ReactNode; - htmlFor?: string; - id?: string; - label: string | ReactNode; - required?: boolean; - title?: string; -} - -export function FormField({ - children, - className, - description, - help, - id, - required, - label, - htmlFor, - title, - ariaLabel, -}: Props) { - return ( - - - - {children} - - {description && {description}} - - ); -} - -const FieldWrapper = styled.div` - ${tw`sw-flex sw-flex-col sw-w-full`} - - &:not(:last-of-type) { - ${tw`sw-mb-6`} - } -`; diff --git a/server/sonar-web/design-system/src/components/InputField.tsx b/server/sonar-web/design-system/src/components/InputField.tsx deleted file mode 100644 index 1dab905fced..00000000000 --- a/server/sonar-web/design-system/src/components/InputField.tsx +++ /dev/null @@ -1,142 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2023 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser 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 - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ -import { css } from '@emotion/react'; -import styled from '@emotion/styled'; -import { forwardRef } from 'react'; -import tw from 'twin.macro'; -import { INPUT_SIZES } from '../helpers/constants'; -import { themeBorder, themeColor, themeContrast } from '../helpers/theme'; -import { InputSizeKeys, ThemedProps } from '../types/theme'; - -interface InputProps extends Omit, 'size'> { - as?: React.ElementType; - className?: string; - isInvalid?: boolean; - isValid?: boolean; - size?: InputSizeKeys; -} - -interface InputTextAreaProps extends React.TextareaHTMLAttributes { - className?: string; - isInvalid?: boolean; - isValid?: boolean; - size?: InputSizeKeys; -} - -export const InputField = forwardRef( - ({ size = 'medium', style, ...props }, ref) => { - return ( - - ); - } -); -InputField.displayName = 'InputField'; - -export const InputTextArea = forwardRef( - ({ size = 'medium', style, ...props }, ref) => { - return ( - - ); - } -); -InputTextArea.displayName = 'InputTextArea'; - -const defaultStyle = (props: ThemedProps) => css` - --border: ${themeBorder('default', 'inputBorder')(props)}; - --focusBorder: ${themeBorder('default', 'inputFocus')(props)}; - --focusOutline: ${themeBorder('focus', 'inputFocus')(props)}; -`; - -const dangerStyle = (props: ThemedProps) => css` - --border: ${themeBorder('default', 'inputDanger')(props)}; - --focusBorder: ${themeBorder('default', 'inputDangerFocus')(props)}; - --focusOutline: ${themeBorder('focus', 'inputDangerFocus')(props)}; -`; - -const successStyle = (props: ThemedProps) => css` - --border: ${themeBorder('default', 'inputSuccess')(props)}; - --focusBorder: ${themeBorder('default', 'inputSuccessFocus')(props)}; - --focusOutline: ${themeBorder('focus', 'inputSuccessFocus')(props)}; -`; - -const getInputVariant = (props: ThemedProps & { isInvalid?: boolean; isValid?: boolean }) => { - const { isValid, isInvalid } = props; - if (isInvalid) { - return dangerStyle; - } else if (isValid) { - return successStyle; - } - return defaultStyle; -}; - -const baseStyle = (props: ThemedProps) => css` - color: ${themeContrast('inputBackground')(props)}; - background: ${themeColor('inputBackground')(props)}; - border: var(--border); - width: var(--inputSize); - transition: border-color 0.2s ease; - - ${tw`sw-body-sm`} - ${tw`sw-box-border`} - ${tw`sw-rounded-2`} - ${tw`sw-px-3 sw-py-2`} - - &::placeholder { - color: ${themeColor('inputPlaceholder')(props)}; - } - - &:hover { - border: var(--focusBorder); - } - - &:active, - &:focus, - &:focus-within, - &:focus-visible { - border: var(--focusBorder); - outline: var(--focusOutline); - } - - &:disabled, - &:disabled:hover { - color: ${themeContrast('inputDisabled')(props)}; - background-color: ${themeColor('inputDisabled')(props)}; - border: ${themeBorder('default', 'inputDisabledBorder')(props)}; - outline: none; - - ${tw`sw-cursor-not-allowed`}; - &::placeholder { - color: ${themeContrast('inputDisabled')(props)}; - } - } -`; - -const StyledInput = styled.input` - input[type='text']& { - ${getInputVariant} - ${baseStyle} - ${tw`sw-h-control`} - } -`; - -const StyledTextArea = styled.textarea` - ${getInputVariant}; - ${baseStyle}; -`; diff --git a/server/sonar-web/design-system/src/components/InputMultiSelect.tsx b/server/sonar-web/design-system/src/components/InputMultiSelect.tsx deleted file mode 100644 index 6a7eca741bf..00000000000 --- a/server/sonar-web/design-system/src/components/InputMultiSelect.tsx +++ /dev/null @@ -1,70 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2023 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser 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 - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ -import styled from '@emotion/styled'; -import classNames from 'classnames'; -import { themeBorder } from '../helpers'; -import { Badge } from './Badge'; -import { LightLabel } from './Text'; -import { ButtonProps, WrapperButton } from './buttons'; -import { ChevronDownIcon } from './icons'; - -interface Props extends Pick { - className?: string; - count?: number; - id?: string; - placeholder: string; - selectedLabel: string; -} - -export function InputMultiSelect(props: Props) { - const { className, count, id, placeholder, selectedLabel } = props; - - return ( - - {count ? selectedLabel : {placeholder}} - -
- {count !== undefined && count > 0 && {count}} - -
-
- ); -} - -const StyledWrapper = styled(WrapperButton)` - border: ${themeBorder('default', 'inputBorder')}; - - &:hover { - border: ${themeBorder('default', 'inputFocus')}; - } - - &:active, - &:focus, - &:focus-within, - &:focus-visible { - border: ${themeBorder('default', 'inputFocus')}; - outline: ${themeBorder('focus', 'inputFocus')}; - } -`; diff --git a/server/sonar-web/design-system/src/components/InputSearch.tsx b/server/sonar-web/design-system/src/components/InputSearch.tsx deleted file mode 100644 index 354fab3b5d2..00000000000 --- a/server/sonar-web/design-system/src/components/InputSearch.tsx +++ /dev/null @@ -1,261 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2023 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser 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 - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -import styled from '@emotion/styled'; -import classNames from 'classnames'; -import { debounce } from 'lodash'; -import React, { PropsWithChildren, useEffect, useMemo, useRef, useState } from 'react'; -import tw, { theme } from 'twin.macro'; -import { DEBOUNCE_DELAY, INPUT_SIZES } from '../helpers/constants'; -import { Key } from '../helpers/keyboard'; -import { themeBorder, themeColor, themeContrast } from '../helpers/theme'; -import { isDefined } from '../helpers/types'; -import { InputSizeKeys } from '../types/theme'; -import { DeferredSpinner, Spinner } from './DeferredSpinner'; -import { InteractiveIcon } from './InteractiveIcon'; -import { CloseIcon } from './icons/CloseIcon'; -import { SearchIcon } from './icons/SearchIcon'; - -interface Props { - autoFocus?: boolean; - className?: string; - clearIconAriaLabel: string; - id?: string; - innerRef?: React.RefCallback; - loading?: boolean; - maxLength?: number; - minLength?: number; - onBlur?: React.FocusEventHandler; - onChange: (value: string) => void; - onFocus?: React.FocusEventHandler; - onKeyDown?: React.KeyboardEventHandler; - onMouseDown?: React.MouseEventHandler; - placeholder?: string; - searchInputAriaLabel?: string; - size?: InputSizeKeys; - tooShortText?: string; - value?: string; -} - -const DEFAULT_MAX_LENGTH = 100; - -export function InputSearch({ - autoFocus, - id, - className, - innerRef, - onBlur, - onChange, - onFocus, - onKeyDown, - onMouseDown, - placeholder, - loading, - minLength, - maxLength = DEFAULT_MAX_LENGTH, - size = 'medium', - value: parentValue, - tooShortText, - searchInputAriaLabel, - clearIconAriaLabel, - children, -}: PropsWithChildren) { - const input = useRef(null); - const [value, setValue] = useState(parentValue ?? ''); - const debouncedOnChange = useMemo(() => debounce(onChange, DEBOUNCE_DELAY), [onChange]); - - const tooShort = isDefined(minLength) && value.length > 0 && value.length < minLength; - const inputClassName = classNames('js-input-search', { - touched: value.length > 0 && (!minLength || minLength > value.length), - 'sw-pr-10': value.length > 0, - }); - - useEffect(() => { - if (parentValue !== undefined) { - setValue(parentValue); - } - }, [parentValue]); - - useEffect(() => { - if (autoFocus && input.current) { - input.current.focus(); - } - }, [autoFocus]); - - const changeValue = (newValue: string) => { - if (newValue.length === 0 || !minLength || minLength <= newValue.length) { - debouncedOnChange(newValue); - } - }; - - const handleInputChange = (event: React.SyntheticEvent) => { - const eventValue = event.currentTarget.value; - setValue(eventValue); - changeValue(eventValue); - }; - - const handleInputKeyDown = (event: React.KeyboardEvent) => { - if (event.key === Key.Escape) { - event.preventDefault(); - handleClearClick(); - } - onKeyDown?.(event); - }; - - const handleClearClick = () => { - onChange(''); - if (parentValue === undefined || parentValue === '') { - setValue(''); - } - input.current?.focus(); - }; - const ref = (node: HTMLInputElement | null) => { - input.current = node; - innerRef?.(node); - }; - - return ( - - - {children ?? ( - - )} - - - - {value && ( - - )} - - {tooShort && tooShortText && isDefined(minLength) && ( - - {tooShortText} - - )} - - - ); -} - -InputSearch.displayName = 'InputSearch'; // so that tests don't see the obfuscated production name - -export const InputSearchWrapper = styled.div` - width: var(--inputSize); - - ${tw`sw-relative sw-inline-block`} - ${tw`sw-whitespace-nowrap`} - ${tw`sw-align-middle`} - ${tw`sw-h-control`} - - ${Spinner} { - top: calc((2.25rem - ${theme('spacing.4')}) / 2); - ${tw`sw-left-3`}; - ${tw`sw-absolute`}; - } -`; - -export const StyledInputWrapper = styled.div` - input { - background: ${themeColor('inputBackground')}; - color: ${themeContrast('inputBackground')}; - border: ${themeBorder('default', 'inputBorder')}; - - ${tw`sw-rounded-2`} - ${tw`sw-box-border`} - ${tw`sw-pl-10`} - ${tw`sw-body-sm`} - ${tw`sw-w-full sw-h-control`} - - &::placeholder { - color: ${themeColor('inputPlaceholder')}; - - ${tw`sw-truncate`} - } - - &:hover { - border: ${themeBorder('default', 'inputFocus')}; - } - - &:focus, - &:active { - border: ${themeBorder('default', 'inputFocus')}; - outline: ${themeBorder('focus', 'inputFocus')}; - } - - &::-webkit-search-decoration, - &::-webkit-search-cancel-button, - &::-webkit-search-results-button, - &::-webkit-search-results-decoration { - ${tw`sw-hidden sw-appearance-none`} - } - } -`; - -const StyledSearchIcon = styled(SearchIcon)` - color: ${themeColor('inputBorder')}; - top: calc((${theme('height.control')} - ${theme('spacing.4')}) / 2); - - ${tw`sw-left-3`} - ${tw`sw-absolute`} - ${tw`sw-z-normal`} -`; - -export const StyledInteractiveIcon = styled(InteractiveIcon)` - ${tw`sw-absolute`} - ${tw`sw-right-2`} -`; - -const StyledNote = styled.span` - color: ${themeColor('inputPlaceholder')}; - top: calc(1px + ${theme('inset.2')}); - - ${tw`sw-absolute`} - ${tw`sw-left-12 sw-right-10`} - ${tw`sw-body-sm`} - ${tw`sw-text-right`} - ${tw`sw-truncate`} - ${tw`sw-pointer-events-none`} -`; diff --git a/server/sonar-web/design-system/src/components/InputSelect.tsx b/server/sonar-web/design-system/src/components/InputSelect.tsx deleted file mode 100644 index 4edc8e58442..00000000000 --- a/server/sonar-web/design-system/src/components/InputSelect.tsx +++ /dev/null @@ -1,198 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2023 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser 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 - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -import { useTheme as themeInfo } from '@emotion/react'; -import classNames from 'classnames'; -import { omit } from 'lodash'; -import { ReactNode } from 'react'; -import ReactSelect, { - GroupBase, - Props as NamedProps, - OptionProps, - StylesConfig, - components, -} from 'react-select'; -import { INPUT_SIZES } from '../helpers'; -import { themeBorder, themeColor, themeContrast } from '../helpers/theme'; -import { InputSizeKeys } from '../types/theme'; -import { SearchHighlighter } from './SearchHighlighter'; -import { ChevronDownIcon } from './icons'; - -export interface LabelValueSelectOption { - Icon?: ReactNode; - label: string; - value: V; -} - -interface StyleExtensionProps { - size?: InputSizeKeys; -} - -export type SelectProps< - V, - Option extends LabelValueSelectOption, - IsMulti extends boolean = false, - Group extends GroupBase