<dialog>
요소를 사용하여 색상 적응형, 반응형, 접근성 있는 미니 및 메가 모달을 빌드하는 방법에 관한 기본 개요입니다.
이 게시물에서는 <dialog>
요소를 사용하여 색상 적응형, 반응형, 접근성이 뛰어난 미니 및 메가 모달을 빌드하는 방법에 관한 생각을 공유하고자 합니다.
데모를 사용해 보고 소스를 확인해 보세요.
동영상을 선호하는 경우 이 게시물의 YouTube 버전을 확인하세요.
개요
<dialog>
요소는
인페이지 문맥 정보 또는 작업에 매우 유용합니다. 양식이 작거나 사용자가 확인해야 하는 유일한 작업이 확인 또는 취소뿐이므로 사용자 환경에서 다중 페이지 작업 대신 동일한 페이지 작업을 통해 이점을 얻을 수 있는 경우를 생각해 보세요.
<dialog>
요소가 최근에 여러 브라우저에서 안정화되었습니다.
요소에 몇 가지 누락된 항목이 있어 이 GUI 챌린지에서는 추가 이벤트, 가벼운 닫기, 맞춤 애니메이션, 미니 및 메가 유형 등 예상되는 개발자 환경 항목을 추가합니다.
마크업
<dialog>
요소의 기본사항은 간단합니다. 이 요소는 자동으로 숨겨지며 콘텐츠를 오버레이하는 스타일이 내장되어 있습니다.
<dialog>
…
</dialog>
이 기준을 개선할 수 있습니다.
일반적으로 대화상자 요소는 모달과 많은 부분을 공유했으며 이름이 같은 경우도 많습니다. 여기서는 작은 대화상자 팝업(미니)과 전체 페이지 대화상자(메가) 모두에 대화상자 요소를 사용했습니다. 두 대화상자의 이름을 메가와 미니로 지정했으며, 두 대화상자 모두 다양한 사용 사례에 맞게 약간 조정했습니다.
유형을 지정할 수 있도록 modal-mode
속성을 추가했습니다.
<dialog id="MegaDialog" modal-mode="mega"></dialog>
<dialog id="MiniDialog" modal-mode="mini"></dialog>
항상 그런 것은 아니지만 일반적으로 대화상자 요소는 일부 상호작용 정보를 수집하는 데 사용됩니다. 대화상자 요소 내의 양식은 함께 작동하도록 만들어집니다.
JavaScript가 사용자가 입력한 데이터에 액세스할 수 있도록 양식 요소로 대화상자 콘텐츠를 래핑하는 것이 좋습니다. 또한 method="dialog"
를 사용하는 양식 내 버튼은 JavaScript 없이 대화상자를 닫고 데이터를 전달할 수 있습니다.
<dialog id="MegaDialog" modal-mode="mega">
<form method="dialog">
…
<button value="cancel">Cancel</button>
<button value="confirm">Confirm</button>
</form>
</dialog>
메가 대화상자
메가 대화상자에는 <header>
, <article>
, <footer>
라는 세 가지 요소가 양식 내에 있습니다.
이는 시맨틱 컨테이너 역할을 하며 대화상자 프레젠테이션의 스타일 타겟이 됩니다. 헤더는 모달의 제목을 지정하고 닫기 버튼을 제공합니다. 이 도움말에서는 양식 입력란 및 정보에 대해 설명합니다. 바닥글에는 작업 버튼 <menu>
이 있습니다.
<dialog id="MegaDialog" modal-mode="mega">
<form method="dialog">
<header>
<h3>Dialog title</h3>
<button onclick="this.closest('dialog').close('close')"></button>
</header>
<article>...</article>
<footer>
<menu>
<button autofocus type="reset" onclick="this.closest('dialog').close('cancel')">Cancel</button>
<button type="submit" value="confirm">Confirm</button>
</menu>
</footer>
</form>
</dialog>
첫 번째 메뉴 버튼에는 autofocus
및 onclick
인라인 이벤트 핸들러가 있습니다. autofocus
속성은 대화상자가 열릴 때 포커스를 받으며 확인 버튼이 아닌 취소 버튼에 이 속성을 지정하는 것이 좋습니다. 이렇게 하면 확인이 의도적이고 실수로 인한 것이 아님을 확인할 수 있습니다.
미니 대화상자
미니 대화상자는 메가 대화상자와 매우 비슷하며, <header>
요소만 누락되어 있습니다. 이렇게 하면 더 작고 인라인에 더 가까워집니다.
<dialog id="MiniDialog" modal-mode="mini">
<form method="dialog">
<article>
<p>Are you sure you want to remove this user?</p>
</article>
<footer>
<menu>
<button autofocus type="reset" onclick="this.closest('dialog').close('cancel')">Cancel</button>
<button type="submit" value="confirm">Confirm</button>
</menu>
</footer>
</form>
</dialog>
대화상자 요소는 데이터와 사용자 상호작용을 수집할 수 있는 전체 뷰포트 요소의 강력한 기반을 제공합니다. 이러한 기본사항을 통해 사이트 또는 앱에서 매우 흥미롭고 강력한 상호작용을 만들 수 있습니다.
접근성
대화상자 요소에는 매우 우수한 접근성이 기본적으로 내장되어 있습니다. 평소와 같이 이러한 기능을 추가하는 대신 이미 많은 기능이 있습니다.
포커스 복원
측면 메뉴 구성요소 빌드에서 수동으로 수행한 것처럼, 무언가를 열고 닫을 때 관련 열기 및 닫기 버튼에 포커스가 올바르게 설정되어야 합니다. 사이드바가 열리면 닫기 버튼에 포커스가 설정됩니다. 닫기 버튼을 누르면 포커스가 열었던 버튼으로 복원됩니다.
대화상자 요소의 경우 기본 동작이 내장되어 있습니다.
대화상자를 안팎으로 애니메이션 처리하려면 이 기능을 사용할 수 없습니다. JavaScript 섹션에서 이 기능을 복원할 예정입니다.
트래핑 포커스
대화상자 요소는 문서에서 inert
를 자동으로 관리합니다. inert
이전에는 JavaScript가 요소에서 포커스가 사라지는 것을 감시하는 데 사용되었으며, 이때 포커스를 가로채 다시 적용했습니다.
inert
이후에는 문서의 모든 부분이 '고정'되어 더 이상 포커스 타겟이 아니거나 마우스와 상호작용할 수 있습니다. 포커스를 트랩하는 대신 포커스가 문서의 유일한 양방향 부분으로 안내됩니다.
요소를 열고 자동으로 포커스 지정
기본적으로 대화상자 요소는 대화상자 마크업의 첫 번째 포커스 가능 요소에 포커스를 할당합니다. 사용자가 기본으로 선택할 수 있는 최적의 요소가 아닌 경우 autofocus
속성을 사용하세요. 앞서 설명한 대로 확인 버튼이 아닌 취소 버튼에 이 아이콘을 배치하는 것이 좋습니다. 이렇게 하면 확인이 의도적이고 실수로 인한 것이 아님을 확인할 수 있습니다.
Esc 키로 닫기
이러한 요소는 방해가 될 수 있으므로 쉽게 닫을 수 있어야 합니다. 다행히 대화상자 요소가 이스케이프 키를 대신 처리하므로 오케스트레이션 부담을 덜 수 있습니다.
스타일
대화상자 요소의 스타일을 지정하는 쉬운 방법과 어려운 방법이 있습니다. 쉬운 경로는 대화상자의 표시 속성을 변경하지 않고 제한사항을 적용함으로써 얻을 수 있습니다. 대화상자를 열고 닫을 때, display
속성을 가져올 때 등의 맞춤 애니메이션을 제공하기 위해 어려운 길을 택했습니다.
개방형 속성을 사용한 스타일 지정
적응형 색상과 전반적인 디자인 일관성을 높이기 위해 CSS 변수 라이브러리 Open Props를 가져왔습니다. 무료로 제공되는 변수 외에도 normalize 파일과 일부 버튼을 가져옵니다. 이 두 가지 모두 Open Props에서 선택적 가져오기로 제공합니다. 이러한 가져오기는 대화상자와 데모를 맞춤설정하는 데 집중하는 데 도움이 되며, 이를 지원하고 보기 좋게 만들기 위해 많은 스타일이 필요하지 않습니다.
<dialog>
요소 스타일 지정
디스플레이 속성 소유
대화상자 요소의 기본 표시 및 숨기기 동작은 디스플레이 속성을 block
에서 none
로 전환합니다. 안타깝게도 안으로만 애니메이션을 적용할 수 있으며 밖으로 애니메이션을 적용할 수는 없습니다. 이 두 가지 동작을 모두 애니메이션으로 처리해 보겠습니다. 첫 번째 단계는
자체 display 속성을 설정하는 것입니다.
dialog {
display: grid;
}
위의 CSS 스니펫에 표시된 대로 표시 속성 값을 변경하여 소유함으로써 적절한 사용자 환경을 촉진하기 위해 상당한 양의 스타일이 관리되어야 합니다. 첫째, 대화상자의 기본 상태가 닫힙니다. 다음 스타일을 사용하여 이 상태를 시각적으로 표현하고 대화상자가 상호작용을 수신하지 못하도록 할 수 있습니다.
dialog:not([open]) {
pointer-events: none;
opacity: 0;
}
이제 대화상자가 표시되지 않으며 열려 있지 않을 때는 상호작용할 수 없습니다. 나중에 대화상자의 inert
속성을 관리하는 JavaScript를 추가하여 키보드 및 스크린 리더 사용자도 숨겨진 대화상자에 도달할 수 없도록 하겠습니다.
대화상자에 적응형 색상 테마 적용
color-scheme
는 문서를 밝은 시스템 환경설정과 어두운 시스템 환경설정에 맞게 브라우저에서 제공하는 적응형 색상 테마로 선택하지만, 그 이상으로 대화상자 요소를 맞춤설정하고 싶었습니다. Open Props는 color-scheme
사용과 마찬가지로 밝은 시스템 환경설정과 어두운 시스템 환경설정에 자동으로 적응하는 몇 가지 표시 경로 색상을 제공합니다. 디자인에서 레이어를 만드는 데 유용하며 색상을 사용하여 레이어 표면의 이러한 모양을 시각적으로 지원하는 것이 좋습니다. 배경 색상은 var(--surface-1)
입니다. 이 레이어 위에 배치하려면 var(--surface-2)
를 사용하세요.
dialog {
…
background: var(--surface-2);
color: var(--text-1);
}
@media (prefers-color-scheme: dark) {
dialog {
border-block-start: var(--border-size-1) solid var(--surface-3);
}
}
헤더 및 바닥글과 같은 하위 요소에 더 많은 적응형 색상이 나중에 추가될 예정입니다. 대화 요소에 추가되는 요소이지만 매력적이고 잘 설계된 대화 디자인을 만드는 데 매우 중요합니다.
반응형 대화상자 크기 조정
대화상자는 기본적으로 파일의 크기를 콘텐츠로 위임하며, 이는 일반적으로 큰 크기입니다. 여기서는 max-inline-size
를 읽을 수 있는 크기(--size-content-3
= 60ch
) 또는 뷰포트 너비의 90%로 제한하는 것이 목표입니다. 이렇게 하면 대화상자가 휴대기기에서 더 넓은 화면으로 표시되지 않고 데스크톱 화면에서 읽기 힘들 정도로 넓지 않게 됩니다. 그런 다음 대화상자가 페이지 높이를 초과하지 않도록 max-block-size
를 추가합니다. 즉, 세로로 긴 대화상자 요소인 경우 대화상자의 스크롤 가능한 영역의 위치를 지정해야 합니다.
dialog {
…
max-inline-size: min(90vw, var(--size-content-3));
max-block-size: min(80vh, 100%);
max-block-size: min(80dvb, 100%);
overflow: hidden;
}
max-block-size
가 두 번 있는 것을 볼 수 있습니다. 첫 번째는 실제 표시 영역 단위인 80vh
를 사용합니다. 실제로 원하는 것은 해외 사용자를 위해 대화상자를 상대적인 흐름 내에 유지하는 것이므로 두 번째 선언에서는 더 안정화될 때를 대비해 논리적이고 최신이며 부분적으로만 지원되는 dvb
단위를 사용합니다.
메가 대화상자 배치
대화상자 요소의 위치를 지정하는 데 도움이 되도록 전체 화면 배경과 대화상자 컨테이너라는 두 부분으로 나누는 것이 좋습니다. 배경은 모든 것을 가려야 하며 이 대화상자가 앞에 있고 뒤에 있는 콘텐츠에는 액세스할 수 없다는 것을 보여주는 음영 효과를 제공해야 합니다. 대화상자 컨테이너는 자유롭게 이 배경화면의 중앙에 위치하고 콘텐츠에 필요한 모양을 지정할 수 있습니다.
다음 스타일은 대화상자 요소를 창에 고정하여 각 모서리까지 늘리고 margin: auto
를 사용하여 콘텐츠를 가운데에 배치합니다.
dialog {
…
margin: auto;
padding: 0;
position: fixed;
inset: 0;
z-index: var(--layer-important);
}
모바일 메가 대화상자 스타일
작은 표시 영역에서는 이 전체 페이지 메가 모달의 스타일을 약간 다르게 지정합니다. 하단 여백을 0
로 설정하면 대화상자 콘텐츠가 뷰포트 하단으로 이동합니다. 몇 가지 스타일을 조정하여 대화상자를 사용자의 엄지손가락에 더 가까운 작업 시트로 바꿀 수 있습니다.
@media (max-width: 768px) {
dialog[modal-mode="mega"] {
margin-block-end: 0;
border-end-end-radius: 0;
border-end-start-radius: 0;
}
}
미니 대화상자 배치
데스크톱 컴퓨터와 같이 더 큰 뷰포트를 사용할 때는 미니 대화상자를 호출한 요소 위에 배치했습니다. 이를 위해서는 JavaScript가 필요합니다. 여기에서 제가 사용하는 기법을 확인할 수 있지만 이 도움말의 범위를 벗어납니다. JavaScript가 없으면 메가 대화상자와 마찬가지로 미니 대화상자가 화면 중앙에 표시됩니다.
눈에 확 띄도록 만들기
마지막으로 대화상자를 꾸며 페이지 위의 부드러운 표면처럼 보이도록 합니다. 대화상자의 모서리를 둥글게 처리하여 부드러운 느낌을 줍니다. 깊이는 Open Props의 세심하게 제작된 쉐도우 프로프 중 하나를 사용하여 구현됩니다.
dialog {
…
border-radius: var(--radius-3);
box-shadow: var(--shadow-6);
}
배경화면 의사 요소 맞춤설정
저는 배경화면을 매우 가볍게 사용하여
backdrop-filter
를 사용하여
흐리게 처리하는 효과를 메가 대화상자에 추가했습니다.
dialog[modal-mode="mega"]::backdrop {
backdrop-filter: blur(25px);
}
또한 향후 브라우저에서 배경 요소 전환을 허용할 수 있기를 바라며 backdrop-filter
에 전환을 적용했습니다.
dialog::backdrop {
transition: backdrop-filter .5s ease;
}
스타일 지정 추가 항목
이 섹션은 일반적인 대화상자 요소보다는 대화상자 요소 데모와 더 관련이 있으므로 '추가 항목'이라고 합니다.
스크롤 제한
대화상자가 표시될 때 사용자는 여전히 그 뒤로 페이지를 스크롤할 수 있는데, 이 작업은 원치 않습니다.
일반적으로 overscroll-behavior
이 일반적인 해결 방법이지만 사양에 따라 스크롤 포트가 아니므로 대화상자에 영향을 미치지 않습니다. 즉, 스크롤러가 아니므로 방지할 수 있는 사항이 없습니다. JavaScript를 사용하여 이 가이드의 새 이벤트(예: '닫힘', '열림')를 확인하고 문서에서 overflow: hidden
를 전환하거나 모든 브라우저에서 :has()
가 안정화될 때까지 기다릴 수 있습니다.
html:has(dialog[open][modal-mode="mega"]) {
overflow: hidden;
}
이제 메가 대화상자가 열려 있으면 html 문서에 overflow: hidden
가 있습니다.
<form>
레이아웃
사용자로부터 상호작용 정보를 수집하는 데 매우 중요한 요소일 뿐만 아니라 여기서는 헤더, 바닥글, 기사 요소를 배치하는 데도 사용합니다. 이 레이아웃에서는 기사 하위 요소를 스크롤 가능한 영역으로 표현하려고 합니다. grid-template-rows
를 사용하여 이 작업을 실행합니다.
도움말 요소에는 1fr
이 지정되고 양식 자체의 최대 높이는 대화상자 요소와 동일합니다. 이 확고한 높이와 확고한 행 크기를 설정하면 기사 요소가 제한되고 오버플로될 때 스크롤될 수 있습니다.
dialog > form {
display: grid;
grid-template-rows: auto 1fr auto;
align-items: start;
max-block-size: 80vh;
max-block-size: 80dvb;
}
대화상자 <header>
스타일 지정
이 요소의 역할은 대화상자 콘텐츠의 제목을 제공하고 찾기 쉬운 닫기 버튼을 제공하는 것입니다. 대화상자 문서 콘텐츠 뒤에 있는 것처럼 보이도록 노출 영역 색상도 지정됩니다. 이러한 요구사항에 따라 플렉스박스 컨테이너, 가장자리까지 간격이 있는 세로 정렬된 항목, 제목과 닫기 버튼에 여유 공간을 제공하는 패딩과 간격이 생깁니다.
dialog > form > header {
display: flex;
gap: var(--size-3);
justify-content: space-between;
align-items: flex-start;
background: var(--surface-2);
padding-block: var(--size-3);
padding-inline: var(--size-5);
}
@media (prefers-color-scheme: dark) {
dialog > form > header {
background: var(--surface-1);
}
}
헤더 닫기 버튼 스타일 지정
데모에서는 Open Props 버튼을 사용하므로 닫기 버튼은 다음과 같이 원형 아이콘 중심의 버튼으로 맞춤설정됩니다.
dialog > form > header > button {
border-radius: var(--radius-round);
padding: .75ch;
aspect-ratio: 1;
flex-shrink: 0;
place-items: center;
stroke: currentColor;
stroke-width: 3px;
}
대화상자 <article>
스타일 지정
이 대화상자에서 article 요소는 특별한 역할을 합니다. 이 요소는 대화상자가 높거나 긴 경우 스크롤할 수 있는 공간입니다.
이를 위해 상위 양식 요소는 자체적으로 몇 가지 최대값을 설정하여 이 도움말 요소가 너무 커질 경우 도달해야 하는 제약 조건을 제공합니다. 스크롤바가 필요한 경우에만 표시되도록 overflow-y: auto
를 설정하고 overscroll-behavior: contain
를 사용하여 스크롤을 포함하면 나머지는 맞춤 프레젠테이션 스타일이 됩니다.
dialog > form > article {
overflow-y: auto;
max-block-size: 100%; /* safari */
overscroll-behavior-y: contain;
display: grid;
justify-items: flex-start;
gap: var(--size-3);
box-shadow: var(--shadow-2);
z-index: var(--layer-1);
padding-inline: var(--size-5);
padding-block: var(--size-3);
}
@media (prefers-color-scheme: light) {
dialog > form > article {
background: var(--surface-1);
}
}
대화상자 <footer>
스타일 지정
바닥글의 역할은 작업 버튼 메뉴를 포함하는 것입니다. Flexbox는 콘텐츠를 바닥글 인라인 축의 끝에 정렬한 후 버튼에 공간을 제공하는 데 사용됩니다.
dialog > form > footer {
background: var(--surface-2);
display: flex;
flex-wrap: wrap;
gap: var(--size-3);
justify-content: space-between;
align-items: flex-start;
padding-inline: var(--size-5);
padding-block: var(--size-3);
}
@media (prefers-color-scheme: dark) {
dialog > form > footer {
background: var(--surface-1);
}
}
대화상자 바닥글 메뉴 스타일 지정
menu
요소는 대화상자의 작업 버튼을 포함하는 데 사용됩니다. gap
를 사용한 래핑 플렉스박스 레이아웃을 사용하여 버튼 간에 공간을 둡니다. 메뉴 요소에는 <ul>
와 같은 패딩이 있습니다. 또한 이 스타일은 필요하지 않으므로 삭제합니다.
dialog > form > footer > menu {
display: flex;
flex-wrap: wrap;
gap: var(--size-3);
padding-inline-start: 0;
}
dialog > form > footer > menu:only-child {
margin-inline-start: auto;
}
애니메이션
대화상자 요소는 창에 들어갔다 나올 때 애니메이션으로 표시되는 경우가 많습니다. 대화상자에 이러한 진입과 종료에 관한 지원 모션을 제공하면 사용자가 흐름을 파악하는 데 도움이 됩니다.
일반적으로 대화상자 요소는 안쪽으로만 애니메이션을 적용할 수 있습니다. 이는 브라우저가 요소의 display
속성을 전환하기 때문입니다. 이전에는 가이드가 디스플레이를 그리드로 설정하고 none으로 설정하지 않았습니다. 이렇게 하면 확대/축소 애니메이션을 사용할 수 있습니다.
Open Props에는 사용 가능한 여러 키프레임 애니메이션이 포함되어 있어 쉽게 읽을 수 있고 조정할 수 있습니다. 다음은 애니메이션 목표와 제가 취한 계층적 접근 방식입니다.
- 감소된 모션은 기본 전환으로, 간단한 불투명도 페이드 인/아웃입니다.
- 모션이 정상이면 슬라이드 및 크기 조정 애니메이션이 추가됩니다.
- 메가 대화상자의 반응형 모바일 레이아웃이 슬라이드 아웃되도록 조정됩니다.
안전하고 의미 있는 기본 전환
Open Props에는 페이드 인 및 페이드 아웃을 위한 키프레임이 포함되어 있지만, 저는 키프레임 애니메이션을 잠재적인 업그레이드로 두고 전환을 위한 이 계층적 접근 방식을 기본으로 사용하는 것을 선호합니다. 앞서 [open]
속성에 따라 1
또는 0
를 조정하여 불투명도로 대화상자의 공개 상태에 스타일을 지정했습니다. 0%와 100% 간에 전환하려면 브라우저에 원하는 시간과 종류의 이완을 알려야 합니다.
dialog {
transition: opacity .5s var(--ease-3);
}
전환에 모션 추가
사용자가 움직임에 동의한다면 메가 및 미니 대화상자가 모두 입구로 위로 슬라이드되고 이탈 시 확장되어야 합니다. prefers-reduced-motion
미디어 쿼리와 몇 가지 Open 프로퍼티를 사용하여 이를 실행할 수 있습니다.
@media (prefers-reduced-motion: no-preference) {
dialog {
animation: var(--animation-scale-down) forwards;
animation-timing-function: var(--ease-squish-3);
}
dialog[open] {
animation: var(--animation-slide-in-up) forwards;
}
}
모바일용 나가기 애니메이션 조정
스타일 지정 섹션의 앞부분에서 메가 대화상자 스타일은 휴대기기에 맞게 조정되어 마치 작은 종이 한 장이 화면 하단에서 위로 밀려 올라가면서 여전히 하단에 붙어 있는 것처럼 작업 시트와 더 유사해졌습니다. 축소 종료 애니메이션이 이 새 디자인에 잘 맞지 않으며, 몇 가지 미디어 쿼리와 일부 Open Props를 사용하여 이를 조정할 수 있습니다.
@media (prefers-reduced-motion: no-preference) and @media (max-width: 768px) {
dialog[modal-mode="mega"] {
animation: var(--animation-slide-out-down) forwards;
animation-timing-function: var(--ease-squish-2);
}
}
자바스크립트
JavaScript로 추가할 수 있는 항목은 다음과 같습니다.
// dialog.js
export default async function (dialog) {
// add light dismiss
// add closing and closed events
// add opening and opened events
// add removed event
// removing loading attribute
}
이러한 추가사항은 가볍게 닫기 (대화상자 배경화면 클릭), 애니메이션 및 양식 데이터를 가져오는 타이밍을 개선하기 위한 추가 이벤트에서 비롯됩니다.
표시등 닫기 추가 중
이 작업은 간단하며 애니메이션이 적용되지 않는 대화상자 요소에 추가하면 좋습니다. 상호작용은 대화상자 요소의 클릭을 관찰하고 이벤트 버블링을 활용하여 클릭된 항목을 평가하여 이루어지며, 최상위 요소인 경우에만 close()
됩니다.
export default async function (dialog) {
dialog.addEventListener('click', lightDismiss)
}
const lightDismiss = ({target:dialog}) => {
if (dialog.nodeName === 'DIALOG')
dialog.close('dismiss')
}
dialog.close('dismiss')
을 확인합니다. 이벤트가 호출되고 문자열이 제공됩니다.
이 문자열은 다른 JavaScript에서 가져와 대화상자가 닫힌 방식에 관한 유용한 정보를 얻을 수 있습니다. 또한 다양한 버튼에서 함수를 호출할 때마다 닫기 문자열을 제공하여 애플리케이션에 사용자 상호작용에 관한 컨텍스트를 제공했습니다.
종료 및 종료된 이벤트 추가
대화상자 요소는 닫기 이벤트와 함께 제공되며, 대화상자 close()
함수가 호출되면 즉시 발생합니다. 이 요소에 애니메이션을 적용하고 있으므로 애니메이션 전후에 이벤트가 있어 데이터를 가져오거나 대화상자 양식을 재설정하는 것이 좋습니다. 여기서는 닫힌 대화상자에 inert
속성 추가를 관리하는 데 사용하고 데모에서는 사용자가 새 이미지를 제출한 경우 아바타 목록을 수정하는 데 사용합니다.
이렇게 하려면 closing
및 closed
라는 두 가지 새 이벤트를 만듭니다. 그런 다음 대화상자에서 내장 닫기 이벤트를 리슨합니다. 여기에서 대화상자를 inert
로 설정하고 closing
이벤트를 전달합니다. 다음 작업은 대화상자에서 애니메이션과 전환이 실행을 완료할 때까지 기다린 다음 closed
이벤트를 전달하는 것입니다.
const dialogClosingEvent = new Event('closing')
const dialogClosedEvent = new Event('closed')
export default async function (dialog) {
…
dialog.addEventListener('close', dialogClose)
}
const dialogClose = async ({target:dialog}) => {
dialog.setAttribute('inert', '')
dialog.dispatchEvent(dialogClosingEvent)
await animationsComplete(dialog)
dialog.dispatchEvent(dialogClosedEvent)
}
const animationsComplete = element =>
Promise.allSettled(
element.getAnimations().map(animation =>
animation.finished))
토스트 구성요소 빌드에서도 사용되는 animationsComplete
함수는 애니메이션 및 전환 약속의 완료를 기반으로 약속을 반환합니다. 이것이 dialogClose
가 비동기 함수인 이유입니다. 그러면 반환된 프로미스를 await
하고 닫힌 이벤트로 확실하게 이동할 수 있습니다.
개업 및 개업 일정 추가
이러한 이벤트는 기본 제공 대화상자 요소가 닫기와 마찬가지로 열기 이벤트를 제공하지 않으므로 추가하기가 쉽지 않습니다. MutationObserver를 사용하여 대화상자의 속성 변경에 관한 유용한 정보를 제공합니다. 이 관찰자에서는 열린 속성의 변경사항을 확인하고 그에 따라 맞춤 이벤트를 관리합니다.
종료 및 종료됨 이벤트를 시작한 것과 비슷하게 opening
및 opened
라는 두 가지 새 이벤트를 만듭니다. 이전에 대화상자 닫기 이벤트를 수신 대기한 경우 이번에는 생성된 변형 관찰자를 사용하여 대화상자의 속성을 확인합니다.
…
const dialogOpeningEvent = new Event('opening')
const dialogOpenedEvent = new Event('opened')
export default async function (dialog) {
…
dialogAttrObserver.observe(dialog, {
attributes: true,
})
}
const dialogAttrObserver = new MutationObserver((mutations, observer) => {
mutations.forEach(async mutation => {
if (mutation.attributeName === 'open') {
const dialog = mutation.target
const isOpen = dialog.hasAttribute('open')
if (!isOpen) return
dialog.removeAttribute('inert')
// set focus
const focusTarget = dialog.querySelector('[autofocus]')
focusTarget
? focusTarget.focus()
: dialog.querySelector('button').focus()
dialog.dispatchEvent(dialogOpeningEvent)
await animationsComplete(dialog)
dialog.dispatchEvent(dialogOpenedEvent)
}
})
})
대화상자 속성이 변경되면 변형 관찰자 콜백 함수가 호출되어 변경사항 목록을 배열로 제공합니다. 속성 변경사항을 반복하여 열려 있는 attributeName
를 찾습니다. 그런 다음 요소에 속성이 있는지 확인합니다. 이렇게 하면 대화상자가 열렸는지 여부를 알 수 있습니다. 대화상자가 열렸다면 inert
속성을 삭제하고 포커스를 autofocus
를 요청하는 요소 또는 대화상자에서 찾은 첫 번째 button
요소로 설정합니다. 마지막으로 닫기 및 닫힘 이벤트와 마찬가지로 즉시 열기 이벤트를 전달하고 애니메이션이 완료될 때까지 기다린 다음 열림 이벤트를 전달합니다.
삭제된 일정 추가
단일 페이지 애플리케이션에서는 경로 또는 기타 애플리케이션 요구사항 및 상태에 따라 대화상자가 추가 및 삭제되는 경우가 많습니다. 대화상자가 삭제될 때 이벤트나 데이터를 정리하는 것이 유용할 수 있습니다.
다른 변형 관찰자로 이를 실행할 수 있습니다. 이번에는 대화상자 요소의 속성을 관찰하는 대신 본문 요소의 하위 요소를 관찰하고 대화상자 요소가 삭제되는지 확인합니다.
…
const dialogRemovedEvent = new Event('removed')
export default async function (dialog) {
…
dialogDeleteObserver.observe(document.body, {
attributes: false,
subtree: false,
childList: true,
})
}
const dialogDeleteObserver = new MutationObserver((mutations, observer) => {
mutations.forEach(mutation => {
mutation.removedNodes.forEach(removedNode => {
if (removedNode.nodeName === 'DIALOG') {
removedNode.removeEventListener('click', lightDismiss)
removedNode.removeEventListener('close', dialogClose)
removedNode.dispatchEvent(dialogRemovedEvent)
}
})
})
})
변형 관찰자 콜백은 문서 본문에서 하위 요소가 추가 또는 삭제될 때마다 호출됩니다. 감시되는 특정 변형은 대화의 nodeName
가 있는 removedNodes
에 관한 것입니다. 대화상자가 삭제되면 메모리를 확보하기 위해 클릭 및 닫기 이벤트가 삭제되고 맞춤 삭제 이벤트가 전달됩니다.
로드 속성 삭제
페이지에 추가되거나 페이지 로드 시 대화상자 애니메이션이 종료 애니메이션을 재생하지 않도록 하기 위해 로드 속성이 대화상자에 추가되었습니다. 다음 스크립트는 대화상자 애니메이션 실행이 완료될 때까지 기다린 후 속성을 삭제합니다. 이제 대화상자가 자유롭게 표시되고 사라질 수 있으며, 방해가 되는 애니메이션은 효과적으로 숨겨졌습니다.
export default async function (dialog) {
…
await animationsComplete(dialog)
dialog.removeAttribute('loading')
}
페이지 로드 시 키프레임 애니메이션 방지 문제에 대해 자세히 알아보세요.
모두 합쳐
다음은 각 섹션을 개별적으로 설명했으므로 전체 dialog.js
입니다.
// custom events to be added to <dialog>
const dialogClosingEvent = new Event('closing')
const dialogClosedEvent = new Event('closed')
const dialogOpeningEvent = new Event('opening')
const dialogOpenedEvent = new Event('opened')
const dialogRemovedEvent = new Event('removed')
// track opening
const dialogAttrObserver = new MutationObserver((mutations, observer) => {
mutations.forEach(async mutation => {
if (mutation.attributeName === 'open') {
const dialog = mutation.target
const isOpen = dialog.hasAttribute('open')
if (!isOpen) return
dialog.removeAttribute('inert')
// set focus
const focusTarget = dialog.querySelector('[autofocus]')
focusTarget
? focusTarget.focus()
: dialog.querySelector('button').focus()
dialog.dispatchEvent(dialogOpeningEvent)
await animationsComplete(dialog)
dialog.dispatchEvent(dialogOpenedEvent)
}
})
})
// track deletion
const dialogDeleteObserver = new MutationObserver((mutations, observer) => {
mutations.forEach(mutation => {
mutation.removedNodes.forEach(removedNode => {
if (removedNode.nodeName === 'DIALOG') {
removedNode.removeEventListener('click', lightDismiss)
removedNode.removeEventListener('close', dialogClose)
removedNode.dispatchEvent(dialogRemovedEvent)
}
})
})
})
// wait for all dialog animations to complete their promises
const animationsComplete = element =>
Promise.allSettled(
element.getAnimations().map(animation =>
animation.finished))
// click outside the dialog handler
const lightDismiss = ({target:dialog}) => {
if (dialog.nodeName === 'DIALOG')
dialog.close('dismiss')
}
const dialogClose = async ({target:dialog}) => {
dialog.setAttribute('inert', '')
dialog.dispatchEvent(dialogClosingEvent)
await animationsComplete(dialog)
dialog.dispatchEvent(dialogClosedEvent)
}
// page load dialogs setup
export default async function (dialog) {
dialog.addEventListener('click', lightDismiss)
dialog.addEventListener('close', dialogClose)
dialogAttrObserver.observe(dialog, {
attributes: true,
})
dialogDeleteObserver.observe(document.body, {
attributes: false,
subtree: false,
childList: true,
})
// remove loading attribute
// prevent page load @keyframes playing
await animationsComplete(dialog)
dialog.removeAttribute('loading')
}
dialog.js
모듈 사용
모듈에서 내보낸 함수는 다음과 같은 새 이벤트와 기능을 추가하려는 대화상자 요소가 호출되고 전달될 것으로 예상합니다.
import GuiDialog from './dialog.js'
const MegaDialog = document.querySelector('#MegaDialog')
const MiniDialog = document.querySelector('#MiniDialog')
GuiDialog(MegaDialog)
GuiDialog(MiniDialog)
이렇게 해서 두 대화상자가 조용히 닫기, 애니메이션 로드 수정, 더 많은 이벤트를 사용할 수 있도록 업그레이드되었습니다.
새 맞춤 이벤트 수신 대기
이제 업그레이드된 각 대화상자 요소는 다음과 같이 5개의 새 이벤트를 수신 대기할 수 있습니다.
MegaDialog.addEventListener('closing', dialogClosing)
MegaDialog.addEventListener('closed', dialogClosed)
MegaDialog.addEventListener('opening', dialogOpening)
MegaDialog.addEventListener('opened', dialogOpened)
MegaDialog.addEventListener('removed', dialogRemoved)
다음은 이러한 이벤트를 처리하는 두 가지 예입니다.
const dialogOpening = ({target:dialog}) => {
console.log('Dialog opening', dialog)
}
const dialogClosed = ({target:dialog}) => {
console.log('Dialog closed', dialog)
console.info('Dialog user action:', dialog.returnValue)
if (dialog.returnValue === 'confirm') {
// do stuff with the form values
const dialogFormData = new FormData(dialog.querySelector('form'))
console.info('Dialog form data', Object.fromEntries(dialogFormData.entries()))
// then reset the form
dialog.querySelector('form')?.reset()
}
}
대화상자 요소로 빌드한 데모에서는 닫힌 이벤트와 양식 데이터를 사용하여 목록에 새 아바타 요소를 추가합니다. 대화상자가 종료 애니메이션을 완료하면 일부 스크립트가 새 아바타로 애니메이션 처리되기 때문에 타이밍이 좋습니다. 새로운 이벤트 덕분에 사용자 환경을 더 원활하게 조정할 수 있습니다.
dialog.returnValue
알림: 여기에는 대화상자 close()
이벤트가 호출될 때 전달된 닫기 문자열이 포함됩니다. dialogClosed
이벤트에서는 대화상자가 닫혔는지, 취소되었는지, 확인되었는지 확인하는 것이 중요합니다. 확인되면 스크립트는 양식 값을 가져와 양식을 재설정합니다. 재설정은 대화상자가 다시 표시될 때 비어 있고 새로 제출할 준비가 되도록 하는 데 유용합니다.
결론
이제 제가 어떻게 했는지 알았으니 어떻게 하시겠어요? 🙂
접근 방식을 다양화하고 웹에서 빌드하는 모든 방법을 알아보겠습니다.
데모를 만들고 트윗해 주세요 링크. 아래 커뮤니티 리믹스 섹션에 추가하겠습니다.
커뮤니티 리믹스
- 3-in-1 대화상자가 있는 @GrimLink
display
속성을 변경하지 않는 멋진 리믹스를 제공한 @mikemai2awesome- @geoffrich_님은 Svelte와 멋진 Svelte FLIP 광택을 제공합니다.
리소스
- GitHub의 소스 코드
- 기념일 로고 아바타