admin: end an editor's card in a dialog's action bar
Save and Cancel leave the form for a footer with a hairline on top;
Save reaches back by the form's id, and .form-actions is dropped.
Assisted-by: Claude Opus 5 via Claude Code
diff --git a/lib/pro_tacts/admin/contacts_edit.rb b/lib/pro_tacts/admin/contacts_edit.rb
index e68a2a3..3a6caa6 100644
--- a/lib/pro_tacts/admin/contacts_edit.rb
+++ b/lib/pro_tacts/admin/contacts_edit.rb
@@ -62,6 +62,10 @@ module ProTacts
ADDABLE_TYPES = %w[phone email address birthday].freeze #: Array[String]
private_constant :ADDABLE_TYPES
+ # The form's id, for the footer's Save outside it.
+ FORM = "contact-form" #: String
+ private_constant :FORM
+
# The address row's component fields, stacked in the value
# column in the order an address form reads — RFC 2426 section
# 3.2.1's own order minus the po box, which no screen shows and
@@ -128,7 +132,7 @@ module ProTacts
end
div(class: "card") do
div(class: "card-body") do
- form(action: "/contacts/#{@contact.id}", method: "post", class: "field-stack") do
+ form(action: "/contacts/#{@contact.id}", method: "post", class: "field-stack", id: FORM) do
input(type: "hidden", name: "etag", value: @contact.etag)
# The caption is an element rather than bare text
# because the row is a grid (admin.css): a text node
@@ -198,15 +202,17 @@ module ProTacts
textarea(name: "note", rows: 4,
placeholder: note ? "removed on save" : nil) { note.to_s }
end
- # Save is the form's submit; Cancel is navigation — a
- # link in Gloss's `.btn` contract, which is what an
- # anchor that acts like a button opts into.
- div(class: "form-actions") do
- a(href: "/contacts/#{@contact.id}", class: "btn") { "Cancel" }
- button(type: "submit", data: {variant: "primary"}) { "Save" }
- end
end
end
+ # The card's action bar, a dialog footer's shape (admin.css),
+ # outside the form and reaching it by id. Save is the form's
+ # submit; Cancel is navigation — a link in Gloss's `.btn`
+ # contract, which is what an anchor that acts like a button
+ # opts into.
+ footer do
+ a(href: "/contacts/#{@contact.id}", class: "btn") { "Cancel" }
+ button(type: "submit", form: FORM, data: {variant: "primary"}) { "Save" }
+ end
end
end
add_property_dialog
diff --git a/lib/pro_tacts/admin/groups_edit.rb b/lib/pro_tacts/admin/groups_edit.rb
index 2840679..95e12cc 100644
--- a/lib/pro_tacts/admin/groups_edit.rb
+++ b/lib/pro_tacts/admin/groups_edit.rb
@@ -26,6 +26,10 @@ module ProTacts
# @rbs @reading: Contact
# @rbs @notice: String?
+ # The form's id, for the footer's Save outside it.
+ FORM = "group-form" #: String
+ private_constant :FORM
+
#: (group: Store::Group, ?notice: String?) -> void
def initialize(group:, notice: nil)
@group = group
@@ -49,7 +53,7 @@ module ProTacts
end
div(class: "card") do
div(class: "card-body") do
- form(action: "/groups/#{@group.id}", method: "post", class: "field-stack") do
+ form(action: "/groups/#{@group.id}", method: "post", class: "field-stack", id: FORM) do
input(type: "hidden", name: "version", value: @group.version)
# The id as the blank's placeholder, because a group
# with no name is shown as its id everywhere else.
@@ -68,12 +72,13 @@ module ProTacts
textarea(name: "note", rows: 4,
placeholder: note ? "removed on save" : nil) { note.to_s }
end
- div(class: "form-actions") do
- a(href: "/groups/#{@group.id}", class: "btn") { "Cancel" }
- button(type: "submit", data: {variant: "primary"}) { "Save" }
- end
end
end
+ # ContactsEdit's action bar.
+ footer do
+ a(href: "/groups/#{@group.id}", class: "btn") { "Cancel" }
+ button(type: "submit", form: FORM, data: {variant: "primary"}) { "Save" }
+ end
end
end
end
diff --git a/public/admin.css b/public/admin.css
index 59653c5..f00c959 100644
--- a/public/admin.css
+++ b/public/admin.css
@@ -280,17 +280,6 @@
color: var(--gl-color-danger);
}
-/* The editor's submit row: the buttons sit beside each other under
- the fields, rather than in the stack's own gap, and start at the
- value column so the form keeps one edge down its whole height.
- Cancel comes first and the primary action last, the order every
- dialog's footer uses. */
-.form-actions {
- display: flex;
- gap: var(--gl-space-2xs);
- margin-inline-start: calc(var(--type-column) + var(--gl-space-s));
-}
-
/* Gloss's field contract styles input and select but not textarea
(base.css only resets it), and the editor's note field is one —
multiline text beside a .field caption. The box mirrors field.css's
@@ -636,6 +625,17 @@ dialog > :not(header):not(footer) {
padding: var(--gl-space-s);
}
+/* An editor's card ends in a dialog's action bar (ContactsEdit,
+ GroupsEdit), restated for a card, which has no footer of its own.
+ Its sides are the card body's, so Save lines up with the fields. */
+.card > footer {
+ display: flex;
+ justify-content: flex-end;
+ gap: var(--gl-space-2xs);
+ padding: var(--gl-space-2xs) var(--gl-space-m);
+ border-top: 1px solid var(--gl-color-border-hairline);
+}
+
/* A dialog's lists of one-line rows — the groups dialog's checkboxes
(Admin::GroupDialog), and the add dialog's radios (ContactsEdit)
through the first rule — are not the settings form Gloss spaces its
diff --git a/test/pro_tacts/admin/test_contacts_pages.rb b/test/pro_tacts/admin/test_contacts_pages.rb
index 233acae..c56eaa7 100644
--- a/test/pro_tacts/admin/test_contacts_pages.rb
+++ b/test/pro_tacts/admin/test_contacts_pages.rb
@@ -872,7 +872,8 @@ class AdminContactsPagesTest < Minitest::Test
assert_equal 200, last_response.status
body = last_response.body
- assert_includes body, '<form action="/contacts/red" method="post" class="field-stack">'
+ assert_includes body, '<form action="/contacts/red" method="post" class="field-stack" id="contact-form">'
+ assert_includes body, '<button type="submit" form="contact-form" data-variant="primary">Save</button>'
assert_includes body, '<input type="text" name="first" value="Ada" ' \
':required="lastBlank" @input="firstBlank = !$el.value.trim()" autofocus>'
assert_includes body, '<input type="text" name="last" value="Lovelace" ' \
diff --git a/test/pro_tacts/admin/test_groups_pages.rb b/test/pro_tacts/admin/test_groups_pages.rb
index b112edd..d8ed38b 100644
--- a/test/pro_tacts/admin/test_groups_pages.rb
+++ b/test/pro_tacts/admin/test_groups_pages.rb
@@ -146,6 +146,17 @@ class AdminGroupsPagesTest < Minitest::Test
## The editor
+ def test_the_editors_save_submits_its_form
+ with_contacts(BOOLES) do |store|
+ id = household(store)
+
+ get "/groups/#{id}/edit"
+
+ assert_includes last_response.body, %(id="group-form")
+ assert_includes last_response.body, '<button type="submit" form="group-form" data-variant="primary">Save</button>'
+ end
+ end
+
def test_the_editor_leaves_membership_to_the_card
with_contacts(BOOLES) do |store|
id = household(store)