@schukai/monster 4.150.9 → 4.152.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.
package/package.json CHANGED
@@ -1 +1 @@
1
- {"author":"Volker Schukai","dependencies":{"@floating-ui/dom":"^1.7.6"},"description":"Monster is a simple library for creating fast, robust and lightweight websites.","files":["source","test/cases","test/util","CHANGELOG.md"],"homepage":"https://monsterjs.org/","keywords":["framework","web","dom","css","sass","mobile-first","app","front-end","templates","schukai","core","shopcloud","alvine","monster","buildmap","stack","observer","observable","uuid","node","nodelist","css-in-js","logger","log","theme"],"license":"AGPL 3.0","main":"source/monster.mjs","module":"source/monster.mjs","name":"@schukai/monster","repository":{"type":"git","url":"https://gitlab.schukai.com/oss/libraries/javascript/monster.git"},"type":"module","version":"4.150.9"}
1
+ {"author":"Volker Schukai","dependencies":{"@floating-ui/dom":"^1.7.6"},"description":"Monster is a simple library for creating fast, robust and lightweight websites.","files":["source","test/cases","test/util","CHANGELOG.md"],"homepage":"https://monsterjs.org/","keywords":["framework","web","dom","css","sass","mobile-first","app","front-end","templates","schukai","core","shopcloud","alvine","monster","buildmap","stack","observer","observable","uuid","node","nodelist","css-in-js","logger","log","theme"],"license":"AGPL 3.0","main":"source/monster.mjs","module":"source/monster.mjs","name":"@schukai/monster","repository":{"type":"git","url":"https://gitlab.schukai.com/oss/libraries/javascript/monster.git"},"type":"module","version":"4.152.0"}
@@ -313,6 +313,9 @@ const selectionVersionSymbol = Symbol("selectionVersion");
313
313
  const closeOnSelectAutoSymbol = Symbol("closeOnSelectAuto");
314
314
  const lastFilterValueSymbol = Symbol("lastFilterValue");
315
315
  const filterInputVersionSymbol = Symbol("filterInputVersion");
316
+ const createOptionElementSymbol = Symbol("createOptionElement");
317
+ const createOptionCandidateSymbol = Symbol("createOptionCandidate");
318
+ const createOptionEventsAttachedSymbol = Symbol("createOptionEventsAttached");
316
319
 
317
320
  /**
318
321
  * @private
@@ -422,6 +425,7 @@ const SELECT_VIEWPORT_PADDING = 12;
422
425
  * @fires monster-options-set this event is fired when the options are set
423
426
  * @fires monster-selection-removed
424
427
  * @fires monster-selection-cleared
428
+ * @fires monster-option-created
425
429
  */
426
430
  class Select extends CustomControl {
427
431
  /**
@@ -555,6 +559,7 @@ class Select extends CustomControl {
555
559
  * @property {string} labels.select-an-option - Placeholder text when no selection has been made.
556
560
  * @property {string} labels.no-options - Message displayed when no options are available from slots or fetch.
557
561
  * @property {string} labels.no-options-found - Message displayed when the filter yields no matching options.
562
+ * @property {string} labels.create-option - Template for the create action. `${label}` and `${value}` are replaced with the candidate data.
558
563
  * @property {string} labels.summary-text.zero - Pluralization template for zero selected entries (e.g., "No entries selected").
559
564
  * @property {string} labels.summary-text.one - Pluralization template for one selected entry.
560
565
  * @property {string} labels.summary-text.other - Pluralization template for multiple selected entries.
@@ -567,6 +572,7 @@ class Select extends CustomControl {
567
572
  * @property {boolean} features.storeFetchedData - If `true`, the raw fetched data is stored and can be retrieved via `getLastFetchedData()`.
568
573
  * @property {boolean} features.useStrictValueComparison - Uses strict comparison (`===`) when matching option values.
569
574
  * @property {boolean} features.showRemoteInfo - When `filter.mode === "remote"`, displays an info badge indicating that more options may exist on the server.
575
+ * @property {boolean} features.createOption - Shows an action for creating and selecting a missing filtered option.
570
576
  * @property {Object} remoteInfo - Configuration for the remote info display.
571
577
  * @property {string|null} remoteInfo.url - URL to fetch the total count of remote options (used when `filter.mode === "remote"`).
572
578
  * @property {Object} placeholder - Placeholder texts for input fields.
@@ -601,12 +607,14 @@ class Select extends CustomControl {
601
607
  * @property {Array} empty.equivalents - Values that are considered "empty" (e.g., `undefined`, `null`, `""`, `NaN`) and are normalized to the default value. Matching is type-safe: a configured string `"0"` only matches the string `"0"`; configure numeric `0` explicitly, via `data-monster-options` JSON, `data-monster-properties`, or `setOption()`, when the number `0` should be treated as empty.
602
608
  * @property {Object} formatter - Functions for formatting display values.
603
609
  * @property {Function} formatter.selection - Callback `(value, option) => string` to format the display text of selected values.
610
+ * @property {Function} formatter.createOption - Callback `(filterValue) => {label, value}|null` used to build a missing option. Return `null` to suppress creation.
604
611
  * @property {Object} classes - CSS classes for various elements.
605
612
  * @property {string} classes.badge - CSS class for selection badges.
606
613
  * @property {string} classes.badgeUnresolved - CSS class used for selection badges whose value could not be hydrated. By default this swaps the badge into a warning style while keeping the raw key visible.
607
614
  * @property {string} classes.statusOrRemoveBadge - CSS class for the status or remove badge.
608
615
  * @property {string} classes.remoteInfo - CSS class for the remote info badge.
609
616
  * @property {string} classes.noOptions - CSS class for the "no options" message.
617
+ * @property {string} classes.createOption - Badge class for the create-option action.
610
618
  * @property {Object} messages - Internal messages for ARIA attributes and screen readers (should not normally be changed).
611
619
  * @property {string|null} messages.control - Message for the main control element.
612
620
  * @property {string|null} messages.selected - Message for the selected items area.
@@ -634,6 +642,7 @@ class Select extends CustomControl {
634
642
  storeFetchedData: false,
635
643
  useStrictValueComparison: false,
636
644
  showRemoteInfo: true,
645
+ createOption: false,
637
646
  },
638
647
 
639
648
  placeholder: {
@@ -689,6 +698,7 @@ class Select extends CustomControl {
689
698
  statusOrRemoveBadge: "empty",
690
699
  remoteInfo: "monster-margin-start-4 monster-margin-top-4",
691
700
  noOptions: "monster-margin-top-4 monster-margin-start-4",
701
+ createOption: "monster-badge-primary",
692
702
  },
693
703
 
694
704
  mapping: {
@@ -710,6 +720,7 @@ class Select extends CustomControl {
710
720
 
711
721
  formatter: {
712
722
  selection: buildSelectionLabel,
723
+ createOption: buildCreatedOption,
713
724
  },
714
725
 
715
726
  templates: {
@@ -1500,6 +1511,7 @@ function getTranslations() {
1500
1511
  case "de":
1501
1512
  return {
1502
1513
  "cannot-be-loaded": "Kann nicht geladen werden",
1514
+ "create-option": "„${label}“ hinzufügen",
1503
1515
  "no-options-available": "Keine Optionen verfügbar.",
1504
1516
  "click-to-load-options": "Klicken, um Optionen zu laden.",
1505
1517
  "select-an-option": "Option auswählen",
@@ -1957,6 +1969,7 @@ function getTranslations() {
1957
1969
  // Fallback to English if locale.language is unrecognized
1958
1970
  return {
1959
1971
  "cannot-be-loaded": "Cannot be loaded",
1972
+ "create-option": "Add “${label}”",
1960
1973
  "no-options-available": "No options available.",
1961
1974
  "click-to-load-options": "Click to load options.",
1962
1975
  "select-an-option": "Select an option",
@@ -2193,6 +2206,9 @@ function fetchIt(url, controlOptions) {
2193
2206
  try {
2194
2207
  importOptionsIntern.call(self, map);
2195
2208
  processAndApplyPaginationData.call(self, map);
2209
+ if (this.getOption("options", []).length > 0) {
2210
+ removeErrorAttribute(this, "No options available.");
2211
+ }
2196
2212
  } catch (e) {
2197
2213
  setStatusOrRemoveBadges.call(this, "error");
2198
2214
  reject(e);
@@ -2974,6 +2990,300 @@ function buildSelectionItem(value, preferredLabel) {
2974
2990
  };
2975
2991
  }
2976
2992
 
2993
+ /**
2994
+ * @private
2995
+ * @param {string} filterValue
2996
+ * @returns {{label: string, value: string}}
2997
+ */
2998
+ function buildCreatedOption(filterValue) {
2999
+ return {
3000
+ label: filterValue,
3001
+ value: filterValue,
3002
+ };
3003
+ }
3004
+
3005
+ /**
3006
+ * @private
3007
+ * @returns {string}
3008
+ */
3009
+ function getRawFilterInputValue() {
3010
+ const element =
3011
+ this.getOption("filter.position") === FILTER_POSITION_INLINE
3012
+ ? this[inlineFilterElementSymbol]
3013
+ : this[popperFilterElementSymbol];
3014
+
3015
+ return element instanceof HTMLInputElement ? element.value.trim() : "";
3016
+ }
3017
+
3018
+ /**
3019
+ * @private
3020
+ * @param {string} filterValue
3021
+ * @returns {{label: string, value: *}|null}
3022
+ */
3023
+ function buildCreateOptionCandidate(filterValue) {
3024
+ const formatter = this.getOption("formatter.createOption");
3025
+ if (!isFunction(formatter)) {
3026
+ return null;
3027
+ }
3028
+
3029
+ const result = formatter.call(this, filterValue);
3030
+ if (result === null || result === undefined) {
3031
+ return null;
3032
+ }
3033
+
3034
+ const candidate = isObject(result)
3035
+ ? { ...result }
3036
+ : { label: String(result), value: result };
3037
+ if (candidate.value === undefined) {
3038
+ candidate.value = candidate.label;
3039
+ }
3040
+ if (candidate.label === undefined) {
3041
+ candidate.label = getSelectionValueLabel.call(this, candidate.value);
3042
+ }
3043
+
3044
+ candidate.label = String(candidate.label).trim();
3045
+ if (candidate.label === "" || isValueIsEmpty.call(this, candidate.value)) {
3046
+ return null;
3047
+ }
3048
+
3049
+ return candidate;
3050
+ }
3051
+
3052
+ /**
3053
+ * @private
3054
+ * @param {{label: string, value: *}} candidate
3055
+ * @param {string} filterValue
3056
+ * @returns {boolean}
3057
+ */
3058
+ function hasEquivalentCreateOption(candidate, filterValue) {
3059
+ const normalizedFilter = filterValue.toLocaleLowerCase();
3060
+ const options = this.getOption("options", []);
3061
+ if (!isArray(options)) {
3062
+ return false;
3063
+ }
3064
+
3065
+ return options.some((option) => {
3066
+ const value = isObject(option) ? option.value : option;
3067
+ const label = isObject(option) ? option.label : option;
3068
+ const normalizedLabel = String(label ?? "")
3069
+ .trim()
3070
+ .toLocaleLowerCase();
3071
+
3072
+ return (
3073
+ normalizedLabel === normalizedFilter ||
3074
+ areLookupValuesEqual.call(this, value, candidate.value)
3075
+ );
3076
+ });
3077
+ }
3078
+
3079
+ /**
3080
+ * @private
3081
+ * @returns {HTMLElement|null}
3082
+ */
3083
+ function ensureCreateOptionElement() {
3084
+ if (!this.shadowRoot) {
3085
+ return null;
3086
+ }
3087
+
3088
+ const element = this.shadowRoot.querySelector(
3089
+ `[${ATTRIBUTE_ROLE}="create-option"]`,
3090
+ );
3091
+ if (!(element instanceof HTMLElement)) {
3092
+ return null;
3093
+ }
3094
+
3095
+ if (this[createOptionEventsAttachedSymbol] !== true) {
3096
+ const activate = (event) => {
3097
+ if (this.getOption("disabled") === true) {
3098
+ return;
3099
+ }
3100
+ event.preventDefault();
3101
+ createFilteredOption.call(this).catch((error) => {
3102
+ addErrorAttribute(this, error);
3103
+ });
3104
+ };
3105
+ const types = new Set(["click", ...this.getOption("toggleEventType", [])]);
3106
+ for (const type of types) {
3107
+ element.addEventListener(type, activate);
3108
+ }
3109
+ this[createOptionEventsAttachedSymbol] = true;
3110
+ }
3111
+
3112
+ this[createOptionElementSymbol] = element;
3113
+ return element;
3114
+ }
3115
+
3116
+ /**
3117
+ * @private
3118
+ */
3119
+ function updateCreateOptionAction() {
3120
+ const element = ensureCreateOptionElement.call(this);
3121
+ if (!(element instanceof HTMLElement)) {
3122
+ return;
3123
+ }
3124
+
3125
+ let candidate = null;
3126
+ const filterValue = getRawFilterInputValue.call(this);
3127
+ if (
3128
+ this.getOption("features.createOption") === true &&
3129
+ this.getOption("disabled") !== true &&
3130
+ getFilterMode.call(this) !== FILTER_MODE_DISABLED &&
3131
+ filterValue !== ""
3132
+ ) {
3133
+ try {
3134
+ candidate = buildCreateOptionCandidate.call(this, filterValue);
3135
+ if (
3136
+ candidate &&
3137
+ hasEquivalentCreateOption.call(this, candidate, filterValue)
3138
+ ) {
3139
+ candidate = null;
3140
+ }
3141
+ } catch (error) {
3142
+ addErrorAttribute(this, error);
3143
+ }
3144
+ }
3145
+
3146
+ this[createOptionCandidateSymbol] = candidate;
3147
+ if (!candidate) {
3148
+ element.setAttribute(`${ATTRIBUTE_PREFIX}visibility`, "hidden");
3149
+ element.removeAttribute(`${ATTRIBUTE_PREFIX}focused`);
3150
+ return;
3151
+ }
3152
+
3153
+ const template = this.getOption("labels.create-option", "Add “${label}”");
3154
+ const text = new Formatter({
3155
+ label: candidate.label,
3156
+ value: getSelectionValueLabel.call(this, candidate.value),
3157
+ }).format(template);
3158
+ const button = element.querySelector("button");
3159
+ const label = element.querySelector(
3160
+ `[${ATTRIBUTE_ROLE}="create-option-label"]`,
3161
+ );
3162
+ if (button instanceof HTMLButtonElement) {
3163
+ button.className = this.getOption(
3164
+ "classes.createOption",
3165
+ "monster-badge-primary",
3166
+ );
3167
+ button.setAttribute("aria-label", text);
3168
+ }
3169
+ element.setAttribute("aria-label", text);
3170
+ if (label instanceof HTMLElement) {
3171
+ label.textContent = text;
3172
+ }
3173
+ element.setAttribute(`${ATTRIBUTE_PREFIX}visibility`, "visible");
3174
+ }
3175
+
3176
+ /**
3177
+ * Waits until the updater has projected a newly appended option.
3178
+ *
3179
+ * @private
3180
+ * @param {*} value
3181
+ * @returns {Promise<HTMLInputElement>}
3182
+ */
3183
+ function waitForCreatedOptionControl(value) {
3184
+ return new Promise((resolve, reject) => {
3185
+ const startedAt = Date.now();
3186
+ const find = () => {
3187
+ for (const input of this[optionsElementSymbol].querySelectorAll(
3188
+ `[${ATTRIBUTE_ROLE}="option-control"]`,
3189
+ )) {
3190
+ if (
3191
+ input instanceof HTMLInputElement &&
3192
+ input.value === String(value)
3193
+ ) {
3194
+ return input;
3195
+ }
3196
+ }
3197
+ return null;
3198
+ };
3199
+ const poll = () => {
3200
+ const input = find();
3201
+ if (input) {
3202
+ resolve(input);
3203
+ return;
3204
+ }
3205
+ if (Date.now() - startedAt >= 1000) {
3206
+ reject(new Error("Created option was not rendered."));
3207
+ return;
3208
+ }
3209
+ setTimeout(poll, 10);
3210
+ };
3211
+ poll();
3212
+ });
3213
+ }
3214
+
3215
+ /**
3216
+ * @private
3217
+ * @returns {Promise<boolean>}
3218
+ */
3219
+ async function createFilteredOption() {
3220
+ updateCreateOptionAction.call(this);
3221
+ const candidate = this[createOptionCandidateSymbol];
3222
+ if (!candidate) {
3223
+ return false;
3224
+ }
3225
+
3226
+ const options = this.getOption("options", []);
3227
+ const nextOptions = (isArray(options) ? options : []).map((option) => {
3228
+ return isObject(option) ? { ...option, filtered: undefined } : option;
3229
+ });
3230
+ nextOptions.push({
3231
+ ...candidate,
3232
+ visibility: candidate.visibility ?? "visible",
3233
+ filtered: undefined,
3234
+ });
3235
+
3236
+ for (const input of [
3237
+ this[inlineFilterElementSymbol],
3238
+ this[popperFilterElementSymbol],
3239
+ ]) {
3240
+ if (input instanceof HTMLInputElement) {
3241
+ input.value = "";
3242
+ }
3243
+ }
3244
+ this[lastFilterValueSymbol] = "";
3245
+ bumpFilterInputVersion.call(this);
3246
+ this[createOptionCandidateSymbol] = null;
3247
+ this[createOptionElementSymbol]?.setAttribute(
3248
+ `${ATTRIBUTE_PREFIX}visibility`,
3249
+ "hidden",
3250
+ );
3251
+
3252
+ this.setOption("options", nextOptions);
3253
+ fireCustomEvent(this, "monster-options-set", { options: nextOptions });
3254
+
3255
+ const createdInput = await waitForCreatedOptionControl.call(
3256
+ this,
3257
+ candidate.value,
3258
+ );
3259
+ await new Promise((resolve) => setTimeout(resolve, 0));
3260
+ const selection =
3261
+ this.getOption("type") === "checkbox"
3262
+ ? [...this.getOption("selection", []), candidate]
3263
+ : [candidate];
3264
+ const selectedValues = new Set(
3265
+ selection.map((item) => String(isObject(item) ? item.value : item)),
3266
+ );
3267
+ for (const input of this[optionsElementSymbol].querySelectorAll(
3268
+ `[${ATTRIBUTE_ROLE}="option-control"]`,
3269
+ )) {
3270
+ if (input instanceof HTMLInputElement) {
3271
+ input.checked = selectedValues.has(input.value);
3272
+ }
3273
+ }
3274
+ createdInput.checked = true;
3275
+ await setSelection.call(this, selection);
3276
+ fireCustomEvent(this, "monster-option-created", { option: candidate });
3277
+
3278
+ if (this.getOption("features.closeOnSelect") === true) {
3279
+ hide.call(this);
3280
+ } else {
3281
+ focusFilter.call(this, { preventScroll: true });
3282
+ }
3283
+
3284
+ return true;
3285
+ }
3286
+
2977
3287
  /**
2978
3288
  * @private
2979
3289
  * @param {string} value The value to look up.
@@ -3234,6 +3544,7 @@ function initOptionObserver() {
3234
3544
  }
3235
3545
 
3236
3546
  setSummaryAndControlText.call(self);
3547
+ updateCreateOptionAction.call(self);
3237
3548
  }).run();
3238
3549
  }),
3239
3550
  );
@@ -3351,6 +3662,34 @@ function getOptionElements() {
3351
3662
  );
3352
3663
  }
3353
3664
 
3665
+ /**
3666
+ * @private
3667
+ * @returns {HTMLElement[]}
3668
+ */
3669
+ function getNavigableOptionElements() {
3670
+ if (!(this[optionsElementSymbol] instanceof HTMLElement)) {
3671
+ return [];
3672
+ }
3673
+
3674
+ const elements = Array.from(
3675
+ this[optionsElementSymbol].querySelectorAll(`[${ATTRIBUTE_ROLE}=option]`),
3676
+ );
3677
+ const createOption = this.shadowRoot?.querySelector?.(
3678
+ `[${ATTRIBUTE_ROLE}="create-option"]`,
3679
+ );
3680
+ if (createOption instanceof HTMLElement) {
3681
+ elements.push(createOption);
3682
+ }
3683
+
3684
+ return elements.filter((element) => {
3685
+ return (
3686
+ element instanceof HTMLElement &&
3687
+ element.getAttribute(`${ATTRIBUTE_PREFIX}visibility`) === "visible" &&
3688
+ element.getAttribute(`${ATTRIBUTE_PREFIX}filtered`) !== "true"
3689
+ );
3690
+ });
3691
+ }
3692
+
3354
3693
  /**
3355
3694
  * With the help of this filter callback, values can be filtered out. Only if the filter function returns true, the value is taken for the map.
3356
3695
  *
@@ -3372,11 +3711,14 @@ function getOptionElements() {
3372
3711
  * @private
3373
3712
  */
3374
3713
  function calcAndSetOptionsDimension() {
3375
- const options = getOptionElements.call(this);
3714
+ const options = Array.from(getOptionElements.call(this));
3715
+ const createOption = this.shadowRoot?.querySelector?.(
3716
+ `[${ATTRIBUTE_ROLE}="create-option"][${ATTRIBUTE_PREFIX}visibility="visible"]`,
3717
+ );
3376
3718
  const container = this[optionsElementSymbol];
3377
3719
  const content =
3378
3720
  this[popperElementSymbol]?.querySelector?.('[part="content"]');
3379
- if (!(container instanceof HTMLElement && options instanceof NodeList)) {
3721
+ if (!(container instanceof HTMLElement && isArray(options))) {
3380
3722
  return;
3381
3723
  }
3382
3724
 
@@ -3395,7 +3737,7 @@ function calcAndSetOptionsDimension() {
3395
3737
  visible++;
3396
3738
  }
3397
3739
 
3398
- if (visible === 0) {
3740
+ if (visible === 0 && !(createOption instanceof HTMLElement)) {
3399
3741
  if (this.getOption("classes.statusOrRemoveBadge") === "error") {
3400
3742
  this.setOption(
3401
3743
  "messages.emptyOptions",
@@ -3442,11 +3784,21 @@ function calcAndSetOptionsDimension() {
3442
3784
  geometry.availableHeight,
3443
3785
  SELECT_MAX_POPPER_HEIGHT,
3444
3786
  );
3445
- const listAvailableHeight = resolveSelectListAvailableHeight({
3787
+ let listAvailableHeight = resolveSelectListAvailableHeight({
3446
3788
  popperElement: this[popperElementSymbol],
3447
3789
  contentElement: content,
3448
3790
  popperMaxHeight,
3449
3791
  });
3792
+ if (createOption instanceof HTMLElement) {
3793
+ const createStyle = getGlobal().getComputedStyle(createOption);
3794
+ listAvailableHeight = Math.max(
3795
+ 0,
3796
+ listAvailableHeight -
3797
+ createOption.getBoundingClientRect().height -
3798
+ readSelectBoxDimension(createStyle.marginTop) -
3799
+ readSelectBoxDimension(createStyle.marginBottom),
3800
+ );
3801
+ }
3450
3802
  const listDimension = resolveSelectListDimension({
3451
3803
  visibleOptionHeights,
3452
3804
  maxVisibleOptions: max,
@@ -3887,54 +4239,24 @@ function activateCurrentOption(direction) {
3887
4239
  throw new Error("no shadow-root is defined");
3888
4240
  }
3889
4241
 
3890
- let focused = this.shadowRoot.querySelector(`[${ATTRIBUTE_PREFIX}focused]`);
3891
-
3892
- if (
3893
- !(focused instanceof HTMLElement) ||
3894
- focused.matches("[data-monster-visibility=hidden]")
3895
- ) {
3896
- for (const [, e] of Object.entries(
3897
- this.shadowRoot.querySelectorAll(`[${ATTRIBUTE_ROLE}=option]`),
3898
- )) {
3899
- if (e.matches("[data-monster-visibility=visible]")) {
3900
- focused = e;
3901
- break;
3902
- }
3903
- }
4242
+ const options = getNavigableOptionElements.call(this);
4243
+ const current = this.shadowRoot.querySelector(`[${ATTRIBUTE_PREFIX}focused]`);
4244
+ const currentIndex = options.indexOf(current);
4245
+ let focused = null;
4246
+ if (currentIndex === -1) {
4247
+ focused = direction === FOCUS_DIRECTION_UP ? options.at(-1) : options[0];
4248
+ } else if (direction === FOCUS_DIRECTION_DOWN) {
4249
+ focused = options[currentIndex + 1] ?? current;
4250
+ } else if (currentIndex > 0) {
4251
+ focused = options[currentIndex - 1];
3904
4252
  } else {
3905
- if (direction === FOCUS_DIRECTION_DOWN) {
3906
- while (focused.nextSibling) {
3907
- focused = focused.nextSibling;
3908
-
3909
- if (
3910
- focused instanceof HTMLElement &&
3911
- focused.hasAttribute(ATTRIBUTE_ROLE) &&
3912
- focused.getAttribute(ATTRIBUTE_ROLE) === "option" &&
3913
- focused.matches("[data-monster-visibility=visible]") &&
3914
- focused.matches(":not([data-monster-filtered=true])")
3915
- ) {
3916
- break;
3917
- }
3918
- }
3919
- } else {
3920
- let found = false;
3921
- while (focused.previousSibling) {
3922
- focused = focused.previousSibling;
3923
- if (
3924
- focused instanceof HTMLElement &&
3925
- focused.hasAttribute(ATTRIBUTE_ROLE) &&
3926
- focused.getAttribute(ATTRIBUTE_ROLE) === "option" &&
3927
- focused.matches("[data-monster-visibility=visible]") &&
3928
- focused.matches(":not([data-monster-filtered=true])")
3929
- ) {
3930
- found = true;
3931
- break;
3932
- }
3933
- }
3934
- if (found === false) {
3935
- focusFilter.call(this, { preventScroll: true });
3936
- }
3937
- }
4253
+ this.shadowRoot
4254
+ .querySelectorAll(`[${ATTRIBUTE_PREFIX}focused]`)
4255
+ .forEach((element) => {
4256
+ element.removeAttribute(`${ATTRIBUTE_PREFIX}focused`);
4257
+ });
4258
+ focusFilter.call(this, { preventScroll: true });
4259
+ return;
3938
4260
  }
3939
4261
 
3940
4262
  new Processing(() => {
@@ -4017,6 +4339,15 @@ function handleFilterKeyboardEvents(event) {
4017
4339
  const shiftKey = event?.["shiftKey"];
4018
4340
 
4019
4341
  switch (event?.["code"]) {
4342
+ case "Enter":
4343
+ updateCreateOptionAction.call(this);
4344
+ if (this[createOptionCandidateSymbol]) {
4345
+ createFilteredOption.call(this).catch((error) => {
4346
+ addErrorAttribute(this, error);
4347
+ });
4348
+ event.preventDefault();
4349
+ }
4350
+ break;
4020
4351
  case "Tab":
4021
4352
  activateCurrentOption.call(this, FOCUS_DIRECTION_DOWN);
4022
4353
  event.preventDefault();
@@ -4053,6 +4384,7 @@ function handleFilterKeyboardEvents(event) {
4053
4384
  * @return {void} This method does not return anything.
4054
4385
  */
4055
4386
  function handleFilterInputEvents() {
4387
+ updateCreateOptionAction.call(this);
4056
4388
  const filterValue = getCurrentFilterInputValue.call(this);
4057
4389
 
4058
4390
  if (filterValue === this[lastFilterValueSymbol]) {
@@ -4339,6 +4671,24 @@ function handleOptionKeyboardEvents(event) {
4339
4671
  }
4340
4672
  }
4341
4673
 
4674
+ /**
4675
+ * @private
4676
+ * @param {KeyboardEvent} event
4677
+ */
4678
+ function handleCreateOptionKeyboardEvents(event) {
4679
+ switch (event?.code) {
4680
+ case "Enter":
4681
+ case "Space":
4682
+ createFilteredOption.call(this).catch((error) => {
4683
+ addErrorAttribute(this, error);
4684
+ });
4685
+ event.preventDefault();
4686
+ break;
4687
+ default:
4688
+ handleOptionKeyboardEvents.call(this, event);
4689
+ }
4690
+ }
4691
+
4342
4692
  /**
4343
4693
  * @private
4344
4694
  * @return {string}
@@ -5235,8 +5585,7 @@ function show() {
5235
5585
 
5236
5586
  delete this[lazyLoadRequestSymbol];
5237
5587
  this[lazyLoadDoneSymbol] = false;
5238
- this[lazyLoadErrorSymbol] =
5239
- e instanceof Error ? e.message : String(e);
5588
+ this[lazyLoadErrorSymbol] = e instanceof Error ? e.message : String(e);
5240
5589
  addErrorAttribute(this, e);
5241
5590
  setStatusOrRemoveBadges.call(this, "error");
5242
5591
  });
@@ -5664,6 +6013,9 @@ function initEventHandler() {
5664
6013
  case "filter":
5665
6014
  handleFilterKeyboardEvents.call(self, event);
5666
6015
  break;
6016
+ case "create-option":
6017
+ handleCreateOptionKeyboardEvents.call(self, event);
6018
+ break;
5667
6019
  case "option-label":
5668
6020
  case "option-control":
5669
6021
  case "option":
@@ -5755,6 +6107,8 @@ function initEventHandler() {
5755
6107
  self.addEventListener("keydown", self[keyEventHandler]);
5756
6108
 
5757
6109
  const callback = () => {
6110
+ ensureCreateOptionElement.call(self);
6111
+ updateCreateOptionAction.call(self);
5758
6112
  if (this[debounceOptionsMutationObserverSymbol] instanceof DeadMansSwitch) {
5759
6113
  try {
5760
6114
  this[debounceOptionsMutationObserverSymbol].touch();
@@ -5962,6 +6316,8 @@ function initControlReferences() {
5962
6316
  this[optionsElementSymbol] = this.shadowRoot.querySelector(
5963
6317
  `[${ATTRIBUTE_ROLE}=options]`,
5964
6318
  );
6319
+ ensureCreateOptionElement.call(this);
6320
+ updateCreateOptionAction.call(this);
5965
6321
  this[noOptionsAvailableElementSymbol] = this.shadowRoot.querySelector(
5966
6322
  `[${ATTRIBUTE_ROLE}="no-options"]`,
5967
6323
  );
@@ -6054,6 +6410,13 @@ function getTemplate() {
6054
6410
  <div part="content" class="flex" data-monster-replace="path:content">
6055
6411
  <div part="options" data-monster-role="options" data-monster-insert="options path:options"
6056
6412
  tabindex="-1"></div>
6413
+ <div data-monster-role="create-option" role="option" aria-selected="false"
6414
+ data-monster-visibility="hidden" tabindex="-1">
6415
+ <button type="button" part="create-option" tabindex="-1">
6416
+ <span aria-hidden="true" data-monster-role="create-option-icon">+</span>
6417
+ <span data-monster-role="create-option-label"></span>
6418
+ </button>
6419
+ </div>
6057
6420
  </div>
6058
6421
  <div part="pagination-footer" data-monster-role="pagination-footer">
6059
6422
  <div part="remote-info"
@@ -14,6 +14,7 @@
14
14
 
15
15
  import { instanceSymbol } from "../../constants.mjs";
16
16
  import { registerCustomElement } from "../../dom/customelement.mjs";
17
+ import { fireCustomEvent } from "../../dom/events.mjs";
17
18
  import { isInteger } from "../../types/is.mjs";
18
19
  import { validateInstance, validateString } from "../../types/validate.mjs";
19
20
  import { Button } from "./button.mjs";
@@ -33,6 +34,7 @@ export { StateButton };
33
34
  * @since 1.5.0
34
35
  * @copyright Volker Schukai
35
36
  * @summary A beautiful button with icons
37
+ * @fires monster-state-changed - Fired when the visual state changes
36
38
  */
37
39
  class StateButton extends Button {
38
40
  /**
@@ -111,7 +113,10 @@ class StateButton extends Button {
111
113
  throw new Error("state not found");
112
114
  }
113
115
 
114
- this.setOption("current", validateInstance(obj, State));
116
+ const previousState = this.getState()?.state ?? "stateless";
117
+ const currentState = validateInstance(obj, State);
118
+ this.setOption("current", currentState);
119
+ dispatchStateChanged.call(this, previousState, currentState.state);
115
120
 
116
121
  if (isInteger(timeout) && timeout > 0) {
117
122
  this[timeoutSymbol] = setTimeout(() => {
@@ -128,7 +133,10 @@ class StateButton extends Button {
128
133
  * @return {StateButton}
129
134
  */
130
135
  removeState() {
131
- this.setOption("current", getStateInstanceFor("stateless"));
136
+ const previousState = this.getState()?.state ?? "stateless";
137
+ const currentState = getStateInstanceFor("stateless");
138
+ this.setOption("current", currentState);
139
+ dispatchStateChanged.call(this, previousState, currentState.state);
132
140
  return this;
133
141
  }
134
142
 
@@ -156,6 +164,23 @@ class StateButton extends Button {
156
164
  }
157
165
  }
158
166
 
167
+ /**
168
+ * @private
169
+ * @param {string} previousState
170
+ * @param {string} state
171
+ * @return {void}
172
+ */
173
+ function dispatchStateChanged(previousState, state) {
174
+ if (previousState === state) {
175
+ return;
176
+ }
177
+
178
+ fireCustomEvent(this, "monster-state-changed", {
179
+ state,
180
+ previousState,
181
+ });
182
+ }
183
+
159
184
  /**
160
185
  * @private
161
186
  * @return {string}
@@ -100,6 +100,10 @@ div[data-monster-role=popper] {
100
100
  z-index: var(--monster-z-index-popover);
101
101
  }
102
102
 
103
+ div[data-monster-role=popper] > [part=content] {
104
+ flex-direction: column;
105
+ }
106
+
103
107
  @media (prefers-color-scheme: light) {
104
108
  [data-monster-role=control] {
105
109
  background-color: var(--monster-bg-color-primary-1);
@@ -357,9 +361,44 @@ div[data-monster-role=selection] {
357
361
  outline-offset: -2px;
358
362
  }
359
363
 
364
+ [data-monster-role=create-option] {
365
+ align-items: center;
366
+ box-sizing: border-box;
367
+ cursor: pointer;
368
+ display: flex;
369
+ min-block-size: 2.75rem;
370
+ padding: var(--monster-select-option-padding, 2px 6px);
371
+ width: 100%;
372
+ }
373
+
374
+ [data-monster-role=create-option][data-monster-visibility=hidden] {
375
+ display: none;
376
+ }
377
+
378
+ [data-monster-role=create-option] > button {
379
+ appearance: none;
380
+ border: 0;
381
+ cursor: inherit;
382
+ display: inline-flex;
383
+ font-family: inherit;
384
+ gap: 0.4em;
385
+ pointer-events: none;
386
+ }
387
+
388
+ [data-monster-role=create-option][data-monster-focused=true] {
389
+ outline: 1px dashed var(--monster-color-selection-2);
390
+ outline-offset: -2px;
391
+ }
392
+
393
+ [data-monster-role=create-option-icon] {
394
+ font-size: 1.15em;
395
+ line-height: 0.75;
396
+ }
397
+
360
398
  @media (prefers-color-scheme: dark) {
361
399
 
362
- [data-monster-role=option][data-monster-focused=true] {
400
+ [data-monster-role=option][data-monster-focused=true],
401
+ [data-monster-role=create-option][data-monster-focused=true] {
363
402
  outline: 1px dashed var(--monster-color-selection-4);
364
403
  }
365
404
  }
@@ -29,11 +29,11 @@ try {
29
29
  --monster-select-control-min-block-size,var(--monster-control-min-block-size,2.5rem)
30
30
  );padding:var(--monster-select-control-padding,.4rem .6rem);margin:0;outline:none;box-sizing:border-box;accent-color:var(
31
31
  --monster-theme-accent,var(--monster-color-secondary-2)
32
- );-o-border-image:initial;border-image:initial;border-width:var(--monster-theme-control-border-width);border-radius:var(--monster-theme-control-border-radius);border-style:var(--monster-theme-control-border-style);border-color:var(--monster-theme-control-border-color);background-color:var(--monster-bg-color-primary-1);color:var(--monster-color-primary-1);display:flex;align-items:center;overflow:hidden;width:-webkit-fill-available;width:-moz-available;width:fill-available}div[data-monster-role=popper]{z-index:var(--monster-z-index-popover)}@media (prefers-color-scheme:light){[data-monster-role=control]{background-color:var(--monster-bg-color-primary-1);color:var(--monster-color-primary-1);border-color:var(--monster-theme-control-border-color)}[data-monster-role=control]:focus{outline:1px dashed var(--monster-color-selection-4);outline-offset:2px}}[data-monster-role=control]:hover{transition:background .8s,color .25s .0833333333s;box-shadow:var(--monster-box-shadow-2)}:host([disabled]) [data-monster-role=control]{cursor:not-allowed}:host([disabled]) [data-monster-role=status-or-remove-badges],[data-monster-role=control][disabled] [data-monster-role=status-or-remove-badges]{display:none}div[data-monster-role=selection]{display:flex;gap:5px;margin:var(--monster-select-selection-margin,0 5px);flex-direction:row;justify-content:flex-start;align-items:center;flex-wrap:var(--monster-select-selection-flex-wrap,wrap);min-height:0}[data-monster-role=option-control]{block-size:var(--monster-select-option-control-size,1rem);flex:0 0 auto;inline-size:var(--monster-select-option-control-size,1rem);margin-right:8px;min-block-size:var(--monster-select-option-control-size,1rem);min-inline-size:var(--monster-select-option-control-size,1rem)}[data-monster-role=badge]{display:inline-flex}[data-monster-role=badge-label]{display:flex;flex-direction:row;align-content:center;justify-content:space-between;align-items:center;gap:7px}[data-monster-role=filter],[data-monster-role=popper-filter]{visibility:hidden;order:99999999;display:flex;flex-grow:200}[data-monster-role=filter].active{visibility:visible;min-width:40%;border:0;outline:none;background-color:var(--monster-bg-color-primary-2);border-color:var(--monster-bg-color-primary-3);color:var(--monster-color-primary-2);border-bottom:1px solid var(--monster-bg-color-primary-3)}.active[data-monster-role=filter][name=popper-filter]{width:calc(100% - var(--monster-border-width)*2);height:1.5em;padding:2px;margin:2.5em}.option-filter-control{margin:-1.1em -1.1em .3em;height:2.5em;background-color:var(--monster-bg-color-primary-2);display:flex;align-items:center}[data-monster-role=remove-badge]{order:2;height:16px;-webkit-mask-image:url(\"data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='currentColor' viewBox='0 0 16 16'%3E%3Cpath d='M4.646 4.646a.5.5 0 0 1 .708 0L8 7.293l2.646-2.647a.5.5 0 0 1 .708.708L8.707 8l2.647 2.646a.5.5 0 0 1-.708.708L8 8.707l-2.646 2.647a.5.5 0 0 1-.708-.708L7.293 8 4.646 5.354a.5.5 0 0 1 0-.708'/%3E%3C/svg%3E\");mask-image:url(\"data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='currentColor' viewBox='0 0 16 16'%3E%3Cpath d='M4.646 4.646a.5.5 0 0 1 .708 0L8 7.293l2.646-2.647a.5.5 0 0 1 .708.708L8.707 8l2.647 2.646a.5.5 0 0 1-.708.708L8 8.707l-2.646 2.647a.5.5 0 0 1-.708-.708L7.293 8 4.646 5.354a.5.5 0 0 1 0-.708'/%3E%3C/svg%3E\");background-color:var(--monster-bg-color-primary-2);margin-left:5px}[data-monster-role=remove-badge],[data-monster-role=status-or-remove-badges]{width:16px;min-height:16px;-webkit-mask-size:.8em;mask-size:.8em;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-position:center center;mask-position:center center}[data-monster-role=status-or-remove-badges]{-webkit-appearance:none;-moz-appearance:none;appearance:none;-webkit-mask-image:url(\"data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='currentColor' viewBox='0 0 16 16'%3E%3Cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708'/%3E%3C/svg%3E\");mask-image:url(\"data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='currentColor' viewBox='0 0 16 16'%3E%3Cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708'/%3E%3C/svg%3E\");background-color:var(--monster-bg-color-primary-4)}.open[data-monster-role=status-or-remove-badges]{-webkit-mask-image:url(\"data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='currentColor' viewBox='0 0 16 16'%3E%3Cpath fill-rule='evenodd' d='M7.646 4.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1-.708.708L8 5.707l-5.646 5.647a.5.5 0 0 1-.708-.708z'/%3E%3C/svg%3E\");mask-image:url(\"data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='currentColor' viewBox='0 0 16 16'%3E%3Cpath fill-rule='evenodd' d='M7.646 4.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1-.708.708L8 5.707l-5.646 5.647a.5.5 0 0 1-.708-.708z'/%3E%3C/svg%3E\");background-color:var(--monster-bg-color-primary-4)}.empty[data-monster-role=status-or-remove-badges]{-webkit-mask-image:url(\"data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='currentColor' class='bi bi-dash-circle' viewBox='0 0 16 16'%3E%3Cpath d='M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14m0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16'/%3E%3Cpath d='M4 8a.5.5 0 0 1 .5-.5h7a.5.5 0 0 1 0 1h-7A.5.5 0 0 1 4 8'/%3E%3C/svg%3E\");mask-image:url(\"data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='currentColor' class='bi bi-dash-circle' viewBox='0 0 16 16'%3E%3Cpath d='M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14m0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16'/%3E%3Cpath d='M4 8a.5.5 0 0 1 .5-.5h7a.5.5 0 0 1 0 1h-7A.5.5 0 0 1 4 8'/%3E%3C/svg%3E\");background-color:var(--monster-bg-color-primary-4)}.clear[data-monster-role=status-or-remove-badges]{-webkit-mask-image:url(\"data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='currentColor' viewBox='0 0 16 16'%3E%3Cpath d='M4.646 4.646a.5.5 0 0 1 .708 0L8 7.293l2.646-2.647a.5.5 0 0 1 .708.708L8.707 8l2.647 2.646a.5.5 0 0 1-.708.708L8 8.707l-2.646 2.647a.5.5 0 0 1-.708-.708L7.293 8 4.646 5.354a.5.5 0 0 1 0-.708'/%3E%3C/svg%3E\");mask-image:url(\"data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='currentColor' viewBox='0 0 16 16'%3E%3Cpath d='M4.646 4.646a.5.5 0 0 1 .708 0L8 7.293l2.646-2.647a.5.5 0 0 1 .708.708L8.707 8l2.647 2.646a.5.5 0 0 1-.708.708L8 8.707l-2.646 2.647a.5.5 0 0 1-.708-.708L7.293 8 4.646 5.354a.5.5 0 0 1 0-.708'/%3E%3C/svg%3E\");background-color:var(--monster-bg-color-primary-4)}.error[data-monster-role=status-or-remove-badges]{-webkit-mask-image:url(\"data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='currentColor' viewBox='0 0 16 16'%3E%3Cpath d='M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14m0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16'/%3E%3Cpath d='M7.002 11a1 1 0 1 1 2 0 1 1 0 0 1-2 0M7.1 4.995a.905.905 0 1 1 1.8 0l-.35 3.507a.552.552 0 0 1-1.1 0z'/%3E%3C/svg%3E\");mask-image:url(\"data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='currentColor' viewBox='0 0 16 16'%3E%3Cpath d='M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14m0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16'/%3E%3Cpath d='M7.002 11a1 1 0 1 1 2 0 1 1 0 0 1-2 0M7.1 4.995a.905.905 0 1 1 1.8 0l-.35 3.507a.552.552 0 0 1-1.1 0z'/%3E%3C/svg%3E\");background-color:var(--monster-bg-color-error-1)}@media (prefers-color-scheme:dark){.error[data-monster-role=status-or-remove-badges]{-webkit-mask-image:url(\"data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='currentColor' viewBox='0 0 16 16'%3E%3Cpath d='M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14m0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16'/%3E%3Cpath d='M7.002 11a1 1 0 1 1 2 0 1 1 0 0 1-2 0M7.1 4.995a.905.905 0 1 1 1.8 0l-.35 3.507a.552.552 0 0 1-1.1 0z'/%3E%3C/svg%3E\");mask-image:url(\"data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='currentColor' viewBox='0 0 16 16'%3E%3Cpath d='M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14m0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16'/%3E%3Cpath d='M7.002 11a1 1 0 1 1 2 0 1 1 0 0 1-2 0M7.1 4.995a.905.905 0 1 1 1.8 0l-.35 3.507a.552.552 0 0 1-1.1 0z'/%3E%3C/svg%3E\");background-color:var(--monster-color-error-4)}}.loading[data-monster-role=status-or-remove-badges]{-webkit-mask-image:url(\"data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='currentColor' class='activity' viewBox='0 0 16 16'%3E%3Ccircle cx='8' cy='8' r='8'/%3E%3C/svg%3E\");mask-image:url(\"data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='currentColor' class='activity' viewBox='0 0 16 16'%3E%3Ccircle cx='8' cy='8' r='8'/%3E%3C/svg%3E\");background-color:var(--monster-bg-color-primary-4);animation-name:activity;animation-duration:1s;animation-iteration-count:infinite;animation-timing-function:cubic-bezier(0,0,.2,1)}@keyframes activity{0%{transform:scale(1)}50%{transform:scale(.1)}to{transform:scale(1)}}[data-monster-role=options]{flex-direction:column;flex-grow:1;scrollbar-gutter:stable;scrollbar-color:var(--monster-color-primary-1) var(--monster-bg-color-primary-1);scrollbar-width:thin;overflow-x:hidden}[data-monster-role=option],[data-monster-role=options]{box-sizing:border-box;display:flex;width:100%;max-width:100%;min-width:0}[data-monster-role=option]{padding:var(--monster-select-option-padding,2px 6px);align-items:center}[data-monster-role=option] label{justify-content:flex-start}[data-monster-role=option] label,[data-monster-role=option] label>div{display:flex;flex-direction:row;align-items:center;width:100%;max-width:100%;min-width:0}[data-monster-role=option] label>div{justify-content:space-between;outline:none}[part=option-label]{max-width:100%;min-width:0;overflow-wrap:anywhere;word-break:break-word}.selected{background-color:var(
32
+ );-o-border-image:initial;border-image:initial;border-width:var(--monster-theme-control-border-width);border-radius:var(--monster-theme-control-border-radius);border-style:var(--monster-theme-control-border-style);border-color:var(--monster-theme-control-border-color);background-color:var(--monster-bg-color-primary-1);color:var(--monster-color-primary-1);display:flex;align-items:center;overflow:hidden;width:-webkit-fill-available;width:-moz-available;width:fill-available}div[data-monster-role=popper]{z-index:var(--monster-z-index-popover)}div[data-monster-role=popper]>[part=content]{flex-direction:column}@media (prefers-color-scheme:light){[data-monster-role=control]{background-color:var(--monster-bg-color-primary-1);color:var(--monster-color-primary-1);border-color:var(--monster-theme-control-border-color)}[data-monster-role=control]:focus{outline:1px dashed var(--monster-color-selection-4);outline-offset:2px}}[data-monster-role=control]:hover{transition:background .8s,color .25s .0833333333s;box-shadow:var(--monster-box-shadow-2)}:host([disabled]) [data-monster-role=control]{cursor:not-allowed}:host([disabled]) [data-monster-role=status-or-remove-badges],[data-monster-role=control][disabled] [data-monster-role=status-or-remove-badges]{display:none}div[data-monster-role=selection]{display:flex;gap:5px;margin:var(--monster-select-selection-margin,0 5px);flex-direction:row;justify-content:flex-start;align-items:center;flex-wrap:var(--monster-select-selection-flex-wrap,wrap);min-height:0}[data-monster-role=option-control]{block-size:var(--monster-select-option-control-size,1rem);flex:0 0 auto;inline-size:var(--monster-select-option-control-size,1rem);margin-right:8px;min-block-size:var(--monster-select-option-control-size,1rem);min-inline-size:var(--monster-select-option-control-size,1rem)}[data-monster-role=badge]{display:inline-flex}[data-monster-role=badge-label]{display:flex;flex-direction:row;align-content:center;justify-content:space-between;align-items:center;gap:7px}[data-monster-role=filter],[data-monster-role=popper-filter]{visibility:hidden;order:99999999;display:flex;flex-grow:200}[data-monster-role=filter].active{visibility:visible;min-width:40%;border:0;outline:none;background-color:var(--monster-bg-color-primary-2);border-color:var(--monster-bg-color-primary-3);color:var(--monster-color-primary-2);border-bottom:1px solid var(--monster-bg-color-primary-3)}.active[data-monster-role=filter][name=popper-filter]{width:calc(100% - var(--monster-border-width)*2);height:1.5em;padding:2px;margin:2.5em}.option-filter-control{margin:-1.1em -1.1em .3em;height:2.5em;background-color:var(--monster-bg-color-primary-2);display:flex;align-items:center}[data-monster-role=remove-badge]{order:2;height:16px;-webkit-mask-image:url(\"data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='currentColor' viewBox='0 0 16 16'%3E%3Cpath d='M4.646 4.646a.5.5 0 0 1 .708 0L8 7.293l2.646-2.647a.5.5 0 0 1 .708.708L8.707 8l2.647 2.646a.5.5 0 0 1-.708.708L8 8.707l-2.646 2.647a.5.5 0 0 1-.708-.708L7.293 8 4.646 5.354a.5.5 0 0 1 0-.708'/%3E%3C/svg%3E\");mask-image:url(\"data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='currentColor' viewBox='0 0 16 16'%3E%3Cpath d='M4.646 4.646a.5.5 0 0 1 .708 0L8 7.293l2.646-2.647a.5.5 0 0 1 .708.708L8.707 8l2.647 2.646a.5.5 0 0 1-.708.708L8 8.707l-2.646 2.647a.5.5 0 0 1-.708-.708L7.293 8 4.646 5.354a.5.5 0 0 1 0-.708'/%3E%3C/svg%3E\");background-color:var(--monster-bg-color-primary-2);margin-left:5px}[data-monster-role=remove-badge],[data-monster-role=status-or-remove-badges]{width:16px;min-height:16px;-webkit-mask-size:.8em;mask-size:.8em;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-position:center center;mask-position:center center}[data-monster-role=status-or-remove-badges]{-webkit-appearance:none;-moz-appearance:none;appearance:none;-webkit-mask-image:url(\"data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='currentColor' viewBox='0 0 16 16'%3E%3Cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708'/%3E%3C/svg%3E\");mask-image:url(\"data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='currentColor' viewBox='0 0 16 16'%3E%3Cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708'/%3E%3C/svg%3E\");background-color:var(--monster-bg-color-primary-4)}.open[data-monster-role=status-or-remove-badges]{-webkit-mask-image:url(\"data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='currentColor' viewBox='0 0 16 16'%3E%3Cpath fill-rule='evenodd' d='M7.646 4.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1-.708.708L8 5.707l-5.646 5.647a.5.5 0 0 1-.708-.708z'/%3E%3C/svg%3E\");mask-image:url(\"data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='currentColor' viewBox='0 0 16 16'%3E%3Cpath fill-rule='evenodd' d='M7.646 4.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1-.708.708L8 5.707l-5.646 5.647a.5.5 0 0 1-.708-.708z'/%3E%3C/svg%3E\");background-color:var(--monster-bg-color-primary-4)}.empty[data-monster-role=status-or-remove-badges]{-webkit-mask-image:url(\"data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='currentColor' class='bi bi-dash-circle' viewBox='0 0 16 16'%3E%3Cpath d='M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14m0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16'/%3E%3Cpath d='M4 8a.5.5 0 0 1 .5-.5h7a.5.5 0 0 1 0 1h-7A.5.5 0 0 1 4 8'/%3E%3C/svg%3E\");mask-image:url(\"data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='currentColor' class='bi bi-dash-circle' viewBox='0 0 16 16'%3E%3Cpath d='M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14m0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16'/%3E%3Cpath d='M4 8a.5.5 0 0 1 .5-.5h7a.5.5 0 0 1 0 1h-7A.5.5 0 0 1 4 8'/%3E%3C/svg%3E\");background-color:var(--monster-bg-color-primary-4)}.clear[data-monster-role=status-or-remove-badges]{-webkit-mask-image:url(\"data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='currentColor' viewBox='0 0 16 16'%3E%3Cpath d='M4.646 4.646a.5.5 0 0 1 .708 0L8 7.293l2.646-2.647a.5.5 0 0 1 .708.708L8.707 8l2.647 2.646a.5.5 0 0 1-.708.708L8 8.707l-2.646 2.647a.5.5 0 0 1-.708-.708L7.293 8 4.646 5.354a.5.5 0 0 1 0-.708'/%3E%3C/svg%3E\");mask-image:url(\"data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='currentColor' viewBox='0 0 16 16'%3E%3Cpath d='M4.646 4.646a.5.5 0 0 1 .708 0L8 7.293l2.646-2.647a.5.5 0 0 1 .708.708L8.707 8l2.647 2.646a.5.5 0 0 1-.708.708L8 8.707l-2.646 2.647a.5.5 0 0 1-.708-.708L7.293 8 4.646 5.354a.5.5 0 0 1 0-.708'/%3E%3C/svg%3E\");background-color:var(--monster-bg-color-primary-4)}.error[data-monster-role=status-or-remove-badges]{-webkit-mask-image:url(\"data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='currentColor' viewBox='0 0 16 16'%3E%3Cpath d='M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14m0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16'/%3E%3Cpath d='M7.002 11a1 1 0 1 1 2 0 1 1 0 0 1-2 0M7.1 4.995a.905.905 0 1 1 1.8 0l-.35 3.507a.552.552 0 0 1-1.1 0z'/%3E%3C/svg%3E\");mask-image:url(\"data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='currentColor' viewBox='0 0 16 16'%3E%3Cpath d='M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14m0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16'/%3E%3Cpath d='M7.002 11a1 1 0 1 1 2 0 1 1 0 0 1-2 0M7.1 4.995a.905.905 0 1 1 1.8 0l-.35 3.507a.552.552 0 0 1-1.1 0z'/%3E%3C/svg%3E\");background-color:var(--monster-bg-color-error-1)}@media (prefers-color-scheme:dark){.error[data-monster-role=status-or-remove-badges]{-webkit-mask-image:url(\"data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='currentColor' viewBox='0 0 16 16'%3E%3Cpath d='M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14m0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16'/%3E%3Cpath d='M7.002 11a1 1 0 1 1 2 0 1 1 0 0 1-2 0M7.1 4.995a.905.905 0 1 1 1.8 0l-.35 3.507a.552.552 0 0 1-1.1 0z'/%3E%3C/svg%3E\");mask-image:url(\"data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='currentColor' viewBox='0 0 16 16'%3E%3Cpath d='M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14m0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16'/%3E%3Cpath d='M7.002 11a1 1 0 1 1 2 0 1 1 0 0 1-2 0M7.1 4.995a.905.905 0 1 1 1.8 0l-.35 3.507a.552.552 0 0 1-1.1 0z'/%3E%3C/svg%3E\");background-color:var(--monster-color-error-4)}}.loading[data-monster-role=status-or-remove-badges]{-webkit-mask-image:url(\"data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='currentColor' class='activity' viewBox='0 0 16 16'%3E%3Ccircle cx='8' cy='8' r='8'/%3E%3C/svg%3E\");mask-image:url(\"data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='currentColor' class='activity' viewBox='0 0 16 16'%3E%3Ccircle cx='8' cy='8' r='8'/%3E%3C/svg%3E\");background-color:var(--monster-bg-color-primary-4);animation-name:activity;animation-duration:1s;animation-iteration-count:infinite;animation-timing-function:cubic-bezier(0,0,.2,1)}@keyframes activity{0%{transform:scale(1)}50%{transform:scale(.1)}to{transform:scale(1)}}[data-monster-role=options]{flex-direction:column;flex-grow:1;scrollbar-gutter:stable;scrollbar-color:var(--monster-color-primary-1) var(--monster-bg-color-primary-1);scrollbar-width:thin;overflow-x:hidden}[data-monster-role=option],[data-monster-role=options]{box-sizing:border-box;display:flex;width:100%;max-width:100%;min-width:0}[data-monster-role=option]{padding:var(--monster-select-option-padding,2px 6px);align-items:center}[data-monster-role=option] label{justify-content:flex-start}[data-monster-role=option] label,[data-monster-role=option] label>div{display:flex;flex-direction:row;align-items:center;width:100%;max-width:100%;min-width:0}[data-monster-role=option] label>div{justify-content:space-between;outline:none}[part=option-label]{max-width:100%;min-width:0;overflow-wrap:anywhere;word-break:break-word}.selected{background-color:var(
33
33
  --monster-theme-accent-subtle,var(--monster-bg-color-primary-2)
34
34
  );color:var(
35
35
  --monster-theme-accent,var(--monster-color-primary-2)
36
- )}[data-monster-role=option][data-monster-filtered=true],[data-monster-role=option][data-monster-visibility=hidden]{display:none}[data-monster-role=option][data-monster-focused=true]{outline:1px dashed var(--monster-color-selection-2);outline-offset:-2px}@media (prefers-color-scheme:dark){[data-monster-role=option][data-monster-focused=true]{outline:1px dashed var(--monster-color-selection-4)}}[data-monster-role=filter]::-moz-placeholder{color:var(--monster-color-primary-2);background-color:var(--monster-bg-color-primary-2)}[data-monster-role=filter]::placeholder{color:var(--monster-color-primary-2);background-color:var(--monster-bg-color-primary-2)}[data-monster-role=option]>input:focus,[data-monster-role=option]>label:focus{outline:none}[data-monster-role=selection-messages]:empty:before,[data-monster-role=summary-messages]:empty:before{content:\"\u00A0\"}.in-button-bar{border-width:var(--monster-border-width)!important;border-style:var(--monster-border-style)!important;border-color:var(--monster-bg-color-primary-4)!important}[data-monster-role=pagination-footer]{align-items:center;-moz-column-gap:var(--monster-select-pagination-footer-column-gap,.75rem);column-gap:var(--monster-select-pagination-footer-column-gap,.75rem);display:flex;flex-wrap:wrap;justify-content:flex-end;min-width:0}[data-monster-role=remote-info]{flex:1 1 12rem;margin:var(--monster-select-pagination-footer-margin-top,.5rem) 0 0;min-width:0}[data-monster-role=remote-info]:empty{flex-basis:0;margin:0}[data-monster-role=remote-info] span{text-align:left;white-space:normal}[data-monster-role=pagination]{flex:0 0 auto;margin-left:auto;margin-top:var(--monster-select-pagination-footer-margin-top,.5rem);max-width:100%}[data-monster-role=pagination]::part(nav){justify-content:flex-end;width:auto}[data-monster-role=pagination]::part(list){grid-template-columns:repeat(3,auto);justify-content:end;margin-bottom:0;margin-top:0;width:auto}
36
+ )}[data-monster-role=option][data-monster-filtered=true],[data-monster-role=option][data-monster-visibility=hidden]{display:none}[data-monster-role=option][data-monster-focused=true]{outline:1px dashed var(--monster-color-selection-2);outline-offset:-2px}[data-monster-role=create-option]{align-items:center;box-sizing:border-box;cursor:pointer;display:flex;min-block-size:2.75rem;padding:var(--monster-select-option-padding,2px 6px);width:100%}[data-monster-role=create-option][data-monster-visibility=hidden]{display:none}[data-monster-role=create-option]>button{-webkit-appearance:none;-moz-appearance:none;appearance:none;border:0;cursor:inherit;display:inline-flex;font-family:inherit;gap:.4em;pointer-events:none}[data-monster-role=create-option][data-monster-focused=true]{outline:1px dashed var(--monster-color-selection-2);outline-offset:-2px}[data-monster-role=create-option-icon]{font-size:1.15em;line-height:.75}@media (prefers-color-scheme:dark){[data-monster-role=create-option][data-monster-focused=true],[data-monster-role=option][data-monster-focused=true]{outline:1px dashed var(--monster-color-selection-4)}}[data-monster-role=filter]::-moz-placeholder{color:var(--monster-color-primary-2);background-color:var(--monster-bg-color-primary-2)}[data-monster-role=filter]::placeholder{color:var(--monster-color-primary-2);background-color:var(--monster-bg-color-primary-2)}[data-monster-role=option]>input:focus,[data-monster-role=option]>label:focus{outline:none}[data-monster-role=selection-messages]:empty:before,[data-monster-role=summary-messages]:empty:before{content:\"\u00A0\"}.in-button-bar{border-width:var(--monster-border-width)!important;border-style:var(--monster-border-style)!important;border-color:var(--monster-bg-color-primary-4)!important}[data-monster-role=pagination-footer]{align-items:center;-moz-column-gap:var(--monster-select-pagination-footer-column-gap,.75rem);column-gap:var(--monster-select-pagination-footer-column-gap,.75rem);display:flex;flex-wrap:wrap;justify-content:flex-end;min-width:0}[data-monster-role=remote-info]{flex:1 1 12rem;margin:var(--monster-select-pagination-footer-margin-top,.5rem) 0 0;min-width:0}[data-monster-role=remote-info]:empty{flex-basis:0;margin:0}[data-monster-role=remote-info] span{text-align:left;white-space:normal}[data-monster-role=pagination]{flex:0 0 auto;margin-left:auto;margin-top:var(--monster-select-pagination-footer-margin-top,.5rem);max-width:100%}[data-monster-role=pagination]::part(nav){justify-content:flex-end;width:auto}[data-monster-role=pagination]::part(list){grid-template-columns:repeat(3,auto);justify-content:end;margin-bottom:0;margin-top:0;width:auto}
37
37
  }`,
38
38
  0,
39
39
  );
@@ -131,6 +131,40 @@ describe("MessageStateButton", function () {
131
131
  });
132
132
  });
133
133
 
134
+ describe("state changes", function () {
135
+ afterEach(() => {
136
+ let mocks = document.getElementById("mocks");
137
+ mocks.innerHTML = "";
138
+ });
139
+
140
+ it("should expose inner state transitions on the public host", function (done) {
141
+ let mocks = document.getElementById("mocks");
142
+ const button = document.createElement("monster-message-state-button");
143
+ const events = [];
144
+ button.innerHTML = "Save";
145
+ button.addEventListener("monster-state-changed", (event) => {
146
+ events.push(event);
147
+ });
148
+ mocks.appendChild(button);
149
+
150
+ setTimeout(() => {
151
+ try {
152
+ button.setState("successful");
153
+
154
+ expect(events).to.have.length(1);
155
+ expect(events[0].target).to.equal(button);
156
+ expect(events[0].detail).to.deep.equal({
157
+ state: "successful",
158
+ previousState: "stateless",
159
+ });
160
+ done();
161
+ } catch (e) {
162
+ done(e);
163
+ }
164
+ }, 0);
165
+ });
166
+ });
167
+
134
168
  describe("lifecycle safety", function () {
135
169
  afterEach(() => {
136
170
  let mocks = document.getElementById("mocks");
@@ -560,6 +560,147 @@ describe('Select', function () {
560
560
  }
561
561
  });
562
562
 
563
+ describe('Create missing filtered options', function () {
564
+ this.timeout(6000);
565
+
566
+ async function createSelect(options = {}) {
567
+ const mocks = document.getElementById('mocks');
568
+ mocks.innerHTML = '';
569
+ const select = document.createElement('monster-select');
570
+ select.setOption('type', 'checkbox');
571
+ select.setOption('filter.mode', 'options');
572
+ select.setOption('filter.position', 'popper');
573
+ select.setOption('options', [
574
+ {label: 'Alpha', value: 'alpha'}
575
+ ]);
576
+ for (const [path, value] of Object.entries(options)) {
577
+ select.setOption(path, value);
578
+ }
579
+ mocks.appendChild(select);
580
+
581
+ await waitForCondition(() => {
582
+ return select.shadowRoot?.querySelector(
583
+ '[data-monster-role="filter"][name="popper-filter"]'
584
+ ) instanceof HTMLInputElement;
585
+ });
586
+
587
+ return select;
588
+ }
589
+
590
+ function enterFilterValue(select, value) {
591
+ const input = select.shadowRoot.querySelector(
592
+ '[data-monster-role="filter"][name="popper-filter"]'
593
+ );
594
+ input.value = value;
595
+ input.dispatchEvent(new Event('input', {
596
+ bubbles: true,
597
+ composed: true
598
+ }));
599
+ return input;
600
+ }
601
+
602
+ afterEach(() => {
603
+ document.getElementById('mocks').innerHTML = '';
604
+ });
605
+
606
+ it('should stay opt-in and suppress empty or existing values', async function () {
607
+ const select = await createSelect();
608
+ const action = select.shadowRoot.querySelector(
609
+ '[data-monster-role="create-option"]'
610
+ );
611
+
612
+ enterFilterValue(select, 'Beta');
613
+ expect(action.getAttribute('data-monster-visibility')).to.equal('hidden');
614
+
615
+ select.setOption('features.createOption', true);
616
+ enterFilterValue(select, ' ');
617
+ expect(action.getAttribute('data-monster-visibility')).to.equal('hidden');
618
+
619
+ enterFilterValue(select, 'ALPHA');
620
+ expect(action.getAttribute('data-monster-visibility')).to.equal('hidden');
621
+ });
622
+
623
+ it('should create badge selections with mouse and touch', async function () {
624
+ const select = await createSelect({
625
+ 'features.createOption': true,
626
+ 'labels.create-option': 'Add tag: ${label}'
627
+ });
628
+
629
+ enterFilterValue(select, 'Beta Tag');
630
+ let action = select.shadowRoot.querySelector(
631
+ '[data-monster-role="create-option"]'
632
+ );
633
+ expect(action.getAttribute('data-monster-visibility')).to.equal('visible');
634
+ expect(action.textContent).to.contain('Add tag: Beta Tag');
635
+ expect(action.querySelector('button').className).to.contain('monster-badge-primary');
636
+ action.click();
637
+
638
+ await waitForCondition(() => select.value.includes('Beta Tag'));
639
+ expect(select.getOption('options').some((option) => {
640
+ return option.value === 'Beta Tag';
641
+ })).to.equal(true);
642
+
643
+ enterFilterValue(select, 'Touch Tag');
644
+ action = select.shadowRoot.querySelector(
645
+ '[data-monster-role="create-option"]'
646
+ );
647
+ action.dispatchEvent(new Event('touch', {
648
+ bubbles: true,
649
+ composed: true,
650
+ cancelable: true
651
+ }));
652
+
653
+ await waitForCondition(() => select.value.includes('Touch Tag'));
654
+ await waitForCondition(() => {
655
+ return select.shadowRoot.querySelectorAll(
656
+ '[data-monster-role="badge-label"]'
657
+ ).length === 2;
658
+ });
659
+ const badges = Array.from(select.shadowRoot.querySelectorAll(
660
+ '[data-monster-role="badge-label"]'
661
+ )).map((badge) => badge.textContent.trim());
662
+ expect(badges).to.include('Beta Tag');
663
+ expect(badges).to.include('Touch Tag');
664
+ });
665
+
666
+ it('should create a configured option from the filter with Enter', async function () {
667
+ const select = await createSelect({
668
+ 'features.createOption': true,
669
+ 'formatter.createOption': (filterValue) => ({
670
+ label: filterValue.toUpperCase(),
671
+ value: `tag:${filterValue.toLowerCase().replaceAll(' ', '-')}`
672
+ })
673
+ });
674
+ let createdOption;
675
+ select.addEventListener('monster-option-created', (event) => {
676
+ createdOption = event.detail.option;
677
+ });
678
+
679
+ const input = enterFilterValue(select, 'New Topic');
680
+ input.dispatchEvent(new KeyboardEvent('keydown', {
681
+ code: 'Enter',
682
+ bubbles: true,
683
+ composed: true,
684
+ cancelable: true
685
+ }));
686
+
687
+ await waitForCondition(() => select.value.includes('tag:new-topic'));
688
+ await waitForCondition(() => createdOption !== undefined);
689
+ await waitForCondition(() => {
690
+ return select.shadowRoot.querySelector(
691
+ '[data-monster-role="badge-label"]'
692
+ );
693
+ });
694
+ expect(createdOption).to.deep.include({
695
+ label: 'NEW TOPIC',
696
+ value: 'tag:new-topic'
697
+ });
698
+ expect(select.shadowRoot.querySelector(
699
+ '[data-monster-role="badge-label"]'
700
+ ).textContent.trim()).to.equal('NEW TOPIC');
701
+ });
702
+ });
703
+
563
704
  describe('With fetch', function () {
564
705
 
565
706
  beforeEach((done) => {
@@ -124,7 +124,52 @@ describe('StateButton', function () {
124
124
 
125
125
 
126
126
  });
127
+
128
+ it('should emit state transitions without duplicate events', function () {
129
+ let mocks = document.getElementById('mocks');
130
+ const button = document.createElement('monster-state-button');
131
+ const events = [];
132
+ button.addEventListener('monster-state-changed', event => events.push(event));
133
+ mocks.appendChild(button);
134
+
135
+ button.setState('activity');
136
+ button.setState('activity');
137
+ button.setState('successful');
138
+ button.removeState();
139
+ button.removeState();
140
+
141
+ expect(events).to.have.length(3);
142
+ expect(events.map(event => event.detail)).to.deep.equal([
143
+ {state: 'activity', previousState: 'stateless'},
144
+ {state: 'successful', previousState: 'activity'},
145
+ {state: 'stateless', previousState: 'successful'},
146
+ ]);
147
+ expect(events.every(event => event.bubbles)).to.be.true;
148
+ expect(events.every(event => event.composed)).to.be.true;
149
+ });
150
+
151
+ it('should emit the automatic timeout transition', function (done) {
152
+ let mocks = document.getElementById('mocks');
153
+ const button = document.createElement('monster-state-button');
154
+ const details = [];
155
+ button.addEventListener('monster-state-changed', event => details.push(event.detail));
156
+ mocks.appendChild(button);
157
+
158
+ button.setState('activity', 10);
159
+
160
+ setTimeout(() => {
161
+ try {
162
+ expect(details).to.deep.equal([
163
+ {state: 'activity', previousState: 'stateless'},
164
+ {state: 'stateless', previousState: 'activity'},
165
+ ]);
166
+ done();
167
+ } catch (e) {
168
+ done(e);
169
+ }
170
+ }, 30);
171
+ });
127
172
  });
128
173
 
129
174
 
130
- });
175
+ });