Skip to content

Commit

Permalink
feat(input): add group and addons
Browse files Browse the repository at this point in the history
  • Loading branch information
andresz1 committed May 27, 2023
1 parent e6ee33b commit 79ba6c3
Show file tree
Hide file tree
Showing 16 changed files with 281 additions and 168 deletions.
14 changes: 13 additions & 1 deletion packages/components/input/src/Input.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import { Meta, StoryFn } from '@storybook/react'

import { Input } from '.'
import { InputGroup } from './InputGroup'
import { InputLeftAddon } from './InputLeftAddon'
import { InputRightAddon } from './InputRighAddon'

const meta: Meta<typeof Input> = {
title: 'Components/Input',
Expand All @@ -11,7 +14,16 @@ export default meta

export const Default: StoryFn = _args => (
<>
<InputGroup>
<InputLeftAddon>https://</InputLeftAddon>

<Input />

<InputRightAddon>.com</InputRightAddon>
</InputGroup>

<Input />
<Input>rewr</Input>

<Input intent="error" />
</>
)
40 changes: 17 additions & 23 deletions packages/components/input/src/Input.styles.tsx
Original file line number Diff line number Diff line change
@@ -1,32 +1,26 @@
import { cva, VariantProps } from 'class-variance-authority'

export const inputStyles = cva(
[
'relative',
'inline-flex',
'items-center',
'justify-between',
'h-sz-44',
'text-body-1',
'px-lg',
'text-ellipsis',
],
['h-sz-44', 'rounded-lg', 'border-sm', 'outline-none', 'text-ellipsis', 'focus:ring-1'],
{
variants: {
isFocused: {
true: [],
false: [],
intent: {
neutral: ['hover:border-outline-high', 'focus:border-outline-high', 'ring-outline-high'],
success: ['border-success', 'ring-success'],
alert: ['border-alert', 'ring-alert'],
error: ['border-error', 'ring-error'],
},
},
}
)
export const labelTextStyles = cva(
['absolute', 'flex', 'items-center', 'h-full', 'transition-all', 'duration-100'],
{
variants: {
isExpanded: {
true: ['text-body-2', 'top-[-50%]', 'p-md'],
false: ['text-body-1', 'top-[0%]', 'opacity-dim-1'],
isLeftAddonVisible: {
true: ['border-l-none', 'pl-md', '!rounded-l-none', '!ring-0'],
false: ['pl-lg'],
},
isRightAddonVisible: {
true: ['border-r-none', 'pr-md', '!rounded-r-none', '!ring-0'],
false: ['pr-lg'],
},
isHovered: {
true: ['border-outline-high'],
false: ['border-outline'],
},
},
}
Expand Down
101 changes: 38 additions & 63 deletions packages/components/input/src/Input.tsx
Original file line number Diff line number Diff line change
@@ -1,76 +1,51 @@
import { useId } from '@radix-ui/react-id'
import { useCombinedState } from '@spark-ui/use-combined-state'
import { cx } from 'class-variance-authority'
import {
ChangeEvent,
ComponentPropsWithoutRef,
forwardRef,
PropsWithChildren,
ReactNode,
useState,
} from 'react'
import { ComponentPropsWithoutRef, FocusEvent, forwardRef, PropsWithChildren } from 'react'

import { inputStyles, labelTextStyles } from './Input.styles'
import { InputFieldset } from './InputFieldset'
import { inputStyles, InputStylesProps } from './Input.styles'
import { useInputGroup } from './InputGroupContext'

export interface InputProps extends ComponentPropsWithoutRef<'input'> {
leftAddon?: ReactNode
rightAddon?: ReactNode
}
export interface InputProps extends ComponentPropsWithoutRef<'input'>, InputStylesProps {}

export const Input = forwardRef<HTMLInputElement, PropsWithChildren<InputProps>>(
(
{
className,
value: valueProp,
defaultValue,
placeholder,
leftAddon,
rightAddon,
children,
...others
},
ref
) => {
const id = useId()
const [value, setValue] = useCombinedState(valueProp, defaultValue)
const [isFocused, setIsFocused] = useState(false)
const isExpanded = isFocused || !!value || !!placeholder || !!leftAddon

const handleFocus = () => {
setIsFocused(true)
({ className, intent = 'neutral', onFocus, onBlur, ...others }, ref) => {
const group = useInputGroup()
const { isLeftAddonVisible, isRightAddonVisible, isHovered } = group

const handleFocus = (event: FocusEvent<HTMLInputElement>) => {
if (onFocus) {
onFocus(event)
}

if (group.onFocus) {
group.onFocus()
}
}

const handleBlur = () => {
setIsFocused(false)
}
const handleBlur = (event: FocusEvent<HTMLInputElement>) => {
if (onBlur) {
onBlur(event)
}

const handleChange = (event: ChangeEvent<HTMLInputElement>) => {
setValue(event.target.value)
if (group.onBlur) {
group.onBlur()
}
}

return (
<div className={cx(inputStyles({ isFocused }), className)}>
{leftAddon}
<input
id={id}
ref={ref}
value={value}
placeholder={placeholder}
className="box-content h-full w-full text-ellipsis bg-transparent outline-0"
onFocus={handleFocus}
onBlur={handleBlur}
onChange={handleChange}
{...others}
/>
<InputFieldset label={children} isExpanded={isExpanded} />

<label htmlFor={id} className={labelTextStyles({ isExpanded })}>
{children}
</label>

{rightAddon}
</div>
<input
ref={ref}
className={inputStyles({
className,
intent,
isHovered: !!isHovered,
isLeftAddonVisible: !!isLeftAddonVisible,
isRightAddonVisible: !!isRightAddonVisible,
})}
onFocus={handleFocus}
onBlur={handleBlur}
{...others}
/>
)
}
)

Input.displayName = 'Input'
34 changes: 34 additions & 0 deletions packages/components/input/src/InputAddon.styles.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { cva, VariantProps } from 'class-variance-authority'

export const inputAddonStyles = cva(['border-sm', 'flex', 'items-center', 'rounded-lg'], {
variants: {
intent: {
neutral: ['border-outline'],
success: ['border-success'],
alert: ['border-alert'],
error: ['border-error'],
},
isFocused: {
true: [],
false: [],
},
isHovered: {
true: [],
false: [],
},
},
compoundVariants: [
{
intent: 'neutral',
isHovered: true,
class: '!border-outline-high',
},
{
intent: 'neutral',
isFocused: true,
class: '!border-outline-high',
},
],
})

export type InputAddonStylesProps = VariantProps<typeof inputAddonStyles>
24 changes: 24 additions & 0 deletions packages/components/input/src/InputAddon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { ComponentPropsWithoutRef, forwardRef, PropsWithChildren } from 'react'

import { inputAddonStyles, InputAddonStylesProps } from './InputAddon.styles'
import { useInputGroup } from './InputGroupContext'

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

export const InputAddon = forwardRef<HTMLDivElement, PropsWithChildren<InputAddonProps>>(
({ className, ...others }, ref) => {
const { intent, isHovered, isFocused } = useInputGroup()

return (
<div
ref={ref}
className={inputAddonStyles({ className, intent, isHovered, isFocused })}
{...others}
/>
)
}
)

InputAddon.displayName = 'InputAddon'
Empty file.
14 changes: 0 additions & 14 deletions packages/components/input/src/InputContainer.tsx

This file was deleted.

41 changes: 0 additions & 41 deletions packages/components/input/src/InputFieldset.styles.ts

This file was deleted.

26 changes: 0 additions & 26 deletions packages/components/input/src/InputFieldset.tsx

This file was deleted.

18 changes: 18 additions & 0 deletions packages/components/input/src/InputGroup.styles.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { cva, VariantProps } from 'class-variance-authority'

export const inputGroupStyles = cva(['inline-flex', 'outline-2', 'rounded-lg', 'outline-primary'], {
variants: {
isFocused: {
true: ['ring-1'],
false: [],
},
intent: {
neutral: ['ring-outline-high'],
success: ['ring-success'],
alert: ['ring-alert'],
error: ['ring-error'],
},
},
})

export type InputGroupStylesProps = VariantProps<typeof inputGroupStyles>
Loading

0 comments on commit 79ba6c3

Please sign in to comment.