anchor_view_components 0.43.0 → 0.45.0

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.
Files changed (44) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +18 -0
  3. data/app/assets/images/icons/percentage.svg +6 -0
  4. data/app/components/anchor/anchor_view_components.ts +2 -0
  5. data/app/components/anchor/assistive_tech_notifications_component.html.erb +23 -0
  6. data/app/components/anchor/assistive_tech_notifications_component.rb +4 -0
  7. data/app/components/anchor/assistive_tech_notifications_component.ts +15 -0
  8. data/app/components/anchor/autocomplete/results_component.html.erb +2 -3
  9. data/app/components/anchor/autocomplete/results_component.rb +2 -2
  10. data/app/components/anchor/autocomplete_component.html.erb +5 -5
  11. data/app/components/anchor/banner_component.rb +4 -6
  12. data/app/components/anchor/button_component.rb +0 -7
  13. data/app/components/anchor/component.rb +1 -1
  14. data/app/components/anchor/copy_to_clipboard_component.en.yml +1 -0
  15. data/app/components/anchor/copy_to_clipboard_component.html.erb +2 -2
  16. data/app/components/anchor/copy_to_clipboard_controller.ts +10 -8
  17. data/app/components/anchor/dialog_component.html.erb +1 -0
  18. data/app/components/anchor/dialog_controller.ts +8 -0
  19. data/app/components/anchor/error_message_component.rb +5 -7
  20. data/app/components/anchor/input_component.rb +1 -5
  21. data/app/components/anchor/label_component.rb +3 -7
  22. data/app/components/anchor/link_component.rb +1 -1
  23. data/app/components/anchor/page/footer_component.html.erb +2 -1
  24. data/app/components/anchor/page/footer_component.rb +8 -0
  25. data/app/components/anchor/radio_button_collection_component.rb +1 -34
  26. data/app/components/anchor/radio_button_component.html.erb +1 -1
  27. data/app/components/anchor/radio_button_component.rb +3 -7
  28. data/app/components/anchor/select_component.rb +1 -5
  29. data/app/components/anchor/toast_component.html.erb +5 -1
  30. data/app/components/anchor/toast_component.rb +4 -0
  31. data/app/components/anchor/toast_controller.ts +5 -0
  32. data/app/helpers/anchor/form_builder.rb +11 -25
  33. data/app/helpers/anchor/model_validators.rb +10 -5
  34. data/app/helpers/anchor/view_helper.rb +1 -0
  35. data/lib/anchor/view_components/version.rb +1 -1
  36. data/lib/cops/anchor/avoid_implicit_super.rb +23 -0
  37. data/previews/anchor/assistive_tech_notifications_component_preview/default.html.erb +10 -0
  38. data/previews/anchor/assistive_tech_notifications_component_preview.rb +5 -0
  39. data/previews/anchor/dialog_component_preview/with_form.html.erb +21 -0
  40. data/previews/anchor/dialog_component_preview.rb +2 -0
  41. data/previews/anchor/page_component_preview/with_supporting_text_and_buttons.html.erb +24 -0
  42. data/previews/anchor/page_component_preview.rb +2 -0
  43. data/previews/forms/with_icons.html.erb +10 -19
  44. metadata +11 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ed9aaede95489205c325ca4ab6820458d84a5131d47aac172a0212f52fd4e228
4
- data.tar.gz: c1eee26d158f6e4276dbbce9813e89a43f06151980a76823a1f91ac6863956a0
3
+ metadata.gz: 02e14b9cbe3088a320100d337b3f23f92c97e03f659bd16d42ac21655838cc07
4
+ data.tar.gz: 4323338bfdedf4ed58101ee366bd21726bc1f3d910d7c824a3bc8eae5ff93d59
5
5
  SHA512:
6
- metadata.gz: 18d8e1894f3e249b37e803df49fb741d550229242f80ed91279eeb3d06efa242146737a25d55320b2f15569ac3dae4f187e99bada58feb4b879523fd6be2435c
7
- data.tar.gz: 736ecc7b962bbafcf45e7f45af8a04f279f552eec2f2712aa115b23246a13ae0bc8dc44af958ea231bc470aad5f69e0001ddd7f45676278cb439c30e8ecc8569
6
+ metadata.gz: 2c89ce43a18e3072e0a24d30ac43a4f489cfeea27c318dff78d4f02769c8c2418f1d179b2a71158eb44bc680cf1bd4b2f7d7cde0fe93948b2e2a55c523b7b645
7
+ data.tar.gz: 35f880936914bd255eec0d3cc77f95352aef81b0dbe60feaada58877100e3c2b45468cd61fe6c0b471bf9836f0055aaff74fa16803cfccf7bfd917fab36542b3
data/CHANGELOG.md CHANGED
@@ -1,5 +1,23 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.45.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 2cf21ed: Add more-horiz icon
8
+ - 2d2c3e6: Added a AssistiveTechNotifications component for notifying assistive tech users of changes that might otherwise only be presented visually. The output is two [ARIA live regions][aria-live-regions]—one `polite`, the other `assertive`—and is meant to be placed in an application’s layout so that it’s rendered on every page. Also provided, is the component’s public JavaScript function, `notifyAssistiveTech`, so that applications can easily send messages to these two live regions.
9
+
10
+ [aria-live-regions]: https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/ARIA_Live_Regions
11
+
12
+ ## 0.44.0
13
+
14
+ ### Minor Changes
15
+
16
+ - a9e927c: Removed the deprecated `tag` parameter from the Button component.
17
+ - 3f6ff7b: When a dialog is closed, any inner form error messages will be removed.
18
+ - e2941f0: Added a `supporting_text` slot to the `Page::Footer` component, which renders
19
+ text next to the buttons.
20
+
3
21
  ## 0.43.0
4
22
 
5
23
  ### Minor Changes
@@ -0,0 +1,6 @@
1
+ <!-- Source: Iconoir, added via `bin/add_icon` -->
2
+ <svg width="24" height="24" stroke-width="1.5" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
3
+ <path d="M17 19C15.8954 19 15 18.1046 15 17C15 15.8954 15.8954 15 17 15C18.1046 15 19 15.8954 19 17C19 18.1046 18.1046 19 17 19Z" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"/>
4
+ <path d="M7 9C5.89543 9 5 8.10457 5 7C5 5.89543 5.89543 5 7 5C8.10457 5 9 5.89543 9 7C9 8.10457 8.10457 9 7 9Z" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"/>
5
+ <path d="M19 5L5 19" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"/>
6
+ </svg>
@@ -10,6 +10,7 @@ import ToastController from "./toast_controller";
10
10
  import ToggleController from "./toggle_controller";
11
11
  import TypeaheadSelectController from "./typeahead_select_controller";
12
12
  import { Application } from "@hotwired/stimulus";
13
+ import { notifyAssistiveTech } from "./assistive_tech_notifications_component";
13
14
 
14
15
  export function registerAnchorControllers(application: Application) {
15
16
  application.register("autocomplete", AutocompleteController);
@@ -31,4 +32,5 @@ export {
31
32
  ToastController,
32
33
  ToggleController,
33
34
  TypeaheadSelectController,
35
+ notifyAssistiveTech,
34
36
  };
@@ -0,0 +1,23 @@
1
+ <%= tag.div(
2
+ **merge_options(
3
+ wrapper_options,
4
+ aria: {
5
+ atomic: true,
6
+ live: "polite",
7
+ },
8
+ class: "sr-only",
9
+ id: "js-assistive-tech-notifications",
10
+ ),
11
+ ) %>
12
+
13
+ <%= tag.div(
14
+ **merge_options(
15
+ wrapper_options,
16
+ aria: {
17
+ atomic: true,
18
+ live: "assertive",
19
+ },
20
+ class: "sr-only",
21
+ id: "js-assistive-tech-notifications-assertive",
22
+ ),
23
+ ) %>
@@ -0,0 +1,4 @@
1
+ module Anchor
2
+ class AssistiveTechNotificationsComponent < Component
3
+ end
4
+ end
@@ -0,0 +1,15 @@
1
+ export function notifyAssistiveTech(message: string, assertive = false) {
2
+ const liveRegionId = assertive
3
+ ? "js-assistive-tech-notifications-assertive"
4
+ : "js-assistive-tech-notifications";
5
+ const liveRegion = document.getElementById(liveRegionId);
6
+
7
+ if (liveRegion) {
8
+ liveRegion.textContent = message;
9
+ } else {
10
+ console.warn(
11
+ `Could not find #${liveRegionId}.`,
12
+ "Make sure to include `anchor_assistive_tech_notifications` in your Rails layout.",
13
+ );
14
+ }
15
+ }
@@ -1,9 +1,8 @@
1
1
  <% results.each do |result| %>
2
2
  <%= tag.li(
3
- **merge_options(
4
- { data: result.data },
3
+ **merge_options(wrapper_options,
5
4
  class: "list-group-item",
6
- data: { autocomplete_value: result.value },
5
+ data: result.data.merge(autocomplete_value: result.value),
7
6
  role: "option",
8
7
  )
9
8
  ) do %>
@@ -3,10 +3,10 @@ module Anchor
3
3
  class ResultsComponent < Component
4
4
  Result = Data.define(:text, :value, :data)
5
5
 
6
- def initialize(data:)
6
+ def initialize(data:, **kwargs)
7
7
  @data = data
8
8
 
9
- super
9
+ super(**kwargs)
10
10
  end
11
11
 
12
12
  private
@@ -1,25 +1,25 @@
1
- <%= tag.div(
1
+ <%= tag.div(**merge_options(wrapper_options,
2
2
  data: { controller: "autocomplete", autocomplete_url_value: src },
3
3
  role: "combobox",
4
4
  class: "group relative"
5
- ) do %>
5
+ )) do %>
6
6
  <% if form_builder.class.module_parent_name == "Anchor" %>
7
7
  <%= form_builder.text_field(
8
8
  name,
9
9
  **merge_options(input_options, default_input_options)
10
10
  ) %>
11
11
  <%= form_builder.text_field(
12
- "#{name}_id".to_sym,
12
+ :"#{name}_id",
13
13
  { class: "hidden", data: { autocomplete_target: "hidden" }}
14
14
  ) %>
15
15
  <% else %>
16
16
  <%= form_builder.input(
17
- "#{name}_search".to_sym,
17
+ :"#{name}_search",
18
18
  input_html: merge_options(input_options, default_input_options)
19
19
  ) %>
20
20
  <div class="hidden">
21
21
  <%= form_builder.input(
22
- "#{name}_id".to_sym,
22
+ :"#{name}_id",
23
23
  input_html: { data: { autocomplete_target: "hidden" } }
24
24
  ) %>
25
25
  </div>
@@ -34,13 +34,11 @@ module Anchor
34
34
  warning: "text-warning",
35
35
  }.freeze
36
36
 
37
- def initialize(**kwargs)
38
- @icon = ICON_MAPPINGS[kwargs[:variant]] ||
39
- ICON_MAPPINGS[VARIANT_DEFAULT]
40
- @icon_variant = ICON_VARIANT_MAPPINGS[kwargs[:variant]] ||
41
- ICON_VARIANT_MAPPINGS[VARIANT_DEFAULT]
37
+ def initialize(variant: VARIANT_DEFAULT, **kwargs)
38
+ @icon = ICON_MAPPINGS[variant]
39
+ @icon_variant = ICON_VARIANT_MAPPINGS[variant]
42
40
 
43
- super
41
+ super(variant:, **kwargs)
44
42
  end
45
43
 
46
44
  private
@@ -35,7 +35,6 @@ module Anchor
35
35
  end
36
36
 
37
37
  def initialize(
38
- tag: nil,
39
38
  type: TYPE_DEFAULT,
40
39
  size: SIZE_DEFAULT,
41
40
  href: nil,
@@ -43,12 +42,6 @@ module Anchor
43
42
  full_width: false,
44
43
  **kwargs
45
44
  )
46
- if tag
47
- ActiveSupport::Deprecation.warn(
48
- "`tag` is now set automatically and should no longer be used."
49
- )
50
- end
51
-
52
45
  @type = fetch_or_fallback(TYPE_OPTIONS, type, TYPE_DEFAULT)
53
46
  @size = SIZE_MAPPINGS[fetch_or_fallback(SIZE_OPTIONS, size,
54
47
  SIZE_DEFAULT)]
@@ -16,7 +16,7 @@ module Anchor
16
16
  .merge(class: classes)
17
17
  .reject { |_, v| deep_blank?(v) }
18
18
 
19
- super
19
+ super # rubocop:disable Anchor/AvoidImplicitSuper
20
20
  end
21
21
 
22
22
  private
@@ -1,2 +1,3 @@
1
1
  en:
2
+ assistive_tech_notification: Copied
2
3
  copy_to_clipboard: Copy to Clipboard
@@ -4,7 +4,7 @@
4
4
  action: "click->copy-to-clipboard#copy",
5
5
  controller: "copy-to-clipboard",
6
6
  copy_to_clipboard_text_to_copy_value: value,
7
- copy_to_clipboard_hidden_class: "hidden",
7
+ copy_to_clipboard_assistive_tech_notification_value: t(".assistive_tech_notification"),
8
8
  },
9
9
  class: "bg-transparent",
10
10
  type: "button",
@@ -15,7 +15,7 @@
15
15
  ) %>
16
16
  <%= anchor_icon(
17
17
  icon: "check",
18
- classes: "hidden",
18
+ hidden: true,
19
19
  data: { copy_to_clipboard_target: "successIcon"},
20
20
  ) %>
21
21
  <% end %>
@@ -1,26 +1,28 @@
1
1
  import { Controller } from "@hotwired/stimulus";
2
+ import { notifyAssistiveTech } from "./assistive_tech_notifications_component";
2
3
 
3
4
  export default class extends Controller<HTMLDivElement> {
4
5
  static targets = [ "initialIcon", "successIcon"];
5
- static classes = [ "hidden" ]
6
+ static values = {
7
+ assistiveTechNotification: String,
8
+ notificationDelay: { type: Number, default: 1500 },
9
+ textToCopy: String,
10
+ };
6
11
 
7
12
  declare readonly initialIconTarget: SVGElement;
8
13
  declare readonly successIconTarget: SVGElement;
9
14
  declare readonly notificationDelayValue: number;
10
15
  declare readonly hiddenClass: string;
11
16
  declare readonly textToCopyValue: string;
17
+ declare readonly assistiveTechNotificationValue: string;
12
18
  declare readonly hasTextToCopyValue: boolean;
13
19
 
14
- static values = {
15
- notificationDelay: { type: Number, default: 1500 },
16
- textToCopy: String,
17
- };
18
-
19
20
  copy(): void {
20
21
  if (this.hasTextToCopyValue) {
21
22
  navigator.clipboard.writeText(this.textToCopyValue);
22
23
  }
23
24
  this.toggleIcons();
25
+ notifyAssistiveTech(this.assistiveTechNotificationValue);
24
26
 
25
27
  setTimeout(() => {
26
28
  this.toggleIcons();
@@ -28,7 +30,7 @@ export default class extends Controller<HTMLDivElement> {
28
30
  }
29
31
 
30
32
  toggleIcons(): void {
31
- this.initialIconTarget.classList.toggle(this.hiddenClass);
32
- this.successIconTarget.classList.toggle(this.hiddenClass);
33
+ this.initialIconTarget.toggleAttribute("hidden");
34
+ this.successIconTarget.toggleAttribute("hidden");
33
35
  }
34
36
  }
@@ -10,6 +10,7 @@
10
10
  ],
11
11
  data: {
12
12
  controller: "dialog",
13
+ action: "close->dialog#reset",
13
14
  testid: title_id,
14
15
  },
15
16
  })) do %>
@@ -8,4 +8,12 @@ export default class extends Controller<HTMLDialogElement> {
8
8
  close(): void {
9
9
  this.element.close();
10
10
  }
11
+
12
+ reset(): void {
13
+ this.#resetErrorMessages();
14
+ }
15
+
16
+ #resetErrorMessages(): void {
17
+ this.element.querySelectorAll("[data-error='true']").forEach((element) => element.remove());
18
+ }
11
19
  }
@@ -13,8 +13,8 @@ module Anchor
13
13
  text-sm
14
14
  ).freeze
15
15
 
16
- def initialize(form_builder:, attribute:, **kwargs)
17
- @form_builder = form_builder
16
+ def initialize(object:, attribute:, **kwargs)
17
+ @object = object
18
18
  @attribute = attribute
19
19
 
20
20
  super(**kwargs)
@@ -22,16 +22,14 @@ module Anchor
22
22
 
23
23
  private
24
24
 
25
- attr_reader :attribute, :form_builder
26
-
27
- delegate :object, to: :form_builder
25
+ attr_reader :attribute
28
26
 
29
27
  def error_message
30
- object.errors.full_messages_for(attribute).to_sentence
28
+ @object.errors.full_messages_for(attribute).to_sentence
31
29
  end
32
30
 
33
31
  def render?
34
- object.errors.has_key?(attribute)
32
+ @object.errors.has_key?(attribute)
35
33
  end
36
34
  end
37
35
  end
@@ -20,23 +20,19 @@ module Anchor
20
20
  ).freeze
21
21
 
22
22
  def initialize(
23
- form_builder:,
24
23
  attribute:,
25
- type:,
26
24
  starting_icon: nil,
27
25
  ending_icon: nil,
28
26
  **kwargs
29
27
  )
30
- @form_builder = form_builder
31
28
  @attribute = attribute
32
- @type = type
33
29
  @starting_icon = starting_icon
34
30
  @ending_icon = ending_icon
35
31
 
36
32
  super(**kwargs)
37
33
  end
38
34
 
39
- attr_reader :attribute, :type, :starting_icon, :ending_icon
35
+ attr_reader :attribute, :starting_icon, :ending_icon
40
36
 
41
37
  def options
42
38
  {
@@ -8,14 +8,10 @@ module Anchor
8
8
 
9
9
  attr_accessor :options
10
10
 
11
- def initialize(form_builder:, attribute:, **options)
12
- @form_builder = form_builder
13
- @attribute = attribute
14
- @options = options.merge(
15
- class: Array(options.delete(:class)) + LABEL_CLASSES
16
- )
11
+ def initialize(object:, attribute:, required: nil)
12
+ @options = { class: LABEL_CLASSES }
17
13
  @required = ModelValidators
18
- .new(@form_builder.object, options)
14
+ .new(object, required:)
19
15
  .attribute_required?(attribute)
20
16
 
21
17
  super()
@@ -10,7 +10,7 @@ module Anchor
10
10
  def initialize(href:, **kwargs)
11
11
  @href = href
12
12
 
13
- super
13
+ super(**kwargs)
14
14
  end
15
15
 
16
16
  attr_reader :href
@@ -1,10 +1,11 @@
1
1
  <%= tag.footer(**merge_options(wrapper_options,
2
2
  class: class_names(
3
3
  Anchor::PageComponent::HORIZONTAL_PADDING_CLASS,
4
- "py-5 flex justify-end gap-4 border-t bg-white border-subdued sticky bottom-0",
4
+ "py-5 flex items-center justify-end gap-4 border-t bg-white border-subdued sticky bottom-0",
5
5
  ),
6
6
  data: { testid: "page-footer" }),
7
7
  ) do %>
8
+ <%= supporting_text %>
8
9
  <%= delete_button unless done_button? %>
9
10
  <%= cancel_button unless done_button? %>
10
11
  <%= save_button unless done_button? %>
@@ -34,6 +34,14 @@ module Anchor
34
34
  **kwargs
35
35
  )
36
36
  }
37
+
38
+ renders_one :supporting_text, lambda { |supporting_text, **kwargs|
39
+ anchor_text(
40
+ supporting_text,
41
+ class: "text-secondary",
42
+ **kwargs
43
+ )
44
+ }
37
45
  end
38
46
  end
39
47
  end
@@ -1,40 +1,7 @@
1
1
  module Anchor
2
2
  class RadioButtonCollectionComponent < Component
3
- attr_reader :attribute, :descriptions, :form_builder
4
-
5
- def initialize(
6
- form_builder:,
7
- attribute:,
8
- collection:,
9
- value_method:,
10
- text_method:,
11
- descriptions: nil,
12
- **options
13
- )
14
- @form_builder = form_builder
15
- @attribute = attribute
16
- @collection = collection
17
- @value_method = value_method
18
- @text_method = text_method
19
- @descriptions = descriptions
20
- @options = options
21
-
22
- super()
23
- end
24
-
25
3
  def options
26
- @options.merge(
27
- class: Array(@options.delete(:class)) +
28
- RadioButtonComponent::INPUT_CLASSES
29
- )
30
- end
31
-
32
- def radio(radio:)
33
- RadioButtonComponent.new(
34
- radio:,
35
- attribute:,
36
- form_builder:
37
- )
4
+ { class: RadioButtonComponent::INPUT_CLASSES }
38
5
  end
39
6
  end
40
7
  end
@@ -5,7 +5,7 @@
5
5
  <% if description.present? %>
6
6
  <div class="flex flex-col gap-1">
7
7
  <%= radio.label %>
8
- <%= description_span %>
8
+ <%= tag.span description, id: description_id, class: DESCRIPTION_CLASSES %>
9
9
  </div>
10
10
  <% else %>
11
11
  <%= tag.div radio.label, class: "self-center" %>
@@ -36,24 +36,20 @@ module Anchor
36
36
  text-sm
37
37
  ).freeze
38
38
 
39
- def initialize(radio:, attribute:, form_builder:, **kwargs)
39
+ def initialize(form_builder:, radio:, attribute:, **kwargs)
40
+ @form_builder = form_builder
40
41
  @radio = radio
41
42
  @attribute = attribute
42
- @form_builder = form_builder
43
43
 
44
44
  super(**kwargs)
45
45
  end
46
46
 
47
47
  private
48
48
 
49
- attr_reader :radio, :attribute, :form_builder
49
+ attr_reader :form_builder, :radio, :attribute
50
50
 
51
51
  delegate :value, :object, to: :radio
52
52
 
53
- def description_span
54
- tag.span description, id: description_id, class: DESCRIPTION_CLASSES
55
- end
56
-
57
53
  def radio_options
58
54
  {
59
55
  aria: description.present? && aria_description,
@@ -1,15 +1,11 @@
1
1
  module Anchor
2
2
  class SelectComponent < Component
3
- attr_reader :attribute
4
-
5
3
  def initialize(
6
- form_builder:,
7
4
  attribute:,
8
5
  choices:,
9
6
  show_marker: true,
10
7
  **kwargs
11
8
  )
12
- @form_builder = form_builder
13
9
  @attribute = attribute
14
10
  @choices = choices
15
11
  @show_marker = show_marker
@@ -24,7 +20,7 @@ module Anchor
24
20
  def html_options
25
21
  {
26
22
  class: InputComponent::INPUT_CLASSES,
27
- data: { testid: "select-#{attribute.to_s.dasherize}" },
23
+ data: { testid: "select-#{@attribute.to_s.dasherize}" },
28
24
  }
29
25
  end
30
26
 
@@ -1,7 +1,11 @@
1
1
  <%= tag.div(
2
2
  **merge_options(wrapper_options, {
3
3
  class: "toast",
4
- data: { controller: "toast", testid: test_id },
4
+ data: {
5
+ controller: "toast",
6
+ toast_assistive_tech_notification_value: assistive_tech_notification,
7
+ testid: test_id,
8
+ },
5
9
  popover: "manual",
6
10
  })
7
11
  ) do %>
@@ -25,6 +25,10 @@ module Anchor
25
25
  content?
26
26
  end
27
27
 
28
+ def assistive_tech_notification
29
+ strip_tags(content)
30
+ end
31
+
28
32
  def test_id
29
33
  wrapper_options.dig(:data, :testid) || "toast"
30
34
  end
@@ -1,11 +1,14 @@
1
1
  import { Controller } from "@hotwired/stimulus";
2
+ import { notifyAssistiveTech } from "./assistive_tech_notifications_component";
2
3
 
3
4
  export default class extends Controller<HTMLDivElement> {
4
5
  static values = {
6
+ assistiveTechNotification: String,
5
7
  hideDelay: { type: Number, default: 3000 },
6
8
  showDelay: { type: Number, default: 200 },
7
9
  };
8
10
 
11
+ declare assistiveTechNotificationValue: string;
9
12
  declare hideDelayValue: number
10
13
  declare showDelayValue: number
11
14
 
@@ -17,6 +20,8 @@ export default class extends Controller<HTMLDivElement> {
17
20
  setTimeout(() => {
18
21
  this.element.remove();
19
22
  }, this.hideDelayValue);
23
+
24
+ notifyAssistiveTech(this.assistiveTechNotificationValue);
20
25
  }
21
26
 
22
27
  disconnect(): void {
@@ -32,15 +32,7 @@ module Anchor
32
32
  options = {},
33
33
  html_options = {}
34
34
  )
35
- render RadioButtonCollectionComponent.new(
36
- form_builder: self,
37
- attribute:,
38
- collection:,
39
- value_method:,
40
- text_method:,
41
- **options,
42
- **html_options
43
- ) do |component|
35
+ render RadioButtonCollectionComponent.new do |component|
44
36
  super(
45
37
  attribute,
46
38
  collection,
@@ -49,7 +41,11 @@ module Anchor
49
41
  options,
50
42
  component.options.merge(html_options),
51
43
  ) do |radio|
52
- render component.radio(radio:)
44
+ render RadioButtonComponent.new(
45
+ radio:,
46
+ attribute:,
47
+ form_builder: self
48
+ )
53
49
  end
54
50
  end
55
51
  end
@@ -86,9 +82,7 @@ module Anchor
86
82
 
87
83
  def email_field(attribute, options = {})
88
84
  render InputComponent.new(
89
- form_builder: self,
90
85
  attribute:,
91
- type: :email,
92
86
  starting_icon: options.delete(:starting_icon),
93
87
  ending_icon: options.delete(:ending_icon)
94
88
  ) do |component|
@@ -98,7 +92,7 @@ module Anchor
98
92
 
99
93
  def error_message_for(attribute, options = {})
100
94
  render ErrorMessageComponent.new(
101
- form_builder: self,
95
+ object:,
102
96
  attribute:,
103
97
  **options
104
98
  )
@@ -106,10 +100,9 @@ module Anchor
106
100
 
107
101
  def label(attribute, text = nil, options = {}, &block)
108
102
  render LabelComponent.new(
109
- form_builder: self,
103
+ object:,
110
104
  attribute:,
111
- text:,
112
- **options
105
+ required: options.delete(:required) { :if_has_validators }
113
106
  ) do |component|
114
107
  if block.present?
115
108
  super
@@ -126,31 +119,26 @@ module Anchor
126
119
 
127
120
  def number_field(attribute, options = {})
128
121
  render InputComponent.new(
129
- form_builder: self,
130
122
  attribute:,
131
- type: :number,
132
123
  starting_icon: options.delete(:starting_icon),
133
124
  ending_icon: options.delete(:ending_icon)
134
125
  ) do |component|
135
- super attribute, component.options.merge(options)
126
+ super(attribute, component.options.merge(options))
136
127
  end
137
128
  end
138
129
 
139
130
  def search_field(attribute, options = {})
140
131
  render InputComponent.new(
141
- form_builder: self,
142
132
  attribute:,
143
- type: :search,
144
133
  starting_icon: options.delete(:starting_icon),
145
134
  ending_icon: options.delete(:ending_icon)
146
135
  ) do |component|
147
- super attribute, component.options.merge(options)
136
+ super(attribute, component.options.merge(options))
148
137
  end
149
138
  end
150
139
 
151
140
  def select(attribute, choices = nil, options = {}, html_options = {}, &)
152
141
  render SelectComponent.new(
153
- form_builder: self,
154
142
  attribute:,
155
143
  choices:,
156
144
  show_marker: options.fetch(:show_marker, true),
@@ -168,9 +156,7 @@ module Anchor
168
156
 
169
157
  def text_field(attribute, options = {})
170
158
  render InputComponent.new(
171
- form_builder: self,
172
159
  attribute:,
173
- type: :text,
174
160
  starting_icon: options.delete(:starting_icon),
175
161
  ending_icon: options.delete(:ending_icon)
176
162
  ) do |component|
@@ -1,13 +1,18 @@
1
1
  module Anchor
2
2
  class ModelValidators
3
- def initialize(model, options)
3
+ # Pass the value :if_has_validators to the required keyword argument to
4
+ # indicate an unspecified value. Passing any other value will result in
5
+ # `required`'s truthiness being used to determine whether the attribute is
6
+ # required, while passing :if_has_validators will check the model's
7
+ # validators.
8
+ def initialize(model, required: :if_has_validators)
4
9
  @model = model
5
- @options = options
10
+ @required = required
6
11
  end
7
12
 
8
13
  def attribute_required?(attribute)
9
- if options.has_key?(:required)
10
- options[:required]
14
+ if required != :if_has_validators
15
+ required
11
16
  elsif has_validators?
12
17
  attribute_required_by_validators?(attribute)
13
18
  else
@@ -17,7 +22,7 @@ module Anchor
17
22
 
18
23
  private
19
24
 
20
- attr_reader :model, :options
25
+ attr_reader :model, :required
21
26
 
22
27
  def has_validators?
23
28
  model.class.respond_to?(:validators_on)
@@ -2,6 +2,7 @@ module Anchor
2
2
  module ViewHelper
3
3
  ANCHOR_HELPERS = %i[
4
4
  action_menu
5
+ assistive_tech_notifications
5
6
  autocomplete/results
6
7
  badge
7
8
  banner
@@ -1,5 +1,5 @@
1
1
  module Anchor
2
2
  module ViewComponents
3
- VERSION = "0.43.0".freeze
3
+ VERSION = "0.45.0".freeze
4
4
  end
5
5
  end
@@ -0,0 +1,23 @@
1
+ module Anchor
2
+ class AvoidImplicitSuper < RuboCop::Cop::Cop
3
+ MSG = "Avoid implicit `super` calls. Prefer `super(**kwargs)`, " \
4
+ "`super()`, or a mix of explicit/implicit arguments, e.g. " \
5
+ "`super(id:, title:, **kwargs)`.".freeze
6
+
7
+ def_node_matcher :initialize_with_implicit_super?, <<~PATTERN
8
+ (def :initialize _ (begin ... $`zsuper ...))
9
+ PATTERN
10
+
11
+ def on_def(node)
12
+ return unless (zsuper = initialize_with_implicit_super?(node))
13
+
14
+ add_offense(zsuper, message:)
15
+ end
16
+
17
+ private
18
+
19
+ def message
20
+ MSG
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,10 @@
1
+ <%= anchor_text do %>
2
+ This component is visually hidden, by design. Inspect the page source and look
3
+ for the two
4
+ <%= anchor_link(
5
+ "ARIA live regions",
6
+ href: "https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/ARIA_Live_Regions",
7
+ ) %>:
8
+ <code>div#js-assistive-tech-notifications</code>
9
+ and <code>div#js-assistive-tech-notifications-assertive</code>.
10
+ <% end %>
@@ -0,0 +1,5 @@
1
+ module Anchor
2
+ class AssistiveTechNotificationsComponentPreview < Preview
3
+ def default; end
4
+ end
5
+ end
@@ -0,0 +1,21 @@
1
+ <% invalid_user = FakeUser.new(name: "").tap(&:validate) %>
2
+
3
+ <%= anchor_dialog(
4
+ id: "my-dialog",
5
+ title: "Dialog Title",
6
+ ) do |dialog| %>
7
+ <% dialog.with_show_button_content("Open dialog") %>
8
+
9
+ <% dialog.with_body do %>
10
+ <%= anchor_form_with(model: invalid_user, url: false, id: 'a-form') do |form| %>
11
+ <%= form.label(:name) %>
12
+ <%= form.text_field(:name, required: true) %>
13
+ <%= form.error_message_for(:name) %>
14
+ <% end %>
15
+ <% end %>
16
+
17
+ <% dialog.with_footer do %>
18
+ <%= anchor_button("Cancel", data: { action: "dialog#close" }) %>
19
+ <%= anchor_button("Save", variant: :primary, type: :submit, form: 'a-form') %>
20
+ <% end %>
21
+ <% end %>
@@ -16,6 +16,8 @@ module Anchor
16
16
  end
17
17
  end
18
18
 
19
+ def with_form; end
20
+
19
21
  def with_footer; end
20
22
 
21
23
  def positioned_right; end
@@ -0,0 +1,24 @@
1
+ <%= anchor_page do |page| %>
2
+ <% page.with_header do |header| %>
3
+ <% header.with_breadcrumbs do |breadcrumbs| %>
4
+ <% breadcrumbs.with_item(href: "#").with_content("Breadcrumb 1") %>
5
+ <% breadcrumbs.with_item(href: "").with_content("Breadcrumb 2") %>
6
+ <% end %>
7
+ <% header.with_title("Page title") %>
8
+ <% end %>
9
+ <% page.with_body do |body| %>
10
+ <%= anchor_form_with(url: false, id: "form") do |form| %>
11
+ <%= tag.div class: "max-w-sm" do %>
12
+ <%= form.label :name, "Name" %>
13
+ <%= form.text_field(
14
+ :name,
15
+ ) %>
16
+ <% end %>
17
+ <% end %>
18
+ <% end %>
19
+ <% page.with_footer do |footer| %>
20
+ <% footer.with_cancel_button(href: "#") %>
21
+ <% footer.with_save_button(form: "form") %>
22
+ <% footer.with_supporting_text("Optional text") %>
23
+ <% end %>
24
+ <% end %>
@@ -15,5 +15,7 @@ module Anchor
15
15
  def with_done_button; end
16
16
 
17
17
  def with_cancel_and_save_buttons; end
18
+
19
+ def with_supporting_text_and_buttons; end
18
20
  end
19
21
  end
@@ -1,20 +1,11 @@
1
- <%= anchor_form_with url: false, method: :get do |form| %>
2
- <%= form.search_field :search, placeholder: "Search…",
3
- starting_icon: :search %>
4
- <%= form.text_field :search, placeholder: "Search…",
5
- ending_icon: :calendar %>
6
- <%= form.text_field :search, placeholder: "Search…", starting_icon: :search,
7
- ending_icon: :calendar %>
8
- <%= form.number_field(
9
- :amount,
10
- placeholder: "0.00",
11
- starting_icon: :dollar,
12
- step: 0.01
13
- ) %>
14
- <%= form.number_field(
15
- :amount,
16
- placeholder: "0",
17
- ending_icon: :dollar,
18
- step: 1
19
- ) %>
1
+ <%= anchor_form_with url: false do |form| %>
2
+ <%= tag.div do %>
3
+ <%= form.label :price %>
4
+ <%= form.text_field :price, inputmode: :numeric, starting_icon: "dollar" %>
5
+ <% end %>
6
+
7
+ <%= tag.div do %>
8
+ <%= form.label :discount %>
9
+ <%= form.text_field :discount, inputmode: :numeric, ending_icon: "percentage" %>
10
+ <% end %>
20
11
  <% end %>
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: anchor_view_components
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.43.0
4
+ version: 0.45.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Buoy Software
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-01-04 00:00:00.000000000 Z
11
+ date: 2024-01-10 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -69,6 +69,7 @@ files:
69
69
  - app/assets/images/icons/nav-arrow-left.svg
70
70
  - app/assets/images/icons/nav-arrow-right.svg
71
71
  - app/assets/images/icons/paste-clipboard.svg
72
+ - app/assets/images/icons/percentage.svg
72
73
  - app/assets/images/icons/plus.svg
73
74
  - app/assets/images/icons/search.svg
74
75
  - app/assets/images/icons/warning-circle.svg
@@ -82,6 +83,9 @@ files:
82
83
  - app/components/anchor/action_menu_component.html.erb
83
84
  - app/components/anchor/action_menu_component.rb
84
85
  - app/components/anchor/anchor_view_components.ts
86
+ - app/components/anchor/assistive_tech_notifications_component.html.erb
87
+ - app/components/anchor/assistive_tech_notifications_component.rb
88
+ - app/components/anchor/assistive_tech_notifications_component.ts
85
89
  - app/components/anchor/autocomplete/results_component.html.erb
86
90
  - app/components/anchor/autocomplete/results_component.rb
87
91
  - app/components/anchor/autocomplete_component.en.yml
@@ -191,10 +195,13 @@ files:
191
195
  - lib/anchor/view_components/svg_resolver.rb
192
196
  - lib/anchor/view_components/version.rb
193
197
  - lib/anchor_view_components.rb
198
+ - lib/cops/anchor/avoid_implicit_super.rb
194
199
  - previews/anchor/action_menu_component_preview.rb
195
200
  - previews/anchor/action_menu_component_preview/autoplacement.html.erb
196
201
  - previews/anchor/action_menu_component_preview/custom_trigger.html.erb
197
202
  - previews/anchor/action_menu_component_preview/opens_a_dialog.html.erb
203
+ - previews/anchor/assistive_tech_notifications_component_preview.rb
204
+ - previews/anchor/assistive_tech_notifications_component_preview/default.html.erb
198
205
  - previews/anchor/badge_component_preview.rb
199
206
  - previews/anchor/banner_component_preview.rb
200
207
  - previews/anchor/banner_component_preview/body_only.html.erb
@@ -209,6 +216,7 @@ files:
209
216
  - previews/anchor/dialog_component_preview/positioned_right.html.erb
210
217
  - previews/anchor/dialog_component_preview/with_custom_show_button.html.erb
211
218
  - previews/anchor/dialog_component_preview/with_footer.html.erb
219
+ - previews/anchor/dialog_component_preview/with_form.html.erb
212
220
  - previews/anchor/filter_component_preview.rb
213
221
  - previews/anchor/forms_preview.rb
214
222
  - previews/anchor/icon_component_preview.rb
@@ -223,6 +231,7 @@ files:
223
231
  - previews/anchor/page_component_preview/with_done_button.html.erb
224
232
  - previews/anchor/page_component_preview/with_page_header_custom_content.html.erb
225
233
  - previews/anchor/page_component_preview/with_search.html.erb
234
+ - previews/anchor/page_component_preview/with_supporting_text_and_buttons.html.erb
226
235
  - previews/anchor/panel_component_preview.rb
227
236
  - previews/anchor/panel_component_preview/with_banner.html.erb
228
237
  - previews/anchor/preview.rb