103 create radio and radio group components (#3313)

* refactor: rename css variable in order to be more descriptive and stick to the naming convention

* refactor: unify controls, add new global control css variables

* feat: add radio component

* docs: add radio stories

* docs: add input docs story

* docs: add simple form story

* refactor: change default radio dot size

* refactor: change class name for radio children wrapper

* refactor: don't reexport inner radio elements

* refactor: simplify RadioGroup interface by removing items props, make label(any) required

The radiogroup must have an accessible name either by a visible label referenced by aria-labelledby or has a label specified with aria-label.
https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Reference/Roles/radiogroup_role

* refactor: use label wrapper for input component to simplify structure

* refactor: add classname for radio group container
This commit is contained in:
Sychev Andrey
2025-03-21 18:52:39 +08:00
committed by GitHub
parent 71f77186c4
commit 584e5bb260
27 changed files with 990 additions and 57 deletions
@@ -9,7 +9,7 @@
--dbv-kit-btn-background: var(--dbv-kit-color-primary-600);
--dbv-kit-btn-background-hover: var(--dbv-kit-color-primary-700);
--dbv-kit-btn-background-active: var(--dbv-kit-color-primary-800);
--dbv-kit-btn-outline-color: var(--dbv-kit-color-outline);
--dbv-kit-btn-outline-color: var(--dbv-kit-control-outline-color);
--dbv-kit-btn-border-width: var(--dbv-kit-control-border-width);
--dbv-kit-btn-border-color: transparent;
@@ -11,13 +11,12 @@
line-height: var(--dbv-kit-checkbox-height);
font-size: var(--dbv-kit-checkbox-font-size);
color: var(--dbv-kit-checkbox-foreground);
outline: var(--dbv-kit-color-outline);
outline: var(--dbv-kit-control-outline-color);
&:hover {
cursor: pointer;
.dbv-kit-checkbox__check {
border-color: var(--dbv-kit-checkbox-border-color-checked);
box-shadow: 0 0 0 2px var(--dbv-kit-checkbox-hover-shadow-color);
}
}
@@ -33,11 +32,12 @@
}
.dbv-kit-checkbox__check {
--check-size: round(calc(var(--dbv-kit-checkbox-height) * var(--dbv-kit-checkbox-check-size)), 2px);
margin-top: calc(var(--dbv-kit-checkbox-height) * (calc(1 - var(--dbv-kit-checkbox-check-size)) / 2));
height: calc(var(--dbv-kit-checkbox-height) * var(--dbv-kit-checkbox-check-size));
line-height: calc(var(--dbv-kit-checkbox-height) * var(--dbv-kit-checkbox-check-size));
width: calc(var(--dbv-kit-checkbox-height) * var(--dbv-kit-checkbox-check-size));
min-width: calc(var(--dbv-kit-checkbox-height) * var(--dbv-kit-checkbox-check-size));
height: var(--check-size);
line-height: var(--check-size);
width: var(--check-size);
min-width: var(--check-size);
border-radius: var(--dbv-kit-checkbox-border-radius);
background-color: var(--dbv-kit-checkbox-background);
border-color: var(--dbv-kit-checkbox-border-color);
@@ -46,7 +46,7 @@
color: var(--dbv-kit-checkbox-icon-color);
outline-offset: var(--dbv-kit-control-outline-offset);
outline-width: var(--dbv-kit-control-outline-width);
outline-color: var(--dbv-kit-color-outline);
outline-color: var(--dbv-kit-control-outline-color);
transition-property: stroke-dashoffset;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
transition-duration: 150ms;
@@ -57,10 +57,6 @@
outline-style: solid;
}
&:hover {
border-color: var(--dbv-kit-checkbox-border-color-checked);
}
&[data-checked='true'],
&[data-indeterminate='true'] {
background-color: var(--dbv-kit-checkbox-background-checked);
@@ -3,22 +3,21 @@
/* Checkbox sizes */
--dbv-kit-checkbox-height: var(--dbv-kit-control-height-medium);
--dbv-kit-checkbox-gap: calc(var(--tw-spacing) * 1.25);
--dbv-kit-checkbox-check-size: 0.75;
--dbv-kit-checkbox-check-size: 0.5625;
/* Checkbox colors */
--dbv-kit-checkbox-foreground: var(--tw-color-black);
--dbv-kit-checkbox-background: transparent;
--dbv-kit-checkbox-background-checked: var(--dbv-kit-color-primary-600);
--dbv-kit-checkbox-background-active: var(--dbv-kit-color-primary-800);
--dbv-kit-checkbox-hover-shadow-color: var(--dbv-kit-color-primary-100);
--dbv-kit-checkbox-hover-shadow-color: var(--dbv-kit-color-primary-200);
--dbv-kit-checkbox-icon-color: var(--tw-color-white);
/* Checkbox border */
--dbv-kit-checkbox-border-width: var(--dbv-kit-control-border-width);
--dbv-kit-checkbox-border-width: calc(var(--dbv-kit-control-border-width) * 2);
--dbv-kit-checkbox-border-radius: var(--dbv-kit-control-border-radius);
--dbv-kit-checkbox-border-style: solid;
--dbv-kit-checkbox-border-color: var(--tw-color-gray-300);
--dbv-kit-checkbox-border-color: var(--dbv-kit-color-control-inactive);
--dbv-kit-checkbox-border-color-checked: var(--dbv-kit-color-primary-600);
/* Checkbox label font */
@@ -2,22 +2,22 @@
:root {
/* Small checkbox */
--dbv-kit-checkbox-small-font-size: calc(var(--dbv-kit-checkbox-font-size) * 0.875);
--dbv-kit-checkbox-small-height: calc(var(--dbv-kit-control-height-small) * 0.875);
--dbv-kit-checkbox-small-height: var(--dbv-kit-control-height-small);
--dbv-kit-checkbox-small-gap: calc(var(--dbv-kit-checkbox-gap) * 0.5);
/* Medium checkbox */
--dbv-kit-checkbox-medium-font-size: var(--dbv-kit-checkbox-font-size);
--dbv-kit-checkbox-medium-height: calc(var(--dbv-kit-control-height-medium) * 0.75);
--dbv-kit-checkbox-medium-height: var(--dbv-kit-control-height-medium);
--dbv-kit-checkbox-medium-gap: var(--dbv-kit-checkbox-gap);
/* Large checkbox */
--dbv-kit-checkbox-large-font-size: calc(var(--dbv-kit-checkbox-font-size) * 1.125);
--dbv-kit-checkbox-large-height: calc(var(--dbv-kit-control-height-large) * 0.75);
--dbv-kit-checkbox-large-height: var(--dbv-kit-control-height-large);
--dbv-kit-checkbox-large-gap: calc(var(--dbv-kit-checkbox-gap) * 1.25);
/* Extra large checkbox */
--dbv-kit-checkbox-xlarge-font-size: calc(var(--dbv-kit-checkbox-font-size) * 1.25);
--dbv-kit-checkbox-xlarge-height: calc(var(--dbv-kit-control-height-xlarge) * 0.75);
--dbv-kit-checkbox-xlarge-height: var(--dbv-kit-control-height-xlarge);
--dbv-kit-checkbox-xlarge-gap: calc(var(--dbv-kit-checkbox-gap) * 1.5);
}
}
@@ -15,21 +15,44 @@
border-width: var(--dbv-kit-input-border-width);
border-style: var(--dbv-kit-input-border-style);
color: var(--dbv-kit-input-foreground);
padding: var(--dbv-kit-input-padding);
padding-inline: var(--dbv-kit-input-padding);
font-size: var(--dbv-kit-input-font-size);
height: var(--dbv-kit-input-height);
&:not(:disabled):hover {
border-color: var(--dbv-kit-color-control-hover);
}
&:disabled {
cursor: not-allowed;
opacity: var(--dbv-kit-control-disabled-opacity);
}
&::placeholder {
color: var(--dbv-kit-input-placeholder-foreground);
}
&:focus-visible,
&[data-focus-visible] {
outline: var(--dbv-kit-control-outline-width) solid var(--dbv-kit-color-outline);
outline: var(--dbv-kit-control-outline-width) solid var(--dbv-kit-control-outline-color);
outline-offset: var(--dbv-kit-control-outline-offset);
}
}
.dbv-kit-input__title {
color: var(--dbv-kit-input-label-foreground);
padding-inline-start: var(--dbv-kit-input-label-padding);
margin-block-end: var(--tw-spacing);
}
.dbv-kit-input__title--required {
&:after {
content: '*';
color: var(--dbv-kit-color-control-required);
margin-inline-start: 0.125rem;
}
}
/* ---------------------------- */
/* Sizes */
@@ -6,18 +6,20 @@
* you may not use this file except in compliance with the License.
*/
import type { ComponentPropsWithRef } from 'react';
import { type ComponentPropsWithRef } from 'react';
import './Input.css';
export interface InputProps extends Omit<ComponentPropsWithRef<'input'>, 'size'> {
size?: 'small' | 'medium' | 'large' | 'xlarge';
label?: React.ReactNode;
}
export const Input: React.FC<InputProps> = function Input({ size, className, ...props }) {
const classNameToApply = `dbv-kit-input dbv-kit-input--${size ?? 'medium'} ${className}`.trim();
const classNameToApply = `dbv-kit-input dbv-kit-input--${size ?? 'medium'} ${className ?? ''}`.trim();
return (
<div className="dbv-kit-input-wrapper">
<label className="dbv-kit-input-wrapper">
{props.label && <div className={`dbv-kit-input__title ${props.required ? 'dbv-kit-input__title--required' : ''}`}>{props.label}</div>}
<input className={classNameToApply} {...props} />
</div>
</label>
);
};
@@ -3,12 +3,16 @@
--dbv-kit-input-radius: var(--dbv-kit-control-border-radius);
--dbv-kit-input-font-size: calc(var(--dbv-kit-font-size-base) * 0.875);
--dbv-kit-input-background: var(--dbv-kit-color-background);
--dbv-kit-input-border-color: var(--tw-color-gray-300);
--dbv-kit-input-border-color: var(--dbv-kit-color-control-inactive);
--dbv-kit-input-border-width: var(--dbv-kit-control-border-width);
--dbv-kit-input-border-style: solid;
--dbv-kit-input-foreground: var(--dbv-kit-color-foreground);
--dbv-kit-input-label-foreground: var(--dbv-kit-color-control-label);
--dbv-kit-input-label-padding: var(--tw-spacing);
--dbv-kit-input-height: var(--dbv-kit-control-height-medium);
--dbv-kit-input-padding: calc(var(--tw-spacing) * 1.1);
--dbv-kit-input-padding: calc(var(--tw-spacing) * 2.5);
--dbv-kit-input-placeholder-foreground: var(--tw-color-gray-500);
--dbv-kit-input-option-active-background: var(--dbv-kit-color-primary-200);
--dbv-kit-input-option-active-border: var(--dbv-kit-color-primary-500);
@@ -0,0 +1,115 @@
@import './_base.css';
@import './_sizes.css';
@layer base {
.dbv-kit-radio {
display: inline-flex;
align-items: start;
gap: var(--dbv-kit-radio-gap);
cursor: pointer;
height: var(--dbv-kit-radio-height);
&:hover .dbv-kit-radio__input:not([aria-disabled='true']) ~ .dbv-kit-radio__control {
background-color: var(--dbv-kit-radio-inactive-hover-background);
box-shadow: 0 0 0 var(--dbv-kit-control-outline-offset) var(--dbv-kit-radio-hover-shadow-color);
}
}
.dbv-kit-radio--small {
--dbv-kit-radio-font-size: var(--dbv-kit-radio-small-font-size);
--dbv-kit-radio-gap: var(--dbv-kit-radio-small-gap);
--dbv-kit-radio-height: var(--dbv-kit-radio-small-height);
--dbv-kit-radio-dot-size: var(--dbv-kit-radio-small-dot-size);
}
.dbv-kit-radio--medium {
--dbv-kit-radio-font-size: var(--dbv-kit-radio-medium-font-size);
--dbv-kit-radio-gap: var(--dbv-kit-radio-medium-gap);
--dbv-kit-radio-height: var(--dbv-kit-radio-medium-height);
--dbv-kit-radio-dot-size: var(--dbv-kit-radio-medium-dot-size);
}
.dbv-kit-radio--large {
--dbv-kit-radio-font-size: var(--dbv-kit-radio-large-font-size);
--dbv-kit-radio-gap: var(--dbv-kit-radio-large-gap);
--dbv-kit-radio-height: var(--dbv-kit-radio-large-height);
--dbv-kit-radio-dot-size: var(--dbv-kit-radio-large-dot-size);
}
.dbv-kit-radio--xlarge {
--dbv-kit-radio-font-size: var(--dbv-kit-radio-xlarge-font-size);
--dbv-kit-radio-gap: var(--dbv-kit-radio-xlarge-gap);
--dbv-kit-radio-height: var(--dbv-kit-radio-xlarge-height);
--dbv-kit-radio-dot-size: var(--dbv-kit-radio-xlarge-dot-size);
}
.dbv-kit-radio__input {
border: 0px;
clip: rect(0px, 0px, 0px, 0px);
height: 1px;
margin: -1px;
overflow: hidden;
padding: 0px;
position: absolute;
white-space: nowrap;
width: 1px;
&[aria-checked='true'] + .dbv-kit-radio__control {
border-color: var(--dbv-kit-radio-active-border);
background-color: var(--dbv-kit-radio-active-background);
&::after {
scale: 1;
}
}
&[data-focus-visible='true'] ~ .dbv-kit-radio__control {
outline: var(--dbv-kit-control-outline-width) solid var(--dbv-kit-control-outline-color);
outline-offset: var(--dbv-kit-control-outline-offset);
box-shadow: 0 0 0 var(--dbv-kit-control-outline-offset) var(--dbv-kit-radio-hover-shadow-color);
}
&[aria-disabled='true'] ~ .dbv-kit-radio__text,
&[aria-disabled='true'] ~ .dbv-kit-radio__control {
opacity: var(--dbv-kit-control-disabled-opacity);
}
}
.dbv-kit-radio__control {
--ring-size: round(calc(var(--dbv-kit-radio-height) * var(--dbv-kit-radio-ring-size)), 2px);
position: relative;
display: inline-flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
width: var(--ring-size);
height: var(--ring-size);
margin-top: calc((var(--dbv-kit-radio-height) - (var(--ring-size))) * 0.5);
border-radius: 50%;
border: 2px solid var(--dbv-kit-radio-inactive-border);
background-color: var(--dbv-kit-radio-inactive-background);
transition:
background-color 0.3s,
border-color 0.3s;
&::after {
content: '';
width: round(calc(var(--dbv-kit-radio-height) * var(--dbv-kit-radio-dot-size)), 2px);
height: round(calc(var(--dbv-kit-radio-height) * var(--dbv-kit-radio-dot-size)), 2px);
border-radius: 50%;
background-color: var(--dbv-kit-radio-active-foreground);
scale: 0;
transition-property: scale;
transition-timing-function: var(--tw-ease-in-out);
transition-duration: 0.2s;
}
}
.dbv-kit-radio__title {
display: inline-block;
vertical-align: middle;
line-height: var(--dbv-kit-radio-height);
font-size: var(--dbv-kit-radio-font-size);
}
}
@@ -0,0 +1,25 @@
/*
* CloudBeaver - Cloud Database Manager
* 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.
*/
import type { ControlSize } from '../types/controls.js';
import { _Radio, type RadioProps as _RadioProps } from './index.js';
import './Radio.css';
export interface RadioProps extends Omit<_RadioProps, 'size'> {
size?: ControlSize;
}
export function Radio({ children, className, size = 'medium', ...props }: RadioProps) {
return (
<label className={`dbv-kit-radio dbv-kit-radio--${size} ${className || ''}`}>
<_Radio className="dbv-kit-radio__input" {...props} />
<div className="dbv-kit-radio__control" />
<span className="dbv-kit-radio__title">{children}</span>
</label>
);
}
@@ -0,0 +1,26 @@
@layer base {
.dbv-kit-radio-group {
display: flex;
&.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);
}
}
.dbv-kit-radio-group__label {
display: block;
color: var(--dbv-kit-select-label-foreground);
padding-inline-start: var(--dbv-kit-select-label-padding-inline);
margin-block-end: var(--tw-spacing);
}
.dbv-kit-radio-group__label--required::after {
content: '*';
color: var(--dbv-kit-color-control-required);
margin-inline-start: 0.125rem;
}
}
@@ -0,0 +1,59 @@
/*
* CloudBeaver - Cloud Database Manager
* 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.
*/
import { useId } from 'react';
import './RadioGroup.css';
import { _RadioGroup, _RadioProvider, type RadioProviderProps } from './index.js';
type LabelProps =
| {
labelledBy: string;
label?: never;
['aria-label']?: never;
}
| {
labelledBy?: never;
label: React.ReactNode;
['aria-label']?: never;
}
| {
labelledBy?: never;
label?: never;
['aria-label']: string;
};
type RadioGroupProps = Pick<RadioProviderProps, 'value' | 'setValue' | 'store' | 'setActiveId' | 'rtl' | 'defaultValue'> &
LabelProps & {
required?: boolean;
className?: string;
vertical?: boolean;
children: React.ReactNode;
};
export function RadioGroup({ className, children, labelledBy, label, ['aria-label']: ariaLabel, ...props }: RadioGroupProps) {
const labelId = useId();
const labelledById = label ? labelId : labelledBy;
return (
<_RadioProvider {...props}>
<div className="dbv-kit-radio-group__container">
{label && (
<div id={labelId} className={`dbv-kit-radio-group__label ${props.required ? 'dbv-kit-radio-group__label--required' : ''}`}>
{label}
</div>
)}
<_RadioGroup
aria-labelledby={labelledById}
aria-label={ariaLabel}
className={`dbv-kit-radio-group ${className ?? ''} ${props.vertical ? 'dbv-kit-radio-group--vertical' : ''}`}
>
{children}
</_RadioGroup>
</div>
</_RadioProvider>
);
}
@@ -0,0 +1,22 @@
@layer base {
:root {
--dbv-kit-radio-inactive-hover-background: transparent;
--dbv-kit-radio-inactive-hover-border: var(--tw-color-gray-400);
--dbv-kit-radio-inactive-background: transparent;
--dbv-kit-radio-inactive-foreground: var(--dbv-kit-color-control-inactive);
--dbv-kit-radio-inactive-border: var(--dbv-kit-color-control-inactive);
--dbv-kit-radio-active-background: transparent;
--dbv-kit-radio-active-border: var(--dbv-kit-color-primary-500);
--dbv-kit-radio-active-foreground: var(--dbv-kit-color-primary-500);
--dbv-kit-radio-hover-shadow-color: var(--dbv-kit-color-primary-200);
--dbv-kit-radio-height: var(--dbv-kit-control-height-medium);
--dbv-kit-radio-ring-size: 0.5625;
--dbv-kit-radio-dot-size: 0.25;
--dbv-kit-radio-font-size: calc(var(--dbv-kit-font-size-base) * 0.875);
--dbv-kit-radio-gap: var(--tw-spacing);
--dbv-kit-select-label-foreground: var(--dbv-kit-color-control-label);
--dbv-kit-select-label-padding-inline: var(--tw-spacing);
}
}
@@ -0,0 +1,27 @@
@layer base {
:root {
/* Small radio */
--dbv-kit-radio-small-gap: calc(var(--dbv-kit-radio-gap) * 0.5);
--dbv-kit-radio-small-font-size: calc(var(--dbv-kit-radio-font-size) * 0.875);
--dbv-kit-radio-small-height: var(--dbv-kit-control-height-small);
--dbv-kit-radio-small-dot-size: 0.25;
/* Medium radio */
--dbv-kit-radio-medium-gap: var(--dbv-kit-radio-gap);
--dbv-kit-radio-medium-font-size: var(--dbv-kit-radio-font-size);
--dbv-kit-radio-medium-height: var(--dbv-kit-control-height-medium);
--dbv-kit-radio-medium-dot-size: 0.25;
/* Large radio */
--dbv-kit-radio-large-gap: calc(var(--dbv-kit-radio-gap) * 1.25);
--dbv-kit-radio-large-font-size: calc(var(--dbv-kit-radio-font-size) * 1.125);
--dbv-kit-radio-large-height: var(--dbv-kit-control-height-large);
--dbv-kit-radio-large-dot-size: 0.25;
/* Extra large radio */
--dbv-kit-radio-xlarge-gap: calc(var(--dbv-kit-radio-gap) * 1.5);
--dbv-kit-radio-xlarge-font-size: calc(var(--dbv-kit-radio-font-size) * 1.25);
--dbv-kit-radio-xlarge-height: var(--dbv-kit-control-height-xlarge);
--dbv-kit-radio-xlarge-dot-size: 0.3;
}
}
@@ -0,0 +1,20 @@
/*
* CloudBeaver - Cloud Database Manager
* 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.
*/
export {
Radio as _Radio,
RadioGroup as _RadioGroup,
RadioProvider as _RadioProvider,
type RadioGroupProps,
type RadioProviderProps,
type RadioProps,
useRadioContext,
useRadioStore,
} from '@ariakit/react';
export { RadioGroup } from './RadioGroup.js';
export { Radio } from './Radio.js';
@@ -21,7 +21,11 @@
font-size: var(--dbv-kit-select-font-size);
outline-width: var(--dbv-kit-control-outline-width);
outline-offset: var(--dbv-kit-control-outline-offset);
outline-color: var(--dbv-kit-color-outline);
outline-color: var(--dbv-kit-control-outline-color);
&:hover {
border-color: var(--dbv-kit-color-control-hover);
}
&[aria-disabled='true'] {
opacity: var(--dbv-kit-control-disabled-opacity);
@@ -30,11 +34,6 @@
&:focus-visible {
outline-style: solid;
}
&:active,
&[data-active] {
padding-top: 0.125rem;
}
}
.dbv-kit-select__label {
@@ -1,3 +1,11 @@
/*
* CloudBeaver - Cloud Database Manager
* 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.
*/
import {
SelectProvider as AriaSelectProvider,
Select as AriaSelect,
@@ -23,27 +31,27 @@ export function SelectProvider({ children, ...props }: SelectProviderProps) {
return <AriaSelectProvider {...props}>{children}</AriaSelectProvider>;
}
export function Select({ className, ref, ...props }: SelectProps) {
return <AriaSelect ref={ref} className={`dbv-kit-select ${className || ''}`} {...props} />;
export function Select({ className, ...props }: SelectProps) {
return <AriaSelect className={`dbv-kit-select ${className || ''}`} {...props} />;
}
export function SelectPopover({ children, ref, className, ...props }: SelectPopoverProps) {
export function SelectPopover({ children, className, ...props }: SelectPopoverProps) {
return (
<AriaSelectPopover ref={ref} className={`dbv-kit-select__popover ${className || ''}`} sameWidth={props.sameWidth ?? true} {...props}>
<AriaSelectPopover className={`dbv-kit-select__popover ${className || ''}`} sameWidth={props.sameWidth ?? true} {...props}>
{children}
</AriaSelectPopover>
);
}
export function SelectItem({ children, ref, className, ...props }: SelectItemProps) {
export function SelectItem({ children, className, ...props }: SelectItemProps) {
return (
<AriaSelectItem ref={ref} className={`dbv-kit-select__item ${className || ''}`} {...props}>
<AriaSelectItem className={`dbv-kit-select__item ${className || ''}`} {...props}>
{children}
</AriaSelectItem>
);
}
export function SelectLabel({ children, ref, className, ...props }: SelectLabelProps) {
export function SelectLabel({ children, className, ...props }: SelectLabelProps) {
return (
<AriaSelectLabel className={`dbv-kit-select__label ${className || ''}`} {...props}>
{children}
@@ -14,7 +14,7 @@
.dbv-kit-select__label--required {
&:after {
content: '*';
color: var(--tw-color-red-600);
color: var(--dbv-kit-color-control-required);
margin-left: 0.125rem;
}
}
@@ -1,3 +1,11 @@
/*
* CloudBeaver - Cloud Database Manager
* 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.
*/
import { useState } from 'react';
import { SelectProvider, Select, SelectPopover, SelectItem, SelectLabel } from './Select.js';
import './SelectField.css';
@@ -40,6 +48,8 @@ export interface SelectFieldProps<T, ItemType = SelectItem<T>> {
description?: React.ReactNode;
name?: string;
disabled?: boolean;
required?: boolean;
@@ -77,6 +87,7 @@ export function SelectField<T, ItemType extends {} = SelectItem<T>>({
className,
selectedRender,
arrowIcon,
name,
}: SelectFieldProps<T, ItemType>) {
const getItemValue = (item: ItemType): T =>
getValueByPath<ItemType, T>(item, itemValue, i => ('value' in i ? (i as unknown as SelectItem<T>).value : (i as unknown as T)));
@@ -112,7 +123,7 @@ export function SelectField<T, ItemType extends {} = SelectItem<T>>({
<SelectProvider value={currentValue as any} setValue={val => handleChange(val as T)}>
{label && <SelectLabel className={labelClassName}>{label}</SelectLabel>}
<Select disabled={disabled}>
<Select name={name} disabled={disabled}>
{displayValue}
{arrowIcon ?? <Select.Arrow />}
</Select>
@@ -7,14 +7,14 @@
--dbv-kit-select-background: var(--dbv-kit-color-background);
--dbv-kit-select-border-width: var(--dbv-kit-control-border-width);
--dbv-kit-select-border-color: var(--tw-color-gray-400);
--dbv-kit-select-border-color: var(--dbv-kit-color-control-inactive);
--dbv-kit-select-border-style: solid;
--dbv-kit-select-border-radius: var(--dbv-kit-control-border-radius);
--dbv-kit-select-font-weight: var(--tw-font-weight-normal);
--dbv-kit-select-font-size: calc(var(--dbv-kit-font-size-base) * 0.875);
--dbv-kit-select-label-foreground: var(--tw-color-gray-600);
--dbv-kit-select-label-foreground: var(--dbv-kit-color-control-label);
--dbv-kit-select-label-padding-inline: var(--tw-spacing);
--dbv-kit-select-popover-background: var(--dbv-kit-color-background);
+5 -1
View File
@@ -9,7 +9,7 @@
--dbv-kit-control-disabled-opacity: 0.5;
/* Outline */
--dbv-kit-color-outline: var(--tw-color-blue-500);
--dbv-kit-control-outline-color: var(--tw-color-blue-500);
--dbv-kit-control-outline-width: 2px;
--dbv-kit-control-outline-offset: 2px;
@@ -23,6 +23,10 @@
/* Colors */
--dbv-kit-color-foreground: var(--tw-color-gray-900);
--dbv-kit-color-background: var(--tw-color-gray-50);
--dbv-kit-color-control-inactive: var(--tw-color-gray-300);
--dbv-kit-color-control-hover: var(--tw-color-gray-400);
--dbv-kit-color-control-label: var(--tw-color-gray-600);
--dbv-kit-color-control-required: var(--tw-color-red-600);
--dbv-kit-color-primary-50: var(--tw-color-blue-50);
--dbv-kit-color-primary-100: var(--tw-color-blue-100);
+2 -1
View File
@@ -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.
@@ -21,3 +21,4 @@ export {
type SelectItemProps,
} from './Select/Select.js';
export { SelectField, type SelectFieldProps, type SelectItem } from './Select/SelectField.js';
export { Radio, RadioGroup, useRadioContext, useRadioStore, type RadioProviderProps, type RadioProps, type RadioGroupProps } from './Radio/index.js';
@@ -0,0 +1,53 @@
/*
* CloudBeaver - Cloud Database Manager
* 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.
*/
import { Button } from '../../Button/Button.js';
import { Checkbox } from '../../Checkbox/Checkbox.js';
import { Input } from '../../Input/Input.js';
import { Radio } from '../../Radio/index.js';
import { RadioGroup } from '../../Radio/RadioGroup.js';
import { SelectField } from '../../Select/SelectField.js';
export const SimpleForm = () => {
return (
<form className="tw:flex tw:flex-col tw:gap-2" onSubmit={e => (e.preventDefault(), console.log('submit'))}>
<Input placeholder="John" name="first-name" label="First Name" />
<Input placeholder="Snow" name="last-name" label="Last Name" />
<SelectField
label="Country"
name="Country"
items={[
{ value: 'USA', label: 'USA' },
{ value: 'Canada', label: 'Canada' },
{ value: 'UK', label: 'UK' },
]}
/>
<RadioGroup vertical label="Marital Status">
<Radio name="status" value={'single'}>
Single
</Radio>
<Radio name="status" value={'married'}>
Married
</Radio>
<Radio name="status" value={'divorced'}>
Divorced
</Radio>
</RadioGroup>
<RadioGroup vertical label="Children">
<Radio value="Yes">Have children</Radio>
<Radio value="No">No</Radio>
</RadioGroup>
<div>
<Checkbox className="tw:flex-row-reverse">Veteran of the console.war</Checkbox>
</div>
<Button type="submit">Submit</Button>
</form>
);
};
@@ -76,7 +76,7 @@ export const States = () => {
};
export const Sizes = () => (
<div className="tw:flex tw:flex-col tw:space-y-4">
<div className="tw:flex tw:flex-col">
<Checkbox size="small"> Small </Checkbox>
<Checkbox size="medium"> Medium </Checkbox>
<Checkbox size="large"> Large </Checkbox>
@@ -136,10 +136,6 @@ export const Tokens = () => {
<code>--dbv-kit-checkbox-background-hover:</code> var(--dbv-kit-color-primary-100);{' '}
<span className="comment"> Checkbox background color on hover</span>
</div>
<div>
<code>--dbv-kit-checkbox-background-active:</code> var(--dbv-kit-color-primary-800);{' '}
<span className="comment"> Checkbox background color when active</span>
</div>
<h3>Borders</h3>
<div>
<code>--dbv-kit-checkbox-border-width:</code> var(--dbv-kit-control-border-width); <span className="comment"> Checkbox border width</span>
@@ -8,7 +8,133 @@
import { Button } from '../../../Button/Button.js';
import { Input } from '../../../Input/Input.js';
export const InputExample = () => {
export const Documentation = () => {
return (
<section>
<h2>Input Component Documentation</h2>
<p>
The Input component is a customizable form control that allows users to enter text. It can be configured with various sizes and states, and
can include labels with optional required indicators.
</p>
<h2>CSS Classes</h2>
<h3>Input Wrapper</h3>
<p>
<code>.dbv-kit-input-wrapper</code> - The container that wraps the input and its label. Controls the layout and positioning.
</p>
<h3>Main Input Element</h3>
<p>
<code>.dbv-kit-input</code> - The main class for the input component. Controls the appearance including size, colors, and borders.
</p>
<h3>Size Variants</h3>
<p>
<code>.dbv-kit-input--small</code> - Smaller size variant of the input.
</p>
<p>
<code>.dbv-kit-input--medium</code> - Default size variant of the input.
</p>
<p>
<code>.dbv-kit-input--large</code> - Larger size variant of the input.
</p>
<p>
<code>.dbv-kit-input--xlarge</code> - Extra large size variant of the input.
</p>
<h3>Label</h3>
<p>
<code>.dbv-kit-input__title</code> - Applied to the label element of the input component.
</p>
<p>
<code>.dbv-kit-input__title--required</code> - Applied to the label when the input is required, adds an asterisk indicator.
</p>
<h2>CSS Variables</h2>
<h3>Base Input Variables</h3>
<p>
<code>--dbv-kit-input-radius</code> - Controls the border radius of the input.
</p>
<p>
<code>--dbv-kit-input-font-size</code> - Sets the default font size of the input text.
</p>
<p>
<code>--dbv-kit-input-background</code> - Sets the background color of the input.
</p>
<p>
<code>--dbv-kit-input-border-color</code> - Defines the default border color of the input.
</p>
<p>
<code>--dbv-kit-input-border-width</code> - Sets the width of the border around the input.
</p>
<p>
<code>--dbv-kit-input-border-style</code> - Defines the style of the border (solid, dashed, etc.).
</p>
<p>
<code>--dbv-kit-input-foreground</code> - Sets the text color of the input.
</p>
<p>
<code>--dbv-kit-input-height</code> - Controls the height of the input field.
</p>
<p>
<code>--dbv-kit-input-padding</code> - Sets the horizontal padding inside the input.
</p>
<p>
<code>--dbv-kit-input-placeholder-foreground</code> - Controls the color of placeholder text.
</p>
<h3>Label Variables</h3>
<p>
<code>--dbv-kit-input-label-foreground</code> - Sets the text color of the label.
</p>
<p>
<code>--dbv-kit-input-label-padding</code> - Controls the padding of the label.
</p>
<h3>Size Variables</h3>
<p>
<code>--dbv-kit-input-small-padding</code> - Controls the padding for small inputs.
</p>
<p>
<code>--dbv-kit-input-small-font-size</code> - Sets the font size for small inputs.
</p>
<p>
<code>--dbv-kit-input-small-height</code> - Controls the height for small inputs.
</p>
<p>
<code>--dbv-kit-input-medium-padding</code> - Controls the padding for medium inputs.
</p>
<p>
<code>--dbv-kit-input-medium-font-size</code> - Sets the font size for medium inputs.
</p>
<p>
<code>--dbv-kit-input-medium-height</code> - Controls the height for medium inputs.
</p>
<p>
<code>--dbv-kit-input-large-padding</code> - Controls the padding for large inputs.
</p>
<p>
<code>--dbv-kit-input-large-font-size</code> - Sets the font size for large inputs.
</p>
<p>
<code>--dbv-kit-input-large-height</code> - Controls the height for large inputs.
</p>
<p>
<code>--dbv-kit-input-xlarge-padding</code> - Controls the padding for extra large inputs.
</p>
<p>
<code>--dbv-kit-input-xlarge-font-size</code> - Sets the font size for extra large inputs.
</p>
<p>
<code>--dbv-kit-input-xlarge-height</code> - Controls the height for extra large inputs.
</p>
</section>
);
};
export const InputText = () => {
return (
<>
<div className="tw:space-y-2">
@@ -19,3 +145,134 @@ export const InputExample = () => {
</>
);
};
export const InputNumber = () => {
return (
<>
<div className="tw:space-y-2">
<Input type="number" label="Age" defaultValue={18} />
<Button>Enter</Button>
</div>
</>
);
};
export const InputEmail = () => {
return (
<>
<datalist id="ice-cream-emails">
<option value="Chocolate@gmail.com"></option>
<option value="Coconut@gmail.com"></option>
<option value="Mint@gmail.com"></option>
<option value="Strawberry@gmail.com"></option>
<option value="Vanilla@gmail.com"></option>
</datalist>
<div className="tw:space-y-2">
<Input list="ice-cream-emails" type="email" label="E-mail" placeholder="john@gmail.com" />
<Button>Send</Button>
</div>
</>
);
};
export const InputMonth = () => {
return (
<>
<div className="tw:space-y-2">
<Input type="month" label="Month" />
<Button>Set</Button>
</div>
</>
);
};
export const InputWeek = () => {
return (
<>
<div className="tw:space-y-2">
<Input type="week" label="Week" />
<Button>Set</Button>
</div>
</>
);
};
export const InputDate = () => {
return (
<>
<div className="tw:space-y-2">
<Input label="Birth date" type="date" />
<Button>Set</Button>
</div>
</>
);
};
export const InputTime = () => {
return (
<>
<div className="tw:space-y-2">
<Input type="time" label="Time" />
<Button>Set</Button>
</div>
</>
);
};
export const InputRange = () => {
return (
<>
This is not a custom component, but a native HTML input range element.
<div className="tw:space-y-2">
<Input type="range" label="Range" min={1} max={100} />
<Button>Set</Button>
</div>
</>
);
};
export const InputSearch = () => {
return (
<>
<div className="tw:space-y-2">
<Input type="search" label="Search" />
<Button>Find</Button>
</div>
</>
);
};
export const InputTel = () => {
return (
<>
<div className="tw:space-y-2">
<Input type="tel" label="Phone" />
<Button>Call</Button>
</div>
</>
);
};
export const InputDisabled = () => {
return (
<>
<div className="tw:space-y-2">
<Input label="Username" value="John" disabled />
<Input label="E-mail" value="john@google.com" disabled />
<div className="tw:mt-2"> To change this fields, please ask your administrator</div>
</div>
</>
);
};
export const InputRequired = () => {
return (
<form>
<div className="tw:space-y-2">
<Input label="Username" required />
<Input label="E-mail" required />
<Button type="submit">Register</Button>
</div>
</form>
);
};
@@ -0,0 +1,270 @@
/*
* CloudBeaver - Cloud Database Manager
* 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.
*/
import { useEffect, useState } from 'react';
import { _Radio, Radio, _RadioProvider, _RadioGroup } from '../../../Radio/index.js';
import { RadioGroup } from '../../../Radio/RadioGroup.js';
export const Documentation = () => {
return (
<section>
<h1>Radio Component Documentation</h1>
<p>
The Radio component is a customizable radio button that allows users to select one option from a set. The appearance can be customized using
CSS classes and variables.
</p>
<h2>Props</h2>
Accept all props from the underlying{' '}
<a target="_blank" href="https://ariakit.org/reference/radio">
{' '}
Radio
</a>{' '}
component.
<h3>Other properties</h3>
<p>
<code>size</code> - Size of the radio button. Default is 'medium'.
</p>
<h2>CSS Classes</h2>
<h3>Main Radio Element</h3>
<p>
<code>.dbv-kit-radio</code> - The main container class for the radio component. Controls the overall appearance including size, colors, and
borders.
</p>
<h3>Control</h3>
<p>
<code>.dbv-kit-radio__control</code> - Applied to the control element of the radio component.
</p>
<h3>Text</h3>
<p>
<code>.dbv-kit-radio__title</code> - Applied to the visible title element of the radio component.
</p>
<h2>CSS Variables</h2>
<h3>Radio Component Variables</h3>
<p>
<code>--dbv-kit-radio-height</code> - Controls the height of the radio component.
</p>
<p>
<code>--dbv-kit-radio-gap</code> - Sets the gap between the radio button and the text.
</p>
<p>
<code>--dbv-kit-radio-inactive-background</code> - Sets the background color of the inactive radio button.
</p>
<p>
<code>--dbv-kit-radio-inactive-border</code> - Sets the border color of the inactive radio button.
</p>
<p>
<code>--dbv-kit-radio-active-background</code> - Sets the background color of the active radio button.
</p>
<p>
<code>--dbv-kit-radio-active-border</code> - Sets the border color of the active radio button.
</p>
<p>
<code>--dbv-kit-radio-active-foreground</code> - Sets the foreground color of the active radio button.
</p>
<p>
<code>--dbv-kit-radio-hover-shadow-color</code> - Defines the shadow color when hovering over the radio button.
</p>
<h3>Size Variables</h3>
<p>
<code>--dbv-kit-radio-small-height</code> - Controls the height of the small radio button.
</p>
<p>
<code>--dbv-kit-radio-medium-height</code> - Controls the height of the medium radio button.
</p>
<p>
<code>--dbv-kit-radio-large-height</code> - Controls the height of the large radio button.
</p>
<p>
<code>--dbv-kit-radio-xlarge-height</code> - Controls the height of the extra-large radio button.
</p>
<h2>RadioGroup Component</h2>
<p>
The RadioGroup component is used to group multiple Radio components together. It supports both controlled and uncontrolled modes, and can be
displayed vertically or horizontally. By default, you should use the RadioGroup component to use Radio components.
</p>
<h3>Props</h3>
<p>
Basically, RadioGroup accepts a list of underlying{' '}
<a target="_blank" href="https://ariakit.org/reference/radio-provider">
{' '}
RadioProvider
</a>{' '}
component props such as 'value' | 'setValue' | 'store' | 'setActiveId' | 'rtl' | 'defaultValue'.{' '}
</p>
<h4 className="tw:underline">Other properties</h4>
<p>
<code>vertical</code> - Boolean to display the radio buttons vertically.
</p>
<p>
<code>labelledBy</code> - ID of the element that labels the radio group, if you don't use label prop.
</p>
<p>
<code>label</code> - React node to display as the label for the radio group. Rendered inside a label element and should respect HTML label
semantics.
</p>
<p>
<code>required</code> - Boolean to mark the radio group as required.
</p>
<p>
<code>className</code> - Additional CSS classes to apply to the radio group.
</p>
<p>
<code>items</code> - Array of objects with 'value' and 'label' properties to render radio buttons. Cannot be used together with children.
</p>
<p>
<code>name</code> - Name of the radio group. Recommended to always provide a name to radio groups if children Radio components don't.
</p>
<p>
<code>size</code> - Size of the radio buttons.
</p>
</section>
);
};
export const RadioGroupControlled = () => {
const [value, setValue] = useState('2');
useEffect(() => {
const intervalId = setInterval(() => {
const randomValue = Math.floor(Math.random() * 3 + 1).toString();
setValue(randomValue);
}, 2000);
return () => clearInterval(intervalId);
}, []);
return (
<RadioGroup aria-label="Controlled radio group" value={value} setValue={val => setValue(val as string)}>
<Radio value={'1'}>Item 1</Radio>
<Radio value={'2'}>Item 2</Radio>
<Radio value={'3'}>Item 3</Radio>
</RadioGroup>
);
};
export const RadioGroupUncontrolled = () => (
<RadioGroup label="Uncontrolled">
<Radio value={'1'}>Item 1</Radio>
<Radio value={'2'}>Item 2</Radio>
<Radio value={'3'}>Item 3</Radio>
</RadioGroup>
);
export const Vertical = () => (
<RadioGroup label="Vertical" vertical>
<Radio value={'1'}>Item 1</Radio>
<Radio value={'2'}>Item 2</Radio>
<Radio value={'3'}>Item 3</Radio>
</RadioGroup>
);
export const Required = () => (
<RadioGroup label="Required" required>
<Radio value={'1'}>Item 1</Radio>
<Radio value={'2'}>Item 2</Radio>
<Radio value={'3'}>Item 3</Radio>
</RadioGroup>
);
export const LabelsReactComponents = () => (
<RadioGroup label="" vertical>
<Radio value={'1'}>
<div className="tw:flex tw:hover:bg-orange-100 tw:transition tw:duration-300 tw:ease-in-out">
<div className="tw:bg-orange-500 tw:text-white tw:px-2">Orange</div>
<div className="tw:ml-2 tw:text-gray-500">Juicy</div>
</div>
</Radio>
<Radio value={'2'}>
<div className="tw:flex tw:hover:bg-green-100">
<div className="tw:bg-green-300 tw:text-green-900 tw:px-2">Apple</div>
<div className="tw:ml-2 tw:text-gray-500">Vital</div>
</div>
</Radio>
<Radio value={'3'}>
<div className="tw:flex tw:hover:bg-rose-100">
<div className="tw:bg-rose-400 tw:text-white tw:px-2">Peach</div>
<div className="tw:ml-2 tw:text-gray-500">Silky</div>
</div>
</Radio>
</RadioGroup>
);
export const GroupWithCustomLabel = () => (
<div>
<div id="fruits-radio" className="tw:text-lg">
Pick a fruit
</div>
<RadioGroup labelledBy="fruits-radio">
<Radio value={'1'}>Apple</Radio>
<Radio value={'2'}>Banana</Radio>
<Radio value={'3'}>Cherry</Radio>
</RadioGroup>
</div>
);
export const Sizes = () => (
<div className="tw:space-y-4">
<RadioGroup label="Small">
<Radio size="small" value={'1'}>
Small
</Radio>
<Radio size="small" value={'2'}>
Small
</Radio>
<Radio size="small" value={'3'}>
Small
</Radio>
</RadioGroup>
<RadioGroup label="Medium">
<Radio size="medium" value={'1'}>
Medium
</Radio>
<Radio size="medium" value={'2'}>
Medium
</Radio>
<Radio size="medium" value={'3'}>
Medium
</Radio>
</RadioGroup>
<RadioGroup label="Large">
<Radio size="large" value={'1'}>
Large
</Radio>
<Radio size="large" value={'2'}>
Large
</Radio>
<Radio size="large" value={'3'}>
Large
</Radio>
</RadioGroup>
<RadioGroup label="Extra Large">
<Radio size="xlarge" value={'1'}>
Extra Large
</Radio>
<Radio size="xlarge" value={'2'}>
Extra Large
</Radio>
<Radio size="xlarge" value={'3'}>
Extra Large
</Radio>
</RadioGroup>
</div>
);
export const RadioDisabled = () => (
<RadioGroup label="Disabled">
<Radio value={'1'} disabled>
Disabled
</Radio>
<Radio value={'2'}>Enabled</Radio>
<Radio value={'3'} disabled>
Disabled
</Radio>
<Radio value={'4'}>Enabled</Radio>
</RadioGroup>
);
@@ -1,3 +1,11 @@
/*
* CloudBeaver - Cloud Database Manager
* 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.
*/
import { Select } from '../../../Select/Select.js';
export const Docs = () => {
@@ -69,7 +77,7 @@ export const Docs = () => {
<code>--dbv-kit-control-outline-width</code> - Defines the width of the focus outline.
</p>
<p>
<code>--dbv-kit-color-outline</code> - Sets the color of the focus outline.
<code>--dbv-kit-control-outline-color</code> - Sets the color of the focus outline.
</p>
<h3>Label Variables</h3>
@@ -1,3 +1,11 @@
/*
* CloudBeaver - Cloud Database Manager
* 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.
*/
export const GlobalTokens = () => {
return (
<div className="tw:p-4 tw:my-4">
@@ -99,8 +107,8 @@ export const GlobalTokens = () => {
<span className="color-sample" style={{ backgroundColor: 'var(--dbv-kit-color-primary-900)' }}></span>
</p>
<p>
<code>--dbv-kit-color-outline:</code> Default control outline color used for focused elements{' '}
<span className="color-sample" style={{ backgroundColor: 'var(--dbv-kit-color-outline)' }}></span>
<code>--dbv-kit-control-outline-color:</code> Default control outline color used for focused elements{' '}
<span className="color-sample" style={{ backgroundColor: 'var(--dbv-kit-control-outline-color)' }}></span>
</p>
<p
style={{