diff --git a/ui/src/checkbox.tsx b/ui/src/checkbox.tsx index 9238bf123e..de90b76d9b 100644 --- a/ui/src/checkbox.tsx +++ b/ui/src/checkbox.tsx @@ -15,6 +15,7 @@ import * as Fluent from '@fluentui/react' import { B, Id, S } from 'h2o-wave' import React from 'react' +import { useControlledComponent } from './hooks' import { wave } from './ui' /** @@ -57,10 +58,12 @@ export interface Checkbox { export const XCheckbox = ({ model: m }: { model: Checkbox }) => { const onChange = (_e?: React.FormEvent, checked?: B) => { + setValue(checked) wave.args[m.name] = checked === null ? null : !!checked if (m.trigger) wave.push() - } - + }, + [value, setValue] = useControlledComponent(m) + // eslint-disable-next-line react-hooks/exhaustive-deps React.useEffect(() => { wave.args[m.name] = !!m.value }, []) @@ -70,7 +73,7 @@ export const inputProps={{ 'data-test': m.name } as React.ButtonHTMLAttributes} label={m.label} defaultIndeterminate={m.indeterminate} - defaultChecked={m.value} + checked={value} onChange={onChange} disabled={m.disabled} /> diff --git a/ui/src/choice_group.tsx b/ui/src/choice_group.tsx index a759da28ef..fa5170c2d5 100644 --- a/ui/src/choice_group.tsx +++ b/ui/src/choice_group.tsx @@ -15,6 +15,7 @@ import * as Fluent from '@fluentui/react' import { B, Id, S } from 'h2o-wave' import React from 'react' +import { useControlledComponent } from './hooks' import { wave } from './ui' /** @@ -67,9 +68,11 @@ export interface ChoiceGroup { export const XChoiceGroup = ({ model: m }: { model: ChoiceGroup }) => { const + [value, setValue] = useControlledComponent(m), optionStyles = { choiceFieldWrapper: { marginRight: 15 } }, options = (m.choices || []).map(({ name, label, disabled }): Fluent.IChoiceGroupOption => ({ key: name, text: label || name, disabled, styles: optionStyles })), onChange = (_e?: React.FormEvent, option?: Fluent.IChoiceGroupOption) => { + setValue(option?.key) if (option) wave.args[m.name] = option.key if (m.trigger) wave.push() } @@ -83,7 +86,7 @@ export const data-test={m.name} label={m.label} required={m.required} - defaultSelectedKey={m.value} + selectedKey={value} options={options} onChange={onChange} /> diff --git a/ui/src/combobox.tsx b/ui/src/combobox.tsx index 95dcc0fbfc..6b601b4191 100644 --- a/ui/src/combobox.tsx +++ b/ui/src/combobox.tsx @@ -15,6 +15,7 @@ import * as Fluent from '@fluentui/react' import { B, Id, S } from 'h2o-wave' import React from 'react' +import { useControlledComponent } from './hooks' import { wave } from './ui' /** @@ -59,7 +60,7 @@ export interface Combobox { export const XCombobox = ({ model: m }: { model: Combobox }) => { const - [text, setText] = React.useState(m.value), + [text, setText] = useControlledComponent(m), options = (m.choices || []).map((text, i): Fluent.IComboBoxOption => ({ key: `${i}`, text })), onChange = (_e: React.FormEvent, option?: Fluent.IComboBoxOption, _index?: number, value?: string) => { const v = option?.text || value || '' diff --git a/ui/src/copyable_text.tsx b/ui/src/copyable_text.tsx index 935158af08..33db18b1f2 100644 --- a/ui/src/copyable_text.tsx +++ b/ui/src/copyable_text.tsx @@ -80,7 +80,7 @@ export const XCopyableText = ({ model }: { model: CopyableText }) => { return (
- + parseInt(s, 10)).filter(n => !isNaN(n)) if (ymd.length !== 3) return undefined - return new Date(ymd[0], ymd[1] - 1, ymd[2]) + return new Date(ymd[0], ymd[1] - 1, ymd[2], 0, 0, 0) } +const transform = (value: S) => { + const + parsedVal = value ? parseDate(value) : null + return parsedVal ? new Date(parsedVal) : undefined +} + export const XDatePicker = ({ model: m }: { model: DatePicker }) => { const defaultVal = m.value || null, - parsedVal = defaultVal ? parseDate(defaultVal) : null, - [value, setValue] = React.useState(parsedVal ? new Date(parsedVal) : undefined), + [value, setValue] = useControlledComponent(m, transform), onSelectDate = (d: Date | null | undefined) => { - const val = (d === null || d === undefined) ? defaultVal : formatDate(d) + const val = !d ? defaultVal : formatDate(d) wave.args[m.name] = val - setValue(val ? new Date(`${val} 00:00:00`) : undefined) + setValue(val) if (m.trigger) wave.push() } diff --git a/ui/src/hooks.ts b/ui/src/hooks.ts new file mode 100644 index 0000000000..c144914dc4 --- /dev/null +++ b/ui/src/hooks.ts @@ -0,0 +1,15 @@ +import { useCallback, useEffect, useState } from "react" + +export function useControlledComponent(props: any, transform?: any) { + const + [value, _setValue] = useState(transform ? transform(props.value) : props.value), + setValue = useCallback((v: any) => { + _setValue(transform ? transform(v) : v) + }, [transform]) + + useEffect(() => { + setValue(props.value) + }, [props, setValue]) + + return [value, setValue] +} \ No newline at end of file diff --git a/ui/src/slider.tsx b/ui/src/slider.tsx index d60292c530..430186c0dd 100644 --- a/ui/src/slider.tsx +++ b/ui/src/slider.tsx @@ -15,6 +15,7 @@ import * as Fluent from '@fluentui/react' import { B, F, Id, S, U } from 'h2o-wave' import React from 'react' +import { useControlledComponent } from './hooks' import { wave } from './ui' /** @@ -63,7 +64,11 @@ export const const { min = 0, max = 100, step = 1, value = 0 } = m, defaultValue = (value < min) ? min : ((value > max) ? max : value), - onChange = (v: U) => wave.args[m.name] = v, + [val, setVal] = useControlledComponent(m), + onChange = (v: U) => { + setVal(v) + wave.args[m.name] = v + }, onChanged = React.useCallback((_e: MouseEvent | KeyboardEvent | TouchEvent, _value: U) => { if (m.trigger) wave.push() }, [m.trigger]) // eslint-disable-next-line react-hooks/exhaustive-deps @@ -77,7 +82,7 @@ export const min={min} max={max} step={step} - defaultValue={defaultValue} + value={val} showValue originFromZero={min < 0 && max >= 0} onChange={onChange} diff --git a/ui/src/textbox.tsx b/ui/src/textbox.tsx index 88dd7e0c35..d3322e6831 100644 --- a/ui/src/textbox.tsx +++ b/ui/src/textbox.tsx @@ -15,6 +15,7 @@ import * as Fluent from '@fluentui/react' import { B, Id, S } from 'h2o-wave' import React from 'react' +import { useControlledComponent } from './hooks' import { debounce, wave } from './ui' /** @@ -74,8 +75,10 @@ export const v = v || (target as HTMLInputElement).value wave.args[m.name] = v ?? (m.value || '') + setValue(v) if (m.trigger) wave.push() - } + }, + [value, setValue] = useControlledComponent(m) // eslint-disable-next-line react-hooks/exhaustive-deps React.useEffect(() => { wave.args[m.name] = m.value || '' }, []) @@ -85,7 +88,7 @@ export const { - const onChange = React.useCallback((_e?: React.FormEvent, checked?: B) => { - wave.args[m.name] = !!checked - if (m.trigger) wave.push() - }, [m.name, m.trigger]) + const + [value, setValue] = useControlledComponent(m), + onChange = React.useCallback((_e?: React.FormEvent, checked?: B) => { + setValue(!!checked) + wave.args[m.name] = !!checked + if (m.trigger) wave.push() + }, [m.name, m.trigger]) // eslint-disable-next-line react-hooks/exhaustive-deps React.useEffect(() => { wave.args[m.name] = !!m.value }, []) @@ -60,7 +64,7 @@ export const