tramway 4.0.1.3 → 4.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5b083339a12222cbc2f03468e474dd7b909fda5f60a8d9d8d3f64f6bc55881de
4
- data.tar.gz: c049ab9613c2aacb337b716d6ee24c097199f19e82a19d96fca7f4241bc01aac
3
+ metadata.gz: d1254075cd4e025f5a4f46d4951fc391bd8c90a4c1a486b3bbe471b0d79cfebb
4
+ data.tar.gz: d37cb6b0408b55f635f75b8cc8f0fb90d5d5efa454a1db5c4303fdf800e45726
5
5
  SHA512:
6
- metadata.gz: 83a5d1fff5a20503ba9e80ebbe9dae8c980973e0943f9d33df8b2ede46e8c6aad4442fb34a47ab7884e483a5c475a217d0525c09b116e30971e3f77d75642272
7
- data.tar.gz: 3c7d778b02ab2b42d96f9668a8c5c65a94204ab7e3e83873b685d3718b1267d81236fc5d5bcfc59a4fc4a2b2f0d1d53a9d23837b4198cd2be8851e7acde6db77
6
+ metadata.gz: 29d854a96abfcadda07911e326932a2347c509a0465dc5e6d4c19533dd7b5299f05aa011d3d928490dc6a17969143dc0490f81206618ecd13bd37040bc5306a3
7
+ data.tar.gz: d176134cef60ad20d9c2617067d0e4c498a966fef4119ef182dd2d13b32ae7919cce4ab2a3d18d076b0a20a7a6a08bda88b667190c072bfd41a2021ffa84c1af
data/README.md CHANGED
@@ -261,6 +261,40 @@ In this example, Tramway calls `Campaign.includes(:user, :comments)` before appl
261
261
  `campaign.user` or `campaign.comments` in the decorator's `index_attributes` does not issue an extra query per row.
262
262
  Omitting `includes` produces identical behavior to before this option existed.
263
263
 
264
+ **filters**
265
+
266
+ Filters are disabled by default on index pages. Enable them by listing the scope names you want selectable under
267
+ `filters` on the `:index` page entry:
268
+
269
+ *config/initializers/tramway.rb*
270
+ ```ruby
271
+ Tramway.configure do |config|
272
+ config.entities = [
273
+ {
274
+ name: :campaign,
275
+ namespace: :admin,
276
+ pages: [
277
+ {
278
+ action: :index,
279
+ filters: [:active, :archived]
280
+ }
281
+ ]
282
+ }
283
+ ]
284
+ end
285
+ ```
286
+
287
+ When filters are configured, Tramway adds a "Filters" toggle to the same row as the search box and pagination, above
288
+ the index listing. Clicking it reveals a list of small buttons, one per configured filter, each showing how many
289
+ records currently match it (e.g. "ACTIVE · 34"). Clicking a button navigates to the index page with that filter
290
+ applied, keeping any existing search query. The currently applied filter is highlighted, and the toggle remembers
291
+ whether the list was last left open or closed.
292
+
293
+ Visiting the index page with `?filter=active` calls `Campaign.active` (the scope must already exist on the model) in
294
+ addition to any configured `scope`. If `filter` does not match one of the configured filters, Tramway raises
295
+ `Tramway::Errors::InvalidFilterError` explaining which filters are available. Omitting `filters` (the default) makes
296
+ Tramway ignore `params[:filter]` entirely.
297
+
264
298
  **show page**
265
299
 
266
300
  To render a show page for an entity, declare a `:show` action inside the `pages` array in
@@ -436,7 +470,9 @@ end
436
470
 
437
471
  You can inject custom content above an entity's index table by defining an
438
472
  `index_header_content` lambda on its decorator. The lambda receives the
439
- collection of decorated records and can render any component you need.
473
+ collection of decorated records and can render any component you need. See
474
+ [docs/users/tramway_entities.md](docs/users/tramway_entities.md) for where exactly this content is placed on
475
+ the page.
440
476
 
441
477
  *config/initializers/tramway.rb*
442
478
  ```ruby
@@ -480,7 +516,7 @@ end
480
516
  ```
481
517
 
482
518
  With this configuration in place, the index page will render the `Create`
483
- button component above the table of projects.
519
+ button and the header content above the table of projects.
484
520
 
485
521
  #### Add header content to show pages
486
522
 
@@ -947,6 +983,22 @@ end
947
983
 
948
984
  will render [this](https://play.tailwindcss.com/UZPTCudFw5)
949
985
 
986
+ #### Icons on navbar items
987
+
988
+ `nav.item` accepts an optional `icon:` option with a Font Awesome class string, rendered before the item's label:
989
+
990
+ ```ruby
991
+ nav.left do
992
+ nav.item 'Users', '/users', icon: 'fa fa-users'
993
+ nav.item 'Podcasts', '/podcasts', icon: 'fa fa-podcast'
994
+ end
995
+ ```
996
+
997
+ Icons are optional — items without one just render as plain text links. When the vertical sidebar is collapsed on
998
+ desktop, items rendered with an `icon:` stay visible and clickable as icon-only links (their text label is hidden
999
+ but still available to screen readers via the item's `aria-label`); items without an icon have nothing to show once
1000
+ collapsed.
1001
+
950
1002
  #### tramway_navbar
951
1003
 
952
1004
  This helper provides several options. Here is YAML view of `tramway_navbar` options structure
@@ -457,13 +457,10 @@ class Navbar extends Controller {
457
457
 
458
458
  this.hiddenInteractionClasses.forEach((className) => {
459
459
  this.desktopHeader.classList.toggle(className, !expanded)
460
- this.desktopMenu.classList.toggle(className, !expanded)
461
460
  })
462
461
 
463
462
  this.desktopHeader.toggleAttribute('inert', !expanded)
464
- this.desktopMenu.toggleAttribute('inert', !expanded)
465
463
  this.desktopHeader.setAttribute('aria-hidden', expanded ? 'false' : 'true')
466
- this.desktopMenu.setAttribute('aria-hidden', expanded ? 'false' : 'true')
467
464
 
468
465
  this.desktopToggleWrapper.classList.toggle('justify-end', expanded)
469
466
  this.desktopToggleWrapper.classList.toggle('justify-center', !expanded)
@@ -485,4 +482,86 @@ class Navbar extends Controller {
485
482
  }
486
483
  }
487
484
 
488
- export { TramwaySelect, UiCheckbox, Tooltip, Navbar }
485
+ class Collapsible extends Controller {
486
+ static targets = ["panel", "icon", "toggle"]
487
+
488
+ connect() {
489
+ this.expanded = true
490
+
491
+ const stored = this.readStored()
492
+ const expanded = stored === null ? true : stored === 'true'
493
+
494
+ this.setExpanded(expanded, { animate: false })
495
+ }
496
+
497
+ toggle(event) {
498
+ event.preventDefault()
499
+
500
+ this.setExpanded(!this.expanded, { persist: true })
501
+ }
502
+
503
+ setExpanded(expanded, { persist = false, animate = true } = {}) {
504
+ this.expanded = expanded
505
+
506
+ if (animate) {
507
+ this.animateTo(expanded)
508
+ } else {
509
+ this.panelTarget.style.height = expanded ? 'auto' : '0px'
510
+ this.panelTarget.style.opacity = expanded ? '1' : '0'
511
+ }
512
+
513
+ const expandedCarrier = this.hasToggleTarget ? this.toggleTarget : this.element
514
+ expandedCarrier.setAttribute('aria-expanded', expanded ? 'true' : 'false')
515
+
516
+ if (this.hasIconTarget) {
517
+ this.iconTarget.classList.toggle('rotate-180', !expanded)
518
+ }
519
+
520
+ if (persist) {
521
+ this.writeStored(expanded)
522
+ }
523
+ }
524
+
525
+ // Animates between the panel's current rendered height and its target height, rather than
526
+ // to/from `auto` directly (which cannot be transitioned). `height` is one of the few
527
+ // properties animate.md tolerates outside transform/opacity, since an accordion has no
528
+ // transform equivalent.
529
+ animateTo(expanded) {
530
+ const panel = this.panelTarget
531
+ const targetHeight = expanded ? panel.scrollHeight : 0
532
+
533
+ panel.style.height = `${panel.getBoundingClientRect().height}px`
534
+ panel.getBoundingClientRect() // force reflow so the browser registers the start value
535
+
536
+ requestAnimationFrame(() => {
537
+ panel.style.height = `${targetHeight}px`
538
+ panel.style.opacity = expanded ? '1' : '0'
539
+ })
540
+
541
+ panel.addEventListener('transitionend', () => {
542
+ if (this.expanded) panel.style.height = 'auto'
543
+ }, { once: true })
544
+ }
545
+
546
+ readStored() {
547
+ try {
548
+ return window.localStorage.getItem(this.storageKey)
549
+ } catch {
550
+ return null
551
+ }
552
+ }
553
+
554
+ writeStored(expanded) {
555
+ try {
556
+ window.localStorage.setItem(this.storageKey, expanded ? 'true' : 'false')
557
+ } catch {
558
+ /* localStorage unavailable (e.g. private mode) — state simply won't persist */
559
+ }
560
+ }
561
+
562
+ get storageKey() {
563
+ return `tramway-collapsible-${this.element.dataset.key || 'default'}`
564
+ }
565
+ }
566
+
567
+ export { TramwaySelect, UiCheckbox, Tooltip, Navbar, Collapsible }
@@ -1,3 +1,6 @@
1
1
  %li.whitespace-nowrap
2
2
  = button_to @href, method: @method, class: "#{style} cursor-pointer", **@options do
3
- = content
3
+ - if icon?
4
+ %i{ class: icon_classes, aria: { hidden: true } }
5
+ %span.tramway-navbar-item-label
6
+ = content
@@ -11,7 +11,8 @@ module Tramway
11
11
  def initialize(**options)
12
12
  @href = options[:href]
13
13
  @method = options[:method]
14
- @options = options.except(:href, :method)
14
+ @icon = options[:icon]
15
+ @options = options.except(:href, :method, :icon)
15
16
  end
16
17
  end
17
18
  end
@@ -1,3 +1,6 @@
1
1
  %li.whitespace-nowrap
2
2
  = link_to @href, class: style, **@options do
3
- = content
3
+ - if icon?
4
+ %i{ class: icon_classes, aria: { hidden: true } }
5
+ %span.tramway-navbar-item-label
6
+ = content
@@ -10,7 +10,10 @@ module Tramway
10
10
  class LinkComponent < Tramway::Nav::ItemComponent
11
11
  def initialize(**options)
12
12
  @href = options[:href]
13
- @options = Rules::TurboHtmlAttributesRules.prepare_turbo_html_attributes(options:)
13
+ @icon = options[:icon]
14
+ @options = Rules::TurboHtmlAttributesRules.prepare_turbo_html_attributes(
15
+ options: options.except(:href, :icon)
16
+ )
14
17
  end
15
18
  end
16
19
  end
@@ -7,10 +7,18 @@ module Tramway
7
7
  class ItemComponent < TailwindComponent
8
8
  def style
9
9
  @style ||= [
10
- 'tramway-navbar-item inline-flex items-center rounded-md px-4 py-3 text-base font-medium text-zinc-100',
11
- 'transition-colors hover:bg-zinc-800 hover:text-zinc-50 sm:px-3 sm:py-2 sm:text-sm'
10
+ 'tramway-navbar-item inline-flex items-center gap-2 rounded-md px-4 py-3 text-base font-medium',
11
+ 'text-zinc-100 transition-colors hover:bg-zinc-800 hover:text-zinc-50 sm:px-3 sm:py-2 sm:text-sm'
12
12
  ].join(' ')
13
13
  end
14
+
15
+ def icon?
16
+ @icon.present?
17
+ end
18
+
19
+ def icon_classes
20
+ [@icon, 'tramway-navbar-item-icon w-5 shrink-0 text-center'].join(' ')
21
+ end
14
22
  end
15
23
  end
16
24
  end
@@ -114,21 +114,16 @@
114
114
  })();
115
115
 
116
116
  :css
117
- .tramway-navbar-desktop-vertical .tramway-navbar-item {
118
- width: 100%;
119
- }
120
-
121
- .tramway-navbar-desktop-vertical .tramway-navbar-item > a,
122
- .tramway-navbar-desktop-vertical .tramway-navbar-item > form {
117
+ .tramway-navbar-desktop-vertical li.whitespace-nowrap {
123
118
  width: 100%;
124
119
  }
125
120
 
126
- .tramway-navbar-desktop-vertical .tramway-navbar-item > form {
121
+ .tramway-navbar-desktop-vertical li.whitespace-nowrap > form {
127
122
  display: block;
123
+ width: 100%;
128
124
  }
129
125
 
130
- .tramway-navbar-desktop-vertical .tramway-navbar-item > a,
131
- .tramway-navbar-desktop-vertical .tramway-navbar-item > form > button {
126
+ .tramway-navbar-desktop-vertical .tramway-navbar-item {
132
127
  width: 100%;
133
128
  justify-content: flex-start;
134
129
  }
@@ -142,12 +137,26 @@
142
137
  padding-left: 6rem;
143
138
  }
144
139
 
145
- html[data-tramway-navbar-expanded="false"] #desktop-navbar-header,
146
- html[data-tramway-navbar-expanded="false"] #desktop-navbar-content {
140
+ html[data-tramway-navbar-expanded="false"] #desktop-navbar-header {
147
141
  opacity: 0;
148
142
  pointer-events: none;
149
143
  }
150
144
 
145
+ html[data-tramway-navbar-expanded="false"] #desktop-navbar-content {
146
+ opacity: 1;
147
+ pointer-events: auto;
148
+ }
149
+
150
+ html[data-tramway-navbar-expanded="false"] #desktop-navbar-content .tramway-navbar-item-label {
151
+ display: none;
152
+ }
153
+
154
+ html[data-tramway-navbar-expanded="false"] #desktop-navbar-content .tramway-navbar-item {
155
+ justify-content: center;
156
+ padding-left: 0;
157
+ padding-right: 0;
158
+ }
159
+
151
160
  html[data-tramway-navbar-expanded="false"] #desktop-navbar-toggle-wrapper {
152
161
  justify-content: center;
153
162
  }
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'tramway/warnings'
4
+ require 'tramway/errors'
4
5
 
5
6
  module Tramway
6
7
  # Main controller for entities pages
@@ -13,18 +14,15 @@ module Tramway
13
14
  include Rails.application.routes.url_helpers
14
15
 
15
16
  def index
16
- if index_scope.present?
17
- model_class.public_send(index_scope)
18
- else
19
- model_class.order(id: :desc)
20
- end => entities
21
-
17
+ entities = base_entities
18
+ entities = apply_filter(entities)
22
19
  entities = preload(entities)
23
20
  entities = search(entities)
24
21
  entities = entities.page(params[:page])
25
22
  @entities = entities
26
23
 
27
24
  @namespace = entity.namespace
25
+ @filter_counts = filter_counts if filters.present?
28
26
  end
29
27
 
30
28
  def show
@@ -91,6 +89,37 @@ module Tramway
91
89
  entity.page(:index).scope
92
90
  end
93
91
 
92
+ def base_entities
93
+ if index_scope.present?
94
+ model_class.public_send(index_scope)
95
+ else
96
+ model_class.order(id: :desc)
97
+ end
98
+ end
99
+
100
+ def filters
101
+ entity.page(:index).filters
102
+ end
103
+
104
+ def apply_filter(entities)
105
+ return entities if params[:filter].blank? || filters.blank?
106
+
107
+ filter = params[:filter].to_s
108
+
109
+ unless filters.map(&:to_s).include?(filter)
110
+ raise Tramway::Errors::InvalidFilterError.new(filter:, available_filters: filters, model_class:)
111
+ end
112
+
113
+ entities.public_send(filter)
114
+ end
115
+
116
+ def filter_counts
117
+ # Count by :id explicitly: a bare `.count` reuses any custom `select` values from the
118
+ # index page's `scope` (e.g. a raw-SQL aliased subquery column), which breaks the
119
+ # generated SQL. Counting a real column side-steps that entirely.
120
+ filters.index_with { |filter| base_entities.public_send(filter).count(:id) }
121
+ end
122
+
94
123
  def preload(entities)
95
124
  includes = entity.page(:index).includes
96
125
 
@@ -0,0 +1,15 @@
1
+ - current_filter = params[:filter]
2
+
3
+ .overflow-hidden.duration-200.ease-out-strong.motion-reduce:duration-0{ class: 'transition-[height,opacity]', data: { 'tramway--collapsible-target': 'panel' } }
4
+ .flex.flex-wrap.gap-2.mt-2
5
+ - @filter_counts.each do |filter_name, count|
6
+ - filter_params = request.query_parameters.except('page').merge(filter: filter_name)
7
+ - button_type = filter_name.to_s == current_filter ? :primary : :inverse
8
+ = tramway_button text: "#{filter_name.to_s.upcase} · #{count}",
9
+ path: public_send(custom_path_method, filter_params), type: button_type, size: :small
10
+
11
+ - if current_filter.present?
12
+ - clear_params = request.query_parameters.except('page', 'filter')
13
+ = tramway_button tag: :a, path: public_send(custom_path_method, clear_params), type: :inverse,
14
+ size: :small, tooltip: { text: t('tramway.actions.clear_filter'), event: :hover } do
15
+ %i.fa.fa-times{ 'aria-hidden' => 'true' }
@@ -16,16 +16,32 @@
16
16
  path: Tramway::Engine.routes.url_helpers.public_send(@entity.routes.new),
17
17
  type: :will
18
18
 
19
- = decorator.constantize.index_header_content.call(@entities) if decorator.constantize.index_header_content.present?
20
-
21
- .flex.flex-row.justify-end.mt-2.gap-4
22
- - if @entity.page(:index).search
23
- = form_with url: public_send(custom_path_method), method: :get, local: true, builder: Tramway::Form::Builder, horizontal: true do |f|
24
- = f.text_field :query, label: false, value: params[:query], placeholder: t('tramway.actions.search')
25
- = f.submit nil, 'aria-label' => t('tramway.actions.search') do
26
- %i.fa.fa-search{ 'aria-hidden' => 'true' }
27
-
28
- = paginate @entities, custom_path_method:
19
+ - if decorator.constantize.index_header_content.present?
20
+ .mt-4
21
+ = decorator.constantize.index_header_content.call(@entities)
22
+
23
+ - filters_key = "#{@entity.namespace}-#{@entity.name}-index-filters" if @filter_counts.present?
24
+
25
+ %div{ data: (@filter_counts.present? ? { controller: 'tramway--collapsible', key: filters_key } : {}) }
26
+ .flex.flex-row.items-center.mt-2.gap-4{ class: (@filter_counts.present? ? 'justify-between' : 'justify-end') }
27
+ - if @filter_counts.present?
28
+ = tramway_button tag: :button, type: :inverse,
29
+ data: { action: 'tramway--collapsible#toggle', 'tramway--collapsible-target': 'toggle' } do
30
+ %i.fa.fa-filter.mr-1{ 'aria-hidden' => 'true' }
31
+ = t('tramway.actions.filters')
32
+ %i.fa.fa-chevron-down.ml-1.transition-transform.duration-200.ease-out-strong.motion-reduce:duration-0{ 'aria-hidden' => 'true', data: { 'tramway--collapsible-target': 'icon' } }
33
+
34
+ .flex.flex-row.items-center.gap-4
35
+ - if @entity.page(:index).search
36
+ = form_with url: public_send(custom_path_method), method: :get, local: true, builder: Tramway::Form::Builder, horizontal: true do |f|
37
+ = f.text_field :query, label: false, value: params[:query], placeholder: t('tramway.actions.search')
38
+ = f.submit nil, 'aria-label' => t('tramway.actions.search') do
39
+ %i.fa.fa-search{ 'aria-hidden' => 'true' }
40
+
41
+ = paginate @entities, custom_path_method:
42
+
43
+ - if @filter_counts.present?
44
+ = render 'tramway/entities/filters', custom_path_method:
29
45
 
30
46
  - if index_attributes.empty?
31
47
  %p.text-center.mt-10
@@ -14,6 +14,8 @@ en:
14
14
  save: "Save"
15
15
  cancel: "Cancel"
16
16
  search: "Search"
17
+ filters: "Filters"
18
+ clear_filter: "Clear filter"
17
19
  notices:
18
20
  created: "The record is created"
19
21
  updated: "The record is updated"
@@ -6,6 +6,9 @@ const buildUtilities = (prefix, start, end) =>
6
6
  module.exports = {
7
7
  safelist: [
8
8
  // === Navbar ===
9
+ 'w-5',
10
+ 'shrink-0',
11
+ 'text-center',
9
12
  'ml-4',
10
13
  'text-white',
11
14
  'md:flex',
@@ -58,6 +61,7 @@ module.exports = {
58
61
  'ease-in-out',
59
62
  'md:transition-[width]',
60
63
  'transition-[padding-left]',
64
+ 'transition-[height,opacity]',
61
65
  'ease-in-out-strong',
62
66
  'md:ease-in-out-strong',
63
67
  'ease-out-strong',
@@ -416,6 +420,7 @@ module.exports = {
416
420
  'mb-4',
417
421
  'mb-2',
418
422
  'mt-8',
423
+ 'mt-4',
419
424
  'mt-2',
420
425
 
421
426
  // === Pagination styles ===
@@ -464,6 +469,8 @@ module.exports = {
464
469
  'whitespace-nowrap',
465
470
  'h-fit',
466
471
  'gap-1',
472
+ 'mr-1',
473
+ 'ml-1',
467
474
  'h-10',
468
475
  'px-2.5',
469
476
  'py-0.5',
@@ -115,7 +115,7 @@ module Tramway
115
115
 
116
116
  def stimulus_controller_imports
117
117
  [
118
- 'import { Navbar, TramwaySelect, UiCheckbox, Tooltip } from "@tramway/tramway"'
118
+ 'import { Navbar, TramwaySelect, UiCheckbox, Tooltip, Collapsible } from "@tramway/tramway"'
119
119
  ]
120
120
  end
121
121
 
@@ -124,7 +124,8 @@ module Tramway
124
124
  "application.register('tramway-navbar', Navbar)",
125
125
  "application.register('tramway-select', TramwaySelect)",
126
126
  "application.register('ui--checkbox', UiCheckbox)",
127
- "application.register('tramway-tooltip', Tooltip)"
127
+ "application.register('tramway-tooltip', Tooltip)",
128
+ "application.register('tramway--collapsible', Collapsible)"
128
129
  ]
129
130
  end
130
131
 
@@ -299,7 +300,8 @@ module Tramway
299
300
  "application.register('tramway-select'",
300
301
  "application.register('table-row-preview'",
301
302
  "application.register('ui--checkbox'",
302
- "application.register('tramway-tooltip'"
303
+ "application.register('tramway-tooltip'",
304
+ "application.register('tramway--collapsible'"
303
305
  ]
304
306
  end
305
307
 
@@ -10,6 +10,7 @@ module Tramway
10
10
  attribute? :scope, Types::Coercible::String
11
11
  attribute? :search, Types::Bool
12
12
  attribute? :includes, Types::Array.default([].freeze)
13
+ attribute? :filters, Types::Array.default([].freeze)
13
14
  end
14
15
  end
15
16
  end
@@ -41,5 +41,25 @@ module Tramway
41
41
  MESSAGE
42
42
  end
43
43
  end
44
+
45
+ # Raised when `params[:filter]` on an entities#index page does not match any of the
46
+ # filters declared for that page in the Tramway config, so the developer (or the user
47
+ # following a crafted link) gets a clear explanation instead of a raw NoMethodError from
48
+ # calling an arbitrary, possibly non-existent scope.
49
+ class InvalidFilterError < StandardError
50
+ def initialize(filter:, available_filters:, model_class:)
51
+ super(<<~MESSAGE)
52
+ Tramway received an unknown filter "#{filter}" for #{model_class} on the \
53
+ entities#index page.
54
+
55
+ Available filters: #{available_filters.join(', ')}.
56
+
57
+ To fix this:
58
+ - pass one of the available filters above in `params[:filter]`, or
59
+ - add `:#{filter}` to this entity's `filters:` list for the `:index` page in your \
60
+ Tramway config if it should be selectable.
61
+ MESSAGE
62
+ end
63
+ end
44
64
  end
45
65
  end
@@ -35,13 +35,13 @@ module Tramway
35
35
  reset_filling
36
36
  end
37
37
 
38
- def item(text_or_url, url = nil, **, &)
38
+ def item(text_or_url, url = nil, icon: nil, **, &)
39
39
  raise 'You cannot provide an argument and a code block at the same time' if provided_url_and_block?(url, &)
40
40
 
41
41
  if url.present?
42
- render_ignoring_block(text_or_url, url, **)
42
+ render_ignoring_block(text_or_url, url, icon:, **)
43
43
  else
44
- render_using_block(text_or_url, **, &)
44
+ render_using_block(text_or_url, icon:, **, &)
45
45
  end => rendered_item
46
46
 
47
47
  @items[@filling] << rendered_item
@@ -73,8 +73,9 @@ module Tramway
73
73
  @filling = nil
74
74
  end
75
75
 
76
- def render_ignoring_block(text_or_url, url, method: nil, **options)
77
- options.merge!(href: url)
76
+ def render_ignoring_block(text_or_url, url, method: nil, icon: nil, **options)
77
+ options.merge!(href: url, icon:)
78
+ apply_icon_accessibility_attributes(options, label: text_or_url, icon:)
78
79
 
79
80
  if method.present? && method.to_sym != :get
80
81
  context.render(Tramway::Nav::Item::ButtonComponent.new(method:, **options)) { text_or_url }
@@ -83,8 +84,8 @@ module Tramway
83
84
  end
84
85
  end
85
86
 
86
- def render_using_block(text_or_url, method: nil, **options, &)
87
- options.merge!(href: text_or_url)
87
+ def render_using_block(text_or_url, method: nil, icon: nil, **options, &)
88
+ options.merge!(href: text_or_url, icon:)
88
89
 
89
90
  if method.present? && method.to_sym != :get
90
91
  context.render(Tramway::Nav::Item::ButtonComponent.new(method:, **options), &)
@@ -92,5 +93,12 @@ module Tramway
92
93
  context.render(Tramway::Nav::Item::LinkComponent.new(method:, **options), &)
93
94
  end
94
95
  end
96
+
97
+ def apply_icon_accessibility_attributes(options, label:, icon:)
98
+ return if icon.blank?
99
+
100
+ options[:title] = label if options[:title].blank?
101
+ options[:'aria-label'] = label if options[:'aria-label'].blank?
102
+ end
95
103
  end
96
104
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Tramway
4
- VERSION = '4.0.1.3'
4
+ VERSION = '4.0.2'
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: tramway
3
3
  version: !ruby/object:Gem::Version
4
- version: 4.0.1.3
4
+ version: 4.0.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - kalashnikovisme
@@ -289,6 +289,7 @@ files:
289
289
  - app/views/kaminari/_prev_page.html.haml
290
290
  - app/views/tramway/chats/_message.html.haml
291
291
  - app/views/tramway/chats/_messages.html.haml
292
+ - app/views/tramway/entities/_filters.html.haml
292
293
  - app/views/tramway/entities/_form.html.haml
293
294
  - app/views/tramway/entities/_list.html.haml
294
295
  - app/views/tramway/entities/edit.html.haml