Skip to content

Commit c8adda1

Browse files
committed
Implement remaining input element types
Add SwiftUI-familiar components for the remaining HTML input types: - ColorPicker for input type="color" - Slider for input type="range" - FileInput for input type="file" - HiddenField for input type="hidden" Each component follows established patterns with: - SwiftUI-inspired naming conventions - Comprehensive documentation with W3C spec references - Full test coverage - Example usage in documentation Updated SlipstreamForWebDevelopers.md to document all input types with their corresponding Slipstream components. This completes the implementation of all standard HTML input types in Slipstream, balancing W3C specifications with SwiftUI idioms.
1 parent cd8f9cb commit c8adda1

9 files changed

Lines changed: 429 additions & 1 deletion

File tree

Sources/Slipstream/Documentation.docc/Guides/SlipstreamForWebDevelopers.md

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,24 @@ provided below is an organized table of W3C HTML tags and their equivalent Slips
179179
:--------|:----------------
180180
[`<form>`](https://html.spec.whatwg.org/multipage/sections.html#the-form-element) | ``Form``
181181
[`<label>`](https://html.spec.whatwg.org/multipage/sections.html#the-label-element) | ``Label``
182-
[`<input>`](https://html.spec.whatwg.org/multipage/sections.html#the-input-element) | ``TextField``
182+
[`<input type="text">`](https://html.spec.whatwg.org/multipage/input.html#text-(type=text)-state-and-search-state-(type=search)) | ``TextField``
183+
[`<input type="search">`](https://html.spec.whatwg.org/multipage/input.html#text-(type=text)-state-and-search-state-(type=search)) | ``TextField``
184+
[`<input type="tel">`](https://html.spec.whatwg.org/multipage/input.html#telephone-state-(type=tel)) | ``TextField``
185+
[`<input type="url">`](https://html.spec.whatwg.org/multipage/input.html#url-state-(type=url)) | ``TextField``
186+
[`<input type="email">`](https://html.spec.whatwg.org/multipage/input.html#email-state-(type=email)) | ``TextField``
187+
[`<input type="password">`](https://html.spec.whatwg.org/multipage/input.html#password-state-(type=password)) | ``TextField``
188+
[`<input type="date">`](https://html.spec.whatwg.org/multipage/input.html#date-state-(type=date)) | ``TextField``
189+
[`<input type="month">`](https://html.spec.whatwg.org/multipage/input.html#month-state-(type=month)) | ``TextField``
190+
[`<input type="week">`](https://html.spec.whatwg.org/multipage/input.html#week-state-(type=week)) | ``TextField``
191+
[`<input type="time">`](https://html.spec.whatwg.org/multipage/input.html#time-state-(type=time)) | ``TextField``
192+
[`<input type="datetime-local">`](https://html.spec.whatwg.org/multipage/input.html#local-date-and-time-state-(type=datetime-local)) | ``TextField``
193+
[`<input type="number">`](https://html.spec.whatwg.org/multipage/input.html#number-state-(type=number)) | ``TextField``
194+
[`<input type="checkbox">`](https://html.spec.whatwg.org/multipage/input.html#checkbox-state-(type=checkbox)) | ``Checkbox``
195+
[`<input type="radio">`](https://html.spec.whatwg.org/multipage/input.html#radio-button-state-(type=radio)) | ``RadioButton``
196+
[`<input type="color">`](https://html.spec.whatwg.org/multipage/input.html#color-state-(type=color)) | ``ColorPicker``
197+
[`<input type="range">`](https://html.spec.whatwg.org/multipage/input.html#range-state-(type=range)) | ``Slider``
198+
[`<input type="file">`](https://html.spec.whatwg.org/multipage/input.html#file-upload-state-(type=file)) | ``FileInput``
199+
[`<input type="hidden">`](https://html.spec.whatwg.org/multipage/input.html#hidden-state-(type=hidden)) | ``HiddenField``
183200
[`<button>`](https://html.spec.whatwg.org/multipage/sections.html#the-button-element) | ``Button``
184201
[`<select>`](https://html.spec.whatwg.org/multipage/sections.html#the-select-element) | ``Picker``
185202
[`<datalist>`](https://html.spec.whatwg.org/multipage/sections.html#the-datalist-element) | ``DataList``
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import SwiftSoup
2+
3+
/// A control that allows the user to select a color.
4+
///
5+
/// This represents an HTML color input element that provides a color picker
6+
/// interface. The value is a color in hexadecimal format.
7+
///
8+
/// ```swift
9+
/// // Basic color picker
10+
/// ColorPicker()
11+
///
12+
/// // Color picker with default value
13+
/// ColorPicker(value: "#ff5733")
14+
///
15+
/// // Color picker with form association
16+
/// ColorPicker(name: "theme-color", value: "#3498db")
17+
///
18+
/// // Color picker with accessibility features
19+
/// ColorPicker(name: "accent", id: "accent-color", autoFocus: true)
20+
/// ```
21+
///
22+
/// - SeeAlso: W3C [input type="color"](https://html.spec.whatwg.org/multipage/input.html#color-state-(type=color)) specification.
23+
@available(iOS 17.0, macOS 14.0, *)
24+
public struct ColorPicker: View {
25+
/// Creates a color picker input.
26+
///
27+
/// - Parameters:
28+
/// - name: The name of the form control, as used in form submission.
29+
/// - value: The default color value in hexadecimal format (e.g., "#ff0000").
30+
/// If not specified, defaults to "#000000" (black) per HTML spec.
31+
/// - id: The unique identifier for the color picker, used for label association.
32+
/// - autoFocus: If true, indicates that the color picker should be focused as soon as
33+
/// the page is loaded, allowing the user to interact with it without having to
34+
/// manually focus it first.
35+
public init(name: String? = nil, value: String? = nil, id: String? = nil, autoFocus: Bool = false) {
36+
self.name = name
37+
self.value = value
38+
self.id = id
39+
self.autoFocus = autoFocus
40+
}
41+
42+
@_documentation(visibility: private)
43+
public func render(_ container: Element, environment: EnvironmentValues) throws {
44+
let element = try container.appendElement("input")
45+
try element.attr("type", "color")
46+
47+
if let name {
48+
try element.attr("name", name)
49+
}
50+
if let value {
51+
try element.attr("value", value)
52+
}
53+
if let id {
54+
try element.attr("id", id)
55+
}
56+
if autoFocus {
57+
try element.attr("autofocus", "")
58+
}
59+
}
60+
61+
private let name: String?
62+
private let value: String?
63+
private let id: String?
64+
private let autoFocus: Bool
65+
}
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import SwiftSoup
2+
3+
/// A control that allows the user to select one or more files.
4+
///
5+
/// This represents an HTML file input element that provides a file picker
6+
/// interface for uploading files to a server.
7+
///
8+
/// ```swift
9+
/// // Basic file input
10+
/// FileInput()
11+
///
12+
/// // File input for images only
13+
/// FileInput(accept: "image/*")
14+
///
15+
/// // Multiple file selection
16+
/// FileInput(name: "documents", multiple: true)
17+
///
18+
/// // File input with specific file types
19+
/// FileInput(name: "avatar", accept: ".jpg,.png,.gif", id: "avatar-upload")
20+
/// ```
21+
///
22+
/// - SeeAlso: W3C [input type="file"](https://html.spec.whatwg.org/multipage/input.html#file-upload-state-(type=file)) specification.
23+
@available(iOS 17.0, macOS 14.0, *)
24+
public struct FileInput: View {
25+
/// Creates a file input.
26+
///
27+
/// - Parameters:
28+
/// - name: The name of the form control, as used in form submission.
29+
/// - accept: A comma-separated list of file types the input should accept.
30+
/// Can be file extensions (e.g., ".jpg,.png") or MIME types (e.g., "image/*").
31+
/// - multiple: If true, allows the user to select multiple files.
32+
/// - id: The unique identifier for the file input, used for label association.
33+
/// - required: If true, indicates that the user must select a file before
34+
/// the owning form can be submitted.
35+
/// - autoFocus: If true, indicates that the file input should be focused as soon as
36+
/// the page is loaded, allowing the user to interact with it without having to
37+
/// manually focus it first.
38+
public init(name: String? = nil, accept: String? = nil, multiple: Bool = false, id: String? = nil, required: Bool = false, autoFocus: Bool = false) {
39+
self.name = name
40+
self.accept = accept
41+
self.multiple = multiple
42+
self.id = id
43+
self.required = required
44+
self.autoFocus = autoFocus
45+
}
46+
47+
@_documentation(visibility: private)
48+
public func render(_ container: Element, environment: EnvironmentValues) throws {
49+
let element = try container.appendElement("input")
50+
try element.attr("type", "file")
51+
52+
if let name {
53+
try element.attr("name", name)
54+
}
55+
if let accept {
56+
try element.attr("accept", accept)
57+
}
58+
if multiple {
59+
try element.attr("multiple", "")
60+
}
61+
if let id {
62+
try element.attr("id", id)
63+
}
64+
if required {
65+
try element.attr("required", "")
66+
}
67+
if autoFocus {
68+
try element.attr("autofocus", "")
69+
}
70+
}
71+
72+
private let name: String?
73+
private let accept: String?
74+
private let multiple: Bool
75+
private let id: String?
76+
private let required: Bool
77+
private let autoFocus: Bool
78+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import SwiftSoup
2+
3+
/// A control that is not displayed to the user but whose value is submitted with the form.
4+
///
5+
/// This represents an HTML hidden input element used to store data that should be
6+
/// sent with the form but not displayed or modified by the user. Hidden fields are
7+
/// commonly used for storing state, CSRF tokens, or other metadata.
8+
///
9+
/// ```swift
10+
/// // Basic hidden field
11+
/// HiddenField(name: "user_id", value: "12345")
12+
///
13+
/// // Hidden field for CSRF protection
14+
/// HiddenField(name: "csrf_token", value: "a1b2c3d4e5")
15+
///
16+
/// // Hidden field with ID for JavaScript access
17+
/// HiddenField(name: "session", value: "xyz", id: "session-id")
18+
/// ```
19+
///
20+
/// - SeeAlso: W3C [input type="hidden"](https://html.spec.whatwg.org/multipage/input.html#hidden-state-(type=hidden)) specification.
21+
@available(iOS 17.0, macOS 14.0, *)
22+
public struct HiddenField: View {
23+
/// Creates a hidden input field.
24+
///
25+
/// - Parameters:
26+
/// - name: The name of the form control, as used in form submission.
27+
/// - value: The value to be sent when the form is submitted.
28+
/// - id: The unique identifier for the hidden field, useful for JavaScript access.
29+
public init(name: String, value: String, id: String? = nil) {
30+
self.name = name
31+
self.value = value
32+
self.id = id
33+
}
34+
35+
@_documentation(visibility: private)
36+
public func render(_ container: Element, environment: EnvironmentValues) throws {
37+
let element = try container.appendElement("input")
38+
try element.attr("type", "hidden")
39+
try element.attr("name", name)
40+
try element.attr("value", value)
41+
42+
if let id {
43+
try element.attr("id", id)
44+
}
45+
}
46+
47+
private let name: String
48+
private let value: String
49+
private let id: String?
50+
}
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import SwiftSoup
2+
3+
/// A control that allows the user to select a value from a range.
4+
///
5+
/// This represents an HTML range input element that provides a slider
6+
/// interface for selecting a numeric value within a specified range.
7+
///
8+
/// ```swift
9+
/// // Basic slider with default range (0-100)
10+
/// Slider()
11+
///
12+
/// // Slider with custom range
13+
/// Slider(min: 0, max: 10)
14+
///
15+
/// // Slider with initial value and step
16+
/// Slider(value: 5, min: 0, max: 100, step: 5)
17+
///
18+
/// // Slider with form association
19+
/// Slider(name: "volume", value: 50, min: 0, max: 100)
20+
/// ```
21+
///
22+
/// - SeeAlso: W3C [input type="range"](https://html.spec.whatwg.org/multipage/input.html#range-state-(type=range)) specification.
23+
@available(iOS 17.0, macOS 14.0, *)
24+
public struct Slider: View {
25+
/// Creates a slider input.
26+
///
27+
/// - Parameters:
28+
/// - name: The name of the form control, as used in form submission.
29+
/// - value: The default numeric value of the slider. If not specified,
30+
/// the default is the midpoint between min and max.
31+
/// - min: The minimum value of the range. Defaults to 0.
32+
/// - max: The maximum value of the range. Defaults to 100.
33+
/// - step: The granularity of the value. If not specified, defaults to 1.
34+
/// - id: The unique identifier for the slider, used for label association.
35+
/// - autoFocus: If true, indicates that the slider should be focused as soon as
36+
/// the page is loaded, allowing the user to interact with it without having to
37+
/// manually focus it first.
38+
public init(name: String? = nil, value: Double? = nil, min: Double = 0, max: Double = 100, step: Double? = nil, id: String? = nil, autoFocus: Bool = false) {
39+
self.name = name
40+
self.value = value
41+
self.min = min
42+
self.max = max
43+
self.step = step
44+
self.id = id
45+
self.autoFocus = autoFocus
46+
}
47+
48+
@_documentation(visibility: private)
49+
public func render(_ container: Element, environment: EnvironmentValues) throws {
50+
let element = try container.appendElement("input")
51+
try element.attr("type", "range")
52+
53+
if let name {
54+
try element.attr("name", name)
55+
}
56+
if let value {
57+
try element.attr("value", String(value))
58+
}
59+
try element.attr("min", String(min))
60+
try element.attr("max", String(max))
61+
if let step {
62+
try element.attr("step", String(step))
63+
}
64+
if let id {
65+
try element.attr("id", id)
66+
}
67+
if autoFocus {
68+
try element.attr("autofocus", "")
69+
}
70+
}
71+
72+
private let name: String?
73+
private let value: Double?
74+
private let min: Double
75+
private let max: Double
76+
private let step: Double?
77+
private let id: String?
78+
private let autoFocus: Bool
79+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import Testing
2+
3+
import Slipstream
4+
5+
struct ColorPickerTests {
6+
@Test func basic() throws {
7+
try #expect(renderHTML(ColorPicker()) == #"<input type="color" />"#)
8+
}
9+
10+
@Test func withName() throws {
11+
try #expect(renderHTML(ColorPicker(name: "theme-color")) == #"<input type="color" name="theme-color" />"#)
12+
}
13+
14+
@Test func withValue() throws {
15+
try #expect(renderHTML(ColorPicker(value: "#ff5733")) == #"<input type="color" value="#ff5733" />"#)
16+
}
17+
18+
@Test func withId() throws {
19+
try #expect(renderHTML(ColorPicker(id: "accent-picker")) == #"<input type="color" id="accent-picker" />"#)
20+
}
21+
22+
@Test func autoFocus() throws {
23+
try #expect(renderHTML(ColorPicker(autoFocus: true)) == #"<input type="color" autofocus />"#)
24+
}
25+
26+
@Test func allAttributes() throws {
27+
try #expect(renderHTML(ColorPicker(name: "accent", value: "#3498db", id: "accent-color", autoFocus: true)) == #"<input type="color" name="accent" value="#3498db" id="accent-color" autofocus />"#)
28+
}
29+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import Testing
2+
3+
import Slipstream
4+
5+
struct FileInputTests {
6+
@Test func basic() throws {
7+
try #expect(renderHTML(FileInput()) == #"<input type="file" />"#)
8+
}
9+
10+
@Test func withName() throws {
11+
try #expect(renderHTML(FileInput(name: "avatar")) == #"<input type="file" name="avatar" />"#)
12+
}
13+
14+
@Test func withAccept() throws {
15+
try #expect(renderHTML(FileInput(accept: "image/*")) == #"<input type="file" accept="image/*" />"#)
16+
}
17+
18+
@Test func withAcceptFileTypes() throws {
19+
try #expect(renderHTML(FileInput(accept: ".jpg,.png,.gif")) == #"<input type="file" accept=".jpg,.png,.gif" />"#)
20+
}
21+
22+
@Test func multiple() throws {
23+
try #expect(renderHTML(FileInput(multiple: true)) == #"<input type="file" multiple />"#)
24+
}
25+
26+
@Test func withId() throws {
27+
try #expect(renderHTML(FileInput(id: "document-upload")) == #"<input type="file" id="document-upload" />"#)
28+
}
29+
30+
@Test func required() throws {
31+
try #expect(renderHTML(FileInput(required: true)) == #"<input type="file" required />"#)
32+
}
33+
34+
@Test func autoFocus() throws {
35+
try #expect(renderHTML(FileInput(autoFocus: true)) == #"<input type="file" autofocus />"#)
36+
}
37+
38+
@Test func allAttributes() throws {
39+
try #expect(renderHTML(FileInput(name: "documents", accept: ".pdf,.doc", multiple: true, id: "doc-upload", required: true, autoFocus: true)) == #"<input type="file" name="documents" accept=".pdf,.doc" multiple id="doc-upload" required autofocus />"#)
40+
}
41+
42+
@Test func imageUploadUseCase() throws {
43+
// Test a common use case for image uploads
44+
try #expect(renderHTML(FileInput(name: "profile-picture", accept: "image/*", id: "avatar-input")) == #"<input type="file" name="profile-picture" accept="image/*" id="avatar-input" />"#)
45+
}
46+
}

0 commit comments

Comments
 (0)