contacts edit: short month names when the full ones do not fit
A select clips its text with no ellipsis, and the birthday row's month
was cut to "Septem" on a phone and "Septembe" in the desktop line at
the reading column's full width. The month select now measures its
widest full name against its own width on every resize and, when it
falls short, shows each month's abbreviation through the option's
label. Values and option text stay the full month.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019fHCYdysADrnpPkHaAHJpp
diff --git a/lib/pro_tacts/admin/contacts_edit.rb b/lib/pro_tacts/admin/contacts_edit.rb
index 3661d2c..8294e0e 100644
--- a/lib/pro_tacts/admin/contacts_edit.rb
+++ b/lib/pro_tacts/admin/contacts_edit.rb
@@ -441,13 +441,32 @@ module ProTacts
# same names for both callers is the point: a birthday needs no
# digest, there being at most one, and the save reads either
# caller's row identically. A nil birthday is the added row.
+ #
+ # The month reads in full where the select has the room for its
+ # widest name and short ("Sep") where it does not: a select
+ # clips its text with no ellipsis, and a phone's narrow shapes
+ # (admin.css) cut "September" to "Septem". Room is the select's
+ # own width, which its track or the line's shrinking sets
+ # whatever its text, so x-resize asks again whenever that
+ # changes. Need is the full names' width: the labels come off
+ # and the select is measured out of flow at max-content, where
+ # nothing squeezes it. All twelve switch together, so the list
+ # never mixes spellings. The short name rides in each option's
+ # label, which a select shows in place of the text; the value and
+ # the text stay the full month, so the save and the picker are
+ # untouched.
#: (Birthday? birthday, ?focus: bool) -> void
def birthday_controls(birthday, focus: false)
+ fit = "const months = $el.querySelectorAll('option[data-short]'), s = $el.style, room = $el.offsetWidth; " \
+ "months.forEach(o => o.removeAttribute('label')); " \
+ "s.position = 'absolute'; s.width = 'max-content'; " \
+ "const need = $el.offsetWidth; s.position = s.width = ''; " \
+ "if (room < need) months.forEach(o => o.label = o.dataset.short)"
select(name: "birthday[month]", aria_label: "month",
- "x-init": focus ? "$el.focus()" : nil) do
+ "x-init": focus ? "$el.focus()" : nil, "x-resize": fit) do
option(value: "", selected: birthday&.month.nil?) { "month" }
- Date::MONTHNAMES.compact.each_with_index do |name, index|
- option(value: index + 1, selected: birthday&.month == index + 1) { name }
+ Date::MONTHNAMES.compact.zip(Date::ABBR_MONTHNAMES.compact).each_with_index do |(name, short), index|
+ option(value: index + 1, selected: birthday&.month == index + 1, data: {short:}) { name }
end
end
input(type: "number", name: "birthday[day]", value: birthday&.day,
diff --git a/test/pro_tacts/admin/test_contacts_pages.rb b/test/pro_tacts/admin/test_contacts_pages.rb
index 953826d..d23719c 100644
--- a/test/pro_tacts/admin/test_contacts_pages.rb
+++ b/test/pro_tacts/admin/test_contacts_pages.rb
@@ -59,6 +59,18 @@ class AdminContactsPagesTest < Minitest::Test
%(<path d="M8 2v4"></path><path d="M16 2v4"></path>) +
%(<rect width="18" height="18" x="3" y="4" rx="2"></rect><path d="M3 10h18"></path></svg></button>) #: String
+ # The month select's fit to its room
+ # (Admin::ContactsEdit#birthday_controls): short names in each
+ # option's label while the select is narrower than its full names,
+ # asked again on every resize. Shared by the standing row and the
+ # added one for the picker's reason above.
+ BIRTHDAY_MONTH_FIT =
+ %(x-resize="const months = $el.querySelectorAll('option[data-short]'), s = $el.style, room = $el.offsetWidth; ) +
+ %(months.forEach(o => o.removeAttribute('label')); ) +
+ %(s.position = 'absolute'; s.width = 'max-content'; ) +
+ %(const need = $el.offsetWidth; s.position = s.width = ''; ) +
+ %(if (room < need) months.forEach(o => o.label = o.dataset.short)") #: String
+
def test_index_lists_recently_updated_contacts
with_contacts({"ada" => ADA}) do
get "/"
@@ -2557,9 +2569,10 @@ class AdminContactsPagesTest < Minitest::Test
body = last_response.body
assert_includes body,
- '<div class="field" data-blank-removes><span>birthday</span><div class="date-row">' \
- '<select name="birthday[month]" aria-label="month"><option value="">month</option>'
- assert_includes body, '<option value="12" selected>December</option></select>'
+ '<div class="field" data-blank-removes><span>birthday</span><div class="date-row">' +
+ %(<select name="birthday[month]" aria-label="month" #{BIRTHDAY_MONTH_FIT}><option value="">month</option>) +
+ '<option value="1" data-short="Jan">January</option>'
+ assert_includes body, '<option value="12" selected data-short="Dec">December</option></select>'
assert_includes body,
'<input type="number" name="birthday[day]" value="10" min="1" max="31" placeholder="day" aria-label="day">'
assert_includes body,
@@ -2814,10 +2827,11 @@ class AdminContactsPagesTest < Minitest::Test
# Asserted in two contiguous pieces: the month names sit
# between the empty option and December.
assert_includes body,
- '<template x-if="kind === \'birthday\'"><div class="date-row">' \
- '<select name="birthday[month]" aria-label="month" x-init="$el.focus()"><option value="" selected>month</option>'
+ '<template x-if="kind === \'birthday\'"><div class="date-row">' +
+ %(<select name="birthday[month]" aria-label="month" x-init="$el.focus()" #{BIRTHDAY_MONTH_FIT}>) +
+ '<option value="" selected>month</option>'
assert_includes body,
- '<option value="12">December</option></select>' \
+ '<option value="12" data-short="Dec">December</option></select>' \
'<input type="number" name="birthday[day]" min="1" max="31" placeholder="day" aria-label="day">' \
'<input type="number" name="birthday[year]" min="1" max="9999" placeholder="year" aria-label="year">' +
BIRTHDAY_PICKER + "</div></template></div></template>"