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#
코드 보기
class PhoneInputDefaultExample extends StatelessComponent {
const PhoneInputDefaultExample({super.key});
@override
Component build(BuildContext context) {
return const PhoneInput(placeholder: 'Phone number');
}
}
class PhoneInputDefaultExample extends StatelessWidget {
const PhoneInputDefaultExample({super.key});
@override
Widget build(BuildContext context) {
return const PhoneInput(placeholder: 'Phone number');
}
}
사용 시기 (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 |
플레이스홀더 |
enabled | bool | true | 상호작용 활성화 |
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)#
역할#
| 요소 | Flutter | Web |
|---|---|---|
| 루트 |
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
|
키보드#
| 키 | Flutter | Web |
|---|---|---|
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)#
| 항목 | Flutter | Web |
|---|---|---|
| 값 타입 별칭 | PhoneNumber = CorePhoneNumber | PhoneInputValue = CorePhoneNumber |
| 번호 편집 | 합성 TextField와 TextEditingController | 합성 TextField의 native input |
| 숫자/+ 포매터 | TextInputFormatters.phoneText | CoreInputFormatters.phoneText |
| 외부 컨트롤러 | TextEditingController 전달 가능 | DOM 편집 값; 해당 컨트롤러 생성자 없음 |
| 국가 선택 패널 | 합성 Popover와 검색 TextField | 합성 Popover와 검색 TextField의 DOM 이벤트 |
| hover 테두리 | 바깥 박스의 MouseRegion 이 hover 를 감지하고 resolver 의 hoverBorderColor 를 고릅니다 |
CSS :hover:not(:focus-within) — 토큰은 클래스, raw 색은 범위 <style> 규칙 |
두 플랫폼 모두 국가 감지는 CorePhoneCountryDetector, 입력 해석은 CorePhoneNumber.fromInput을 사용합니다. 폼에 보고하는 값은 내부 검색 문자열이 아니라 PhoneInput의 국가·번호 값입니다.