Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 66 additions & 7 deletions docs/app/javascript/controllers/ruby_ui/calendar_controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export default class extends Controller {
"calendar",
"title",
"weekdaysTemplate",
"disabledDateTemplate",
"selectedDateTemplate",
"todayDateTemplate",
"currentMonthDateTemplate",
Expand All @@ -16,6 +17,10 @@ export default class extends Controller {
type: String,
default: null,
},
minDate: {
type: String,
default: null,
},
viewDate: {
type: String,
default: new Date().toISOString().slice(0, 10),
Expand Down Expand Up @@ -43,13 +48,21 @@ export default class extends Controller {

selectDay(e) {
e.preventDefault();
if (this.isDateDisabled(e.currentTarget.dataset.day)) return;

// Set the selected date value
this.selectedDateValue = e.currentTarget.dataset.day;
}

selectedDateValueChanged(value, prevValue) {
const selectedDate = this.selectedDate();
if (!selectedDate) {
this.updateCalendar();
return;
}

// update the viewDateValue to the first day of month of the selected date (This will trigger updateCalendar() function)
const newViewDate = new Date(this.selectedDateValue);
const newViewDate = new Date(selectedDate);
newViewDate.setDate(2); // set the day to the 2nd (to avoid issues with months with different number of days and timezones)
this.viewDateValue = newViewDate.toISOString().slice(0, 10);

Expand All @@ -58,7 +71,7 @@ export default class extends Controller {

// update the input value
this.rubyUiCalendarInputOutlets.forEach((outlet) => {
const formattedDate = this.formatDate(this.selectedDate());
const formattedDate = this.formatDate(selectedDate);
outlet.setValue(formattedDate);
});
}
Expand Down Expand Up @@ -101,10 +114,20 @@ export default class extends Controller {

renderDay(day) {
const today = new Date();
const selectedDate = this.selectedDate();
let dateHTML = "";
const data = { day: day, dayDate: day.getDate() };

if (day.toDateString() === this.selectedDate().toDateString()) {
if (this.isDateDisabled(day)) {
// disabledDate
dateHTML = Mustache.render(
this.disabledDateTemplateTarget.innerHTML,
data,
);
} else if (
selectedDate &&
day.toDateString() === selectedDate.toDateString()
) {
// selectedDate
// Render the selected date template target innerHTML with Mustache
dateHTML = Mustache.render(
Expand Down Expand Up @@ -137,13 +160,13 @@ export default class extends Controller {
}

selectedDate() {
return new Date(this.selectedDateValue);
return this.parseDate(this.selectedDateValue);
}

viewDate() {
return this.viewDateValue
? new Date(this.viewDateValue)
: this.selectedDate();
return (
this.parseDate(this.viewDateValue) || this.selectedDate() || new Date()
);
}

getFullWeeksStartAndEndInMonth() {
Expand Down Expand Up @@ -246,4 +269,40 @@ export default class extends Controller {
return "th";
}
}

minDate() {
return this.parseDate(this.minDateValue);
}

isDateDisabled(date) {
const minDate = this.minDate();
const candidate = this.parseDate(date);

if (!minDate || !candidate) return false;

return this.startOfDay(candidate) < this.startOfDay(minDate);
}

parseDate(value) {
if (!value) return null;
if (value instanceof Date) return new Date(value);

const isoDate = value.toString().match(/^(\d{4})-(\d{2})-(\d{2})/);
if (isoDate) {
return new Date(
Number(isoDate[1]),
Number(isoDate[2]) - 1,
Number(isoDate[3]),
);
}

const date = new Date(value);
return Number.isNaN(date.getTime()) ? null : date;
}

startOfDay(date) {
const normalizedDate = new Date(date);
normalizedDate.setHours(0, 0, 0, 0);
return normalizedDate;
}
}
9 changes: 9 additions & 0 deletions docs/app/views/docs/calendar.rb
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,15 @@ def view_template
RUBY
end

render Docs::VisualCodeExample.new(title: "Minimum date", description: "Disable dates before a given date", context: self) do
<<~RUBY
div(class: 'space-y-4') do
Input(type: 'string', placeholder: "Select a date", class: 'rounded-md border shadow', id: 'minimum-date', data_controller: 'ruby-ui--calendar-input')
Calendar(input_id: '#minimum-date', min_date: Date.current, class: 'rounded-md border shadow')
end
RUBY
end

render Components::ComponentSetup::Tabs.new(component_name: component)

render Docs::ComponentsTable.new(component_files(component))
Expand Down
4 changes: 3 additions & 1 deletion gem/lib/ruby_ui/calendar/calendar.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@

module RubyUI
class Calendar < Base
def initialize(selected_date: nil, input_id: nil, date_format: "yyyy-MM-dd", **attrs)
def initialize(selected_date: nil, min_date: nil, input_id: nil, date_format: "yyyy-MM-dd", **attrs)
@selected_date = selected_date
@min_date = min_date
@input_id = input_id
@date_format = date_format
super(**attrs)
Expand All @@ -30,6 +31,7 @@ def default_attrs
data: {
controller: "ruby-ui--calendar",
ruby_ui__calendar_selected_date_value: @selected_date&.to_s,
ruby_ui__calendar_min_date_value: @min_date&.to_s,
ruby_ui__calendar_format_value: @date_format,
ruby_ui__calendar_ruby_ui__calendar_input_outlet: @input_id
}
Expand Down
73 changes: 66 additions & 7 deletions gem/lib/ruby_ui/calendar/calendar_controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export default class extends Controller {
"calendar",
"title",
"weekdaysTemplate",
"disabledDateTemplate",
"selectedDateTemplate",
"todayDateTemplate",
"currentMonthDateTemplate",
Expand All @@ -16,6 +17,10 @@ export default class extends Controller {
type: String,
default: null,
},
minDate: {
type: String,
default: null,
},
viewDate: {
type: String,
default: new Date().toISOString().slice(0, 10),
Expand Down Expand Up @@ -43,13 +48,21 @@ export default class extends Controller {

selectDay(e) {
e.preventDefault();
if (this.isDateDisabled(e.currentTarget.dataset.day)) return;

// Set the selected date value
this.selectedDateValue = e.currentTarget.dataset.day;
}

selectedDateValueChanged(value, prevValue) {
const selectedDate = this.selectedDate();
if (!selectedDate) {
this.updateCalendar();
return;
}

// update the viewDateValue to the first day of month of the selected date (This will trigger updateCalendar() function)
const newViewDate = new Date(this.selectedDateValue);
const newViewDate = new Date(selectedDate);
newViewDate.setDate(2); // set the day to the 2nd (to avoid issues with months with different number of days and timezones)
this.viewDateValue = newViewDate.toISOString().slice(0, 10);

Expand All @@ -58,7 +71,7 @@ export default class extends Controller {

// update the input value
this.rubyUiCalendarInputOutlets.forEach((outlet) => {
const formattedDate = this.formatDate(this.selectedDate());
const formattedDate = this.formatDate(selectedDate);
outlet.setValue(formattedDate);
});
}
Expand Down Expand Up @@ -101,10 +114,20 @@ export default class extends Controller {

renderDay(day) {
const today = new Date();
const selectedDate = this.selectedDate();
let dateHTML = "";
const data = { day: day, dayDate: day.getDate() };

if (day.toDateString() === this.selectedDate().toDateString()) {
if (this.isDateDisabled(day)) {
// disabledDate
dateHTML = Mustache.render(
this.disabledDateTemplateTarget.innerHTML,
data,
);
} else if (
selectedDate &&
day.toDateString() === selectedDate.toDateString()
) {
// selectedDate
// Render the selected date template target innerHTML with Mustache
dateHTML = Mustache.render(
Expand Down Expand Up @@ -137,13 +160,13 @@ export default class extends Controller {
}

selectedDate() {
return new Date(this.selectedDateValue);
return this.parseDate(this.selectedDateValue);
}

viewDate() {
return this.viewDateValue
? new Date(this.viewDateValue)
: this.selectedDate();
return (
this.parseDate(this.viewDateValue) || this.selectedDate() || new Date()
);
}

getFullWeeksStartAndEndInMonth() {
Expand Down Expand Up @@ -246,4 +269,40 @@ export default class extends Controller {
return "th";
}
}

minDate() {
return this.parseDate(this.minDateValue);
}

isDateDisabled(date) {
const minDate = this.minDate();
const candidate = this.parseDate(date);

if (!minDate || !candidate) return false;

return this.startOfDay(candidate) < this.startOfDay(minDate);
}

parseDate(value) {
if (!value) return null;
if (value instanceof Date) return new Date(value);

const isoDate = value.toString().match(/^(\d{4})-(\d{2})-(\d{2})/);
if (isoDate) {
return new Date(
Number(isoDate[1]),
Number(isoDate[2]) - 1,
Number(isoDate[3]),
);
}

const date = new Date(value);
return Number.isNaN(date.getTime()) ? null : date;
}

startOfDay(date) {
const normalizedDate = new Date(date);
normalizedDate.setHours(0, 0, 0, 0);
return normalizedDate;
}
}
20 changes: 20 additions & 0 deletions gem/lib/ruby_ui/calendar/calendar_days.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ class CalendarDays < Base
BASE_CLASS = "inline-flex items-center justify-center rounded-md text-sm ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 h-8 w-8 p-0 font-normal aria-selected:opacity-100"

def view_template
render_disabled_date_template
render_selected_date_template
render_today_date_template
render_current_month_date_template
Expand All @@ -13,6 +14,25 @@ def view_template

private

def render_disabled_date_template
date_template("disabledDateTemplate") do
button(
data_day: "{{day}}",
name: "day",
class:
[
BASE_CLASS,
"cursor-not-allowed bg-background text-muted-foreground hover:bg-background hover:text-muted-foreground"
],
disabled: true,
role: "gridcell",
tabindex: "-1",
type: "button",
aria_disabled: "true"
) { "{{dayDate}}" }
end
end

def render_selected_date_template
date_template("selectedDateTemplate") do
button(
Expand Down
9 changes: 9 additions & 0 deletions gem/lib/ruby_ui/calendar/calendar_docs.rb
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,15 @@ def view_template
RUBY
end

render Docs::VisualCodeExample.new(title: "Minimum date", description: "Disable dates before a given date", context: self) do
<<~RUBY
div(class: 'space-y-4') do
Input(type: 'string', placeholder: "Select a date", class: 'rounded-md border shadow', id: 'minimum-date', data_controller: 'ruby-ui--calendar-input')
Calendar(input_id: '#minimum-date', min_date: Date.current, class: 'rounded-md border shadow')
end
RUBY
end

render Components::ComponentSetup::Tabs.new(component_name: component)

render Docs::ComponentsTable.new(component_files(component))
Expand Down
11 changes: 11 additions & 0 deletions gem/test/ruby_ui/calendar_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,15 @@ def test_render_with_all_items

assert_match(/Select a date/, output)
end

def test_render_with_min_date
output = phlex do
RubyUI.Calendar(min_date: "2026-05-07")
end

assert_match(/data-ruby-ui--calendar-min-date-value="2026-05-07"/, output)
assert_match(/data-ruby-ui--calendar-target="disabledDateTemplate"/, output)
assert_match(/disabled/, output)
assert_match(/aria-disabled="true"/, output)
end
end