-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #147 from 2wheeh/feature/issue-127/user-chip
[#127] Add UserChip Component
- Loading branch information
Showing
5 changed files
with
204 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
'use client'; | ||
|
||
import Text from '@/components/atomic/text'; | ||
import { useOutsideClick } from '@/hooks/use-outside-click'; | ||
import { useRef, useState } from 'react'; | ||
|
||
export default function UserChip({ nickname, tag }: { nickname: string; tag: string }) { | ||
const [isDropdownOpen, setDropdownOpen] = useState(false); | ||
const ref = useRef<HTMLDivElement>(null); | ||
|
||
const handleClick = () => setDropdownOpen((prev) => !prev); | ||
useOutsideClick({ ref, handler: () => setDropdownOpen(false) }); | ||
|
||
return ( | ||
<div ref={ref} className="relative"> | ||
<div | ||
className="flex w-fit items-center rounded bg-gray-100 px-1 hover:bg-gray-200" | ||
onClick={handleClick} | ||
> | ||
<Text weight="medium" nowrap> | ||
{nickname} | ||
</Text> | ||
<Text size="sm" weight="light" nowrap> | ||
{tag} | ||
</Text> | ||
</div> | ||
{isDropdownOpen && ( | ||
// TODO: replace with a normalized Dropdown with a proper event handler | ||
<div className="absolute right-0 top-7 flex items-center rounded-lg border bg-white px-2 py-1 opacity-70 hover:bg-gray-200"> | ||
<Text color="black" size="sm"> | ||
작성글 보기 | ||
</Text> | ||
</div> | ||
)} | ||
</div> | ||
); | ||
} |
105 changes: 105 additions & 0 deletions
105
ui/packages/mr-c.app/src/hooks/use-outside-click.test.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,105 @@ | ||
import { useRef } from 'react'; | ||
import { useOutsideClick } from '@/hooks/use-outside-click'; | ||
import { cleanup, click, clickRight, pointerDown, pointerUp, render } from '@/lib/test-utils'; | ||
|
||
function OutsideClicker({ onOutsideClick }: { onOutsideClick: () => void }) { | ||
const ref = useRef<HTMLDivElement>(null); | ||
|
||
useOutsideClick({ | ||
ref, | ||
handler: onOutsideClick, | ||
}); | ||
|
||
return ( | ||
<> | ||
<div ref={ref}>Element</div> | ||
<div>Outside</div> | ||
</> | ||
); | ||
} | ||
|
||
describe('useOutsideClick', () => { | ||
let outsideClickCount: number; | ||
let element: HTMLElement; | ||
let outsideElement: HTMLElement; | ||
|
||
beforeEach(() => { | ||
outsideClickCount = 0; | ||
}); | ||
|
||
beforeAll(() => { | ||
const { getByText } = render(<OutsideClicker onOutsideClick={() => outsideClickCount++} />); | ||
|
||
element = getByText('Element'); | ||
outsideElement = getByText('Outside'); | ||
}); | ||
|
||
afterAll(() => { | ||
cleanup(); | ||
}); | ||
|
||
test('should register clicks on other elements, the body, and the document', () => { | ||
expect(outsideClickCount).toEqual(0); | ||
|
||
click(element); | ||
expect(outsideClickCount).toEqual(0); | ||
|
||
click(outsideElement); | ||
expect(outsideClickCount).toEqual(1); | ||
|
||
click(document); | ||
expect(outsideClickCount).toEqual(2); | ||
|
||
click(document.body); | ||
expect(outsideClickCount).toEqual(3); | ||
}); | ||
|
||
test('should register right clicks on other elements, the body, and the document', () => { | ||
expect(outsideClickCount).toEqual(0); | ||
|
||
clickRight(element); | ||
expect(outsideClickCount).toEqual(0); | ||
|
||
clickRight(outsideElement); | ||
expect(outsideClickCount).toEqual(1); | ||
|
||
clickRight(document); | ||
expect(outsideClickCount).toEqual(2); | ||
|
||
clickRight(document.body); | ||
expect(outsideClickCount).toEqual(3); | ||
}); | ||
|
||
test('should fire once on one click sequnce: mouse Down -> Up', () => { | ||
expect(outsideClickCount).toEqual(0); | ||
|
||
pointerDown(outsideElement); | ||
expect(outsideClickCount).toEqual(0); | ||
pointerUp(outsideElement); | ||
expect(outsideClickCount).toEqual(1); | ||
|
||
pointerDown(outsideElement); | ||
pointerDown(outsideElement); | ||
expect(outsideClickCount).toEqual(1); | ||
|
||
pointerUp(outsideElement); | ||
pointerUp(outsideElement); | ||
expect(outsideClickCount).toEqual(2); | ||
}); | ||
|
||
test('should fire for the click that both mouse Down/Up happen outside', () => { | ||
expect(outsideClickCount).toEqual(0); | ||
|
||
pointerDown(element); | ||
pointerUp(outsideElement); | ||
expect(outsideClickCount).toEqual(0); | ||
|
||
pointerDown(outsideElement); | ||
pointerUp(element); | ||
expect(outsideClickCount).toEqual(0); | ||
|
||
pointerDown(outsideElement); | ||
pointerUp(outsideElement); | ||
expect(outsideClickCount).toEqual(1); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,42 @@ | ||
import { useEffect, useRef } from 'react'; | ||
|
||
interface UseOutsideClickProps { | ||
ref: React.RefObject<HTMLElement>; | ||
handler?: (e: Event) => void; | ||
} | ||
|
||
export function useOutsideClick({ ref, handler }: UseOutsideClickProps) { | ||
const stateRef = useRef({ | ||
isPointerDown: false, | ||
}); | ||
|
||
const state = stateRef.current; | ||
|
||
useEffect(() => { | ||
const onPointerDown = (event: PointerEvent) => { | ||
if (isValidEvent(event, ref)) { | ||
state.isPointerDown = true; | ||
} | ||
}; | ||
|
||
const onPointerUp = (event: PointerEvent) => { | ||
if (state.isPointerDown && handler && isValidEvent(event, ref)) { | ||
state.isPointerDown = false; | ||
handler(event); | ||
} | ||
}; | ||
|
||
document.addEventListener('pointerdown', onPointerDown, true); | ||
document.addEventListener('pointerup', onPointerUp, true); | ||
|
||
return () => { | ||
document.removeEventListener('pointerdown', onPointerDown, true); | ||
document.removeEventListener('pointerup', onPointerUp, true); | ||
}; | ||
}, [handler, ref, state]); | ||
} | ||
|
||
function isValidEvent(event: Event, ref: React.RefObject<HTMLElement>) { | ||
const target = event.target; | ||
return target instanceof Node && !ref.current?.contains(target); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
import { fireEvent } from '@testing-library/react/pure'; | ||
export { cleanup, render } from '@testing-library/react/pure'; | ||
// why use pure? | ||
// to avoid cleanup afterEach | ||
// so we can render once beforeAll | ||
// https://github.com/testing-library/react-testing-library/issues/541 | ||
|
||
export function click(el: Node) { | ||
fireEvent.pointerDown(el); | ||
fireEvent.pointerUp(el); | ||
} | ||
|
||
export function clickRight(el: Node) { | ||
fireEvent.pointerDown(el, { button: 2 }); | ||
fireEvent.pointerUp(el, { button: 2 }); | ||
} | ||
|
||
export const pointerDown = (el: Node) => fireEvent.pointerDown(el); | ||
export const pointerUp = (el: Node) => fireEvent.pointerUp(el); |