@tide-tds/components 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/Accordion.d.ts +66 -0
- package/dist/Accordion.js +127 -0
- package/dist/AlertDialog.d.ts +8 -0
- package/dist/AlertDialog.js +18 -0
- package/dist/Combobox.d.ts +59 -0
- package/dist/Combobox.js +206 -0
- package/dist/Dialog.d.ts +70 -0
- package/dist/Dialog.js +255 -0
- package/dist/Input.d.ts +2 -5
- package/dist/Input.js +2 -8
- package/dist/Menu.d.ts +72 -0
- package/dist/Menu.js +226 -0
- package/dist/Popover.d.ts +43 -0
- package/dist/Popover.js +147 -0
- package/dist/Select.d.ts +62 -0
- package/dist/Select.js +169 -0
- package/dist/Tabs.d.ts +66 -0
- package/dist/Tabs.js +159 -0
- package/dist/Tag.d.ts +48 -0
- package/dist/Tag.js +102 -0
- package/dist/Tooltip.d.ts +53 -0
- package/dist/Tooltip.js +129 -0
- package/dist/index.d.ts +11 -1
- package/dist/index.js +11 -1
- package/dist/input-style.d.ts +3 -0
- package/dist/input-style.js +7 -1
- package/dist/list-collection.d.ts +23 -0
- package/dist/list-collection.js +99 -0
- package/dist/list-style.js +45 -0
- package/dist/listbox.d.ts +15 -0
- package/dist/listbox.js +90 -0
- package/dist/overlay-style.js +84 -0
- package/dist/overlay.d.ts +7 -0
- package/dist/overlay.js +130 -0
- package/package.json +3 -3
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
// src/overlay-style.ts
|
|
2
|
+
/**
|
|
3
|
+
* 오버레이 표면. raised 표면(`bg-raised`), `shadow-md`, radius `lg`, 경계 `border-raised`(light 투명 / dark gray-800).
|
|
4
|
+
* - 글자는 `text-default`다. `text-default × bg-raised` 4.5:1이 등록되어 있다.
|
|
5
|
+
* - `border-raised`는 장식용 경계라 대비 검사에서 빠진다. light의 투명 border는 forced-colors에서 CanvasText로 칠해져
|
|
6
|
+
* 그림자 없이도 경계가 남는다(spec Elevation, 사용자 이야기 37). forced-colors에서는 브라우저가 box-shadow를 지운다.
|
|
7
|
+
*/
|
|
8
|
+
const overlaySurfaceClass = "rounded-lg border border-raised bg-raised text-default shadow-md";
|
|
9
|
+
/**
|
|
10
|
+
* 「가끔 씀」 오버레이의 등장·퇴장(spec 「motion 적용」). 요소에 `data-open`(열림)과 `data-placement`(트리거 기준 방향,
|
|
11
|
+
* `top`·`bottom`·`left`·`right`)가 있어야 한다.
|
|
12
|
+
*
|
|
13
|
+
* - 등장: `@starting-style`(`starting:`)에서 opacity 0 + blur 4px(`blur-xs`) + 트리거 쪽으로 4px(`*-translate-*-1`)
|
|
14
|
+
* 떨어진 곳에서 시작해 `normal`(300ms)로 제자리에 온다.
|
|
15
|
+
* - 퇴장: `data-open`이 빠지면 같은 조합의 절반(blur 2px, 2px 이동)으로 `fast`(150ms)에 사라진다.
|
|
16
|
+
* duration은 도착 상태의 값을 쓰므로 기본 `duration-fast`에 열림만 `data-open:duration-normal`이다. easing은 둘 다 out이다.
|
|
17
|
+
* - 닫힌 상태는 `display: none`(`hidden`)이고 `transition-discrete`(`transition-behavior: allow-discrete`)로 display도
|
|
18
|
+
* 전환 대상에 넣어, 퇴장 전환이 끝날 때 사라진다. 이 둘을 지원하지 않는 브라우저에서는 바로 나타나고 사라진다.
|
|
19
|
+
* 요소를 DOM에서 빼는 시점은 `useOverlayPresence`가 전환이 끝나기를 기다려 정한다.
|
|
20
|
+
* - 퇴장 중에는 `pointer-events-none`으로 눌리지 않는다.
|
|
21
|
+
* - reduced-motion: 이동·blur와 그 전환 대상(filter, translate)은 `motion-safe:`일 때만 둔다. 움직임 줄이기에서는
|
|
22
|
+
* opacity(와 display)만 같은 시간으로 바뀐다.
|
|
23
|
+
* - `transition-all`은 쓰지 않는다. 전환 대상을 적는다.
|
|
24
|
+
*/
|
|
25
|
+
const popoverMotionClass = [
|
|
26
|
+
"transition-[opacity,display] transition-discrete duration-fast ease-out data-open:duration-normal motion-safe:transition-[opacity,filter,translate,display]",
|
|
27
|
+
"not-data-open:pointer-events-none not-data-open:hidden not-data-open:opacity-0 motion-safe:not-data-open:blur-[2px]",
|
|
28
|
+
"motion-safe:not-data-open:data-[placement=bottom]:-translate-y-0.5 motion-safe:not-data-open:data-[placement=top]:translate-y-0.5",
|
|
29
|
+
"motion-safe:not-data-open:data-[placement=right]:-translate-x-0.5 motion-safe:not-data-open:data-[placement=left]:translate-x-0.5",
|
|
30
|
+
"starting:opacity-0 motion-safe:starting:blur-xs",
|
|
31
|
+
"motion-safe:data-[placement=bottom]:starting:-translate-y-1 motion-safe:data-[placement=top]:starting:translate-y-1",
|
|
32
|
+
"motion-safe:data-[placement=right]:starting:-translate-x-1 motion-safe:data-[placement=left]:starting:translate-x-1"
|
|
33
|
+
].join(" ");
|
|
34
|
+
/**
|
|
35
|
+
* 「자주 씀」 오버레이(Tooltip, Menu, Select·Combobox 목록)의 등장·퇴장(spec 「motion 적용」, 티켓 26).
|
|
36
|
+
* 요소에 `data-open`(열림)이 있어야 한다.
|
|
37
|
+
*
|
|
38
|
+
* - 등장: 바로 나타난다. 열린 동안은 `transition-none`이라 퇴장 중에 다시 열려도 바로 불투명해진다.
|
|
39
|
+
* 새로 넣은 요소는 이전 스타일이 없어 전환이 생기지 않으므로 `starting:`도 두지 않는다.
|
|
40
|
+
* - 퇴장: `data-open`이 빠지면 opacity만 `fast`(150ms), out으로 사라진다. 전환 대상은 opacity와 display다.
|
|
41
|
+
* duration·전환 대상은 도착 상태의 값을 쓰므로 닫힌 상태에서만 전환이 있다.
|
|
42
|
+
* - 닫힌 상태는 `display: none`(`hidden`)이고 `transition-discrete`로 퇴장이 끝날 때 사라진다(popoverMotionClass와 같다).
|
|
43
|
+
* DOM에서 빼는 시점은 `useOverlayPresence`가 정한다. 퇴장 중에는 `pointer-events-none`이다.
|
|
44
|
+
* - reduced-motion: 이동·blur·확대가 없으므로 바꿀 것이 없다. opacity 전환은 그대로 둔다.
|
|
45
|
+
*/
|
|
46
|
+
const frequentMotionClass = ["transition-[opacity,display] transition-discrete duration-fast ease-out data-open:transition-none", "not-data-open:pointer-events-none not-data-open:hidden not-data-open:opacity-0"].join(" ");
|
|
47
|
+
/**
|
|
48
|
+
* Dialog류(Dialog, AlertDialog) 표면(spec radius·Elevation, 티켓 30). 가운데에 뜨는 모달이라 트리거에 붙는
|
|
49
|
+
* 오버레이보다 한 단계 높다. raised 표면(`bg-raised`), `shadow-lg`, radius `xl`, 경계 `border-raised`.
|
|
50
|
+
* 글자·경계·forced-colors 처리는 `overlaySurfaceClass`와 같다(`text-default × bg-raised` 4.5:1 등록, `border-raised`는 장식용).
|
|
51
|
+
*/
|
|
52
|
+
const dialogSurfaceClass = "rounded-xl border border-raised bg-raised text-default shadow-lg";
|
|
53
|
+
/**
|
|
54
|
+
* 모달 뒤를 가리는 반투명 막(backdrop, `bg-overlay`)의 등장·퇴장(티켓 30). 요소에 `data-open`이 있어야 한다.
|
|
55
|
+
*
|
|
56
|
+
* - 등장: `starting:`에서 opacity 0으로 시작해 `normal`(300ms)로 나타난다. 퇴장: `data-open`이 빠지면 `fast`(150ms)로 사라진다.
|
|
57
|
+
* - 닫힌 상태는 `display: none`(`hidden`) + `transition-discrete`로 퇴장이 끝날 때 사라지고, 퇴장 중에는
|
|
58
|
+
* `pointer-events-none`이다(popoverMotionClass와 같은 구조). 안의 Dialog도 이 값을 물려받아 눌리지 않는다.
|
|
59
|
+
* - opacity만 바뀌므로 움직임 줄이기에서도 같다.
|
|
60
|
+
* - `useOverlayPresence`는 이 요소(막)의 전환이 끝나기를 기다린다. 안의 Dialog 전환(`dialogMotionClass`)도 같은
|
|
61
|
+
* duration이라 함께 끝난다.
|
|
62
|
+
*/
|
|
63
|
+
const backdropMotionClass = [
|
|
64
|
+
"transition-[opacity,display] transition-discrete duration-fast ease-out data-open:duration-normal",
|
|
65
|
+
"not-data-open:pointer-events-none not-data-open:hidden not-data-open:opacity-0",
|
|
66
|
+
"starting:opacity-0"
|
|
67
|
+
].join(" ");
|
|
68
|
+
/**
|
|
69
|
+
* Dialog류의 등장·퇴장(spec 「motion 적용」 「가끔 씀」 Dialog류, 티켓 30). 요소에 `data-open`이 있어야 한다.
|
|
70
|
+
*
|
|
71
|
+
* - 등장: `starting:`에서 opacity 0 + scale 0.96(`scale-96`)으로 시작해 `normal`(300ms), out으로 제자리에 온다.
|
|
72
|
+
* - 퇴장: `data-open`이 빠지면 같은 조합을 약하게(scale 0.98, `scale-98`) `fast`(150ms)로 사라진다.
|
|
73
|
+
* duration은 도착 상태의 값을 쓰므로 기본 `duration-fast`에 열림만 `data-open:duration-normal`이다.
|
|
74
|
+
* - 화면에서 빼는 일(display)은 바깥 막(`backdropMotionClass`)이 한다. 이 요소는 opacity·scale만 전환한다.
|
|
75
|
+
* - reduced-motion: 확대(scale)와 그 전환 대상은 `motion-safe:`일 때만 둔다. 움직임 줄이기에서는 opacity만 바뀐다.
|
|
76
|
+
* - `transition-all`은 쓰지 않는다.
|
|
77
|
+
*/
|
|
78
|
+
const dialogMotionClass = [
|
|
79
|
+
"transition-[opacity] duration-fast ease-out data-open:duration-normal motion-safe:transition-[opacity,scale]",
|
|
80
|
+
"not-data-open:opacity-0 motion-safe:not-data-open:scale-98",
|
|
81
|
+
"starting:opacity-0 motion-safe:starting:scale-96"
|
|
82
|
+
].join(" ");
|
|
83
|
+
|
|
84
|
+
export { backdropMotionClass, dialogMotionClass, dialogSurfaceClass, frequentMotionClass, overlaySurfaceClass, popoverMotionClass };
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import "react";
|
|
2
|
+
import "react-stately";
|
|
3
|
+
/**
|
|
4
|
+
* 트리거 기준 위치. 앞은 트리거의 어느 쪽에 놓을지(주축), 뒤는 그 변에서 어디에 맞출지(교차축)다.
|
|
5
|
+
* 공간이 모자라면 반대쪽으로 뒤집는다(React Aria `shouldFlip` 기본값).
|
|
6
|
+
*/
|
|
7
|
+
export type OverlayPlacement = "top" | "top start" | "top end" | "bottom" | "bottom start" | "bottom end" | "left" | "left top" | "left bottom" | "right" | "right top" | "right bottom";
|
package/dist/overlay.js
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { jsx } from "react/jsx-runtime";
|
|
4
|
+
import { useEffect, useLayoutEffect, useState } from "react";
|
|
5
|
+
import { Overlay, usePopover } from "react-aria";
|
|
6
|
+
|
|
7
|
+
// src/overlay.tsx
|
|
8
|
+
const isAxis = (value) => value === "top" || value === "bottom" || value === "left" || value === "right";
|
|
9
|
+
/**
|
|
10
|
+
* 열림 상태가 꺼져도 퇴장 전환이 끝날 때까지 오버레이를 DOM에 남긴다. DOM에 둘지(`true`)를 돌려준다.
|
|
11
|
+
*
|
|
12
|
+
* 닫힌 뒤 요소의 `getAnimations()`(CSS transition 포함)가 모두 끝나면 뺀다. 전환이 없으면(지원하지 않는 브라우저,
|
|
13
|
+
* 전환이 없는 오버레이) 바로 뺀다. 퇴장 중에 다시 열리면 빼지 않는다. React Aria hooks에는 퇴장을 기다리는 hook이
|
|
14
|
+
* 없어서(react-aria-components의 `isExiting`은 Component 층에만 있다) 이 hook이 그 일을 한다.
|
|
15
|
+
*/
|
|
16
|
+
function useOverlayPresence(isOpen, ref) {
|
|
17
|
+
const [present, setPresent] = useState(isOpen);
|
|
18
|
+
if (isOpen && !present) setPresent(true);
|
|
19
|
+
useLayoutEffect(() => {
|
|
20
|
+
if (isOpen || !present) return;
|
|
21
|
+
const element = ref.current;
|
|
22
|
+
if (!element || typeof element.getAnimations !== "function") {
|
|
23
|
+
setPresent(false);
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
let cancelled = false;
|
|
27
|
+
const waitForExit = () => {
|
|
28
|
+
const animations = element.getAnimations();
|
|
29
|
+
if (animations.length === 0) {
|
|
30
|
+
setPresent(false);
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
Promise.allSettled(animations.map((animation) => animation.finished)).then(() => {
|
|
34
|
+
if (!cancelled) waitForExit();
|
|
35
|
+
});
|
|
36
|
+
};
|
|
37
|
+
waitForExit();
|
|
38
|
+
return () => {
|
|
39
|
+
cancelled = true;
|
|
40
|
+
};
|
|
41
|
+
}, [
|
|
42
|
+
isOpen,
|
|
43
|
+
present,
|
|
44
|
+
ref
|
|
45
|
+
]);
|
|
46
|
+
return isOpen || present;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* React Aria가 위치 style에 넣는 `zIndex: 100000`을 빼고 주축 방향을 `data-placement`로 붙인다.
|
|
50
|
+
* 쌓임은 쌓임 단계 class(`z-popover` 등)로만 정한다(spec 「오버레이」).
|
|
51
|
+
*
|
|
52
|
+
* `placement`가 아직 없으면(처음 렌더) 요청한 placement의 주축을 쓴다. 등장 전환의 시작 방향이 첫 스타일 계산에서
|
|
53
|
+
* 정해지기 때문이다. 공간이 없어 뒤집힌 경우에만 첫 프레임의 시작 방향이 요청한 쪽이 된다(4px).
|
|
54
|
+
*/
|
|
55
|
+
function overlayPositionProps(props, placement, requested) {
|
|
56
|
+
const { zIndex: _zIndex, ...style } = props.style ?? {};
|
|
57
|
+
const fallback = requested.split(" ")[0];
|
|
58
|
+
return {
|
|
59
|
+
...props,
|
|
60
|
+
style,
|
|
61
|
+
"data-placement": isAxis(placement) ? placement : fallback
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* 트리거에 붙는 오버레이(Popover, Menu, Select·Combobox 목록)의 위치·닫기 동작. React Aria `usePopover`를 감싼다.
|
|
66
|
+
* - 닫기: Esc, 바깥 누름, 포커스가 바깥으로 나감(`usePopover` 기본). 위에 쌓인 오버레이부터 닫힌다.
|
|
67
|
+
* - 모달: 기본은 바깥을 `inert`로 막고 스크롤을 잠근다. `nonModal`이면 둘 다 하지 않는다.
|
|
68
|
+
* - 위치: 공간이 모자라면 반대쪽으로 뒤집고, 주축 길이는 `max-height`로 줄여 트리거를 덮지 않는다(WCAG 2.4.11, Comments 참고).
|
|
69
|
+
* 돌려받은 `overlayProps`(style 포함)를 오버레이 요소에 펼친다. 쌓임 단계 class는 부르는 쪽이 붙인다.
|
|
70
|
+
*/
|
|
71
|
+
function usePopoverOverlay({ triggerRef, popoverRef, placement, offset, nonModal = false }, state) {
|
|
72
|
+
const { popoverProps, placement: actual } = usePopover({
|
|
73
|
+
triggerRef,
|
|
74
|
+
popoverRef,
|
|
75
|
+
placement,
|
|
76
|
+
offset,
|
|
77
|
+
isNonModal: nonModal
|
|
78
|
+
}, state);
|
|
79
|
+
return { overlayProps: overlayPositionProps(popoverProps, actual, placement) };
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* 트리거가 놓인 Theme(`data-theme`)을 portal로 나간 오버레이에 옮긴다. Theme을 `<html>`이 아닌 일부 영역에
|
|
83
|
+
* 붙인 앱에서도 오버레이가 같은 색이 된다. 오버레이에 이미 `data-theme`이 있으면 두고, 트리거 위에 Theme이 없으면
|
|
84
|
+
* 붙이지 않는다(`:root` 기본값을 따른다). 위치를 계산하기 전(첫 스타일 계산 전)에 붙도록 위치 hook보다 먼저 부른다.
|
|
85
|
+
*/
|
|
86
|
+
function useOverlayTheme(triggerRef, overlayRef) {
|
|
87
|
+
useLayoutEffect(() => {
|
|
88
|
+
const overlay = overlayRef.current;
|
|
89
|
+
if (!overlay || overlay.hasAttribute("data-theme")) return;
|
|
90
|
+
const theme = triggerRef.current?.closest("[data-theme]")?.getAttribute("data-theme");
|
|
91
|
+
if (theme) overlay.setAttribute("data-theme", theme);
|
|
92
|
+
}, [triggerRef, overlayRef]);
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* 오버레이 안에서 Tab을 누르면 기본 동작을 막고 닫는다(포커스는 `OverlayPortal`의 FocusScope가 트리거로 돌린다).
|
|
96
|
+
* 목록 오버레이(Menu, Select)가 쓴다(WAI-ARIA APG Menu Button·Select-Only Combobox: Tab은 목록을 닫는다).
|
|
97
|
+
*
|
|
98
|
+
* React Aria 목록 hook(useMenu·useListBox)은 Tab으로 닫지 않고, 항목의 keydown 처리가 전파를 멈추며 FocusScope가
|
|
99
|
+
* document capture에서 Tab을 먼저 다룬다. 열린 동안 바깥은 inert라 브라우저 기본 Tab은 갈 곳이 없다. 그래서 그보다
|
|
100
|
+
* 앞인 window capture에서 받는다(티켓 27에서 Menu 안에 두었다가 티켓 28에서 이곳으로 옮겼다).
|
|
101
|
+
* 전파도 멈춘다. listbox(useListBox 안의 useSelectableCollection)는 Tab에서 포커스를 목록 안 요소로 옮기는데, 그러면
|
|
102
|
+
* 닫힌 뒤 포커스가 트리거로 돌아오지 않았다(Select 테스트로 확인).
|
|
103
|
+
*/
|
|
104
|
+
function useCloseOnTab(overlayRef, close) {
|
|
105
|
+
useEffect(() => {
|
|
106
|
+
const onKeyDown = (event) => {
|
|
107
|
+
if (event.key !== "Tab" || !overlayRef.current?.contains(event.target)) return;
|
|
108
|
+
event.preventDefault();
|
|
109
|
+
event.stopPropagation();
|
|
110
|
+
close();
|
|
111
|
+
};
|
|
112
|
+
window.addEventListener("keydown", onKeyDown, true);
|
|
113
|
+
return () => window.removeEventListener("keydown", onKeyDown, true);
|
|
114
|
+
}, [close, overlayRef]);
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* 오버레이를 `document.body`에 portal한다(React Aria `Overlay`). top-layer(`<dialog>`, popover API)는 쓰지 않는다.
|
|
118
|
+
* 안을 `FocusScope`(`restoreFocus`)로 감싸서, DOM에서 빠질 때 포커스가 열기 전 요소(보통 트리거)로 돌아간다.
|
|
119
|
+
* 역할 hook이 포커스 가두기를 요청하면(`useDialog` 등) Tab이 안에서 돈다. 서버 렌더에서는 아무것도 그리지 않는다.
|
|
120
|
+
* `focusManagement={false}`면 FocusScope 없이 portal만 한다(Tooltip).
|
|
121
|
+
*/
|
|
122
|
+
function OverlayPortal({ isExiting, focusManagement = true, children }) {
|
|
123
|
+
return /* @__PURE__ */ jsx(Overlay, {
|
|
124
|
+
isExiting,
|
|
125
|
+
disableFocusManagement: !focusManagement,
|
|
126
|
+
children
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export { OverlayPortal, overlayPositionProps, useCloseOnTab, useOverlayPresence, useOverlayTheme, usePopoverOverlay };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tide-tds/components",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Tide 디자인 시스템의 React Component",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -23,8 +23,8 @@
|
|
|
23
23
|
"./styles.css": "./dist/styles.css"
|
|
24
24
|
},
|
|
25
25
|
"dependencies": {
|
|
26
|
-
"@tide-tds/foundation": "0.
|
|
27
|
-
"@tide-tds/icons": "0.
|
|
26
|
+
"@tide-tds/foundation": "0.4.0",
|
|
27
|
+
"@tide-tds/icons": "0.4.0",
|
|
28
28
|
"clsx": "^2.1.1",
|
|
29
29
|
"react-aria": "^3.53.1",
|
|
30
30
|
"react-stately": "^3.51.0",
|