@lumx/core 4.9.0-next.0 → 4.9.0-next.10

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 (44) hide show
  1. package/components-and-utils.css +36 -4
  2. package/js/components/Combobox/ComboboxButton.d.ts +54 -0
  3. package/js/components/Combobox/ComboboxInput.d.ts +49 -0
  4. package/js/components/Combobox/ComboboxList.d.ts +47 -0
  5. package/js/components/Combobox/ComboboxOption.d.ts +74 -0
  6. package/js/components/Combobox/ComboboxOptionAction.d.ts +35 -0
  7. package/js/components/Combobox/ComboboxOptionMoreInfo.d.ts +54 -0
  8. package/js/components/Combobox/ComboboxOptionSkeleton.d.ts +41 -0
  9. package/js/components/Combobox/ComboboxPopover.d.ts +50 -0
  10. package/js/components/Combobox/ComboboxSection.d.ts +58 -0
  11. package/js/components/Combobox/ComboboxState.d.ts +90 -0
  12. package/js/components/Combobox/index.d.ts +25 -0
  13. package/js/components/Combobox/setupCombobox.d.ts +24 -0
  14. package/js/components/Combobox/setupComboboxButton.d.ts +16 -0
  15. package/js/components/Combobox/setupComboboxInput.d.ts +28 -0
  16. package/js/components/Combobox/setupListbox.d.ts +21 -0
  17. package/js/components/Combobox/subscribeComboboxState.d.ts +23 -0
  18. package/js/components/Combobox/types.d.ts +124 -0
  19. package/js/components/Combobox/utils.d.ts +27 -0
  20. package/js/components/List/ListItem.d.ts +58 -0
  21. package/js/components/List/ListItemAction.d.ts +24 -0
  22. package/js/components/List/index.d.ts +39 -0
  23. package/js/components/Mosaic/Tests.d.ts +13 -0
  24. package/js/components/Tabs/TabList.d.ts +39 -0
  25. package/js/components/Tabs/TabListTests.d.ts +12 -0
  26. package/js/components/Tabs/TabPanelTests.d.ts +11 -0
  27. package/js/components/Tabs/TabProviderTestUtils.d.ts +17 -0
  28. package/js/components/Tabs/Tests.d.ts +11 -0
  29. package/js/components/Tabs/constants.d.ts +4 -0
  30. package/js/components/Tabs/state.d.ts +34 -0
  31. package/js/components/Text/index.d.ts +1 -1
  32. package/js/components/UserBlock/Tests.d.ts +13 -0
  33. package/js/types/jsx/PropsToOverride.d.ts +1 -1
  34. package/js/utils/browser/createSelectorTreeWalker.d.ts +13 -0
  35. package/js/utils/focusNavigation/createGridFocusNavigation.d.ts +13 -0
  36. package/js/utils/focusNavigation/index.d.ts +2 -1
  37. package/js/utils/focusNavigation/types.d.ts +28 -7
  38. package/js/utils/typeahead/index.d.ts +29 -0
  39. package/lumx.css +36 -4
  40. package/package.json +2 -2
  41. package/scss/_components_classes.scss +2 -1
  42. package/scss/components/combobox/_index.scss +44 -0
  43. package/scss/components/list/_mixins.scss +47 -33
  44. package/stories/types.d.ts +2 -0
@@ -39,10 +39,30 @@ export interface ListNavigationOptions {
39
39
  */
40
40
  itemActiveSelector?: string;
41
41
  }
42
- /** Focus navigation controller interface for 1D lists. */
42
+ /** Options for 2D grid navigation. */
43
+ export interface GridNavigationOptions {
44
+ type: 'grid';
45
+ /** The container element (grid root) to scan for rows and cells. */
46
+ container: HTMLElement;
47
+ /** CSS selector to identify row elements within the container. */
48
+ rowSelector: string;
49
+ /** CSS selector to identify cell elements within each row. */
50
+ cellSelector: string;
51
+ /**
52
+ * Predicate to determine if a row should be included in navigation.
53
+ * Rows for which this returns `false` are skipped.
54
+ * Default: all rows are visible.
55
+ */
56
+ isRowVisible?: (row: HTMLElement) => boolean;
57
+ /** Whether navigation wraps at boundaries. Default: false. */
58
+ wrap?: boolean;
59
+ }
60
+ /** Union of all navigation option types. */
61
+ export type NavigationOptions = ListNavigationOptions | GridNavigationOptions;
62
+ /** Focus navigation controller interface — works for both 1D lists and 2D grids. */
43
63
  export interface FocusNavigationController {
44
64
  /** The navigation structure type. */
45
- readonly type: 'list';
65
+ readonly type: 'list' | 'grid';
46
66
  /** The currently active item, or null if no item is active. */
47
67
  readonly activeItem: HTMLElement | null;
48
68
  /** Whether an item is currently active. */
@@ -60,7 +80,8 @@ export interface FocusNavigationController {
60
80
  goToItem(item: HTMLElement): boolean;
61
81
  /**
62
82
  * Navigate by offset from the current item.
63
- * Positive offsets move forward, negative move backward.
83
+ * In list mode, moves by items. In grid mode, moves by rows.
84
+ * Positive offsets move forward/down, negative move backward/up.
64
85
  */
65
86
  goToOffset(offset: number): boolean;
66
87
  /**
@@ -70,12 +91,12 @@ export interface FocusNavigationController {
70
91
  goToItemMatching(predicate: (item: HTMLElement) => boolean): boolean;
71
92
  /** Clear the active item — no item is active after this call. */
72
93
  clear(): void;
73
- /** Navigate up (previous item in vertical list). */
94
+ /** Navigate up (previous item in vertical list, previous row in grid). */
74
95
  goUp(): boolean;
75
- /** Navigate down (next item in vertical list). */
96
+ /** Navigate down (next item in vertical list, next row in grid). */
76
97
  goDown(): boolean;
77
- /** Navigate left (previous item in horizontal list). */
98
+ /** Navigate left (previous item in horizontal list, previous cell in grid). */
78
99
  goLeft(): boolean;
79
- /** Navigate right (next item in horizontal list). */
100
+ /** Navigate right (next item in horizontal list, next cell in grid). */
80
101
  goRight(): boolean;
81
102
  }
@@ -0,0 +1,29 @@
1
+ /** Typeahead interface. */
2
+ export interface Typeahead {
3
+ /**
4
+ * Handle a printable character keypress.
5
+ * @param key The character typed.
6
+ * @param currentItem The currently active item.
7
+ * @returns The matched item, or null.
8
+ */
9
+ handle(key: string, currentItem: HTMLElement | null): HTMLElement | null;
10
+ /** Reset the accumulated search string. */
11
+ reset(): void;
12
+ }
13
+ /**
14
+ * Create a typeahead controller for keyboard navigation in list-like widgets
15
+ * (combobox, menu, listbox, tree, etc.).
16
+ *
17
+ * Accumulates typed characters and matches them against item labels.
18
+ * Supports single-char cycling (e.g. pressing "a" repeatedly cycles through
19
+ * items starting with "a") and multi-char prefix matching.
20
+ *
21
+ * Uses a {@link TreeWalker} for lazy DOM traversal — items are visited one
22
+ * at a time without materializing the full list.
23
+ *
24
+ * @param getWalker Callback returning a fresh TreeWalker over navigable items, or null if unavailable.
25
+ * @param getItemValue Callback extracting the text label from an item element.
26
+ * @param signal AbortSignal for cleanup.
27
+ * @returns Typeahead instance.
28
+ */
29
+ export declare function createTypeahead(getWalker: () => TreeWalker | null, getItemValue: (item: HTMLElement) => string, signal: AbortSignal): Typeahead;
package/lumx.css CHANGED
@@ -5686,6 +5686,38 @@ table {
5686
5686
  text-overflow: ellipsis;
5687
5687
  }
5688
5688
 
5689
+ /* ==========================================================================
5690
+ Combobox
5691
+ ========================================================================== */
5692
+ .lumx-combobox-popover {
5693
+ overflow-y: auto;
5694
+ }
5695
+
5696
+ .lumx-combobox-popover:empty, .lumx-combobox-popover:not(:has(.lumx-combobox-option)):not(:has(.lumx-combobox-state)):not(:has(.lumx-combobox-option-skeleton)),
5697
+ .lumx-combobox-list:empty,
5698
+ .lumx-combobox-list:not(:has(.lumx-combobox-option)):not(:has(.lumx-combobox-state)):not(:has(.lumx-combobox-option-skeleton)) {
5699
+ display: none;
5700
+ }
5701
+
5702
+ .lumx-combobox-state {
5703
+ text-align: center;
5704
+ }
5705
+
5706
+ .lumx-combobox-option-skeleton .lumx-skeleton-typography {
5707
+ width: min(65%, 200px);
5708
+ }
5709
+ .lumx-combobox-option-skeleton:nth-child(3n+1) .lumx-skeleton-typography {
5710
+ width: min(70%, 230px);
5711
+ }
5712
+ .lumx-combobox-option-skeleton:nth-child(3n+2) .lumx-skeleton-typography {
5713
+ width: min(55%, 170px);
5714
+ }
5715
+
5716
+ .lumx-combobox-option-more-info__popover {
5717
+ max-width: 256px;
5718
+ padding: 16px;
5719
+ }
5720
+
5689
5721
  /* ==========================================================================
5690
5722
  Comment block
5691
5723
  ========================================================================== */
@@ -8785,7 +8817,7 @@ table {
8785
8817
  color: var(--lumx-color-dark-N);
8786
8818
  background-color: transparent;
8787
8819
  }
8788
- .lumx-list-item__link[data-focus-visible-added] {
8820
+ .lumx-list-item__link[data-focus-visible-added], .lumx-list-item__link:has([data-focus-visible-added]) {
8789
8821
  outline: 2px solid var(--lumx-color-dark-N);
8790
8822
  outline-offset: -2px;
8791
8823
  }
@@ -8882,14 +8914,14 @@ table {
8882
8914
 
8883
8915
  /* Section
8884
8916
  ========================================================================== */
8885
- .lumx-list-section:not(:first-child):not(.lumx-list-section + .lumx-list-section):not(.lumx-list-divider + .lumx-list-section)::before {
8917
+ .lumx-list-section:not([hidden]) ~ .lumx-list-section:not(.lumx-list-section:not([hidden]) + .lumx-list-section):not(.lumx-list-divider + .lumx-list-section)::before {
8886
8918
  content: "";
8887
8919
  display: block;
8888
8920
  height: 1px;
8889
8921
  margin: 8px 0;
8890
8922
  background-color: var(--lumx-color-dark-L5);
8891
8923
  }
8892
- .lumx-list-section:not(:last-child):not(:has(+ .lumx-list-divider))::after {
8924
+ .lumx-list-section:not(:last-child):not(:has(+ .lumx-list-divider)):not(:has(+ [hidden]))::after {
8893
8925
  content: "";
8894
8926
  display: block;
8895
8927
  height: 1px;
@@ -10536,7 +10568,7 @@ table {
10536
10568
  color: var(--lumx-color-dark-N);
10537
10569
  background-color: transparent;
10538
10570
  }
10539
- .lumx-side-navigation-item__link[data-focus-visible-added] {
10571
+ .lumx-side-navigation-item__link[data-focus-visible-added], .lumx-side-navigation-item__link:has([data-focus-visible-added]) {
10540
10572
  outline: 2px solid var(--lumx-color-dark-N);
10541
10573
  outline-offset: -2px;
10542
10574
  }
package/package.json CHANGED
@@ -7,7 +7,7 @@
7
7
  },
8
8
  "dependencies": {
9
9
  "@floating-ui/dom": "^1.7.5",
10
- "@lumx/icons": "^4.9.0-next.0",
10
+ "@lumx/icons": "^4.9.0-next.10",
11
11
  "classnames": "^2.3.2",
12
12
  "focus-visible": "^5.0.2",
13
13
  "lodash": "4.17.23",
@@ -69,7 +69,7 @@
69
69
  "update-version-changelog": "yarn version-changelog ../../CHANGELOG.md"
70
70
  },
71
71
  "sideEffects": false,
72
- "version": "4.9.0-next.0",
72
+ "version": "4.9.0-next.10",
73
73
  "devDependencies": {
74
74
  "@rollup/plugin-typescript": "^12.3.0",
75
75
  "@testing-library/dom": "^10.4.1",
@@ -3,6 +3,7 @@
3
3
  @import "./components/button/index";
4
4
  @import "./components/checkbox/index";
5
5
  @import "./components/chip/index";
6
+ @import "./components/combobox/index";
6
7
  @import "./components/comment-block/index";
7
8
  @import "./components/date-picker/index";
8
9
  @import "./components/dialog/index";
@@ -49,4 +50,4 @@
49
50
  @import "./components/toolbar/index";
50
51
  @import "./components/tooltip/index";
51
52
  @import "./components/uploader/index";
52
- @import "./components/user-block/index";
53
+ @import "./components/user-block/index";
@@ -0,0 +1,44 @@
1
+ /* ==========================================================================
2
+ Combobox
3
+ ========================================================================== */
4
+
5
+ .#{$lumx-base-prefix}-combobox-popover {
6
+ overflow-y: auto;
7
+ }
8
+
9
+ // Hide popover and list when there is no option, no state, and no skeleton
10
+ .#{$lumx-base-prefix}-combobox-popover,
11
+ .#{$lumx-base-prefix}-combobox-list {
12
+ &:empty,
13
+ &:not(:has(.#{$lumx-base-prefix}-combobox-option)):not(:has(.#{$lumx-base-prefix}-combobox-state)):not(
14
+ :has(.#{$lumx-base-prefix}-combobox-option-skeleton)
15
+ ) {
16
+ display: none;
17
+ }
18
+ }
19
+
20
+ .#{$lumx-base-prefix}-combobox-state {
21
+ text-align: center;
22
+ }
23
+
24
+ // Skeleton option placeholders — width variation via :nth-child cycling
25
+ .#{$lumx-base-prefix}-combobox-option-skeleton {
26
+ .#{$lumx-base-prefix}-skeleton-typography {
27
+ width: min(65%, 200px);
28
+ }
29
+
30
+ &:nth-child(3n + 1) .#{$lumx-base-prefix}-skeleton-typography {
31
+ width: min(70%, 230px);
32
+ }
33
+
34
+ &:nth-child(3n + 2) .#{$lumx-base-prefix}-skeleton-typography {
35
+ width: min(55%, 170px);
36
+ }
37
+ }
38
+
39
+ .#{$lumx-base-prefix}-combobox-option-more-info {
40
+ &__popover {
41
+ max-width: $lumx-size-xxl;
42
+ padding: $lumx-spacing-unit-big;
43
+ }
44
+ }
@@ -1,4 +1,4 @@
1
- @use "sass:map";
1
+ @use 'sass:map';
2
2
 
3
3
  @mixin lumx-list() {
4
4
  padding: $lumx-spacing-unit 0;
@@ -12,7 +12,7 @@
12
12
  text-decoration: none;
13
13
  outline: none;
14
14
 
15
- @if $size == "huge" {
15
+ @if $size == 'huge' {
16
16
  align-items: flex-start;
17
17
  padding-top: $lumx-spacing-unit * 2;
18
18
  padding-bottom: $lumx-spacing-unit * 2;
@@ -30,23 +30,24 @@
30
30
  width: 100%;
31
31
  cursor: pointer;
32
32
  text-align: left;
33
- @include lumx-state(lumx-base-const("state", "DEFAULT"), lumx-base-const("emphasis", "LOW"), "dark");
33
+ @include lumx-state(lumx-base-const('state', 'DEFAULT'), lumx-base-const('emphasis', 'LOW'), 'dark');
34
34
 
35
- &[data-focus-visible-added] {
36
- outline: 2px solid lumx-color-variant("dark", "N");
35
+ &[data-focus-visible-added],
36
+ &:has([data-focus-visible-added]) {
37
+ outline: 2px solid lumx-color-variant('dark', 'N');
37
38
  outline-offset: -2px;
38
39
  }
39
40
 
40
- &:not([aria-disabled="true"]) {
41
+ &:not([aria-disabled='true']) {
41
42
  &:hover,
42
43
  &[data-lumx-hover],
43
44
  &[data-focus-visible-added] {
44
- @include lumx-state(lumx-base-const("state", "HOVER"), lumx-base-const("emphasis", "LOW"), "dark");
45
+ @include lumx-state(lumx-base-const('state', 'HOVER'), lumx-base-const('emphasis', 'LOW'), 'dark');
45
46
  }
46
47
 
47
48
  &:active,
48
- &[data-lumx-active], {
49
- @include lumx-state(lumx-base-const("state", "ACTIVE"), lumx-base-const("emphasis", "LOW"), "dark");
49
+ &[data-lumx-active] {
50
+ @include lumx-state(lumx-base-const('state', 'ACTIVE'), lumx-base-const('emphasis', 'LOW'), 'dark');
50
51
  }
51
52
  }
52
53
  }
@@ -54,25 +55,33 @@
54
55
  @mixin lumx-list-item-highlighted() {
55
56
  cursor: pointer;
56
57
 
57
- @include lumx-state(lumx-base-const("state", "HOVER"), lumx-base-const("emphasis", "LOW"), "dark");
58
+ @include lumx-state(lumx-base-const('state', 'HOVER'), lumx-base-const('emphasis', 'LOW'), 'dark');
58
59
 
59
60
  &:active {
60
- @include lumx-state(lumx-base-const("state", "ACTIVE"), lumx-base-const("emphasis", "LOW"), "dark");
61
+ @include lumx-state(lumx-base-const('state', 'ACTIVE'), lumx-base-const('emphasis', 'LOW'), 'dark');
61
62
  }
62
63
  }
63
64
 
64
65
  @mixin lumx-list-item-selected($component: null) {
65
- @include lumx-state-as-selected(lumx-base-const("state", "DEFAULT"), lumx-base-const("theme", "LIGHT"), $component);
66
+ @include lumx-state-as-selected(lumx-base-const('state', 'DEFAULT'), lumx-base-const('theme', 'LIGHT'), $component);
66
67
 
67
68
  &:hover,
68
69
  &[data-lumx-hover],
69
70
  &[data-focus-visible-added] {
70
- @include lumx-state-as-selected(lumx-base-const("state", "HOVER"), lumx-base-const("theme", "LIGHT"), $component);
71
+ @include lumx-state-as-selected(
72
+ lumx-base-const('state', 'HOVER'),
73
+ lumx-base-const('theme', 'LIGHT'),
74
+ $component
75
+ );
71
76
  }
72
77
 
73
78
  &:active,
74
- &[data-lumx-active], {
75
- @include lumx-state-as-selected(lumx-base-const("state", "ACTIVE"), lumx-base-const("theme", "LIGHT"), $component);
79
+ &[data-lumx-active] {
80
+ @include lumx-state-as-selected(
81
+ lumx-base-const('state', 'ACTIVE'),
82
+ lumx-base-const('theme', 'LIGHT'),
83
+ $component
84
+ );
76
85
  }
77
86
  }
78
87
 
@@ -85,17 +94,17 @@
85
94
  @mixin lumx-list-item-before($size) {
86
95
  @include lumx-list-item-edge($size);
87
96
 
88
- @if $size == "tiny" {
89
- width: map.get($lumx-sizes, lumx-base-const("size", "S"));
97
+ @if $size == 'tiny' {
98
+ width: map.get($lumx-sizes, lumx-base-const('size', 'S'));
90
99
  margin-right: $lumx-spacing-unit;
91
100
  } @else {
92
- width: map.get($lumx-sizes, lumx-base-const("size", "M"));
101
+ width: map.get($lumx-sizes, lumx-base-const('size', 'M'));
93
102
  margin-right: $lumx-spacing-unit * 2;
94
103
  }
95
104
  }
96
105
 
97
106
  @mixin lumx-list-item-content() {
98
- @include lumx-typography("body1");
107
+ @include lumx-typography('body1');
99
108
 
100
109
  flex: 1 1 auto;
101
110
  }
@@ -103,7 +112,7 @@
103
112
  @mixin lumx-list-item-after($size) {
104
113
  @include lumx-list-item-edge($size);
105
114
 
106
- @if $size == "tiny" {
115
+ @if $size == 'tiny' {
107
116
  margin-left: $lumx-spacing-unit;
108
117
  } @else {
109
118
  margin-left: $lumx-spacing-unit * 2;
@@ -111,16 +120,16 @@
111
120
  }
112
121
 
113
122
  @mixin lumx-list-subheader() {
114
- @include lumx-typography("overline");
123
+ @include lumx-typography('overline');
115
124
 
116
125
  display: flex;
117
126
  align-items: center;
118
- height: map.get($lumx-list-item-sizes, "tiny");
119
- color: lumx-color-variant("dark", "L2");
127
+ height: map.get($lumx-list-item-sizes, 'tiny');
128
+ color: lumx-color-variant('dark', 'L2');
120
129
  }
121
130
 
122
131
  @mixin lumx-list-subheader-icon {
123
- @include lumx-icon-size(lumx-base-const("size", "XXS"));
132
+ @include lumx-icon-size(lumx-base-const('size', 'XXS'));
124
133
 
125
134
  margin-right: $lumx-spacing-unit;
126
135
  }
@@ -128,15 +137,19 @@
128
137
  @mixin lumx-list-divider() {
129
138
  height: 1px;
130
139
  margin: $lumx-spacing-unit 0;
131
- background-color: lumx-color-variant("dark", $lumx-divider-color-variant);
140
+ background-color: lumx-color-variant('dark', $lumx-divider-color-variant);
132
141
  }
133
142
 
134
143
  @mixin lumx-list-auto-section-divider() {
135
144
  $divider: '.#{$lumx-base-prefix}-list-divider';
136
-
137
- // Insert a divider before each section NOT directly following another section or an explicit divider.
138
- // Avoids double dividers when two sections are adjacent or when a ListDivider separates them.
139
- &:not(:first-child):not(& + &):not(#{$divider} + &) {
145
+ $section: &;
146
+
147
+ // Insert a divider before each section that:
148
+ // 1. Is not directly following another visible section (avoids double dividers)
149
+ // 2. Is not directly following an explicit divider
150
+ // 3. Has at least one preceding visible section (uses `~` general sibling combinator
151
+ // so hidden-first-sections don't get a spurious top divider)
152
+ #{$section}:not([hidden]) ~ &:not(#{$section}:not([hidden]) + &):not(#{$divider} + &) {
140
153
  &::before {
141
154
  content: '';
142
155
  display: block;
@@ -145,8 +158,9 @@
145
158
  }
146
159
 
147
160
  // Insert a divider after each section that is not the last child
148
- // and not directly followed by an explicit divider.
149
- &:not(:last-child):not(:has(+ #{$divider})) {
161
+ // and not directly followed by an explicit divider or a hidden element
162
+ // (when followed by a hidden element, the next visible section's ::before handles the divider).
163
+ &:not(:last-child):not(:has(+ #{$divider})):not(:has(+ [hidden])) {
150
164
  &::after {
151
165
  content: '';
152
166
  display: block;
@@ -156,10 +170,10 @@
156
170
  }
157
171
 
158
172
  @mixin lumx-list-item-padding($size) {
159
- @if $size == "big" {
173
+ @if $size == 'big' {
160
174
  padding-right: $lumx-spacing-unit * 2;
161
175
  padding-left: $lumx-spacing-unit * 2;
162
- } @else if $size == "huge" {
176
+ } @else if $size == 'huge' {
163
177
  padding-right: $lumx-spacing-unit * 3;
164
178
  padding-left: $lumx-spacing-unit * 3;
165
179
  }
@@ -18,6 +18,8 @@ interface StoryDecorators {
18
18
  withValueOnChange?: (options?: {
19
19
  valueProp?: string;
20
20
  valueTransform?: (v: any) => any;
21
+ onChangeProp?: string;
22
+ valueExtract?: (v: any) => any;
21
23
  }) => Decorator;
22
24
  /** Decorator forcing a minimum screen size for Chromatic snapshots */
23
25
  withChromaticForceScreenSize?: () => Decorator;