primer_view_components 0.47.0 → 0.49.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 (66) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +24 -16
  3. data/app/assets/javascripts/components/primer/primer.d.ts +1 -0
  4. data/app/assets/javascripts/lib/primer/forms/character_counter.d.ts +41 -0
  5. data/app/assets/javascripts/lib/primer/forms/primer_text_area.d.ts +13 -0
  6. data/app/assets/javascripts/lib/primer/forms/primer_text_field.d.ts +2 -0
  7. data/app/assets/javascripts/primer_view_components.js +1 -1
  8. data/app/assets/javascripts/primer_view_components.js.map +1 -1
  9. data/app/assets/styles/primer_view_components.css +1 -1
  10. data/app/assets/styles/primer_view_components.css.map +1 -1
  11. data/app/components/primer/alpha/action_list.css +1 -1
  12. data/app/components/primer/alpha/action_list.css.map +1 -1
  13. data/app/components/primer/alpha/auto_complete/auto_complete.html.erb +8 -6
  14. data/app/components/primer/alpha/text_area.rb +1 -0
  15. data/app/components/primer/alpha/text_field.css +1 -1
  16. data/app/components/primer/alpha/text_field.css.map +1 -1
  17. data/app/components/primer/alpha/text_field.rb +1 -0
  18. data/app/components/primer/alpha/toggle_switch.html.erb +2 -2
  19. data/app/components/primer/alpha/tool_tip.js +12 -5
  20. data/app/components/primer/alpha/tool_tip.ts +14 -5
  21. data/app/components/primer/alpha/underline_nav.css +1 -1
  22. data/app/components/primer/alpha/underline_nav.css.map +1 -1
  23. data/app/components/primer/beta/auto_complete/item.html.erb +5 -4
  24. data/app/components/primer/beta/avatar.rb +10 -2
  25. data/app/components/primer/beta/avatar_stack.css +1 -1
  26. data/app/components/primer/beta/avatar_stack.css.json +5 -3
  27. data/app/components/primer/beta/avatar_stack.css.map +1 -1
  28. data/app/components/primer/beta/avatar_stack.pcss +12 -1
  29. data/app/components/primer/beta/avatar_stack.rb +6 -0
  30. data/app/components/primer/beta/button.css +1 -1
  31. data/app/components/primer/beta/button.css.map +1 -1
  32. data/app/components/primer/beta/spinner.html.erb +2 -2
  33. data/app/components/primer/primer.d.ts +1 -0
  34. data/app/components/primer/primer.js +1 -0
  35. data/app/components/primer/primer.ts +1 -0
  36. data/app/forms/text_area_with_character_limit_form.rb +13 -0
  37. data/app/forms/text_field_with_character_limit_form.rb +13 -0
  38. data/app/lib/primer/forms/caption.html.erb +16 -9
  39. data/app/lib/primer/forms/character_counter.d.ts +41 -0
  40. data/app/lib/primer/forms/character_counter.js +114 -0
  41. data/app/lib/primer/forms/character_counter.ts +129 -0
  42. data/app/lib/primer/forms/dsl/input.rb +23 -0
  43. data/app/lib/primer/forms/dsl/text_area_input.rb +12 -1
  44. data/app/lib/primer/forms/dsl/text_field_input.rb +10 -1
  45. data/app/lib/primer/forms/primer_text_area.d.ts +13 -0
  46. data/app/lib/primer/forms/primer_text_area.js +53 -0
  47. data/app/lib/primer/forms/primer_text_area.ts +37 -0
  48. data/app/lib/primer/forms/primer_text_field.d.ts +2 -0
  49. data/app/lib/primer/forms/primer_text_field.js +16 -2
  50. data/app/lib/primer/forms/primer_text_field.ts +16 -3
  51. data/app/lib/primer/forms/text_area.html.erb +6 -4
  52. data/app/lib/primer/forms/text_field.rb +8 -0
  53. data/lib/primer/view_components/version.rb +1 -1
  54. data/previews/primer/alpha/action_menu_preview/submitting_forms.html.erb +1 -1
  55. data/previews/primer/alpha/text_area_preview.rb +23 -2
  56. data/previews/primer/alpha/text_field_preview.rb +28 -7
  57. data/previews/primer/alpha/tree_view_preview/loading_failure.html.erb +53 -1
  58. data/previews/primer/beta/avatar_stack_preview.rb +9 -0
  59. data/previews/primer/forms_preview/text_area_with_character_limit_form.html.erb +3 -0
  60. data/previews/primer/forms_preview/text_field_with_character_limit_form.html.erb +3 -0
  61. data/previews/primer/forms_preview.rb +6 -0
  62. data/static/arguments.json +18 -0
  63. data/static/form_previews.json +10 -0
  64. data/static/info_arch.json +109 -0
  65. data/static/previews.json +91 -0
  66. metadata +14 -2
@@ -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
+ }
@@ -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_system_arguments
37
40
  # @param system_arguments [Hash] A hash of attributes passed to the underlying Rails builder methods. These options may mean something special depending on the type of input, otherwise they are emitted as HTML attributes. See the [Rails documentation](https://guides.rubyonrails.org/form_helpers.html) for more information. In addition, the usual Primer utility arguments are accepted in system arguments. For example, passing `mt: 2` will add the `mt-2` class to the input. See the Primer system arguments docs for details.
38
41
 
@@ -112,6 +115,7 @@ module Primer
112
115
 
113
116
  @ids = {}.tap do |id_map|
114
117
  id_map[:validation] = "validation-#{@base_id}" if supports_validation?
118
+ id_map[:character_limit_caption] = "character_limit-#{@base_id}" if character_limit?
115
119
  id_map[:caption] = "caption-#{@base_id}" if caption? || caption_template?
116
120
  end
117
121
 
@@ -196,6 +200,25 @@ module Primer
196
200
  form.render_caption_template(caption_template_name)
197
201
  end
198
202
 
203
+ def character_limit?
204
+ false
205
+ end
206
+
207
+ def character_limit_id
208
+ ids[:character_limit_caption]
209
+ end
210
+
211
+ def character_limit_target_prefix
212
+ case type
213
+ when :text_field
214
+ "primer-text-field"
215
+ when :text_area
216
+ "primer-text-area"
217
+ else
218
+ ""
219
+ end
220
+ end
221
+
199
222
  def valid?
200
223
  supports_validation? && validation_messages.empty? && !@invalid
201
224
  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
+ visually_hide_label inset monospace field_wrap_classes auto_check_src character_limit
10
10
  ]
11
11
  )
12
12
 
@@ -24,6 +24,11 @@ module Primer
24
24
  @inset = system_arguments.delete(:inset)
25
25
  @monospace = system_arguments.delete(:monospace)
26
26
  @auto_check_src = system_arguments.delete(:auto_check_src)
27
+ @character_limit = system_arguments.delete(:character_limit)
28
+
29
+ if @character_limit.present? && @character_limit.to_i <= 0
30
+ raise ArgumentError, "character_limit must be a positive integer, got #{@character_limit}"
31
+ end
27
32
 
28
33
  if @leading_visual
29
34
  @leading_visual[:classes] = class_names(
@@ -67,6 +72,10 @@ module Primer
67
72
  true
68
73
  end
69
74
 
75
+ def character_limit?
76
+ @character_limit.present?
77
+ end
78
+
70
79
  def validation_arguments
71
80
  if auto_check_src.present?
72
81
  super.merge(
@@ -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>
@@ -77,6 +77,14 @@ module Primer
77
77
  Primer::Beta::Truncate.new(**truncate_arguments).with_content(text)
78
78
  end
79
79
  end
80
+
81
+ def character_limit_validation_arguments
82
+ {
83
+ class: "FormControl-inlineValidation",
84
+ id: @input.character_limit_validation_id,
85
+ hidden: true
86
+ }
87
+ end
80
88
  end
81
89
  end
82
90
  end
@@ -5,7 +5,7 @@ module Primer
5
5
  module ViewComponents
6
6
  module VERSION
7
7
  MAJOR = 0
8
- MINOR = 47
8
+ MINOR = 49
9
9
  PATCH = 0
10
10
 
11
11
  STRING = [MAJOR, MINOR, PATCH].join(".")
@@ -8,7 +8,7 @@
8
8
  }) do |item| %>
9
9
  <% if params["time"] %>
10
10
  <% item.with_description.with_content("Last submitted at #{params["time"]}") %>
11
- <% elsif %>
11
+ <% else %>
12
12
  <% item.with_description.with_content("Not yet submitted") %>
13
13
  <% end %>
14
14
  <% end %>
@@ -16,6 +16,7 @@ module Primer
16
16
  # @param disabled toggle
17
17
  # @param invalid toggle
18
18
  # @param validation_message text
19
+ # @param character_limit number
19
20
  def playground(
20
21
  name: "my-text-area",
21
22
  id: "my-text-area",
@@ -26,7 +27,8 @@ module Primer
26
27
  full_width: true,
27
28
  disabled: false,
28
29
  invalid: false,
29
- validation_message: nil
30
+ validation_message: nil,
31
+ character_limit: nil
30
32
  )
31
33
  system_arguments = {
32
34
  name: name,
@@ -38,7 +40,8 @@ module Primer
38
40
  full_width: full_width,
39
41
  disabled: disabled,
40
42
  invalid: invalid,
41
- validation_message: validation_message
43
+ validation_message: validation_message,
44
+ character_limit: character_limit
42
45
  }
43
46
 
44
47
  render(Primer::Alpha::TextArea.new(**system_arguments))
@@ -93,6 +96,24 @@ module Primer
93
96
  def with_validation_message
94
97
  render(Primer::Alpha::TextArea.new(validation_message: "An error occurred!", name: "my-text-area", label: "Tell me about yourself"))
95
98
  end
99
+
100
+ # @label With character limit
101
+ # @snapshot interactive
102
+ def with_character_limit
103
+ render(Primer::Alpha::TextArea.new(character_limit: 10, name: "my-text-area", label: "Tell me about yourself"))
104
+ end
105
+
106
+ # @label With character limit, over limit
107
+ # @snapshot interactive
108
+ def with_character_limit_over_limit
109
+ render(Primer::Alpha::TextArea.new(character_limit: 10, name: "my-text-area", label: "Tell me about yourself", value: "This text is definitely over the limit."))
110
+ end
111
+
112
+ # @label With character limit and caption
113
+ # @snapshot
114
+ def with_character_limit_and_caption
115
+ render(Primer::Alpha::TextArea.new(character_limit: 100, caption: "With a caption.", name: "my-text-area", label: "Tell me about yourself"))
116
+ end
96
117
  #
97
118
  # @!endgroup
98
119
  end
@@ -24,6 +24,7 @@ module Primer
24
24
  # @param monospace toggle
25
25
  # @param leading_visual_icon octicon
26
26
  # @param leading_spinner toggle
27
+ # @param character_limit number
27
28
  def playground(
28
29
  name: "my-text-field",
29
30
  id: "my-text-field",
@@ -42,7 +43,8 @@ module Primer
42
43
  inset: false,
43
44
  monospace: false,
44
45
  leading_visual_icon: nil,
45
- leading_spinner: false
46
+ leading_spinner: false,
47
+ character_limit: nil
46
48
  )
47
49
  system_arguments = {
48
50
  name: name,
@@ -61,7 +63,8 @@ module Primer
61
63
  placeholder: placeholder,
62
64
  inset: inset,
63
65
  monospace: monospace,
64
- leading_spinner: leading_spinner
66
+ leading_spinner: leading_spinner,
67
+ character_limit: character_limit
65
68
  }
66
69
 
67
70
  if leading_visual_icon
@@ -197,7 +200,7 @@ module Primer
197
200
  end
198
201
 
199
202
  # @label With trailing label
200
- # @snapshot
203
+ # @snapshot
201
204
  def with_trailing_label
202
205
  render(Primer::Alpha::TextField.new(trailing_visual: { label: { text: "Hello" } }, name: "my-text-field-15", label: "My text field"))
203
206
  end
@@ -213,6 +216,24 @@ module Primer
213
216
  def with_validation_message
214
217
  render(Primer::Alpha::TextField.new(validation_message: "An error occurred!", name: "my-text-field-17", label: "My text field"))
215
218
  end
219
+
220
+ # @label With character limit
221
+ # @snapshot interactive
222
+ def with_character_limit
223
+ render(Primer::Alpha::TextField.new(character_limit: 10, name: "my-text-field-18", label: "Username"))
224
+ end
225
+
226
+ # @label With character limit, over limit
227
+ # @snapshot interactive
228
+ def with_character_limit_over_limit
229
+ render(Primer::Alpha::TextField.new(character_limit: 10, name: "my-text-field-19", label: "Tell me about yourself", value: "This text is definitely over the limit."))
230
+ end
231
+
232
+ # @label With character limit and caption
233
+ # @snapshot
234
+ def with_character_limit_and_caption
235
+ render(Primer::Alpha::TextField.new(character_limit: 20, caption: "Choose a unique username.", name: "my-text-field-20", label: "Username"))
236
+ end
216
237
  #
217
238
  # @!endgroup
218
239
 
@@ -220,24 +241,24 @@ module Primer
220
241
  #
221
242
  # @label Auto check request ok
222
243
  def with_auto_check_ok
223
- render(Primer::Alpha::TextField.new(auto_check_src: UrlHelpers.primer_view_components.example_check_ok_path, name: "my-text-field-18", label: "My text field"))
244
+ render(Primer::Alpha::TextField.new(auto_check_src: UrlHelpers.primer_view_components.example_check_ok_path, name: "my-text-field-21", label: "My text field"))
224
245
  end
225
246
 
226
247
  # @label Auto check request accepted
227
248
  def with_auto_check_accepted
228
- render(Primer::Alpha::TextField.new(auto_check_src: UrlHelpers.primer_view_components.example_check_accepted_path, name: "my-text-field-19", label: "My text field"))
249
+ render(Primer::Alpha::TextField.new(auto_check_src: UrlHelpers.primer_view_components.example_check_accepted_path, name: "my-text-field-22", label: "My text field"))
229
250
  end
230
251
 
231
252
  # @label Auto check request error
232
253
  def with_auto_check_error
233
- render(Primer::Alpha::TextField.new(auto_check_src: UrlHelpers.primer_view_components.example_check_error_path, name: "my-text-field-20", label: "My text field"))
254
+ render(Primer::Alpha::TextField.new(auto_check_src: UrlHelpers.primer_view_components.example_check_error_path, name: "my-text-field-23", label: "My text field"))
234
255
  end
235
256
  #
236
257
  # @!endgroup
237
258
 
238
259
  # @label With data target attribute
239
260
  def with_data_target
240
- render(Primer::Alpha::TextField.new(name: "my-text-field", label: "My text field", data: { target: "custom-component.inputElement" }))
261
+ render(Primer::Alpha::TextField.new(name: "my-text-field-24", label: "My text field", data: { target: "custom-component.inputElement" }))
241
262
  end
242
263
  #
243
264
  # @!endgroup