From 4e2078e5a1d4eb25a73aa2f9940e0db6f2582258 Mon Sep 17 00:00:00 2001 From: Mattes Mohr Date: Thu, 20 Aug 2026 17:39:19 +0200 Subject: [PATCH 1/2] Introduce the command attribute --- .../Attributes/ContentAttributes.swift | 36 +++++++++++++++++++ .../Abstraction/Tokens/ValueTokens.swift | 29 +++++++++++++++ Tests/HTMLKitTests/AttributesTests.swift | 21 +++++++++-- 3 files changed, 84 insertions(+), 2 deletions(-) diff --git a/Sources/HTMLKit/Abstraction/Attributes/ContentAttributes.swift b/Sources/HTMLKit/Abstraction/Attributes/ContentAttributes.swift index 76c7d4b4..5f193626 100644 --- a/Sources/HTMLKit/Abstraction/Attributes/ContentAttributes.swift +++ b/Sources/HTMLKit/Abstraction/Attributes/ContentAttributes.swift @@ -4414,3 +4414,39 @@ extension ImageSizesAttribute where Self: EmptyNode { return self.mutate(key: "imagesizes", value: value) } } + +/// A type that provides the `command` modifier. +@_documentation(visibility: internal) +public protocol CommandAttribute: Attribute { + + /// Set an action to perform for the button element. + /// + /// - Parameters: + /// - action: The action to perform when triggered. + /// - target: The identifier of the target to invoke. + /// + /// - Returns: The element + func command(_ action: ActionCommand, for target: String) -> Self +} + +extension CommandAttribute where Self: ContentNode { + + internal func mutate(command value: AttributeData) -> Self { + return self.mutate(key: "command", value: value) + } + + internal func mutate(commandfor value: AttributeData) -> Self { + return self.mutate(key: "commandfor", value: value) + } +} + +extension CommandAttribute where Self: EmptyNode { + + internal func mutate(command value: AttributeData) -> Self { + return self.mutate(key: "command", value: value) + } + + internal func mutate(commandfor value: AttributeData) -> Self { + return self.mutate(key: "commandfor", value: value) + } +} diff --git a/Sources/HTMLKit/Abstraction/Tokens/ValueTokens.swift b/Sources/HTMLKit/Abstraction/Tokens/ValueTokens.swift index 9d845e80..e2e59f5e 100644 --- a/Sources/HTMLKit/Abstraction/Tokens/ValueTokens.swift +++ b/Sources/HTMLKit/Abstraction/Tokens/ValueTokens.swift @@ -1,5 +1,34 @@ import Collections +/// A command of a button element. +/// +/// ```swift +/// Button { +/// "Lorem ipsum" +/// } +/// .command(.togglePopover, for: "id") +/// ``` +public enum ActionCommand: String { + + /// Toggles the popover element. + case togglePopover = "toggle-popover" + + /// Shows the popover element. + case showPopover = "show-popover" + + /// Hides the popover element. + case hidePopover = "hide-popover" + + /// Closes the dialog element. + case closeDialog = "close" + + /// Requests to close the dialog element. + case requestClose = "request-close" + + /// Opens the dialog element. + case showModal = "show-modal" +} + public enum Credential { /// A credential mode for cross-origin requests. diff --git a/Tests/HTMLKitTests/AttributesTests.swift b/Tests/HTMLKitTests/AttributesTests.swift index 032b8caa..0eae355c 100644 --- a/Tests/HTMLKitTests/AttributesTests.swift +++ b/Tests/HTMLKitTests/AttributesTests.swift @@ -10,9 +10,9 @@ final class AttributesTests: XCTestCase { @ContentBuilder var body: Content } - typealias AllAttributes = AccessKeyAttribute & AcceptAttribute & ActionAttribute & AlternateAttribute & AsynchronouslyAttribute & AutocapitalizeAttribute & AutocompleteAttribute & AutofocusAttribute & AutoplayAttribute & CharsetAttribute & CheckedAttribute & CiteAttribute & ClassAttribute & ColumnsAttribute & ColumnSpanAttribute & ContentAttribute & EditAttribute & ControlsAttribute & DataAttribute & DateTimeAttribute & DefaultAttribute & DeferAttribute & DirectionAttribute & DisabledAttribute & DownloadAttribute & DragAttribute & EncodingAttribute & EnterKeyAttribute & ForAttribute & FormAttribute & FormActionAttribute & EquivalentAttribute & HeadersAttribute & HeightAttribute & HiddenAttribute & HighAttribute & ReferenceAttribute & ReferenceLanguageAttribute & IdentifierAttribute & IsMapAttribute & InputModeAttribute & IsAttribute & ItemAttribute & ItemPropertyAttribute & KindAttribute & LabelAttribute & LanguageAttribute & ListAttribute & LoopAttribute & LowAttribute & MaximumValueAttribute & MaximumLengthAttribute & MediaAttribute & MethodAttribute & MinimumValueAttribute & MinimumLengthAttribute & MultipleAttribute & MutedAttribute & NameAttribute & NonceAttribute & NoValidateAttribute & OpenAttribute & OptimumAttribute & PatternAttribute & PartAttribute & PingAttribute & PlaceholderAttribute & PosterAttribute & PreloadAttribute & ReadOnlyAttribute & ReferrerPolicyAttribute & RelationshipAttribute & RequiredAttribute & ReversedAttribute & RoleAttribute & RowsAttribute & RowSpanAttribute & SandboxAttribute & ScopeAttribute & ShapeAttribute & SizeAttribute & SizesAttribute & SlotAttribute & SpanAttribute & SpellCheckAttribute & SourceAttribute & StartAttribute & StepAttribute & StyleAttribute & TabulatorAttribute & TargetAttribute & TitleAttribute & TranslateAttribute & TypeAttribute & ValueAttribute & WidthAttribute & WrapAttribute & PropertyAttribute & SelectedAttribute & WindowEventAttribute & FocusEventAttribute & PointerEventAttribute & MouseEventAttribute & WheelEventAttribute & InputEventAttribute & KeyboardEventAttribute & DragEventAttribute & ClipboardEventAttribute & SelectionEventAttribute & MediaEventAttribute & FormEventAttribute & DetailEventAttribute & AtomicAccessibilityAttribute & BusyAccessibilityAttribute & ControlsAccessibilityAttribute & CurrentAccessibilityAttribute & DescriptionsAccessibilityAttribute & DetailAccessibilityAttribute & DisabledAccessibilityAttribute & FlowAccessibilityAttribute & PopupAccessibilityAttribute & HiddenAccessibilityAttribute & InvalidAccessibilityAttribute & ShortcutsAccessibilityAttribute & LabelAccessibilityAttribute & LabelsAccessibilityAttribute & LiveAccessibilityAttribute & OwnsAccessibilityAttribute & MultilineAccessibilityAttribute & RowIndexAccessibilityAttribute & RelevantAccessibilityAttribute & RoleDescriptionAccessibilityAttribute & SortAccessibilityAttribute & OrientationAccessibilityAttribute & RequiredAccessibilityAttribute & ReadOnlyAccessibilityAttribute & ModalAccessibilityAttribute & LevelAccessibilityAttribute & HintAccessibilityAttribute & PositionAccessibilityAttribute & MultiselectAccessibilityAttribute & RowCountAccessibilityAttribute & ColumnIndexAccessibilityAttribute & ColumnCountAccessibilityAttribute & RowSpanAccessibilityAttribute & ColumnSpanAccessibilityAttribute & MaximumValueAccessibilityAttribute & MinimumValueAccessibilityAttribute & ValueAccessibilityAttribute & PressedAccessibilityAttribute & SelectedAccessibilityAttribute & CheckedAccessibilityAttribute & ExpandedAccessibilityAttribute & FocusedAccessibilityAttribute & CompletionAccessibilityAttribute & DrawAttribute & FillAttribute & StrokeAttribute & RadiusAttribute & PositionPointAttribute & RadiusPointAttribute & CenterPointAttribute & ViewBoxAttribute & NamespaceAttribute & PointsAttribute & ShadowRootModeAttribute & InertAttribute & FetchPriorityAttribute & LoadingAttribute & SourcesAttribute & DecodingAttribute & BlockingAttribute & PopoverAttribute & PopoverTargetAttribute & UseMapAttribute & PlaysInlineAttribute & IntegrityAttribute & AsAttribute & CrossOriginAttribute & SourceLanguageAttribute & SourceDocumentAttribute & AbbreviatedAttribute & ImageSourcesAttribute & ImageSizesAttribute + typealias AllAttributes = AccessKeyAttribute & AcceptAttribute & ActionAttribute & AlternateAttribute & AsynchronouslyAttribute & AutocapitalizeAttribute & AutocompleteAttribute & AutofocusAttribute & AutoplayAttribute & CharsetAttribute & CheckedAttribute & CiteAttribute & ClassAttribute & ColumnsAttribute & ColumnSpanAttribute & ContentAttribute & EditAttribute & ControlsAttribute & DataAttribute & DateTimeAttribute & DefaultAttribute & DeferAttribute & DirectionAttribute & DisabledAttribute & DownloadAttribute & DragAttribute & EncodingAttribute & EnterKeyAttribute & ForAttribute & FormAttribute & FormActionAttribute & EquivalentAttribute & HeadersAttribute & HeightAttribute & HiddenAttribute & HighAttribute & ReferenceAttribute & ReferenceLanguageAttribute & IdentifierAttribute & IsMapAttribute & InputModeAttribute & IsAttribute & ItemAttribute & ItemPropertyAttribute & KindAttribute & LabelAttribute & LanguageAttribute & ListAttribute & LoopAttribute & LowAttribute & MaximumValueAttribute & MaximumLengthAttribute & MediaAttribute & MethodAttribute & MinimumValueAttribute & MinimumLengthAttribute & MultipleAttribute & MutedAttribute & NameAttribute & NonceAttribute & NoValidateAttribute & OpenAttribute & OptimumAttribute & PatternAttribute & PartAttribute & PingAttribute & PlaceholderAttribute & PosterAttribute & PreloadAttribute & ReadOnlyAttribute & ReferrerPolicyAttribute & RelationshipAttribute & RequiredAttribute & ReversedAttribute & RoleAttribute & RowsAttribute & RowSpanAttribute & SandboxAttribute & ScopeAttribute & ShapeAttribute & SizeAttribute & SizesAttribute & SlotAttribute & SpanAttribute & SpellCheckAttribute & SourceAttribute & StartAttribute & StepAttribute & StyleAttribute & TabulatorAttribute & TargetAttribute & TitleAttribute & TranslateAttribute & TypeAttribute & ValueAttribute & WidthAttribute & WrapAttribute & PropertyAttribute & SelectedAttribute & WindowEventAttribute & FocusEventAttribute & PointerEventAttribute & MouseEventAttribute & WheelEventAttribute & InputEventAttribute & KeyboardEventAttribute & DragEventAttribute & ClipboardEventAttribute & SelectionEventAttribute & MediaEventAttribute & FormEventAttribute & DetailEventAttribute & AtomicAccessibilityAttribute & BusyAccessibilityAttribute & ControlsAccessibilityAttribute & CurrentAccessibilityAttribute & DescriptionsAccessibilityAttribute & DetailAccessibilityAttribute & DisabledAccessibilityAttribute & FlowAccessibilityAttribute & PopupAccessibilityAttribute & HiddenAccessibilityAttribute & InvalidAccessibilityAttribute & ShortcutsAccessibilityAttribute & LabelAccessibilityAttribute & LabelsAccessibilityAttribute & LiveAccessibilityAttribute & OwnsAccessibilityAttribute & MultilineAccessibilityAttribute & RowIndexAccessibilityAttribute & RelevantAccessibilityAttribute & RoleDescriptionAccessibilityAttribute & SortAccessibilityAttribute & OrientationAccessibilityAttribute & RequiredAccessibilityAttribute & ReadOnlyAccessibilityAttribute & ModalAccessibilityAttribute & LevelAccessibilityAttribute & HintAccessibilityAttribute & PositionAccessibilityAttribute & MultiselectAccessibilityAttribute & RowCountAccessibilityAttribute & ColumnIndexAccessibilityAttribute & ColumnCountAccessibilityAttribute & RowSpanAccessibilityAttribute & ColumnSpanAccessibilityAttribute & MaximumValueAccessibilityAttribute & MinimumValueAccessibilityAttribute & ValueAccessibilityAttribute & PressedAccessibilityAttribute & SelectedAccessibilityAttribute & CheckedAccessibilityAttribute & ExpandedAccessibilityAttribute & FocusedAccessibilityAttribute & CompletionAccessibilityAttribute & DrawAttribute & FillAttribute & StrokeAttribute & RadiusAttribute & PositionPointAttribute & RadiusPointAttribute & CenterPointAttribute & ViewBoxAttribute & NamespaceAttribute & PointsAttribute & ShadowRootModeAttribute & InertAttribute & FetchPriorityAttribute & LoadingAttribute & SourcesAttribute & DecodingAttribute & BlockingAttribute & PopoverAttribute & PopoverTargetAttribute & UseMapAttribute & PlaysInlineAttribute & IntegrityAttribute & AsAttribute & CrossOriginAttribute & SourceLanguageAttribute & SourceDocumentAttribute & AbbreviatedAttribute & ImageSourcesAttribute & ImageSizesAttribute & CommandAttribute - struct Tag: ContentNode, GlobalElement, AllAttributes { + struct Tag: ContentNode, GlobalElement, AllAttributes { var name: String { "tag" } @@ -1226,6 +1226,10 @@ final class AttributesTests: XCTestCase { func imageSizes(_ candidates: HTMLKit.SizeCandidate...) -> Tag { return mutate(imagesizes: .init(EnumeratedList(values: candidates, separator: ", "), context: .tainted(.html))) } + + func command(_ action: ActionCommand, for target: String) -> Tag { + return mutate(command:.init(action.rawValue, context: .trusted)).mutate(commandfor: .init(target, context: .tainted(.html))) + } } var renderer = Renderer() @@ -3939,4 +3943,17 @@ final class AttributesTests: XCTestCase { """ ) } + + func testCommandAttribute() throws { + + let view = TestView { + Tag {}.command(.hidePopover, for: "test") + } + + XCTAssertEqual(try renderer.render(view: view), + """ + + """ + ) + } } From 63f95db2cc7f9a5994ffe78e3685ae74975ded90 Mon Sep 17 00:00:00 2001 From: Mattes Mohr Date: Thu, 20 Aug 2026 17:39:45 +0200 Subject: [PATCH 2/2] Add conformance to the element --- Sources/HTMLKit/Abstraction/Elements/FormElements.swift | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Sources/HTMLKit/Abstraction/Elements/FormElements.swift b/Sources/HTMLKit/Abstraction/Elements/FormElements.swift index e9d0b18a..4461c582 100644 --- a/Sources/HTMLKit/Abstraction/Elements/FormElements.swift +++ b/Sources/HTMLKit/Abstraction/Elements/FormElements.swift @@ -2293,7 +2293,7 @@ public struct Button: ContentNode, FormElement { } } -extension Button: GlobalContentAttributes, GlobalEventAttributes, GlobalAccessibilityAttributes, DisabledAttribute, FormAttribute, FormActionAttribute, NameAttribute, TypeAttribute, ValueAttribute, PopoverTargetAttribute, PressedAccessibilityAttribute, SelectedAccessibilityAttribute, ExpandedAccessibilityAttribute { +extension Button: GlobalContentAttributes, GlobalEventAttributes, GlobalAccessibilityAttributes, DisabledAttribute, FormAttribute, FormActionAttribute, NameAttribute, TypeAttribute, ValueAttribute, PopoverTargetAttribute, CommandAttribute, PressedAccessibilityAttribute, SelectedAccessibilityAttribute, ExpandedAccessibilityAttribute { public func accessKey(_ value: Character) -> Button { return mutate(accesskey: .init("\(value)", context: .trusted)) @@ -2514,6 +2514,10 @@ extension Button: GlobalContentAttributes, GlobalEventAttributes, GlobalAccessib return copy } + public func command(_ action: ActionCommand, for target: String) -> Button { + return mutate(command:.init(action.rawValue, context: .trusted)).mutate(commandfor: .init(target, context: .tainted(.html))) + } + public func custom(key: String, value: String, context: EscapeContext = .tainted(.html)) -> Button { return mutate(key: key, value: .init(value, context: context)) }