Class: Shadcn::Calendar::Component
- Inherits:
-
ApplicationViewComponent
- Object
- ViewComponent::Base
- ApplicationViewComponent
- Shadcn::Calendar::Component
- Defined in:
- app/components/shadcn/calendar/component.rb
Overview
Port of registry/new-york-v4/ui/calendar.tsx.
Upstream is 220 lines that hand every class to react-day-picker and let
it render — so the class map is fully specified there, and the markup is
the package's. That markup is a plain <table role="grid">, which is why
this port has no dependency: what the package mostly is — 3,479 lines of
locale data, 2,046 of Ethiopic, Hebrew, Hijri, Persian and Jalali
calendars, and 576 wrapping date-fns — a Rails app already has in Date
and I18n. See features/calendar.md.
The grid is computed in Calendar::Month and rendered here, so a calendar
is readable with no JavaScript at all — the one family in this gem where
that is true of the whole component rather than of its resting state.
Constant Summary collapse
- RTL_CHEVRONS =
Upstream writes these two with
String.raw, so the backslash is part of the class. Single-quoted here for the same reason: in a double-quoted Ruby string\_is just_, and the token would stop matching the element it names. 'rtl:**:[.rdp-button\_next>svg]:rotate-180 rtl:**:[.rdp-button\_previous>svg]:rotate-180'- PREVIOUS_CLASS =
The two
rdp-*classes this port does render, and the only ones. They are not styling — they are what the twoRTL_CHEVRONSselectors above point at, and upstream's own rule would match nothing without them. Every otherrdp-*name comes fromgetDefaultClassNames(), a function call rather than a string in the vendored source, and nothing here styles them. "rdp-button_previous"- NEXT_CLASS =
"rdp-button_next"- MODES =
%i[single multiple range].freeze
- CAPTION_LAYOUTS =
%i[label dropdown dropdown_months dropdown_years].freeze
- MONTHS_CLASS =
"relative flex flex-col gap-4 md:flex-row"- MONTH_CLASS =
"flex w-full flex-col gap-4"- NAV_CLASS =
"absolute inset-x-0 top-0 flex w-full items-center justify-between gap-1"- CAPTION_CLASS =
"flex h-(--cell-size) w-full items-center justify-center px-(--cell-size)"- CAPTION_LABEL_CLASS =
"font-medium select-none"- CAPTION_LABEL_LABEL_CLASS =
"text-sm"- CAPTION_LABEL_DROPDOWN_CLASS =
"flex h-8 items-center gap-1 rounded-md pr-1 pl-2 text-sm " \ "[&>svg]:size-3.5 [&>svg]:text-muted-foreground"
- DROPDOWNS_CLASS =
"flex h-(--cell-size) w-full items-center justify-center gap-1.5 text-sm font-medium"- DROPDOWN_ROOT_CLASS =
"relative rounded-md border border-input shadow-xs has-focus:border-ring " \ "has-focus:ring-[3px] has-focus:ring-ring/50"
- DROPDOWN_CLASS =
"absolute inset-0 bg-popover opacity-0"- STATUS_STYLE =
What a screen reader is left with once the caption is two dropdowns: the month has to be announced when it changes, and the dropdowns announce themselves as controls rather than as the answer. Upstream's own inline style, which is
sr-onlywritten out. "border:0;clip:rect(0 0 0 0);height:1px;margin:-1px;overflow:hidden;padding:0;" \ "position:absolute;width:1px;white-space:nowrap;word-wrap:normal"
- GRID_CLASS =
"w-full border-collapse"- WEEKDAYS_CLASS =
"flex"- WEEKDAY_CLASS =
"flex-1 rounded-md text-[0.8rem] font-normal text-muted-foreground select-none"- WEEK_CLASS =
"mt-2 flex w-full"- WEEK_NUMBER_HEADER_CLASS =
"w-(--cell-size) select-none"- WEEK_NUMBER_CLASS =
"text-[0.8rem] text-muted-foreground select-none"- WEEK_NUMBER_INNER_CLASS =
"flex size-(--cell-size) items-center justify-center text-center"- DAY_CLASS =
"group/day relative aspect-square h-full w-full p-0 text-center select-none " \ "[&:last-child[data-selected=true]_button]:rounded-r-md"
- DAY_FIRST_CLASS =
"[&:first-child[data-selected=true]_button]:rounded-l-md"- DAY_FIRST_WITH_WEEK_NUMBER_CLASS =
"[&:nth-child(2)[data-selected=true]_button]:rounded-l-md"- TODAY_CLASS =
"rounded-md bg-accent text-accent-foreground data-[selected=true]:rounded-none"- OUTSIDE_CLASS =
"text-muted-foreground aria-selected:text-muted-foreground"- DISABLED_CLASS =
"text-muted-foreground opacity-50"- HIDDEN_CLASS =
"invisible"- RANGE_START_CLASS =
"rounded-l-md bg-accent"- RANGE_MIDDLE_CLASS =
"rounded-none"- RANGE_END_CLASS =
"rounded-r-md bg-accent"- NAV_BUTTON_CLASS =
"size-(--cell-size) p-0 select-none aria-disabled:opacity-50"- DAY_BUTTON_CLASS =
"flex aspect-square size-auto w-full min-w-(--cell-size) flex-col gap-1 leading-none " \ "font-normal group-data-[focused=true]/day:relative group-data-[focused=true]/day:z-10 " \ "group-data-[focused=true]/day:border-ring group-data-[focused=true]/day:ring-[3px] " \ "group-data-[focused=true]/day:ring-ring/50 data-[range-end=true]:rounded-md " \ "data-[range-end=true]:rounded-r-md data-[range-end=true]:bg-primary " \ "data-[range-end=true]:text-primary-foreground data-[range-middle=true]:rounded-none " \ "data-[range-middle=true]:bg-accent data-[range-middle=true]:text-accent-foreground " \ "data-[range-start=true]:rounded-md data-[range-start=true]:rounded-l-md " \ "data-[range-start=true]:bg-primary data-[range-start=true]:text-primary-foreground " \ "data-[selected-single=true]:bg-primary data-[selected-single=true]:text-primary-foreground " \ "dark:hover:text-accent-foreground [&>span]:text-xs [&>span]:opacity-70"
- CHEVRON_CLASS =
"size-4"
Constants inherited from ApplicationViewComponent
ApplicationViewComponent::CONTENTS_STYLE, ApplicationViewComponent::VOID_TAGS
Instance Attribute Summary collapse
-
#button_variant ⇒ Object
readonly
Returns the value of attribute button_variant.
-
#caption_layout ⇒ Object
readonly
Returns the value of attribute caption_layout.
-
#disabled ⇒ Object
readonly
Returns the value of attribute disabled.
-
#end_month ⇒ Object
readonly
Returns the value of attribute end_month.
-
#fixed_weeks ⇒ Object
readonly
Returns the value of attribute fixed_weeks.
-
#mode ⇒ Object
readonly
Returns the value of attribute mode.
-
#month ⇒ Object
readonly
Returns the value of attribute month.
-
#months ⇒ Object
readonly
Returns the value of attribute months.
-
#name ⇒ Object
readonly
Returns the value of attribute name.
-
#number_of_months ⇒ Object
readonly
Returns the value of attribute number_of_months.
-
#required ⇒ Object
readonly
Returns the value of attribute required.
-
#selected ⇒ Object
readonly
Returns the value of attribute selected.
-
#show_outside_days ⇒ Object
readonly
Returns the value of attribute show_outside_days.
-
#show_week_number ⇒ Object
readonly
Returns the value of attribute show_week_number.
-
#start_month ⇒ Object
readonly
Returns the value of attribute start_month.
-
#week_starts_on ⇒ Object
readonly
Returns the value of attribute week_starts_on.
Attributes inherited from ApplicationViewComponent
Instance Method Summary collapse
- #call ⇒ Object
-
#element_attributes(**defaults) ⇒ Object
Everything the controller needs to draw a month the server never rendered, and nothing else.
-
#initialize(month: nil, mode: :single, selected: nil, disabled: nil, name: nil, required: false, caption_layout: :label, button_variant: :ghost, show_outside_days: true, show_week_number: false, fixed_weeks: false, number_of_months: 1, start_month: nil, end_month: nil, week_starts_on: nil, **attributes) ⇒ Component
constructor
month:is which month is shown,selected:what is chosen in it, and they are separate on purpose — upstream'smonthandselectedare two props for the same reason: a range can end in a month nobody is looking at.
Methods inherited from ApplicationViewComponent
#css_classes, default_tag, #merged_style, #render_element, #shadcn_t, slot_name, #style_variants, #tag_name
Constructor Details
#initialize(month: nil, mode: :single, selected: nil, disabled: nil, name: nil, required: false, caption_layout: :label, button_variant: :ghost, show_outside_days: true, show_week_number: false, fixed_weeks: false, number_of_months: 1, start_month: nil, end_month: nil, week_starts_on: nil, **attributes) ⇒ Component
month: is which month is shown, selected: what is chosen in it, and
they are separate on purpose — upstream's month and selected are two
props for the same reason: a range can end in a month nobody is looking
at.
disabled: takes anything that answers === — a Date, a Range, a
lambda — or an array of those. That is Ruby's own matcher protocol, and
it covers what upstream's Matcher union covers without inventing a
shape for it.
112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 |
# File 'app/components/shadcn/calendar/component.rb', line 112 def initialize(month: nil, mode: :single, selected: nil, disabled: nil, name: nil, required: false, caption_layout: :label, button_variant: :ghost, show_outside_days: true, show_week_number: false, fixed_weeks: false, number_of_months: 1, start_month: nil, end_month: nil, week_starts_on: nil, **attributes) @mode = MODES.include?(mode&.to_sym) ? mode.to_sym : :single @selected = Array(selected.is_a?(Range) ? [ selected.begin, selected.end ] : selected).compact.map(&:to_date) # A range arrives either as a `Range` or as the two ends of one, because # that is what comes back from the controller and from a form. Both mean # the same thing here, and the modifier classes read this rather than # the argument's shape. @range = if selected.is_a?(Range) then selected.begin.to_date..selected.end.to_date elsif @mode == :range && @selected.size == 2 then @selected.first..@selected.last end @disabled = disabled @name = name @required = required @caption_layout = CAPTION_LAYOUTS.include?(caption_layout&.to_sym) ? caption_layout.to_sym : :label @button_variant = &.to_sym || :ghost @show_outside_days = show_outside_days @show_week_number = show_week_number @fixed_weeks = fixed_weeks @number_of_months = [ number_of_months.to_i, 1 ].max @start_month = start_month&.to_date @end_month = end_month&.to_date @week_starts_on = week_starts_on @month = Month.new(month || @selected.first || Date.current, week_starts_on:, fixed_weeks:) # Upstream's `numberOfMonths`: one nav for the lot, and a caption and a # grid each. The months run forward from the one that was asked for. @months = Array.new(@number_of_months) { |offset| offset.zero? ? @month : @month.plus_months(offset) } super(**attributes) end |
Instance Attribute Details
#button_variant ⇒ Object (readonly)
Returns the value of attribute button_variant.
98 99 100 |
# File 'app/components/shadcn/calendar/component.rb', line 98 def @button_variant end |
#caption_layout ⇒ Object (readonly)
Returns the value of attribute caption_layout.
98 99 100 |
# File 'app/components/shadcn/calendar/component.rb', line 98 def caption_layout @caption_layout end |
#disabled ⇒ Object (readonly)
Returns the value of attribute disabled.
98 99 100 |
# File 'app/components/shadcn/calendar/component.rb', line 98 def disabled @disabled end |
#end_month ⇒ Object (readonly)
Returns the value of attribute end_month.
98 99 100 |
# File 'app/components/shadcn/calendar/component.rb', line 98 def end_month @end_month end |
#fixed_weeks ⇒ Object (readonly)
Returns the value of attribute fixed_weeks.
98 99 100 |
# File 'app/components/shadcn/calendar/component.rb', line 98 def fixed_weeks @fixed_weeks end |
#mode ⇒ Object (readonly)
Returns the value of attribute mode.
98 99 100 |
# File 'app/components/shadcn/calendar/component.rb', line 98 def mode @mode end |
#month ⇒ Object (readonly)
Returns the value of attribute month.
98 99 100 |
# File 'app/components/shadcn/calendar/component.rb', line 98 def month @month end |
#months ⇒ Object (readonly)
Returns the value of attribute months.
98 99 100 |
# File 'app/components/shadcn/calendar/component.rb', line 98 def months @months end |
#name ⇒ Object (readonly)
Returns the value of attribute name.
98 99 100 |
# File 'app/components/shadcn/calendar/component.rb', line 98 def name @name end |
#number_of_months ⇒ Object (readonly)
Returns the value of attribute number_of_months.
98 99 100 |
# File 'app/components/shadcn/calendar/component.rb', line 98 def number_of_months @number_of_months end |
#required ⇒ Object (readonly)
Returns the value of attribute required.
98 99 100 |
# File 'app/components/shadcn/calendar/component.rb', line 98 def required @required end |
#selected ⇒ Object (readonly)
Returns the value of attribute selected.
98 99 100 |
# File 'app/components/shadcn/calendar/component.rb', line 98 def selected @selected end |
#show_outside_days ⇒ Object (readonly)
Returns the value of attribute show_outside_days.
98 99 100 |
# File 'app/components/shadcn/calendar/component.rb', line 98 def show_outside_days @show_outside_days end |
#show_week_number ⇒ Object (readonly)
Returns the value of attribute show_week_number.
98 99 100 |
# File 'app/components/shadcn/calendar/component.rb', line 98 def show_week_number @show_week_number end |
#start_month ⇒ Object (readonly)
Returns the value of attribute start_month.
98 99 100 |
# File 'app/components/shadcn/calendar/component.rb', line 98 def start_month @start_month end |
#week_starts_on ⇒ Object (readonly)
Returns the value of attribute week_starts_on.
98 99 100 |
# File 'app/components/shadcn/calendar/component.rb', line 98 def week_starts_on @week_starts_on end |
Instance Method Details
#call ⇒ Object
189 190 191 192 193 194 195 196 197 198 |
# File 'app/components/shadcn/calendar/component.rb', line 189 def call # Block content lands after the months, which is where upstream's # `footer` prop renders — and inside the root, which is what lets a # preset button's `data-action` reach this controller at all. render_element(body: safe_join([ tag.div(safe_join([ nav, *months.map { |shown| month_body(shown) } ]), class: MONTHS_CLASS), inputs, content ].compact)) end |
#element_attributes(**defaults) ⇒ Object
Everything the controller needs to draw a month the server never
rendered, and nothing else. The names, the formats and the modifier
classes all cross from here rather than being written in JavaScript:
I18n stays the only thing that decides how a date reads, and Tailwind
only ever sees a class in Ruby source.
151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 |
# File 'app/components/shadcn/calendar/component.rb', line 151 def element_attributes(**defaults) super(**{ "data-controller" => "shadcn--calendar", "data-shadcn--calendar-month-value" => month.first.iso8601, "data-shadcn--calendar-today-value" => Date.current.iso8601, "data-shadcn--calendar-week-starts-on-value" => month.week_starts_on, "data-shadcn--calendar-fixed-weeks-value" => fixed_weeks, "data-shadcn--calendar-show-outside-days-value" => show_outside_days, "data-shadcn--calendar-show-week-number-value" => show_week_number, "data-shadcn--calendar-start-month-value" => start_month&.beginning_of_month&.iso8601, "data-shadcn--calendar-end-month-value" => end_month&.end_of_month&.iso8601, "data-shadcn--calendar-mode-value" => mode, "data-shadcn--calendar-required-value" => required, "data-shadcn--calendar-input-names-value" => input_names.to_json, "data-shadcn--calendar-selected-value" => selected_dates.to_json, "data-shadcn--calendar-disabled-value" => portable_matchers.to_json, "data-shadcn--calendar-day-names-value" => day_names.to_json, "data-shadcn--calendar-short-day-names-value" => short_day_names.to_json, "data-shadcn--calendar-month-names-value" => month_names.to_json, "data-shadcn--calendar-short-month-names-value" => short_month_names.to_json, "data-shadcn--calendar-date-format-value" => day_label_format, "data-shadcn--calendar-month-format-value" => MONTH_FORMAT, # Fetched with the placeholder as its own argument, so what crosses is # the template rather than a rendered sentence — `I18n` raises on a # missing interpolation and the controller needs the `%{date}` intact. "data-shadcn--calendar-today-label-value" => shadcn_t("calendar.today_label", date: "%{date}"), "data-shadcn--calendar-selected-label-value" => shadcn_t("calendar.selected_label", date: "%{date}"), "data-shadcn--calendar-week-number-label-value" => shadcn_t("calendar.week_number", number: "%{number}"), "data-shadcn--calendar-today-class-value" => TODAY_CLASS, "data-shadcn--calendar-outside-class-value" => OUTSIDE_CLASS, "data-shadcn--calendar-disabled-class-value" => DISABLED_CLASS, "data-shadcn--calendar-hidden-class-value" => HIDDEN_CLASS, "data-shadcn--calendar-range-start-class-value" => RANGE_START_CLASS, "data-shadcn--calendar-range-middle-class-value" => RANGE_MIDDLE_CLASS, "data-shadcn--calendar-range-end-class-value" => RANGE_END_CLASS }.compact.merge(defaults)) end |