웹 접근성 이슈로 인한 display
와 transition
문제
작업을 진행할 때, 웹 접근성 이슈로 display
와 transition
을 사용해야 하는 경우가 생긴다.(다른 방법 있나? 있음 알려주쇼)
그런데 다들 알고 있겠지만, display: none
상태에서 transition
이 작동하지 않는다.
display
속성 자체가 애니메이션화(transition)할 수 없기 때문이고, display
속성은 요소가 화면에 표시될지 여부를 결정하는 속성인데, 이는 이진 상태(보이거나 보이지 않음)로 처리되기 때문이다.
따라서 display: none
에서 display: block
등으로 변경할 때 중간 상태가 없으므로 transition
애니메이션이 작동하지 않는다.
뭐 암튼 그렇다고 나도 잘 몰라 🥲
top, center, bottom을 적용해놨고 1개했는데 3개를 쓸 수 있는 효과~
골치 아픈 상황
여하튼 이게 참 골때리는 게, 꼭 display
와 transition
을 공존해서 사용해야 하는 경우가 생긴다.
CSS와 JS의 부분에 아직 리팩토링을 더 진행해야 하는 부분이 있지만, 사실 CSS는 건들지도 않음... 😂
결론
거, 간단하게 좀 갑시다.
import styled, { css } from 'styled-components';
// 공통 스타일 정의
const CommonSkin = css`
// 딤
position: fixed;
bottom: 0;
left: 0;
width: 100%;
height: 100%;
flex-direction: column;
z-index: 1000;
overflow: hidden;
transition: opacity 0.5s ease, visibility 0.5s ease;
background-color: rgba(0, 0, 0, 0.5);
> .inner {
height: 0; /* 초기 높이 */
box-sizing: border-box;
transition: height 0.3s ease-in-out; /* 트랜지션 애니메이션 settimeout의 일치시킨다. */
background: #fff;
overflow: hidden;
}
&.isActive {
display: flex;
visibility: visible; /* 보이도록 설정 */
opacity: 1; /* 불투명하도록 설정 */
> .inner {
height: auto; /* 높이를 자동으로 설정하여 콘텐츠가 보이도록 설정 */
}
}
&.popup-top {
justify-content: flex-start;
}
&.popup-center {
justify-content: center;
> .inner {
width: 80%;
margin: 0 auto;
}
}
&.popup-bottom {
justify-content: flex-end;
}
`;
const BasicSkin = css`
/* 추가적인 BasicSkin 스타일이 여기에 들어갈 수 있습니다. */
`;
const ThemeSkin = css`
/* 추가적인 ThemeSkin 스타일이 여기에 들어갈 수 있습니다. */
`;
const Style = (type) => {
switch (type) {
case 'BasicSkin':
return styled.div`
${CommonSkin}
${BasicSkin}
`;
case 'ThemeSkin':
return styled.div`
${CommonSkin}
${ThemeSkin}
`;
default:
return styled.div`
${CommonSkin}
${BasicSkin}
`;
}
};
export default Style;
// Modal 컴포넌트
import React, { useState, useEffect, useRef } from 'react';
import Style from './ModalStyle'; // CSS 모듈을 임포트합니다.
import { IconClosed } from '../../assets/images/common/IconSet';
import Heading from '../Heading/Heading';
const Modal = Style('BasicSkin'); // 스킨 타입에 맞게 스타일 적용
const ModalWrap = ({ title, content, className, modalOpen, closeModal }) => {
const popupRef = useRef(null);
const [isClosing, setIsClosing] = useState(false); // 모달 닫기 상태를 관리
// 키보드 포커스 이동 웹 접근성 관련
useEffect(() => {
const popup = popupRef.current;
if (!popup) return;
// 포커스 가능한 요소들 찾기
const focusableElements = popup.querySelectorAll(
'a[href], area[href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), button:not([disabled]), iframe, object, embed, *[tabindex]:not([tabindex="-1"]), *[contenteditable]'
);
if (focusableElements.length === 0) return;
const firstFocusableElement = focusableElements[0];
const lastFocusableElement = focusableElements[focusableElements.length - 1];
const handleTabKey = (e) => {
if (e.key === 'Tab') {
if (e.shiftKey) {
// Shift + Tab: 역방향 이동
if (document.activeElement === firstFocusableElement) {
e.preventDefault();
lastFocusableElement.focus();
}
} else {
// Tab: 순방향 이동
if (document.activeElement === lastFocusableElement) {
e.preventDefault();
firstFocusableElement.focus();
}
}
}
};
// 팝업 열렸을 때 첫 번째 포커스 가능한 요소로 포커스 이동
firstFocusableElement?.focus();
// 포커스 트랩을 위한 이벤트 핸들러 등록
document.addEventListener('keydown', handleTabKey);
return () => {
// cleanup
document.removeEventListener('keydown', handleTabKey);
};
}, [modalOpen]);
// 탑, 바톰 팝업이 열릴때 실행
useEffect(() => {
if (popupRef.current) {
const isChecked = popupRef.current.classList.contains('popup-center');
if (isChecked) return;
}
if (modalOpen && popupRef.current) {
const inner = popupRef.current.querySelector('.inner');
if (inner) {
inner.style.height = '0px'; // 애니메이션 시작 전 높이를 0으로 설정
const height = inner.scrollHeight; // 실제 높이를 계산
inner.style.height = `${height}px`; // 높이를 설정하여 슬라이드 다운 애니메이션 적용
}
}
}, [modalOpen]); // 모달이 열릴 때만 실행되도록 합니다.
// 팝업이 닫힐 때
const handleClose = () => {
if (popupRef.current) {
const isChecked = popupRef.current.classList.contains('popup-center');
if (isChecked) {
// 디폴트 "센터" 팝업이 닫힐때
closeModal(); // 전달받은 closeModal을 닫는다.
setIsClosing(false); // 닫히는 상태를 다시 false로 설정
} else {
// 탑, 바톰 팝업이 닫힐때
setIsClosing(true); // 닫히는 상태를 true로 설정합니다.
const inner = popupRef.current.querySelector('.inner');
if (inner) {
inner.style.height = '0px'; // 높이를 0으로 설정하여 닫히는 애니메이션 실행
}
// 애니메이션이 끝난 후에 모달을 닫습니다.
setTimeout(() => {
closeModal(); // 전달받은 closeModal을 닫는다.
setIsClosing(false); // 닫히는 상태를 다시 false로 설정
}, 300); // 트랜지션 시간과 일치시킵니다 (0.3초)
}
}
};
if (!modalOpen && !isClosing) return null; // 모달이 보이지 않을 때는 렌더링 안함.
return (
<Modal
className={
modalOpen ? `isActive ${className || 'popup-center'}` : className || 'popup-center'
}
ref={popupRef}
style={{ display: modalOpen || isClosing ? 'flex' : 'none' }}
>
<div className={'inner'}>
<header>
<Heading level={2} title={title || '팝업 기본 타이틀 입니다.'} />
<button className={'btn-closed'} onClick={handleClose}>
<IconClosed />
<span className={'blind'}>닫기</span>
</button>
</header>
<main>{content || '팝업 컨텐츠를 넣어주세요.'}</main>
<footer>
<button className="close" onClick={handleClose}>
close
</button>
</footer>
</div>
</Modal>
);
};
export default ModalWrap;
import React, { useEffect, useState, useRef } from 'react';
import Button from '../../Components/Button/Button';
import Modal from '../../Components/Popup/Modal';
const MainWrap = Style(APP_SKIN);
const PubMain = () => {
const [isModalOpen, setIsModalOpen] = useState(false);
// 모달 팝업 포커스 이동 관련
const triggerButtonRef = useRef(null); // 버튼의 참조를 저장하기 위한 ref
// 모달 열기
const openModal = (e) => {
triggerButtonRef.current = e.currentTarget; // 클릭된 버튼 참조를 저장
setIsModalOpen(true);
console.log('열기 버튼 클릭 ');
};
// 모달 닫기
const closeModal = () => {
setIsModalOpen(false);
console.log('닫기 버튼 클릭');
};
// 모달이 닫힌 후에 포커스를 저장된 버튼으로 이동
useEffect(() => {
if (!isModalOpen && triggerButtonRef.current) {
triggerButtonRef.current.focus(); // 저장된 버튼으로 포커스 이동
}
}, [isModalOpen]);
const modalItem = {
title: '타이틀',
content: '컨텐츠 그리고 또는 컴포넌트',
className: 'popup-top' // popup-top, popup-bottom 를 제공하고 default는 center이 된다.
};
return (
<>
{/*모달 팝업 샘플 테스트 */}
<Button onClick={openModal} name={'모달 팝업'} />
<Modal
title={modalItem.title}
content={modalItem.content}
className={modalItem.className}
modalOpen={isModalOpen}
closeModal={closeModal}
/>
</>
);
};
export default PubMain;
'개발연습막쓰기 > React 개발연습 막쓰기' 카테고리의 다른 글
쇼핑몰 및 채팅 서비스에서 별점 주기 컴포넌트 구현 (0) | 2024.09.09 |
---|---|
[RN] - react-native-image-crop-picker (0) | 2020.09.11 |
[RN] - Device top height (0) | 2020.09.02 |
[RN] hitSlop (0) | 2020.08.27 |
RN ios, android 디바이스별 폰트 설정 관련 (0) | 2020.08.26 |