Skip to content

Commit

Permalink
feat(components): add Input, Checkbox and RadioGroup components
Browse files Browse the repository at this point in the history
  • Loading branch information
aarthy-dk committed Nov 7, 2024
1 parent 6f1f595 commit b8cb63a
Show file tree
Hide file tree
Showing 6 changed files with 317 additions and 4 deletions.
8 changes: 7 additions & 1 deletion testgen/ui/components/frontend/css/shared.css
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ body {
--secondary-text-color: #0000008a;
--disabled-text-color: #00000042;
--caption-text-color: rgba(49, 51, 63, 0.6); /* Match Streamlit's caption color */
--border-color: rgba(0, 0, 0, .12);
--form-field-color: rgb(240, 242, 246); /* Match Streamlit's form field color */
--border-color: rgba(0, 0, 0, .12);
--tooltip-color: #333d;
--dk-card-background: #fff;

Expand Down Expand Up @@ -71,6 +72,7 @@ body {
--secondary-text-color: rgba(255, 255, 255, .7);
--disabled-text-color: rgba(255, 255, 255, .5);
--caption-text-color: rgba(250, 250, 250, .6); /* Match Streamlit's caption color */
--form-field-color: rgb(38, 39, 48); /* Match Streamlit's form field color */
--border-color: rgba(255, 255, 255, .25);
--dk-card-background: #14181f;

Expand All @@ -94,6 +96,10 @@ body {
}
}

.clickable {
cursor: pointer;
}

.hidden {
display: none !important;
}
Expand Down
83 changes: 83 additions & 0 deletions testgen/ui/components/frontend/js/components/checkbox.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
/**
* @typedef Properties
* @type {object}
* @property {string} label
* @property {boolean?} checked
* @property {function?} onChange
* @property {number?} width
*/
import van from '../van.min.js';
import { getValue, loadStylesheet } from '../utils.js';

const { input, label } = van.tags;

const Checkbox = (/** @type Properties */ props) => {
loadStylesheet('checkbox', stylesheet);

return label(
{
class: 'flex-row fx-gap-2 clickable',
style: () => `width: ${props.width ? getValue(props.width) + 'px' : 'auto'}`,
},
input({
type: 'checkbox',
class: 'tg-checkbox--input clickable',
checked: props.checked,
onchange: van.derive(() => {
const onChange = props.onChange?.val ?? props.onChange;
return onChange ? (event) => onChange(event.target.checked) : null;
}),
}),
props.label,
);
};

const stylesheet = new CSSStyleSheet();
stylesheet.replace(`
.tg-checkbox--input {
appearance: none;
box-sizing: border-box;
margin: 0;
width: 18px;
height: 18px;
border: 1px solid var(--secondary-text-color);
border-radius: 4px;
position: relative;
transition-property: border-color, background-color;
transition-duration: 0.3s;
}
.tg-checkbox--input:focus,
.tg-checkbox--input:focus-visible {
outline: none;
}
.tg-checkbox--input:focus-visible::before {
content: '';
box-sizing: border-box;
position: absolute;
top: -4px;
left: -4px;
width: 24px;
height: 24px;
border: 3px solid var(--border-color);
border-radius: 7px;
}
.tg-checkbox--input:checked {
border-color: transparent;
background-color: var(--primary-color);
}
.tg-checkbox--input:checked::after {
position: absolute;
top: -4px;
left: -3px;
content: 'check';
font-family: 'Material Symbols Rounded';
font-size: 22px;
color: white;
}
`);

export { Checkbox };
104 changes: 104 additions & 0 deletions testgen/ui/components/frontend/js/components/input.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
/**
* @typedef Properties
* @type {object}
* @property {string?} label
* @property {(string | number)?} value
* @property {string?} placeholder
* @property {string?} icon
* @property {boolean?} clearable
* @property {function?} onChange
* @property {number?} width
*/
import van from '../van.min.js';
import { debounce, getValue, loadStylesheet } from '../utils.js';

const { input, label, i } = van.tags;

const Input = (/** @type Properties */ props) => {
loadStylesheet('input', stylesheet);

const value = van.derive(() => getValue(props.value) ?? '');
van.derive(() => {
const onChange = props.onChange?.val ?? props.onChange;
onChange?.(value.val);
});

return label(
{
class: 'flex-column fx-gap-1 text-caption text-capitalize tg-input--label',
style: () => `width: ${props.width ? getValue(props.width) + 'px' : 'auto'}`,
},
props.label,
() => getValue(props.icon) ? i(
{ class: 'material-symbols-rounded tg-input--icon' },
props.icon,
) : '',
() => getValue(props.clearable) ? i(
{
class: () => `material-symbols-rounded tg-input--clear clickable ${value.val ? '' : 'hidden'}`,
onclick: () => value.val = '',
},
'clear',
) : '',
input({
class: 'tg-input--field',
value,
placeholder: () => getValue(props.placeholder) ?? '',
oninput: debounce(event => value.val = event.target.value, 300),
}),
);
};

const stylesheet = new CSSStyleSheet();
stylesheet.replace(`
.tg-input--label {
position: relative;
}
.tg-input--icon {
position: absolute;
bottom: 5px;
left: 4px;
font-size: 22px;
}
.tg-input--icon ~ .tg-input--field {
padding-left: 28px;
}
.tg-input--clear {
position: absolute;
bottom: 6px;
right: 4px;
font-size: 20px;
}
.tg-input--clear ~ .tg-input--field {
padding-right: 24px;
}
.tg-input--field {
box-sizing: border-box;
width: 100%;
height: 32px;
border-radius: 8px;
border: 1px solid transparent;
transition: border-color 0.3s;
background-color: var(--form-field-color);
padding: 4px 8px;
color: var(--primary-text-color);
font-size: 14px;
}
.tg-input--field::placeholder {
color: var(--disabled-text-color);
}
.tg-input--field:focus,
.tg-input--field:focus-visible {
outline: none;
border-color: var(--primary-color);
}
`);

export { Input };
104 changes: 104 additions & 0 deletions testgen/ui/components/frontend/js/components/radio_group.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
/**
* @typedef Option
* @type {object}
* @property {string} label
* @property {string | number | boolean | null} value
*
* @typedef Properties
* @type {object}
* @property {string} label
* @property {Option[]} options
* @property {string | number | boolean | null} selected
* @property {function?} onChange
* @property {number?} width
*/
import van from '../van.min.js';
import { getRandomId, getValue, loadStylesheet } from '../utils.js';

const { div, input, label } = van.tags;

const RadioGroup = (/** @type Properties */ props) => {
loadStylesheet('radioGroup', stylesheet);
const groupName = getRandomId();

return div(
{ style: () => `width: ${props.width ? getValue(props.width) + 'px' : 'auto'}` },
div(
{ class: 'text-caption text-capitalize mb-1' },
props.label,
),
() => div(
{ class: 'flex-row fx-gap-4 tg-radio-group' },
getValue(props.options).map(option => label(
{ class: 'flex-row fx-gap-2 text-capitalize clickable' },
input({
type: 'radio',
name: groupName,
value: option.value,
checked: () => option.value === getValue(props.value),
onchange: van.derive(() => {
const onChange = props.onChange?.val ?? props.onChange;
return onChange ? () => onChange(option.value) : null;
}),
class: 'tg-radio-group--input',
}),
option.label,
)),
),
);
};

const stylesheet = new CSSStyleSheet();
stylesheet.replace(`
.tg-radio-group {
height: 32px;
}
.tg-radio-group--input {
appearance: none;
box-sizing: border-box;
margin: 0;
width: 18px;
height: 18px;
border: 1px solid var(--secondary-text-color);
border-radius: 9px;
position: relative;
transition-property: border-color, background-color;
transition-duration: 0.3s;
}
.tg-radio-group--input:focus,
.tg-radio-group--input:focus-visible {
outline: none;
}
.tg-radio-group--input:focus-visible::before {
content: '';
box-sizing: border-box;
position: absolute;
top: -4px;
left: -4px;
width: 24px;
height: 24px;
border: 3px solid var(--border-color);
border-radius: 12px;
}
.tg-radio-group--input:checked {
border-color: var(--primary-color);
}
.tg-radio-group--input:checked::after {
content: '';
box-sizing: border-box;
position: absolute;
top: 3px;
left: 3px;
width: 10px;
height: 10px;
background-color: var(--primary-color);
border-radius: 5px;
}
`);

export { RadioGroup };
4 changes: 2 additions & 2 deletions testgen/ui/components/frontend/js/components/select.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,15 @@
*/
import van from '../van.min.js';
import { Streamlit } from '../streamlit.js';
import { getValue, loadStylesheet } from '../utils.js';
import { getRandomId, getValue, loadStylesheet } from '../utils.js';

const { div, label, option, select } = van.tags;

const Select = (/** @type {Properties} */ props) => {
loadStylesheet('select', stylesheet);
Streamlit.setFrameHeight();

const domId = Math.random().toString(36).substring(2);
const domId = getRandomId();
const changeHandler = props.onChange || post;
return div(
{class: 'tg-select'},
Expand Down
18 changes: 17 additions & 1 deletion testgen/ui/components/frontend/js/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,4 +53,20 @@ function getValue(/** @type object */ prop) { // van state or static value
return prop;
}

export { emitEvent, enforceElementWidth, getValue, loadStylesheet, resizeFrameHeightToElement };
function getRandomId() {
return Math.random().toString(36).substring(2);
}

// https://stackoverflow.com/a/75988895
function debounce(
/** @type function */ callback,
/** @type number */ wait,
) {
let timeoutId = null;
return (...args) => {
window.clearTimeout(timeoutId);
timeoutId = window.setTimeout(() => callback(...args), wait);
};
}

export { debounce, emitEvent, enforceElementWidth, getRandomId, getValue, loadStylesheet, resizeFrameHeightToElement };

0 comments on commit b8cb63a

Please sign in to comment.