From 99ff4a8b235b99ed0faea9376c05fac503dc1798 Mon Sep 17 00:00:00 2001 From: KimByeongHun Date: Tue, 26 May 2026 23:00:25 +0900 Subject: [PATCH 1/3] =?UTF-8?q?fix:=20=EA=B8=B4=20=ED=8F=B4=EB=8D=94?= =?UTF-8?q?=EB=AA=85=20=EB=93=9C=EB=A1=AD=EB=8B=A4=EC=9A=B4=20=EB=A7=90?= =?UTF-8?q?=EC=A4=84=EC=9E=84=20=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- components/ui/filter-chip.tsx | 40 ++++++++++++++++++++++++++++++----- 1 file changed, 35 insertions(+), 5 deletions(-) diff --git a/components/ui/filter-chip.tsx b/components/ui/filter-chip.tsx index 4eef242..0882ef0 100644 --- a/components/ui/filter-chip.tsx +++ b/components/ui/filter-chip.tsx @@ -1,8 +1,15 @@ import { useState } from 'react'; -import { Pressable, StyleSheet, Text, View } from 'react-native'; +import { Pressable, StyleSheet, Text, useWindowDimensions, View } from 'react-native'; import { IconSymbol } from '@/components/ui/icon-symbol'; import { Colors, Typography } from '@/constants/theme'; +const EXPANDED_LABEL_THRESHOLD = 6; +const MAX_VISIBLE_LABEL_CHARS = 15; +const LABEL_MAX_WIDTH = Typography.caption.fontSize * MAX_VISIBLE_LABEL_CHARS; +const DROPDOWN_MIN_WIDTH = 120; +const DROPDOWN_HORIZONTAL_PADDING = 32; +const DROPDOWN_MAX_CONTENT_WIDTH = LABEL_MAX_WIDTH + DROPDOWN_HORIZONTAL_PADDING; + // ─── Types ──────────────────────────────────────────────────────────────────── export type FilterChipVariant = 'active' | 'inactive'; @@ -29,11 +36,12 @@ interface DropdownProps { items: FilterChipItem[]; selectedValue: string; onSelect: (value: string) => void; + width: number; } -function Dropdown({ items, selectedValue, onSelect }: DropdownProps) { +function Dropdown({ items, selectedValue, onSelect, width }: DropdownProps) { return ( - + {items.map((item, index) => { const isSelected = item.value === selectedValue; const isLast = index === items.length - 1; @@ -55,6 +63,8 @@ function Dropdown({ items, selectedValue, onSelect }: DropdownProps) { dropdownStyles.itemLabel, { color: isSelected ? Colors.brand.text : Colors.brand.textSecondary }, ]} + numberOfLines={1} + ellipsizeMode="tail" > {item.label} @@ -75,7 +85,7 @@ const dropdownStyles = StyleSheet.create({ marginTop: 4, backgroundColor: Colors.brand.surface, borderRadius: 10, - minWidth: 120, + minWidth: DROPDOWN_MIN_WIDTH, zIndex: 100, shadowColor: Colors.brand.text, shadowOffset: { width: 0, height: 2 }, @@ -117,9 +127,15 @@ export function FilterChip({ onPress, }: FilterChipProps) { const [isOpen, setIsOpen] = useState(false); + const { width } = useWindowDimensions(); const currentItem = items.find((i) => i.value === selectedValue); const displayLabel = label ?? currentItem?.label ?? ''; + const hasLongDropdownItem = items.some((item) => item.label.length > EXPANDED_LABEL_THRESHOLD); + const dropdownWidth = Math.min( + hasLongDropdownItem ? DROPDOWN_MAX_CONTENT_WIDTH : DROPDOWN_MIN_WIDTH, + Math.max(DROPDOWN_MIN_WIDTH, width - 40), + ); const isActive = variant === 'active'; const labelColor = disabled @@ -151,7 +167,13 @@ export function FilterChip({ accessibilityRole="button" accessibilityState={{ disabled, expanded: isOpen }} > - {displayLabel} + + {displayLabel} + {icon && ( )} @@ -178,12 +201,17 @@ const chipStyles = StyleSheet.create({ wrapper: { position: 'relative', alignSelf: 'flex-start', + flexShrink: 1, + minWidth: 0, + maxWidth: '100%', zIndex: 10, }, chip: { flexDirection: 'row', alignItems: 'center', + flexShrink: 1, gap: 4, + maxWidth: '100%', paddingVertical: 8, paddingHorizontal: 12, borderRadius: 100, @@ -200,6 +228,8 @@ const chipStyles = StyleSheet.create({ }, label: { ...Typography.caption, + flexShrink: 1, + minWidth: 0, }, iconRotated: { transform: [{ rotate: '180deg' }], From 35e67ba45f903dba3124c8756cf03ff49c4833d9 Mon Sep 17 00:00:00 2001 From: KimByeongHun Date: Tue, 26 May 2026 23:18:33 +0900 Subject: [PATCH 2/3] =?UTF-8?q?fix:=20=ED=8F=B4=EB=8D=94=20=EB=93=9C?= =?UTF-8?q?=EB=A1=AD=EB=8B=A4=EC=9A=B4=20=EA=B8=B4=20=EB=AA=A9=EB=A1=9D=20?= =?UTF-8?q?=EC=8A=A4=ED=81=AC=EB=A1=A4=20=EB=8F=99=EC=9E=91=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- components/ui/filter-chip.tsx | 131 +++++++++++++++++++++++----------- 1 file changed, 89 insertions(+), 42 deletions(-) diff --git a/components/ui/filter-chip.tsx b/components/ui/filter-chip.tsx index 0882ef0..10a1b5c 100644 --- a/components/ui/filter-chip.tsx +++ b/components/ui/filter-chip.tsx @@ -1,5 +1,5 @@ -import { useState } from 'react'; -import { Pressable, StyleSheet, Text, useWindowDimensions, View } from 'react-native'; +import { useRef, useState } from 'react'; +import { Modal, Pressable, ScrollView, StyleSheet, Text, useWindowDimensions, View } from 'react-native'; import { IconSymbol } from '@/components/ui/icon-symbol'; import { Colors, Typography } from '@/constants/theme'; @@ -9,6 +9,9 @@ const LABEL_MAX_WIDTH = Typography.caption.fontSize * MAX_VISIBLE_LABEL_CHARS; const DROPDOWN_MIN_WIDTH = 120; const DROPDOWN_HORIZONTAL_PADDING = 32; const DROPDOWN_MAX_CONTENT_WIDTH = LABEL_MAX_WIDTH + DROPDOWN_HORIZONTAL_PADDING; +const DROPDOWN_MAX_HEIGHT_RATIO = 0.6; +const DROPDOWN_SCREEN_PADDING = 20; +const DROPDOWN_MIN_HEIGHT = 120; // ─── Types ──────────────────────────────────────────────────────────────────── @@ -37,52 +40,71 @@ interface DropdownProps { selectedValue: string; onSelect: (value: string) => void; width: number; + maxHeight: number; + left: number; + top: number; + onDismiss: () => void; } -function Dropdown({ items, selectedValue, onSelect, width }: DropdownProps) { +function Dropdown({ items, selectedValue, onSelect, width, maxHeight, left, top, onDismiss }: DropdownProps) { return ( - - {items.map((item, index) => { - const isSelected = item.value === selectedValue; - const isLast = index === items.length - 1; - - return ( - - onSelect(item.value)} - style={({ pressed }) => [ - dropdownStyles.item, - isSelected && dropdownStyles.itemSelected, - pressed && dropdownStyles.itemPressed, - ]} - accessibilityRole="menuitem" - accessibilityState={{ selected: isSelected }} - > - - {item.label} - - - {!isLast && } - - ); - })} - + + + + + 6} + keyboardShouldPersistTaps="handled" + > + {items.map((item, index) => { + const isSelected = item.value === selectedValue; + const isLast = index === items.length - 1; + + return ( + + onSelect(item.value)} + style={({ pressed }) => [ + dropdownStyles.item, + isSelected && dropdownStyles.itemSelected, + pressed && dropdownStyles.itemPressed, + ]} + accessibilityRole="menuitem" + accessibilityState={{ selected: isSelected }} + > + + {item.label} + + + {!isLast && } + + ); + })} + + + + ); } const dropdownStyles = StyleSheet.create({ + modalRoot: { + flex: 1, + }, + backdrop: { + ...StyleSheet.absoluteFillObject, + }, container: { position: 'absolute', - top: '100%', - left: 0, - marginTop: 4, backgroundColor: Colors.brand.surface, borderRadius: 10, minWidth: DROPDOWN_MIN_WIDTH, @@ -94,6 +116,9 @@ const dropdownStyles = StyleSheet.create({ elevation: 4, overflow: 'hidden', }, + scroll: { + flexGrow: 0, + }, item: { paddingVertical: 12, paddingHorizontal: 16, @@ -127,7 +152,9 @@ export function FilterChip({ onPress, }: FilterChipProps) { const [isOpen, setIsOpen] = useState(false); - const { width } = useWindowDimensions(); + const [chipBounds, setChipBounds] = useState({ x: 0, y: 0, width: 0, height: 0 }); + const wrapperRef = useRef(null); + const { width, height } = useWindowDimensions(); const currentItem = items.find((i) => i.value === selectedValue); const displayLabel = label ?? currentItem?.label ?? ''; @@ -136,6 +163,15 @@ export function FilterChip({ hasLongDropdownItem ? DROPDOWN_MAX_CONTENT_WIDTH : DROPDOWN_MIN_WIDTH, Math.max(DROPDOWN_MIN_WIDTH, width - 40), ); + const dropdownLeft = Math.max( + DROPDOWN_SCREEN_PADDING, + Math.min(chipBounds.x, width - dropdownWidth - DROPDOWN_SCREEN_PADDING), + ); + const dropdownTop = chipBounds.y + chipBounds.height + 4; + const dropdownMaxHeight = Math.max( + DROPDOWN_MIN_HEIGHT, + Math.min(height * DROPDOWN_MAX_HEIGHT_RATIO, height - dropdownTop - DROPDOWN_SCREEN_PADDING), + ); const isActive = variant === 'active'; const labelColor = disabled @@ -145,7 +181,14 @@ export function FilterChip({ function handlePress() { if (disabled) return; if (isActive && items.length > 0) { - setIsOpen((prev) => !prev); + if (isOpen) { + setIsOpen(false); + } else { + wrapperRef.current?.measureInWindow((x, y, measuredWidth, measuredHeight) => { + setChipBounds({ x, y, width: measuredWidth, height: measuredHeight }); + setIsOpen(true); + }); + } } onPress?.(); } @@ -156,7 +199,7 @@ export function FilterChip({ } return ( - + [ @@ -191,6 +234,10 @@ export function FilterChip({ selectedValue={selectedValue ?? ''} onSelect={handleSelect} width={dropdownWidth} + maxHeight={dropdownMaxHeight} + left={dropdownLeft} + top={dropdownTop} + onDismiss={() => setIsOpen(false)} /> )} From 811a5317e5fb5d70544c075588c99bf6dd9371d9 Mon Sep 17 00:00:00 2001 From: KimByeongHun Date: Tue, 26 May 2026 23:40:58 +0900 Subject: [PATCH 3/3] =?UTF-8?q?fix:=20=ED=8F=B4=EB=8D=94=20=EB=93=9C?= =?UTF-8?q?=EB=A1=AD=EB=8B=A4=EC=9A=B4=20=EB=AA=A8=EB=8B=AC=20=EC=9C=84?= =?UTF-8?q?=EC=B9=98=20=EB=B3=B4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- components/ui/filter-chip.tsx | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/components/ui/filter-chip.tsx b/components/ui/filter-chip.tsx index 10a1b5c..891bc7b 100644 --- a/components/ui/filter-chip.tsx +++ b/components/ui/filter-chip.tsx @@ -1,5 +1,6 @@ import { useRef, useState } from 'react'; import { Modal, Pressable, ScrollView, StyleSheet, Text, useWindowDimensions, View } from 'react-native'; +import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { IconSymbol } from '@/components/ui/icon-symbol'; import { Colors, Typography } from '@/constants/theme'; @@ -153,7 +154,8 @@ export function FilterChip({ }: FilterChipProps) { const [isOpen, setIsOpen] = useState(false); const [chipBounds, setChipBounds] = useState({ x: 0, y: 0, width: 0, height: 0 }); - const wrapperRef = useRef(null); + const chipRef = useRef(null); + const insets = useSafeAreaInsets(); const { width, height } = useWindowDimensions(); const currentItem = items.find((i) => i.value === selectedValue); @@ -167,7 +169,7 @@ export function FilterChip({ DROPDOWN_SCREEN_PADDING, Math.min(chipBounds.x, width - dropdownWidth - DROPDOWN_SCREEN_PADDING), ); - const dropdownTop = chipBounds.y + chipBounds.height + 4; + const dropdownTop = chipBounds.y + chipBounds.height + insets.top + 4; const dropdownMaxHeight = Math.max( DROPDOWN_MIN_HEIGHT, Math.min(height * DROPDOWN_MAX_HEIGHT_RATIO, height - dropdownTop - DROPDOWN_SCREEN_PADDING), @@ -184,7 +186,7 @@ export function FilterChip({ if (isOpen) { setIsOpen(false); } else { - wrapperRef.current?.measureInWindow((x, y, measuredWidth, measuredHeight) => { + chipRef.current?.measureInWindow((x, y, measuredWidth, measuredHeight) => { setChipBounds({ x, y, width: measuredWidth, height: measuredHeight }); setIsOpen(true); }); @@ -199,8 +201,9 @@ export function FilterChip({ } return ( - + [ chipStyles.chip,