Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cbowdoin/bottom sheet supports x axis constraints #1951

Merged
Merged
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 @@ -91,6 +91,14 @@ class BottomSheetDemoController: DemoController {
bottomSheetViewController?.preferredExpandedContentHeight = sender.isOn ? 0 : 400
}

@objc private func toggleTrailingEdge(_ sender: BooleanCell) {
bottomSheetViewController?.anchoredEdge = sender.isOn ? .trailing : .center
}

@objc private func togglePreferredWidth(_ sender: BooleanCell) {
bottomSheetViewController?.preferredWidth = sender.isOn ? 400 : 0
}

@objc private func showTransientSheet() {
let hostingVC = UIHostingController(rootView: BottomSheetDemoListContentView())

Expand Down Expand Up @@ -249,7 +257,15 @@ class BottomSheetDemoController: DemoController {
DemoItem(title: "Hide collapsed content", type: .boolean, action: #selector(toggleCollapsedContentHiding), isOn: collapsedContentHidingEnabled),
DemoItem(title: "Flexible sheet height", type: .boolean, action: #selector(toggleFlexibleSheetHeight), isOn: bottomSheetViewController?.isFlexibleHeight ?? false),
DemoItem(title: "Use custom handle accessibility label", type: .boolean, action: #selector(toggleHandleUsingCustomAccessibilityLabel), isOn: isHandleUsingCustomAccessibilityLabel),
DemoItem(title: "Full screen sheet content", type: .boolean, action: #selector(toggleFullScreenSheetContent), isOn: bottomSheetViewController?.preferredExpandedContentHeight == 0)
DemoItem(title: "Full screen sheet content", type: .boolean, action: #selector(toggleFullScreenSheetContent), isOn: bottomSheetViewController?.preferredExpandedContentHeight == 0),
DemoItem(title: "Attach to trailing edge",
type: .boolean,
action: #selector(toggleTrailingEdge),
isOn: bottomSheetViewController?.anchoredEdge == .trailing),
DemoItem(title: "Set preferred width to 400",
type: .boolean,
action: #selector(togglePreferredWidth),
isOn: bottomSheetViewController?.preferredWidth == 400)
],
[
DemoItem(title: "Show transient sheet", type: .action, action: #selector(showTransientSheet))
Expand Down
82 changes: 78 additions & 4 deletions ios/FluentUI/Bottom Sheet/BottomSheetController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,13 @@ public protocol BottomSheetControllerDelegate: AnyObject {
case transitioning // Sheet is between states, only used during user interaction / animation
}

/// Defines where the sheet should be postionioned relative to the screen space
@objc(MSFBottomSheetAnchorEdge) public enum BottomSheetAnchorEdge: Int {
case center // Sheet is centered on the screen
case leading // Sheet is constrained to the leading edge
case trailing // Sheet is constrained to the trailing edge
}

@objc(MSFBottomSheetController)
public class BottomSheetController: UIViewController, Shadowable, TokenizedControlInternal {

Expand Down Expand Up @@ -223,6 +230,28 @@ public class BottomSheetController: UIViewController, Shadowable, TokenizedContr
}
}

/// Setting this property will result in the sheet trying to be as close to this width as possible.
/// If the declared width is too large it will roll back to the maximum width
@objc open var preferredWidth: CGFloat = 0 {
cbowdoin marked this conversation as resolved.
Show resolved Hide resolved
didSet {
let shouldInvalidateLayout = (preferredWidth != oldValue) && isViewLoaded
if shouldInvalidateLayout {
view.setNeedsLayout()
}
}
}

/// Represents where the sheet should appear on the screen.
/// Defaults to being centered
@objc open var anchoredEdge: BottomSheetAnchorEdge = .center {
didSet {
guard anchoredEdge != oldValue && isViewLoaded else {
return
}
view.setNeedsLayout()
}
}

/// When enabled, users will be able to move the sheet to the hidden state by swiping down.
@objc open var allowsSwipeToHide: Bool = false

Expand Down Expand Up @@ -721,8 +750,8 @@ public class BottomSheetController: UIViewController, Shadowable, TokenizedContr
// Source of truth for the sheet frame at a given offset from the top of the root view bounds.
// The output is only meaningful once view.bounds is non-zero i.e. a layout pass has occured.
private func sheetFrame(offset: CGFloat) -> CGRect {
let availableWidth: CGFloat = view.bounds.width
let sheetWidth = shouldAlwaysFillWidth ? availableWidth : min(Constants.maxSheetWidth, availableWidth)
let sheetWidth: CGFloat = determineSheetWidth()

let sheetHeight: CGFloat

if isFlexibleHeight {
Expand All @@ -732,8 +761,50 @@ public class BottomSheetController: UIViewController, Shadowable, TokenizedContr
sheetHeight = expandedSheetHeight
}

return CGRect(origin: CGPoint(x: (view.bounds.width - sheetWidth) / 2, y: offset),
size: CGSize(width: sheetWidth, height: sheetHeight))
// Calculates the location to put the left edge of the sheet relative to the view
// For right aligned we get the width of the view offset by the sheets width and the padding
// For left aligned we only need to add in the padding
// For center aligned we need the position of the center offset by half the sheets width
let xPosition: CGFloat
let isLeftToRight: Bool = UIView.userInterfaceLayoutDirection(for: view.semanticContentAttribute) == .leftToRight
switch anchoredEdge {
case .center:
xPosition = (view.bounds.width - sheetWidth) / 2
case .leading:
if isLeftToRight {
xPosition = Constants.horizontalSheetPadding
} else {
xPosition = view.bounds.width - sheetWidth - Constants.horizontalSheetPadding
}
case .trailing:
if isLeftToRight {
xPosition = view.bounds.width - sheetWidth - Constants.horizontalSheetPadding
} else {
xPosition = Constants.horizontalSheetPadding
}
}

let frame = CGRect(origin: CGPoint(x: xPosition, y: offset),
size: CGSize(width: sheetWidth, height: sheetHeight))
return frame
}

// Helper function to determine how wide the sheet should be
private func determineSheetWidth() -> CGFloat {
// Width will the fill the screen if should always fill width
// Otherwise we will try and set the size to the preferred width as long as its between the max and min width
// If its not between those we will make the maximum width size
let availableWidth: CGFloat = view.bounds.width
let maxWidth = min(Constants.maxSheetWidth, availableWidth)
let determinedWidth: CGFloat
if shouldAlwaysFillWidth {
determinedWidth = availableWidth
} else if Constants.minSheetWidth...maxWidth ~= preferredWidth {
determinedWidth = preferredWidth
} else {
determinedWidth = maxWidth
}
return determinedWidth
}

private func translationRubberBandFactor(for currentOffset: CGFloat) -> CGFloat {
Expand Down Expand Up @@ -1064,6 +1135,9 @@ public class BottomSheetController: UIViewController, Shadowable, TokenizedContr
// Minimum padding from top when the sheet is fully expanded
static let minimumTopExpandedPadding: CGFloat = 25.0

// The padding allocated to the space between the sheet and the edge when attached to the leading or trailing edge
static let horizontalSheetPadding: CGFloat = GlobalTokens.spacing(.size80)

static let expandedContentAlphaTransitionLength: CGFloat = 30

static let maxSheetWidth: CGFloat = 610
Expand Down
Loading