Skip to content
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 @@ -155,7 +155,7 @@ provided below is an organized table of W3C HTML tags and their equivalent Slips
W3C tag | Slipstream view
:--------|:----------------
[`<table>`](https://html.spec.whatwg.org/multipage/sections.html#the-table-element) | ``Table``
[`<caption>`](https://html.spec.whatwg.org/multipage/sections.html#the-caption-element) | [Not implemented yet](https://github.com/jverkoey/slipstream/issues/25)
[`<caption>`](https://html.spec.whatwg.org/multipage/sections.html#the-caption-element) | ``Caption``
[`<colgroup>`](https://html.spec.whatwg.org/multipage/sections.html#the-colgroup-element) | [Not implemented yet](https://github.com/jverkoey/slipstream/issues/25)
[`<col>`](https://html.spec.whatwg.org/multipage/sections.html#the-col-element) | [Not implemented yet](https://github.com/jverkoey/slipstream/issues/25)
[`<tbody>`](https://html.spec.whatwg.org/multipage/sections.html#the-tbody-element) | ``TableBody``
Expand All @@ -170,7 +170,7 @@ provided below is an organized table of W3C HTML tags and their equivalent Slips
W3C tag | Slipstream view
:--------|:----------------
[`<form>`](https://html.spec.whatwg.org/multipage/sections.html#the-form-element) | ``Form``
[`<label>`](https://html.spec.whatwg.org/multipage/sections.html#the-label-element) | [Not implemented yet](https://github.com/jverkoey/slipstream/issues/25)
[`<label>`](https://html.spec.whatwg.org/multipage/sections.html#the-label-element) | ``Label``
[`<input>`](https://html.spec.whatwg.org/multipage/sections.html#the-input-element) | ``TextField``
[`<button>`](https://html.spec.whatwg.org/multipage/sections.html#the-button-element) | ``Button``
[`<select>`](https://html.spec.whatwg.org/multipage/sections.html#the-select-element) | ``Picker``
Expand All @@ -181,8 +181,8 @@ provided below is an organized table of W3C HTML tags and their equivalent Slips
[`<output>`](https://html.spec.whatwg.org/multipage/sections.html#the-output-element) | [Not implemented yet](https://github.com/jverkoey/slipstream/issues/25)
[`<progress>`](https://html.spec.whatwg.org/multipage/sections.html#the-progress-element) | [Not implemented yet](https://github.com/jverkoey/slipstream/issues/25)
[`<meter>`](https://html.spec.whatwg.org/multipage/sections.html#the-meter-element) | [Not implemented yet](https://github.com/jverkoey/slipstream/issues/25)
[`<fieldset>`](https://html.spec.whatwg.org/multipage/sections.html#the-fieldset-element) | [Not implemented yet](https://github.com/jverkoey/slipstream/issues/25)
[`<legend>`](https://html.spec.whatwg.org/multipage/sections.html#the-legend-element) | [Not implemented yet](https://github.com/jverkoey/slipstream/issues/25)
[`<fieldset>`](https://html.spec.whatwg.org/multipage/sections.html#the-fieldset-element) | ``Fieldset``
[`<legend>`](https://html.spec.whatwg.org/multipage/sections.html#the-legend-element) | ``Legend``

### Interactive elements

Expand Down
32 changes: 32 additions & 0 deletions Sources/Slipstream/Fundamentals/AttributeModifier.swift
Original file line number Diff line number Diff line change
Expand Up @@ -107,3 +107,35 @@ where Content: Sendable {
}
}
}

/// A modifier that conditionally sets an HTML attribute on views based on a boolean condition.
///
/// This modifier is used for boolean HTML attributes like `disabled`, `checked`, `required`, etc.
/// When the condition is true, the attribute is set with an empty value. When false, the attribute
/// is not set at all.
@available(iOS 17.0, macOS 14.0, *)
public struct ConditionalAttributeModifier<T: View>: ViewModifier {
private let attribute: String
private let condition: Bool

/// Creates a conditional attribute modifier.
///
/// - Parameters:
/// - attribute: The HTML attribute name to conditionally set.
/// - condition: Whether the attribute should be present.
public init(_ attribute: String, condition: Bool) {
self.attribute = attribute
self.condition = condition
}

@ViewBuilder
public func body(content: T) -> some View {
if condition {
AttributeModifierView([attribute: ""]) {
content
}
} else {
content
}
}
}
24 changes: 24 additions & 0 deletions Sources/Slipstream/W3C/Attributes/View+disabled.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
extension View {
/// Disables the element, preventing user interaction.
///
/// For form-associated elements like fieldset, this attribute disables all
/// descendant form controls. When disabled, form controls cannot be focused,
/// edited, or submitted.
///
/// ```swift
/// Fieldset {
/// Legend("Payment Information")
/// TextField("Card Number", type: .text)
/// }
/// .disabled()
/// ```
///
/// - Parameter condition: A Boolean value that determines whether the element is disabled.
/// - Returns: A view with the disabled attribute conditionally set.
///
/// - SeeAlso: W3C [`disabled`](https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#attr-fe-disabled) specification.
@available(iOS 17.0, macOS 14.0, *)
public func disabled(_ condition: Bool = true) -> some View {
return modifier(ConditionalAttributeModifier("disabled", condition: condition))
}
}
61 changes: 61 additions & 0 deletions Sources/Slipstream/W3C/Elements/Forms/Fieldset.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import SwiftSoup

/// A view that groups related form controls together.
///
/// The fieldset element represents a set of form controls (or other content)
/// grouped together, optionally with a caption. The caption is given by the
/// first legend element child of the fieldset element, if any.
///
/// ```swift
/// Form {
/// Fieldset {
/// Legend("Shipping Address")
/// TextField("Street", type: .text)
/// TextField("City", type: .text)
/// TextField("Zip", type: .text)
/// }
/// Fieldset {
/// Legend("Payment Method")
/// RadioButton(name: "payment", value: "credit")
/// RadioButton(name: "payment", value: "debit")
/// }
/// }
/// ```
///
/// The `disabled` modifier can be used to disable all form controls within
/// the fieldset:
///
/// ```swift
/// Fieldset {
/// Legend("Disabled Section")
/// TextField("Name", type: .text)
/// }
/// .disabled()
///
/// - SeeAlso: W3C [`fieldset`](https://html.spec.whatwg.org/multipage/form-elements.html#the-fieldset-element) specification.
@available(iOS 17.0, macOS 14.0, *)
public struct Fieldset<Content>: View where Content: View {
/// Creates a fieldset with content.
///
/// - Parameters:
/// - name: The name of the fieldset for form submission purposes.
/// - content: A view builder that creates the fieldset's content.
public init(name: String? = nil, @ViewBuilder content: @escaping @Sendable () -> Content) {
self.name = name
self.content = content
}

@_documentation(visibility: private)
public func render(_ container: Element, environment: EnvironmentValues) throws {
let element = try container.appendElement("fieldset")

if let name {
try element.attr("name", name)
}

try self.content().render(element, environment: environment)
}

@ViewBuilder private let content: @Sendable () -> Content
private let name: String?
}
37 changes: 37 additions & 0 deletions Sources/Slipstream/W3C/Elements/Forms/Legend.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/// A view that represents a caption for a fieldset.
///
/// The legend element represents a caption for the content of its parent fieldset element.
///
/// ```swift
/// Fieldset {
/// Legend("Personal Information")
/// TextField("Name", type: .text)
/// TextField("Email", type: .email)
/// }
/// ```
///
/// - SeeAlso: W3C [`legend`](https://html.spec.whatwg.org/multipage/form-elements.html#the-legend-element) specification.
@available(iOS 17.0, macOS 14.0, *)
public struct Legend<Content>: W3CElement where Content: View {
@_documentation(visibility: private)
public let tagName: String = "legend"

@_documentation(visibility: private)
@ViewBuilder public let content: @Sendable () -> Content

/// Creates a legend with custom content.
///
/// - Parameter content: A view builder that creates the legend's content.
public init(@ViewBuilder content: @escaping @Sendable () -> Content) {
self.content = content
}

/// Creates a legend with static text.
///
/// - Parameter text: The text content of the legend.
public init(_ text: String) where Content == DOMString {
self.content = {
DOMString(text)
}
}
}
93 changes: 93 additions & 0 deletions Tests/SlipstreamTests/W3C/Forms/FieldsetTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import Testing

import Slipstream

struct FieldsetTests {
@Test func emptyBlock() throws {
try #expect(renderHTML(Fieldset {}) == "<fieldset></fieldset>")
}

@Test func withLegend() throws {
try #expect(renderHTML(Fieldset {
Legend("Personal Information")
}) == """
<fieldset>
<legend>Personal Information</legend>
</fieldset>
""")
}

@Test func withFormControls() throws {
try #expect(renderHTML(Fieldset {
Legend("Shipping Address")
TextField("Street", type: .text)
TextField("City", type: .text)
}) == """
<fieldset>
<legend>Shipping Address</legend>
<input type="text" placeholder="Street" />
<input type="text" placeholder="City" />
</fieldset>
""")
}

@Test func disabled() throws {
try #expect(renderHTML(Fieldset {
Legend("Disabled Section")
}.disabled()) == """
<fieldset disabled>
<legend>Disabled Section</legend>
</fieldset>
""")
}

@Test func withName() throws {
try #expect(renderHTML(Fieldset(name: "shipping") {
Legend("Shipping")
}) == """
<fieldset name="shipping">
<legend>Shipping</legend>
</fieldset>
""")
}

@Test func allAttributes() throws {
try #expect(renderHTML(Fieldset(name: "payment") {
Legend("Payment Method")
}.disabled()) == """
<fieldset name="payment" disabled>
<legend>Payment Method</legend>
</fieldset>
""")
}

@Test func nestedFieldsets() throws {
try #expect(renderHTML(Fieldset {
Legend("Customer Information")
Fieldset {
Legend("Personal Details")
TextField("Name", type: .text)
}
Fieldset {
Legend("Contact Details")
TextField("Email", type: .email)
}
}) == """
<fieldset>
<legend>Customer Information</legend>
<fieldset>
<legend>Personal Details</legend>
<input type="text" placeholder="Name" />
</fieldset>
<fieldset>
<legend>Contact Details</legend>
<input type="email" placeholder="Email" />
</fieldset>
</fieldset>
""")
}

@Test func attribute() throws {
try #expect(renderHTML(Fieldset {}.language("en")) == #"<fieldset lang="en"></fieldset>"#)
}
}
23 changes: 23 additions & 0 deletions Tests/SlipstreamTests/W3C/Forms/LegendTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import Testing

import Slipstream

struct LegendTests {
@Test func emptyBlock() throws {
try #expect(renderHTML(Legend {}) == "<legend></legend>")
}

@Test func withText() throws {
try #expect(renderHTML(Legend {
DOMString("Personal Information")
}) == "<legend>Personal Information</legend>")
}

@Test func withStringLiteral() throws {
try #expect(renderHTML(Legend("Shipping Address")) == "<legend>Shipping Address</legend>")
}

@Test func attribute() throws {
try #expect(renderHTML(Legend {}.language("en")) == #"<legend lang="en"></legend>"#)
}
}