feat: adds search input with animation and minimized state
This commit is contained in:
parent
b6f24857c1
commit
e59396e0cf
|
@ -1,11 +1,13 @@
|
||||||
import { useState } from 'react'
|
import { useRef, useState } from 'react'
|
||||||
|
|
||||||
import { Avatar, Button, Tag, Text } from '@status-im/components'
|
import { Avatar, Button, Input, Tag, Text } from '@status-im/components'
|
||||||
import { OpenIcon } from '@status-im/icons'
|
import { OpenIcon, SearchIcon } from '@status-im/icons'
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
|
|
||||||
import { FilterAuthor } from './filters/filter-author'
|
import { FilterAuthor } from './filters/filter-author'
|
||||||
|
|
||||||
|
import type { TextInput } from 'react-native'
|
||||||
|
|
||||||
const issues = [
|
const issues = [
|
||||||
{
|
{
|
||||||
id: 5154,
|
id: 5154,
|
||||||
|
@ -93,6 +95,10 @@ const authors = [
|
||||||
export const TableIssues = () => {
|
export const TableIssues = () => {
|
||||||
const [activeTab, setActiveTab] = useState<'open' | 'closed'>('open')
|
const [activeTab, setActiveTab] = useState<'open' | 'closed'>('open')
|
||||||
|
|
||||||
|
const [issuesSearchText, setIssuesSearchText] = useState('')
|
||||||
|
const inputRef = useRef<TextInput>(null)
|
||||||
|
const [isMinimized, setIsMinimized] = useState(true)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="border-neutral-10 overflow-hidden rounded-2xl border">
|
<div className="border-neutral-10 overflow-hidden rounded-2xl border">
|
||||||
<div className="bg-neutral-5 border-neutral-10 flex border-b p-3">
|
<div className="bg-neutral-5 border-neutral-10 flex border-b p-3">
|
||||||
|
@ -119,6 +125,19 @@ export const TableIssues = () => {
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<div className="flex items-center justify-end">
|
<div className="flex items-center justify-end">
|
||||||
|
<div className="flex px-1">
|
||||||
|
<Input
|
||||||
|
placeholder="Find Author"
|
||||||
|
icon={<SearchIcon size={20} />}
|
||||||
|
size={32}
|
||||||
|
value={issuesSearchText}
|
||||||
|
onChangeText={setIssuesSearchText}
|
||||||
|
onHandleMinimized={() => setIsMinimized(!isMinimized)}
|
||||||
|
minimized={isMinimized}
|
||||||
|
direction="rtl"
|
||||||
|
ref={inputRef}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<FilterAuthor authors={authors} />
|
<FilterAuthor authors={authors} />
|
||||||
<div className="pr-3">
|
<div className="pr-3">
|
||||||
<Button size={32} variant="ghost">
|
<Button size={32} variant="ghost">
|
||||||
|
|
|
@ -1,10 +1,11 @@
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useRef, useState } from 'react'
|
||||||
|
|
||||||
import { SearchIcon } from '@status-im/icons'
|
import { SearchIcon } from '@status-im/icons'
|
||||||
|
|
||||||
import { Input } from './input'
|
import { Input } from './input'
|
||||||
|
|
||||||
import type { Meta, StoryObj } from '@storybook/react'
|
import type { Meta, StoryObj } from '@storybook/react'
|
||||||
|
import type { TextInput } from 'react-native'
|
||||||
|
|
||||||
// More on how to set up stories at: https://storybook.js.org/docs/7.0/react/writing-stories/introduction
|
// More on how to set up stories at: https://storybook.js.org/docs/7.0/react/writing-stories/introduction
|
||||||
const meta: Meta<typeof Input> = {
|
const meta: Meta<typeof Input> = {
|
||||||
|
@ -52,6 +53,33 @@ const InputSearch = () => {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const InputSearchMinimzed = () => {
|
||||||
|
const [value, setValue] = useState('')
|
||||||
|
|
||||||
|
const [isMinimized, setIsMinimized] = useState(true)
|
||||||
|
const inputRef = useRef<TextInput>(null)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Input
|
||||||
|
onHandleMinimized={() => setIsMinimized(!isMinimized)}
|
||||||
|
placeholder="Please type something..."
|
||||||
|
value={value}
|
||||||
|
onChangeText={setValue}
|
||||||
|
icon={<SearchIcon size={20} />}
|
||||||
|
onClear={() => setValue('')}
|
||||||
|
size={32}
|
||||||
|
minimized={isMinimized}
|
||||||
|
ref={inputRef}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Minimized: Story = {
|
||||||
|
render: () => <InputSearchMinimzed />,
|
||||||
|
}
|
||||||
|
|
||||||
export const CompleteExample: Story = {
|
export const CompleteExample: Story = {
|
||||||
render: () => <InputSearch />,
|
render: () => <InputSearch />,
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,7 +6,6 @@ import { focusableInputHOC } from '@tamagui/focusable'
|
||||||
import { TextInput } from 'react-native'
|
import { TextInput } from 'react-native'
|
||||||
|
|
||||||
import { Button } from '../button'
|
import { Button } from '../button'
|
||||||
import { IconButton } from '../icon-button'
|
|
||||||
import { Text } from '../text'
|
import { Text } from '../text'
|
||||||
|
|
||||||
import type { GetProps } from '@tamagui/core'
|
import type { GetProps } from '@tamagui/core'
|
||||||
|
@ -16,6 +15,118 @@ setupReactNative({
|
||||||
TextInput,
|
TextInput,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
type Props = GetProps<typeof InputFrame> & {
|
||||||
|
button?: {
|
||||||
|
label: string
|
||||||
|
onPress: () => void
|
||||||
|
}
|
||||||
|
endLabel?: string
|
||||||
|
icon?: React.ReactElement
|
||||||
|
label?: string
|
||||||
|
onClear?: () => void
|
||||||
|
onHandleMinimized?: (isMinimized: boolean) => void
|
||||||
|
size?: 40 | 32
|
||||||
|
error?: boolean
|
||||||
|
minimized?: boolean
|
||||||
|
direction?: 'ltr' | 'rtl'
|
||||||
|
}
|
||||||
|
|
||||||
|
const _Input = (props: Props, ref: Ref<TextInput>) => {
|
||||||
|
const {
|
||||||
|
button,
|
||||||
|
color = '$neutral-50',
|
||||||
|
endLabel,
|
||||||
|
error,
|
||||||
|
icon,
|
||||||
|
label,
|
||||||
|
onClear,
|
||||||
|
size = 40,
|
||||||
|
minimized,
|
||||||
|
placeholder,
|
||||||
|
value,
|
||||||
|
direction = 'ltr',
|
||||||
|
onHandleMinimized,
|
||||||
|
...rest
|
||||||
|
} = props
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack flexDirection={direction === 'ltr' ? 'row' : 'row-reverse'}>
|
||||||
|
{Boolean(label || endLabel) && (
|
||||||
|
<Stack flexDirection="row" justifyContent="space-between" pb={8}>
|
||||||
|
{label && (
|
||||||
|
<Text size={13} color="$neutral-50" weight="medium">
|
||||||
|
{label}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
{endLabel && (
|
||||||
|
<Text size={13} color="$neutral-50">
|
||||||
|
{endLabel}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
)}
|
||||||
|
<InputContainer
|
||||||
|
size={size}
|
||||||
|
error={error}
|
||||||
|
minimized={minimized}
|
||||||
|
onPress={event => {
|
||||||
|
event.stopPropagation()
|
||||||
|
event.preventDefault()
|
||||||
|
|
||||||
|
if (onHandleMinimized && minimized) {
|
||||||
|
onHandleMinimized(false)
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-ignore ref is not inferred correctly here
|
||||||
|
ref?.current?.focus()
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{icon ? (
|
||||||
|
<Stack flexShrink={0}>{cloneElement(icon, { color })}</Stack>
|
||||||
|
) : null}
|
||||||
|
<InputBase
|
||||||
|
value={value}
|
||||||
|
placeholder={minimized && !value ? '' : placeholder}
|
||||||
|
flex={1}
|
||||||
|
ref={ref}
|
||||||
|
onBlur={() => {
|
||||||
|
if (!value && onHandleMinimized && !minimized) {
|
||||||
|
onHandleMinimized?.(true)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
{...rest}
|
||||||
|
/>
|
||||||
|
<Stack flexDirection="row" alignItems="center">
|
||||||
|
{Boolean(onClear) && !!value && (
|
||||||
|
<Stack
|
||||||
|
role="button"
|
||||||
|
accessibilityRole="button"
|
||||||
|
pr={4}
|
||||||
|
onPress={onClear}
|
||||||
|
cursor="pointer"
|
||||||
|
hoverStyle={{ opacity: 0.6 }}
|
||||||
|
animation="fast"
|
||||||
|
>
|
||||||
|
<ClearIcon size={20} />
|
||||||
|
</Stack>
|
||||||
|
)}
|
||||||
|
{button && (
|
||||||
|
<Button onPress={button.onPress} variant="outline" size={24}>
|
||||||
|
{button.label}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
</InputContainer>
|
||||||
|
</Stack>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const Input = forwardRef(_Input)
|
||||||
|
|
||||||
|
export { Input }
|
||||||
|
export type { Props as InputProps }
|
||||||
|
|
||||||
const InputFrame = styled(
|
const InputFrame = styled(
|
||||||
TextInput,
|
TextInput,
|
||||||
{
|
{
|
||||||
|
@ -46,6 +157,8 @@ const InputFrame = styled(
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const InputBase = focusableInputHOC(InputFrame)
|
||||||
|
|
||||||
const InputContainer = styled(Stack, {
|
const InputContainer = styled(Stack, {
|
||||||
name: 'InputContainer',
|
name: 'InputContainer',
|
||||||
tag: 'div',
|
tag: 'div',
|
||||||
|
@ -54,11 +167,12 @@ const InputContainer = styled(Stack, {
|
||||||
gap: 8,
|
gap: 8,
|
||||||
|
|
||||||
borderWidth: 1,
|
borderWidth: 1,
|
||||||
borderColor: '$neutral-20',
|
borderColor: '$neutral-30',
|
||||||
|
|
||||||
paddingHorizontal: 12,
|
paddingHorizontal: 12,
|
||||||
|
|
||||||
animation: 'fast',
|
animation: 'medium',
|
||||||
|
width: '100%',
|
||||||
|
|
||||||
hoverStyle: {
|
hoverStyle: {
|
||||||
borderColor: '$neutral-40',
|
borderColor: '$neutral-40',
|
||||||
|
@ -85,6 +199,15 @@ const InputContainer = styled(Stack, {
|
||||||
borderRadius: '$10',
|
borderRadius: '$10',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
minimized: {
|
||||||
|
true: {
|
||||||
|
width: 32,
|
||||||
|
paddingHorizontal: 0,
|
||||||
|
paddingLeft: 5,
|
||||||
|
|
||||||
|
cursor: 'pointer',
|
||||||
|
},
|
||||||
|
},
|
||||||
error: {
|
error: {
|
||||||
true: {
|
true: {
|
||||||
borderColor: '$danger-50-opa-40',
|
borderColor: '$danger-50-opa-40',
|
||||||
|
@ -99,76 +222,3 @@ const InputContainer = styled(Stack, {
|
||||||
},
|
},
|
||||||
} as const,
|
} as const,
|
||||||
})
|
})
|
||||||
|
|
||||||
type Props = GetProps<typeof InputFrame> & {
|
|
||||||
button?: {
|
|
||||||
label: string
|
|
||||||
onPress: () => void
|
|
||||||
}
|
|
||||||
endLabel?: string
|
|
||||||
icon?: React.ReactElement
|
|
||||||
label?: string
|
|
||||||
onClear?: () => void
|
|
||||||
size?: 40 | 32
|
|
||||||
error?: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
const InputBase = focusableInputHOC(InputFrame)
|
|
||||||
|
|
||||||
const _Input = (props: Props, ref: Ref<HTMLDivElement>) => {
|
|
||||||
const {
|
|
||||||
button,
|
|
||||||
color = '$neutral-50',
|
|
||||||
endLabel,
|
|
||||||
error,
|
|
||||||
icon,
|
|
||||||
label,
|
|
||||||
onClear,
|
|
||||||
size = 40,
|
|
||||||
value,
|
|
||||||
...rest
|
|
||||||
} = props
|
|
||||||
return (
|
|
||||||
<Stack>
|
|
||||||
{Boolean(label || endLabel) && (
|
|
||||||
<Stack flexDirection="row" justifyContent="space-between" pb={8}>
|
|
||||||
{label && (
|
|
||||||
<Text size={13} color="$neutral-50" weight="medium">
|
|
||||||
{label}
|
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
{endLabel && (
|
|
||||||
<Text size={13} color="$neutral-50">
|
|
||||||
{endLabel}
|
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
</Stack>
|
|
||||||
)}
|
|
||||||
<InputContainer size={size} ref={ref} error={error}>
|
|
||||||
{icon ? cloneElement(icon, { color }) : null}
|
|
||||||
<InputBase value={value} {...rest} flex={1} />
|
|
||||||
<Stack flexDirection="row" alignItems="center">
|
|
||||||
{Boolean(onClear) && !!value && (
|
|
||||||
<Stack pr={4}>
|
|
||||||
<IconButton
|
|
||||||
variant="ghost"
|
|
||||||
icon={<ClearIcon size={20} />}
|
|
||||||
onPress={onClear}
|
|
||||||
/>
|
|
||||||
</Stack>
|
|
||||||
)}
|
|
||||||
{button && (
|
|
||||||
<Button onPress={button.onPress} variant="outline" size={24}>
|
|
||||||
{button.label}
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</Stack>
|
|
||||||
</InputContainer>
|
|
||||||
</Stack>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const Input = forwardRef(_Input)
|
|
||||||
|
|
||||||
export { Input }
|
|
||||||
export type { Props as InputProps }
|
|
||||||
|
|
Loading…
Reference in New Issue