diff --git a/ios/FluentUI.Demo/FluentUI.Demo/Demos.swift b/ios/FluentUI.Demo/FluentUI.Demo/Demos.swift index 98c536880..5dacf2443 100644 --- a/ios/FluentUI.Demo/FluentUI.Demo/Demos.swift +++ b/ios/FluentUI.Demo/FluentUI.Demo/Demos.swift @@ -36,7 +36,7 @@ struct Demos { DemoDescriptor("IndeterminateProgressBar", IndeterminateProgressBarDemoController.self, supportsVisionOS: false), DemoDescriptor("Label", LabelDemoController.self, supportsVisionOS: true), DemoDescriptor("ListActionItem", ListActionItemDemoController.self, supportsVisionOS: false), - DemoDescriptor("ListItem", ListItemDemoController.self, supportsVisionOS: false), + DemoDescriptor("ListItem", ListItemDemoController.self, supportsVisionOS: true), DemoDescriptor("MultilineCommandBar", MultilineCommandBarDemoController.self, supportsVisionOS: false), DemoDescriptor("NavigationController", NavigationControllerDemoController.self, supportsVisionOS: true), DemoDescriptor("NotificationView", NotificationViewDemoController.self, supportsVisionOS: true), diff --git a/ios/FluentUI.Demo/FluentUI.Demo/Demos/ListItemDemoController_SwiftUI.swift b/ios/FluentUI.Demo/FluentUI.Demo/Demos/ListItemDemoController_SwiftUI.swift index 46bd8cd29..4ce21b822 100644 --- a/ios/FluentUI.Demo/FluentUI.Demo/Demos/ListItemDemoController_SwiftUI.swift +++ b/ios/FluentUI.Demo/FluentUI.Demo/Demos/ListItemDemoController_SwiftUI.swift @@ -21,7 +21,8 @@ class ListItemDemoControllerSwiftUI: UIHostingController { } struct ListItemDemoView: View { - @State var showingAlert: Bool = false + @State var showingPrimaryAlert: Bool = false + @State var showingSecondaryAlert: Bool = false @ObservedObject var fluentTheme: FluentTheme = .shared let accessoryTypes: [ListItemAccessoryType] = [.none, .checkmark, .detailButton, .disclosureIndicator] @@ -32,6 +33,7 @@ struct ListItemDemoView: View { @State var showFooter: Bool = false @State var showLeadingContent: Bool = true @State var showTrailingContent: Bool = true + @State var isTappable: Bool = true @State var isDisabled: Bool = false @State var accessoryType: ListItemAccessoryType = .none @State var leadingContentSize: ListItemLeadingContentSize = .default @@ -41,6 +43,7 @@ struct ListItemDemoView: View { @State var footerLineLimit: Int = 1 @State var trailingContentFocusableElementCount: Int = 0 @State var trailingContentToggleEnabled: Bool = true + @State var renderStandalone: Bool = false public var body: some View { @@ -73,7 +76,9 @@ struct ListItemDemoView: View { .accessibilityIdentifier("leadingContentSwitch") FluentUIDemoToggle(titleKey: "Show trailing content", isOn: $showTrailingContent) .accessibilityIdentifier("trailingContentSwitch") + FluentUIDemoToggle(titleKey: "Tappable", isOn: $isTappable) FluentUIDemoToggle(titleKey: "Disabled", isOn: $isDisabled) + FluentUIDemoToggle(titleKey: "Render standalone", isOn: $renderStandalone) } @ViewBuilder @@ -135,58 +140,75 @@ struct ListItemDemoView: View { .resizable() } + @ViewBuilder + var listItem: some View { + ListItem(title: title, + subtitle: showSubtitle ? subtitle : "", + footer: showFooter ? footer : "", + leadingContent: { + if showLeadingContent { + leadingContent + } + }, + trailingContent: { + if showTrailingContent { + switch trailingContentFocusableElementCount { + case 0: + Text("Spreadsheet") + case 1: + Toggle("", isOn: $trailingContentToggleEnabled) + default: + HStack { + Button { + showingSecondaryAlert = true + } label: { + Text("Button 1") + } + Button { + showingSecondaryAlert = true + } label: { + Text("Button 2") + } + } + } + } + }, + action: !isTappable ? nil : { + showingPrimaryAlert = true + } + ) + .backgroundStyleType(backgroundStyle) + .accessoryType(accessoryType) + .leadingContentSize(leadingContentSize) + .titleLineLimit(titleLineLimit) + .subtitleLineLimit(subtitleLineLimit) + .footerLineLimit(footerLineLimit) + .combineTrailingContentAccessibilityElement(trailingContentFocusableElementCount < 2) + .onAccessoryTapped { + showingSecondaryAlert = true + } + .disabled(isDisabled) + .alert("List Item tapped", isPresented: $showingPrimaryAlert) { + Button("OK", role: .cancel) { } + } + .alert("Detail button tapped", isPresented: $showingSecondaryAlert) { + Button("OK", role: .cancel) { } + } + } + @ViewBuilder var content: some View { VStack { + if renderStandalone { + listItem + } List { - Section { - ListItem(title: title, - subtitle: showSubtitle ? subtitle : "", - footer: showFooter ? footer : "", - leadingContent: { - if showLeadingContent { - leadingContent - } - }, - trailingContent: { - if showTrailingContent { - switch trailingContentFocusableElementCount { - case 0: - Text("Spreadsheet") - case 1: - Toggle("", isOn: $trailingContentToggleEnabled) - default: - HStack { - Button { - showingAlert = true - } label: { - Text("Button 1") - } - Button { - showingAlert = true - } label: { - Text("Button 2") - } - } - } - } - }) - .backgroundStyleType(backgroundStyle) - .accessoryType(accessoryType) - .leadingContentSize(leadingContentSize) - .titleLineLimit(titleLineLimit) - .subtitleLineLimit(subtitleLineLimit) - .footerLineLimit(footerLineLimit) - .combineTrailingContentAccessibilityElement(trailingContentFocusableElementCount < 2) - .onAccessoryTapped { - showingAlert = true - } - .disabled(isDisabled) - .alert("Detail button tapped", isPresented: $showingAlert) { - Button("OK", role: .cancel) { } + if !renderStandalone { + Section { + listItem + } header: { + Text("ListItem") } - } header: { - Text("ListItem") } controls } diff --git a/ios/FluentUI/List/ListItem.swift b/ios/FluentUI/List/ListItem.swift index a8444899b..e4f4038ae 100644 --- a/ios/FluentUI/List/ListItem.swift +++ b/ios/FluentUI/List/ListItem.swift @@ -17,25 +17,28 @@ public struct ListItem: View { - // MARK: Initializer - - /// Creates a ListItem view - /// - Parameters: - /// - title: Text that appears as the first line of text - /// - subtitle: Text that appears as the second line of text - /// - footer: Text that appears as the third line of text - /// - leadingContent: The content that appears on the leading edge of the view - /// - trailingContent: The content that appears on the trailing edge of the view, next to the accessory type if provided + // MARK: Initializer + + /// Creates a `ListItem` + /// - Parameters: + /// - title: Text that appears as the first line of text + /// - subtitle: Text that appears as the second line of text + /// - footer: Text that appears as the third line of text + /// - leadingContent: The content that appears on the leading edge of the view + /// - trailingContent: The content that appears on the trailing edge of the view, next to the accessory type if provided + /// - action: The action to be dispatched by tapping on the `ListItem` public init(title: Title, subtitle: Subtitle = String(), footer: Footer = String(), @ViewBuilder leadingContent: @escaping () -> LeadingContent, - @ViewBuilder trailingContent: @escaping () -> TrailingContent) { + @ViewBuilder trailingContent: @escaping () -> TrailingContent, + action: (() -> Void)? = nil) { self.title = title self.subtitle = subtitle self.footer = footer self.leadingContent = leadingContent self.trailingContent = trailingContent + self.action = action let layoutType = ListItem.layoutType(subtitle: subtitle, footer: footer) self.tokenSet = ListItemTokenSet(customViewSize: { layoutType.leadingContentSize }) } @@ -92,27 +95,30 @@ public struct ListItem LeadingContent)? private var trailingContent: (() -> TrailingContent)? + private var action: (() -> Void)? private let footer: Footer private let subtitle: Subtitle private let title: Title } +// MARK: Internal structs + +private struct ListItemButtonStyle: SwiftUI.ButtonStyle { + init(backgroundStyleType: ListItemBackgroundStyleType, tokenSet: ListItemTokenSet) { + self.backgroundStyleType = backgroundStyleType + self.tokenSet = tokenSet + } + + func makeBody(configuration: Configuration) -> some View { + let backgroundColor = configuration.isPressed ? tokenSet[.cellBackgroundSelectedColor].uiColor : .clear + let cornerRadius = backgroundStyleType == .plain && Compatibility.isDeviceIdiomVision() ? 16.0 : 0 + + return configuration.label + .background(Color(backgroundColor)) + .clipShape(RoundedRectangle(cornerRadius: cornerRadius)) + .contentShape(RoundedRectangle(cornerRadius: cornerRadius)) + .pointerInteraction(isEnabled) + } + + let backgroundStyleType: ListItemBackgroundStyleType + let tokenSet: ListItemTokenSet + + @Environment(\.isEnabled) private var isEnabled: Bool +} + // MARK: Constants private struct AccessibilityIdentifiers { @@ -287,10 +335,12 @@ private struct AccessibilityIdentifiers { public extension ListItem where LeadingContent == EmptyView, TrailingContent == EmptyView { init(title: Title, subtitle: Subtitle = String(), - footer: Footer = String()) { + footer: Footer = String(), + action: (() -> Void)? = nil) { self.title = title self.subtitle = subtitle self.footer = footer + self.action = action let layoutType = ListItem.layoutType(subtitle: subtitle, footer: footer) self.tokenSet = ListItemTokenSet(customViewSize: { layoutType.leadingContentSize }) } @@ -300,11 +350,13 @@ public extension ListItem where TrailingContent == EmptyView { init(title: Title, subtitle: Subtitle = String(), footer: Footer = String(), - @ViewBuilder leadingContent: @escaping () -> LeadingContent) { + @ViewBuilder leadingContent: @escaping () -> LeadingContent, + action: (() -> Void)? = nil) { self.title = title self.subtitle = subtitle self.footer = footer self.leadingContent = leadingContent + self.action = action let layoutType = ListItem.layoutType(subtitle: subtitle, footer: footer) self.tokenSet = ListItemTokenSet(customViewSize: { layoutType.leadingContentSize }) } @@ -314,11 +366,13 @@ public extension ListItem where LeadingContent == EmptyView { init(title: Title, subtitle: Subtitle = String(), footer: Footer = String(), - @ViewBuilder trailingContent: @escaping () -> TrailingContent) { + @ViewBuilder trailingContent: @escaping () -> TrailingContent, + action: (() -> Void)? = nil) { self.title = title self.subtitle = subtitle self.footer = footer self.trailingContent = trailingContent + self.action = action let layoutType = ListItem.layoutType(subtitle: subtitle, footer: footer) self.tokenSet = ListItemTokenSet(customViewSize: { layoutType.leadingContentSize }) }