Skip to content

Commit fb63c4a

Browse files
authored
feat: implement custom pin-input component (#2)
* feat: add breadcrumb component * feat: add pin-input component * feat: add controlled and uncontrolled props Changes made: - add value, onChange props for controlled PinInput - add defaultValue for uncontrolled PinInput - add onComplete for completed action - add disabled and readOnly props * feat: add autoFocus option * feat: add ariaLabel for a11y * feat: add validation in handlePaste * fix: update focus logic when paste Update focus logic to the following - if pasted value is less than input length, focus on the next input - if not, focus on the last input * refactor: add comments for props and remove unused codes * feat: add className prop for input container * refactor: update length type to be 1 ~ 12 instead of number * refactor: extract pin-input logics into a custom hook * fix: add id in pin-input for label * feat: add modular pin-input component * tempo: add custom component page * feat: add onIncomplete function * feat: update component props for type inference * refactor: rename/replace pin-input with modular new pin-input * feat: add extra components page
1 parent 8b47f6c commit fb63c4a

File tree

8 files changed

+1234
-0
lines changed

8 files changed

+1234
-0
lines changed

package.json

+2
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
"react-dom": "^18.2.0",
4242
"react-hook-form": "^7.49.3",
4343
"react-router-dom": "^6.21.3",
44+
"react-syntax-highlighter": "^15.5.0",
4445
"recharts": "^2.11.0",
4546
"tailwind-merge": "^2.2.1",
4647
"tailwindcss-animate": "^1.0.7",
@@ -50,6 +51,7 @@
5051
"@types/node": "^20.11.6",
5152
"@types/react": "^18.2.43",
5253
"@types/react-dom": "^18.2.17",
54+
"@types/react-syntax-highlighter": "^15.5.11",
5355
"@typescript-eslint/eslint-plugin": "^6.14.0",
5456
"@typescript-eslint/parser": "^6.14.0",
5557
"@vitejs/plugin-react-swc": "^3.5.0",

pnpm-lock.yaml

+150
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/components/custom/breadcrumb.tsx

+85
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import * as React from 'react'
2+
import { cn } from '@/lib/utils'
3+
4+
interface BreadcrumbProps extends React.ComponentPropsWithoutRef<'nav'> {
5+
children:
6+
| React.ReactElement<typeof BreadcrumbItem>
7+
| React.ReactElement<typeof BreadcrumbItem>[]
8+
separator?: React.ReactNode
9+
}
10+
11+
const BreadcrumbContext = React.createContext<boolean>(false)
12+
13+
const Breadcrumb = React.forwardRef<HTMLElement, BreadcrumbProps>(
14+
({ className, children, separator, ...props }, ref) => {
15+
const validChildren = getValidChildren(children)
16+
17+
const count = validChildren.length
18+
19+
const clones = validChildren.map((child, index) =>
20+
React.cloneElement(child, {
21+
separator,
22+
isLastChild: count === index + 1,
23+
})
24+
)
25+
26+
return (
27+
<BreadcrumbContext.Provider value={true}>
28+
<nav ref={ref} aria-label='breadcrumb' className={className} {...props}>
29+
<ol className={cn(`flex`)}>{clones}</ol>
30+
</nav>
31+
</BreadcrumbContext.Provider>
32+
)
33+
}
34+
)
35+
Breadcrumb.displayName = 'Breadcrumb'
36+
37+
interface InternalBreadcrumbItemProps {
38+
separator?: React.ReactNode
39+
isLastChild: boolean
40+
}
41+
42+
interface BreadcrumbItemProps
43+
extends Omit<
44+
React.ComponentPropsWithoutRef<'li'>,
45+
keyof InternalBreadcrumbItemProps
46+
> {}
47+
48+
const BreadcrumbItem = React.forwardRef<HTMLLIElement, BreadcrumbItemProps>(
49+
({ className, children, ...props }, ref) => {
50+
const { separator, isLastChild, ...rest } =
51+
props as InternalBreadcrumbItemProps
52+
53+
// Check if BreadcrumbItem is used within Breadcrumb
54+
const isInsideBreadcrumb = React.useContext(BreadcrumbContext)
55+
if (!isInsideBreadcrumb) {
56+
throw new Error(
57+
`${BreadcrumbItem.displayName} must be used within ${Breadcrumb.displayName}.`
58+
)
59+
}
60+
61+
return (
62+
<li ref={ref} className={cn(`group`, className)} {...rest}>
63+
{children}
64+
{!isLastChild && (
65+
<span className='mx-2 *:!inline-block'>{separator ?? '/'}</span>
66+
)}
67+
</li>
68+
)
69+
}
70+
)
71+
BreadcrumbItem.displayName = 'BreadcrumbItem'
72+
73+
/* ========== Util Func ========== */
74+
75+
const getValidChildren = (children: React.ReactNode) =>
76+
React.Children.toArray(children).filter((child) => {
77+
if (React.isValidElement(child) && child.type === BreadcrumbItem) {
78+
return React.isValidElement(child)
79+
}
80+
throw new Error(
81+
`${Breadcrumb.displayName} can only have ${BreadcrumbItem.displayName} as children.`
82+
)
83+
}) as React.ReactElement[]
84+
85+
export { Breadcrumb, BreadcrumbItem }

0 commit comments

Comments
 (0)