Skip to content

Commit

Permalink
fix: make value behavior consistent #1154
Browse files Browse the repository at this point in the history
  • Loading branch information
aalencar committed Mar 25, 2022
1 parent bdc99f9 commit b17a197
Show file tree
Hide file tree
Showing 16 changed files with 211 additions and 178 deletions.
29 changes: 17 additions & 12 deletions ui/src/checkbox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'

/**
Expand Down Expand Up @@ -55,24 +56,28 @@ export interface Checkbox {
}

export const
XCheckbox = ({ model: m }: { model: Checkbox }) => {
const onChange = (_e?: React.FormEvent<HTMLElement>, checked?: B) => {
wave.args[m.name] = checked === null ? null : !!checked
if (m.trigger) wave.push()
}
XCheckbox = (props: { model: Checkbox }) => {
const
{ name, label, disabled, indeterminate, trigger } = props.model,
onChange = (_e?: React.FormEvent<HTMLElement>, checked?: B) => {
setValue(checked)
wave.args[name] = checked === null ? null : !!checked
if (trigger) wave.push()
},
[value, setValue] = useControlledComponent(props, props.model.value)

// eslint-disable-next-line react-hooks/exhaustive-deps
React.useEffect(() => { wave.args[m.name] = !!m.value }, [])
React.useEffect(() => { wave.args[name] = !!value }, [])

return (
<Fluent.Checkbox
data-test={m.name}
inputProps={{ 'data-test': m.name } as React.ButtonHTMLAttributes<HTMLButtonElement>}
label={m.label}
defaultIndeterminate={m.indeterminate}
defaultChecked={m.value}
data-test={name}
inputProps={{ 'data-test': name } as React.ButtonHTMLAttributes<HTMLButtonElement>}
label={label}
defaultIndeterminate={indeterminate}
checked={!!value}
onChange={onChange}
disabled={m.disabled}
disabled={disabled}
/>
)
}
9 changes: 5 additions & 4 deletions ui/src/checklist.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { B, Id, S, U } from 'h2o-wave'
import React from 'react'
import { stylesheet } from 'typestyle'
import { Choice } from './choice_group'
import { useControlledComponent } from './hooks'
import { clas, margin } from './theme'
import { wave } from './ui'

Expand Down Expand Up @@ -74,13 +75,13 @@ export const
const
defaultSelection = React.useMemo(() => new Set<S>(m.values), [m.values]),
getMappedChoices = React.useCallback(() => m.choices?.map(c => ({ c, selected: defaultSelection.has(c.name) })) || [], [defaultSelection, m.choices]),
[choices, setChoices] = React.useState(getMappedChoices()),
[choices, setChoices] = useControlledComponent(m, getMappedChoices()),
capture = (choices: { c: Choice, selected: B }[]) => {
wave.args[m.name] = choices.filter(({ selected }) => selected).map(({ c }) => c.name)
if (m.trigger) wave.push()
},
select = (value: B) => {
const _choices = choices.map(({ c, selected }) => ({ c, selected: c.disabled ? selected : value }))
const _choices = choices.map(({ c, selected }: { c: Choice, selected: B}) => ({ c, selected: c.disabled ? selected : value }))
setChoices(_choices)
capture(_choices)
},
Expand All @@ -92,7 +93,7 @@ export const
setChoices(_choices)
capture(_choices)
},
items = choices.map(({ c, selected }, i) => (
items = choices.map(({ c, selected }: { c: Choice, selected: B}, i: number) => (
<Fluent.Checkbox
key={i}
data-test={`checkbox-${i + 1}`}
Expand All @@ -106,7 +107,7 @@ export const
))
// eslint-disable-next-line react-hooks/exhaustive-deps
React.useEffect(() => { wave.args[m.name] = m.values || [] }, [])
React.useEffect(() => { setChoices(getMappedChoices()) }, [getMappedChoices, m.choices])
React.useEffect(() => { setChoices(getMappedChoices()) }, [getMappedChoices, m.choices, setChoices])
return (
<div data-test={m.name}>
<Fluent.Label>{m.label}</Fluent.Label>
Expand Down
22 changes: 13 additions & 9 deletions ui/src/choice_group.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'

/**
Expand Down Expand Up @@ -65,25 +66,28 @@ export interface ChoiceGroup {
}

export const
XChoiceGroup = ({ model: m }: { model: ChoiceGroup }) => {
XChoiceGroup = (props: { model: ChoiceGroup }) => {
const
{ name, label, required, trigger } = props.model,
[value, setValue] = useControlledComponent(props, props.model.value),
optionStyles = { choiceFieldWrapper: { marginRight: 15 } },
options = (m.choices || []).map(({ name, label, disabled }): Fluent.IChoiceGroupOption => ({ key: name, text: label || name, disabled, styles: optionStyles })),
options = (props.model.choices || []).map(({ name, label, disabled }): Fluent.IChoiceGroupOption => ({ key: name, text: label || name, disabled, styles: optionStyles })),
onChange = (_e?: React.FormEvent<HTMLElement>, option?: Fluent.IChoiceGroupOption) => {
if (option) wave.args[m.name] = option.key
if (m.trigger) wave.push()
setValue(option?.key)
if (option) wave.args[name] = option.key
if (trigger) wave.push()
}

// eslint-disable-next-line react-hooks/exhaustive-deps
React.useEffect(() => { wave.args[m.name] = m.value || null }, [])
React.useEffect(() => { wave.args[name] = value || null }, [])

return (
<Fluent.ChoiceGroup
styles={{ flexContainer: { display: 'flex', flexWrap: 'wrap' } }}
data-test={m.name}
label={m.label}
required={m.required}
defaultSelectedKey={m.value}
data-test={name}
label={label}
required={required}
selectedKey={value}
options={options}
onChange={onChange}
/>
Expand Down
6 changes: 4 additions & 2 deletions ui/src/combobox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'

/**
Expand Down Expand Up @@ -57,9 +58,10 @@ export interface Combobox {


export const
XCombobox = ({ model: m }: { model: Combobox }) => {
XCombobox = (props: { model: Combobox }) => {
const
[text, setText] = React.useState(m.value),
m = props.model,
[text, setText] = useControlledComponent(props, m.value),
options = (m.choices || []).map((text, i): Fluent.IComboBoxOption => ({ key: `${i}`, text })),
onChange = (_e: React.FormEvent<Fluent.IComboBox>, option?: Fluent.IComboBoxOption, _index?: number, value?: string) => {
const v = option?.text || value || ''
Expand Down
2 changes: 1 addition & 1 deletion ui/src/copyable_text.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ export const XCopyableText = ({ model }: { model: CopyableText }) => {

return (
<div data-test={name} className={multiline ? css.multiContainer : css.compactContainer}>
<Fluent.TextField componentRef={ref} defaultValue={value} label={label} multiline={multiline} styles={{ root: { width: pc(100) } }} readOnly />
<Fluent.TextField componentRef={ref} value={value} label={label} multiline={multiline} styles={{ root: { width: pc(100) } }} readOnly />
<Fluent.PrimaryButton
title='Copy to clipboard'
onClick={onClick}
Expand Down
15 changes: 8 additions & 7 deletions ui/src/date_picker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import * as Fluent from '@fluentui/react'
import { B, Id, S, U } from 'h2o-wave'
import React from 'react'
import { useControlledComponent } from './hooks'
import { wave } from './ui'

/**
Expand Down Expand Up @@ -56,19 +57,19 @@ const
if (ss.length !== 3) return undefined
const ymd = ss.map(s => 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)
}

export const
XDatePicker = ({ model: m }: { model: DatePicker }) => {
XDatePicker = (props: { model: DatePicker }) => {
const
m = props.model,
defaultVal = m.value || null,
parsedVal = defaultVal ? parseDate(defaultVal) : null,
[value, setValue] = React.useState<Date | undefined>(parsedVal ? new Date(parsedVal) : undefined),
[value, setValue] = useControlledComponent(props, m.value),
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()
}

Expand All @@ -79,7 +80,7 @@ export const
<Fluent.DatePicker
data-test={m.name}
label={m.label}
value={value}
value={parseDate(value)}
placeholder={m.placeholder}
disabled={m.disabled}
onSelectDate={onSelectDate}
Expand Down
32 changes: 25 additions & 7 deletions ui/src/dropdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { B, Id, S, U } from 'h2o-wave'
import React from 'react'
import { stylesheet } from 'typestyle'
import { Choice } from './choice_group'
import { useControlledComponent } from './hooks'
import { fuzzysearch } from './parts/utils'
import { clas, cssVar, pc } from './theme'
import { wave } from './ui'
Expand Down Expand Up @@ -81,11 +82,13 @@ const
marginTop: 16
}
}),
BaseDropdown = ({ name, label, required, disabled, value, values, choices, trigger, placeholder }: Dropdown) => {
BaseDropdown = (props: Dropdown) => {
const
{ name, label, required, disabled, value, values, choices, trigger, placeholder } = props,
isMultivalued = !!values,
selection = React.useMemo(() => isMultivalued ? new Set<S>(values) : null, [isMultivalued, values]),
[selectedOptions, setSelectedOptions] = React.useState(Array.from(selection || [])),
[selected, setSelected] = useControlledComponent(props, value),
[selectedOptions, setSelectedOptions] = useControlledComponent(props, values),
options = (choices || []).map(({ name, label, disabled }): Fluent.IDropdownOption => ({ key: name, text: label || name, disabled })),
onChange = (_e?: React.FormEvent<HTMLElement>, option?: Fluent.IDropdownOption) => {
if (option) {
Expand All @@ -97,6 +100,7 @@ const
wave.args[name] = selectedOpts
setSelectedOptions(selectedOpts)
} else {
setSelected(String(option.key))
wave.args[name] = optionKey
}
}
Expand Down Expand Up @@ -134,7 +138,7 @@ const
required={required}
disabled={disabled}
multiSelect={isMultivalued || undefined}
defaultSelectedKey={!isMultivalued ? value : undefined}
selectedKey={!isMultivalued ? selected : undefined}
selectedKeys={isMultivalued ? selectedOptions : undefined}
onChange={onChange}
/>
Expand All @@ -151,8 +155,9 @@ const
},
ROW_HEIGHT = 44,
PAGE_SIZE = 40,
DialogDropdown = ({ name, choices, values, value, disabled, required, trigger, placeholder, label }: Dropdown) => {
DialogDropdown = (props: Dropdown) => {
const
{ name, choices, values, value, disabled, required, trigger, placeholder, label } = props,
isMultivalued = !!values,
[isDialogHidden, setIsDialogHidden] = React.useState(true),
initialSelectedMap = React.useMemo(() => {
Expand All @@ -162,14 +167,15 @@ const
}, [value, values]),
items = React.useMemo<DropdownItem[]>(() => choices?.map(({ name, label }, idx) => ({ name, text: label || name, idx, checked: initialSelectedMap.has(name) })) || [], [initialSelectedMap, choices]),
[filteredItems, setFilteredItems] = React.useState(items),
[textValue, setTextValue] = React.useState(() => {
getInitialTextValue = () => {
if (!values?.length && !value) return

const itemsMap = new Map<S, S>(items.map(({ name, text }) => [name, text]))

if (values?.length) return values.map(v => itemsMap.get(v) || '').filter(Boolean).join(', ')
if (value) return itemsMap.get(value)
}),
},
[textValue, setTextValue] = useControlledComponent(props, getInitialTextValue()),
toggleDialog = React.useCallback(() => setIsDialogHidden(!isDialogHidden), [isDialogHidden]),
cancelDialog = React.useCallback(() => {
toggleDialog()
Expand All @@ -184,7 +190,7 @@ const
initialSelectedMap.clear()
result.forEach(({ name }) => initialSelectedMap.set(name, true))
cancelDialog()
}, [cancelDialog, initialSelectedMap, items, name, trigger]),
}, [cancelDialog, initialSelectedMap, items, name, trigger, setTextValue]),
selectAll = (checked = true) => () => setFilteredItems(filteredItems.map(i => { i.checked = checked; return i })),
onSearchChange = (_e?: React.ChangeEvent<HTMLInputElement>, newVal = '') => setFilteredItems(newVal ? items.filter(({ text }) => fuzzysearch(text, newVal)) : items),
onChecked = React.useCallback((idx: U) => (_ev?: React.FormEvent<HTMLElement | HTMLInputElement>, checked = false) => {
Expand All @@ -203,6 +209,18 @@ const
checked={item.checked} />
: null, [onChecked]),
getPageSpecification = React.useCallback(() => ({ itemCount: PAGE_SIZE, height: ROW_HEIGHT * PAGE_SIZE, } as Fluent.IPageSpecification), [])

React.useEffect(() => {
setFilteredItems(items => {
return items.map(i => {
if (isMultivalued ? props?.values?.includes(i.name) : i.name === props.value) {
return ({ ...i, checked: true})
} else {
return ({ ...i, checked: false })
}
})
})
}, [props, initialSelectedMap, isMultivalued])

return (
<>
Expand Down
7 changes: 7 additions & 0 deletions ui/src/hooks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { useEffect, useState } from "react"

export function useControlledComponent(props: any, val: any) {
const [value, setValue] = useState(val)
useEffect(() => setValue(val), [props])
return [value, setValue]
}
Loading

0 comments on commit b17a197

Please sign in to comment.