@lion/ui 0.1.2 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/components/combobox/src/LionCombobox.js +34 -65
  2. package/components/combobox/src/utils/makeMatchingTextBold.js +68 -0
  3. package/components/combobox/test/lion-combobox.test.js +166 -1
  4. package/custom-elements.json +256 -0
  5. package/dist-types/components/combobox/src/LionCombobox.d.ts +13 -0
  6. package/dist-types/components/combobox/src/LionCombobox.d.ts.map +1 -1
  7. package/dist-types/components/combobox/src/utils/makeMatchingTextBold.d.ts +10 -0
  8. package/dist-types/components/combobox/src/utils/makeMatchingTextBold.d.ts.map +1 -0
  9. package/dist-types/components/combobox/test/lion-combobox.test.d.ts +1 -0
  10. package/dist-types/components/combobox/test/lion-combobox.test.d.ts.map +1 -1
  11. package/dist-types/exports/combobox.d.ts +1 -0
  12. package/dist-types/exports/types/core.d.ts +2 -0
  13. package/dist-types/exports/types/core.d.ts.map +1 -1
  14. package/dist-types/tsconfig.tsbuildinfo +1 -1
  15. package/docs/components/checkbox-group/overview.md +2 -2
  16. package/docs/components/collapsible/overview.md +1 -1
  17. package/docs/components/combobox/overview.md +1 -1
  18. package/docs/components/combobox/use-cases.md +40 -16
  19. package/docs/components/dialog/overview.md +1 -1
  20. package/docs/components/fieldset/overview.md +5 -5
  21. package/docs/components/form/overview.md +3 -3
  22. package/docs/components/icon/use-cases.md +1 -1
  23. package/docs/components/input/overview.md +1 -1
  24. package/docs/components/input-amount/overview.md +3 -3
  25. package/docs/components/input-date/overview.md +3 -3
  26. package/docs/components/input-datepicker/overview.md +5 -5
  27. package/docs/components/input-email/overview.md +2 -2
  28. package/docs/components/input-iban/overview.md +2 -2
  29. package/docs/components/input-range/overview.md +2 -2
  30. package/docs/components/input-stepper/overview.md +1 -1
  31. package/docs/components/input-tel/overview.md +1 -1
  32. package/docs/components/input-tel/use-cases.md +6 -6
  33. package/docs/components/input-tel-dropdown/overview.md +1 -1
  34. package/docs/components/listbox/src/listboxData.js +66 -0
  35. package/docs/components/radio-group/overview.md +3 -3
  36. package/docs/components/select-rich/use-cases.md +1 -1
  37. package/docs/components/tabs/examples.md +1 -1
  38. package/docs/fundamentals/index.md +4 -4
  39. package/docs/fundamentals/node-tools/providence-analytics/LocalConfiguration.md +1 -1
  40. package/docs/fundamentals/node-tools/providence-analytics/QueryResult.md +1 -1
  41. package/docs/fundamentals/node-tools/providence-analytics/analyzer.md +1 -1
  42. package/docs/fundamentals/node-tools/providence-analytics/dashboard.md +1 -1
  43. package/docs/fundamentals/node-tools/providence-analytics/overview.md +3 -3
  44. package/docs/fundamentals/node-tools/publish-docs/overview.md +3 -3
  45. package/docs/fundamentals/node-tools/rocket-preset-extend-lion-docs/overview.md +2 -2
  46. package/docs/fundamentals/systems/core/overview.md +1 -1
  47. package/docs/fundamentals/systems/form/formatting-and-parsing.md +4 -4
  48. package/docs/fundamentals/systems/form/model-value.md +1 -1
  49. package/docs/fundamentals/systems/form/overview.md +28 -28
  50. package/docs/fundamentals/systems/form/validate.md +1 -1
  51. package/docs/fundamentals/systems/localize/overview.md +4 -4
  52. package/docs/fundamentals/systems/overlays/configuration.md +2 -2
  53. package/docs/fundamentals/systems/overlays/overview.md +5 -5
  54. package/docs/fundamentals/systems/overlays/use-cases.md +2 -2
  55. package/docs/fundamentals/tools/helpers/overview.md +1 -1
  56. package/docs/fundamentals/tools/singleton-manager/overview.md +4 -4
  57. package/docs/guides/how-to/create-a-custom-field.md +5 -5
  58. package/docs/guides/how-to/extend-a-native-input.md +2 -2
  59. package/docs/guides/index.md +8 -8
  60. package/docs/guides/principles/index.md +5 -5
  61. package/docs/guides/principles/styling.md +1 -1
  62. package/exports/combobox.js +4 -0
  63. package/exports/types/core.ts +2 -0
  64. package/package.json +8 -1
@@ -1,62 +1,11 @@
1
- import { html, css } from 'lit';
2
1
  import { browserDetection } from '@lion/ui/core.js';
3
- import { OverlayMixin, withDropdownConfig } from '@lion/ui/overlays.js';
4
2
  import { LionListbox } from '@lion/ui/listbox.js';
3
+ import { OverlayMixin, withDropdownConfig } from '@lion/ui/overlays.js';
4
+ import { css, html } from 'lit';
5
+ import { makeMatchingTextBold, unmakeMatchingTextBold } from './utils/makeMatchingTextBold.js';
5
6
 
6
- const matchReverseFns = new WeakMap();
7
7
  const matchA11ySpanReverseFns = new WeakMap();
8
8
 
9
- /**
10
- * @param {Node} root
11
- * @param {string} matchingString
12
- * @param {Node} option
13
- */
14
- function makeMatchingTextBold(root, matchingString, option) {
15
- Array.from(root.childNodes).forEach(childNode => {
16
- if (childNode.nodeName === '#text') {
17
- // check for match based on nodeValue
18
-
19
- const re = new RegExp(`^(.*)(${matchingString})(.*)$`, 'i');
20
- // @ts-ignore
21
- const match = childNode.nodeValue.match(re);
22
-
23
- if (match) {
24
- // 1. textContent before match
25
-
26
- const textBefore = document.createTextNode(match[1]);
27
- root.appendChild(textBefore);
28
-
29
- // 2. matched part
30
- const boldElement = document.createElement('b');
31
- // eslint-disable-next-line prefer-destructuring
32
- boldElement.textContent = match[2];
33
- root.appendChild(boldElement);
34
-
35
- // 3. textContent after match
36
-
37
- const textAfter = document.createTextNode(match[3]);
38
- root.appendChild(textAfter);
39
- root.removeChild(childNode);
40
-
41
- matchReverseFns.set(option, () => {
42
- root.appendChild(childNode);
43
- if (root.contains(textBefore) && textBefore.parentNode !== null) {
44
- textBefore.parentNode.removeChild(textBefore);
45
- }
46
- if (root.contains(boldElement) && boldElement.parentNode !== null) {
47
- boldElement.parentNode.removeChild(boldElement);
48
- }
49
- if (root.contains(textAfter) && textAfter.parentNode !== null) {
50
- textAfter.parentNode.removeChild(textAfter);
51
- }
52
- });
53
- }
54
- } else {
55
- makeMatchingTextBold(childNode, matchingString, option);
56
- }
57
- });
58
- }
59
-
60
9
  // TODO: make ListboxOverlayMixin that is shared between SelectRich and Combobox
61
10
  // TODO: extract option matching based on 'typed character cache' and share that logic
62
11
  // on Listbox or ListNavigationWithActiveDescendantMixin
@@ -304,7 +253,6 @@ export class LionCombobox extends OverlayMixin(LionListbox) {
304
253
  * @type {'begin'|'all'}
305
254
  */
306
255
  this.matchMode = 'all';
307
-
308
256
  /**
309
257
  * When true, the listbox is open and textbox goes from a value to empty, all options are shown.
310
258
  * By default, the listbox closes on empty, similar to wai-aria example and <datalist>
@@ -642,14 +590,27 @@ export class LionCombobox extends OverlayMixin(LionListbox) {
642
590
  * @param {string} matchingString
643
591
  * @protected
644
592
  */
645
- // eslint-disable-next-line class-methods-use-this
646
593
  _onFilterMatch(option, matchingString) {
647
- makeMatchingTextBold(option, matchingString, option);
594
+ this._highlightMatchedOption(option, matchingString);
595
+
596
+ // Alternatively, an extension can add an animation here
597
+ option.style.display = '';
598
+ }
599
+
600
+ /**
601
+ * @overridable
602
+ * @param {Element} option
603
+ * @param {string} matchingString
604
+ * @protected
605
+ */
606
+ // eslint-disable-next-line class-methods-use-this
607
+ _highlightMatchedOption(option, matchingString) {
608
+ makeMatchingTextBold(option, matchingString);
648
609
 
649
610
  // For Safari, we need to add a label to the element
650
611
  if (option.textContent) {
651
612
  const a11ySpan = document.createElement('span');
652
- a11ySpan.setAttribute('aria-label', option.textContent);
613
+ a11ySpan.setAttribute('aria-label', option.textContent.replace(/\s+/g, ' '));
653
614
  Array.from(option.childNodes).forEach(childNode => {
654
615
  a11ySpan.appendChild(childNode);
655
616
  });
@@ -664,8 +625,6 @@ export class LionCombobox extends OverlayMixin(LionListbox) {
664
625
  }
665
626
  });
666
627
  }
667
- // Alternatively, an extension can add an animation here
668
- option.style.display = '';
669
628
  }
670
629
 
671
630
  /**
@@ -677,14 +636,24 @@ export class LionCombobox extends OverlayMixin(LionListbox) {
677
636
  */
678
637
  // eslint-disable-next-line no-unused-vars, class-methods-use-this
679
638
  _onFilterUnmatch(option, curValue, prevValue) {
680
- if (matchReverseFns.has(option)) {
681
- matchReverseFns.get(option)();
682
- }
639
+ this._unhighlightMatchedOption(option);
640
+
641
+ // Alternatively, an extension can add an animation here
642
+ option.style.display = 'none';
643
+ }
644
+
645
+ /**
646
+ * @overridable
647
+ * @param {Element} option
648
+ * @protected
649
+ */
650
+ // eslint-disable-next-line class-methods-use-this
651
+ _unhighlightMatchedOption(option) {
652
+ unmakeMatchingTextBold(option);
653
+
683
654
  if (matchA11ySpanReverseFns.has(option)) {
684
655
  matchA11ySpanReverseFns.get(option)();
685
656
  }
686
- // Alternatively, an extension can add an animation here
687
- option.style.display = 'none';
688
657
  }
689
658
  /* eslint-enable no-param-reassign */
690
659
 
@@ -0,0 +1,68 @@
1
+ const matchReverseFns = new WeakMap();
2
+
3
+ /**
4
+ * @param {Node} root
5
+ * @param {string} matchingString
6
+ */
7
+ export function makeMatchingTextBold(root, matchingString) {
8
+ Array.from(root.childNodes).forEach(childNode => {
9
+ if (childNode.nodeName === '#text') {
10
+ // check for match based on nodeValue
11
+
12
+ const re = new RegExp(`^(.*?)(${matchingString})(.*)$`, 'i');
13
+ // @ts-ignore
14
+ const match = childNode.nodeValue.match(re);
15
+
16
+ if (match) {
17
+ // 1. textContent before match
18
+ const textBefore = document.createTextNode(match[1]);
19
+ root.appendChild(textBefore);
20
+
21
+ // 2. matched part
22
+ const boldElement = document.createElement('b');
23
+ // eslint-disable-next-line prefer-destructuring
24
+ boldElement.textContent = match[2];
25
+ root.appendChild(boldElement);
26
+
27
+ // 3. textContent after match
28
+ const textAfter = document.createTextNode(match[3]);
29
+ root.appendChild(textAfter);
30
+
31
+ root.removeChild(childNode);
32
+
33
+ matchReverseFns.set(root, () => {
34
+ root.appendChild(childNode);
35
+ if (root.contains(textBefore) && textBefore.parentNode !== null) {
36
+ textBefore.parentNode.removeChild(textBefore);
37
+ }
38
+ if (root.contains(boldElement) && boldElement.parentNode !== null) {
39
+ boldElement.parentNode.removeChild(boldElement);
40
+ }
41
+ if (root.contains(textAfter) && textAfter.parentNode !== null) {
42
+ textAfter.parentNode.removeChild(textAfter);
43
+ }
44
+ });
45
+ }
46
+ } else {
47
+ makeMatchingTextBold(childNode, matchingString);
48
+ }
49
+ });
50
+ }
51
+
52
+ /**
53
+ * @param {Node} root
54
+ */
55
+ export function unmakeMatchingTextBold(root) {
56
+ if (matchReverseFns.has(root)) {
57
+ matchReverseFns.get(root)();
58
+ }
59
+ Array.from(root.childNodes).forEach(childNode => {
60
+ if (childNode.nodeName === '#text') {
61
+ if (matchReverseFns.has(childNode)) {
62
+ matchReverseFns.get(childNode)();
63
+ }
64
+ } else {
65
+ unmakeMatchingTextBold(childNode);
66
+ }
67
+ });
68
+ }
@@ -13,6 +13,7 @@ import { LionCombobox } from '@lion/ui/combobox.js';
13
13
  * @typedef {import('../types/SelectionDisplay.js').SelectionDisplay} SelectionDisplay
14
14
  * @typedef {import('../../listbox/types/ListboxMixinTypes.js').ListboxHost} ListboxHost
15
15
  * @typedef {import('../../form-core/types/FormControlMixinTypes.js').FormControlHost} FormControlHost
16
+ * @typedef {import('@lion/ui/listbox.js').LionOption} LionOption
16
17
  */
17
18
 
18
19
  /**
@@ -1632,7 +1633,7 @@ describe('lion-combobox', () => {
1632
1633
  });
1633
1634
  });
1634
1635
 
1635
- it('highlights matching options', async () => {
1636
+ it('highlights first occcurence of matching option', async () => {
1636
1637
  const el = /** @type {LionCombobox} */ (
1637
1638
  await fixture(html`
1638
1639
  <lion-combobox name="foo" match-mode="all">
@@ -1645,6 +1646,16 @@ describe('lion-combobox', () => {
1645
1646
  );
1646
1647
  const options = el.formElements;
1647
1648
 
1649
+ mimicUserTyping(/** @type {LionCombobox} */ (el), 'c');
1650
+
1651
+ await el.updateComplete;
1652
+ expect(options[0]).lightDom.to.equal(`<span aria-label="Artichoke">Arti<b>c</b>hoke</span>`);
1653
+ expect(options[1]).lightDom.to.equal(`<span aria-label="Chard"><b>C</b>hard</span>`);
1654
+ expect(options[2]).lightDom.to.equal(`<span aria-label="Chicory"><b>C</b>hicory</span>`);
1655
+ expect(options[3]).lightDom.to.equal(
1656
+ `<span aria-label="Victoria Plum">Vi<b>c</b>toria Plum</span>`,
1657
+ );
1658
+
1648
1659
  mimicUserTyping(/** @type {LionCombobox} */ (el), 'ch');
1649
1660
 
1650
1661
  await el.updateComplete;
@@ -1662,6 +1673,48 @@ describe('lion-combobox', () => {
1662
1673
  expect(options[3]).lightDom.to.equal(`Victoria Plum`);
1663
1674
  });
1664
1675
 
1676
+ it('highlights matching complex options', async () => {
1677
+ const el = /** @type {LionCombobox} */ (
1678
+ await fixture(html`
1679
+ <lion-combobox name="foo" match-mode="all">
1680
+ <lion-option .choiceValue="${'Artichoke'}">
1681
+ <div>Artichoke</div>
1682
+ <small>Cardoon</small>
1683
+ </lion-option>
1684
+ <lion-option .choiceValue="${'Chard'}">
1685
+ <div>Chard</div>
1686
+ <small>Beet</small>
1687
+ </lion-option>
1688
+ <lion-option .choiceValue="${'Chicory'}">
1689
+ <div>Chicory</div>
1690
+ <small>Chicory</small>
1691
+ </lion-option>
1692
+ <lion-option .choiceValue="${'Victoria Plum'}">
1693
+ <div>Victoria Plum</div>
1694
+ <small>Prunus domestica</small>
1695
+ </lion-option>
1696
+ </lion-combobox>
1697
+ `)
1698
+ );
1699
+ const options = el.formElements;
1700
+
1701
+ mimicUserTyping(/** @type {LionCombobox} */ (el), 'ch');
1702
+
1703
+ await el.updateComplete;
1704
+ expect(options[0]).lightDom.to.equal(
1705
+ `<span aria-label=" Artichoke Cardoon "><div>Arti<b>ch</b>oke</div><small>Cardoon</small>`,
1706
+ );
1707
+ expect(options[1]).lightDom.to.equal(
1708
+ `<span aria-label=" Chard Beet "><div><b>Ch</b>ard</div><small>Beet</small>`,
1709
+ );
1710
+ expect(options[2]).lightDom.to.equal(
1711
+ `<span aria-label=" Chicory Chicory "><div><b>Ch</b>icory</div><small><b>Ch</b>icory</small>`,
1712
+ );
1713
+ expect(options[3]).lightDom.to.equal(
1714
+ `<div>Victoria Plum</div><small>Prunus domestica</small>`,
1715
+ );
1716
+ });
1717
+
1665
1718
  it('synchronizes textbox when autocomplete is "inline" or "both"', async () => {
1666
1719
  const el = /** @type {LionCombobox} */ (
1667
1720
  await fixture(html`
@@ -1999,6 +2052,98 @@ describe('lion-combobox', () => {
1999
2052
  await performChecks();
2000
2053
  });
2001
2054
  });
2055
+
2056
+ it('allows to override "_onFilterMatch" and "_onFilterUmatch"', async () => {
2057
+ class X extends LionCombobox {
2058
+ /**
2059
+ * @overridable
2060
+ * @param {LionOption & {__originalInnerHTML?:string}} option
2061
+ * @param {string} matchingString
2062
+ * @protected
2063
+ */
2064
+ _onFilterMatch(option, matchingString) {
2065
+ Array.from(option.children).forEach(child => {
2066
+ if (child.hasAttribute('data-key')) {
2067
+ this._highlightMatchedOption(child, matchingString);
2068
+ }
2069
+ });
2070
+ // Alternatively, an extension can add an animation here
2071
+ // option.style.display = '';
2072
+ }
2073
+
2074
+ /**
2075
+ * @overridable
2076
+ * @param {LionOption & {__originalInnerHTML?:string}} option
2077
+ * @param {string} [curValue]
2078
+ * @param {string} [prevValue]
2079
+ * @protected
2080
+ */
2081
+ // eslint-disable-next-line no-unused-vars
2082
+ _onFilterUnmatch(option, curValue, prevValue) {
2083
+ Array.from(option.children).forEach(child => {
2084
+ if (child.hasAttribute('data-key')) {
2085
+ this._unhighlightMatchedOption(child);
2086
+ }
2087
+ });
2088
+ // Alternatively, an extension can add an animation here
2089
+ // option.style.display = 'none';
2090
+ }
2091
+ }
2092
+ const tagName = defineCE(X);
2093
+ const tag = unsafeStatic(tagName);
2094
+
2095
+ const el = /** @type {LionCombobox} */ (
2096
+ await fixture(html`
2097
+ <${tag} name="foo" autocomplete="both">
2098
+ <lion-option .choiceValue="${'Artichoke'}">
2099
+ <div data-key>Artichoke</div>
2100
+ <small>Cardoon</small>
2101
+ </lion-option>
2102
+ <lion-option .choiceValue="${'Chard'}">
2103
+ <div data-key>Chard</div>
2104
+ <small>Beet</small>
2105
+ </lion-option>
2106
+ <lion-option .choiceValue="${'Chicory'}">
2107
+ <div data-key>Chicory</div>
2108
+ <small>Chicory</small>
2109
+ </lion-option>
2110
+ <lion-option .choiceValue="${'Victoria Plum'}">
2111
+ <div data-key>Victoria Plum</div>
2112
+ <small>Prunus domestica</small>
2113
+ </lion-option>
2114
+ </${tag}>
2115
+ `)
2116
+ );
2117
+ const options = el.formElements;
2118
+
2119
+ mimicUserTyping(/** @type {LionCombobox} */ (el), 'ch');
2120
+
2121
+ await el.updateComplete;
2122
+ expect(options[0]).lightDom.to.equal(
2123
+ `<div data-key><span aria-label="Artichoke">Arti<b>ch</b>oke</span></div><small>Cardoon</small>`,
2124
+ );
2125
+ expect(options[1]).lightDom.to.equal(
2126
+ `<div data-key><span aria-label="Chard"><b>Ch</b>ard</div></span><small>Beet</small>`,
2127
+ );
2128
+ expect(options[2]).lightDom.to.equal(
2129
+ `<div data-key><span aria-label="Chicory"><b>Ch</b>icory</span></div><small>Chicory</small>`,
2130
+ );
2131
+ expect(options[3]).lightDom.to.equal(
2132
+ `<div data-key>Victoria Plum</div><small>Prunus domestica</small>`,
2133
+ );
2134
+
2135
+ mimicUserTyping(/** @type {LionCombobox} */ (el), 'D');
2136
+
2137
+ await el.updateComplete;
2138
+ expect(options[0]).lightDom.to.equal(`<div data-key>Artichoke</div><small>Cardoon</small>`);
2139
+ expect(options[1]).lightDom.to.equal(
2140
+ `<div data-key><span aria-label="Chard">Char<b>d</b></div></span><small>Beet</small>`,
2141
+ );
2142
+ expect(options[2]).lightDom.to.equal(`<div data-key>Chicory</div><small>Chicory</small>`);
2143
+ expect(options[3]).lightDom.to.equal(
2144
+ `<div data-key>Victoria Plum</div><small>Prunus domestica</small>`,
2145
+ );
2146
+ });
2002
2147
  });
2003
2148
 
2004
2149
  describe('Active index behavior', () => {
@@ -2278,6 +2423,26 @@ describe('lion-combobox', () => {
2278
2423
  expect(labelledElement).to.not.be.null;
2279
2424
  expect(labelledElement.innerText).to.equal('Artichoke');
2280
2425
  });
2426
+
2427
+ it('adds aria-label to highlighted complex options', async () => {
2428
+ const el = /** @type {LionCombobox} */ (
2429
+ await fixture(html`
2430
+ <lion-combobox name="foo" match-mode="all">
2431
+ <lion-option .choiceValue="${'Artichoke'}">
2432
+ <div>Artichoke</div>
2433
+ <small>Cardoon</small>
2434
+ </lion-option>
2435
+ </lion-combobox>
2436
+ `)
2437
+ );
2438
+ const options = el.formElements;
2439
+
2440
+ mimicUserTyping(/** @type {LionCombobox} */ (el), 'choke');
2441
+ await el.updateComplete;
2442
+ const labelledElement = options[0].querySelector('span[aria-label=" Artichoke Cardoon "]');
2443
+ expect(labelledElement).to.not.be.null;
2444
+ expect(labelledElement.innerText).to.equal('Artichoke\nCardoon');
2445
+ });
2281
2446
  });
2282
2447
  });
2283
2448