Skip to content

Commit

Permalink
feat: Allow dynamic value change for tab card #1154 (#2101)
Browse files Browse the repository at this point in the history
Co-authored-by: Martin Turoci <martin.turoci@h2o.ai>
  • Loading branch information
marek-mihok and mturoci committed Jan 15, 2024
1 parent ac513f8 commit 1fb7b5c
Show file tree
Hide file tree
Showing 3 changed files with 131 additions and 21 deletions.
110 changes: 107 additions & 3 deletions ui/src/tab.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@
// See the License for the specific language governing permissions and
// limitations under the License.

import { initializeIcons } from '@fluentui/react'
import { fireEvent, render } from '@testing-library/react'
import * as T from 'h2o-wave'
import React from 'react'
Expand All @@ -30,9 +29,9 @@ const
changed: T.box(false)
}

describe('Meta.tsx', () => {
beforeAll(() => initializeIcons())
describe('Tab.tsx', () => {
beforeEach(() => {
window.location.hash = ''
wave.args[name] = null
jest.clearAllMocks()
})
Expand Down Expand Up @@ -64,13 +63,118 @@ describe('Meta.tsx', () => {
expect(pushMock).toHaveBeenCalledTimes(0)
})

it('Set args when value is updated', () => {
const items = [{ name: 'tab1' }, { name: 'tab2' }]
const props = { ...tabProps, state: { items, value: 'tab1' } }
const { rerender, getAllByRole } = render(<View {...props} />)
expect(getAllByRole('tab')[0]).toHaveClass('is-selected')
expect(wave.args['tab2']).toBeUndefined()

props.state.value = 'tab2'
rerender(<View {...props} />)
expect(wave.args['tab2']).toBe(true)
})

it('Does not set args when value is updated - hash name', () => {
const items = [{ name: '#tab1' }, { name: '#tab2' }]
const props = { ...tabProps, state: { items, value: '#tab1' } }
const { rerender } = render(<View {...props} />)
expect(wave.args[name]).toBeNull()

props.state.value = '#tab2'
rerender(<View {...props} />)

expect(wave.args[name]).toBeNull()
})

it('Selects tab when value is updated', () => {
const items = [{ name: 'tab1' }, { name: 'tab2' }]
const props = { ...tabProps, state: { items, value: 'tab1' } }
const { rerender, getAllByRole } = render(<View {...props} />)
expect(getAllByRole('tab')[0]).toHaveClass('is-selected')
expect(getAllByRole('tab')[1]).not.toHaveClass('is-selected')

props.state.value = 'tab2'
rerender(<View {...props} />)
expect(getAllByRole('tab')[0]).not.toHaveClass('is-selected')
expect(getAllByRole('tab')[1]).toHaveClass('is-selected')
})

it('Selects tab when value is updated twice to the same value', () => {
const items = [{ name: 'tab1' }, { name: 'tab2' }]
const props = { ...tabProps, state: { items, value: 'tab1' } }
const { rerender, getAllByRole } = render(<View {...props} />)
expect(getAllByRole('tab')[0]).toHaveClass('is-selected')
expect(getAllByRole('tab')[1]).not.toHaveClass('is-selected')

props.state.value = 'tab2'
rerender(<View {...props} />)
expect(getAllByRole('tab')[0]).not.toHaveClass('is-selected')
expect(getAllByRole('tab')[1]).toHaveClass('is-selected')

fireEvent.click(getAllByRole('tab')[0])
expect(getAllByRole('tab')[0]).toHaveClass('is-selected')
expect(getAllByRole('tab')[1]).not.toHaveClass('is-selected')

props.state.value = 'tab2'
rerender(<View {...props} />)
expect(getAllByRole('tab')[0]).not.toHaveClass('is-selected')
expect(getAllByRole('tab')[1]).toHaveClass('is-selected')
})

it('Selects tab when value is updated - hash name', () => {
const items = [{ name: '#tab1' }, { name: '#tab2' }]
const props = { ...tabProps, state: { items, value: '#tab1' } }
const { rerender, getAllByRole } = render(<View {...props} />)
expect(getAllByRole('tab')[0]).toHaveClass('is-selected')
expect(getAllByRole('tab')[1]).not.toHaveClass('is-selected')

props.state.value = '#tab2'
rerender(<View {...props} />)
expect(getAllByRole('tab')[0]).not.toHaveClass('is-selected')
expect(getAllByRole('tab')[1]).toHaveClass('is-selected')
})

it('Selects tab when value is updated twice to the same value - hash name', () => {
const items = [{ name: '#tab1' }, { name: '#tab2' }]
const props = { ...tabProps, state: { items, value: '#tab1' } }
const { rerender, getAllByRole } = render(<View {...props} />)
expect(getAllByRole('tab')[0]).toHaveClass('is-selected')
expect(getAllByRole('tab')[1]).not.toHaveClass('is-selected')

props.state.value = '#tab2'
rerender(<View {...props} />)
expect(getAllByRole('tab')[0]).not.toHaveClass('is-selected')
expect(getAllByRole('tab')[1]).toHaveClass('is-selected')

fireEvent.click(getAllByRole('tab')[0])
expect(getAllByRole('tab')[0]).toHaveClass('is-selected')
expect(getAllByRole('tab')[1]).not.toHaveClass('is-selected')

props.state.value = '#tab2'
rerender(<View {...props} />)
expect(getAllByRole('tab')[0]).not.toHaveClass('is-selected')
expect(getAllByRole('tab')[1]).toHaveClass('is-selected')
})

it('Sets url hash - hash name', () => {
const { getByRole } = render(<View {...{ ...tabProps, state: { items: [{ name: hashName }] } }} />)
fireEvent.click(getByRole('tab'))

expect(window.location.hash).toBe(hashName)
})

it('Sets url hash when value is updated - hash name', () => {
const items = [{ name: '#tab1' }, { name: '#tab2' }]
const props = { ...{ ...tabProps, state: { items, value: '#tab1' } } }
const { rerender } = render(<View {...props} />)
expect(window.location.hash).toBe('')

props.state.value = '#tab2'
rerender(<View {...props} />)
expect(window.location.hash).toBe('#tab2')
})

it('Sets default tab', () => {
const items = [{ name: 'tab1' }, { name: 'tab2' }]
const { getAllByRole } = render(<View {...{ ...tabProps, state: { items, value: 'tab2' } }} />)
Expand Down
32 changes: 19 additions & 13 deletions ui/src/tab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
// limitations under the License.

import { Pivot, PivotItem } from '@fluentui/react'
import { B, Model, S } from 'h2o-wave'
import { B, Model, S, box } from 'h2o-wave'
import React from 'react'
import { stylesheet } from 'typestyle'
import { CardEffect, cards } from './layout'
Expand Down Expand Up @@ -45,19 +45,19 @@ const
export const
View = bond(({ name, state, changed }: Model<State>) => {
const
valueB = box<S | undefined>(state.value),
setArgs = (name: S) => {
if (name.startsWith('#')) window.location.hash = name.substring(1)
else if (state.name) wave.args[state.name] = name
else wave.args[name] = true
},
onLinkClick = (item?: PivotItem) => {
const name = item?.props.itemKey
if (!name) return
if (name.startsWith('#')) {
window.location.hash = name.substring(1)
return
}
if (state.name) {
wave.args[state.name] = name
} else {
wave.args[name] = true
}
wave.push()
state.value = name
valueB(name)
setArgs(name)
if (!name.startsWith('#')) wave.push()
},
render = () => {
const
Expand All @@ -67,11 +67,17 @@ export const
))
return (
<div data-test={name} className={css.card}>
<Pivot linkFormat={linkFormat} onLinkClick={onLinkClick} defaultSelectedKey={state.value}>{items}</Pivot>
<Pivot linkFormat={linkFormat} onLinkClick={onLinkClick} selectedKey={valueB() || state.items[0].name}>{items}</Pivot>
</div>
)
},
update = (prevProps: Model<State>) => {
if (prevProps.state.value === valueB()) return
valueB(prevProps.state.value)
setArgs(prevProps.state.value || prevProps.state.items[0].name)
}
return { render, changed }

return { render, changed, update, valueB }
})

cards.register('tab', View, { effect: CardEffect.Transparent })
10 changes: 5 additions & 5 deletions ui/src/ui.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,14 @@ import * as React from 'react'
// React Component + Dataflow
//

interface Renderable {
interface Renderable<T> {
render(): JSX.Element
init?(): void
update?(): void
update?(prevProps: Readonly<T>, prevState: Readonly<any>, snapshot?: any): void
dispose?(): void
}

export function bond<TProps, TState extends Renderable>(ctor: (props: TProps) => TState) {
export function bond<TProps, TState extends Renderable<TProps>>(ctor: (props: TProps) => TState) {
return class extends React.Component<TProps> {
private readonly model: TState
private readonly arrows: Disposable[]
Expand All @@ -52,8 +52,8 @@ export function bond<TProps, TState extends Renderable>(ctor: (props: TProps) =>
componentDidMount() {
if (this.model.init) this.model.init()
}
componentDidUpdate() {
if (this.model.update) this.model.update()
componentDidUpdate(prevProps: Readonly<TProps>, prevState: Readonly<any>, snapshot?: any): void {
if (this.model.update) this.model.update(prevProps, prevState, snapshot)
}
componentWillUnmount() {
if (this.model.dispose) this.model.dispose()
Expand Down

0 comments on commit 1fb7b5c

Please sign in to comment.