macOS에서 Microsoft Teams 사용, 설치, 기능 및 호환성 포함
참고: 이 답변은 자동으로 번역되었습니다. 그 결과 문법 오류나 어색한 표현이 포함될 수 있습니다.
안녕하세요 지원 정
Microsoft Q&A 포럼에 질문을 올려주셔서 감사합니다
명확히 말씀드리면, 저는 포럼 모더레이터로서 동일한 환경에서 이 문제를 재현하고 최적의 해결책을 찾는 데 제한이 있습니다. 그러나 귀하가 설명하신 동작과 정확히 일치하는 사용자 보고서를 발견했습니다: https://github.com/OfficeDev/microsoft-teams-library-js/issues/2920
이 스레드에 연결된 리소스는 영어로만 제공됩니다. 한국어 콘텐츠를 선호하거나 필요로 하는 분들께 불편을 끼쳐드려 진심으로 사과드립니다. 안타깝게도 해당 자료의 한국어 버전은 현재 제공되지 않습니다. 내용을 이해하는 데 도움이 필요하시면 신뢰할 수 있는 번역 도구나 서비스를 사용하시길 권장합니다.
이 보고서는 최근에 열렸으며, 최소한 한 명 이상의 사용자가 동일한 예기치 않은 동작을 겪고 있음을 나타냅니다. 아쉽게도 아직 공식적인 해결책은 제공되지 않았으며, Microsoft가 문제를 확인하고 추가 조사를 진행할 때까지 기다려야 합니다.
그동안 가능한 우회 방법을 검토했습니다. 그 중 하나는 네이티브 <select> 태그를 사용하지 않고 동일한 기능을 구현하는 커스텀 요소를 만드는 것입니다. 저는 온라인 미리보기 도구에서 테스트했지만 macOS용 Teams 데스크톱 환경에서는 직접 테스트하지 못했습니다. 아래의 커스텀 HTML, CSS, JavaScript를 적용하는 것을 고려해 보시기 바랍니다.
HTML:
<!-- 접근 가능한 커스텀 셀렉트(콤보박스 + 리스트박스) -->
<div class="select" id="projectStatus" data-placeholder="Select a status">
<!-- 폼 제출용 숨김 입력: 현재 선택 값을 반영 -->
<input type="hidden" name="projectStatus" value="">
<!-- 콤보박스 버튼(접힌 상태의 셀렉트 역할) -->
<button
type="button"
class="select__button"
aria-haspopup="listbox"
aria-expanded="false"
aria-controls="projectStatus-listbox"
aria-labelledby="projectStatus-label projectStatus-button"
id="projectStatus-button">
<span id="projectStatus-label" class="select__label">Status</span>
<span class="select__value" data-empty="true">Select a status</span>
<span class="select__chevron" aria-hidden="true">▾</span>
</button>
<!-- 팝업 리스트박스(시스템 팝오버가 아닌 DOM 내부에 렌더링) -->
<div class="select__popup" role="listbox" id="projectStatus-listbox"
tabindex="-1" aria-labelledby="projectStatus-label"
aria-activedescendant="">
<div role="option" class="select__option" id="opt-1" data-value="new" aria-selected="false">New</div>
<div role="option" class="select__option" id="opt-2" data-value="in_progress" aria-selected="false">In Progress</div>
<div role="option" class="select__option" id="opt-3" data-value="blocked" aria-selected="false">Blocked</div>
<div role="option" class="select__option" id="opt-4" data-value="done" aria-selected="false">Done</div>
</div>
</div>
CSS:
/* 루트 컨테이너 */
.select {
position: relative;
display: inline-block;
font: 14px/1.4 system-ui, -apple-system, Segoe UI, Roboto, sans-serif;
color: #111;
}
/* 버튼(접힌 상태 표시) */
.select__button {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 6px 10px;
min-width: 200px;
border: 1px solid #8a8a8a;
border-radius: 4px;
background: #fff;
cursor: pointer;
}
/* 포커스 가시성(Teams Fluent 톤에 맞춘 색상 계열) */
.select__button:focus-visible {
outline: 2px solid #2563eb;
outline-offset: 2px;
}
/* 플레이스홀더 색상 */
.select__value[data-empty="true"] {
color: #6b7280;
}
/* 오른쪽 정렬을 위한 셰브론 */
.select__chevron { margin-left: auto; }
/* 팝업(리스트박스) */
.select__popup {
position: absolute;
z-index: 1000;
top: calc(100% + 4px);
left: 0;
min-width: 100%;
max-height: 220px;
overflow: auto;
border: 1px solid #d1d5db;
border-radius: 4px;
background: #fff;
box-shadow: 0 8px 16px rgba(0,0,0,.12);
display: none; /* 기본적으로 닫힘 */
}
/* 열린 상태(자바스크립트로 토글) */
.select--open .select__popup {
display: block;
}
/* 옵션 아이템 */
.select__option {
padding: 8px 10px;
cursor: pointer;
}
/* 선택된 옵션 강조(aria-selected="true") */
.select__option[aria-selected="true"] {
background: #eef2ff;
}
/* 마우스 호버 상태 */
.select__option:hover {
background: #f3f4f6;
}
/* 키보드 네비게이션으로 활성(현재 위치)된 옵션 표시 */
.select__option.is-active {
background: #e0f2fe; /* 호버와 구분되는 연한 강조색 */
outline: 2px solid #2563eb22; /* 부드러운 포커스 윤곽선 */
}
/* 선택된 옵션의 가독성 향상(굵게) */
.select__option[aria-selected="true"] {
background: #eef2ff;
font-weight: 600;
}
JS:
<script>
(() => {
// 단일 셀렉트 컴포넌트 초기화 함수
const initSelect = (root) => {
const button = root.querySelector('.select__button');
const popup = root.querySelector('.select__popup');
const options = Array.from(popup.querySelectorAll('.select__option'));
const valueEl = root.querySelector('.select__value');
const hiddenInput = root.querySelector('input[type="hidden"]');
let open = false;
let activeIndex = -1; // 키보드로 하이라이트된 옵션 인덱스
let typeahead = ''; // 타이프어헤드 버퍼
let typeaheadTimer = null; // 타이프어헤드 타이머
// 열린/닫힌 상태 토글 및 포커스 관리
const setExpanded = (expanded) => {
open = expanded;
root.classList.toggle('select--open', expanded);
button.setAttribute('aria-expanded', String(expanded));
if (expanded) {
// 팝업에 포커스 이동(스크롤 점프 방지)
popup.focus({ preventScroll: true });
// 현재 선택값 기준으로 활성 인덱스 설정
const currentValue = hiddenInput.value;
activeIndex = options.findIndex((o) => o.dataset.value === currentValue);
if (activeIndex < 0) activeIndex = 0;
highlight(activeIndex);
ensureVisible(options[activeIndex], popup);
} else {
// 닫을 때 버튼으로 포커스 되돌림
button.focus({ preventScroll: true });
}
};
// 특정 인덱스 선택(값 반영 + 이벤트 발생)
const selectIndex = (i) => {
const opt = options[i];
if (!opt) return;
options.forEach((o) => o.setAttribute('aria-selected', 'false'));
opt.setAttribute('aria-selected', 'true');
valueEl.textContent = opt.textContent.trim();
valueEl.dataset.empty = 'false';
hiddenInput.value = opt.dataset.value;
// 폼 로직을 위한 change 이벤트 디스패치
hiddenInput.dispatchEvent(new Event('change', { bubbles: true }));
};
// 활성(현재 키보드 위치) 하이라이트 + ARIA 동기화
const highlight = (i) => {
activeIndex = Math.max(0, Math.min(options.length - 1, i));
options.forEach((o, idx) => {
o.classList.toggle('is-active', idx === activeIndex);
});
// 스크린 리더가 현재 활성 옵션을 인지하도록 설정
const activeOpt = options[activeIndex];
popup.setAttribute('aria-activedescendant', activeOpt.id);
// 활성 항목이 뷰포트 안에 보이도록 스크롤 조정
ensureVisible(activeOpt, popup);
};
// 활성 항목을 가시 영역에 유지
const ensureVisible = (el, container) => {
if (!el) return;
const cr = container.getBoundingClientRect();
const er = el.getBoundingClientRect();
if (er.top < cr.top) {
container.scrollTop -= (cr.top - er.top);
} else if (er.bottom > cr.bottom) {
container.scrollTop += (er.bottom - cr.bottom);
}
};
// 팝업 열기(현재 선택값 기준으로 활성 설정)
const openPopup = () => {
setExpanded(true);
const currentValue = hiddenInput.value;
const currentIdx = options.findIndex((o) => o.dataset.value === currentValue);
activeIndex = currentIdx >= 0 ? currentIdx : 0;
highlight(activeIndex);
};
// 팝업 닫기
const closePopup = () => setExpanded(false);
// 버튼 상호작용: 클릭으로 토글
button.addEventListener('click', () => {
setExpanded(!open);
});
// 버튼 키보드: 열기/빠른 선택
button.addEventListener('keydown', (e) => {
switch (e.key) {
case 'ArrowDown':
case 'ArrowUp':
case 'Enter':
case ' ':
e.preventDefault();
openPopup();
break;
case 'Home':
e.preventDefault();
selectIndex(0);
break;
case 'End':
e.preventDefault();
selectIndex(options.length - 1);
break;
}
});
// 팝업 키보드 네비게이션
popup.addEventListener('keydown', (e) => {
switch (e.key) {
case 'ArrowDown':
e.preventDefault();
highlight(activeIndex + 1);
break;
case 'ArrowUp':
e.preventDefault();
highlight(activeIndex - 1);
break;
case 'PageDown':
e.preventDefault();
highlight(activeIndex + 5);
break;
case 'PageUp':
e.preventDefault();
highlight(activeIndex - 5);
break;
case 'Home':
e.preventDefault();
highlight(0);
break;
case 'End':
e.preventDefault();
highlight(options.length - 1);
break;
case 'Enter':
case ' ':
e.preventDefault();
selectIndex(activeIndex);
closePopup();
break;
case 'Escape':
e.preventDefault();
closePopup();
break;
default:
// 타이프어헤드: 입력한 글자로 시작하는 옵션 찾기
if (e.key.length === 1) {
typeahead += e.key.toLowerCase();
clearTimeout(typeaheadTimer);
typeaheadTimer = setTimeout(() => { typeahead = ''; }, 600);
const found = options.findIndex((o) =>
o.textContent.trim().toLowerCase().startsWith(typeahead)
);
if (found >= 0) highlight(found);
}
}
});
// 옵션 클릭 선택
options.forEach((opt, idx) => {
opt.addEventListener('click', (e) => {
e.preventDefault();
highlight(idx);
selectIndex(idx);
closePopup();
});
});
// 바깥 클릭 시 닫기(컴포넌트 외부 클릭 감지)
document.addEventListener('mousedown', (e) => {
if (!root.contains(e.target)) closePopup();
});
// WKWebView의 blur 연쇄를 완화: 컴포넌트 내부 포커스 유지
popup.addEventListener('blur', (e) => {
// blur가 컴포넌트 외부로 이동하면 닫기
if (!root.contains(e.relatedTarget)) closePopup();
}, true);
// 초기 플레이스홀더/기본 선택값 설정
const placeholder = root.dataset.placeholder;
if (placeholder) {
valueEl.textContent = placeholder;
valueEl.dataset.empty = 'true';
} else {
selectIndex(0);
}
};
// 페이지 내 모든 .select 컴포넌트 초기화
document.querySelectorAll('.select').forEach(initSelect);
})();
</script>
도움이 되길 바랍니다.
답변이 도움이 된다면 **"답변 수락"**을 클릭하고 친절하게 추천해 주세요. 이 답변에 대해 추가 질문이 있으시면 "댓글"을 클릭해 주세요.
참고: 이 스레드와 관련된 이메일 알림을 받으려면 당사 문서의 안내 절차를 따라 이메일 알림을 활성화해 주시기 바랍니다.