Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ public struct StandardImageIdentifiers {

// Icon size 20x20
public struct Medium {
public static let adBlockerCheckmark = "adBlockerCheckmarkMedium"
public static let adBlockerCross = "adBlockerCrossMedium"
public static let arrowClockwise = "arrowClockwiseMedium"
public static let bookmarkBadgeFillBlue50 = "bookmarkBadgeFillMediumBlue50"
public static let cross = "crossMedium"
Expand Down
121 changes: 121 additions & 0 deletions BrowserKit/Sources/MenuKit/MenuSiteBadge.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/

import UIKit
import Common

final class MenuSiteBadge: UIView, ThemeApplicable {
private struct UX {
static let cornerRadius: CGFloat = 12
static let borderWidth: CGFloat = 1
static let horizontalPadding: CGFloat = 10
static let verticalPadding: CGFloat = 6
static let contentSpacing: CGFloat = 4
static let iconSize: CGFloat = 16
static let chevronSize: CGFloat = 20
}

var tapHandler: (() -> Void)?
private let mainMenuHelper: MainMenuInterface

private lazy var stack: UIStackView = .build { [weak self] stack in
guard let self else { return }
stack.isLayoutMarginsRelativeArrangement = true
stack.layoutMargins = UIEdgeInsets(top: UX.verticalPadding,
left: UX.horizontalPadding,
bottom: UX.verticalPadding,
right: UX.horizontalPadding)
stack.distribution = .fill
stack.axis = .horizontal
stack.clipsToBounds = true
stack.spacing = UX.contentSpacing
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(self.tapped))
stack.isUserInteractionEnabled = true
stack.addGestureRecognizer(tapGesture)
}

private var label: UILabel = .build { label in
label.font = FXFontStyles.Regular.footnote.scaledFont()
label.numberOfLines = 0
label.lineBreakMode = .byWordWrapping
label.adjustsFontForContentSizeCategory = true
label.accessibilityTraits = .button

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should make the entire view the a11y element here instead of the label, that way during voiceover the whole view is focused.

Would remove label.accessibilityTraits = .button and add label.isAccessibilityElement = false

then add this to the init:

isAccessibilityElement = true
accessibilityTraits = .button

and accessibilityLabel = text in configure()

}

private var icon: UIImageView = .build { imageView in
imageView.contentMode = .scaleAspectFit
}

private var chevron: UIImageView = .build { imageView in
let imageName = StandardImageIdentifiers.Large.chevronRight
let image = UIImage(named: imageName)?
.withRenderingMode(.alwaysTemplate)
.imageFlippedForRightToLeftLayoutDirection() ?? UIImage()
imageView.image = image
imageView.contentMode = .scaleAspectFit
}

init(mainMenuHelper: MainMenuInterface) {
self.mainMenuHelper = mainMenuHelper
super.init(frame: .zero)
setupViews()
}

required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}

override func layoutSubviews() {
super.layoutSubviews()
if #available(iOS 26.0, *) {
stack.layer.cornerRadius = stack.frame.height / 2
} else {
stack.layer.cornerRadius = UX.cornerRadius
stack.layer.borderWidth = UX.borderWidth
}
}

private func setupViews() {
addSubview(stack)
stack.addArrangedSubview(icon)
stack.addArrangedSubview(label)
stack.addArrangedSubview(chevron)

NSLayoutConstraint.activate([
stack.topAnchor.constraint(equalTo: topAnchor),
stack.bottomAnchor.constraint(equalTo: bottomAnchor),
stack.leadingAnchor.constraint(equalTo: leadingAnchor),
stack.trailingAnchor.constraint(equalTo: trailingAnchor),

icon.widthAnchor.constraint(equalToConstant: UX.iconSize),
chevron.widthAnchor.constraint(equalToConstant: UX.chevronSize)
])
}

func configure(text: String, iconName: String, useTemplate: Bool) {
label.text = text
let image: UIImage = useTemplate
? UIImage(named: iconName)?.withRenderingMode(.alwaysTemplate) ?? UIImage()
: UIImage(named: iconName) ?? UIImage()
icon.image = image
}

func applyTheme(theme: Theme) {
label.textColor = theme.colors.textSecondary
stack.layer.borderColor = theme.colors.actionSecondaryHover.cgColor
if #available(iOS 26.0, *) {
stack.backgroundColor = theme.colors.layerSurfaceMedium
.withAlphaComponent(mainMenuHelper.backgroundAlpha())
} else {
stack.backgroundColor = .clear
}
icon.tintColor = theme.colors.iconSecondary
chevron.tintColor = theme.colors.iconSecondary
}

@objc
private func tapped() {
tapHandler?()
}
}
148 changes: 59 additions & 89 deletions BrowserKit/Sources/MenuKit/MenuSiteProtectionsHeader.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,24 +7,31 @@ import Common
import SiteImageView
import ComponentLibrary

public struct MenuSiteAdBlockerBadgeData {
public let title: String
public let image: String
public let shouldUseRenderMode: Bool

public init(title: String, image: String, shouldUseRenderMode: Bool) {
self.title = title
self.image = image
self.shouldUseRenderMode = shouldUseRenderMode
}
}

public final class MenuSiteProtectionsHeader: UIView, ThemeApplicable {
private struct UX {
static let closeButtonSize: CGFloat = 30
static let contentLabelsSpacing: CGFloat = 1
static let horizontalContentMargin: CGFloat = 16
static let favIconSize: CGFloat = 40
static let siteProtectionsContentTopMargin: CGFloat = 4
static let siteProtectionsContentCornerRadius: CGFloat = 12
static let siteProtectionsContentBorderWidth: CGFloat = 1
static let siteProtectionsContentHorizontalPadding: CGFloat = 10
static let siteProtectionsContentVerticalPadding: CGFloat = 6
static let siteProtectionsIcon: CGFloat = 16
static let siteProtectionsMoreSettingsIcon: CGFloat = 20
static let siteProtectionsContentSpacing: CGFloat = 4
static let badgesTopMargin: CGFloat = 4
static let badgesSpacing: CGFloat = 8
}

public var closeButtonCallback: (() -> Void)?
public var siteProtectionsButtonCallback: (() -> Void)?
public var adBlockerButtonCallback: (() -> Void)?
public var mainMenuHelper: MainMenuInterface = MainMenuHelper()

private var contentLabels: UIStackView = .build { stack in
Expand Down Expand Up @@ -57,42 +64,26 @@ public final class MenuSiteProtectionsHeader: UIView, ThemeApplicable {
button.setImage(UIImage(named: imageName)?.withRenderingMode(.alwaysTemplate) ?? UIImage(), for: .normal)
}

private lazy var siteProtectionsContent: UIStackView = .build { [weak self] stack in
guard let self else { return }
stack.isLayoutMarginsRelativeArrangement = true
stack.layoutMargins = UIEdgeInsets(top: UX.siteProtectionsContentVerticalPadding,
left: UX.siteProtectionsContentHorizontalPadding,
bottom: UX.siteProtectionsContentVerticalPadding,
right: UX.siteProtectionsContentHorizontalPadding)
stack.distribution = .fill
private lazy var badgesStack: UIStackView = .build { stack in

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like for large dynamic type we will need to either add horizontal compression, or better yet, vertical stacking when dynamic type gets too large

Image

stack.axis = .horizontal
stack.clipsToBounds = true
stack.spacing = UX.siteProtectionsContentSpacing
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(siteProtectionsTapped))
stack.isUserInteractionEnabled = true
stack.addGestureRecognizer(tapGesture)
}

private var siteProtectionsLabel: UILabel = .build { label in
label.font = FXFontStyles.Regular.footnote.scaledFont()
label.numberOfLines = 0
label.lineBreakMode = .byWordWrapping
label.adjustsFontForContentSizeCategory = true
label.accessibilityTraits = .button
stack.alignment = .center
stack.distribution = .fill
stack.spacing = UX.badgesSpacing
}

private var siteProtectionsIcon: UIImageView = .build { imageView in
imageView.contentMode = .scaleAspectFit
}
private lazy var siteProtectionsBadge: MenuSiteBadge = {
let badge = MenuSiteBadge(mainMenuHelper: mainMenuHelper)
badge.translatesAutoresizingMaskIntoConstraints = false
badge.tapHandler = { [weak self] in self?.siteProtectionsButtonCallback?() }
return badge
}()

private var siteProtectionsMoreSettingsIcon: UIImageView = .build { imageView in
let imageName = StandardImageIdentifiers.Large.chevronRight
let image = UIImage(named: imageName)?
.withRenderingMode(.alwaysTemplate)
.imageFlippedForRightToLeftLayoutDirection() ?? UIImage()
imageView.image = image
imageView.contentMode = .scaleAspectFit
}
private lazy var adBlockerBadge: MenuSiteBadge = {
let badge = MenuSiteBadge(mainMenuHelper: mainMenuHelper)
badge.translatesAutoresizingMaskIntoConstraints = false
badge.tapHandler = { [weak self] in self?.adBlockerButtonCallback?() }
return badge
}()

init() {
super.init(frame: .zero)
Expand All @@ -103,34 +94,22 @@ public final class MenuSiteProtectionsHeader: UIView, ThemeApplicable {
fatalError("init(coder:) has not been implemented")
}

override public func layoutSubviews() {
super.layoutSubviews()
if #available(iOS 26.0, *) {
siteProtectionsContent.layer.cornerRadius = siteProtectionsContent.frame.height / 2
} else {
siteProtectionsContent.layer.cornerRadius = UX.siteProtectionsContentCornerRadius
siteProtectionsContent.layer.borderWidth = UX.siteProtectionsContentBorderWidth
}
}

private func setupViews() {
contentLabels.addArrangedSubview(titleLabel)
contentLabels.addArrangedSubview(subtitleLabel)
addSubviews(contentLabels, favicon, closeButton, siteProtectionsContent)
siteProtectionsContent.addArrangedSubview(siteProtectionsIcon)
siteProtectionsContent.addArrangedSubview(siteProtectionsLabel)
siteProtectionsContent.addArrangedSubview(siteProtectionsMoreSettingsIcon)
addSubviews(contentLabels, favicon, closeButton, badgesStack)
badgesStack.addArrangedSubview(siteProtectionsBadge)

let siteProtectionsTopFromFavicon = siteProtectionsContent.topAnchor.constraint(
let badgesTopFromFavicon = badgesStack.topAnchor.constraint(
greaterThanOrEqualTo: favicon.bottomAnchor,
constant: UX.siteProtectionsContentTopMargin
constant: UX.badgesTopMargin
)

let siteProtectionsTopFromLabels = siteProtectionsContent.topAnchor.constraint(
let badgesTopFromLabels = badgesStack.topAnchor.constraint(
equalTo: contentLabels.bottomAnchor,
constant: UX.siteProtectionsContentTopMargin
constant: UX.badgesTopMargin
)
siteProtectionsTopFromLabels.priority = .defaultHigh
badgesTopFromLabels.priority = .defaultHigh
NSLayoutConstraint.activate([
contentLabels.topAnchor.constraint(equalTo: self.topAnchor),
contentLabels.trailingAnchor.constraint(equalTo: closeButton.leadingAnchor,
Expand All @@ -145,17 +124,14 @@ public final class MenuSiteProtectionsHeader: UIView, ThemeApplicable {
closeButton.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: -UX.horizontalContentMargin),
closeButton.topAnchor.constraint(equalTo: self.topAnchor),

siteProtectionsIcon.widthAnchor.constraint(equalToConstant: UX.siteProtectionsIcon),
siteProtectionsMoreSettingsIcon.widthAnchor.constraint(equalToConstant: UX.siteProtectionsMoreSettingsIcon),

siteProtectionsTopFromLabels,
siteProtectionsTopFromFavicon,
siteProtectionsContent.trailingAnchor.constraint(lessThanOrEqualTo: closeButton.leadingAnchor),
siteProtectionsContent.bottomAnchor.constraint(equalTo: self.bottomAnchor),
badgesTopFromLabels,
badgesTopFromFavicon,
badgesStack.leadingAnchor.constraint(equalTo: favicon.leadingAnchor),
badgesStack.trailingAnchor.constraint(lessThanOrEqualTo: closeButton.leadingAnchor),
badgesStack.bottomAnchor.constraint(equalTo: self.bottomAnchor),

closeButton.widthAnchor.constraint(equalToConstant: UX.closeButtonSize),
closeButton.heightAnchor.constraint(equalToConstant: UX.closeButtonSize),
siteProtectionsContent.leadingAnchor.constraint(equalTo: favicon.leadingAnchor)
closeButton.heightAnchor.constraint(equalToConstant: UX.closeButtonSize)
])

closeButton.layer.cornerRadius = 0.5 * UX.closeButtonSize
Expand All @@ -167,17 +143,24 @@ public final class MenuSiteProtectionsHeader: UIView, ThemeApplicable {
image: String?,
state: String,
stateImage: String,
shouldUseRenderMode: Bool
shouldUseRenderMode: Bool,
adBlocker: MenuSiteAdBlockerBadgeData? = nil
) {
titleLabel.text = title
subtitleLabel.text = subtitle
siteProtectionsLabel.text = state
let siteProtectionsImage: UIImage = if shouldUseRenderMode {
UIImage(named: stateImage)?.withRenderingMode(.alwaysTemplate) ?? UIImage()
siteProtectionsBadge.configure(text: state,
iconName: stateImage,
useTemplate: shouldUseRenderMode)
if let adBlocker {
if adBlockerBadge.superview == nil {
badgesStack.addArrangedSubview(adBlockerBadge)
}
adBlockerBadge.configure(text: adBlocker.title,
iconName: adBlocker.image,
useTemplate: adBlocker.shouldUseRenderMode)
} else {
UIImage(named: stateImage) ?? UIImage()
adBlockerBadge.removeFromSuperview()
}
siteProtectionsIcon.image = siteProtectionsImage

let image = FaviconImageViewModel(siteURLString: image,
faviconCornerRadius: UX.favIconSize / 2)
Expand All @@ -197,25 +180,12 @@ public final class MenuSiteProtectionsHeader: UIView, ThemeApplicable {
closeButtonCallback?()
}

@objc
func siteProtectionsTapped() {
siteProtectionsButtonCallback?()
}

public func applyTheme(theme: Theme) {
titleLabel.textColor = theme.colors.textPrimary
subtitleLabel.textColor = theme.colors.textSecondary
closeButton.tintColor = theme.colors.iconSecondary
closeButton.backgroundColor = theme.colors.actionCloseButton.withAlphaComponent(mainMenuHelper.backgroundAlpha())
siteProtectionsLabel.textColor = theme.colors.textSecondary
siteProtectionsContent.layer.borderColor = theme.colors.actionSecondaryHover.cgColor
if #available(iOS 26.0, *) {
let backgroundColor = theme.colors.layerSurfaceMedium.withAlphaComponent(mainMenuHelper.backgroundAlpha())
siteProtectionsContent.backgroundColor = backgroundColor
} else {
siteProtectionsContent.backgroundColor = .clear
}
siteProtectionsIcon.tintColor = theme.colors.iconSecondary
siteProtectionsMoreSettingsIcon.tintColor = theme.colors.iconSecondary
siteProtectionsBadge.applyTheme(theme: theme)
adBlockerBadge.applyTheme(theme: theme)
}
}
1 change: 1 addition & 0 deletions BrowserKit/Sources/Shared/Prefs.swift
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ public struct PrefsKeys {
public static let ShowClipboardBar = "showClipboardBar"
public static let ShowRelayMaskSuggestions = "showRelayMaskSuggestions"
public static let BlockOpeningExternalApps = "blockOpeningExternalApps"
public static let BlockAds = "blockAds"
public static let NewTabCustomUrlPrefKey = "HomePageURLPref"
public static let GoogleTopSiteAddedKey = "googleTopSiteAddedKey"
public static let GoogleTopSiteHideKey = "googleTopSiteHideKey"
Expand Down
Loading