From c141e12bc130a633816f9e1f63cd82db9459cf89 Mon Sep 17 00:00:00 2001 From: ANKIT JAISWAL Date: Fri, 12 Jun 2026 15:44:17 +0530 Subject: [PATCH 1/2] fix(ios): prevent modal confirm button overlap with font scaling Fixes confirm/cancel buttons becoming untappable when Dynamic Type is enabled in portrait and landscape modal date pickers. - Expand height adjustments to standard Dynamic Type categories - Add landscape height buffer and picker clipping - Reserve button touch area above UIAlertAction rows Refs henninghall/react-native-date-picker#960 Refs punchhdev.atlassian.net/browse/MF-3464 Co-authored-by: Cursor --- ios/RNDatePickerManager.mm | 84 +++++++++++++++++++++++++++++--------- 1 file changed, 65 insertions(+), 19 deletions(-) diff --git a/ios/RNDatePickerManager.mm b/ios/RNDatePickerManager.mm index da42873c..5e3e7562 100644 --- a/ios/RNDatePickerManager.mm +++ b/ios/RNDatePickerManager.mm @@ -87,6 +87,53 @@ - (UIView *)view [view setTextColorProp:[RCTConvert NSString:json]]; } +- (BOOL)isLandscapeOrientation +{ + UIInterfaceOrientation orientation = UIApplication.sharedApplication.statusBarOrientation; + return UIInterfaceOrientationIsLandscape(orientation); +} + +- (NSInteger)heightIncrementForContentSizeCategory:(UIContentSizeCategory)category +{ + if ([category isEqualToString:UIContentSizeCategoryLarge]) { + return 10; + } + if ([category isEqualToString:UIContentSizeCategoryExtraLarge]) { + return 20; + } + if ([category isEqualToString:UIContentSizeCategoryExtraExtraLarge]) { + return 35; + } + if ([category isEqualToString:UIContentSizeCategoryExtraExtraExtraLarge]) { + return 50; + } + if ([category isEqualToString:UIContentSizeCategoryAccessibilityMedium]) { + return 10; + } + if ([category isEqualToString:UIContentSizeCategoryAccessibilityLarge]) { + return 15; + } + if ([category isEqualToString:UIContentSizeCategoryAccessibilityExtraLarge]) { + return 40; + } + if ([category isEqualToString:UIContentSizeCategoryAccessibilityExtraExtraLarge]) { + return 70; + } + if ([category isEqualToString:UIContentSizeCategoryAccessibilityExtraExtraExtraLarge]) { + return 90; + } + return 0; +} + +- (NSInteger)buttonAreaReserveForHeightIncrement:(NSInteger)heightIncrement isLandscape:(BOOL)isLandscape +{ + NSInteger reserve = 60 + (heightIncrement / 2); + if (isLandscape) { + reserve += 20; + } + return reserve; +} + RCT_EXPORT_METHOD(openPicker:(NSDictionary *) props onConfirm:(RCTResponseSenderBlock) onConfirm onCancel:(RCTResponseSenderBlock) onCancel) @@ -94,6 +141,7 @@ - (UIView *)view dispatch_async(dispatch_get_main_queue(), ^{ bool iPad = UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad; + BOOL isLandscape = [self isLandscapeOrientation]; UIViewController *rootViewController = [UIApplication sharedApplication].delegate.window.rootViewController; CGRect rootBounds = rootViewController.view.bounds; NSString * title = [RCTConvert NSString:[props objectForKey:@"title"]]; @@ -108,20 +156,10 @@ - (UIView *)view CGRect pickerBounds = picker.bounds; - - // Detect the content size category UIContentSizeCategory contentSize = UIApplication.sharedApplication.preferredContentSizeCategory; - int heightIncrement = 0; - - // Adjust height based on content size category - if ([contentSize isEqualToString:UIContentSizeCategoryAccessibilityLarge]) { - heightIncrement = 15; - } else if ([contentSize isEqualToString:UIContentSizeCategoryAccessibilityExtraLarge]) { - heightIncrement = 40; - } else if ([contentSize isEqualToString:UIContentSizeCategoryAccessibilityExtraExtraLarge]) { - heightIncrement = 70; - } else if ([contentSize isEqualToString:UIContentSizeCategoryAccessibilityExtraExtraExtraLarge]) { - heightIncrement = 90; + NSInteger heightIncrement = [self heightIncrementForContentSizeCategory:contentSize]; + if (isLandscape) { + heightIncrement += 50; } // height @@ -129,17 +167,26 @@ - (UIView *)view int alertHeightPx = iPad ? (title ? 300 : 260) : (title ? 370 + heightIncrement : 340 + heightIncrement); NSLayoutConstraint *height = [NSLayoutConstraint constraintWithItem:alertView attribute:NSLayoutAttributeHeight relatedBy:NSLayoutRelationEqual toItem:nil attribute:NSLayoutAttributeNotAnAttribute multiplier:1 constant:alertHeightPx]; [alertView addConstraint:height]; - pickerBounds.size.height = pickerHeight; // width - double pickerWidth = [self getPickerWidth:alertView]; + double pickerWidth = [self getPickerWidth:alertView isLandscape:isLandscape]; int alertWidthPx = pickerWidth; NSLayoutConstraint *width = [NSLayoutConstraint constraintWithItem:alertView attribute:NSLayoutAttributeWidth relatedBy:NSLayoutRelationEqual toItem:nil attribute:NSLayoutAttributeNotAnAttribute multiplier:1 constant:alertWidthPx]; [alertView addConstraint:width]; pickerBounds.size.width = pickerWidth; - // top padding - pickerBounds.origin.y += iPad ? (title ? 20: 5) : (title ? 30 : 10); + CGFloat pickerTop = iPad ? (title ? 20 : 5) : (title ? 30 : 10); + NSInteger buttonAreaReserve = [self buttonAreaReserveForHeightIncrement:heightIncrement isLandscape:isLandscape]; + CGFloat maxPickerBottom = alertHeightPx - buttonAreaReserve; + CGFloat availablePickerHeight = MAX(maxPickerBottom - pickerTop, 0); + CGFloat targetPickerHeight = pickerHeight; + + if (isLandscape) { + targetPickerHeight = pickerHeight - 20; + } + pickerBounds.size.height = MIN(targetPickerHeight, availablePickerHeight); + pickerBounds.origin.y = pickerTop; + picker.clipsToBounds = YES; [picker setFrame: pickerBounds]; @@ -220,10 +267,9 @@ - (UIView *)view }); } -- (double) getPickerWidth :(UIView *) alertView +- (double) getPickerWidth:(UIView *)alertView isLandscape:(BOOL)isLandscape { bool iPad = UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad; - bool isLandscape = UIDeviceOrientationIsLandscape([UIDevice currentDevice].orientation); if(iPad) return 320; if (isLandscape) return 320; return alertView.bounds.size.width - 15; From 7a14e91718480b78a688562d51c46818a5be9966 Mon Sep 17 00:00:00 2001 From: ANKIT JAISWAL Date: Fri, 12 Jun 2026 16:31:54 +0530 Subject: [PATCH 2/2] fix(ios): cap modal alert height and reserve action button area Rework layout math so alert height is derived from title + picker + scaled action-button area, then capped to available screen height. Prevents picker/action overlap in landscape with Dynamic Type. Refs henninghall/react-native-date-picker#960 Refs punchhdev.atlassian.net/browse/MF-3464 Co-authored-by: Cursor --- ios/RNDatePickerManager.mm | 66 ++++++++++++++++++++++++++------------ 1 file changed, 45 insertions(+), 21 deletions(-) diff --git a/ios/RNDatePickerManager.mm b/ios/RNDatePickerManager.mm index 5e3e7562..c7960065 100644 --- a/ios/RNDatePickerManager.mm +++ b/ios/RNDatePickerManager.mm @@ -125,13 +125,41 @@ - (NSInteger)heightIncrementForContentSizeCategory:(UIContentSizeCategory)catego return 0; } -- (NSInteger)buttonAreaReserveForHeightIncrement:(NSInteger)heightIncrement isLandscape:(BOOL)isLandscape +- (NSInteger)actionButtonAreaHeightForContentSizeCategory:(UIContentSizeCategory)category { - NSInteger reserve = 60 + (heightIncrement / 2); + NSInteger baseHeight = 118; + return baseHeight + [self heightIncrementForContentSizeCategory:category]; +} + +- (NSInteger)titleAreaHeightForTitle:(NSString *)title iPad:(bool)iPad +{ + if (!title) { + return iPad ? 8 : 10; + } + return iPad ? 36 : 44; +} + +- (NSInteger)alertHeightForTitle:(NSString *)title + iPad:(bool)iPad + pickerHeight:(double)pickerHeight + buttonArea:(NSInteger)buttonArea + isLandscape:(BOOL)isLandscape + rootBounds:(CGRect)rootBounds +{ + NSInteger titleArea = [self titleAreaHeightForTitle:title iPad:iPad]; + NSInteger padding = isLandscape ? 12 : 16; + NSInteger contentHeight = titleArea + (NSInteger)ceil(pickerHeight) + buttonArea + padding; + NSInteger alertHeightPx = iPad ? MAX(contentHeight, title ? 300 : 260) : contentHeight; + + CGFloat maxHeight = CGRectGetHeight(rootBounds); if (isLandscape) { - reserve += 20; + maxHeight = maxHeight * 0.92; + } else { + maxHeight = maxHeight * 0.78; } - return reserve; + maxHeight = MAX(maxHeight, titleArea + buttonArea + 140); + + return MIN(alertHeightPx, (NSInteger)floor(maxHeight)); } RCT_EXPORT_METHOD(openPicker:(NSDictionary *) props @@ -157,14 +185,14 @@ - (NSInteger)buttonAreaReserveForHeightIncrement:(NSInteger)heightIncrement isLa CGRect pickerBounds = picker.bounds; UIContentSizeCategory contentSize = UIApplication.sharedApplication.preferredContentSizeCategory; - NSInteger heightIncrement = [self heightIncrementForContentSizeCategory:contentSize]; - if (isLandscape) { - heightIncrement += 50; - } - - // height + NSInteger buttonArea = [self actionButtonAreaHeightForContentSizeCategory:contentSize]; double pickerHeight = [self getPickerHeight:alertView]; - int alertHeightPx = iPad ? (title ? 300 : 260) : (title ? 370 + heightIncrement : 340 + heightIncrement); + int alertHeightPx = [self alertHeightForTitle:title + iPad:iPad + pickerHeight:pickerHeight + buttonArea:buttonArea + isLandscape:isLandscape + rootBounds:rootBounds]; NSLayoutConstraint *height = [NSLayoutConstraint constraintWithItem:alertView attribute:NSLayoutAttributeHeight relatedBy:NSLayoutRelationEqual toItem:nil attribute:NSLayoutAttributeNotAnAttribute multiplier:1 constant:alertHeightPx]; [alertView addConstraint:height]; @@ -175,17 +203,13 @@ - (NSInteger)buttonAreaReserveForHeightIncrement:(NSInteger)heightIncrement isLa [alertView addConstraint:width]; pickerBounds.size.width = pickerWidth; - CGFloat pickerTop = iPad ? (title ? 20 : 5) : (title ? 30 : 10); - NSInteger buttonAreaReserve = [self buttonAreaReserveForHeightIncrement:heightIncrement isLandscape:isLandscape]; - CGFloat maxPickerBottom = alertHeightPx - buttonAreaReserve; - CGFloat availablePickerHeight = MAX(maxPickerBottom - pickerTop, 0); - CGFloat targetPickerHeight = pickerHeight; - - if (isLandscape) { - targetPickerHeight = pickerHeight - 20; - } - pickerBounds.size.height = MIN(targetPickerHeight, availablePickerHeight); + NSInteger titleArea = [self titleAreaHeightForTitle:title iPad:iPad]; + CGFloat pickerTop = (CGFloat)titleArea; + CGFloat maxPickerBottom = alertHeightPx - buttonArea - 8; + CGFloat availablePickerHeight = MAX(maxPickerBottom - pickerTop, 120); + pickerBounds.size.height = MIN(pickerHeight, availablePickerHeight); pickerBounds.origin.y = pickerTop; + pickerBounds.origin.x = MAX((alertWidthPx - pickerWidth) / 2.0, 0); picker.clipsToBounds = YES; [picker setFrame: pickerBounds];