Skip to content

Commit

Permalink
feat(input): inputgroup status indicator
Browse files Browse the repository at this point in the history
  • Loading branch information
Powerplex committed Jun 29, 2023
1 parent 64f8d6a commit ccb8496
Show file tree
Hide file tree
Showing 12 changed files with 94 additions and 39 deletions.
12 changes: 12 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

14 changes: 7 additions & 7 deletions packages/components/input/src/Input.doc.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ npm install @spark-ui/input
## Import

```tsx
import { Input } from '@spark-ui/input'
import { Input, InputGroup } from '@spark-ui/input'
```

## Props
Expand Down Expand Up @@ -50,12 +50,6 @@ Use `disabled` prop to indicate the input is disabled.

<Canvas of={stories.Disabled} />

## Intent

Use `intent` prop to change the color of the input.

<Canvas of={stories.Intent} />

## InputGroup

Use `InputGroup` component to group your input with addons and elements like icons.
Expand All @@ -72,6 +66,12 @@ Use `InputGroup` component to group your input with addons and elements like ico
}}
/>

## Status

Use `status` prop to change the color of the input.

<Canvas of={stories.Status} />

### Addons

<Canvas of={stories.GroupAddons} />
Expand Down
18 changes: 11 additions & 7 deletions packages/components/input/src/Input.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,39 +37,43 @@ export const Disabled: StoryFn = _args => (
<Input defaultValue="IPhone" disabled aria-label="Phone type" />
)

const intents: InputProps['intent'][] = ['neutral', 'success', 'alert', 'error']
const statuses: InputProps['status'][] = ['success', 'alert', 'error']

export const Intent: StoryFn = _args => {
export const Status: StoryFn = _args => {
return (
<div className="flex flex-col gap-md">
{intents.map(intent => (
<Input key={intent} intent={intent} aria-label="Phone type" />
{statuses.map(status => (
<InputGroup status={status}>
<Input key={status} aria-label="Phone type" />
<InputGroup.StatusIndicator />
</InputGroup>
))}
</div>
)
}

export const GroupAddons: StoryFn = _args => {
return (
<InputGroup>
<InputGroup status="error">
<InputGroup.LeftAddon>https://</InputGroup.LeftAddon>
<Input aria-label="Website" />
<InputGroup.StatusIndicator />
<InputGroup.RightAddon>.com</InputGroup.RightAddon>
</InputGroup>
)
}

export const GroupElements: StoryFn = _args => {
return (
<InputGroup>
<InputGroup status="error">
<InputGroup.LeftElement>
<Icon>
<PenOutline />
</Icon>
</InputGroup.LeftElement>

<Input placeholder="Type here..." />

<InputGroup.StatusIndicator />
<InputGroup.RightElement>
<Icon>
<Check />
Expand Down
10 changes: 5 additions & 5 deletions packages/components/input/src/Input.styles.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { cva, VariantProps } from 'class-variance-authority'

export const inputStyles = cva([], {
variants: {
intent: {
status: {
neutral: [],
success: [],
alert: [],
Expand All @@ -15,7 +15,7 @@ export const inputStyles = cva([], {
},
compoundVariants: [
{
intent: 'neutral',
status: 'neutral',
isGrouped: false,
class: [
'border-outline',
Expand All @@ -25,17 +25,17 @@ export const inputStyles = cva([], {
],
},
{
intent: 'success',
status: 'success',
isGrouped: false,
class: ['border-success', 'ring-success'],
},
{
intent: 'alert',
status: 'alert',
isGrouped: false,
class: ['border-alert', 'ring-alert'],
},
{
intent: 'error',
status: 'error',
isGrouped: false,
class: ['border-error', 'ring-error'],
},
Expand Down
6 changes: 3 additions & 3 deletions packages/components/input/src/Input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,16 @@ import { InputPrimitive, InputPrimitiveProps } from './InputPrimitive'
export interface InputProps extends InputPrimitiveProps, InputStylesProps {}

export const Input = forwardRef<HTMLInputElement, InputProps>(
({ className: classNameProp, intent: intentProp = 'neutral', ...others }, ref) => {
({ className: classNameProp, status: statusProp = 'neutral', ...others }, ref) => {
const field = useFormFieldControl()
const group = useInputGroup()
const isGrouped = !!group
const intent = field.state ?? intentProp
const status = field.state ?? statusProp

return (
<InputPrimitive
ref={ref}
className={inputStyles({ className: classNameProp, intent, isGrouped })}
className={inputStyles({ className: classNameProp, status, isGrouped })}
{...others}
/>
)
Expand Down
2 changes: 1 addition & 1 deletion packages/components/input/src/InputAddon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { useInputGroup } from './InputGroupContext'

export interface InputAddonProps
extends ComponentPropsWithoutRef<'div'>,
Omit<InputAddonStylesProps, 'intent' | 'isDisabled'> {}
Omit<InputAddonStylesProps, 'status' | 'isDisabled'> {}

export const InputAddon = forwardRef<HTMLDivElement, PropsWithChildren<InputAddonProps>>(
({ className, ...others }, ref) => {
Expand Down
2 changes: 1 addition & 1 deletion packages/components/input/src/InputContainer.styles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export const inputContainerStyles = cva(
],
{
variants: {
intent: {
status: {
neutral: [
'border-outline',
'peer-hover:border-outline-high',
Expand Down
4 changes: 2 additions & 2 deletions packages/components/input/src/InputContainer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,10 @@ export interface InputContainerProps
}

export const InputContainer = forwardRef<HTMLDivElement, PropsWithChildren<InputContainerProps>>(
({ className, intent, asChild, ...others }, ref) => {
({ className, status = 'neutral', asChild, ...others }, ref) => {
const Component = asChild ? Slot : 'div'

return <Component ref={ref} className={inputContainerStyles({ intent })} {...others} />
return <Component ref={ref} className={inputContainerStyles({ status })} {...others} />
}
)

Expand Down
14 changes: 8 additions & 6 deletions packages/components/input/src/InputGroup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,18 +16,18 @@ import { InputContainer, InputContainerProps } from './InputContainer'
import { inputGroupStyles, InputGroupStylesProps } from './InputGroup.styles'
import { InputGroupContext } from './InputGroupContext'
export interface InputGroupProps extends ComponentPropsWithoutRef<'div'>, InputGroupStylesProps {
intent?: InputContainerProps['intent']
status?: InputContainerProps['status']
isDisabled?: boolean
}

export const InputGroup = forwardRef<HTMLDivElement, PropsWithChildren<InputGroupProps>>(
(
{ className, children: childrenProp, intent: intentProp = 'neutral', isDisabled, ...others },
{ className, children: childrenProp, status: statusProp = 'neutral', isDisabled, ...others },
ref
) => {
const { state } = useFormFieldControl()
const children = Children.toArray(childrenProp).filter(isValidElement)
const intent = state ?? intentProp
const status = state ?? statusProp

const getDisplayName = (element?: ReactElement) => {
return element ? (element.type as FC).displayName : ''
Expand All @@ -38,6 +38,7 @@ export const InputGroup = forwardRef<HTMLDivElement, PropsWithChildren<InputGrou
}

const input = findElement('Input', 'TextField', 'Textarea')
const statusIndicator = findElement('InputGroup.StatusIndicator')
const left = findElement('InputGroup.LeftAddon', 'InputGroup.LeftElement')
const right = findElement('InputGroup.RightAddon', 'InputGroup.RightElement')
const isLeftAddonVisible = getDisplayName(left) === 'InputGroup.LeftAddon'
Expand All @@ -48,13 +49,15 @@ export const InputGroup = forwardRef<HTMLDivElement, PropsWithChildren<InputGrou

const value = useMemo(() => {
return {
status,
isDisabled: !!isDisabled,
isLeftElementVisible,
isRightElementVisible,
isLeftAddonVisible,
isRightAddonVisible,
}
}, [
status,
isDisabled,
isLeftElementVisible,
isRightElementVisible,
Expand All @@ -77,24 +80,23 @@ export const InputGroup = forwardRef<HTMLDivElement, PropsWithChildren<InputGrou
<>
{input}

<InputContainer intent={intent} />
<InputContainer status={status} />

{isLeftElementVisible && left}

{isRightElementVisible && right}
</>
) : (
cloneElement(input as ReactElement, {
elements: (
<>
{isLeftElementVisible && left}

{isRightElementVisible && right}
</>
),
})
)}

{statusIndicator}
{isRightAddonVisible && right}
</div>
</InputGroupContext.Provider>
Expand Down
4 changes: 2 additions & 2 deletions packages/components/input/src/InputGroupContext.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { createContext, useContext } from 'react'

import { InputContainerProps } from './InputContainer'
import { type InputContainerProps } from './InputContainer'

export interface InputGroupContext extends Pick<InputContainerProps, 'intent'> {
export interface InputGroupContext extends Pick<InputContainerProps, 'status'> {
isDisabled?: boolean
isLeftElementVisible: boolean
isRightElementVisible: boolean
Expand Down
32 changes: 32 additions & 0 deletions packages/components/input/src/InputStatusIndicator.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { Icon } from '@spark-ui/icon'
import { AlertOutline } from '@spark-ui/icons/dist/icons/AlertOutline'
import { Check } from '@spark-ui/icons/dist/icons/Check'
import { WarningOutline } from '@spark-ui/icons/dist/icons/WarningOutline'

import { useInputGroup } from './InputGroupContext'

export type InputStatusIndicatorProps = any

export const InputStatusIndicator = () => {
const group = useInputGroup()

if (!group?.status || group?.status === 'neutral') return null

const { status } = group

const statusMap = {
error: { icon: <AlertOutline /> },
alert: { icon: <WarningOutline /> },
success: { icon: <Check /> },
}

return (
<div className="flex items-center pr-lg align-middle">
<Icon intent={status} size="md">
{statusMap[status].icon}
</Icon>
</div>
)
}

InputStatusIndicator.displayName = 'InputStatusIndicator'
15 changes: 10 additions & 5 deletions packages/components/input/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { FC } from 'react'

import { InputGroup as Root, InputGroupProps } from './InputGroup'
import { InputLeftAddon, InputLeftAddonProps } from './InputLeftAddon'
import { InputLeftElement, InputLeftElementProps } from './InputLeftElement'
import { InputRightAddon, InputRightAddonProps } from './InputRightAddon'
import { InputRightElement, InputRightElementProps } from './InputRightElement'
import { InputGroup as Root, type InputGroupProps } from './InputGroup'
import { InputLeftAddon, type InputLeftAddonProps } from './InputLeftAddon'
import { InputLeftElement, type InputLeftElementProps } from './InputLeftElement'
import { InputRightAddon, type InputRightAddonProps } from './InputRightAddon'
import { InputRightElement, type InputRightElementProps } from './InputRightElement'
import { InputStatusIndicator, type InputStatusIndicatorProps } from './InputStatusIndicator'

export { useInputGroup } from './InputGroupContext'

Expand All @@ -16,20 +17,24 @@ export { type InputLeftAddonProps } from './InputLeftAddon'
export { type InputLeftElementProps } from './InputLeftElement'
export { type InputRightAddonProps } from './InputRightAddon'
export { type InputRightElementProps } from './InputRightElement'
export { type InputStatusIndicatorProps } from './InputStatusIndicator'

export const InputGroup: FC<InputGroupProps> & {
LeftAddon: FC<InputLeftAddonProps>
RightAddon: FC<InputRightAddonProps>
LeftElement: FC<InputLeftElementProps>
RightElement: FC<InputRightElementProps>
StatusIndicator: FC<InputStatusIndicatorProps>
} = Object.assign(Root, {
LeftAddon: InputLeftAddon,
RightAddon: InputRightAddon,
LeftElement: InputLeftElement,
RightElement: InputRightElement,
StatusIndicator: InputStatusIndicator,
})

InputGroup.LeftAddon.displayName = 'InputGroup.LeftAddon'
InputGroup.RightAddon.displayName = 'InputGroup.RightAddon'
InputGroup.LeftElement.displayName = 'InputGroup.LeftElement'
InputGroup.RightElement.displayName = 'InputGroup.RightElement'
InputGroup.StatusIndicator.displayName = 'InputGroup.StatusIndicator'

0 comments on commit ccb8496

Please sign in to comment.