vira 31.7.1 → 31.7.2

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.
@@ -16,7 +16,6 @@ export * from './vira-image.element.js';
16
16
  export * from './vira-input.element.js';
17
17
  export * from './vira-link.element.js';
18
18
  export * from './vira-modal.element.js';
19
- export * from './vira-overflow-switch.element.js';
20
19
  export * from './vira-progress.element.js';
21
20
  export * from './vira-select.element.js';
22
21
  export * from './vira-tabs.element.js';
@@ -16,7 +16,6 @@ export * from './vira-image.element.js';
16
16
  export * from './vira-input.element.js';
17
17
  export * from './vira-link.element.js';
18
18
  export * from './vira-modal.element.js';
19
- export * from './vira-overflow-switch.element.js';
20
19
  export * from './vira-progress.element.js';
21
20
  export * from './vira-select.element.js';
22
21
  export * from './vira-tabs.element.js';
@@ -76,4 +76,8 @@ export declare const ViraTabs: import("element-vir").DeclarativeElementDefinitio
76
76
  menuIsDisabled: boolean;
77
77
  /** Offset for the dropdown pop-up. Only used when tabs overflow into a dropdown. */
78
78
  menuPopUpOffset: Readonly<PopUpOffset>;
79
- }>, {}, {}, "vira-tabs-bar-top" | "vira-tabs-bar-bottom" | "vira-tabs-bar-left" | "vira-tabs-bar-right" | "vira-tabs-color-accent" | "vira-tabs-color-plain" | "vira-tabs-icon-layout-vertical" | "vira-tabs-icon-layout-horizontal", "vira-tabs-active-color" | "vira-tabs-active-hover-color" | "vira-tabs-inactive-color" | "vira-tabs-inactive-hover-color" | "vira-tabs-bar-thickness", readonly [], readonly []>;
79
+ }>, {
80
+ isOverflowing: boolean;
81
+ /** A callback to remove all internal observers. */
82
+ cleanupObserver: undefined | (() => void);
83
+ }, {}, "vira-tabs-bar-top" | "vira-tabs-bar-bottom" | "vira-tabs-bar-left" | "vira-tabs-bar-right" | "vira-tabs-color-accent" | "vira-tabs-color-plain" | "vira-tabs-icon-layout-vertical" | "vira-tabs-icon-layout-horizontal" | "vira-tabs-overflowing", "vira-tabs-active-color" | "vira-tabs-active-hover-color" | "vira-tabs-inactive-color" | "vira-tabs-inactive-hover-color" | "vira-tabs-bar-thickness", readonly [], readonly []>;
@@ -1,19 +1,19 @@
1
1
  import { check } from '@augment-vir/assert';
2
2
  import { filterMap } from '@augment-vir/common';
3
- import { classMap, css, html, nothing } from 'element-vir';
3
+ import { classMap, css, html, nothing, onDomCreated } from 'element-vir';
4
4
  import { routeHasPaths, } from 'spa-router-vir';
5
5
  import { createFocusStyles } from '../styles/focus.js';
6
6
  import { viraFormCssVars } from '../styles/form-styles.js';
7
7
  import { ViraColorVariant } from '../styles/form-variants.js';
8
8
  import { noNativeFormStyles, noUserSelect, viraDisabledStyles, viraTheme } from '../styles/index.js';
9
9
  import { defineViraElement } from '../util/define-vira-element.js';
10
+ import { createOverflowObserver } from '../util/overflow-observer.js';
10
11
  import { renderMenuItemEntries } from '../util/pop-up-helpers.js';
11
12
  import { ViraMenuTrigger } from './pop-up/vira-menu-trigger.element.js';
12
13
  import { ViraMenuCornerStyle } from './pop-up/vira-menu.element.js';
13
14
  import { ViraButton } from './vira-button.element.js';
14
15
  import { ViraIcon } from './vira-icon.element.js';
15
16
  import { ViraLink } from './vira-link.element.js';
16
- import { ViraOverflowSwitch } from './vira-overflow-switch.element.js';
17
17
  /**
18
18
  * Controls which edge of the tab the selection indicator bar appears on.
19
19
  *
@@ -45,6 +45,13 @@ export var ViraTabsIconLayout;
45
45
  */
46
46
  export const ViraTabs = defineViraElement()({
47
47
  tagName: 'vira-tabs',
48
+ state() {
49
+ return {
50
+ isOverflowing: false,
51
+ /** A callback to remove all internal observers. */
52
+ cleanupObserver: undefined,
53
+ };
54
+ },
48
55
  hostClasses: {
49
56
  'vira-tabs-bar-top': ({ inputs }) => inputs.barDirection === ViraTabsBarDirection.Top,
50
57
  'vira-tabs-bar-bottom': ({ inputs }) => !inputs.barDirection || inputs.barDirection === ViraTabsBarDirection.Bottom,
@@ -54,6 +61,7 @@ export const ViraTabs = defineViraElement()({
54
61
  'vira-tabs-color-plain': ({ inputs }) => inputs.colorVariant === ViraColorVariant.Plain,
55
62
  'vira-tabs-icon-layout-vertical': ({ inputs }) => !inputs.iconLayout || inputs.iconLayout === ViraTabsIconLayout.Vertical,
56
63
  'vira-tabs-icon-layout-horizontal': ({ inputs }) => inputs.iconLayout === ViraTabsIconLayout.Horizontal,
64
+ 'vira-tabs-overflowing': ({ state }) => state.isOverflowing,
57
65
  },
58
66
  cssVars: {
59
67
  'vira-tabs-active-color': viraFormCssVars['vira-form-accent-primary-color'].value,
@@ -69,7 +77,6 @@ export const ViraTabs = defineViraElement()({
69
77
  box-sizing: border-box;
70
78
  ${noUserSelect};
71
79
  width: 100%;
72
- height: 100%;
73
80
  }
74
81
 
75
82
  .tabs-container {
@@ -215,8 +222,19 @@ export const ViraTabs = defineViraElement()({
215
222
  }
216
223
  }
217
224
 
218
- ${ViraOverflowSwitch} {
219
- max-width: 100%;
225
+ ${hostClasses['vira-tabs-overflowing'].selector} .tabs-container {
226
+ visibility: hidden;
227
+ height: 0;
228
+ }
229
+
230
+ .overflow-menu {
231
+ display: none;
232
+ }
233
+
234
+ ${hostClasses['vira-tabs-overflowing'].selector} .overflow-menu {
235
+ display: flex;
236
+ align-items: center;
237
+ width: fit-content;
220
238
  }
221
239
 
222
240
  ${ViraLink} {
@@ -229,12 +247,14 @@ export const ViraTabs = defineViraElement()({
229
247
  }
230
248
 
231
249
  ${ViraMenuTrigger} {
232
- margin-top: -1lh;
233
- margin-bottom: 2.6px;
250
+ margin: 3px 0;
234
251
  }
235
252
  `;
236
253
  },
237
- render({ inputs }) {
254
+ cleanup({ state }) {
255
+ state.cleanupObserver?.();
256
+ },
257
+ render({ inputs, state, updateState, host }) {
238
258
  const tabs = filterMap(inputs.tabs, (tab) => {
239
259
  if (tab.isHidden) {
240
260
  return undefined;
@@ -308,34 +328,43 @@ export const ViraTabs = defineViraElement()({
308
328
  };
309
329
  }, check.isTruthy));
310
330
  return html `
311
- <${ViraOverflowSwitch.assign({
312
- automaticallySwitch: true,
313
- })}>
314
- <ul
315
- class="tabs-container"
316
- role="tablist"
317
- slot=${ViraOverflowSwitch.slotNames.large}
318
- >
319
- ${tabs}
320
- </ul>
321
- <${ViraMenuTrigger.assign({
331
+ <${ViraMenuTrigger.assign({
322
332
  horizontalAnchor: inputs.menuHorizontalAnchor,
323
333
  isDisabled: inputs.menuIsDisabled,
324
334
  popUpOffset: inputs.menuPopUpOffset,
325
335
  menuCornerStyle: ViraMenuCornerStyle.AllRounded,
326
336
  })}
327
- slot=${ViraOverflowSwitch.slotNames.small}
328
- >
329
- <${ViraButton.assign({
337
+ class="overflow-menu"
338
+ >
339
+ <${ViraButton.assign({
330
340
  text: selectedTab?.label || '',
331
341
  showMenuCaret: true,
332
342
  colorVariant: ViraColorVariant.Neutral,
333
343
  })}
334
- slot=${ViraMenuTrigger.slotNames.trigger}
335
- ></${ViraButton}>
336
- ${menuItems}
337
- </${ViraMenuTrigger}>
338
- </${ViraOverflowSwitch}>
344
+ slot=${ViraMenuTrigger.slotNames.trigger}
345
+ ></${ViraButton}>
346
+ ${menuItems}
347
+ </${ViraMenuTrigger}>
348
+ <ul
349
+ class="tabs-container"
350
+ role="tablist"
351
+ ${onDomCreated((tabsElement) => {
352
+ state.cleanupObserver?.();
353
+ updateState({
354
+ cleanupObserver: createOverflowObserver({
355
+ element: tabsElement,
356
+ widthElement: host,
357
+ onChange(isOverflowing) {
358
+ updateState({
359
+ isOverflowing,
360
+ });
361
+ },
362
+ }),
363
+ });
364
+ })}
365
+ >
366
+ ${tabs}
367
+ </ul>
339
368
  `;
340
369
  },
341
370
  });
@@ -1,6 +1,7 @@
1
1
  export * from './define-table.js';
2
2
  export * from './define-vira-element.js';
3
3
  export * from './dynamic-element.js';
4
+ export * from './overflow-observer.js';
4
5
  export * from './pop-up-helpers.js';
5
6
  export * from './pop-up-manager.js';
6
7
  export * from './shared-text-input-logic.js';
@@ -1,6 +1,7 @@
1
1
  export * from './define-table.js';
2
2
  export * from './define-vira-element.js';
3
3
  export * from './dynamic-element.js';
4
+ export * from './overflow-observer.js';
4
5
  export * from './pop-up-helpers.js';
5
6
  export * from './pop-up-manager.js';
6
7
  export * from './shared-text-input-logic.js';
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Creates an observer that monitors whether an element's content overflows its visible width. Uses
3
+ * a ResizeObserver for size changes and a MutationObserver for DOM content changes.
4
+ *
5
+ * @returns A cleanup function that disconnects all observers.
6
+ */
7
+ export declare function createOverflowObserver({ element, widthElement, onChange, }: Readonly<{
8
+ /** The element whose `scrollWidth` is measured for content size. */
9
+ element: Element;
10
+ /**
11
+ * Optional separate element whose `clientWidth` is used as the available width. Defaults to
12
+ * `element`. Useful when `element` may be collapsed but the available width should come from a
13
+ * parent.
14
+ */
15
+ widthElement?: Element | undefined;
16
+ onChange: (isOverflowing: boolean) => void;
17
+ }>): () => void;
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Creates an observer that monitors whether an element's content overflows its visible width. Uses
3
+ * a ResizeObserver for size changes and a MutationObserver for DOM content changes.
4
+ *
5
+ * @returns A cleanup function that disconnects all observers.
6
+ */
7
+ export function createOverflowObserver({ element, widthElement, onChange, }) {
8
+ const availableWidthElement = widthElement || element;
9
+ function checkOverflow() {
10
+ onChange(element.scrollWidth > availableWidthElement.clientWidth);
11
+ }
12
+ const resizeObserver = new ResizeObserver(checkOverflow);
13
+ resizeObserver.observe(element);
14
+ if (availableWidthElement !== element) {
15
+ resizeObserver.observe(availableWidthElement);
16
+ }
17
+ const mutationObserver = new MutationObserver(checkOverflow);
18
+ mutationObserver.observe(element, {
19
+ childList: true,
20
+ subtree: true,
21
+ characterData: true,
22
+ });
23
+ checkOverflow();
24
+ return () => {
25
+ resizeObserver.disconnect();
26
+ mutationObserver.disconnect();
27
+ };
28
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vira",
3
- "version": "31.7.1",
3
+ "version": "31.7.2",
4
4
  "description": "A simple and highly versatile design system using element-vir.",
5
5
  "keywords": [
6
6
  "design",
@@ -1,21 +0,0 @@
1
- /**
2
- * An element switches between two slots based on their overflow.
3
- *
4
- * @category Elements
5
- * @see https://electrovir.github.io/vira/book/elements/vira-overflow-switch
6
- */
7
- export declare const ViraOverflowSwitch: import("element-vir").DeclarativeElementDefinition<"vira-overflow-switch", ((Required<Pick<{
8
- automaticallySwitch: boolean;
9
- useSmall: boolean;
10
- }, "automaticallySwitch">> & Partial<Record<"useSmall", never>>) | (Required<Pick<{
11
- automaticallySwitch: boolean;
12
- useSmall: boolean;
13
- }, "useSmall">> & Partial<Record<"automaticallySwitch", never>>)) & Omit<{
14
- automaticallySwitch: boolean;
15
- useSmall: boolean;
16
- }, "automaticallySwitch" | "useSmall">, {
17
- isOverflowing: boolean;
18
- resizeObserver: undefined | ResizeObserver;
19
- /** Called on cleanup to clear all listeners. */
20
- cleanupListeners: undefined | (() => void);
21
- }, {}, "vira-overflow-switch-show-small", "vira-overflow-switch-", readonly ["large", "small"], readonly []>;
@@ -1,114 +0,0 @@
1
- import { css, html, onDomCreated } from 'element-vir';
2
- import { listenTo } from 'typed-event-target';
3
- import { defineViraElement } from '../util/define-vira-element.js';
4
- /**
5
- * An element switches between two slots based on their overflow.
6
- *
7
- * @category Elements
8
- * @see https://electrovir.github.io/vira/book/elements/vira-overflow-switch
9
- */
10
- export const ViraOverflowSwitch = defineViraElement()({
11
- tagName: 'vira-overflow-switch',
12
- slotNames: [
13
- /** The child to render, if it fits. */
14
- 'large',
15
- /** The child to render if the large one does not fit. */
16
- 'small',
17
- ],
18
- state() {
19
- return {
20
- isOverflowing: false,
21
- resizeObserver: undefined,
22
- /** Called on cleanup to clear all listeners. */
23
- cleanupListeners: undefined,
24
- };
25
- },
26
- hostClasses: {
27
- 'vira-overflow-switch-show-small': ({ state, inputs }) => state.isOverflowing || !!inputs.useSmall,
28
- },
29
- styles: ({ hostClasses }) => css `
30
- :host {
31
- display: inline-block;
32
- max-width: 100%;
33
- }
34
-
35
- .large,
36
- .small {
37
- display: inline-block;
38
- }
39
-
40
- .small {
41
- display: none;
42
- }
43
-
44
- /**
45
- * When the large content overflows, hide it but keep it in layout so we can measure it.
46
- * The small content is then shown instead. Using height: 0 + overflow: hidden instead of
47
- * position: absolute keeps the large element in flow so the host's width still reflects
48
- * the available container space, allowing the ResizeObserver to detect when there is
49
- * enough room to un-collapse.
50
- */
51
- ${hostClasses['vira-overflow-switch-show-small'].selector} .large {
52
- visibility: hidden;
53
- height: 0;
54
- overflow: hidden;
55
- }
56
-
57
- ${hostClasses['vira-overflow-switch-show-small'].selector} .small {
58
- display: inline-block;
59
- }
60
- `,
61
- cleanup({ state, updateState }) {
62
- state.cleanupListeners?.();
63
- updateState({
64
- cleanupListeners: undefined,
65
- });
66
- },
67
- render({ slotNames, updateState, inputs, host, state }) {
68
- return html `
69
- <div
70
- class="large"
71
- ${onDomCreated((largeElement) => {
72
- if (!inputs.automaticallySwitch) {
73
- return;
74
- }
75
- const overflowParams = {
76
- elementToTest: largeElement,
77
- host,
78
- updateState,
79
- };
80
- const resizeObserver = new ResizeObserver(() => {
81
- updateOverflowing(overflowParams);
82
- });
83
- resizeObserver.observe(host);
84
- /**
85
- * Also observe the large slot wrapper itself in case its own layout changes
86
- * without host resizing.
87
- */
88
- resizeObserver.observe(largeElement);
89
- const removeSlotChangeListener = listenTo(largeElement, 'slotchange', () => {
90
- updateOverflowing(overflowParams);
91
- });
92
- /** Initial measurement: defer until after first layout. */
93
- updateOverflowing(overflowParams);
94
- state.cleanupListeners?.();
95
- updateState({
96
- cleanupListeners() {
97
- resizeObserver.disconnect();
98
- removeSlotChangeListener();
99
- },
100
- });
101
- })}
102
- >
103
- <slot name=${slotNames.large}></slot>
104
- </div>
105
- <div class="small"><slot name=${slotNames.small}></slot></div>
106
- `;
107
- },
108
- });
109
- function updateOverflowing({ elementToTest, host, updateState, }) {
110
- const isOverflowing = elementToTest.scrollWidth > host.clientWidth;
111
- updateState({
112
- isOverflowing,
113
- });
114
- }