Changeset 259550 in webkit


Ignore:
Timestamp:
Apr 5, 2020 9:34:39 AM (4 years ago)
Author:
Wenson Hsieh
Message:

[iOS] Ugly and misaligned form validation bubble
https://bugs.webkit.org/show_bug.cgi?id=208472
<rdar://problem/59984027>

Reviewed by Tim Horton.

In iOS 13, the view of a UIViewController that is presented as a popover encompasses the arrow (connected to
the popover) that points to the target rect. This means that our current logic for laying out the inner text
label of a form validation bubble on iOS no longer works, since it sets a frame that is offset vertically and
horizontally from the bounds of the view controller's view.

To fix this, we need to respect the safe area insets of the view controller's view when laying out the label.
The idiomatic way to do this is to subclass -viewSafeAreaInsetsDidChange and -viewWillLayoutSubviews on the view
controller, and update the subview's (i.e. label's) frame; unfortunately, since ValidationBubble is implemented
in WebCore, we can't explicitly link against UIKit, so we need to dynamically create a UIViewController subclass
and override these subclassing hooks to get our desired behavior.

  • platform/ValidationBubble.h:
  • platform/ios/ValidationBubbleIOS.mm:

(invokeUIViewControllerSelector):
(WebValidationBubbleViewController_dealloc):
(WebValidationBubbleViewController_viewDidLoad):
(WebValidationBubbleViewController_viewWillLayoutSubviews):
(WebValidationBubbleViewController_viewSafeAreaInsetsDidChange):
(WebValidationBubbleViewController_labelFrame):
(WebValidationBubbleViewController_label):
(allocWebValidationBubbleViewControllerInstance):

Subclass and create a custom UIViewController to ensure that the label is vertically centered in its popover.
See above for more details.

(WebCore::ValidationBubble::ValidationBubble):
(WebCore::ValidationBubble::show):

Minor style fixes: remove extraneous .get()s on RetainPtr, and use property syntax when possible.

(WebCore::ValidationBubble::setAnchorRect):

Additionally remove a line of code that currently forces the form validation popover to present below its target
rect (and therefore have an arrow pointing up). It wasn't apparent why this logic was added in r208361, but it
seems the intention wasn't to restrict the popover to presenting below the target.

This allows the form validation popover to show up in the case where the input element is aligned to the very
bottom of the web view, such that there isn't enough space below the field to show the validation bubble.

Location:
trunk/Source/WebCore
Files:
3 edited

Legend:

Unmodified
Added
Removed
  • trunk/Source/WebCore/ChangeLog

    r259544 r259550  
     12020-04-05  Wenson Hsieh  <wenson_hsieh@apple.com>
     2
     3        [iOS] Ugly and misaligned form validation bubble
     4        https://bugs.webkit.org/show_bug.cgi?id=208472
     5        <rdar://problem/59984027>
     6
     7        Reviewed by Tim Horton.
     8
     9        In iOS 13, the view of a `UIViewController` that is presented as a popover encompasses the arrow (connected to
     10        the popover) that points to the target rect. This means that our current logic for laying out the inner text
     11        label of a form validation bubble on iOS no longer works, since it sets a frame that is offset vertically and
     12        horizontally from the bounds of the view controller's view.
     13
     14        To fix this, we need to respect the safe area insets of the view controller's view when laying out the label.
     15        The idiomatic way to do this is to subclass -viewSafeAreaInsetsDidChange and -viewWillLayoutSubviews on the view
     16        controller, and update the subview's (i.e. label's) frame; unfortunately, since ValidationBubble is implemented
     17        in WebCore, we can't explicitly link against UIKit, so we need to dynamically create a UIViewController subclass
     18        and override these subclassing hooks to get our desired behavior.
     19
     20        * platform/ValidationBubble.h:
     21        * platform/ios/ValidationBubbleIOS.mm:
     22        (invokeUIViewControllerSelector):
     23        (WebValidationBubbleViewController_dealloc):
     24        (WebValidationBubbleViewController_viewDidLoad):
     25        (WebValidationBubbleViewController_viewWillLayoutSubviews):
     26        (WebValidationBubbleViewController_viewSafeAreaInsetsDidChange):
     27        (WebValidationBubbleViewController_labelFrame):
     28        (WebValidationBubbleViewController_label):
     29        (allocWebValidationBubbleViewControllerInstance):
     30
     31        Subclass and create a custom UIViewController to ensure that the label is vertically centered in its popover.
     32        See above for more details.
     33
     34        (WebCore::ValidationBubble::ValidationBubble):
     35        (WebCore::ValidationBubble::show):
     36
     37        Minor style fixes: remove extraneous `.get()`s on `RetainPtr`, and use property syntax when possible.
     38
     39        (WebCore::ValidationBubble::setAnchorRect):
     40
     41        Additionally remove a line of code that currently forces the form validation popover to present below its target
     42        rect (and therefore have an arrow pointing up). It wasn't apparent why this logic was added in r208361, but it
     43        seems the intention wasn't to restrict the popover to presenting below the target.
     44
     45        This allows the form validation popover to show up in the case where the input element is aligned to the very
     46        bottom of the web view, such that there isn't enough space below the field to show the validation bubble.
     47
    1482020-04-04  Rob Buis  <rbuis@igalia.com>
    249
  • trunk/Source/WebCore/platform/ValidationBubble.h

    r237266 r259550  
    4040OBJC_CLASS WebValidationBubbleDelegate;
    4141OBJC_CLASS WebValidationBubbleTapRecognizer;
     42OBJC_CLASS WebValidationBubbleViewController;
    4243#endif
    4344
     
    8687    RetainPtr<NSPopover> m_popover;
    8788#elif PLATFORM(IOS_FAMILY)
    88     RetainPtr<UIViewController> m_popoverController;
     89    RetainPtr<WebValidationBubbleViewController> m_popoverController;
    8990    RetainPtr<WebValidationBubbleTapRecognizer> m_tapRecognizer;
    9091    RetainPtr<WebValidationBubbleDelegate> m_popoverDelegate;
  • trunk/Source/WebCore/platform/ios/ValidationBubbleIOS.mm

    r240494 r259550  
    3030#import "ValidationBubble.h"
    3131
     32#import <UIKit/UIGeometry.h>
     33#import <objc/message.h>
     34#import <objc/runtime.h>
    3235#import <pal/ios/UIKitSoftLink.h>
    3336#import <pal/spi/ios/UIKitSPI.h>
     
    3639#import <wtf/text/WTFString.h>
    3740
     41static const CGFloat validationBubbleHorizontalPadding = 17;
     42static const CGFloat validationBubbleVerticalPadding = 9;
     43static const CGFloat validationBubbleMaxLabelWidth = 300;
     44
     45@interface WebValidationBubbleViewController : UIViewController
     46@property (nonatomic, readonly) UILabel *label;
     47@property (nonatomic, readonly) CGRect labelFrame;
     48@end
     49
     50static void invokeUIViewControllerSelector(id instance, SEL selector)
     51{
     52    objc_super superClass { instance, PAL::getUIViewControllerClass() };
     53    auto superclassFunction = reinterpret_cast<void(*)(objc_super*, SEL)>(objc_msgSendSuper);
     54    superclassFunction(&superClass, selector);
     55}
     56
     57static void WebValidationBubbleViewController_dealloc(WebValidationBubbleViewController *instance, SEL)
     58{
     59    [instance.label release];
     60    [instance setValue:nil forKey:@"_label"];
     61
     62    invokeUIViewControllerSelector(instance, @selector(dealloc));
     63}
     64
     65static void WebValidationBubbleViewController_viewDidLoad(WebValidationBubbleViewController *instance, SEL)
     66{
     67    invokeUIViewControllerSelector(instance, @selector(viewDidLoad));
     68
     69    UILabel *label = [PAL::allocUILabelInstance() init];
     70    label.font = [PAL::getUIFontClass() preferredFontForTextStyle:PAL::get_UIKit_UIFontTextStyleCallout()];
     71    label.lineBreakMode = NSLineBreakByTruncatingTail;
     72    label.numberOfLines = 4;
     73    [instance.view addSubview:label];
     74    [instance setValue:label forKey:@"_label"];
     75}
     76
     77static void WebValidationBubbleViewController_viewWillLayoutSubviews(WebValidationBubbleViewController *instance, SEL)
     78{
     79    invokeUIViewControllerSelector(instance, @selector(viewWillLayoutSubviews));
     80
     81    instance.label.frame = instance.labelFrame;
     82}
     83
     84static void WebValidationBubbleViewController_viewSafeAreaInsetsDidChange(WebValidationBubbleViewController *instance, SEL)
     85{
     86    invokeUIViewControllerSelector(instance, @selector(viewSafeAreaInsetsDidChange));
     87
     88    instance.label.frame = instance.labelFrame;
     89}
     90
     91static CGRect WebValidationBubbleViewController_labelFrame(WebValidationBubbleViewController *instance, SEL)
     92{
     93    auto frameWithPadding = UIEdgeInsetsInsetRect(instance.view.bounds, instance.view.safeAreaInsets);
     94    return UIEdgeInsetsInsetRect(frameWithPadding, UIEdgeInsetsMake(validationBubbleVerticalPadding, validationBubbleHorizontalPadding, validationBubbleVerticalPadding, validationBubbleHorizontalPadding));
     95}
     96
     97static UILabel *WebValidationBubbleViewController_label(WebValidationBubbleViewController *instance, SEL)
     98{
     99    return [instance valueForKey:@"_label"];
     100}
     101
     102static WebValidationBubbleViewController *allocWebValidationBubbleViewControllerInstance()
     103{
     104    static Class theClass = nil;
     105    static dispatch_once_t onceToken;
     106    dispatch_once(&onceToken, ^{
     107        theClass = objc_allocateClassPair(PAL::getUIViewControllerClass(), "WebValidationBubbleViewController", 0);
     108        class_addMethod(theClass, @selector(dealloc), (IMP)WebValidationBubbleViewController_dealloc, "v@:");
     109        class_addMethod(theClass, @selector(viewDidLoad), (IMP)WebValidationBubbleViewController_viewDidLoad, "v@:");
     110        class_addMethod(theClass, @selector(viewWillLayoutSubviews), (IMP)WebValidationBubbleViewController_viewWillLayoutSubviews, "v@:");
     111        class_addMethod(theClass, @selector(viewSafeAreaInsetsDidChange), (IMP)WebValidationBubbleViewController_viewSafeAreaInsetsDidChange, "v@:");
     112        class_addMethod(theClass, @selector(label), (IMP)WebValidationBubbleViewController_label, "v@:");
     113        class_addMethod(theClass, @selector(labelFrame), (IMP)WebValidationBubbleViewController_labelFrame, "v@:");
     114        class_addIvar(theClass, "_label", sizeof(UILabel *), log2(sizeof(UILabel *)), "@");
     115        objc_registerClassPair(theClass);
     116    });
     117    return (WebValidationBubbleViewController *)[theClass alloc];
     118}
     119
    38120@interface WebValidationBubbleTapRecognizer : NSObject
    39121@end
     
    88170namespace WebCore {
    89171
    90 static const CGFloat horizontalPadding = 17;
    91 static const CGFloat verticalPadding = 9;
    92 static const CGFloat maxLabelWidth = 300;
    93 
    94 ValidationBubble::ValidationBubble(UIView* view, const String& message, const Settings&)
     172ValidationBubble::ValidationBubble(UIView *view, const String& message, const Settings&)
    95173    : m_view(view)
    96174    , m_message(message)
    97175{
    98     m_popoverController = adoptNS([PAL::allocUIViewControllerInstance() init]);
     176    m_popoverController = adoptNS([allocWebValidationBubbleViewControllerInstance() init]);
    99177    [m_popoverController setModalPresentationStyle:UIModalPresentationPopover];
    100 
    101     RetainPtr<UIView> popoverView = adoptNS([PAL::allocUIViewInstance() initWithFrame:CGRectZero]);
    102     [m_popoverController setView:popoverView.get()];
    103178    m_tapRecognizer = adoptNS([[WebValidationBubbleTapRecognizer alloc] initWithPopoverController:m_popoverController.get()]);
    104179
    105     RetainPtr<UILabel> label = adoptNS([PAL::allocUILabelInstance() initWithFrame:CGRectZero]);
    106     [label setText:message];
    107     [label setFont:[PAL::getUIFontClass() preferredFontForTextStyle:PAL::get_UIKit_UIFontTextStyleCallout()]];
    108     m_fontSize = [[label font] pointSize];
    109     [label setLineBreakMode:NSLineBreakByTruncatingTail];
    110     [label setNumberOfLines:4];
    111     [popoverView addSubview:label.get()];
    112 
    113     CGSize labelSize = [label sizeThatFits:CGSizeMake(maxLabelWidth, CGFLOAT_MAX)];
    114     [label setFrame:CGRectMake(horizontalPadding, verticalPadding, labelSize.width, labelSize.height)];
    115     [popoverView setFrame:CGRectMake(horizontalPadding, verticalPadding, labelSize.width + horizontalPadding * 2, labelSize.height + verticalPadding * 2)];
    116 
    117     [m_popoverController setPreferredContentSize:popoverView.get().frame.size];
     180    UILabel *label = [m_popoverController label];
     181    label.text = message;
     182    m_fontSize = label.font.pointSize;
     183    CGSize labelSize = [label sizeThatFits:CGSizeMake(validationBubbleMaxLabelWidth, CGFLOAT_MAX)];
     184    [m_popoverController setPreferredContentSize:CGSizeMake(labelSize.width + validationBubbleHorizontalPadding * 2, labelSize.height + validationBubbleVerticalPadding * 2)];
    118185}
    119186
     
    133200    [m_presentingViewController presentViewController:m_popoverController.get() animated:NO completion:[protectedThis]() {
    134201        // Hide this popover from VoiceOver and instead announce the message.
    135         [protectedThis->m_popoverController.get().view setAccessibilityElementsHidden:YES];
     202        [protectedThis->m_popoverController view].accessibilityElementsHidden = YES;
    136203    }];
    137204
     
    149216}
    150217
    151 void ValidationBubble::setAnchorRect(const IntRect& anchorRect, UIViewController* presentingViewController)
     218void ValidationBubble::setAnchorRect(const IntRect& anchorRect, UIViewController *presentingViewController)
    152219{
    153220    if (!presentingViewController)
     
    157224    m_popoverDelegate = adoptNS([[WebValidationBubbleDelegate alloc] init]);
    158225    presentationController.delegate = m_popoverDelegate.get();
    159     presentationController.passthroughViews = [NSArray arrayWithObjects:presentingViewController.view, m_view, nil];
    160 
    161     presentationController.permittedArrowDirections = UIPopoverArrowDirectionUp;
     226    presentationController.passthroughViews = @[ presentingViewController.view, m_view ];
    162227    presentationController.sourceView = m_view;
    163228    presentationController.sourceRect = CGRectMake(anchorRect.x(), anchorRect.y(), anchorRect.width(), anchorRect.height());
Note: See TracChangeset for help on using the changeset viewer.