Skip to content

Commit 7b6c254

Browse files
committed
- Checkbox rewrite to use normal input instead of radix to work with react-hook-form
- getPortal utility is not exported
1 parent 3f135f0 commit 7b6c254

File tree

10 files changed

+486
-502
lines changed

10 files changed

+486
-502
lines changed
Lines changed: 116 additions & 112 deletions
Original file line numberDiff line numberDiff line change
@@ -1,140 +1,144 @@
1-
import React, { CSSProperties, useState } from "react"
2-
import * as RCheckbox from "@radix-ui/react-checkbox"
1+
import React, {
2+
type InputHTMLAttributes,
3+
forwardRef,
4+
useState,
5+
type ForwardedRef,
6+
ReactNode,
7+
useImperativeHandle,
8+
useRef,
9+
useEffect,
10+
} from "react"
311
import { twJoin, twMerge } from "tailwind-merge"
412
import CheckboxIcon from "@atlaskit/icon/glyph/checkbox"
513
import CheckboxIndeterminateIcon from "@atlaskit/icon/glyph/checkbox-indeterminate"
614

715
const indeterminateState = "indeterminate" as const
816

9-
type CheckboxProps = {
10-
label: React.ReactChild
11-
id?: string
12-
value?: string
17+
type CheckboxProps2 = Omit<
18+
InputHTMLAttributes<HTMLInputElement>,
19+
"type" | "checked"
20+
> & {
21+
indeterminate?: boolean
1322
checked?: boolean | typeof indeterminateState
14-
title?: string
15-
defaultChecked?: boolean
16-
disabled?: boolean
23+
label?: ReactNode
1724
invalid?: boolean
18-
required?: boolean
19-
indeterminate?: boolean
20-
autoFocus?: boolean
21-
name?: string
22-
onChange?: (event: { target: { checked: boolean } }) => void
23-
onCheckedChange?: (checked: boolean | typeof indeterminateState) => void
24-
style?: CSSProperties
25-
className?: string
25+
onCheckedChange?: (checked: boolean) => void
2626
}
2727

2828
const checkBoxStyles =
29-
"box-border focus:border-selected-border mx-2 ease-linear transition duration-200 flex flex-none h-[14px] w-[14px] cursor-default items-center justify-center rounded-[3px] border-[2.5px] outline-none outline-0 outline-offset-0 focus:border-2 disabled:border-disabled invalid:border-danger-bold" as const
29+
"absolute left-0 box-border border-border focus:border-selected-border mr-2 ease-linear transition duration-150 flex flex-none h-[14px] w-[14px] cursor-default items-center justify-center rounded-[3px] border-[2.5px] outline-none outline-0 outline-offset-0 focus:border-2" as const
30+
31+
const disabledStyles = "cursor-not-allowed border-disabled" as const
3032

3133
const checkBoxCheckedStyles =
32-
"text-selected-text border-selected-border" as const
33-
const checkBoxUncheckedStyles = "border-border" as const
34+
"text-selected-text border-selected-border opacity-100" as const
35+
const checkBoxUncheckedStyles = "border-border text-transparent" as const
3436
const checkBoxInvalidStyles = "border-danger-border" as const
3537

38+
//#region label styles
3639
const disabledLabelStyles = "aria-disabled:text-disabled-text" as const
3740

3841
const requiredLabelStyles =
3942
"aria-required:after:content-['*'] aria-required:after:text-danger-bold aria-required:after:ml-0.5"
43+
//#endregion
4044

41-
function Checkbox({
42-
label,
43-
value,
44-
checked: checkedProp,
45-
defaultChecked,
46-
disabled,
47-
invalid,
48-
/**
49-
* indeterminate is a special case where the checkbox is neither checked nor unchecked. It replaces the checked state by a different icon.
50-
*/
51-
indeterminate,
52-
required,
53-
autoFocus,
54-
onChange,
55-
onCheckedChange,
56-
name,
57-
style,
58-
className,
59-
...props
60-
}: CheckboxProps) {
61-
const [checked, setChecked] = useState<boolean | typeof indeterminateState>(
62-
checkedProp ?? defaultChecked ?? false,
63-
)
45+
const Checkbox = forwardRef(
46+
(
47+
{
48+
id,
49+
className,
50+
style,
51+
label,
52+
disabled,
53+
required,
54+
checked: checkedProp,
55+
defaultChecked,
56+
indeterminate: indeterminateProp,
57+
invalid,
58+
onChange,
59+
onCheckedChange,
60+
...props
61+
}: CheckboxProps2,
62+
ref: ForwardedRef<HTMLInputElement>,
63+
) => {
64+
const [checked, setChecked] = useState(
65+
checkedProp ?? defaultChecked ?? false,
66+
)
6467

65-
if (indeterminate) {
66-
if (checkedProp !== undefined || checkedProp !== undefined) {
67-
if (checkedProp === true && checked !== indeterminateState) {
68-
setChecked(indeterminateState)
69-
} else if (!checkedProp && checked !== false) {
70-
setChecked(false)
71-
}
72-
}
73-
} else {
68+
const inputRef = useRef<HTMLInputElement>(null)
69+
// forward the local ref to the forwarded ref
70+
useImperativeHandle(ref, () => inputRef.current!)
71+
72+
// update from outside
7473
if (checkedProp !== undefined && checked !== checkedProp) {
75-
setChecked(checkedProp ?? false)
74+
setChecked(checkedProp)
7675
}
77-
}
7876

79-
return (
80-
<div className={twMerge("flex items-center", className)}>
81-
<RCheckbox.Root
82-
name={name}
83-
value={value}
77+
const indeterminate =
78+
indeterminateProp ?? checked === indeterminateState
79+
80+
// this needs to be in a useEffect to check if the input is checked after react rendered the component, else it will still have the old checked value
81+
// this is needed because the input is not controlled by react
82+
// eslint-disable-next-line react-hooks/exhaustive-deps
83+
useEffect(() => {
84+
const checked = inputRef.current?.checked
85+
setChecked(checked ?? false)
86+
})
87+
88+
return (
89+
<div
90+
className={twMerge(
91+
"relative flex items-center justify-start",
92+
className,
93+
)}
8494
style={style}
85-
checked={checked}
86-
disabled={disabled}
87-
required={required}
88-
aria-invalid={invalid}
89-
autoFocus={autoFocus}
90-
onCheckedChange={(e: RCheckbox.CheckedState) => {
91-
onCheckedChange?.(e)
92-
if (checked === indeterminateState) {
93-
if (e === true || e === indeterminateState) {
94-
setChecked(false)
95-
onChange?.({ target: { checked: false } })
96-
return
97-
}
98-
} else {
99-
if (indeterminate && e === true) {
100-
setChecked(indeterminateState)
101-
} else {
102-
setChecked(e)
103-
}
104-
onChange?.({ target: { checked: !!e } })
105-
}
106-
}}
107-
className={`${twMerge(
108-
checkBoxStyles,
109-
invalid ? checkBoxInvalidStyles : undefined,
110-
)} ${
111-
checked ? checkBoxCheckedStyles : checkBoxUncheckedStyles
112-
} `}
113-
{...props}
11495
>
115-
<RCheckbox.Indicator className="relative box-border flex h-4 w-4 flex-none items-center justify-center">
116-
{typeof checked === "boolean" && checked === true && (
117-
<div className="absolute inset-0 flex items-center justify-center">
118-
<CheckboxIcon label="" />
119-
</div>
120-
)}
121-
{checked === indeterminateState && (
122-
<div className="absolute inset-0 flex items-center justify-center">
123-
<CheckboxIndeterminateIcon label="" />
124-
</div>
96+
<div
97+
className={`${twMerge(
98+
checkBoxStyles,
99+
invalid ? checkBoxInvalidStyles : undefined,
100+
disabled ? disabledStyles : undefined,
101+
)} ${
102+
checked
103+
? checkBoxCheckedStyles
104+
: checkBoxUncheckedStyles
105+
} `}
106+
>
107+
{!indeterminate ? (
108+
<CheckboxIcon label="" />
109+
) : (
110+
<CheckboxIndeterminateIcon label="" />
125111
)}
126-
</RCheckbox.Indicator>
127-
</RCheckbox.Root>
128-
<label
129-
aria-disabled={disabled}
130-
aria-required={required}
131-
className={twJoin(disabledLabelStyles, requiredLabelStyles)}
132-
>
133-
{label}
134-
</label>
135-
</div>
136-
)
137-
}
112+
</div>
113+
<input
114+
type="checkbox"
115+
id={id}
116+
ref={inputRef}
117+
disabled={disabled}
118+
checked={!!checked}
119+
required={required}
120+
className={"mr-2 opacity-0"}
121+
onChange={(e) => {
122+
onCheckedChange?.(e.target.checked)
123+
onChange?.(e)
124+
setChecked(e.target.checked)
125+
}}
126+
{...props}
127+
/>
128+
129+
<label
130+
htmlFor={id}
131+
aria-disabled={disabled}
132+
aria-required={required}
133+
aria-invalid={invalid}
134+
className={twJoin(disabledLabelStyles, requiredLabelStyles)}
135+
>
136+
{label}
137+
</label>
138+
</div>
139+
)
140+
},
141+
)
142+
Checkbox.displayName = "Checkbox"
138143

139-
const memoizedCheckbox = React.memo(Checkbox)
140-
export { memoizedCheckbox as Checkbox }
144+
export { Checkbox }

library/src/components/Modal.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ function Container({
5757
{trigger && <RDialog.Trigger>{trigger}</RDialog.Trigger>}
5858

5959
{usePortal ? (
60-
<RDialog.Portal container={getPortal()}>
60+
<RDialog.Portal container={getPortal("uikts-modal")}>
6161
{content}
6262
</RDialog.Portal>
6363
) : (

library/src/components/inputs/Select.tsx

Lines changed: 24 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -82,31 +82,35 @@ const Select = forwardRef(
8282
) => {
8383
const items = useMemo(() => {
8484
if (Array.isArray(options)) {
85-
const items = options.map((option) => (
86-
<SelectItem
87-
value={option.value}
88-
key={option.label}
89-
disabled={option.disabled}
90-
>
91-
{option.label}
92-
</SelectItem>
93-
))
94-
return <RSelect.Group>{items}</RSelect.Group>
95-
}
96-
return Object.entries(options).map(([groupName, options]) => (
97-
<RSelect.Group key={groupName}>
98-
<RSelect.Label className={selectGroupLabelStyles}>
99-
{groupName}
100-
</RSelect.Label>
101-
{options.map((option) => (
85+
const items = options.map((option) => {
86+
return (
10287
<SelectItem
10388
value={option.value}
10489
key={option.label}
10590
disabled={option.disabled}
10691
>
10792
{option.label}
10893
</SelectItem>
109-
))}
94+
)
95+
})
96+
return <RSelect.Group>{items}</RSelect.Group>
97+
}
98+
return Object.entries(options).map(([groupName, options]) => (
99+
<RSelect.Group key={groupName}>
100+
<RSelect.Label className={selectGroupLabelStyles}>
101+
{groupName}
102+
</RSelect.Label>
103+
{options.map((option) => {
104+
return (
105+
<SelectItem
106+
value={option.value}
107+
key={option.label}
108+
disabled={option.disabled}
109+
>
110+
{option.label}
111+
</SelectItem>
112+
)
113+
})}
110114
</RSelect.Group>
111115
))
112116
}, [options])
@@ -156,13 +160,14 @@ const Select = forwardRef(
156160
aria-required={required}
157161
>
158162
<RSelect.Value ref={ref} placeholder={placeholder} />
163+
159164
<RSelect.Icon className="flex items-center justify-center">
160165
<ChevronDownIcon label="open select" />
161166
</RSelect.Icon>
162167
</RSelect.Trigger>
163168

164169
{usePortal ? (
165-
<RSelect.Portal container={getPortal()}>
170+
<RSelect.Portal container={getPortal("uikts-select")}>
166171
{content}
167172
</RSelect.Portal>
168173
) : (
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import React from "react"
2+
import { default as RSelect, type Props } from "react-select"
3+
4+
export function Select(props: Props) {
5+
return <RSelect {...props} />
6+
}

library/src/utils/getPortal.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
export const portalContainerID = "#uikts-portal" as const
22

3-
export function getPortal() {
3+
export function getPortal(insidePortalContainerID: string) {
44
let portalNode = document.getElementById(portalContainerID)
55
if (!portalNode) {
66
portalNode = document.createElement("div")
@@ -10,5 +10,13 @@ export function getPortal() {
1010
const body = document.getElementsByTagName("body")[0]
1111
body.appendChild(portalNode)
1212
}
13-
return portalNode
13+
let insidePortalNode = portalNode.querySelector(
14+
"#" + insidePortalContainerID,
15+
) as HTMLElement | null
16+
if (!insidePortalNode) {
17+
insidePortalNode = document.createElement("div")
18+
insidePortalNode.setAttribute("id", insidePortalContainerID)
19+
portalNode.appendChild(insidePortalNode)
20+
}
21+
return insidePortalNode
1422
}

library/src/utils/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
import { debounceHelper, useDebounceHelper } from "./debounce"
22
import { rateLimitHelper, useRateLimitHelper } from "./rateLimit"
3+
import { getPortal } from "./getPortal"
34

45
export {
56
rateLimitHelper,
67
useRateLimitHelper,
78
debounceHelper,
89
useDebounceHelper,
10+
getPortal,
911
}

0 commit comments

Comments
 (0)