dialog (HTML Element)
모달/비모달(팝업, 모달, 알림창) 상자를 표준적이고 접근성있게 구현
JavaScript 사용하여 제어
| methods | ||
|---|---|---|
dialog.showModal() |
모달 열기 | 최상위 레이어에 배치 ESC 키 닫기 ::backdrop 가상요소 |
dialog.show() |
비모달 열기 | ::backdrop 가상 요소가 없어 다른 요소와 상호작용 |
dialog.close() |
모달/비모달 닫기 |
| CSS features | |
|---|---|
| (모달)::backdrop | 바로 뒤 전체 화면 |
| ~s allow-discrete ( transition-behavior) |
불연속 속성(display, visibility, overlay)을 애니메이션 대상에 추가 overlay ~초 까지 레이어(z-index) 유지 |
| @starting-style | 애니메이션 시작 순간 초기 스타일 |
<button onclick="openModal()">open</button>
<dialog id="myModal">
<div class="modal-content">Contents</div>
</dialog>dialog {
/* Exit State */
transform: translateY(20px);
opacity: 0;
transition:
transform 0.3s,
opacity 0.3s,
display 0.3s allow-discrete,
overlay 0.3s allow-discrete; /* 최상단 레이어 유지 */
}
/* Open State */
dialog[open] {
transform: translateY(0);
opacity: 1;
/* Before-Open State */
@starting-style {
transform: translateY(20px);
opacity: 0;
}
}function openModal() {
document.querySelector('#myModal').showModal()
}popover (HTML Attribute)
JavaScript 없이 css만으로 툴팁, 드롭다운, 모달 구현 가능
- Top Layer 자동 처리 (복잡한 z-index 해결)
- 접근성 자동 처리
- 모달 접근성은
<dialog>가 더 적합(tab focus)
<button popovertarget="my-popover">열기</button>
<div id="my-popover" popover>내용</div>popovertarget:<buttom>,<input>요소로 popover 제어 (제어할 popover id값 받음)
이 외 태그로(div, a) 사용하려면 JavaScript로 제어하며, 접근성 등 직접 처리해야함popovertargetaction: popover 제어 요소에 작업 지정 (showhidetoggle)
| popover Values | 사용 예 | |
|---|---|---|
auto (default) popover="auto" === popover === popover="" |
외부 클릭 닫기, ESC 닫기 가능(light-dismissed) | 툴팁, 드롭다운, 유저 프로필 |
| manual | JavaScript로 닫기 제어 | toast 알림 |
| hint 실험적 | hint open 시 auto 닫지 않음 실험단계로 기능 구현 잘 안됨 |
| popover CSS features | |
|---|---|
| ::backdrop | 바로 뒤 전체 화면 |
| :popover-open | popover open 될 때 스타일 |
| popover Methods | |
|---|---|
HTMLElement.hidePopover |
숨긴 후 display:none |
HTMLElement.showPopover |
최상위 레이어에 추가 |
HTMLElement.togglePopover |
현재 상태 반전 HTMLElement.togglePopover(boolean) |
Toast Popover
<button onclick="createToast('완료되었습니다.')">Trigger</button>.toast[popover] {
inset: unset;
right: 20px;
bottom: 20px;
/* Exit State */
transform: translateX(100%);
opacity: 0;
transition:
bottom 0.3s,
transform 0.3s,
opacity 0.3s,
display 0.3s allow-discrete,
overlay 0.3s allow-discrete;
}
/* Open State */
.toast[popover]:popover-open {
transform: translate(0);
opacity: 1;
/* Before-Open State */
@starting-style {
transform: translateY(100%);
opacity: 0;
}
}const createToast = (msg) => {
const popover = document.createElement('div')
popover.popover = 'manual'
popover.classList.add('toast')
popover.textContent = msg
document.body.appendChild(popover)
popover.showPopover()
moveToast()
setTimeout(async() => {
popover.hidePopover()
await Promise.allSettled(popover.getAnimations().map(ani => ani.finished))
popover.remove()
}, 3000)
}
const moveToast = () => {
const toasts = Array.from(document.querySelectorAll('.toast:popover-open')).reverse()
const margin = 10
const initialBottom = 20
toasts.forEach((toast, i) => {
const toastHeight = parseInt(toast.offsetHeight) || 0
toast.style.bottom = `${initialBottom + i * (toastHeight + margin)}px`
})
}ToggleEvent: newState
<popover>요소가 전환되는 상태 String (<details>도 사용가능)
popover.addEventListener('toggle', (event) => { // 또는 beforetoggle (데이터 불러올 때)
// event.newState === "open" | "closed"
})OG Tag
<meta property="twitter:title" content="">
<meta property="twitter:description" content="">
<meta property="twitter:card" content="summary_large_image">
<meta property="twitter:image" content="_share_main.png">
<meta property="twitter:image:width" content="500">
<meta property="twitter:image:height" content="250">
<meta property="og:title" content=""/>
<meta property="og:description" content="">
<meta property="og:image" content="_share_main.png">
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" />
<meta property="og:site_name" content="사이트 명">
<meta property="og:type" content="website">
<meta property="og:url" content="url">Event Resize, Scroll 최적화
Debounce
- 마지막 호출, delay 후 실행 : 이벤트 하나로 묶어 처리
- 실시간 검색, 검색 필터링, 창 크기 조정, 유효성 검사
const debounce = (cb: Function, delay) => {
let timer = null;
return function (...args) {
if (timer) clearTimeout(timer);
timer = setTimeout(() => {
cb.apply(this, args)
timer = null;
}, delay);
}
}Throttler
- delay 마다 실행 : 이벤트 중에도 delay 마다 업데이트
- 스크롤, API 호출
const throttler = (cb: Function, delay) => {
let timer = null;
return function (...args) {
if (!timer) {
cb.apply(this, args);
timer = setTimeout(() => {
timer = null;
}, delay);
}
}
};Utility
Radio Readonly
someRadio.forEach(item=>{
item.addEventListener('click', e=> e.preventDefault())
})제한시간 타이머
const createCountdown = (limitTime) =>{
const displayElement = document.querySelector('.printTime');
if (!displayElement) return;
let tickSetTimeout;
const numericLimit = Number(limitTime) || 0;
const endTime = Date.now() + (numericLimit * 1000);
const render = (time) =>{
const min = String(Math.floor(time/60)).padStart(2, "0");
const sec = String(time % 60).padStart(2, "0");
const timeStr = min + sec;
const timeHtml = timeStr.split('')
.map(val => `<span class="inp">${val}</span>`)
.join('');
displayElement.innerHTML = timeHtml;
}
const tick = ()=>{
const syncTime = endTime - Date.now();
const remainingTime = Math.max(0, Math.ceil(syncTime / 1000));
const nextDelay = syncTime % 1000 || 1000;
render(remainingTime);
if (remainingTime <= 0) {
clearTimeout(tickSetTimeout);
return
}
tickSetTimeout = setTimeout(tick, nextDelay);
}
tick();
return {
stop: () => clearTimeout(tickSetTimeout)
}
}