Files
superport/lib/screens/equipment/widgets/autocomplete_text_field.dart
JiWoong Sul 49b203d366
Some checks failed
Flutter Test & Quality Check / Test on macos-latest (push) Has been cancelled
Flutter Test & Quality Check / Test on ubuntu-latest (push) Has been cancelled
Flutter Test & Quality Check / Build APK (push) Has been cancelled
feat(ui): full‑width ShadTable across app; fix rent dialog width; correct equipment pagination
- ShadTable: ensure full-width via LayoutBuilder+ConstrainedBox minWidth
- BaseListScreen: default data area padding = 0 for table edge-to-edge
- Vendor/Model/User/Company/Inventory/Zipcode: set columnSpanExtent per column
  and add final filler column to absorb remaining width; pin date/status/actions
  widths; ensure date text is single-line
- Equipment: unify card/border style; define fixed column widths + filler;
  increase checkbox column to 56px to avoid overflow
- Rent list: migrate to ShadTable.list with fixed widths + filler column
- Rent form dialog: prevent infinite width by bounding ShadProgress with
  SizedBox and remove Expanded from option rows; add safe selectedOptionBuilder
- Admin list: fix const with non-const argument in table column extents
- Services/Controller: remove hardcoded perPage=10; use BaseListController
  perPage; trust server meta (total/totalPages) in equipment pagination
- widgets/shad_table: ConstrainedBox(minWidth=viewport) so table stretches

Run: flutter analyze → 0 errors (warnings remain).
2025-09-09 22:38:08 +09:00

168 lines
4.8 KiB
Dart

import 'package:flutter/material.dart';
import 'package:superport/screens/common/theme_shadcn.dart';
/// 자동완성 텍스트 필드 위젯
///
/// 입력, 드롭다운, 포커스, 필터링, 선택 기능을 모두 포함한다.
class AutocompleteTextField extends StatefulWidget {
final String label;
final String value;
final List<String> items;
final bool isRequired;
final String hintText;
final void Function(String) onChanged;
final void Function(String) onSelected;
final FocusNode? focusNode;
const AutocompleteTextField({
super.key,
required this.label,
required this.value,
required this.items,
required this.onChanged,
required this.onSelected,
this.isRequired = false,
this.hintText = '',
this.focusNode,
});
@override
State<AutocompleteTextField> createState() => _AutocompleteTextFieldState();
}
class _AutocompleteTextFieldState extends State<AutocompleteTextField> {
late final TextEditingController _controller;
late final FocusNode _focusNode;
late List<String> _filteredItems;
bool _showDropdown = false;
@override
void initState() {
super.initState();
_controller = TextEditingController(text: widget.value);
_focusNode = widget.focusNode ?? FocusNode();
_filteredItems = List.from(widget.items);
_controller.addListener(_onTextChanged);
_focusNode.addListener(() {
setState(() {
if (_focusNode.hasFocus) {
_showDropdown = _filteredItems.isNotEmpty;
} else {
_showDropdown = false;
}
});
});
}
@override
void didUpdateWidget(covariant AutocompleteTextField oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.value != _controller.text) {
_controller.text = widget.value;
}
if (widget.items != oldWidget.items) {
_filteredItems = List.from(widget.items);
}
}
@override
void dispose() {
if (widget.focusNode == null) {
_focusNode.dispose();
}
_controller.dispose();
super.dispose();
}
// 입력값 변경 시 필터링
void _onTextChanged() {
final text = _controller.text;
setState(() {
if (text.isEmpty) {
_filteredItems = List.from(widget.items);
} else {
_filteredItems =
widget.items
.where(
(item) => item.toLowerCase().contains(text.toLowerCase()),
)
.toList();
// 시작 부분이 일치하는 항목 우선 정렬
_filteredItems.sort((a, b) {
bool aStartsWith = a.toLowerCase().startsWith(text.toLowerCase());
bool bStartsWith = b.toLowerCase().startsWith(text.toLowerCase());
if (aStartsWith && !bStartsWith) return -1;
if (!aStartsWith && bStartsWith) return 1;
return a.compareTo(b);
});
}
_showDropdown = _filteredItems.isNotEmpty && _focusNode.hasFocus;
widget.onChanged(text);
});
}
void _handleSelect(String value) {
setState(() {
_controller.text = value;
_showDropdown = false;
});
widget.onSelected(value);
}
@override
Widget build(BuildContext context) {
return Stack(
children: [
TextFormField(
controller: _controller,
focusNode: _focusNode,
decoration: InputDecoration(
labelText: widget.label,
hintText: widget.hintText,
),
validator: (value) {
if (widget.isRequired && (value == null || value.isEmpty)) {
return '${widget.label}을(를) 입력해주세요';
}
return null;
},
onSaved: (value) {
widget.onSelected(value ?? '');
},
),
if (_showDropdown)
Positioned(
top: 50,
left: 0,
right: 0,
child: Container(
decoration: BoxDecoration(
color: ShadcnTheme.card,
border: Border.all(color: ShadcnTheme.border),
borderRadius: BorderRadius.circular(4),
boxShadow: ShadcnTheme.shadowSm,
),
constraints: const BoxConstraints(maxHeight: 200),
child: ListView.builder(
shrinkWrap: true,
itemCount: _filteredItems.length,
itemBuilder: (context, index) {
return InkWell(
onTap: () => _handleSelect(_filteredItems[index]),
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 8,
),
child: Text(_filteredItems[index]),
),
);
},
),
),
),
],
);
}
}