@primer/view-components 0.48.0 → 0.49.0-rc.136ed2d2

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.
@@ -22,6 +22,7 @@ import './beta/relative_time';
22
22
  import './alpha/tab_container';
23
23
  import '../../lib/primer/forms/primer_multi_input';
24
24
  import '../../lib/primer/forms/primer_text_field';
25
+ import '../../lib/primer/forms/primer_text_area';
25
26
  import '../../lib/primer/forms/toggle_switch_input';
26
27
  import './alpha/action_menu/action_menu_element';
27
28
  import './alpha/select_panel_element';
@@ -22,6 +22,7 @@ import './beta/relative_time';
22
22
  import './alpha/tab_container';
23
23
  import '../../lib/primer/forms/primer_multi_input';
24
24
  import '../../lib/primer/forms/primer_text_field';
25
+ import '../../lib/primer/forms/primer_text_area';
25
26
  import '../../lib/primer/forms/toggle_switch_input';
26
27
  import './alpha/action_menu/action_menu_element';
27
28
  import './alpha/select_panel_element';
@@ -0,0 +1,41 @@
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 declare class CharacterCounter {
6
+ private inputElement;
7
+ private characterLimitElement;
8
+ private characterLimitSrElement;
9
+ private SCREEN_READER_DELAY;
10
+ private announceTimeout;
11
+ private isInitialLoad;
12
+ constructor(inputElement: HTMLInputElement | HTMLTextAreaElement, characterLimitElement: HTMLElement, characterLimitSrElement: HTMLElement);
13
+ /**
14
+ * Initialize character counting by setting up event listener and initial count
15
+ */
16
+ initialize(signal?: AbortSignal): void;
17
+ /**
18
+ * Clean up any pending timeouts
19
+ */
20
+ cleanup(): void;
21
+ /**
22
+ * Pluralizes a word based on the count
23
+ */
24
+ private pluralize;
25
+ /**
26
+ * Update the character count display and validation state
27
+ */
28
+ private updateCharacterCount;
29
+ /**
30
+ * Announce character count to screen readers with debouncing
31
+ */
32
+ private announceToScreenReader;
33
+ /**
34
+ * Set error when character limit is exceeded
35
+ */
36
+ private setError;
37
+ /**
38
+ * Clear error when back under character limit
39
+ */
40
+ private clearError;
41
+ }
@@ -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,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
+ }
@@ -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);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@primer/view-components",
3
- "version": "0.48.0",
3
+ "version": "0.49.0-rc.136ed2d2",
4
4
  "description": "ViewComponents for the Primer Design System",
5
5
  "main": "app/assets/javascripts/primer_view_components.js",
6
6
  "module": "app/components/primer/primer.js",
@@ -2846,6 +2846,12 @@
2846
2846
  "default": "N/A",
2847
2847
  "description": "When set to `true`, the field will take up all the horizontal space allowed by its container. Defaults to `true`."
2848
2848
  },
2849
+ {
2850
+ "name": "character_limit",
2851
+ "type": "Number",
2852
+ "default": "N/A",
2853
+ "description": "Optional character limit for the input. If provided, a character counter will be displayed below the input."
2854
+ },
2849
2855
  {
2850
2856
  "name": "name",
2851
2857
  "type": "String",
@@ -2976,6 +2982,12 @@
2976
2982
  "default": "N/A",
2977
2983
  "description": "When set to `true`, the field will take up all the horizontal space allowed by its container. Defaults to `true`."
2978
2984
  },
2985
+ {
2986
+ "name": "character_limit",
2987
+ "type": "Number",
2988
+ "default": "N/A",
2989
+ "description": "Optional character limit for the input. If provided, a character counter will be displayed below the input."
2990
+ },
2979
2991
  {
2980
2992
  "name": "name",
2981
2993
  "type": "String",
@@ -107,6 +107,16 @@
107
107
  "preview_path": "primer/forms/auto_complete_form",
108
108
  "name": "auto_complete_form",
109
109
  "snapshot": "true"
110
+ },
111
+ {
112
+ "preview_path": "primer/forms/text_area_with_character_limit_form",
113
+ "name": "text_area_with_character_limit_form",
114
+ "snapshot": "true"
115
+ },
116
+ {
117
+ "preview_path": "primer/forms/text_field_with_character_limit_form",
118
+ "name": "text_field_with_character_limit_form",
119
+ "snapshot": "true"
110
120
  }
111
121
  ]
112
122
  }
@@ -8584,6 +8584,12 @@
8584
8584
  "default": "N/A",
8585
8585
  "description": "When set to `true`, the field will take up all the horizontal space allowed by its container. Defaults to `true`."
8586
8586
  },
8587
+ {
8588
+ "name": "character_limit",
8589
+ "type": "Number",
8590
+ "default": "N/A",
8591
+ "description": "Optional character limit for the input. If provided, a character counter will be displayed below the input."
8592
+ },
8587
8593
  {
8588
8594
  "name": "name",
8589
8595
  "type": "String",
@@ -8812,6 +8818,45 @@
8812
8818
  "color-contrast"
8813
8819
  ]
8814
8820
  }
8821
+ },
8822
+ {
8823
+ "preview_path": "primer/alpha/text_area/with_character_limit",
8824
+ "name": "with_character_limit",
8825
+ "snapshot": "interactive",
8826
+ "skip_rules": {
8827
+ "wont_fix": [
8828
+ "region"
8829
+ ],
8830
+ "will_fix": [
8831
+ "color-contrast"
8832
+ ]
8833
+ }
8834
+ },
8835
+ {
8836
+ "preview_path": "primer/alpha/text_area/with_character_limit_over_limit",
8837
+ "name": "with_character_limit_over_limit",
8838
+ "snapshot": "interactive",
8839
+ "skip_rules": {
8840
+ "wont_fix": [
8841
+ "region"
8842
+ ],
8843
+ "will_fix": [
8844
+ "color-contrast"
8845
+ ]
8846
+ }
8847
+ },
8848
+ {
8849
+ "preview_path": "primer/alpha/text_area/with_character_limit_and_caption",
8850
+ "name": "with_character_limit_and_caption",
8851
+ "snapshot": "true",
8852
+ "skip_rules": {
8853
+ "wont_fix": [
8854
+ "region"
8855
+ ],
8856
+ "will_fix": [
8857
+ "color-contrast"
8858
+ ]
8859
+ }
8815
8860
  }
8816
8861
  ],
8817
8862
  "subcomponents": []
@@ -8842,6 +8887,12 @@
8842
8887
  "default": "N/A",
8843
8888
  "description": "When set to `true`, the field will take up all the horizontal space allowed by its container. Defaults to `true`."
8844
8889
  },
8890
+ {
8891
+ "name": "character_limit",
8892
+ "type": "Number",
8893
+ "default": "N/A",
8894
+ "description": "Optional character limit for the input. If provided, a character counter will be displayed below the input."
8895
+ },
8845
8896
  {
8846
8897
  "name": "name",
8847
8898
  "type": "String",
@@ -9288,6 +9339,45 @@
9288
9339
  ]
9289
9340
  }
9290
9341
  },
9342
+ {
9343
+ "preview_path": "primer/alpha/text_field/with_character_limit",
9344
+ "name": "with_character_limit",
9345
+ "snapshot": "interactive",
9346
+ "skip_rules": {
9347
+ "wont_fix": [
9348
+ "region"
9349
+ ],
9350
+ "will_fix": [
9351
+ "color-contrast"
9352
+ ]
9353
+ }
9354
+ },
9355
+ {
9356
+ "preview_path": "primer/alpha/text_field/with_character_limit_over_limit",
9357
+ "name": "with_character_limit_over_limit",
9358
+ "snapshot": "interactive",
9359
+ "skip_rules": {
9360
+ "wont_fix": [
9361
+ "region"
9362
+ ],
9363
+ "will_fix": [
9364
+ "color-contrast"
9365
+ ]
9366
+ }
9367
+ },
9368
+ {
9369
+ "preview_path": "primer/alpha/text_field/with_character_limit_and_caption",
9370
+ "name": "with_character_limit_and_caption",
9371
+ "snapshot": "true",
9372
+ "skip_rules": {
9373
+ "wont_fix": [
9374
+ "region"
9375
+ ],
9376
+ "will_fix": [
9377
+ "color-contrast"
9378
+ ]
9379
+ }
9380
+ },
9291
9381
  {
9292
9382
  "preview_path": "primer/alpha/text_field/with_auto_check_ok",
9293
9383
  "name": "with_auto_check_ok",
@@ -6989,6 +6989,45 @@
6989
6989
  "color-contrast"
6990
6990
  ]
6991
6991
  }
6992
+ },
6993
+ {
6994
+ "preview_path": "primer/alpha/text_area/with_character_limit",
6995
+ "name": "with_character_limit",
6996
+ "snapshot": "interactive",
6997
+ "skip_rules": {
6998
+ "wont_fix": [
6999
+ "region"
7000
+ ],
7001
+ "will_fix": [
7002
+ "color-contrast"
7003
+ ]
7004
+ }
7005
+ },
7006
+ {
7007
+ "preview_path": "primer/alpha/text_area/with_character_limit_over_limit",
7008
+ "name": "with_character_limit_over_limit",
7009
+ "snapshot": "interactive",
7010
+ "skip_rules": {
7011
+ "wont_fix": [
7012
+ "region"
7013
+ ],
7014
+ "will_fix": [
7015
+ "color-contrast"
7016
+ ]
7017
+ }
7018
+ },
7019
+ {
7020
+ "preview_path": "primer/alpha/text_area/with_character_limit_and_caption",
7021
+ "name": "with_character_limit_and_caption",
7022
+ "snapshot": "true",
7023
+ "skip_rules": {
7024
+ "wont_fix": [
7025
+ "region"
7026
+ ],
7027
+ "will_fix": [
7028
+ "color-contrast"
7029
+ ]
7030
+ }
6992
7031
  }
6993
7032
  ]
6994
7033
  },
@@ -7284,6 +7323,45 @@
7284
7323
  ]
7285
7324
  }
7286
7325
  },
7326
+ {
7327
+ "preview_path": "primer/alpha/text_field/with_character_limit",
7328
+ "name": "with_character_limit",
7329
+ "snapshot": "interactive",
7330
+ "skip_rules": {
7331
+ "wont_fix": [
7332
+ "region"
7333
+ ],
7334
+ "will_fix": [
7335
+ "color-contrast"
7336
+ ]
7337
+ }
7338
+ },
7339
+ {
7340
+ "preview_path": "primer/alpha/text_field/with_character_limit_over_limit",
7341
+ "name": "with_character_limit_over_limit",
7342
+ "snapshot": "interactive",
7343
+ "skip_rules": {
7344
+ "wont_fix": [
7345
+ "region"
7346
+ ],
7347
+ "will_fix": [
7348
+ "color-contrast"
7349
+ ]
7350
+ }
7351
+ },
7352
+ {
7353
+ "preview_path": "primer/alpha/text_field/with_character_limit_and_caption",
7354
+ "name": "with_character_limit_and_caption",
7355
+ "snapshot": "true",
7356
+ "skip_rules": {
7357
+ "wont_fix": [
7358
+ "region"
7359
+ ],
7360
+ "will_fix": [
7361
+ "color-contrast"
7362
+ ]
7363
+ }
7364
+ },
7287
7365
  {
7288
7366
  "preview_path": "primer/alpha/text_field/with_auto_check_ok",
7289
7367
  "name": "with_auto_check_ok",