Skip to content

Commit

Permalink
Add Input as TextField replacement, and a Select as Select replacemen…
Browse files Browse the repository at this point in the history
…t (not as flexible currently, but compatible with react-hook-form)
  • Loading branch information
marcus-wishes committed Dec 6, 2023
1 parent 87e46c2 commit 7e2574a
Show file tree
Hide file tree
Showing 10 changed files with 396 additions and 9 deletions.
1 change: 1 addition & 0 deletions library/src/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,4 @@ export * from "./Toggle"
export * from "./sidebar"
export * from "./filters"
export * from "./Pagination"
export * from "./inputs"
64 changes: 64 additions & 0 deletions library/src/components/inputs/Inputs.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import React, {
forwardRef,
type ComponentPropsWithoutRef,
ForwardedRef,
} from "react"
import { twJoin, twMerge } from "tailwind-merge"

//#region Label
const labelNormalStyles = "text-text-subtlest text-xs font-semibold"
const requiredStyles =
"aria-required:after:content-['*'] aria-required:after:text-danger-bold aria-required:after:ml-0.5"
const invalidStyles = "aria-invalid:text-danger-text"

const labelStyles = twJoin(labelNormalStyles, requiredStyles, invalidStyles)
export function Label({
required = false,
className,
...props
}: ComponentPropsWithoutRef<"label"> & { required?: boolean }) {
return (
<label
aria-required={required}
className={twMerge(labelStyles, className)}
{...props}
/>
)
}

//#endregion

//#region Input
const inputActiveStyles =
"p-2 rounded border border-input-border bg-input ease-in-out transition duration-200"
const inputFocusStyles =
"focus:border-selected-bold focus:bg-input-active outline-none hover:bg-input-hovered"
const inputDisabledStyles =
"disabled:bg-disabled disabled:text-disabled-text disabled:cursor-not-allowed disabled:border-transparent"
const invalidInputStyles = "aria-invalid:border-danger-border"

const inputStyles = twJoin(
inputActiveStyles,
inputFocusStyles,
inputDisabledStyles,
invalidInputStyles,
)

const Input = forwardRef(
(
{ className, ...props }: ComponentPropsWithoutRef<"input">,
ref: ForwardedRef<HTMLInputElement>,
) => {
return (
<input
ref={ref}
className={twMerge(inputStyles, className)}
{...props}
/>
)
},
)
Input.displayName = "Input"

export { Input }
//#endregion
168 changes: 168 additions & 0 deletions library/src/components/inputs/Select.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
import React, {
CSSProperties,
ForwardedRef,
forwardRef,
useCallback,
useMemo,
} from "react"
import * as RSelect from "@radix-ui/react-select"
import { twJoin, twMerge } from "tailwind-merge"

import ChevronDownIcon from "@atlaskit/icon/glyph/chevron-down"

type SelectOption = {
label: string
value: string
disabled?: boolean
}

type SelectProps = {
defaultOpen?: boolean
open?: boolean
defaultValue?: string
value?: string
placeholder?: string
required?: boolean
disabled?: boolean
ref?: React.Ref<HTMLButtonElement>
onChange?: (event: { target: { value: string } }) => void
onValueChange?: (selectedValue: string) => void
options: SelectOption[] | { [groupName: string]: SelectOption[] }
className?: string
style?: CSSProperties
}

const selectNormalStyles =
"p-2 text-left bg-input-active rounded border border-input-border ease-in-out transition duration-200 flex items-center justify-between w-full"
const selectFocusStyles =
"focus:border-selected-bold focus:bg-input-active outline-none hover:bg-input-hovered"
const selectDisabledStyles =
"disabled:bg-disabled disabled:text-disabled-text disabled:cursor-not-allowed disabled:border-transparent"
const selectStyles = twJoin(
selectNormalStyles,
selectFocusStyles,
selectDisabledStyles,
)

const selectGroupLabelStyles =
"text-text-subtlest text-2xs font-[500] uppercase pt-4 pb-0.5 px-4" as const

const Select = forwardRef(
(
{
defaultOpen,
open,
defaultValue,
placeholder,
value,
required,
disabled,
onChange,
onValueChange,
options,
className,
style,
...props
}: SelectProps,
ref: ForwardedRef<HTMLButtonElement>,
) => {
const items = useMemo(() => {
if (Array.isArray(options)) {
const items = options.map((option) => (
<SelectItem
value={option.value}
key={option.label}
disabled={option.disabled}
>
{option.label}
</SelectItem>
))
return <RSelect.Group>{items}</RSelect.Group>
}
return Object.entries(options).map(([groupName, options]) => (
<RSelect.Group key={groupName}>
<RSelect.Label className={selectGroupLabelStyles}>
{groupName}
</RSelect.Label>
{options.map((option) => (
<SelectItem
value={option.value}
key={option.label}
disabled={option.disabled}
>
{option.label}
</SelectItem>
))}
</RSelect.Group>
))
}, [options])

const onValueChangeCB = useCallback(
(newValue: string) => {
onChange?.({ target: { value: newValue } })
onValueChange?.(newValue)
},
[onChange, onValueChange],
)

return (
<RSelect.Root
onValueChange={onValueChangeCB}
open={open}
defaultOpen={defaultOpen}
defaultValue={defaultValue}
value={value}
required={required}
disabled={disabled}
{...props}
>
<RSelect.Trigger
className={twMerge(selectStyles, className)}
style={style}
disabled={disabled}
aria-required={required}
ref={ref}
>
<RSelect.Value placeholder={placeholder} />
<RSelect.Icon className="flex items-center justify-center">
<ChevronDownIcon label="open select" />
</RSelect.Icon>
</RSelect.Trigger>

<RSelect.Content className="bg-surface-raised shadow-overlay py-2">
{items}
</RSelect.Content>
</RSelect.Root>
)
},
)
Select.displayName = "Select"
export { Select }

const normalStyles =
"px-4 normal-case font-normal text-sm text-text py-1.5 outline-none border-l-[2.5px] border-l-transparent cursor-pointer" as const
const hoverStyles =
"hover:bg-surface-overlay-hovered active:bg-surface-overlay-pressed hover:border-l-selected-bold" as const
const selectedStyles =
"data-[state=checked]:bg-selected-subtle data-[state=checked]:hover:bg-selected-subtle-hovered data-[state=checked]:active:bg-selected-subtle-pressed data-[state=checked]:border-l-selected-bold" as const

const selectItemStyle = twJoin(normalStyles, hoverStyles, selectedStyles)

const SelectItem = React.forwardRef(
(
{ children, className, ...props }: RSelect.SelectItemProps,
forwardedRef: ForwardedRef<HTMLDivElement>,
) => {
return (
<RSelect.Item
//disabled={isDisabled}
className={twMerge(selectItemStyle, className)}
{...props}
ref={forwardedRef}
>
<RSelect.ItemText>{children}</RSelect.ItemText>
</RSelect.Item>
)
},
)
SelectItem.displayName = "SelectItem"
2 changes: 2 additions & 0 deletions library/src/components/inputs/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { Input, Label } from "./Inputs"
export { Select } from "./Select"
75 changes: 75 additions & 0 deletions package-lock.json

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

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@
"@radix-ui/react-collapsible": "^1.0.3",
"@radix-ui/react-dropdown-menu": "^2.0.6",
"@radix-ui/react-icons": "^1.3.0",
"@radix-ui/react-select": "^2.0.0",
"@radix-ui/react-switch": "^1.0.3",
"@radix-ui/react-tabs": "^1.0.4",
"dayjs": "^1.11.10",
Expand Down
8 changes: 4 additions & 4 deletions showcase/public/showcase-sources.txt
Original file line number Diff line number Diff line change
Expand Up @@ -1022,20 +1022,20 @@ function CheckboxShowcase(props: ShowcaseProps) {
<div style={{ display: "flex", gap: "1rem" }}>
<Checkbox
label="controlled"
isChecked={isCheckboxActive}
checked={isCheckboxActive}
onCheckedChange={setIsCheckboxActive}
/>
<Checkbox label="uncontrolled" defaultChecked />
<Checkbox label="disabled" isDisabled />
<Checkbox label="disabled" disabled />
<Checkbox label="invalid" isInvalid />
<Checkbox
label="indeterminate"
isIndeterminate
isChecked={isCheckboxActive}
checked={isCheckboxActive}
onCheckedChange={setIsCheckboxActive}
/>
<Checkbox label="indeterminate uncontrolled" isIndeterminate />
<Checkbox label="required" isIndeterminate isRequired />
<Checkbox label="required" isIndeterminate required />
</div>
)
//#endregion checkbox
Expand Down
Loading

0 comments on commit 7e2574a

Please sign in to comment.