1/** 2 * @file BasicCheckbox.js 3 * @description This is a custom web component that represents a simple checkbox or radio button with customizable text. 4 * 5 * @class CheckboxElement 6 * @extends HTMLElement 7 * 8 * @classdesc A custom HTML element that provides a checkbox or radio button interface with customizable text. 9 * This component features a user-friendly UI and supports dynamic changes to the checkbox or radio button state. 10 * 11 * @example 12 * <checkbox-element label="I agree to the terms and conditions"></checkbox-element> 13 * 14 * @example 15 * <div class="radio-buttons-group"> 16 * <checkbox-element label="Option 1" radio name="group1"></checkbox-element> 17 * <checkbox-element label="Option 2" radio name="group1" checked></checkbox-element> 18 * <checkbox-element label="Option 3" radio name="group1"></checkbox-element> 19 * </div> 20 * 21 * @attribute {string} label - The text displayed next to the checkbox or radio button. 22 * @attribute {boolean} checked - If present, the checkbox or radio button is checked. 23 * @attribute {boolean} disabled - If present, the checkbox or radio button is disabled. 24 * @attribute {boolean} radio - If present, the element behaves like a radio button, allowing only one selection per group. 25 * @attribute {string} name - Attribute to group radio buttons together when creating them. 26 * @attribute {string} value - Must be used in combination with the "form-checkbox" class in order to send form data 27 * @attribute {boolean} error - Puts the checkbox into error state (red box + optional message) 28 * @attribute {string} error-message - Message shown under the checkbox when `error` is set 29 * @attribute {boolean} disable-select-all 30 * @attribute {boolean} max-content - If present, the checkbox container width will be changed from "100%" to "max-content". 31 * 32 * @cssprop --label-font-size - font size of the label text, default 1rem 33 * @cssprop --label-color - text/fill color of the label text, default #484848 34 * @cssprop --label-line-height - line height of the label text, default 1.5 35 * Disables the built-in automatic select-all synchronization. 36 * Use this when selection state is managed by a parent component (e.g. tables with pagination, virtualized rows, or bulk actions). 37 * Required when selection is managed by a parent component. 38 * Tables often re-render rows (replace DOM), making checkbox-level DOM queries 39 * for auto select-all unreliable. Parent should orchestrate selection. 40 * 41 * @method toggleCheck - Toggles the checked state of the checkbox or radio button. 42 * @method handleChange - Handles changes to the checkbox or radio button state and dispatches a 'change' event. 43 * @method handleRadio - Handles radio button behavior by deselecting other radio buttons in the group. 44 * @method isChecked - Returns the checked state of the checkbox or radio button. 45 * @method setError(message) - Puts the checkbox into error state with an optional message 46 * @method clearError() - Clears error state and hides the error message 47 * 48 * @event change - Dispatched when the checked state of the checkbox or radio button changes. 49 */ 50import { WebComponent } from '../../../js/Components/Base/WebComponent.js'; 51 52// Static markup: same structure as before, just parsed once per class (via 53// WebComponent.getTemplateClone) instead of built with createElement calls on every 54// instance. display:flex/align-items:flex-start are already set by the .checkbox-container 55// rule below, so the inline styles previously set in JS were redundant and are dropped. 56const TEMPLATE_HTML = ` 57 <label class="checkbox-container"> 58 <input type="checkbox" class="checkbox-input"> 59 <span class="custom-checkbox"></span> 60 <span class="checkbox-label ml-half huge text-dark" part="checkbox-label"></span> 61 </label> 62 <div class="checkbox-error-message" aria-live="polite"></div> 63`; 64 65const COMPONENT_CSS = ` 66 :host { 67 user-select: none; 68 -webkit-user-select: none; 69 } 70 .checkbox-container { 71 cursor: pointer; 72 user-select: none; 73 display: flex; 74 align-items: var(--checkbox-align, flex-start); 75 gap: 8px; 76 width: 100%; 77 } 78 /* Label-less checkbox (table select-all, row select): with
78nothing to 79 sit beside the box, the gap and the empty label span still widened the 80 row, so every ring drawn around it -- hover and keyboard focus alike -- 81 enclosed that dead space instead of the box. */ 82 .checkbox-container.no-label { 83 gap: 0; 84 width: max-content; 85 } 86 .checkbox-container.no-label .checkbox-label { display: none; } 87 .checkbox-input { 88 position: absolute; 89 opacity: 0; 90 width: 0; 91 height: 0; 92 } 93 .custom-checkbox { 94 width: 20px; 95 height: 20px; 96 background-color: #fff; 97 border: 1px solid #ccc; 98 border-radius: 4px; 99 display: inline-block; 100 position: relative; 101 flex-shrink: 0; 102 } 103 104 /* ============================= */ 105 /* Hover states (box only) */ 106 /* ============================= */ 107 108 /* Hover â unchecked + enabled Visual: dark border, white background. */ 109 .checkbox-input:not(:checked):not(:indeterminate):not(:disabled) + .custom-checkbox:hover { 110 background-color: #fff; 111 border: 1px solid #3F4242; 112 } 113 114 /* Hover â checked + enabled Visual: lighter green fill plus a darker 115 border. The fill shift alone reads too close to the checked color 116 (#038580 vs #08A6A0), so the host-level halo (below) does the heavy 117 lifting and the border change reinforces the box itself. */ 118 .checkbox-input:checked:not(:disabled) + .custom-checkbox:hover { 119 background-color: #08A6A0; 120 border: 1px solid #026E6A; 121 } 122 123 /* Hover â indeterminate + enabled Visual: matches checked hover. */ 124 .checkbox-input:indeterminate:not(:disabled) + .custom-checkbox:hover { 125 background-color: #08A6A0; 126 border: 1px solid #026E6A; 127 } 128 129 /* ============================= */ 130 /* Hover ring around full row */ 131 /* ============================= */ 132 133 /* Hover halo wraps the checkbox AND its label so the affordance is 134 obvious â hovering anywhere on the row lights up an outline around 135 the whole item. :has() narrows by state so disabled boxes never get 136 a ring. */ 137 :host(:has(.checkbox-input:not(:checked):not(:indeterminate):not(:disabled))):hover { 138 outline: 1.5px solid #3F4242; 139 outline-offset: 3px; 140 border-radius: 4px; 141 box-shadow: 0 0 0 4px rgba(63, 66, 66, 0.15); 142 } 143 :host(:has(.checkbox-input:checked:not(:disabled))):hover, 144 :host(:has(.checkbox-input:indeterminate:not(:disabled))):hover { 145 outline: 1.5px solid #026E6A; 146 outline-offset: 3px; 147 border-radius: 4px; 148 box-shadow: 0 0 0 4px rgba(3, 133, 128, 0.25); 149 } 150 151 /* Hover â disabled (safety override) Visual: no hover effect */ 152 .checkbox-input:disabled + .custom-checkbox:hover { 153 background-color: #D8DBDB; 154 border: 1px solid #D8DBDB; 155 } 156 157 /* ============================= */ 158 /* Selected states (box fill) */ 159 /* ============================= */ 160 161 /* Checked OR indeterminate (enabled or disabled) Visual: dark green fill */ 162 .checkbox-input:checked + .custom-checkbox, .checkbox-input:indeterminate + .custom-checkbox{ 163 background-color: #038580; 164 border: 1px solid #038580; 165 } 166 167 /* ============================= */ 168 /* Check mark */ 169 /* ============================= */ 170 171 /* Checked â enabled Visual: white check mark */ 172 .checkbox-input:checked + .custom-checkbox::after { 173 content: ''; 174 position: absolute; 175 top: 3px; 176 left: 7px; 177 width: 5px; 178 height: 10px; 179 border: solid white; 180 border-width: 0 2px 2px 0; 181 transform: rotate(45deg); 182 z-index: 1; 183 } 184 185 /* Checked â disabled Visual: gray check mark */ 186 .checkbox-input:checked:disabled + .custom-checkbox::after { 187 border-color: #9DA6A5; 188 } 189 190 191 192 /* ============================= */ 193 /* Indeterminate mark */ 194 /* ============================= */ 195 196 /* Indeterminate â enabled Visual: white horizontal dash */ 197 .checkbox-input:indeterminate + .custom-checkbox::after { 198 content: ''; 199 position: absolute; 200 top: 9px; 201 left: 4px; 202 width: 12px; 203 height: 2px; 204 background: white; 205 border-radius: 2px; 206 z-index: 1; 207 } 208 209 /* Indeterminate â disabled Visual: gray horizontal dash */ 210 .checkbox-input:indeterminate:disabled + .custom-checkbox::after { 211 border: 1px solid #9DA6A5; 212 background: #9DA6A5; 213 } 214 215 /* ============================= */ 216 /* Disabled state (box) */ 217 /* ============================= */ 218 219 /* Disabled checkbox Visual: gray fill, gray border, no interaction */ 220 .checkbox-input:disabled + .custom-checkbox { 221 background-color: #D8DBDB; 222 border: 1px solid #D8DBDB;
223 cursor: not-allowed; 224 } 225 226 /* ============================= */ 227 /* Error state (box + message) */ 228 /* ============================= */ 229 230 /* Error â unchecked Visual: red border, white background */ 231 :host([error]) .checkbox-input:not(:checked):not(:indeterminate):not(:disabled) + .custom-checkbox { 232 background-color: #fff; 233 border: 1.5px solid #D63952; 234 } 235 236 /* Error â checked/indeterminate Visual: red border stays visible around the filled box */ 237 :host([error]) .checkbox-input:checked:not(:disabled) + .custom-checkbox, 238 :host([error]) .checkbox-input:indeterminate:not(:disabled) + .custom-checkbox { 239 border: 1.5px solid #D63952; 240 box-shadow: 0 0 0 1px #D63952; 241 } 242 243 .checkbox-error-message { 244 color: #D63952; 245 font-size: 13px; 246 line-height: 1.4; 247 margin-top: 4px; 248 } 249 250 /* ============================= */ 251 /* Keyboard focus indicator */ 252 /* ============================= */ 253 254 /* Ring wraps the entire row (checkbox + label) when focus arrives via 255 Tab/arrow keys. Outline goes on the inner .checkbox-container (a 256 flex block with defined dimensions) rather than the host â the host 257 is display: inline by default, so a host-level outline draws at the 258 inline content's bounding box and produces stray fragments. The 259 keyboard-focused class is added from JS because :focus-visible inside 260 :has() doesn't reactively update on the host across all Chromium 261 versions. Color stays brand teal for UI consistency. */ 262 :host(.keyboard-focused) .checkbox-container { 263 outline: 2px solid #0389A0; 264 outline-offset: 3px; 265 border-radius: 4px; 266 box-shadow: 0 0 0 4px rgba(3, 137, 160, 0.2); 267 } 268 .checkbox-input:focus-visible + .custom-checkbox { 269 border-color: #0389A0; 270 } 271 272 /* ============================= */ 273 /* Label text */ 274 /* ============================= */ 275 276 /* Checkbox label text */ 277 .checkbox-label { 278 font-family: 'Plus Jakarta Sans', 'Roboto', sans-serif; 279 font-size: var(--label-font-size, 1rem); 280 font-weight: 400; 281 color: var(--label-color, #484848); 282 line-height: var(--label-line-height, 1.5); 283 fill: var(--label-color, #484848); 284 min-width: 0; 285 white-space: normal; 286 overflow-wrap: anywhere; 287 word-break: break-word; 288 } 289 `; 290 291class CheckboxElement extends HTMLElement { 292 constructor() { 293 super(); 294 this.attachShadow({ mode: 'open' }); 295 296 this.shadowRoot.appendChild(WebComponent.getTemplateClone(CheckboxElement, TEMPLATE_HTML)); 297 298 const sheet = WebComponent.getComponentStyleSheet(CheckboxElement, COMPONENT_CSS); 299 if (sheet) { 300 this.shadowRoot.adoptedStyleSheets = [sheet]; 301 } else { 302 const style = document.createElement('style'); 303 style.textContent = COMPONENT_CSS; 304 this.shadowRoot.appendChild(style); 305 } 306 307 this.container = this.shadowRoot.querySelector('.checkbox-container'); 308 this.checkbox = this.shadowRoot.querySelector('.checkbox-input'); 309 this.customCheckbox = this.shadowRoot.querySelector('.custom-checkbox'); 310 this.labelText = this.shadowRoot.querySelector('.checkbox-label'); 311 this.labelText.innerHTML = this.getAttribute('label') || ''; 312 this._syncLabelPresence(); 313 314 // Error message shown below the row when `error` is set (accessibility.md: error container 315 // adjacent to the input, inside an aria-live region so it's announced without a full reload). 316 // The element itself is static markup (TEMPLATE_HTML); only the id needs to be unique per instance. 317 this._errorId = 'checkbox-error-' + Math.random().toString(36).slice(2); 318 this.errorMessageEl = this.shadowRoot.querySelector('.checkbox-error-message'); 319 this.errorMessageEl.id = this._errorId; 320 this.errorMessageEl.style.display = 'none'; 321 } 322 323 /** 324 * Called when the element is added to the document's DOM. 325 * Initializes the checkbox or radio button state and adds event listeners. 326 */ 327 connectedCallback() { 328 if (this.hasAttribute('checked')) { 329 this.checkbox.checked = true; 330 } 331 if (this.hasAttribute('disabled')) { 332 this.checkbox.disabled = true; 333 } 334 if (this.hasAttribute('indeterminate')) { 335 this.checkbox.indeterminate = true; 336 } 337 if (this.hasAttribute('radio')) { 338 this.checkbox.type = 'radio'; 339 // Set a common name to ensure only one radio can be selected 340 if (!this.hasAttribute('name')) { 341 this.checkbox.name = 'default-radio-group'; 342 } else { 343 this.checkbox.name = this.getAttribute('name'); 344 } 345 } 346 347 //Variation on style for a wider label that takes all available width and not just the max-content 348 let wider = this.getAttribute('wider') ?? false; 349 if(wider){ 350 this.container.style.width = "100%"; 351 this.labelText.style.width = "100%"; 352 } 353 354 // Handle max-content attribute 355 if (this.hasAttribute('max-content')) { 356 this.container.style.width = 'max-content'; 357 } 358 359 // Only create hidden input if it has class "form-checkbox" 360 if ( 361 this.classList.contains('form-checkbox') && 362 this.hasAttribute('name') && 363 this.checkbox.checked 364 ) { 365 this.hiddenInput = document.createElement('input'); 366 this.hiddenInput.type = 'hidden'; 367 this.hiddenInput.name = this.getAttribute('name'); 368 this.hiddenInput.value = this.getAttribute('value') || 'on'; 369 this.closest('form')?.appendChild(this.hiddenInput); 370 } 371 372 this.checkbox.addEventListener('change', () => this.handleChange()); 373 374 // Enter toggles the checkbox to match Space; native checkbox inputs respond 375 // to Space but not Enter, and keyboard users expect both to activate. 376 this.checkbox.addEventListener('keydown', (e) => { 377 if (e.key === 'Enter' && !this.checkbox.disabled) { 378 e.preventDefault(); 379 this.toggleCheck(); 380 } 381 }); 382 383 // Mirror :focus-visible into a host class so the wrap-around ring fires 384 // reliably (see CSS note above). matches(':focus-visible') tells us focus 385 // arrived via keyboard, not mouse â so clicking the checkbox stays clean. 386 this.checkbox.addEventListener('focus', () => { 387 if (this.checkbox.matches(':focus-visible')) { 388 this.classList.add('keyboard-focused'); 389 } 390 }); 391 this.checkbox.addEventListener('blur', () => { 392 this.classList.remove('keyboard-focused'); 393 }); 394 } 395 396 /** 397 * Observed attributes for the CheckboxElement component. 398 * @returns {Array<string>} The list of attribute names to observe. 399 */ 400 static get observedAttributes() { 401 return ['label', 'checked', 'disabled', 'indeterminate', 'radio', 'name', 'value', 'max-content', 'error', 'error-message']; 402 } 403 404 /** 405 * Called when an observed attribute has been added, removed, updated, or replaced. 406 * Handles updates to the label, checked, disabled, and radio attributes. 407 * 408 * @param {string} name - The name of the attribute that changed. 409 * @param {string} oldValue - The old value of the attribute. 410 * @param {string} newValue - The new value of the attribute. 411 */ 412 attributeChangedCallback(name, oldValue, newValue) { 413 if (name === 'label' && newValue !== oldValue) { 414 this.labelText.innerHTML = newValue; 415 this._syncLabelPresence(); 416 } else if (name === 'checked') { 417 this.checkbox.checked = newValue !== null; 418 } else if (name === 'disabled') { 419 this.checkbox.disabled = newValue !== null; 420 } else if (name === 'indeterminate') { 421 this.checkbox.indeterminate = newValue !== null; 422 } else if (name === 'radio') { 423 this.checkbox.type = newValue !== null ? 'radio' : 'checkbox'; 424 } else if (name === 'name') { 425 this.checkbox.name = newValue; 426 } else if (name === 'value') { 427 this.checkbox.value = newValue; 428 } else if (name === 'max-content') { 429 this.container.style.width = newValue !== null ? 'max-content' : ''; 430 } else if (name === 'error' || name === 'error-message') { 431 this._updateError(); 432 } 433 } 434 435 /** Marks the row label-less so the hover and focus rings hug the box. */ 436 _syncLabelPresence() { 437 this.container.classList.toggle('no-label', !this.labelText.innerHTML.trim()); 438 } 439 440 /** 441 * Syncs the error box styling, message text and aria-describedby from the 442 * `error` / `error-message` attributes. 443 */ 444 _updateError() { 445 const hasError = this.hasAttribute('error'); 446 const message = this.getAttribute('error-message') || ''; 447 this.errorMessageEl.textContent = message; 448 this.errorMessageEl.style.display = hasError && message ? '' : 'none'; 449 if (hasError) { 450 this.checkbox.setAttribute('aria-describedby', this._errorId); 451 } else { 452 this.checkbox.removeAttribute('aria-describedby'); 453 } 454 } 455 456 /** 457 * Puts the checkbox into error state (red box) with an optional message shown below it. 458 * @param {string} [message] - Error text; omit for just the red box with no message. 459 */ 460 setError(message) { 461 if (message) this.setAttribute('error-message', message); 462 this.setAttribute('error', ''); 463 } 464 465 /** 466 * Clears error state and hides the error message. 467 */ 468 clearError() { 469 this.removeAttribute('error'); 470 this.removeAttribute('error-message'); 471 } 472 473 /** 474 * Toggles the checked state of the checkbox or radio button. 475 */ 476 toggleCheck(state = null) { 477 // If a specific state is provided, set it; otherwise, toggle 478 if (state !== null) { 479 this.checkbox.checked = state; 480 } else { 481 this.checkbox.checked = !this.checkbox.checked; 482 } 483 484 this.handleChange(); 485 } 486 487 /** 488 * Handles changes to the checkbox or radio button state and dispatches a 'change' event. 489 */ 490 handleChange() { 491 // Handle radio button behavior 492 if (this.checkbox.type === 'radio' && this.checkbox.checked) { 493 this.handleRadio(); 494 } 495 496 /**
497 * Automatic select-all behavior is disabled when `disable-select-all` 498 * is present. 499 * 500 * This is required when selection state is managed by a parent component 501 * (e.g. a data table handling pagination, bulk actions, or cross-page 502 * selection). 503 */ 504 if (!this.hasAttribute('disable-select-all')) { 505 if (this.classList.contains('select-all-option') || this.classList.contains('select-option')) { 506 this.handleSelectAll(); 507 } 508 } 509 510 const name = this.getAttribute('name'); 511 const value = this.getAttribute('value') || 'on'; 512 513 //only append hidden input if checkbox is checked 514 if (this.classList.contains('form-checkbox') && name) { 515 if (this.checkbox.checked) { 516 if (!this.hiddenInput) { 517 this.hiddenInput = document.createElement('input'); 518 this.hiddenInput.type = 'hidden'; 519 this.hiddenInput.name = name; 520 this.hiddenInput.value = value; 521 this.closest('form')?.appendChild(this.hiddenInput); 522 } else { 523 this.hiddenInput.value = value; 524 } 525 } else { 526 if (this.hiddenInput) { 527 this.hiddenInput.remove(); 528 this.hiddenInput = null; 529 } 530 } 531 } 532 533 // Sync the checked attribute with the internal checkbox state 534 if (this.checkbox.checked) { 535 this.setAttribute('checked', ''); 536 } else { 537 this.removeAttribute('checked'); 538 } 539 540 // Dispatch a 'change' event with the checked state 541 this.dispatchEvent(new CustomEvent('change', { 542 detail: { checked: this.checkbox.checked }, 543 bubbles: true 544 })); 545 } 546 547 548 /** 549 * Handles radio button behavior by deselecting other radio buttons in the group. 550 */ 551 handleRadio() { 552 const root = this.getRootNode(); 553 554 const radios = root.querySelectorAll(`checkbox-element[radio][name="${this.checkbox.name}"]`); 555 556 //Uncheck all other radio buttons in the group 557 radios.forEach((radio) => { 558 if (radio !== this && radio.isChecked()) { 559 radio.toggleCheck(); 560 } 561 }); 562 } 563 564 /** 565 * Handles the select all option by toggling the checked state of all checkboxes given a source. 566 */ 567 handleSelectAll() { 568 const source = this.dataset.source; 569 const root = this.getRootNode(); 570 571 if (this.classList.contains('select-all-option')) { 572 const isAllChecked = this.isChecked(); 573 const selectOptions = root.querySelectorAll(`checkbox-element.select-option[data-source="${source}"]`); 574 575 selectOptions.forEach((option) => {
576 //Match all checkboxes with the status of the `select-all-option` button 577 if (option.isChecked() !== isAllChecked) { 578 option.toggleCheck(); 579 } 580 }); 581 } else if (this.classList.contains('select-option')) { 582 //Check if all checkboxes with `select-option` class are checked 583 //If all are checked, check the `select-all-option` button 584 //If part of them are checked, set the `select-all-option` button to indeterminate state 585 //If not all are checked, uncheck the `select-all-option` button 586 587 const selectOptions = root.querySelectorAll(`checkbox-element.select-option[data-source="${source}"]`); 588 const selectAllOption = root.querySelector(`checkbox-element.select-all-option[data-source="${source}"]`); 589 590 591 const checkedCount = Array.from(selectOptions).filter(opt => opt.isChecked()).length; 592 const totalCount = selectOptions.length; 593 594 const allChecked = checkedCount === totalCount && totalCount > 0; 595 const noneChecked = checkedCount === 0; 596 const isIndeterminate = !allChecked && !noneChecked; 597 598 selectAllOption.checkbox.checked = allChecked; 599 selectAllOption.checkbox.indeterminate = isIndeterminate; 600 } 601 } 602 603 /** 604 * Returns the checked state of the checkbox or radio button. 605 * 606 * @returns {boolean} - True if the checkbox or radio button is checked, false otherwise. 607 */ 608 isChecked() { 609 return this.checkbox.checked; 610 } 611 612 613 /** 614 * Sets the indeterminate state of the checkbox. 615 * * @param {boolean} value - True to set the checkbox to indeterminate, false otherwise. 616 */ 617 setIndeterminate(value) { 618 this.checkbox.indeterminate = Boolean(value); 619 } 620 621 622 /** 623 * Sets the checked state of the checkbox or radio button, 624 * and updates the "checked" attribute accordingly. 625 * 626 * @param {boolean} value - True to check the element, false to uncheck it. 627 */ 628 set checked(value) { 629 const isChecked = Boolean(value); 630 this.checkbox.checked = isChecked; 631 632 if (isChecked) { 633 this.setAttribute('checked', ''); 634 } else { 635 this.removeAttribute('checked'); 636 } 637 } 638} 639 640// Only define the custom element if it hasn't been defined yet 641if (!customElements.get('checkbox-element')) { 642 customElements.define('checkbox-element', CheckboxElement); 643}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.