Improve tag input keyboard navigation (#650)
This commit is contained in:
		
							parent
							
								
									d0182a3e13
								
							
						
					
					
						commit
						a1f939ad29
					
				@ -1,9 +1,14 @@
 | 
				
			|||||||
import { LitElement, html, css } from "lit";
 | 
					import { LitElement, html, css } from "lit";
 | 
				
			||||||
import { state, property, query } from "lit/decorators.js";
 | 
					import { state, property, query } from "lit/decorators.js";
 | 
				
			||||||
import { msg, localized, str } from "@lit/localize";
 | 
					import { msg, localized, str } from "@lit/localize";
 | 
				
			||||||
import type { SlInput, SlMenu, SlPopup } from "@shoelace-style/shoelace";
 | 
					import type {
 | 
				
			||||||
 | 
					  SlInput,
 | 
				
			||||||
 | 
					  SlMenu,
 | 
				
			||||||
 | 
					  SlMenuItem,
 | 
				
			||||||
 | 
					  SlPopup,
 | 
				
			||||||
 | 
					  SlTag,
 | 
				
			||||||
 | 
					} from "@shoelace-style/shoelace";
 | 
				
			||||||
import inputCss from "@shoelace-style/shoelace/dist/components/input/input.styles.js";
 | 
					import inputCss from "@shoelace-style/shoelace/dist/components/input/input.styles.js";
 | 
				
			||||||
import union from "lodash/fp/union";
 | 
					 | 
				
			||||||
import debounce from "lodash/fp/debounce";
 | 
					import debounce from "lodash/fp/debounce";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export type Tags = string[];
 | 
					export type Tags = string[];
 | 
				
			||||||
@ -105,6 +110,36 @@ export class TagInput extends LitElement {
 | 
				
			|||||||
        display: none;
 | 
					        display: none;
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    .shake {
 | 
				
			||||||
 | 
					      animation: shake 0.82s cubic-bezier(0.36, 0.07, 0.19, 0.97) both;
 | 
				
			||||||
 | 
					      transform: translate3d(0, 0, 0);
 | 
				
			||||||
 | 
					      backface-visibility: hidden;
 | 
				
			||||||
 | 
					      perspective: 1000px;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @keyframes shake {
 | 
				
			||||||
 | 
					      10%,
 | 
				
			||||||
 | 
					      90% {
 | 
				
			||||||
 | 
					        transform: translate3d(-1px, 0, 0);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      20%,
 | 
				
			||||||
 | 
					      80% {
 | 
				
			||||||
 | 
					        transform: translate3d(2px, 0, 0);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      30%,
 | 
				
			||||||
 | 
					      50%,
 | 
				
			||||||
 | 
					      70% {
 | 
				
			||||||
 | 
					        transform: translate3d(-3px, 0, 0);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      40%,
 | 
				
			||||||
 | 
					      60% {
 | 
				
			||||||
 | 
					        transform: translate3d(3px, 0, 0);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
  `;
 | 
					  `;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @property({ type: Array })
 | 
					  @property({ type: Array })
 | 
				
			||||||
@ -129,14 +164,20 @@ export class TagInput extends LitElement {
 | 
				
			|||||||
  @state()
 | 
					  @state()
 | 
				
			||||||
  private dropdownIsOpen?: boolean;
 | 
					  private dropdownIsOpen?: boolean;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @query(".form-control")
 | 
				
			||||||
 | 
					  private formControl!: HTMLElement;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @query("#input")
 | 
					  @query("#input")
 | 
				
			||||||
  private input?: HTMLInputElement;
 | 
					  private input!: HTMLInputElement;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @query("#dropdown")
 | 
				
			||||||
 | 
					  private dropdown!: HTMLDivElement;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @query("sl-menu")
 | 
					  @query("sl-menu")
 | 
				
			||||||
  private menu!: SlMenu;
 | 
					  private menu!: SlMenu;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @query("sl-popup")
 | 
					  @query("sl-popup")
 | 
				
			||||||
  private popup!: SlPopup;
 | 
					  private combobox!: SlPopup;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  connectedCallback() {
 | 
					  connectedCallback() {
 | 
				
			||||||
    if (this.initialTags) {
 | 
					    if (this.initialTags) {
 | 
				
			||||||
@ -153,13 +194,17 @@ export class TagInput extends LitElement {
 | 
				
			|||||||
        this.setAttribute("data-invalid", "");
 | 
					        this.setAttribute("data-invalid", "");
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    if (changedProperties.has("dropdownIsOpen") && this.dropdownIsOpen) {
 | 
					    if (changedProperties.has("dropdownIsOpen")) {
 | 
				
			||||||
      this.popup.reposition();
 | 
					      if (this.dropdownIsOpen) {
 | 
				
			||||||
 | 
					        this.openDropdown();
 | 
				
			||||||
 | 
					      } else {
 | 
				
			||||||
 | 
					        this.closeDropdown();
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  reportValidity() {
 | 
					  reportValidity() {
 | 
				
			||||||
    this.input?.reportValidity();
 | 
					    this.input.reportValidity();
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  render() {
 | 
					  render() {
 | 
				
			||||||
@ -175,7 +220,18 @@ export class TagInput extends LitElement {
 | 
				
			|||||||
        </label>
 | 
					        </label>
 | 
				
			||||||
        <div
 | 
					        <div
 | 
				
			||||||
          class="input input--medium input--standard"
 | 
					          class="input input--medium input--standard"
 | 
				
			||||||
 | 
					          tabindex="-1"
 | 
				
			||||||
          @click=${this.onInputWrapperClick}
 | 
					          @click=${this.onInputWrapperClick}
 | 
				
			||||||
 | 
					          @focusout=${(e: FocusEvent) => {
 | 
				
			||||||
 | 
					            const currentTarget = e.currentTarget as SlMenuItem;
 | 
				
			||||||
 | 
					            const relatedTarget = e.relatedTarget as HTMLElement;
 | 
				
			||||||
 | 
					            if (
 | 
				
			||||||
 | 
					              this.dropdownIsOpen &&
 | 
				
			||||||
 | 
					              !currentTarget?.contains(relatedTarget)
 | 
				
			||||||
 | 
					            ) {
 | 
				
			||||||
 | 
					              this.dropdownIsOpen = false;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					          }}
 | 
				
			||||||
        >
 | 
					        >
 | 
				
			||||||
          ${this.renderTags()}
 | 
					          ${this.renderTags()}
 | 
				
			||||||
          <sl-popup
 | 
					          <sl-popup
 | 
				
			||||||
@ -192,8 +248,8 @@ export class TagInput extends LitElement {
 | 
				
			|||||||
              style="min-width: ${placeholder.length}ch"
 | 
					              style="min-width: ${placeholder.length}ch"
 | 
				
			||||||
              @focus=${this.onFocus}
 | 
					              @focus=${this.onFocus}
 | 
				
			||||||
              @blur=${this.onBlur}
 | 
					              @blur=${this.onBlur}
 | 
				
			||||||
              @keydown=${this.onKeydown}
 | 
					 | 
				
			||||||
              @input=${this.onInput}
 | 
					              @input=${this.onInput}
 | 
				
			||||||
 | 
					              @keydown=${this.onKeydown}
 | 
				
			||||||
              @keyup=${this.onKeyup}
 | 
					              @keyup=${this.onKeyup}
 | 
				
			||||||
              @paste=${this.onPaste}
 | 
					              @paste=${this.onPaste}
 | 
				
			||||||
              ?required=${this.required && !this.tags.length}
 | 
					              ?required=${this.required && !this.tags.length}
 | 
				
			||||||
@ -204,11 +260,17 @@ export class TagInput extends LitElement {
 | 
				
			|||||||
            />
 | 
					            />
 | 
				
			||||||
            <div
 | 
					            <div
 | 
				
			||||||
              id="dropdown"
 | 
					              id="dropdown"
 | 
				
			||||||
              class="dropdown ${this.dropdownIsOpen === true
 | 
					              class="dropdown hidden"
 | 
				
			||||||
                ? "animateShow"
 | 
					              @animationend=${(e: AnimationEvent) => {
 | 
				
			||||||
                : this.dropdownIsOpen === false
 | 
					                const el = e.target as HTMLDivElement;
 | 
				
			||||||
                ? "animateHide"
 | 
					                if (e.animationName === "dropdownShow") {
 | 
				
			||||||
                : "hidden"}"
 | 
					                  el.classList.remove("animateShow");
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                if (e.animationName === "dropdownHide") {
 | 
				
			||||||
 | 
					                  el.classList.add("hidden");
 | 
				
			||||||
 | 
					                  el.classList.remove("animateHide");
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					              }}
 | 
				
			||||||
            >
 | 
					            >
 | 
				
			||||||
              <sl-menu
 | 
					              <sl-menu
 | 
				
			||||||
                role="listbox"
 | 
					                role="listbox"
 | 
				
			||||||
@ -219,7 +281,7 @@ export class TagInput extends LitElement {
 | 
				
			|||||||
                  e.stopPropagation();
 | 
					                  e.stopPropagation();
 | 
				
			||||||
                  if (e.key === "Escape") {
 | 
					                  if (e.key === "Escape") {
 | 
				
			||||||
                    this.dropdownIsOpen = false;
 | 
					                    this.dropdownIsOpen = false;
 | 
				
			||||||
                    this.input?.focus();
 | 
					                    this.input.focus();
 | 
				
			||||||
                  }
 | 
					                  }
 | 
				
			||||||
                }}
 | 
					                }}
 | 
				
			||||||
                @sl-select=${this.onSelect}
 | 
					                @sl-select=${this.onSelect}
 | 
				
			||||||
@ -251,9 +313,40 @@ export class TagInput extends LitElement {
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  private renderTag = (content: string) => {
 | 
					  private renderTag = (content: string) => {
 | 
				
			||||||
    const removeTag = () => {
 | 
					    const removeTag = (e: CustomEvent | KeyboardEvent) => {
 | 
				
			||||||
      this.tags = this.tags.filter((v) => v !== content);
 | 
					      this.tags = this.tags.filter((v) => v !== content);
 | 
				
			||||||
      this.dispatchChange();
 | 
					      this.dispatchChange();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      const tag = e.currentTarget as SlTag;
 | 
				
			||||||
 | 
					      const focusTarget = tag.previousElementSibling as HTMLElement | null;
 | 
				
			||||||
 | 
					      (focusTarget || this.input).focus();
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					    const onKeydown = (e: KeyboardEvent) => {
 | 
				
			||||||
 | 
					      const el = e.currentTarget as SlTag;
 | 
				
			||||||
 | 
					      switch (e.key) {
 | 
				
			||||||
 | 
					        // TODO localize, handle RTL
 | 
				
			||||||
 | 
					        case "ArrowLeft": {
 | 
				
			||||||
 | 
					          const focusTarget = el.previousElementSibling as HTMLElement | null;
 | 
				
			||||||
 | 
					          focusTarget?.focus();
 | 
				
			||||||
 | 
					          break;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        case "ArrowRight": {
 | 
				
			||||||
 | 
					          let focusTarget = el.nextElementSibling as HTMLElement | null;
 | 
				
			||||||
 | 
					          if (!focusTarget) return;
 | 
				
			||||||
 | 
					          if (focusTarget === this.combobox) {
 | 
				
			||||||
 | 
					            focusTarget = this.input || null;
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					          focusTarget?.focus();
 | 
				
			||||||
 | 
					          break;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        case "Backspace":
 | 
				
			||||||
 | 
					        case "Delete": {
 | 
				
			||||||
 | 
					          removeTag(e);
 | 
				
			||||||
 | 
					          break;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        default:
 | 
				
			||||||
 | 
					          break;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
    return html`
 | 
					    return html`
 | 
				
			||||||
      <btrix-tag
 | 
					      <btrix-tag
 | 
				
			||||||
@ -261,13 +354,33 @@ export class TagInput extends LitElement {
 | 
				
			|||||||
        removable
 | 
					        removable
 | 
				
			||||||
        @sl-remove=${removeTag}
 | 
					        @sl-remove=${removeTag}
 | 
				
			||||||
        title=${content}
 | 
					        title=${content}
 | 
				
			||||||
        >${content}</btrix-tag
 | 
					        tabindex="-1"
 | 
				
			||||||
 | 
					        @keydown=${onKeydown}
 | 
				
			||||||
 | 
					        @animationend=${(e: AnimationEvent) => {
 | 
				
			||||||
 | 
					          if (e.animationName === "shake") {
 | 
				
			||||||
 | 
					            (e.target as SlTag).classList.remove("shake");
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        }}
 | 
				
			||||||
      >
 | 
					      >
 | 
				
			||||||
 | 
					        ${content}
 | 
				
			||||||
 | 
					      </btrix-tag>
 | 
				
			||||||
    `;
 | 
					    `;
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private openDropdown() {
 | 
				
			||||||
 | 
					    this.combobox.reposition();
 | 
				
			||||||
 | 
					    this.dropdown.classList.add("animateShow");
 | 
				
			||||||
 | 
					    this.dropdown.classList.remove("hidden");
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private closeDropdown() {
 | 
				
			||||||
 | 
					    this.combobox.reposition();
 | 
				
			||||||
 | 
					    this.dropdown.classList.add("animateHide");
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  private onSelect(e: CustomEvent) {
 | 
					  private onSelect(e: CustomEvent) {
 | 
				
			||||||
    this.addTags([e.detail.item.value]);
 | 
					    this.addTags([e.detail.item.value]);
 | 
				
			||||||
 | 
					    this.input.focus();
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  private onFocus(e: FocusEvent) {
 | 
					  private onFocus(e: FocusEvent) {
 | 
				
			||||||
@ -280,16 +393,18 @@ export class TagInput extends LitElement {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  private onBlur(e: FocusEvent) {
 | 
					  private onBlur(e: FocusEvent) {
 | 
				
			||||||
    const relatedTarget = e.relatedTarget as HTMLElement;
 | 
					    const relatedTarget = e.relatedTarget as HTMLElement;
 | 
				
			||||||
    if (this.menu?.contains(relatedTarget)) {
 | 
					    if (relatedTarget) {
 | 
				
			||||||
      // Keep focus on form control if moving to menu selection
 | 
					      if (this.menu?.contains(relatedTarget)) {
 | 
				
			||||||
      return;
 | 
					        // Keep focus on form control if moving to menu selection
 | 
				
			||||||
    }
 | 
					        return;
 | 
				
			||||||
    if (
 | 
					      }
 | 
				
			||||||
      relatedTarget.tagName.includes("BUTTON") &&
 | 
					      if (
 | 
				
			||||||
      relatedTarget.getAttribute("type") === "reset"
 | 
					        relatedTarget.tagName.includes("BUTTON") &&
 | 
				
			||||||
    ) {
 | 
					        relatedTarget.getAttribute("type") === "reset"
 | 
				
			||||||
      // Don't add tag if resetting form
 | 
					      ) {
 | 
				
			||||||
      return;
 | 
					        // Don't add tag if resetting form
 | 
				
			||||||
 | 
					        return;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    const input = e.target as HTMLInputElement;
 | 
					    const input = e.target as HTMLInputElement;
 | 
				
			||||||
    (input.parentElement as HTMLElement).classList.remove("input--focused");
 | 
					    (input.parentElement as HTMLElement).classList.remove("input--focused");
 | 
				
			||||||
@ -297,19 +412,45 @@ export class TagInput extends LitElement {
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  private onKeydown(e: KeyboardEvent) {
 | 
					  private onKeydown(e: KeyboardEvent) {
 | 
				
			||||||
    if (e.key === "ArrowDown") {
 | 
					    if (this.dropdownIsOpen && (e.key === "ArrowDown" || e.key === "Tab")) {
 | 
				
			||||||
      e.preventDefault();
 | 
					      e.preventDefault();
 | 
				
			||||||
      this.menu?.querySelector("sl-menu-item")?.focus();
 | 
					      const menuItem = this.menu?.querySelector("sl-menu-item");
 | 
				
			||||||
 | 
					      if (menuItem) {
 | 
				
			||||||
 | 
					        // Reset roving tabindex set by shoelace
 | 
				
			||||||
 | 
					        this.menu!.setCurrentItem(menuItem);
 | 
				
			||||||
 | 
					        menuItem.focus();
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
      return;
 | 
					      return;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    if (e.key === "," || e.key === "Enter") {
 | 
					    switch (e.key) {
 | 
				
			||||||
      e.preventDefault();
 | 
					      case "Backspace":
 | 
				
			||||||
 | 
					      case "Delete":
 | 
				
			||||||
      const input = e.target as HTMLInputElement;
 | 
					      // TODO localize, handle RTL
 | 
				
			||||||
      const value = input.value.trim();
 | 
					      case "ArrowLeft": {
 | 
				
			||||||
      if (value) {
 | 
					        if (this.input.selectionStart! > 0) return;
 | 
				
			||||||
        this.addTags([value]);
 | 
					        e.preventDefault();
 | 
				
			||||||
 | 
					        const focusTarget = this.combobox
 | 
				
			||||||
 | 
					          .previousElementSibling as HTMLElement | null;
 | 
				
			||||||
 | 
					        focusTarget?.focus();
 | 
				
			||||||
 | 
					        break;
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					      case "ArrowRight": {
 | 
				
			||||||
 | 
					        // if (isInputEl && this.input!.selectionEnd! > this.input!.value.length)
 | 
				
			||||||
 | 
					        //   return;
 | 
				
			||||||
 | 
					        break;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      case ",":
 | 
				
			||||||
 | 
					      case "Enter": {
 | 
				
			||||||
 | 
					        e.preventDefault();
 | 
				
			||||||
 | 
					        const input = e.target as HTMLInputElement;
 | 
				
			||||||
 | 
					        const value = input.value.trim();
 | 
				
			||||||
 | 
					        if (value) {
 | 
				
			||||||
 | 
					          this.addTags([value]);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        break;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      default:
 | 
				
			||||||
 | 
					        break;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -339,29 +480,61 @@ export class TagInput extends LitElement {
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  private onPaste(e: ClipboardEvent) {
 | 
					  private onPaste(e: ClipboardEvent) {
 | 
				
			||||||
    const text = e.clipboardData?.getData("text");
 | 
					    const input = e.target as HTMLInputElement;
 | 
				
			||||||
    if (text) {
 | 
					    if (!input.value) {
 | 
				
			||||||
      this.addTags(text.split(","));
 | 
					      e.preventDefault();
 | 
				
			||||||
 | 
					      const text = e.clipboardData
 | 
				
			||||||
 | 
					        ?.getData("text")
 | 
				
			||||||
 | 
					        // Remove zero-width characters
 | 
				
			||||||
 | 
					        .replace(/[\u200B-\u200D\uFEFF]/g, "")
 | 
				
			||||||
 | 
					        .trim();
 | 
				
			||||||
 | 
					      if (text) {
 | 
				
			||||||
 | 
					        this.addTags(text.split(","));
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  private onInputWrapperClick(e: MouseEvent) {
 | 
					  private onInputWrapperClick(e: MouseEvent) {
 | 
				
			||||||
    if (e.target === e.currentTarget) {
 | 
					    if (e.target === e.currentTarget) {
 | 
				
			||||||
      this.input?.focus();
 | 
					      this.input.focus();
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  private async addTags(tags: Tags) {
 | 
					  private async addTags(tags: Tags) {
 | 
				
			||||||
    await this.updateComplete;
 | 
					    await this.updateComplete;
 | 
				
			||||||
    this.tags = union(
 | 
					    const repeatTags: Tags = [];
 | 
				
			||||||
      tags.map((v) => v.trim().toLocaleLowerCase()).filter((v) => v),
 | 
					    const uniqueTags: Tags = [...this.tags];
 | 
				
			||||||
      this.tags
 | 
					
 | 
				
			||||||
    );
 | 
					    tags.forEach((str) => {
 | 
				
			||||||
 | 
					      const tag = str // Remove zero-width characters
 | 
				
			||||||
 | 
					        .replace(/[\u200B-\u200D\uFEFF]/g, "")
 | 
				
			||||||
 | 
					        .trim()
 | 
				
			||||||
 | 
					        .toLocaleLowerCase();
 | 
				
			||||||
 | 
					      if (tag) {
 | 
				
			||||||
 | 
					        if (uniqueTags.includes(tag)) {
 | 
				
			||||||
 | 
					          repeatTags.push(tag);
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					          uniqueTags.push(tag);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					    this.tags = uniqueTags;
 | 
				
			||||||
    this.dispatchChange();
 | 
					    this.dispatchChange();
 | 
				
			||||||
    this.dropdownIsOpen = false;
 | 
					    this.dropdownIsOpen = false;
 | 
				
			||||||
    this.input!.value = "";
 | 
					    this.input!.value = "";
 | 
				
			||||||
 | 
					    if (repeatTags.length) {
 | 
				
			||||||
 | 
					      repeatTags.forEach(this.shakeTag);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private shakeTag = (tag: string) => {
 | 
				
			||||||
 | 
					    const tagEl = this.formControl.querySelector(
 | 
				
			||||||
 | 
					      `btrix-tag[title="${tag}"]`
 | 
				
			||||||
 | 
					    ) as SlTag;
 | 
				
			||||||
 | 
					    if (!tagEl) return;
 | 
				
			||||||
 | 
					    tagEl.classList.add("shake");
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  private async dispatchChange() {
 | 
					  private async dispatchChange() {
 | 
				
			||||||
    await this.updateComplete;
 | 
					    await this.updateComplete;
 | 
				
			||||||
    this.dispatchEvent(
 | 
					    this.dispatchEvent(
 | 
				
			||||||
 | 
				
			|||||||
@ -1,4 +1,6 @@
 | 
				
			|||||||
import { css } from "lit";
 | 
					import { css, html } from "lit";
 | 
				
			||||||
 | 
					import { state, property, query } from "lit/decorators.js";
 | 
				
			||||||
 | 
					import { ifDefined } from "lit/directives/if-defined.js";
 | 
				
			||||||
import SLTag from "@shoelace-style/shoelace/dist/components/tag/tag.js";
 | 
					import SLTag from "@shoelace-style/shoelace/dist/components/tag/tag.js";
 | 
				
			||||||
import tagStyles from "@shoelace-style/shoelace/dist/components/tag/tag.styles.js";
 | 
					import tagStyles from "@shoelace-style/shoelace/dist/components/tag/tag.styles.js";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -11,39 +13,71 @@ import tagStyles from "@shoelace-style/shoelace/dist/components/tag/tag.styles.j
 | 
				
			|||||||
 * ```
 | 
					 * ```
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
export class Tag extends SLTag {
 | 
					export class Tag extends SLTag {
 | 
				
			||||||
  static styles = css`
 | 
					  static shadowRootOptions = {
 | 
				
			||||||
    ${tagStyles}
 | 
					    ...SLTag.shadowRootOptions,
 | 
				
			||||||
 | 
					    delegatesFocus: true,
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    :host {
 | 
					  static styles = [
 | 
				
			||||||
      max-width: 100%;
 | 
					    tagStyles,
 | 
				
			||||||
    }
 | 
					    css`
 | 
				
			||||||
 | 
					      :host {
 | 
				
			||||||
 | 
					        max-width: 100%;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    .tag {
 | 
					      :focus {
 | 
				
			||||||
      height: var(--tag-height, 1.5rem);
 | 
					        outline: 0;
 | 
				
			||||||
      background-color: var(--sl-color-blue-100);
 | 
					      }
 | 
				
			||||||
      border-color: var(--sl-color-blue-500);
 | 
					 | 
				
			||||||
      color: var(--sl-color-blue-600);
 | 
					 | 
				
			||||||
      font-family: var(--sl-font-sans);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    .tag__content {
 | 
					      :focus .tag {
 | 
				
			||||||
      max-width: 100%;
 | 
					        background-color: var(--sl-color-blue-500);
 | 
				
			||||||
      white-space: nowrap;
 | 
					        border-color: var(--sl-color-blue-500);
 | 
				
			||||||
      overflow: hidden;
 | 
					      }
 | 
				
			||||||
      text-overflow: ellipsis;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    .tag__remove {
 | 
					      :focus .tag,
 | 
				
			||||||
      color: var(--sl-color-blue-600);
 | 
					      :focus .tag__remove {
 | 
				
			||||||
      border-radius: 100%;
 | 
					        color: var(--sl-color-neutral-0);
 | 
				
			||||||
      transition: background-color 0.1s;
 | 
					      }
 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    .tag__remove:hover {
 | 
					      .tag,
 | 
				
			||||||
      background-color: var(--sl-color-blue-600);
 | 
					      .tag__remove {
 | 
				
			||||||
      color: #fff;
 | 
					        transition: background-color 0.1s, color 0.1s;
 | 
				
			||||||
    }
 | 
					      }
 | 
				
			||||||
  `;
 | 
					
 | 
				
			||||||
 | 
					      .tag {
 | 
				
			||||||
 | 
					        height: var(--tag-height, 1.5rem);
 | 
				
			||||||
 | 
					        background-color: var(--sl-color-blue-100);
 | 
				
			||||||
 | 
					        border-color: var(--sl-color-blue-500);
 | 
				
			||||||
 | 
					        color: var(--sl-color-blue-600);
 | 
				
			||||||
 | 
					        font-family: var(--sl-font-sans);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      .tag__content {
 | 
				
			||||||
 | 
					        max-width: 100%;
 | 
				
			||||||
 | 
					        white-space: nowrap;
 | 
				
			||||||
 | 
					        overflow: hidden;
 | 
				
			||||||
 | 
					        text-overflow: ellipsis;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      .tag__remove {
 | 
				
			||||||
 | 
					        color: var(--sl-color-blue-600);
 | 
				
			||||||
 | 
					        border-radius: 100%;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      .tag__remove:hover {
 | 
				
			||||||
 | 
					        background-color: var(--sl-color-blue-600);
 | 
				
			||||||
 | 
					        color: var(--sl-color-neutral-0);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    `,
 | 
				
			||||||
 | 
					  ];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @property({ type: String, noAccessor: true })
 | 
				
			||||||
 | 
					  tabindex?: string;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  pill = true;
 | 
					  pill = true;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  render() {
 | 
				
			||||||
 | 
					    const template = super.render();
 | 
				
			||||||
 | 
					    return html`<span tabindex=${ifDefined(this.tabindex)}>${template}</span>`;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
		Reference in New Issue
	
	Block a user