From b99dacf0cd1620cd77e057ce7a151c1601d6a826 Mon Sep 17 00:00:00 2001 From: Katsuya HIDAKA Date: Fri, 9 Oct 2026 11:01:56 +0900 Subject: Auto-fit Gantt column widths Double-click a column's resize handle to fit its header and visible rows. Save the fitted width. --- app/assets/stylesheets/gantt.css | 9 ++++- app/helpers/gantt_helper.rb | 13 ++++++- .../controllers/gantt/column_controller.js | 35 ++++++++++++++++--- app/views/gantts/_chart.html.erb | 2 ++ test/system/gantts_test.rb | 24 +++++++++++++ 5 files changed, 76 insertions(+), 7 deletions(-) diff --git a/app/assets/stylesheets/gantt.css b/app/assets/stylesheets/gantt.css index d4447857a..62cf7f1a1 100644 --- a/app/assets/stylesheets/gantt.css +++ b/app/assets/stylesheets/gantt.css @@ -51,7 +51,7 @@ max-inline-size: max(100px, calc(100% - 100px)); } -/* On a narrow chart, dragging may make the subject column wider than the available space. */ +/* On a narrow chart, dragging or auto-fit may make the subject column wider than the available space. */ .gantt-chart.is-manually-resized [data-gantt-column="subjects"] { flex: 0 0 var(--gantt-subject-display-width); inline-size: var(--gantt-subject-display-width); @@ -60,6 +60,7 @@ .gantt-chart .ui-resizable-e { cursor: ew-resize; + z-index: 30; } .gantt-pane { @@ -218,6 +219,12 @@ white-space: nowrap; } +/* Applied temporarily when auto-fitting a column to measure its header and row content widths. */ +.gantt-column-measuring .gantt-pane > header, +.gantt-column-measuring .gantt-row { + inline-size: max-content; +} + .gantt-row-hover { background-color: rgba(var(--oc-yellow-1-rgb), 0.5); } diff --git a/app/helpers/gantt_helper.rb b/app/helpers/gantt_helper.rb index d9116fcc7..dfd3a2475 100644 --- a/app/helpers/gantt_helper.rb +++ b/app/helpers/gantt_helper.rb @@ -259,7 +259,7 @@ module GanttHelper 'subjects', min_width: 100, width: 'var(--gantt-subject-width)', data: { 'gantt--chart-target': 'subjectColumn', - # When the subject column is dragged on a narrow chart, + # When the subject column is dragged or auto-fitted on a narrow chart, # let the user's chosen width take precedence over automatic shrinking. action: %w( gantt--column:resize-start->gantt--chart#handleSubjectColumnResizeStart @@ -284,4 +284,15 @@ module GanttHelper tag.div(**options, &) end + + def gantt_column_resize_handle_tag + tag.div( + class: 'ui-resizable-handle ui-resizable-e', + data: { + 'gantt--column-target': 'handle', + action: 'dblclick->gantt--column#fitWidth:prevent' + }, + aria: {hidden: true} + ) + end end diff --git a/app/javascript/controllers/gantt/column_controller.js b/app/javascript/controllers/gantt/column_controller.js index 4cd53c3ee..0cd026651 100644 --- a/app/javascript/controllers/gantt/column_controller.js +++ b/app/javascript/controllers/gantt/column_controller.js @@ -44,6 +44,8 @@ class ColumnWidthStore { } export default class extends Controller { + static targets = ["handle"] + static values = { minWidth: Number, // Local value @@ -89,18 +91,35 @@ export default class extends Controller { return this.element.dataset.ganttColumn } + fitWidth() { + const extraPadding = 10 + let width = 0 + + // Temporarily apply this class to measure the content width needed to fit the column. + this.element.classList.add("gantt-column-measuring") + try { + this.element.querySelectorAll(".gantt-pane > header, .gantt-row:not([hidden])").forEach((content) => { + width = Math.max(width, content.getBoundingClientRect().width) + }) + } finally { + this.element.classList.remove("gantt-column-measuring") + } + + width = Math.max(this.minWidthValue, Math.ceil(width) + extraPadding) + this.dispatch("resize-start") + this.#setWidth(width) + this.#saveWidth(width) + } + #setupResizable() { const options = { - handles: "e", + handles: { e: this.handleTarget }, minWidth: this.minWidthValue, - zIndex: 30, start: () => { this.dispatch("resize-start") }, resize: (_event, ui) => { - this.element.style.setProperty("--gantt-column-width", `${ui.size.width}px`) - this.element.style.removeProperty("width") - this.dispatch("resize", { detail: { width: ui.size.width } }) + this.#setWidth(ui.size.width) }, stop: (_event, ui) => { if (ui.originalSize.width !== ui.size.width) { @@ -112,6 +131,12 @@ export default class extends Controller { this.#$element.resizable(options) } + #setWidth(width) { + this.element.style.setProperty("--gantt-column-width", `${width}px`) + this.element.style.removeProperty("width") + this.dispatch("resize", { detail: { width } }) + } + #restoreWidth() { const width = this.#columnWidthStore.get(this.#columnName) diff --git a/app/views/gantts/_chart.html.erb b/app/views/gantts/_chart.html.erb index 68fb42c35..a8f566cb0 100644 --- a/app/views/gantts/_chart.html.erb +++ b/app/views/gantts/_chart.html.erb @@ -23,6 +23,7 @@ <% end %> + <%= gantt_column_resize_handle_tag %> <% end %> <% @@ -39,6 +40,7 @@ <%= gantt.selected_column_content(column: column, top: layout.content_top, zoom: layout.zoom, g_width: layout.chart_width).html_safe %> + <%= gantt_column_resize_handle_tag %> <% end %> <% end %> diff --git a/test/system/gantts_test.rb b/test/system/gantts_test.rb index a190cd805..3fa836786 100644 --- a/test/system/gantts_test.rb +++ b/test/system/gantts_test.rb @@ -155,6 +155,30 @@ class GanttsTest < ApplicationSystemTestCase assert_selector(subject_column) { column_width('subjects') > narrow_width } end + test 'automatic column width fitting' do + Issue.find(3).update!( + subject: 'Cannot save a recipe after changing the ingredients and selecting a different category' + ) + visit_gantt + assert_selector '.gantt-column[data-gantt-column="subjects"].ui-resizable' + + expected_width = page.evaluate_script(<<~JS) + (() => { + const row = document.querySelector('[data-gantt-column="subjects"] .gantt-row[data-gantt-row-key="issue-3"]'); + const range = document.createRange(); + range.selectNodeContents(row); + return Math.ceil(range.getBoundingClientRect().right - row.getBoundingClientRect().left) + 10; + })() + JS + + find('.gantt-column[data-gantt-column="subjects"] .ui-resizable-e').double_click + assert_equal expected_width, column_width('subjects') + + page.refresh + assert_selector '.gantt-column[data-gantt-column="subjects"].ui-resizable' + assert_equal expected_width, column_width('subjects') + end + test 'context menu and tooltip interactions' do visit_gantt -- 2.51.0