From 79ba6c3897c12cfe7298000a3e553245de3f54d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20Alvarez=20Sordo?= Date: Sat, 27 May 2023 02:46:47 +0200 Subject: [PATCH] feat(input): add group and addons --- .../components/input/src/Input.stories.tsx | 14 ++- .../components/input/src/Input.styles.tsx | 40 +++---- packages/components/input/src/Input.tsx | 101 +++++++----------- .../components/input/src/InputAddon.styles.ts | 34 ++++++ packages/components/input/src/InputAddon.tsx | 24 +++++ .../input/src/InputContainer.styles.ts | 0 .../components/input/src/InputContainer.tsx | 14 --- .../input/src/InputFieldset.styles.ts | 41 ------- .../components/input/src/InputFieldset.tsx | 26 ----- .../input/src/InputGroup.styles.tsx | 18 ++++ packages/components/input/src/InputGroup.tsx | 82 ++++++++++++++ .../components/input/src/InputGroupContext.ts | 18 ++++ packages/components/input/src/InputIcon.tsx | 11 ++ .../components/input/src/InputLeftAddon.tsx | 13 +++ .../components/input/src/InputPrimitive.tsx | 0 .../components/input/src/InputRighAddon.tsx | 13 +++ 16 files changed, 281 insertions(+), 168 deletions(-) create mode 100644 packages/components/input/src/InputAddon.styles.ts create mode 100644 packages/components/input/src/InputAddon.tsx delete mode 100644 packages/components/input/src/InputContainer.styles.ts delete mode 100644 packages/components/input/src/InputContainer.tsx delete mode 100644 packages/components/input/src/InputFieldset.styles.ts delete mode 100644 packages/components/input/src/InputFieldset.tsx create mode 100644 packages/components/input/src/InputGroup.styles.tsx create mode 100644 packages/components/input/src/InputGroup.tsx create mode 100644 packages/components/input/src/InputGroupContext.ts create mode 100644 packages/components/input/src/InputIcon.tsx create mode 100644 packages/components/input/src/InputLeftAddon.tsx delete mode 100644 packages/components/input/src/InputPrimitive.tsx create mode 100644 packages/components/input/src/InputRighAddon.tsx diff --git a/packages/components/input/src/Input.stories.tsx b/packages/components/input/src/Input.stories.tsx index 9b413dfa2..a4f9bf452 100644 --- a/packages/components/input/src/Input.stories.tsx +++ b/packages/components/input/src/Input.stories.tsx @@ -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 = { title: 'Components/Input', @@ -11,7 +14,16 @@ export default meta export const Default: StoryFn = _args => ( <> + + https:// + + + + .com + + - rewr + + ) diff --git a/packages/components/input/src/Input.styles.tsx b/packages/components/input/src/Input.styles.tsx index 06aa0d471..5895422e4 100644 --- a/packages/components/input/src/Input.styles.tsx +++ b/packages/components/input/src/Input.styles.tsx @@ -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'], }, }, } diff --git a/packages/components/input/src/Input.tsx b/packages/components/input/src/Input.tsx index 7b552741c..d5a1d8dea 100644 --- a/packages/components/input/src/Input.tsx +++ b/packages/components/input/src/Input.tsx @@ -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>( - ( - { - 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) => { + if (onFocus) { + onFocus(event) + } + + if (group.onFocus) { + group.onFocus() + } } - const handleBlur = () => { - setIsFocused(false) - } + const handleBlur = (event: FocusEvent) => { + if (onBlur) { + onBlur(event) + } - const handleChange = (event: ChangeEvent) => { - setValue(event.target.value) + if (group.onBlur) { + group.onBlur() + } } return ( -
- {leftAddon} - - - - - - {rightAddon} -
+ ) } ) + +Input.displayName = 'Input' diff --git a/packages/components/input/src/InputAddon.styles.ts b/packages/components/input/src/InputAddon.styles.ts new file mode 100644 index 000000000..b882141e2 --- /dev/null +++ b/packages/components/input/src/InputAddon.styles.ts @@ -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 diff --git a/packages/components/input/src/InputAddon.tsx b/packages/components/input/src/InputAddon.tsx new file mode 100644 index 000000000..fd8910fbb --- /dev/null +++ b/packages/components/input/src/InputAddon.tsx @@ -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 {} + +export const InputAddon = forwardRef>( + ({ className, ...others }, ref) => { + const { intent, isHovered, isFocused } = useInputGroup() + + return ( +
+ ) + } +) + +InputAddon.displayName = 'InputAddon' diff --git a/packages/components/input/src/InputContainer.styles.ts b/packages/components/input/src/InputContainer.styles.ts deleted file mode 100644 index e69de29bb..000000000 diff --git a/packages/components/input/src/InputContainer.tsx b/packages/components/input/src/InputContainer.tsx deleted file mode 100644 index 7f56497c0..000000000 --- a/packages/components/input/src/InputContainer.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import { cx } from 'class-variance-authority' -import { ComponentPropsWithoutRef, forwardRef, PropsWithChildren } from 'react' - -import { inputStyles } from './Input.styles' - -export interface InputProps extends ComponentPropsWithoutRef<'div'> { - isFocused?: boolean -} - -export const InputContainer = forwardRef>( - ({ className, isFocused, ...others }, ref) => { - return
- } -) diff --git a/packages/components/input/src/InputFieldset.styles.ts b/packages/components/input/src/InputFieldset.styles.ts deleted file mode 100644 index 3e2147658..000000000 --- a/packages/components/input/src/InputFieldset.styles.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { cva, type VariantProps } from 'class-variance-authority' - -export const inputFieldsetStyles = cva([ - 'absolute', - 'w-full', - 'h-full', - 'pointer-events-none', - 'rounded-lg', - 'border-sm', - 'border-outline', - 'hover:border-outline-high', - 'left-none', - 'top-none', - 'px-lg', - 'block', -]) - -export const inputFieldsetLegendStyles = cva( - [ - 'text-body-2', - 'transition-all', - 'duration-100', - 'text-transparent', - 'h-none', - 'overflow-hidden', - 'py-none', - 'px-md', - ], - { - variants: { - isExpanded: { - true: ['block'], - false: ['hidden'], - }, - }, - } -) - -export const inputFieldsetLegendMandatoryStyles = cva(['text-caption']) - -export type InputFieldsetStylesProps = VariantProps diff --git a/packages/components/input/src/InputFieldset.tsx b/packages/components/input/src/InputFieldset.tsx deleted file mode 100644 index da59a5a23..000000000 --- a/packages/components/input/src/InputFieldset.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import React, { ReactNode } from 'react' - -import { type InputProps } from './Input' -import { - inputFieldsetLegendMandatoryStyles, - inputFieldsetLegendStyles, - inputFieldsetStyles, - type InputFieldsetStylesProps, -} from './InputFieldset.styles' - -export interface InputFieldsetProps extends InputFieldsetStylesProps { - mandatory?: boolean | string - placeholder?: string - label: ReactNode - isExpanded?: boolean -} - -export const InputFieldset = ({ label, mandatory, isExpanded = true }: InputFieldsetProps) => { - return ( - - ) -} - -InputFieldset.displayName = 'InputFieldset' diff --git a/packages/components/input/src/InputGroup.styles.tsx b/packages/components/input/src/InputGroup.styles.tsx new file mode 100644 index 000000000..522d9c1f8 --- /dev/null +++ b/packages/components/input/src/InputGroup.styles.tsx @@ -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 diff --git a/packages/components/input/src/InputGroup.tsx b/packages/components/input/src/InputGroup.tsx new file mode 100644 index 000000000..398f8e11e --- /dev/null +++ b/packages/components/input/src/InputGroup.tsx @@ -0,0 +1,82 @@ +import { + Children, + ComponentPropsWithoutRef, + forwardRef, + isValidElement, + PropsWithChildren, + ReactElement, + useMemo, + useState, +} from 'react' + +import { Input } from './Input' +import { inputGroupStyles, InputGroupStylesProps } from './InputGroup.styles' +import { InputGroupContext } from './InputGroupContext' +import { InputLeftAddon } from './InputLeftAddon' +import { InputRightAddon } from './InputRighAddon' + +export interface InputGroupProps extends ComponentPropsWithoutRef<'div'>, InputGroupStylesProps {} + +export const InputGroup = forwardRef>( + ({ className, children: childrenProp, intent = 'neutral', ...others }, ref) => { + const [isFocused, setIsFocused] = useState(false) + const [isHovered, setIsHovered] = useState(false) + + const children = Children.toArray(childrenProp).filter(isValidElement) + const input = children.find((child: ReactElement) => child.type === Input) + const rightAddon = children.find((child: ReactElement) => child.type === InputRightAddon) + const leftAddon = children.find((child: ReactElement) => child.type === InputLeftAddon) + const isLeftAddonVisible = !!leftAddon + const isRightAddonVisible = !!rightAddon + + const value = useMemo(() => { + const handleFocus = () => { + setIsFocused(true) + } + + const handleBlur = () => { + setIsFocused(false) + } + + return { + intent, + isHovered, + isFocused, + isLeftAddonVisible, + isRightAddonVisible, + onFocus: handleFocus, + onBlur: handleBlur, + } + }, [intent, isHovered, isFocused, isLeftAddonVisible, isRightAddonVisible]) + + const handleMouseEnter = () => { + setIsHovered(true) + } + + const handleMouseLeave = () => { + setIsHovered(false) + } + + return ( + +
+ {leftAddon} + {input} + {rightAddon} +
+
+ ) + } +) + +InputGroup.displayName = 'InputGroup' diff --git a/packages/components/input/src/InputGroupContext.ts b/packages/components/input/src/InputGroupContext.ts new file mode 100644 index 000000000..d2b80f8c3 --- /dev/null +++ b/packages/components/input/src/InputGroupContext.ts @@ -0,0 +1,18 @@ +import { createContext, useContext } from 'react' + +import { InputGroupProps } from './InputGroup' + +export interface InputGroupContext extends Pick { + isHovered: boolean + isFocused: boolean + isLeftAddonVisible: boolean + isRightAddonVisible: boolean + onFocus: () => void + onBlur: () => void +} + +export const InputGroupContext = createContext>({}) + +export const useInputGroup = () => { + return useContext(InputGroupContext) +} diff --git a/packages/components/input/src/InputIcon.tsx b/packages/components/input/src/InputIcon.tsx new file mode 100644 index 000000000..36fb86fd5 --- /dev/null +++ b/packages/components/input/src/InputIcon.tsx @@ -0,0 +1,11 @@ +import { ComponentPropsWithoutRef, forwardRef, PropsWithChildren } from 'react' + +export type InputIconProps = ComponentPropsWithoutRef<'div'> + +export const InputIcon = forwardRef>( + ({ className, children, ...others }, ref) => { + return
+ } +) + +InputIcon.displayName = 'InputIcon' diff --git a/packages/components/input/src/InputLeftAddon.tsx b/packages/components/input/src/InputLeftAddon.tsx new file mode 100644 index 000000000..f594cdd2e --- /dev/null +++ b/packages/components/input/src/InputLeftAddon.tsx @@ -0,0 +1,13 @@ +import { forwardRef } from 'react' + +import { InputAddon, InputAddonProps } from './InputAddon' + +export type InputLeftAddonProps = InputAddonProps + +export const InputLeftAddon = forwardRef( + ({ className, ...others }, ref) => { + return + } +) + +InputLeftAddon.displayName = 'InputLeftAddon' diff --git a/packages/components/input/src/InputPrimitive.tsx b/packages/components/input/src/InputPrimitive.tsx deleted file mode 100644 index e69de29bb..000000000 diff --git a/packages/components/input/src/InputRighAddon.tsx b/packages/components/input/src/InputRighAddon.tsx new file mode 100644 index 000000000..c212b69e9 --- /dev/null +++ b/packages/components/input/src/InputRighAddon.tsx @@ -0,0 +1,13 @@ +import { forwardRef } from 'react' + +import { InputAddon, InputAddonProps } from './InputAddon' + +export type InputRightAddonProps = InputAddonProps + +export const InputRightAddon = forwardRef( + ({ className, ...others }, ref) => { + return + } +) + +InputRightAddon.displayName = 'InputRightAddon'