Skip to content

Selectย #4

@hsskey

Description

@hsskey

๐Ÿ“‹ ์ปดํฌ๋„ŒํŠธ ์ •๋ณด

  • ์ปดํฌ๋„ŒํŠธ ์ด๋ฆ„: Select
  • ์ปดํฌ๋„ŒํŠธ ์„ค๋ช…: Select ์ปดํฌ๋„ŒํŠธ๋Š” ์‚ฌ์šฉ์ž๊ฐ€ ์ œํ•œ๋œ ์˜ต์…˜ ์ง‘ํ•ฉ์—์„œ ํ•˜๋‚˜ ์ด์ƒ์˜ ์˜ต์…˜์„ ์„ ํƒํ•  ์ˆ˜ ์žˆ๋„๋ก ํ•˜๋Š” ์ปจํŠธ๋กค
  • ์‚ฌ์šฉ ์‚ฌ๋ก€: ์‚ฌ์šฉ์ž์—๊ฒŒ ์„ ํƒ ์˜ต์…˜์„ ์ œ๊ณตํ•˜๊ณ ์ž ํ•  ๋•Œ ์‚ฌ์šฉ. ๊ตญ๊ฐ€ ๋ชฉ๋ก, ์นดํ…Œ๊ณ ๋ฆฌ ์„ ํƒ, ์„ค์ • ๋“ฑ ๋‹ค์–‘ํ•œ ์ƒํ™ฉ์—์„œ ํ™œ์šฉ ๊ฐ€๋Šฅ

๐Ÿ“š ์ฐธ์กฐ ๋ฐ ๋ ˆํผ๋Ÿฐ์Šค

  • ARIA Authoring Practices: W3C ARIA Patters
    • ๊ด€๋ จ ํŒจํ„ด ๋ฐ ์ ์šฉ๋œ ์ ‘๊ทผ์„ฑ ๊ธฐ์ค€ ํ™•์ธ
  • ๊ธฐํƒ€ ๋ ˆํผ๋Ÿฐ์Šค: MUI, Ant Design ๋“ฑ์˜ ๋ฌธ์„œ์™€ ๋น„๊ตํ•˜์—ฌ ํŒจํ„ด ๋ถ„์„

๐Ÿ“ ์ ‘๊ทผ์„ฑ ๋ฐ ARIA ์ ์šฉ ์—ฌ๋ถ€

  • ARIA Pattern ์ ์šฉ: ARIA Pattern ๋งํฌ
    • ์‚ฌ์šฉ๋œ ARIA ์†์„ฑ: role="combobox", aria-controls, aria-expanded, aria-activedescendant ๋“ฑ์ด ์‚ฌ์šฉ๋จ. role="combobox"๋กœ ์š”์†Œ๋ฅผ ์ฝค๋ณด๋ฐ•์Šค๋กœ ์‹๋ณ„ํ•˜๊ณ , aria-controls๋กœ ํŒ์—… ์š”์†Œ๋ฅผ ์ฐธ์กฐํ•จ. aria-expanded๋Š” ํŒ์—… ์ƒํƒœ๋ฅผ ๋‚˜ํƒ€๋‚ด๊ณ , aria-activedescendant๋Š” ํ˜„์žฌ ํฌ์ปค์Šค๋œ ์˜ต์…˜์„ ๊ฐ€๋ฆฌํ‚ด
  • ์ ‘๊ทผ์„ฑ ๊ด€๋ จ ๊ณ ๋ ค์‚ฌํ•ญ: ํ‚ค๋ณด๋“œ ๋‚ด๋น„๊ฒŒ์ด์…˜ ๋ฐ ์Šคํฌ๋ฆฐ ๋ฆฌ๋” ์‚ฌ์šฉ์„ฑ ๊ณ ๋ ค. ์˜ต์…˜ ์„ ํƒ ์‹œ live region์„ ํ†ตํ•ด ๋ณ€๊ฒฝ์‚ฌํ•ญ์„ ์•ˆ๋‚ดํ•˜๊ณ , ์„ ํƒ๋œ ์˜ต์…˜์— aria-selected="true" ์ ์šฉ
  • ์ ‘๊ทผ์„ฑ ๊ธฐ๋Šฅ ์š”์•ฝ: ์ฝค๋ณด๋ฐ•์Šค์™€ ํŒ์—…์˜ ๊ด€๊ณ„๋ฅผ ARIA ์†์„ฑ์œผ๋กœ ๋ช…ํ™•ํžˆ ํ•˜๊ณ , ํ‚ค๋ณด๋“œ๋กœ ์ ‘๊ทผ ๊ฐ€๋Šฅํ•˜๋„๋ก ๊ตฌํ˜„. ์ƒํƒœ ๋ณ€ํ™”๋ฅผ ์Šคํฌ๋ฆฐ ๋ฆฌ๋”์— ์•Œ๋ฆฌ๊ธฐ ์œ„ํ•ด live region ํ™œ์šฉ

๐Ÿ—๏ธ ๋งˆํฌ์—… ๊ตฌ์กฐ ๋ถ„์„

  • HTML ์‹œ๋งจํ‹ฑ ํƒœ๊ทธ ์‚ฌ์šฉ: <select> ์š”์†Œ ์‚ฌ์šฉ. ๋„ค์ดํ‹ฐ๋ธŒ HTML ์š”์†Œ๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์ฝค๋ณด๋ฐ•์Šค ์—ญํ• ์„ ๋‚˜ํƒ€๋ƒ„
  • ๋งˆํฌ์—… ๊ตฌ์กฐ ๋น„๊ต:
// WAI-ARIA ์˜ˆ์‹œ
<div class="select">
  <div id="exp_elem" role="combobox" aria-expanded="false">
    <div role="textbox" aria-readonly="true"></div>
  </div>
  <ul role="listbox" aria-labelledby="exp_elem">
    <li role="option"></li>
  </ul>
</div>

// MUI
<div class="MuiSelect-root">
  <div class="MuiSelect-select" aria-expanded="false">
    <span>Selected option</span>
    <input value="" />
  </div>
  <svg class="MuiSvgIcon-root"></svg>
</div>

// Ant Design 
<div class="ant-select">
  <div class="ant-select-selector">
    <span class="ant-select-selection-item"></span>
    <span class="ant-select-arrow">
      <span class="ant-select-arrow-icon"></span>
    </span>
  </div>
</div>
  • ๊ตฌ์กฐ์  ์„ ํƒ ์ด์œ :
    • WAI-ARIA: ARIA ์—ญํ• ๊ณผ ์ƒํƒœ๋ฅผ ๋ช…์‹œ์ ์œผ๋กœ ์ œ๊ณตํ•˜์—ฌ ์ ‘๊ทผ์„ฑ์„ ํ™•๋ณดํ•จ. role="combobox"๋กœ ์š”์†Œ์˜ ์—ญํ• ์„ ๋ช…ํ™•ํžˆ ํ•˜๊ณ , aria-expanded๋กœ ํŒ์—… ์ƒํƒœ ์ „๋‹ฌ. ์˜ต์…˜์€ role="listbox" ๋‚ด๋ถ€์— role="option"์œผ๋กœ ์ œ๊ณต
    • MUI, Ant Design: <div> ์š”์†Œ๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์ฝค๋ณด๋ฐ•์Šค ๊ตฌ์กฐ๋ฅผ ๊ตฌ์„ฑํ•จ. WAI-ARIA์— ๋น„ํ•ด ๋ช…์‹œ์ ์ธ ์—ญํ•  ํ‘œ์‹œ๋Š” ์ ์ง€๋งŒ, ํด๋ž˜์Šค๋ช…์„ ํ†ตํ•ด ์˜๋ฏธ๋ฅผ ํŒŒ์•…ํ•  ์ˆ˜ ์žˆ์Œ. ๊ธฐ๋ณธ์ ์ธ ์ ‘๊ทผ์„ฑ์€ ๊ณ ๋ คํ•˜๊ณ  ์žˆ์œผ๋‚˜, ์ถ”๊ฐ€์ ์ธ ARIA ์†์„ฑ ์ ์šฉ์ด ํ•„์š”ํ•ด ๋ณด์ž„

๐Ÿ’ก UI ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ๋น„๊ต ๋ถ„์„

  • MUI: <div> ๊ธฐ๋ฐ˜ ๊ตฌ์กฐ. ๊ธฐ๋ณธ ์ ‘๊ทผ์„ฑ์„ ๊ณ ๋ คํ•˜๊ณ  ์žˆ์œผ๋‚˜ ARIA ์†์„ฑ ์ ์šฉ์ด ๋ถ€์กฑํ•ด ๋ณด์ž„. ํด๋ž˜์Šค๋ช…์„ ํ†ตํ•ด ์ฝค๋ณด๋ฐ•์Šค์˜ ๊ฐ ๋ถ€๋ถ„์„ ์‹๋ณ„ํ•  ์ˆ˜ ์žˆ์Œ
  • Ant Design: <div> ๊ธฐ๋ฐ˜ ๊ตฌ์กฐ. ํด๋ž˜์Šค๋ช…์œผ๋กœ ์ฝค๋ณด๋ฐ•์Šค์˜ ๊ฐ ๋ถ€๋ถ„์„ ํ‘œ์‹œ. ARIA ์†์„ฑ ์ ์šฉ์ด ๊ฑฐ์˜ ์—†์–ด ์ ‘๊ทผ์„ฑ ๊ฐœ์„ ์ด ํ•„์š”ํ•ด ๋ณด์ž„

โš™๏ธ ๊ตฌํ˜„ ๋ฐ ์„ค๊ณ„ ๊ณ ๋ ค์‚ฌํ•ญ

  • ์ฃผ์š” ์„ค๊ณ„ ๊ณ ๋ ค์‚ฌํ•ญ: ๋„ค์ดํ‹ฐ๋ธŒ <select> ๋˜๋Š” <div> ๊ธฐ๋ฐ˜ ์ปค์Šคํ…€ ์ฝค๋ณด๋ฐ•์Šค ๊ตฌํ˜„ ์‹œ, WAI-ARIA ํŒจํ„ด์— ๋”ฐ๋ฅธ ๋งˆํฌ์—…๊ณผ ํ‚ค๋ณด๋“œ ์ธํ„ฐ๋ž™์…˜ ๊ตฌํ˜„์ด ์ค‘์š”ํ•จ. ์ƒํƒœ ๋ณ€ํ™”๋ฅผ live region์œผ๋กœ ์•Œ๋ฆฌ๋Š” ๊ฒƒ๋„ ์ ‘๊ทผ์„ฑ ํ–ฅ์ƒ์— ๋„์›€์ด ๋จ
  • ์ฝ”๋“œ ์˜ˆ์‹œ:
<div class="select">
  <div tabindex="0" 
       id="exp_elem" 
       role="combobox" 
       aria-labelledby="exp_label"
       aria-expanded="false" 
       aria-controls="exp_elem_list"
       aria-activedescendant=""
       aria-required="true">
    <span id="exp_label">Select an option</span>
    <div role="textbox" aria-readonly="true"></div>
  </div>
  <ul id="exp_elem_list" role="listbox">
    <li id="exp_elem_list_1" role="option">Option 1</li>
    <li id="exp_elem_list_2" role="option">Option 2</li>
    <li id="exp_elem_list_3" role="option">Option 3</li>
  </ul>
  <div aria-live="polite" aria-atomic="true" class="visually-hidden">
    Option 1 selected
  </div>
</div>
// ์ฝค๋ณด๋ฐ•์Šค ํ† ๊ธ€ ํ•จ์ˆ˜
function toggleSelectExpanded(select) {
  const expanded = select.getAttribute('aria-expanded') === 'true';
  select.setAttribute('aria-expanded', !expanded);
}

// ์˜ต์…˜ ์„ ํƒ ํ•จ์ˆ˜
function selectOption(select, option) {
  const selected = select.querySelector('[aria-selected="true"]');
  if (selected) {
    selected.setAttribute('aria-selected', 'false');
  }
  option.setAttribute('aria-selected', 'true');
  select.querySelector('div[role="textbox"]').textContent = option.textContent;
  select.setAttribute('aria-activedescendant', option.id);
  
  // Live region์œผ๋กœ ์„ ํƒ ์‚ฌํ•ญ ์•Œ๋ฆผ 
  const liveRegion = select.nextElementSibling;
  liveRegion.textContent = option.textContent + ' selected';
}

// ํ‚ค๋ณด๋“œ ์ด๋ฒคํŠธ ํ•ธ๋“ค๋ง
function handleKeyboard(event) {
  const { key } = event;
  const select = event.currentTarget;
  const options = select.nextElementSibling.querySelectorAll('[role="option"]');
  let activeIndex = Array.from(options).indexOf(document.getElementById(select.getAttribute('aria-activedescendant')));

  if (key === 'ArrowDown') {
    event.preventDefault();
    activeIndex = Math.min(activeIndex + 1, options.length - 1);
    selectOption(select, options[activeIndex]);
  } 
  else if (key === 'ArrowUp') {
    event.preventDefault();
    activeIndex = Math.max(activeIndex - 1, 0);
    selectOption(select, options[activeIndex]);
  }
  else if (key === 'Escape') {
    select.querySelector('div[role="textbox"]').textContent = '';
    select.setAttribute('aria-activedescendant', '');
    toggleSelectExpanded(select);
  }
}

๐Ÿ” ํ…Œ์ŠคํŠธ ๋ฐ ๊ฒ€ํ† 

  • ์ ‘๊ทผ์„ฑ ํ…Œ์ŠคํŠธ: ํ‚ค๋ณด๋“œ ๋‚ด๋น„๊ฒŒ์ด์…˜ ๋ฐ ์Šคํฌ๋ฆฐ ๋ฆฌ๋” ํ˜ธํ™˜์„ฑ ํ…Œ์ŠคํŠธ. ์˜ต์…˜ ์„ ํƒ ์‹œ live region์„ ํ†ตํ•ด ์ ์ ˆํ•œ ์•ˆ๋‚ด๊ฐ€ ์ œ๊ณต๋˜๋Š”์ง€ ํ™•์ธ
  • ๋ธŒ๋ผ์šฐ์ € ํ˜ธํ™˜์„ฑ ํ…Œ์ŠคํŠธ: ์ฃผ์š” ๋ธŒ๋ผ์šฐ์ €์—์„œ์˜ ๋ Œ๋”๋ง ๋ฐ ๊ธฐ๋Šฅ ๋™์ž‘ ํ™•์ธ
  • ์‚ฌ์šฉ์ž ํ”ผ๋“œ๋ฐฑ: ๋‹ค์–‘ํ•œ ์‚ฌ์šฉ์ž๋ฅผ ๋Œ€์ƒ์œผ๋กœ ์‚ฌ์šฉ์„ฑ ํ…Œ์ŠคํŠธ๋ฅผ ์ง„ํ–‰ํ•˜๊ณ , ํ”ผ๋“œ๋ฐฑ์„ ์ˆ˜๋ ดํ•˜์—ฌ ๊ฐœ์„ 

๐Ÿ“Œ ์ถ”๊ฐ€ ์ฐธ๊ณ  ์‚ฌํ•ญ

  • ๊ด€๋ จ ๋ฌธ์„œ: W3C WAI-ARIA Combobox Pattern, MDN - <select> element
  • Web.dev Multi-Select Article
  • ์˜๊ฒฌ ๋ฐ ์ œ์•ˆ: Select ์ปดํฌ๋„ŒํŠธ ๊ตฌํ˜„ ์‹œ ์ ‘๊ทผ์„ฑ์„ ๊ณ ๋ คํ•œ ์„ค๊ณ„ ๋ฐ WAI-ARIA ํŒจํ„ด ์ ์šฉ์— ๋Œ€ํ•œ ์˜๊ฒฌ ๋ฐ ์ œ์•ˆ

๐ŸŽฏ ์—ญํ• , ์†์„ฑ, ์ƒํƒœ ๋ฐ ํƒœ๊ทธ ์†์„ฑ

  • Role, Attribute, State
    • Role: role="combobox", role="textbox", role="listbox", role="option" ์‚ฌ์šฉํ•˜์—ฌ ์ปดํฌ๋„ŒํŠธ์˜ ์—ญํ•  ๋ช…์‹œ
    • Attribute & State: aria-expanded, aria-controls, aria-activedescendant, aria-selected ๋“ฑ์„ ์‚ฌ์šฉํ•˜์—ฌ ์ฝค๋ณด๋ฐ•์Šค์˜ ์ƒํƒœ์™€ ๊ด€๊ณ„ ํ‘œํ˜„
      • aria-expanded: ์ฝค๋ณด๋ฐ•์Šค ํŒ์—… ์ƒํƒœ(์—ด๋ฆผ/๋‹ซํž˜) ํ‘œ์‹œ
      • aria-controls: ์ฝค๋ณด๋ฐ•์Šค๊ฐ€ ์ œ์–ดํ•˜๋Š” ํŒ์—…(listbox) ์š”์†Œ ๋ช…์‹œ
      • aria-activedescendant: ํ˜„์žฌ ํฌ์ปค์Šค๋œ ์˜ต์…˜ ์š”์†Œ์˜ ID ์ฐธ์กฐ
      • aria-selected: ์„ ํƒ๋œ ์˜ต์…˜์ž„์„ ๋‚˜ํƒ€๋ƒ„
  • JavaScript ๋ฐ CSS ์‚ฌ์šฉ ์—ฌ๋ถ€
    • JavaScript ์‚ฌ์šฉ: ํ‚ค๋ณด๋“œ ์ด๋ฒคํŠธ ํ•ธ๋“ค๋ง, ํŒ์—… ํ† ๊ธ€, ์˜ต์…˜ ์„ ํƒ ๋“ฑ์˜ ๊ธฐ๋Šฅ ๊ตฌํ˜„์— ์‚ฌ์šฉ
    • CSS ์ ์šฉ: ์ฝค๋ณด๋ฐ•์Šค์˜ ๋ ˆ์ด์•„์›ƒ ๋ฐ ๋””์ž์ธ์„ ์œ„ํ•œ CSS ์Šคํƒ€์ผ ์ ์šฉ. ๋‹จ, ์‹œ๊ฐ์  ํšจ๊ณผ๊ฐ€ ์ ‘๊ทผ์„ฑ์„ ์ €ํ•ดํ•˜์ง€ ์•Š๋„๋ก ์ฃผ์˜

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions