[CB] move radio button to ui-kit #6031 (#3533)

* dbeaver/pro#6031 replaces old radio button with ui-kit one

* lint fix

* Update webapp/packages/core-blocks/src/FormControls/RadioGroup.tsx

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* pr fixes

* adds labelledby for options configType

* adds gap:0 for vertical radio group

* pr fixes

* cleanup

* radio button fixes for dark theme

* nowrap for radio button words

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: mr-anton-t <42037741+mr-anton-t@users.noreply.github.com>
This commit is contained in:
sergeyteleshev
2025-07-01 21:36:37 +08:00
committed by GitHub
co-authored by Copilot mr-anton-t
parent 7c0d666b0a
commit dce7797bba
18 changed files with 77 additions and 213 deletions
@@ -1,13 +1,11 @@
@layer base {
.dbv-kit-radio-group {
display: flex;
gap: calc(var(--dbv-kit-radio-gap) * 4);
&.dbv-kit-radio-group--vertical {
flex-direction: column;
}
&:not(.dbv-kit-radio-group--vertical) .dbv-kit-radio {
margin-inline-end: calc(var(--dbv-kit-radio-gap) * 4);
gap: 0;
}
}
@@ -48,7 +48,7 @@ export function RadioGroup({ className, children, labelledBy, label, ['aria-labe
</div>
)}
<_RadioGroup
aria-labelledby={labelledById}
aria-labelledby={ariaLabel ? undefined : labelledById}
aria-label={ariaLabel}
className={clsx('dbv-kit-radio-group', className, props.vertical && 'dbv-kit-radio-group--vertical')}
>
@@ -33,10 +33,6 @@
composes: theme-switch__thumb from global;
}
.radioRipple {
composes: theme-radio_ripple from global;
}
.switchBody {
display: flex;
align-items: center;
@@ -0,0 +1,37 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2024 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
*/
@layer components {
.theme-dark .radio-field {
--dbv-kit-radio-hover-shadow-color: color-mix(in srgb, var(--theme-on-secondary), black 60%);
}
.radio-field {
display: flex;
align-items: center;
--dbv-kit-radio-active-border: var(--theme-primary);
--dbv-kit-radio-active-foreground: var(--theme-primary);
--dbv-kit-radio-inactive-border: color-mix(in srgb, var(--theme-on-secondary), white 30%);
--dbv-kit-radio-hover-shadow-color: color-mix(in srgb, var(--theme-on-secondary), white 80%);
--dbv-kit-radio-medium-dot-size: 0.3;
--dbv-kit-radio-medium-height: 32px;
--dbv-kit-radio-small-height: 24px;
--dbv-kit-radio-small-gap: calc(var(--dbv-kit-radio-gap) * 0.25);
--dbv-kit-radio-medium-gap: calc(var(--dbv-kit-radio-gap) * 3);
& .dbv-kit-radio__title {
white-space: nowrap;
}
}
}
@@ -1,73 +0,0 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2024 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
*/
.radio {
composes: theme-radio from global;
}
.radioNoRipple {
composes: theme-radio_no-ripple from global;
}
.radioBackground {
composes: theme-radio_background from global;
}
.input {
composes: theme-radio_native-control from global;
}
.radioOuterCircle {
composes: theme-radio_outer-circle from global;
}
.radioInnerCircle {
composes: theme-radio_inner-circle from global;
}
.radioRipple {
composes: theme-radio_ripple from global;
}
.field {
display: inline-flex;
align-items: center;
vertical-align: middle;
}
.label {
cursor: pointer;
.disabled {
cursor: auto;
}
}
.primary {
composes: theme-radio_primary from global;
}
.small {
composes: theme-radio_small from global;
}
.field.menu {
padding: 0;
& .radio {
width: 14px;
height: 14px;
}
& .radioBackground {
width: 14px;
height: 14px;
}
& .radioInnerCircle {
border-width: 7px;
}
}
.disabledRadio {
composes: theme-radio_disabled from global;
}
.disabledInput {
opacity: 0 !important;
}
@@ -1,6 +1,6 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2024 DBeaver Corp and others
* Copyright (C) 2020-2025 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
@@ -10,17 +10,16 @@ import { useCallback, useContext } from 'react';
import { filterLayoutFakeProps, getLayoutProps } from '../Containers/filterLayoutFakeProps.js';
import type { ILayoutSizeProps } from '../Containers/ILayoutSizeProps.js';
import { s } from '../s.js';
import { useS } from '../useS.js';
import { Field } from './Field.js';
import { FormContext } from './FormContext.js';
import style from './Radio.module.css';
import { RadioGroupContext } from './RadioGroupContext.js';
import { clsx, Radio as UiKitRadio } from '@dbeaver/ui-kit';
import type { ControlSize } from '@dbeaver/ui-kit/types/controls';
import './Radio.css';
type BaseProps = Omit<React.InputHTMLAttributes<HTMLInputElement>, 'onChange' | 'value' | 'checked'> &
type BaseProps = Omit<React.InputHTMLAttributes<HTMLInputElement>, 'onChange' | 'value' | 'checked' | 'size'> &
ILayoutSizeProps & {
mod?: Array<'primary' | 'small' | 'menu'>;
ripple?: boolean;
size?: ControlSize;
};
type ControlledProps = BaseProps & {
@@ -50,14 +49,10 @@ export const Radio: RadioType = observer(function Radio({
id: controlledId,
checked: controlledChecked,
onChange,
mod,
ripple = true,
className,
children,
...rest
}: ControlledProps | ObjectProps<any, any>) {
const styles = useS(style);
const layoutProps = getLayoutProps(rest);
rest = filterLayoutFakeProps(rest);
const formContext = useContext(FormContext);
@@ -100,35 +95,17 @@ export const Radio: RadioType = observer(function Radio({
}
return (
<Field {...layoutProps} className={s(styles, { field: true, menu: mod?.includes('menu') }, className)}>
<div
className={s(styles, {
radio: true,
primary: mod?.includes('primary'),
small: mod?.includes('small') || mod?.includes('menu'),
disabledRadio: rest.disabled,
radioNoRipple: !ripple,
})}
<Field {...layoutProps} className={clsx('radio-field', className)}>
<UiKitRadio
id={id}
name={name}
value={value?.toString() ?? ''}
checked={checked}
onChange={handleChange}
{...rest}
>
<input
{...rest}
className={s(styles, { input: true, disabledInput: rest.disabled })}
type="radio"
id={id}
name={name}
value={value ?? ''}
checked={checked}
onChange={handleChange}
/>
<div className={s(styles, { radioBackground: true })}>
<div className={s(styles, { radioOuterCircle: true })} />
<div className={s(styles, { radioInnerCircle: true })} />
</div>
{ripple && <div className={s(styles, { radioRipple: true })} />}
</div>
<label className={s(styles, { label: true, disabled: rest.disabled })} htmlFor={id}>
{children}
</label>
</UiKitRadio>
</Field>
);
});
@@ -1,6 +1,6 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2024 DBeaver Corp and others
* Copyright (C) 2020-2025 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
@@ -9,11 +9,12 @@ import { observer } from 'mobx-react-lite';
import { useCallback, useContext, useMemo, useState } from 'react';
import { FormContext } from './FormContext.js';
import { RadioGroup as UiKitRadioGroup } from '@dbeaver/ui-kit';
import { type IRadioGroupContext, RadioGroupContext } from './RadioGroupContext.js';
type BaseProps = React.PropsWithChildren<{
type BaseProps = React.PropsWithChildren<React.ComponentProps<typeof UiKitRadioGroup>> & {
name: string;
}>;
};
type ControlledProps<T> = BaseProps & {
value?: T;
@@ -41,6 +42,7 @@ export const RadioGroup: RadioGroupType = observer(function RadioGroup({
state,
onChange,
children,
...rest
}: ControlledProps<string | number> | ObjectProps<any, any>) {
const formContext = useContext(FormContext);
const [selfValue, setValue] = useState<string | number>();
@@ -72,8 +74,14 @@ export const RadioGroup: RadioGroupType = observer(function RadioGroup({
value,
onChange: handleChange,
}),
[value, value, handleChange],
[value, handleChange],
);
return <RadioGroupContext.Provider value={context}>{children}</RadioGroupContext.Provider>;
return (
<RadioGroupContext.Provider value={context}>
<UiKitRadioGroup {...rest}>
{children}
</UiKitRadioGroup>
</RadioGroupContext.Provider>
);
});
@@ -19,7 +19,6 @@
@import './theme-ripple';
@import './ripple';
@import './color';
@import './radio';
@import './switch';
@import './placeholder';
@import './form-controls';
@@ -1,67 +0,0 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2024 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
*/
@import '@material/radio/mixins';
@mixin radio-core-styles($query: mdc-feature-all()) {
@include mdc-radio-core-styles;
.theme-radio {
@extend .mdc-radio;
}
.theme-radio_native-control {
@extend .mdc-radio__native-control;
&:disabled {
opacity: 0;
}
}
.theme-radio_background {
@extend .mdc-radio__background;
}
.theme-radio_outer-circle {
@extend .mdc-radio__outer-circle;
}
.theme-radio_inner-circle {
@extend .mdc-radio__inner-circle;
}
.theme-radio_ripple {
@extend .mdc-radio__ripple;
}
.theme-radio--disabled {
@extend .mdc-radio--disabled;
}
.theme-radio_primary {
$mdc-radio-baseline-theme-color: primary;
$mdc-radio-unchecked-color: rgba(mdc-theme-prop-value(on-surface), 0.54);
$mdc-radio-disabled-circle-color: rgba(mdc-theme-prop-value(on-surface), 0.26);
@include mdc-radio-unchecked-stroke-color($mdc-radio-unchecked-color, $query: $query);
@include mdc-radio-checked-stroke-color($mdc-radio-baseline-theme-color, $query: $query);
@include mdc-radio-ink-color($mdc-radio-baseline-theme-color, $query: $query);
@include mdc-radio-disabled-unchecked-stroke-color($mdc-radio-disabled-circle-color, $query: $query);
@include mdc-radio-disabled-checked-stroke-color($mdc-radio-disabled-circle-color, $query: $query);
@include mdc-radio-disabled-ink-color($mdc-radio-disabled-circle-color, $query: $query);
@include mdc-radio-focus-indicator-color($mdc-radio-baseline-theme-color, $query: $query);
}
.theme-radio_small {
@include mdc-radio-density(-3);
}
.theme-radio_no-ripple {
@include mdc-radio-ripple-size(0);
}
}
@@ -17,7 +17,6 @@
@include theme-ripple;
@include layout-grid;
@include form-controls;
@include radio-core-styles;
@include switch-core-styles;
@include link;
@include placeholder;
@@ -17,7 +17,6 @@
@include theme-ripple;
@include layout-grid;
@include form-controls;
@include radio-core-styles;
@include switch-core-styles;
@include link;
@include placeholder;
@@ -13,7 +13,6 @@
@include colors;
@include ripple-core-styles;
@include theme-ripple;
@include radio-core-styles;
@include switch-core-styles;
@include form-controls;
@include link;
@@ -13,7 +13,6 @@
@include colors;
@include ripple-core-styles;
@include theme-ripple;
@include radio-core-styles;
@include switch-core-styles;
@include form-controls;
@include link;
@@ -49,7 +49,7 @@ export const MenuActionElement = observer<IMenuActionElementProps>(function Menu
>
<MenuItemElement
label={actionInfo.label}
icon={<Radio checked={checked} mod={['primary', 'menu']} ripple={false} />}
icon={<Radio checked={checked} size="small" />}
tooltip={actionInfo.tooltip}
loading={loading}
/>
@@ -106,7 +106,7 @@ export const MenuItemRenderer = observer<IMenuItemRendererProps>(function MenuIt
checked={item.checked}
onClick={() => onClick()}
>
<MenuItemElement label={item.label} icon={<Radio checked={item.checked} mod={['primary', 'menu']} ripple={false} />} tooltip={item.tooltip} />
<MenuItemElement label={item.label} icon={<Radio size="small" checked={item.checked} />} tooltip={item.tooltip} />
</MenuItemRadio>
);
}
@@ -84,6 +84,7 @@ const driverConfiguration: IDriverConfiguration[] = [
isVisible: driver => driver.configurationTypes.includes(DriverConfigurationType.Url),
},
];
export const Options: TabContainerPanelComponent<IConnectionFormProps> = observer(function Options({ formState, tabId }) {
const { selected } = useTab(tabId);
const isAdmin = usePermission(EAdminPermission.admin);
@@ -106,6 +107,7 @@ export const Options: TabContainerPanelComponent<IConnectionFormProps> = observe
const connectionInfoAuthPropertiesResource = useResource(Options, ConnectionInfoAuthPropertiesResource, optionsPart.connectionKey, {
active: selected && !!optionsPart.connectionKey,
});
const configurationTypeLabel = translate('connections_connection_configuration');
//@TODO it's here until the profile implementation in the CloudBeaver
const readonly = formState.isDisabled || formState.isReadOnly || connectionInfoAuthResource.data?.authModel === PROFILE_AUTH_MODEL_ID;
@@ -220,17 +222,17 @@ export const Options: TabContainerPanelComponent<IConnectionFormProps> = observe
{translate('connections_connection_driver')}
</Combobox>
{configurationTypes.length > 1 && (
<FormFieldDescription label={translate('connections_connection_configuration')} tiny>
<FormFieldDescription label={configurationTypeLabel} tiny>
<Container gap>
<RadioGroup name="configurationType" state={optionsPart.state}>
<RadioGroup aria-label={configurationTypeLabel} name="configurationType" state={optionsPart.state}>
{configurationTypes.map(conf => (
<Radio
key={conf.value}
id={conf.value}
value={conf.value}
mod={['primary', 'small']}
readOnly={readonly || configurationTypes.length < 2}
disabled={readonly}
small
keepSize
>
{conf.name}
@@ -9,8 +9,5 @@
.container {
display: flex;
flex-direction: column;
}
.radio {
padding: 0;
}
padding: 4px 10px;
}
@@ -59,9 +59,7 @@ export const BooleanValuePresentation: TabContainerPanelComponent<IDataValuePane
return (
<div className={classes['container']}>
<Radio
className={classes['radio']}
id="true_value"
mod={['primary']}
checked={value === true}
disabled={readonly}
onClick={() => editAction.set(firstSelectedCell, true)}
@@ -69,9 +67,7 @@ export const BooleanValuePresentation: TabContainerPanelComponent<IDataValuePane
TRUE
</Radio>
<Radio
className={classes['radio']}
id="false_value"
mod={['primary']}
checked={value === false}
disabled={readonly}
onClick={() => editAction.set(firstSelectedCell, false)}
@@ -80,9 +76,7 @@ export const BooleanValuePresentation: TabContainerPanelComponent<IDataValuePane
</Radio>
{nullable && (
<Radio
className={classes['radio']}
id="null_value"
mod={['primary']}
checked={value === null}
disabled={readonly}
onClick={() => editAction.set(firstSelectedCell, null)}