PhoneInput | CoUI
본문으로 건너뛰기
LogoCoUI

PhoneInput

국가 코드와 함께 전화번호를 입력하는 컴포넌트

목차

국가 선택과 함께 전화번호를 입력하는 컴포넌트입니다. Flutter/Web 양쪽에서 동일한 API(PhoneInput)를 제공합니다.

기본 사용법 (Basic Usage)#

Flutter와 Web 모두 PhoneInput 클래스와 CoreCountry 타입을 동일하게 사용합니다. 국가는 CoreCountry.byCode('KR') 룩업, const CoreCountry(...) 리터럴, 또는 전체 목록 CoreCountry.all(247개) 중에서 지정합니다.

// 기본 전화번호 입력 (CoreCountry 사용)
PhoneInput(
  initialCountry: CoreCountry.byCode('KR'),
  onChanged: (phoneNumber) {
    handlePhoneChanged(phoneNumber);
  },
)

// 초기값 설정 (초기 번호 포함)
PhoneInput(
  initialCountry: CoreCountry.unitedStates,
  initialNumber: '5551234567',
  onChanged: handlePhoneChanged,
)

// 특정 국가만 허용
PhoneInput(
  initialCountry: CoreCountry.byCode('KR'),
  countries: [
    CoreCountry.byCode('KR')!,
    CoreCountry.byCode('US')!,
    CoreCountry.byCode('JP')!,
  ],
  onChanged: handlePhoneChanged,
)
// 기본 전화번호 입력 (CoreCountry 사용)
PhoneInput(
  initialCountry: CoreCountry.byCode('KR'),
  onChanged: (value) {
    handlePhoneChanged(value);
  },
)

// 초기값 설정 (초기 번호 포함)
PhoneInput(
  initialCountry: CoreCountry.unitedStates,
  initialNumber: '5551234567',
  placeholder: '전화번호 입력',
  onChanged: handlePhoneChanged,
)

// 특정 국가만 허용
PhoneInput(
  initialCountry: CoreCountry.byCode('KR'),
  countries: [
    CoreCountry.byCode('KR')!,
    CoreCountry.byCode('US')!,
    CoreCountry.byCode('JP')!,
  ],
  onChanged: handlePhoneChanged,
)

전화번호 값의 글자 스타일#

전화번호 입력값은 numberFieldStyle.valueStyle로 지정합니다. 이 슬롯은 Flutter와 Web 모두 실제 입력 필드에 전달됩니다. bodyTextStyle은 국가 선택 목록의 국가명에만 적용되며, 검색 필드 스타일은 searchFieldStyle로 독립적으로 지정합니다.

PhoneInput(
  phoneInputStyle: const CorePhoneInputStyle(
    numberFieldStyle: CoreTextFieldStyle(
      valueStyle: CoreTextStyle(fontSize: 17, fontWeight: 600, lineHeight: 1.4),
    ),
  ),
)

슬롯을 생략하면 기존 입력값 스타일과 테두리 없는 내부 필드를 유지합니다. 테마와 인스턴스의 중첩 스타일은 필드별로 합쳐지며 인스턴스 값이 우선합니다. 각 필드의 placeholderStyle은 공통 placeholderColor보다 우선합니다. 글자 크기를 바꾸어도 입력·국가 선택·비활성 상태의 동작은 바뀌지 않습니다.

빠른 오버라이드 (Chain)#

이미 만든 PhoneInput 인스턴스에 스타일을 빠르게 덧붙이고 싶다면 withStyle 체인을 쓸 수 있습니다. 생성자의 style 슬롯 인자와 동일하게 동작하지만, 이미 구성된 위젯 위에서 바로 이어 쓸 수 있습니다. .radius16처럼 Core 토큰 상수 이름과 똑같은 이름의 getter도 있습니다 — withStyle을 한 번 더 줄인 sugar로, 이름이 곧 값이라(radius16 == CoreRadius.radius16) 어느 컴포넌트에서 써도 뜻이 갈리지 않습니다.

class PhoneInputChainExample extends StatelessWidget {
  const PhoneInputChainExample({super.key});

  @override
  Widget build(BuildContext context) {
    return Column(
      mainAxisSize: MainAxisSize.min,
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        // 토큰-정확 getter combo — 이름이 곧 값 (getter 이름 = Core 토큰 상수 1:1).
        const PhoneInput(placeholder: 'Phone number').radius16.primary,
        const Gap.space12(),
        // withStyle full-control — getter 가 없는 필드(borderColor·borderWidth)까지 한 번에.
        const PhoneInput(placeholder: 'Full control').withStyle(
          const CorePhoneInputStyle(
            backgroundColor: CoreColor.token(CoreColors.surfaceContainer),
            borderColor: CoreColor.token(CoreColors.tertiary),
            borderRadius: CoreBorderRadius.all(CoreRadius.radius24),
            borderWidth: CoreStrokeWidth.stroke2,
          ),
        ),
      ],
    );
  }
}
class PhoneInputChainExample extends StatelessComponent {
  const PhoneInputChainExample({super.key});

  @override
  Component build(BuildContext context) {
    return div(
      [
        // 토큰-정확 getter combo — 이름이 곧 값 (getter 이름 = Core 토큰 상수 1:1).
        const PhoneInput(placeholder: 'Phone number').radius16.primary,
        const Gap.space12(),
        // withStyle full-control — getter 가 없는 필드(borderColor·borderWidth)까지 한 번에.
        const PhoneInput(placeholder: 'Full control').withStyle(
          const CorePhoneInputStyle(
            backgroundColor: CoreColor.token(CoreColors.surfaceContainer),
            borderColor: CoreColor.token(CoreColors.tertiary),
            borderRadius: CoreBorderRadius.all(CoreRadius.radius24),
            borderWidth: CoreStrokeWidth.stroke2,
          ),
        ),
      ],
      classes: 'flex flex-col items-start',
    );
  }
}

Live Preview#

사용 시기 (When to Use)#

이 컴포넌트를 사용하세요:

  • 글로벌 서비스에서 여러 국가의 전화번호를 입력받아야 할 때
  • 국가 코드(다이얼 코드)와 함께 전화번호를 저장해야 할 때

대신 다른 컴포넌트를 사용하세요:

  • TextField: 단일 국가 내수 서비스에서 단순 숫자 입력으로 충분할 때
  • Select + TextField 조합: 국가 선택과 번호 입력을 완전히 독립 제어해야 할 때

Props / Parameters#

PhoneNumber와 PhoneInputValue는 같은 CorePhoneNumber의 플랫폼별 별칭입니다. 합성 자식 슬롯이 없는 입력이므로 W 타입 구분은 필요하지 않습니다.

속성타입기본값설명
initialCountry CoreCountry? null 초기 선택 국가
initialNumber String? null 초기 번호
initialValue CorePhoneNumber? null 국가와 번호 동시 지정; 개별 초기값보다 우선
countries List<CoreCountry>? null 선택 가능한 국가 목록
onChanged CoreValueChanged<CorePhoneNumber>? null 국가·번호 값 변경 콜백
filterPlusCode bool true 선택된 국가의 +다이얼코드를 타이핑했을 때만 제거
placeholder String? null 플레이스홀더
enabledbooltrue상호작용 활성화
onlyNumber bool true 숫자와 맨 앞 + 하나만 입력 허용
phoneInputStyle CorePhoneInputStyle? null chrome 슬롯

PhoneNumber(Flutter)와 PhoneInputValue(Web)는 같은 CorePhoneNumber 의 이름 두 개입니다 — 한쪽에서 저장한 값을 다른 쪽에서 그대로 받습니다. country / number 를 들고, countryCode · dialCode · fullNumber · value 를 파생해 줍니다.

Flutter 전용 파라미터#

속성타입기본값설명
controller TextEditingController? null Flutter 런타임 인프라 — Web은 브라우저 native <input>이 같은 기능을 제공

CorePhoneInputStyle 필드#

필드타입설명
borderRadius CoreBorderRadius? Outer border radius override.
flagGapStyle CoreGapStyle? Nested gap style between flag and dial code inside the selector — forwarded to Gap(gapStyle: …) .
countryGapStyle CoreGapStyle? Nested gap style between country selector and input field — forwarded to Gap(gapStyle: …) .
maxWidth double? Maximum width of the input row (logical px).
padding CoreEdgeInsets? Inner padding override. No default* , deliberately: this is the OUTER row's padding, and a flush row is the design. The padding a phone input actually shows belongs to its parts, and each of those is stated — [defaultSelectorPadding], [defaultInputFieldPadding], [defaultDropdownItemPadding]. This field only exists so a caller can inset the assembled row, and Web emits it under if (padding != null) , so a default would add standing padding outside every phone input and shift the whole control against the three inner paddings that were measured for a flush row.
height double? Outer container height override (logical px). A stated height stays as stated under every density — only [defaultHeight], the control row, moves.
borderWidth double? Outer border stroke width override (logical px).
inputFieldPadding CoreEdgeInsets? Inner padding of the phone-number input field.
selectorPadding CoreEdgeInsets? Country selector trigger padding override.
dropdownItemPadding CoreEdgeInsets? Dropdown / search item padding override.
dropdownItemGapStyle CoreGapStyle? Nested gap style between dropdown-item flag and country name — forwarded to Gap(gapStyle: …) .
popupMaxWidth double? Maximum width of the country picker popup (logical px).
popupMaxHeight double? Maximum height of the country picker popup (logical px).
popoverTopOffset double? Vertical offset between the trigger and the popup (logical px).
chevronIconStyle CoreIconStyle? Chevron icon style override — size defers to [defaultChevronIconStyle].
flagSize double? Flag emoji font size in the selector (logical px).
dropdownFlagSize double? Flag emoji font size inside dropdown items (logical px).
disabledOpacity double? Opacity applied to the whole component when disabled.
popupBorderRadius CoreBorderRadius? Popup border radius override.
dropdownDialCodeTextStyle CoreTextStyle? Muted dropdown text style override — drives both the dropdown-item dial-code span and the "No results" empty hint (Flutter), and the dropdown-item dial-code span (Web). Both the typography role token and the text colour are carried inside this slot, overlaying [defaultDropdownDialCodeTextStyle].
backgroundColor CoreColor? Outer container background fill override — defers to [defaultBackgroundColor].
borderColor CoreColor? Outer container border colour override — defers to [defaultBorderColor]. The popup panel border rides [popoverStyle] and the selector↔input divider rides [separatorColor].
focusBorderColor CoreColor? Outer container focused border colour override — defers to [defaultFocusBorderColor].
hoverBorderColor CoreColor? Outer container border colour override while the pointer is over the field — defers to [defaultHoverBorderColor]. Outranked by the focused border ([focusBorderColor]) and absent while the field is disabled. Not the selector / row hover background — that is [hoverColor].
hoverColor CoreColor? Hover background override (dropdown rows) — defers to [defaultHoverColor].
separatorColor CoreColor? Selector↔input / picker divider colour override — defers to [defaultSeparatorColor].
placeholderColor CoreColor? Placeholder text colour override (phone field + search input) — defers to [defaultPlaceholderColor].
chevronColor CoreColor? Selector chevron icon colour override — enabled state, defers to [defaultChevronColor].
disabledChevronColor CoreColor? Selector chevron icon colour override — disabled state, defers to [defaultDisabledChevronColor].
dialCodeTextStyle CoreTextStyle? Selector dial-code label text style override — enabled state. Both the labelLarge role token and the onSurface colour live inside the slot, overlaying [defaultDialCodeTextStyle].
disabledDialCodeColor CoreColor? Selector dial-code label colour override — disabled state, defers to [defaultDisabledDialCodeColor].
bodyTextStyle CoreTextStyle? Country-name label text style override — defers to [defaultBodyTextStyle].
numberFieldStyle CoreTextFieldStyle? Style override for the phone-number TextField . The default remains chromeless; use this slot for input typography without changing the country selector or search field.
searchFieldStyle CoreTextFieldStyle? Style override for the country-search TextField inside the dropdown. Defers to [defaultSearchFieldStyle], independently of [numberFieldStyle].
popoverStyle CorePopoverStyle? Style override for the country-dropdown Popover panel — border / surface / radius / shadow flow through its nested panelStyle ; defers to [defaultPopoverStyle].
flagBorderRadius CoreBorderRadius? Flag corner radius override — the flag glyph in the selector and in the list rows. null → [defaultFlagBorderRadius].
separatorHeight double? Height override of the hairline between the selector and the number (logical px). null → [defaultSeparatorHeight].
disabledBackgroundColor CoreColor? Outer container fill override while disabled. null → [defaultDisabledBackgroundColor].
disabledBorderColor CoreColor? Outer container border colour override while disabled. null → [defaultDisabledBorderColor].
disabledPlaceholderColor CoreColor? Number-field placeholder colour override while disabled. null → [defaultDisabledPlaceholderColor].
dropdownListPadding CoreEdgeInsets? Padding of the country list inside the panel override. null → [defaultDropdownListPadding].
dropdownItemBorderRadius CoreBorderRadius? Country row corner radius override. null → [defaultDropdownItemBorderRadius].
dropdownCheckIconStyle CoreIconStyle? Selected-country check glyph chrome override, merged onto [defaultDropdownCheckIconStyle].
searchIconStyle CoreIconStyle? Search glyph chrome override in front of the country search field, merged onto [defaultSearchIconStyle].

스타일 시스템 (Style System)#

테마 커스터마이징 (Theme)#

CorePhoneInputTheme으로 프로젝트 기본 스타일을 오버라이드:

CoreComponentTheme(
  phoneInput: CorePhoneInputTheme(
    style: CorePhoneInputStyle(
      borderRadius: CoreBorderRadius.all(CoreRadius.radius4),
      flagGapStyle: CoreGapStyle(size: CoreSpace.space8),
      countryGapStyle: CoreGapStyle(size: CoreSpace.space16),
      maxWidth: 200.0,
    ),
  ),
)

Resolve 우선순위: 위젯 파라미터 > CorePhoneInputTheme > 디자인 시스템 기본값.

동작 스펙 (Behavior)#

인터랙션#

  • 국가 선택: 다이얼 코드 버튼 클릭 시 국가 목록 팝업
  • 번호 입력: onlyNumber: true 시 숫자 + 맨 앞 + 하나만 허용
  • 키보드: TextInputType.phone / inputmode="tel" 로 전화번호 키패드 활성화
  • 자동완성: autocomplete="tel" / AutofillHints.telephoneNumber
  • 포인터 올리기: 바깥 테두리가 outlineVariant 에서 outline 으로 한 단계 진해집니다(hoverBorderColor). 국가 행의 hover 배경(hoverColor)과는 별개의 값이며, 선택기에는 hover 배경이 없습니다

읽기 규칙 (양쪽 공통)#

타이핑된 것만 제거하고, 나머지는 추측하지 않습니다. filterPlusCode: true(기본)는 선택된 국가의 +다이얼코드로 시작할 때 그것만 벗깁니다. 그 외에는 입력한 그대로 전달됩니다.

입력 칸을 고쳐 쓰는 게 아니라 읽을 때 파생하므로, 같은 텍스트를 두고 국가만 바꾸면 새 다이얼 코드로 다시 해석됩니다. 양 플랫폼이 CorePhoneNumber.fromInput 하나를 호출합니다.

왜 선행 0을 떼지 않나 (0.129 변경)

이전에는 선행 0과 + 없이 적힌 다이얼 코드도 제거했습니다. 둘 다 남은 숫자가 무슨 뜻인지에 대한 추측이고, 어떤 추측도 모든 나라에서 맞지 않습니다:

국가입력올바른 E.164
영국 07911123456 +447911123456 (트렁크 0 제거)
이탈리아 0612345678 +390612345678 (트렁크 0 유지)

두 입력은 모양이 같고 다이얼 코드에는 이를 구분할 정보가 없습니다. 그래서 0을 떼면 이탈리아가 깨지고, 안 떼면 영국이 깨집니다 — 손상이 사라지는 게 아니라 옮겨갑니다. 구분하려면 국가별 national prefix 표가 필요한데 CoreCountry(code/dialCode/name)에도 그 원본 데이터셋에도 없습니다.

그래서 지금은 입력한 값을 그대로 읽습니다. 다이얼 코드를 떼고 싶으면 타이핑하면 됩니다 — 그래서 onlyNumber가 켜져 있어도 맨 앞 + 는 입력할 수 있습니다.

타이핑으로 국가 감지 (양쪽 공통)#

입력이 + 로 시작할 때만 다이얼 코드를 매칭해 국가 선택을 따라 바꿉니다. + 가 없으면 그 숫자는 국가 번호가 아니라 국내 번호이므로, 자기 번호를 그냥 적는 사용자의 국가를 뺏지 않습니다.

매칭은 가장 긴 접두 일치입니다 — +1 은 그 자체로 다이얼 코드지만 +1242(바하마) 같은 더 긴 코드의 접두이기도 해서, 목록 순서대로 첫 일치를 고르면 카리브 국가들이 영영 감지되지 않습니다.

답이 하나가 아니면 현재 선택을 그대로 둡니다:

입력결과
+82…대한민국으로 전환 (후보 1개)
+1…유지 — 미국·캐나다가 같은 코드라 모호
+8유지 — 그 길이에 일치하는 코드 없음
821012345678유지 — + 로 시작하지 않음

모호한 경우에 어느 나라를 고를지 정하는 표는 만들지 않습니다. 그 표의 근거(인구인지 시장 규모인지)가 어디에도 없고, 어떻게 정하든 반대편 나라 사용자에게는 매번 틀린 값이 선택되기 때문입니다 — "후보 없음이면 유지" 와 같은 답을 주는 것이 일관됩니다.

탐색 범위는 countries 로 넘긴 목록입니다. 목록을 좁혔다면 감지도 그 바깥으로 나가지 않습니다. 드롭다운에서 직접 고른 국가는 덮어쓰지 않습니다 — 감지는 번호 칸을 고칠 때만 동작합니다.

판정은 CorePhoneCountryDetector.detect 한 곳에 있고 양 플랫폼이 그것을 호출합니다.

모양 (양쪽 공통)#

  • 상자는 높이 48(lg 행) · 모서리 12 입니다. 선택기는 앞 14 · 뒤 10, 번호 칸은 앞뒤 12 로 들여 놓습니다. 안쪽 번호 칸은 테두리도 면도 없이 합성되어 바깥 상자 하나만 경계를 그립니다. 선택기와 번호 사이는 높이 20 의 얇은 Divider 입니다
  • 국기는 높이 14 에 모서리 4(part) 이고, 국가 코드는 bodyLarge, 펼침 아이콘은 onSurfaceSubtle 입니다
  • 국가 목록 패널은 ItemPicker 의 패널과 같습니다 — surfaceBright 채움 · outlineVariant 테두리 · 모서리 12 · 그림자 lg, 테두리 없는 검색 + 검색 아이콘 + Divider, 목록 안쪽 여백 6, 행 여백 8 · 모서리 8, 선택된 국가는 primary 체크만 표시합니다

검색 (양쪽 공통)#

  • 국가 드롭다운 내 검색 입력: name/dialCode/code 기준 필터링
  • 양쪽 모두 드롭다운 패널 최상단에 검색 입력창 (autofocus)

상태#

  • enabled: false: 투명도를 낮추지 않고 disabledContainer 채움 · disabledOutline 테두리 · disabledContent 글자(국가 코드 · 펼침 아이콘 · placeholder)로 그립니다 + 인터랙션 차단
  • 바깥 테두리: 기본은 outlineVariant 이고, 우선순위는 disabled > focus > hover > rest 입니다. 번호 칸에 포커스가 있으면 포인터가 올라와 있어도 focusBorderColor 가 유지되고, 비활성 상태는 hover 단계를 받지 않습니다. 선택기와 입력 사이의 구분선(separatorColor)은 outlineVariant 로 그대로입니다

포커스가 hover 를 이기는 방식 (Web)

Tailwind v3 는 같은 specificity 에서 hover: 를 focus-within: 뒤에 emit 하므로, 평범한 hover:border-* 클래스는 포커스된 칸 위에 포인터가 있는 동안 focus-within:border-* 를 이깁니다. 그래서 Web 은 hover 규칙의 셀렉터 자체에서 포커스를 제외합니다(:hover:not(:focus-within)) — 두 규칙의 emit 순서와 무관하게 포커스 테두리가 이깁니다. 토큰 색은 arbitrary-variant 클래스([&:hover:not(:focus-within)]:border-outline)로, 토큰이 아닌(raw) 색은 인스턴스 범위 <style> 규칙으로 나갑니다.

사용 가이드라인 (Usage Guidelines)#

국가 선택과 국내 번호를 함께 받으려면 PhoneInput을 사용합니다. 국가 선택이 필요 없는 문자열 입력은 TextField의 tel 타입, 고정 구간 입력은 FormattedInput을 선택합니다. PhoneInput은 번호를 구조화하지만 국가별 유효성이나 E.164 정규화를 추측하지 않습니다.

✅ Do#

국가 + 번호를 함께 다룰 때는 initialValue 하나로 넘기기

PhoneInput(
  initialValue: PhoneNumber(
    country: CoreCountry.byCode('KR')!,
    number: '1012345678',
  ),
  onChanged: handlePhoneChanged,
)

initialValue를 주면 그 안의 country/number가 initialCountry/initialNumber보다 우선해 완전히 대체합니다. 국가와 번호를 함께 다루는 값이 있으면 세 파라미터를 따로 채우지 말고 initialValue 하나로 넘기세요.


❌ Don't#

앞자리 0이나 플러스 없는 다이얼 코드가 알아서 떨어지리라 기대하지 않기

// ❌ 떨어지지 않는다 — 값은 '0821012345678' 그대로 온다
PhoneInput(initialNumber: '0821012345678')

// ✅ 다이얼 코드를 떼려면 타이핑된 형태로 받는다
PhoneInput(initialNumber: '+821012345678')  // → number: '1012345678'

이걸 대신해 주던 filterZeroCode / filterCountryCode 플래그는 0.129부터 읽히지 않았고 지금은 없습니다. 국가별로 정답이 갈리는 추측이었고, 어느 기본값도 영국과 이탈리아를 동시에 만족시키지 못합니다(위 "읽기 규칙" 참조).

접근성 (Accessibility)#

역할#

요소FlutterWeb
루트 Semantics(container: true, label: "국가 선택") — 국가 버튼과 전화번호 입력을 함께 감쌉니다. 펼침 상태는 Popover 가 소유합니다 역할 없는 <div>
국가 선택기 역할 없음 — MouseRegion + GestureDetector <button type="button"> + aria-label="국가 선택" + aria-haspopup="listbox" + aria-expanded
국가 목록역할 없음 (ListView)role="listbox"
국가 항목 역할 없음 <button type="button" role="option"> + aria-selected (하이라이트), tabindex="-1"
전화번호 입력 TextField — autofillHints: [telephoneNumber] , onlyNumber 일 때만 keyboardType: .phone TextField(type: .tel) 이 전화번호 입력을 렌더합니다
국가 검색 입력 TextField role="combobox" + aria-controls (목록) + aria-activedescendant (하이라이트된 국가)
팝오버 패널 역할 없음. 감싸는 Popover 는 트리거에 Semantics(container: true, expanded: …) 만 얹습니다 Popover 패널에 role="dialog" , 트리거 wrapper <div> 에 aria-haspopup="dialog" / aria-expanded

키보드#

키FlutterWeb
Tab 전화번호 입력만 정지점입니다 — 국가 선택기와 국가 항목은 포커스를 받지 않습니다 국가 버튼 → 전화번호 입력. 패널에서는 검색 입력에 포커스가 머물고 국가는 개별 정지점이 아님
Enter / Space 국가 선택기·국가 항목에서 동작하지 않습니다 국가 버튼과 국가 항목(<button>)을 활성화합니다
Escape 패널이 닫히지 않습니다 (비모달 Popover 는 Escape 를 배선하지 않습니다) 패널을 닫습니다 (document keydown 리스너)
↑ ↓ 목록 이동 없음 — 국가 항목이 포커스 대상이 아닙니다 이전/다음 국가 하이라이트, 끝에서 순환하고 화면 안으로 스크롤
Enter (검색 입력)국가 선택 경로 없음하이라이트된 국가 선택 후 닫기
문자 입력패널 검색 필드에 입력하면 국가 목록이 즉시 필터링됩니다동일

Web 패널은 현재 국가를 하이라이트한 채로 열립니다. 입력하면 첫 결과로 하이라이트가 옮겨지고, 포커스가 아니라 aria-activedescendant 가 목록을 따라갑니다.

브라우저 검증 — Web 키보드 프로브(tool/parity/keyboard_probe.dart)가 검색 입력의 aria-activedescendant 가 실제 기대 옵션을 가리키는지 확인합니다.

스크린 리더#

  • 접근 가능한 이름의 출처가 플랫폼마다 다릅니다. Web 은 phoneInputSelectCountry(기본값 Select country / 한국어 국가 선택)를 국가 <button> 하나에만 aria-label 로 붙입니다. Flutter 는 같은 문자열을 바깥 Semantics(container: true, label: …) 에 붙여 국가 버튼과 전화번호 입력이 함께 그 이름 아래에서 읽힙니다.
  • 전화번호 입력에는 전용 이름 파라미터가 없습니다. 표시 문구는 placeholder 로 전달됩니다. 필드의 의미가 필요하면 호출부의 폼 라벨과 설명을 함께 제공하세요.
  • 패널 열림 상태는 양쪽 다 통보됩니다. Flutter 는 Semantics(expanded: …), Web 은 국가 버튼의 aria-expanded 로 전달합니다.
  • 국가 항목 이름은 국기·국가명·국가번호가 순서대로 읽힙니다. 국기는 Web 에서 role="img" + aria-hidden="true" 로 숨겨진 장식이라 이름에 들어가지 않습니다.
  • Web 검색 입력에는 로케일 검색 문구의 이름이 있고, 하이라이트 행을 aria-activedescendant / aria-selected 로 전달합니다.
  • 라이브 리전은 없습니다. 국가를 바꿔도 선택 결과가 따로 통보되지 않습니다.

포커스 관리#

  • 진입 — Web 은 패널이 마운트된 다음 프레임에 검색 <input> 을 명시적으로 포커스합니다. Flutter 검색 필드도 autofocus: true 를 선언하지만, 비모달 오버레이의 포커스 스코프는 감싸는 스코프에 포커스된 자식이 없을 때만 포커스를 가져가므로 — 전화번호 입력에 포커스를 둔 채 국가 버튼을 누르면 발화하지 않고 포커스가 그대로 남습니다.
  • 이탈 — 양 플랫폼 모두 패널이 닫힐 때 포커스를 트리거로 되돌리지 않습니다. Web 은 국가 선택기가 mousedown 에서 preventDefault() 로 포커스 획득 자체를 막고 있어(루트의 focus-within 테두리를 번호 필드에만 묶기 위한 의도적 처리) 돌아갈 트리거 포커스가 없습니다.
  • 트랩 — 없습니다. 패널은 오버레이 레이어로 포탈되고 배경은 inert 가 되지 않으므로, Web 에서 Tab 은 패널을 벗어나 문서 순서를 계속 걸어갑니다.

알려진 제약#

  • Flutter 에서는 국가 선택기와 국가 목록 항목이 포인터 전용입니다. 둘 다 MouseRegion + GestureDetector 로만 그려져 포커스를 받지 못하므로, 키보드만으로는 국가를 바꿀 수 없습니다. Web 은 같은 자리를 <button type="button"> 으로 그려 Tab / Enter 로 조작됩니다. 대안은 호출부에서 initialCountry 또는 initialValue 로 국가를 지정하는 것뿐입니다.
  • Flutter 의 "국가 선택" 라벨이 국가 버튼이 아니라 PhoneInput 전체에 붙습니다. 바깥 Semantics(container: true, label: …) 하나가 국가 버튼과 전화번호 입력을 함께 감싸므로, 리더는 번호 입력까지 그 이름 아래에서 읽습니다. Web 은 같은 문자열을 국가 버튼 하나에만 aria-label 로 붙입니다.
  • 하이라이트와 포인터 hover 의 색은 같습니다. Web 의 aria-selected 는 현재 하이라이트를 뜻하고 입력에 포커스가 머뭅니다. 선택된 국가는 두 플랫폼 모두 체크 아이콘으로만 보이며, Flutter 는 국가 선택 상태를 별도 시맨틱으로 전달하지 않습니다.

크로스 플랫폼 차이점 (Platform Differences)#

항목FlutterWeb
값 타입 별칭PhoneNumber = CorePhoneNumberPhoneInputValue = CorePhoneNumber
번호 편집합성 TextField와 TextEditingController합성 TextField의 native input
숫자/+ 포매터TextInputFormatters.phoneTextCoreInputFormatters.phoneText
외부 컨트롤러TextEditingController 전달 가능DOM 편집 값; 해당 컨트롤러 생성자 없음
국가 선택 패널합성 Popover와 검색 TextField합성 Popover와 검색 TextField의 DOM 이벤트
hover 테두리 바깥 박스의 MouseRegion 이 hover 를 감지하고 resolver 의 hoverBorderColor 를 고릅니다 CSS :hover:not(:focus-within) — 토큰은 클래스, raw 색은 범위 <style> 규칙

두 플랫폼 모두 국가 감지는 CorePhoneCountryDetector, 입력 해석은 CorePhoneNumber.fromInput을 사용합니다. 폼에 보고하는 값은 내부 검색 문자열이 아니라 PhoneInput의 국가·번호 값입니다.

  • TextField: 단일 국가용 일반 전화번호 입력
  • Select: 국가 선택만 독립 제공
  • Form: 폼 내에서 PhoneInput 사용