openproject-primer_view_components 0.80.2 → 0.81.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 (82) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +36 -22
  3. data/README.md +20 -1
  4. data/app/assets/javascripts/components/primer/primer.d.ts +1 -0
  5. data/app/assets/javascripts/lib/primer/forms/character_counter.d.ts +41 -0
  6. data/app/assets/javascripts/lib/primer/forms/primer_text_area.d.ts +13 -0
  7. data/app/assets/javascripts/lib/primer/forms/primer_text_field.d.ts +2 -0
  8. data/app/assets/javascripts/primer_view_components.js +1 -1
  9. data/app/assets/javascripts/primer_view_components.js.map +1 -1
  10. data/app/assets/styles/primer_view_components.css +1 -1
  11. data/app/assets/styles/primer_view_components.css.map +1 -1
  12. data/app/components/primer/alpha/action_list/item.rb +2 -1
  13. data/app/components/primer/alpha/auto_complete/auto_complete.html.erb +8 -6
  14. data/app/components/primer/alpha/select_panel.rb +1 -1
  15. data/app/components/primer/alpha/select_panel_element.js +1 -1
  16. data/app/components/primer/alpha/select_panel_element.ts +1 -1
  17. data/app/components/primer/alpha/stack.rb +1 -0
  18. data/app/components/primer/alpha/tab_nav.css +1 -1
  19. data/app/components/primer/alpha/tab_nav.css.json +1 -0
  20. data/app/components/primer/alpha/tab_nav.css.map +1 -1
  21. data/app/components/primer/alpha/tab_nav.pcss +7 -1
  22. data/app/components/primer/alpha/text_area.rb +1 -0
  23. data/app/components/primer/alpha/text_field.rb +1 -0
  24. data/app/components/primer/alpha/toggle_switch.html.erb +2 -2
  25. data/app/components/primer/alpha/tool_tip.js +12 -5
  26. data/app/components/primer/alpha/tool_tip.ts +14 -5
  27. data/app/components/primer/beta/auto_complete/item.html.erb +5 -4
  28. data/app/components/primer/beta/avatar.rb +4 -0
  29. data/app/components/primer/beta/avatar_stack.rb +6 -0
  30. data/app/components/primer/beta/blankslate.css +1 -1
  31. data/app/components/primer/beta/blankslate.css.map +1 -1
  32. data/app/components/primer/beta/blankslate.pcss +2 -0
  33. data/app/components/primer/beta/spinner.html.erb +2 -2
  34. data/app/components/primer/primer.d.ts +1 -0
  35. data/app/components/primer/primer.js +1 -0
  36. data/app/components/primer/primer.ts +1 -0
  37. data/app/controllers/primer/view_components/toggle_switch_controller.rb +2 -2
  38. data/app/forms/check_box_with_nested_form.rb +9 -5
  39. data/app/forms/text_area_with_character_limit_form.rb +13 -0
  40. data/app/forms/text_field_with_character_limit_form.rb +13 -0
  41. data/app/lib/primer/forms/caption.html.erb +16 -9
  42. data/app/lib/primer/forms/character_counter.d.ts +41 -0
  43. data/app/lib/primer/forms/character_counter.js +114 -0
  44. data/app/lib/primer/forms/character_counter.ts +129 -0
  45. data/app/lib/primer/forms/check_box.rb +28 -0
  46. data/app/lib/primer/forms/dsl/input.rb +23 -0
  47. data/app/lib/primer/forms/dsl/multi_input.rb +3 -1
  48. data/app/lib/primer/forms/dsl/text_area_input.rb +12 -1
  49. data/app/lib/primer/forms/dsl/text_field_input.rb +11 -1
  50. data/app/lib/primer/forms/form_control.html.erb +2 -1
  51. data/app/lib/primer/forms/primer_text_area.d.ts +13 -0
  52. data/app/lib/primer/forms/primer_text_area.js +53 -0
  53. data/app/lib/primer/forms/primer_text_area.ts +37 -0
  54. data/app/lib/primer/forms/primer_text_field.d.ts +2 -0
  55. data/app/lib/primer/forms/primer_text_field.js +16 -2
  56. data/app/lib/primer/forms/primer_text_field.ts +16 -3
  57. data/app/lib/primer/forms/text_area.html.erb +6 -4
  58. data/app/lib/primer/forms/text_field.html.erb +1 -1
  59. data/app/lib/primer/forms/text_field.rb +8 -0
  60. data/lib/primer/accessibility.rb +9 -3
  61. data/lib/primer/view_components/engine.rb +1 -4
  62. data/lib/primer/view_components/version.rb +2 -2
  63. data/previews/primer/alpha/action_menu_preview/submitting_forms.html.erb +1 -1
  64. data/previews/primer/alpha/form_control_preview/playground.html.erb +4 -2
  65. data/previews/primer/alpha/octicon_symbols_preview/playground.html.erb +1 -1
  66. data/previews/primer/alpha/overlay_preview.rb +0 -25
  67. data/previews/primer/alpha/text_area_preview.rb +29 -8
  68. data/previews/primer/alpha/text_field_preview.rb +34 -4
  69. data/previews/primer/alpha/toggle_switch_preview.rb +14 -14
  70. data/previews/primer/alpha/tree_view_preview/loading_failure.html.erb +53 -1
  71. data/previews/primer/alpha/tree_view_preview/stress_test.html.erb +43 -0
  72. data/previews/primer/beta/button_preview/all_schemes.html.erb +1 -1
  73. data/previews/primer/beta/button_preview/invisible_all_visuals.html.erb +1 -1
  74. data/previews/primer/beta/button_preview/summary_as_button.html.erb +10 -1
  75. data/previews/primer/forms_preview/text_area_with_character_limit_form.html.erb +3 -0
  76. data/previews/primer/forms_preview/text_field_with_character_limit_form.html.erb +3 -0
  77. data/previews/primer/forms_preview.rb +6 -0
  78. data/static/arguments.json +13 -1
  79. data/static/form_previews.json +10 -0
  80. data/static/info_arch.json +106 -15
  81. data/static/previews.json +93 -14
  82. metadata +15 -2
@@ -0,0 +1,114 @@
1
+ /**
2
+ * Shared character counting functionality for text inputs with character limits.
3
+ * Handles real-time character count updates, validation, and aria-live announcements.
4
+ */
5
+ export class CharacterCounter {
6
+ constructor(inputElement, characterLimitElement, characterLimitSrElement) {
7
+ this.inputElement = inputElement;
8
+ this.characterLimitElement = characterLimitElement;
9
+ this.characterLimitSrElement = characterLimitSrElement;
10
+ this.SCREEN_READER_DELAY = 500;
11
+ this.announceTimeout = null;
12
+ this.isInitialLoad = true;
13
+ }
14
+ /**
15
+ * Initialize character counting by setting up event listener and initial count
16
+ */
17
+ initialize(signal) {
18
+ this.inputElement.addEventListener('keyup', () => this.updateCharacterCount(), signal ? { signal } : undefined); // Keyup used over input for better screen reader support
19
+ this.inputElement.addEventListener('paste', () => setTimeout(() => this.updateCharacterCount(), 50), // Gives the pasted content time to register
20
+ signal ? { signal } : undefined);
21
+ this.updateCharacterCount();
22
+ this.isInitialLoad = false;
23
+ }
24
+ /**
25
+ * Clean up any pending timeouts
26
+ */
27
+ cleanup() {
28
+ if (this.announceTimeout) {
29
+ clearTimeout(this.announceTimeout);
30
+ }
31
+ }
32
+ /**
33
+ * Pluralizes a word based on the count
34
+ */
35
+ pluralize(count, string) {
36
+ return count === 1 ? string : `${string}s`;
37
+ }
38
+ /**
39
+ * Update the character count display and validation state
40
+ */
41
+ updateCharacterCount() {
42
+ if (!this.characterLimitElement)
43
+ return;
44
+ const maxLengthAttr = this.characterLimitElement.getAttribute('data-max-length');
45
+ if (!maxLengthAttr)
46
+ return;
47
+ const maxLength = parseInt(maxLengthAttr, 10);
48
+ const currentLength = this.inputElement.value.length;
49
+ const charactersRemaining = maxLength - currentLength;
50
+ let message = '';
51
+ if (charactersRemaining >= 0) {
52
+ const characterText = this.pluralize(charactersRemaining, 'character');
53
+ message = `${charactersRemaining} ${characterText} remaining`;
54
+ const textSpan = this.characterLimitElement.querySelector('.FormControl-caption-text');
55
+ if (textSpan) {
56
+ textSpan.textContent = message;
57
+ }
58
+ this.clearError();
59
+ }
60
+ else {
61
+ const charactersOver = -charactersRemaining;
62
+ const characterText = this.pluralize(charactersOver, 'character');
63
+ message = `${charactersOver} ${characterText} over`;
64
+ const textSpan = this.characterLimitElement.querySelector('.FormControl-caption-text');
65
+ if (textSpan) {
66
+ textSpan.textContent = message;
67
+ }
68
+ this.setError();
69
+ }
70
+ // We don't want this announced on initial load
71
+ if (!this.isInitialLoad) {
72
+ this.announceToScreenReader(message);
73
+ }
74
+ }
75
+ /**
76
+ * Announce character count to screen readers with debouncing
77
+ */
78
+ announceToScreenReader(message) {
79
+ if (this.announceTimeout) {
80
+ clearTimeout(this.announceTimeout);
81
+ }
82
+ this.announceTimeout = window.setTimeout(() => {
83
+ if (this.characterLimitSrElement) {
84
+ this.characterLimitSrElement.textContent = message;
85
+ }
86
+ }, this.SCREEN_READER_DELAY);
87
+ }
88
+ /**
89
+ * Set error when character limit is exceeded
90
+ */
91
+ setError() {
92
+ this.inputElement.setAttribute('invalid', 'true');
93
+ this.inputElement.setAttribute('aria-invalid', 'true');
94
+ this.characterLimitElement.classList.add('fgColor-danger');
95
+ // Show danger icon
96
+ const icon = this.characterLimitElement.querySelector('.FormControl-caption-icon');
97
+ if (icon) {
98
+ icon.removeAttribute('hidden');
99
+ }
100
+ }
101
+ /**
102
+ * Clear error when back under character limit
103
+ */
104
+ clearError() {
105
+ this.inputElement.removeAttribute('invalid');
106
+ this.inputElement.removeAttribute('aria-invalid');
107
+ this.characterLimitElement.classList.remove('fgColor-danger');
108
+ // Hide danger icon
109
+ const icon = this.characterLimitElement.querySelector('.FormControl-caption-icon');
110
+ if (icon) {
111
+ icon.setAttribute('hidden', '');
112
+ }
113
+ }
114
+ }
@@ -0,0 +1,129 @@
1
+ /**
2
+ * Shared character counting functionality for text inputs with character limits.
3
+ * Handles real-time character count updates, validation, and aria-live announcements.
4
+ */
5
+ export class CharacterCounter {
6
+ private SCREEN_READER_DELAY: number = 500
7
+ private announceTimeout: number | null = null
8
+ private isInitialLoad: boolean = true
9
+
10
+ constructor(
11
+ private inputElement: HTMLInputElement | HTMLTextAreaElement,
12
+ private characterLimitElement: HTMLElement,
13
+ private characterLimitSrElement: HTMLElement,
14
+ ) {}
15
+
16
+ /**
17
+ * Initialize character counting by setting up event listener and initial count
18
+ */
19
+ initialize(signal?: AbortSignal): void {
20
+ this.inputElement.addEventListener('keyup', () => this.updateCharacterCount(), signal ? {signal} : undefined) // Keyup used over input for better screen reader support
21
+ this.inputElement.addEventListener(
22
+ 'paste',
23
+ () => setTimeout(() => this.updateCharacterCount(), 50), // Gives the pasted content time to register
24
+ signal ? {signal} : undefined,
25
+ )
26
+ this.updateCharacterCount()
27
+ this.isInitialLoad = false
28
+ }
29
+
30
+ /**
31
+ * Clean up any pending timeouts
32
+ */
33
+ cleanup(): void {
34
+ if (this.announceTimeout) {
35
+ clearTimeout(this.announceTimeout)
36
+ }
37
+ }
38
+
39
+ /**
40
+ * Pluralizes a word based on the count
41
+ */
42
+ private pluralize(count: number, string: string): string {
43
+ return count === 1 ? string : `${string}s`
44
+ }
45
+
46
+ /**
47
+ * Update the character count display and validation state
48
+ */
49
+ private updateCharacterCount(): void {
50
+ if (!this.characterLimitElement) return
51
+
52
+ const maxLengthAttr = this.characterLimitElement.getAttribute('data-max-length')
53
+ if (!maxLengthAttr) return
54
+
55
+ const maxLength = parseInt(maxLengthAttr, 10)
56
+ const currentLength = this.inputElement.value.length
57
+ const charactersRemaining = maxLength - currentLength
58
+ let message = ''
59
+
60
+ if (charactersRemaining >= 0) {
61
+ const characterText = this.pluralize(charactersRemaining, 'character')
62
+ message = `${charactersRemaining} ${characterText} remaining`
63
+ const textSpan = this.characterLimitElement.querySelector('.FormControl-caption-text')
64
+ if (textSpan) {
65
+ textSpan.textContent = message
66
+ }
67
+ this.clearError()
68
+ } else {
69
+ const charactersOver = -charactersRemaining
70
+ const characterText = this.pluralize(charactersOver, 'character')
71
+ message = `${charactersOver} ${characterText} over`
72
+ const textSpan = this.characterLimitElement.querySelector('.FormControl-caption-text')
73
+ if (textSpan) {
74
+ textSpan.textContent = message
75
+ }
76
+ this.setError()
77
+ }
78
+
79
+ // We don't want this announced on initial load
80
+ if (!this.isInitialLoad) {
81
+ this.announceToScreenReader(message)
82
+ }
83
+ }
84
+
85
+ /**
86
+ * Announce character count to screen readers with debouncing
87
+ */
88
+ private announceToScreenReader(message: string): void {
89
+ if (this.announceTimeout) {
90
+ clearTimeout(this.announceTimeout)
91
+ }
92
+
93
+ this.announceTimeout = window.setTimeout(() => {
94
+ if (this.characterLimitSrElement) {
95
+ this.characterLimitSrElement.textContent = message
96
+ }
97
+ }, this.SCREEN_READER_DELAY)
98
+ }
99
+
100
+ /**
101
+ * Set error when character limit is exceeded
102
+ */
103
+ private setError(): void {
104
+ this.inputElement.setAttribute('invalid', 'true')
105
+ this.inputElement.setAttribute('aria-invalid', 'true')
106
+ this.characterLimitElement.classList.add('fgColor-danger')
107
+
108
+ // Show danger icon
109
+ const icon = this.characterLimitElement.querySelector('.FormControl-caption-icon')
110
+ if (icon) {
111
+ icon.removeAttribute('hidden')
112
+ }
113
+ }
114
+
115
+ /**
116
+ * Clear error when back under character limit
117
+ */
118
+ private clearError(): void {
119
+ this.inputElement.removeAttribute('invalid')
120
+ this.inputElement.removeAttribute('aria-invalid')
121
+ this.characterLimitElement.classList.remove('fgColor-danger')
122
+
123
+ // Hide danger icon
124
+ const icon = this.characterLimitElement.querySelector('.FormControl-caption-icon')
125
+ if (icon) {
126
+ icon.setAttribute('hidden', '')
127
+ }
128
+ }
129
+ }
@@ -11,6 +11,13 @@ module Primer
11
11
  @input.add_label_classes("FormControl-label")
12
12
  @input.add_input_classes("FormControl-checkbox")
13
13
 
14
+ # Generate custom ID that preserves brackets from the name
15
+ unless @input.input_arguments[:id].present?
16
+ generate_custom_id
17
+ # Update the label's for attribute to match the new ID
18
+ @input.label_arguments[:for] = @input.input_arguments[:id]
19
+ end
20
+
14
21
  return unless @input.scheme == :array
15
22
 
16
23
  @input.input_arguments[:multiple] = true
@@ -32,6 +39,27 @@ module Primer
32
39
 
33
40
  private
34
41
 
42
+ def generate_custom_id
43
+ # Generate an ID from the name that preserves special characters like brackets
44
+ # For array scheme: name + "_" + value (e.g., "permissions[3]_foo")
45
+ # For boolean scheme: just the name (e.g., "long_o")
46
+ base_name = @input.name.to_s
47
+
48
+ # For array scheme, Rails appends [] to the name, so we remove it for ID generation
49
+ # but only the trailing [] that Rails adds, not brackets that are part of the original name
50
+ # Regex /\[\]$/ matches literal "[]" at the end of the string
51
+ base_name = base_name.sub(/\[\]$/, "")
52
+
53
+ # For array scheme, append the value to make IDs unique
54
+ # For boolean scheme, just use the base name
55
+ # Note: Rails automatically escapes HTML attributes, so special characters are safe
56
+ if @input.scheme == :array && @input.value.present?
57
+ @input.input_arguments[:id] = "#{base_name}_#{@input.value}"
58
+ else
59
+ @input.input_arguments[:id] = base_name
60
+ end
61
+ end
62
+
35
63
  def checked_value
36
64
  @input.value || "1"
37
65
  end
@@ -33,6 +33,9 @@ module Primer
33
33
  # @!macro [new] form_full_width_arguments
34
34
  # @param full_width [Boolean] When set to `true`, the field will take up all the horizontal space allowed by its container. Defaults to `true`.
35
35
 
36
+ # @!macro [new] form_input_character_limit_arguments
37
+ # @param character_limit [Number] Optional character limit for the input. If provided, a character counter will be displayed below the input.
38
+
36
39
  # @!macro [new] form_input_width_arguments
37
40
  # @param input_width [Symbol] The width of the field. <%= one_of(Primer::Forms::Dsl::Input::INPUT_WIDTH_OPTIONS) %>
38
41
 
@@ -128,6 +131,7 @@ module Primer
128
131
 
129
132
  @ids = {}.tap do |id_map|
130
133
  id_map[:validation] = "validation-#{@base_id}" if supports_validation?
134
+ id_map[:character_limit_caption] = "character_limit-#{@base_id}" if character_limit?
131
135
  id_map[:caption] = "caption-#{@base_id}" if caption? || caption_template?
132
136
  end
133
137
 
@@ -213,6 +217,25 @@ module Primer
213
217
  form.render_caption_template(caption_template_name)
214
218
  end
215
219
 
220
+ def character_limit?
221
+ false
222
+ end
223
+
224
+ def character_limit_id
225
+ ids[:character_limit_caption]
226
+ end
227
+
228
+ def character_limit_target_prefix
229
+ case type
230
+ when :text_field
231
+ "primer-text-field"
232
+ when :text_area
233
+ "primer-text-area"
234
+ else
235
+ ""
236
+ end
237
+ end
238
+
216
239
  def valid?
217
240
  supports_validation? && validation_messages.empty? && !@invalid
218
241
  end
@@ -39,7 +39,9 @@ module Primer
39
39
  new_options[:data] ||= {}
40
40
  new_options[:data][:name] = name
41
41
  new_options[:data][:targets] = "primer-multi-input.fields"
42
- new_options[:id] = nil if options[:hidden]
42
+ new_options[:id] = "#{@name}_#{name}"
43
+ new_options[:aria] ||= {}
44
+ new_options[:aria][:labelledby] = "label-#{base_id}"
43
45
  new_options[:disabled] = true if options[:hidden] # disable to avoid submitting to server
44
46
  new_options
45
47
  end
@@ -5,13 +5,20 @@ module Primer
5
5
  module Dsl
6
6
  # :nodoc:
7
7
  class TextAreaInput < Input
8
- attr_reader :name, :label
8
+ attr_reader :name, :label, :character_limit
9
9
 
10
10
  def initialize(name:, label:, **system_arguments)
11
11
  @name = name
12
12
  @label = label
13
+ @character_limit = system_arguments.delete(:character_limit)
14
+
15
+ if @character_limit.present? && @character_limit.to_i <= 0
16
+ raise ArgumentError, "character_limit must be a positive integer, got #{@character_limit}"
17
+ end
13
18
 
14
19
  super(**system_arguments)
20
+
21
+ add_input_data(:target, "primer-text-area.inputElement")
15
22
  end
16
23
 
17
24
  def to_component
@@ -22,6 +29,10 @@ module Primer
22
29
  :text_area
23
30
  end
24
31
 
32
+ def character_limit?
33
+ @character_limit.present?
34
+ end
35
+
25
36
  # :nocov:
26
37
  def focusable?
27
38
  true
@@ -6,7 +6,7 @@ module Primer
6
6
  attr_reader(
7
7
  *%i[
8
8
  name label show_clear_button leading_visual leading_spinner trailing_visual clear_button_id
9
- visually_hide_label inset monospace field_wrap_classes auto_check_src
9
+ clear_button_label visually_hide_label inset monospace field_wrap_classes auto_check_src character_limit
10
10
  ]
11
11
  )
12
12
 
@@ -21,9 +21,15 @@ module Primer
21
21
  @trailing_visual = system_arguments.delete(:trailing_visual)
22
22
  @leading_spinner = !!system_arguments.delete(:leading_spinner)
23
23
  @clear_button_id = system_arguments.delete(:clear_button_id) || SecureRandom.uuid
24
+ @clear_button_label = system_arguments.delete(:clear_button_label)
24
25
  @inset = system_arguments.delete(:inset)
25
26
  @monospace = system_arguments.delete(:monospace)
26
27
  @auto_check_src = system_arguments.delete(:auto_check_src)
28
+ @character_limit = system_arguments.delete(:character_limit)
29
+
30
+ if @character_limit.present? && @character_limit.to_i <= 0
31
+ raise ArgumentError, "character_limit must be a positive integer, got #{@character_limit}"
32
+ end
27
33
 
28
34
  if @leading_visual
29
35
  @leading_visual[:classes] = class_names(
@@ -69,6 +75,10 @@ module Primer
69
75
  true
70
76
  end
71
77
 
78
+ def character_limit?
79
+ @character_limit.present?
80
+ end
81
+
72
82
  def validation_arguments
73
83
  if auto_check_src.present?
74
84
  super.merge(
@@ -1,7 +1,8 @@
1
1
  <% if @input.form_control? %>
2
2
  <%= content_tag(@tag, **@form_group_arguments) do %>
3
3
  <% if @input.label %>
4
- <%= builder.label(@input.name, **@input.label_arguments) do %>
4
+ <% label_id = @input.label_arguments[:id] || "label-#{@input.base_id}" %>
5
+ <%= builder.label(@input.name, **@input.label_arguments.merge(id: label_id)) do %>
5
6
  <%= @input.label %>
6
7
  <% if @input.required? %>
7
8
  <span aria-hidden="true">*</span>
@@ -0,0 +1,13 @@
1
+ export declare class PrimerTextAreaElement extends HTMLElement {
2
+ #private;
3
+ inputElement: HTMLTextAreaElement;
4
+ characterLimitElement: HTMLElement;
5
+ characterLimitSrElement: HTMLElement;
6
+ connectedCallback(): void;
7
+ disconnectedCallback(): void;
8
+ }
9
+ declare global {
10
+ interface Window {
11
+ PrimerTextAreaElement: typeof PrimerTextAreaElement;
12
+ }
13
+ }
@@ -0,0 +1,53 @@
1
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
2
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
4
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
6
+ };
7
+ var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) {
8
+ if (kind === "m") throw new TypeError("Private method is not writable");
9
+ if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter");
10
+ if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it");
11
+ return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
12
+ };
13
+ var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {
14
+ if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
15
+ if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
16
+ return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
17
+ };
18
+ var _PrimerTextAreaElement_characterCounter;
19
+ import { controller, target } from '@github/catalyst';
20
+ import { CharacterCounter } from './character_counter';
21
+ let PrimerTextAreaElement = class PrimerTextAreaElement extends HTMLElement {
22
+ constructor() {
23
+ super(...arguments);
24
+ _PrimerTextAreaElement_characterCounter.set(this, null);
25
+ }
26
+ connectedCallback() {
27
+ if (this.characterLimitElement) {
28
+ __classPrivateFieldSet(this, _PrimerTextAreaElement_characterCounter, new CharacterCounter(this.inputElement, this.characterLimitElement, this.characterLimitSrElement), "f");
29
+ __classPrivateFieldGet(this, _PrimerTextAreaElement_characterCounter, "f").initialize();
30
+ }
31
+ }
32
+ disconnectedCallback() {
33
+ __classPrivateFieldGet(this, _PrimerTextAreaElement_characterCounter, "f")?.cleanup();
34
+ }
35
+ };
36
+ _PrimerTextAreaElement_characterCounter = new WeakMap();
37
+ __decorate([
38
+ target
39
+ ], PrimerTextAreaElement.prototype, "inputElement", void 0);
40
+ __decorate([
41
+ target
42
+ ], PrimerTextAreaElement.prototype, "characterLimitElement", void 0);
43
+ __decorate([
44
+ target
45
+ ], PrimerTextAreaElement.prototype, "characterLimitSrElement", void 0);
46
+ PrimerTextAreaElement = __decorate([
47
+ controller
48
+ ], PrimerTextAreaElement);
49
+ export { PrimerTextAreaElement };
50
+ if (!window.customElements.get('primer-text-area')) {
51
+ Object.assign(window, { PrimerTextAreaElement });
52
+ window.customElements.define('primer-text-area', PrimerTextAreaElement);
53
+ }
@@ -0,0 +1,37 @@
1
+ import {controller, target} from '@github/catalyst'
2
+ import {CharacterCounter} from './character_counter'
3
+
4
+ @controller
5
+ export class PrimerTextAreaElement extends HTMLElement {
6
+ @target inputElement: HTMLTextAreaElement
7
+ @target characterLimitElement: HTMLElement
8
+ @target characterLimitSrElement: HTMLElement
9
+
10
+ #characterCounter: CharacterCounter | null = null
11
+
12
+ connectedCallback(): void {
13
+ if (this.characterLimitElement) {
14
+ this.#characterCounter = new CharacterCounter(
15
+ this.inputElement,
16
+ this.characterLimitElement,
17
+ this.characterLimitSrElement,
18
+ )
19
+ this.#characterCounter.initialize()
20
+ }
21
+ }
22
+
23
+ disconnectedCallback(): void {
24
+ this.#characterCounter?.cleanup()
25
+ }
26
+ }
27
+
28
+ declare global {
29
+ interface Window {
30
+ PrimerTextAreaElement: typeof PrimerTextAreaElement
31
+ }
32
+ }
33
+
34
+ if (!window.customElements.get('primer-text-area')) {
35
+ Object.assign(window, {PrimerTextAreaElement})
36
+ window.customElements.define('primer-text-area', PrimerTextAreaElement)
37
+ }
@@ -15,6 +15,8 @@ export declare class PrimerTextFieldElement extends HTMLElement {
15
15
  validationErrorIcon: HTMLElement;
16
16
  leadingVisual: HTMLElement;
17
17
  leadingSpinner: HTMLElement;
18
+ characterLimitElement: HTMLElement;
19
+ characterLimitSrElement: HTMLElement;
18
20
  connectedCallback(): void;
19
21
  disconnectedCallback(): void;
20
22
  clearContents(): void;
@@ -1,4 +1,3 @@
1
- /* eslint-disable custom-elements/expose-class-on-global */
2
1
  var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
3
2
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4
3
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
@@ -16,13 +15,15 @@ var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (
16
15
  if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it");
17
16
  return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
18
17
  };
19
- var _PrimerTextFieldElement_abortController;
18
+ var _PrimerTextFieldElement_abortController, _PrimerTextFieldElement_characterCounter;
20
19
  import '@github/auto-check-element';
21
20
  import { controller, target } from '@github/catalyst';
21
+ import { CharacterCounter } from './character_counter';
22
22
  let PrimerTextFieldElement = class PrimerTextFieldElement extends HTMLElement {
23
23
  constructor() {
24
24
  super(...arguments);
25
25
  _PrimerTextFieldElement_abortController.set(this, void 0);
26
+ _PrimerTextFieldElement_characterCounter.set(this, null);
26
27
  }
27
28
  connectedCallback() {
28
29
  __classPrivateFieldGet(this, _PrimerTextFieldElement_abortController, "f")?.abort();
@@ -40,9 +41,15 @@ let PrimerTextFieldElement = class PrimerTextFieldElement extends HTMLElement {
40
41
  const errorMessage = await event.detail.response.text();
41
42
  this.setError(errorMessage);
42
43
  }, { signal });
44
+ // Set up character limit tracking if present
45
+ if (this.characterLimitElement) {
46
+ __classPrivateFieldSet(this, _PrimerTextFieldElement_characterCounter, new CharacterCounter(this.inputElement, this.characterLimitElement, this.characterLimitSrElement), "f");
47
+ __classPrivateFieldGet(this, _PrimerTextFieldElement_characterCounter, "f").initialize(signal);
48
+ }
43
49
  }
44
50
  disconnectedCallback() {
45
51
  __classPrivateFieldGet(this, _PrimerTextFieldElement_abortController, "f")?.abort();
52
+ __classPrivateFieldGet(this, _PrimerTextFieldElement_characterCounter, "f")?.cleanup();
46
53
  }
47
54
  clearContents() {
48
55
  this.inputElement.value = '';
@@ -92,6 +99,7 @@ let PrimerTextFieldElement = class PrimerTextFieldElement extends HTMLElement {
92
99
  }
93
100
  };
94
101
  _PrimerTextFieldElement_abortController = new WeakMap();
102
+ _PrimerTextFieldElement_characterCounter = new WeakMap();
95
103
  __decorate([
96
104
  target
97
105
  ], PrimerTextFieldElement.prototype, "inputElement", void 0);
@@ -113,6 +121,12 @@ __decorate([
113
121
  __decorate([
114
122
  target
115
123
  ], PrimerTextFieldElement.prototype, "leadingSpinner", void 0);
124
+ __decorate([
125
+ target
126
+ ], PrimerTextFieldElement.prototype, "characterLimitElement", void 0);
127
+ __decorate([
128
+ target
129
+ ], PrimerTextFieldElement.prototype, "characterLimitSrElement", void 0);
116
130
  PrimerTextFieldElement = __decorate([
117
131
  controller
118
132
  ], PrimerTextFieldElement);
@@ -1,8 +1,7 @@
1
- /* eslint-disable custom-elements/expose-class-on-global */
2
-
3
1
  import '@github/auto-check-element'
4
2
  import type {AutoCheckErrorEvent, AutoCheckSuccessEvent} from '@github/auto-check-element'
5
3
  import {controller, target} from '@github/catalyst'
4
+ import {CharacterCounter} from './character_counter'
6
5
 
7
6
  declare global {
8
7
  interface HTMLElementEventMap {
@@ -20,8 +19,11 @@ export class PrimerTextFieldElement extends HTMLElement {
20
19
  @target validationErrorIcon: HTMLElement
21
20
  @target leadingVisual: HTMLElement
22
21
  @target leadingSpinner: HTMLElement
22
+ @target characterLimitElement: HTMLElement
23
+ @target characterLimitSrElement: HTMLElement
23
24
 
24
25
  #abortController: AbortController | null
26
+ #characterCounter: CharacterCounter | null = null
25
27
 
26
28
  connectedCallback(): void {
27
29
  this.#abortController?.abort()
@@ -48,16 +50,27 @@ export class PrimerTextFieldElement extends HTMLElement {
48
50
  },
49
51
  {signal},
50
52
  )
53
+
54
+ // Set up character limit tracking if present
55
+ if (this.characterLimitElement) {
56
+ this.#characterCounter = new CharacterCounter(
57
+ this.inputElement,
58
+ this.characterLimitElement,
59
+ this.characterLimitSrElement,
60
+ )
61
+ this.#characterCounter.initialize(signal)
62
+ }
51
63
  }
52
64
 
53
65
  disconnectedCallback() {
54
66
  this.#abortController?.abort()
67
+ this.#characterCounter?.cleanup()
55
68
  }
56
69
 
57
70
  clearContents() {
58
71
  this.inputElement.value = ''
59
72
  this.inputElement.focus()
60
- this.inputElement.dispatchEvent(new Event('input', { bubbles: true, cancelable: false }))
73
+ this.inputElement.dispatchEvent(new Event('input', {bubbles: true, cancelable: false}))
61
74
  }
62
75
 
63
76
  clearError(): void {
@@ -1,5 +1,7 @@
1
- <%= render(FormControl.new(input: @input)) do %>
2
- <%= content_tag(:div, **@field_wrap_arguments) do %>
3
- <%= builder.text_area(@input.name, **@input.input_arguments) %>
1
+ <primer-text-area>
2
+ <%= render(FormControl.new(input: @input)) do %>
3
+ <%= content_tag(:div, **@field_wrap_arguments) do %>
4
+ <%= builder.text_area(@input.name, **@input.input_arguments) %>
5
+ <% end %>
4
6
  <% end %>
5
- <% end %>
7
+ </primer-text-area>
@@ -13,7 +13,7 @@
13
13
  <%= builder.text_field(@input.name, **@input.input_arguments) %>
14
14
  <% end %>
15
15
  <% if @input.show_clear_button? %>
16
- <button type="button" id="<%= @input.clear_button_id %>" class="FormControl-input-trailingAction" aria-label="Clear" data-action="click:primer-text-field#clearContents">
16
+ <button type="button" id="<%= @input.clear_button_id %>" class="FormControl-input-trailingAction" aria-label="<%= @input.clear_button_label || 'Clear' %>" data-action="click:primer-text-field#clearContents">
17
17
  <%= render(Primer::Beta::Octicon.new(icon: :"x-circle-fill")) %>
18
18
  </button>
19
19
  <% end %>