vira 32.4.0 → 33.0.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/dist/directives/index.d.ts +2 -0
- package/dist/directives/index.js +2 -0
- package/dist/directives/popover.directive.d.ts +29 -0
- package/dist/directives/popover.directive.js +45 -0
- package/dist/directives/tooltip.directive.d.ts +60 -0
- package/dist/directives/tooltip.directive.js +99 -0
- package/dist/elements/color/vira-color-picker.element.d.ts +1 -1
- package/dist/elements/color/vira-color-picker.element.js +17 -15
- package/dist/elements/index.d.ts +5 -5
- package/dist/elements/index.js +5 -5
- package/dist/elements/{pop-up → popover}/vira-menu-item.element.d.ts +4 -4
- package/dist/elements/{pop-up → popover}/vira-menu-item.element.js +3 -3
- package/dist/elements/popover/vira-menu-trigger.element.d.ts +56 -0
- package/dist/elements/{pop-up → popover}/vira-menu-trigger.element.js +19 -30
- package/dist/elements/popover/vira-menu.element.d.ts +35 -0
- package/dist/elements/popover/vira-menu.element.js +262 -0
- package/dist/elements/popover/vira-popover-trigger.element.d.ts +49 -0
- package/dist/elements/popover/vira-popover-trigger.element.js +301 -0
- package/dist/elements/popover/vira-tooltip.element.d.ts +10 -0
- package/dist/elements/popover/vira-tooltip.element.js +176 -0
- package/dist/elements/vira-drawer.element.js +1 -1
- package/dist/elements/vira-dropdown.element.d.ts +28 -12
- package/dist/elements/vira-dropdown.element.js +172 -59
- package/dist/elements/vira-form.element.js +10 -16
- package/dist/elements/vira-json-form.element.js +40 -36
- package/dist/elements/vira-modal.element.js +1 -1
- package/dist/elements/vira-tabs.element.d.ts +3 -9
- package/dist/elements/vira-tabs.element.js +5 -6
- package/dist/elements/vira-theme-switcher.element.js +3 -3
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/util/anchored-popover.d.ts +40 -0
- package/dist/util/anchored-popover.js +70 -0
- package/dist/util/index.d.ts +4 -3
- package/dist/util/index.js +4 -3
- package/dist/util/{pop-up-helpers.d.ts → menu-helpers.d.ts} +4 -18
- package/dist/util/{pop-up-helpers.js → menu-helpers.js} +2 -17
- package/dist/util/popover-manager.d.ts +123 -0
- package/dist/util/popover-manager.js +412 -0
- package/dist/util/vira-dropdown-option.d.ts +30 -0
- package/dist/util/vira-dropdown-option.js +8 -0
- package/dist/util/vira-form-fields.d.ts +2 -2
- package/dist/util/vira-theme-client.d.ts +1 -1
- package/dist/util/vira-theme-client.js +3 -3
- package/package.json +2 -1
- package/dist/elements/pop-up/vira-menu-trigger.element.d.ts +0 -45
- package/dist/elements/pop-up/vira-menu.element.d.ts +0 -36
- package/dist/elements/pop-up/vira-menu.element.js +0 -88
- package/dist/elements/pop-up/vira-pop-up-trigger.element.d.ts +0 -109
- package/dist/elements/pop-up/vira-pop-up-trigger.element.js +0 -408
- package/dist/elements/vira-select.element.d.ts +0 -44
- package/dist/elements/vira-select.element.js +0 -418
- package/dist/util/pop-up-manager.d.ts +0 -200
- package/dist/util/pop-up-manager.js +0 -258
- package/dist/util/vira-select-option.d.ts +0 -31
- package/dist/util/vira-select-option.js +0 -8
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import { colorCss } from '@electrovir/color';
|
|
2
|
+
import { css, html } from 'element-vir';
|
|
3
|
+
import { listenTo } from 'typed-event-target';
|
|
4
|
+
import { viraFormCssVars } from '../../styles/form-styles.js';
|
|
5
|
+
import { viraTheme } from '../../styles/vira-color-theme.js';
|
|
6
|
+
import { createAnchoredPopoverStyles } from '../../util/anchored-popover.js';
|
|
7
|
+
import { defineViraElement } from '../../util/define-vira-element.js';
|
|
8
|
+
/**
|
|
9
|
+
* Centered with insets instead of `position-area`, because browsers shift a centered
|
|
10
|
+
* `position-area` box back onto the screen rather than moving on to the next
|
|
11
|
+
* `position-try-fallbacks` entry. These insets leave a space that is centered on the anchor and as
|
|
12
|
+
* wide as possible, and `unsafe` stops the browser from shifting the tooltip, so a tooltip wider
|
|
13
|
+
* than that space overflows it and moves on to an aligned fallback. Each `anchor(center)` is
|
|
14
|
+
* measured from the side its inset is on, so the same formula works for `left` and `right`.
|
|
15
|
+
*/
|
|
16
|
+
const centeredStyles = css `
|
|
17
|
+
position-area: none;
|
|
18
|
+
top: 0;
|
|
19
|
+
bottom: anchor(top);
|
|
20
|
+
left: max(0px, 2 * anchor(center) - 100%);
|
|
21
|
+
right: max(0px, 2 * anchor(center) - 100%);
|
|
22
|
+
align-self: end;
|
|
23
|
+
justify-self: unsafe center;
|
|
24
|
+
`;
|
|
25
|
+
/** `unsafe` for the same reason as {@link centeredStyles}. */
|
|
26
|
+
const leftAlignedStyles = css `
|
|
27
|
+
position-area: block-start span-inline-end;
|
|
28
|
+
inset: auto;
|
|
29
|
+
align-self: normal;
|
|
30
|
+
justify-self: unsafe start;
|
|
31
|
+
`;
|
|
32
|
+
/** `unsafe` for the same reason as {@link centeredStyles}. */
|
|
33
|
+
const rightAlignedStyles = css `
|
|
34
|
+
position-area: block-start span-inline-start;
|
|
35
|
+
inset: auto;
|
|
36
|
+
align-self: normal;
|
|
37
|
+
justify-self: unsafe end;
|
|
38
|
+
`;
|
|
39
|
+
/**
|
|
40
|
+
* Centered but shifted to stay on the screen. For a tooltip that fits on neither side of the
|
|
41
|
+
* anchor.
|
|
42
|
+
*/
|
|
43
|
+
const shiftedStyles = css `
|
|
44
|
+
position-area: block-start span-all;
|
|
45
|
+
inset: auto;
|
|
46
|
+
align-self: normal;
|
|
47
|
+
justify-self: anchor-center;
|
|
48
|
+
`;
|
|
49
|
+
/**
|
|
50
|
+
* The popover rendered by the `tooltip` directive. The element itself is the popover, centered
|
|
51
|
+
* above `anchor` when there's room. Its children are the tooltip contents.
|
|
52
|
+
*
|
|
53
|
+
* @category Popover
|
|
54
|
+
* @category Elements
|
|
55
|
+
*/
|
|
56
|
+
export const ViraTooltip = defineViraElement()({
|
|
57
|
+
tagName: 'vira-tooltip',
|
|
58
|
+
styles: css `
|
|
59
|
+
${createAnchoredPopoverStyles({
|
|
60
|
+
fallbacks: [
|
|
61
|
+
{
|
|
62
|
+
name: 'vira-tooltip-above-left-aligned',
|
|
63
|
+
styles: leftAlignedStyles,
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
name: 'vira-tooltip-above-right-aligned',
|
|
67
|
+
styles: rightAlignedStyles,
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
name: 'vira-tooltip-below-centered',
|
|
71
|
+
styles: centeredStyles,
|
|
72
|
+
flipBlock: true,
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
name: 'vira-tooltip-below-left-aligned',
|
|
76
|
+
styles: leftAlignedStyles,
|
|
77
|
+
flipBlock: true,
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
name: 'vira-tooltip-below-right-aligned',
|
|
81
|
+
styles: rightAlignedStyles,
|
|
82
|
+
flipBlock: true,
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
name: 'vira-tooltip-above-shifted',
|
|
86
|
+
styles: shiftedStyles,
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
name: 'vira-tooltip-below-shifted',
|
|
90
|
+
styles: shiftedStyles,
|
|
91
|
+
flipBlock: true,
|
|
92
|
+
},
|
|
93
|
+
],
|
|
94
|
+
})}
|
|
95
|
+
|
|
96
|
+
:host {
|
|
97
|
+
anchor-name: --vira-tooltip;
|
|
98
|
+
${colorCss(viraTheme.colors['vira-grey-behind-bg-highest-contrast'])};
|
|
99
|
+
${centeredStyles}
|
|
100
|
+
margin-bottom: 7px;
|
|
101
|
+
padding: 4px 8px;
|
|
102
|
+
border-radius: 6px;
|
|
103
|
+
box-sizing: border-box;
|
|
104
|
+
width: max-content;
|
|
105
|
+
/*
|
|
106
|
+
Not 100%, which is the width of the current position-area. A tooltip limited to that
|
|
107
|
+
width shrinks to fit it rather than overflowing onto the next fallback.
|
|
108
|
+
*/
|
|
109
|
+
max-width: min(300px, 100vw);
|
|
110
|
+
font-size: ${viraFormCssVars['vira-form-small-text-size'].value};
|
|
111
|
+
overflow-wrap: break-word;
|
|
112
|
+
pointer-events: none;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/*
|
|
116
|
+
Separate popovers rather than children or pseudo-elements of the tooltip, because those can't
|
|
117
|
+
use anchor() to reach the anchor element from inside the tooltip's top-layer box.
|
|
118
|
+
*/
|
|
119
|
+
.caret {
|
|
120
|
+
/*
|
|
121
|
+
position-anchor is set explicitly because its initial value differs between browsers.
|
|
122
|
+
*/
|
|
123
|
+
position-anchor: auto;
|
|
124
|
+
position: fixed;
|
|
125
|
+
inset: auto;
|
|
126
|
+
margin: 0;
|
|
127
|
+
padding: 0;
|
|
128
|
+
border: none;
|
|
129
|
+
height: auto;
|
|
130
|
+
width: 10px;
|
|
131
|
+
left: calc(anchor(center) - 5px);
|
|
132
|
+
background-color: ${viraTheme.colors['vira-grey-behind-bg-highest-contrast'].background
|
|
133
|
+
.value};
|
|
134
|
+
pointer-events: none;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/*
|
|
138
|
+
Each caret spans the gap between the tooltip and the anchor on its own side, so the caret
|
|
139
|
+
on the side without the tooltip gets a negative height, which renders as nothing. This
|
|
140
|
+
follows the tooltip's position-try-fallbacks choice without knowing which one won.
|
|
141
|
+
*/
|
|
142
|
+
.caret-down {
|
|
143
|
+
top: anchor(--vira-tooltip bottom);
|
|
144
|
+
bottom: calc(anchor(top) + 2px);
|
|
145
|
+
clip-path: polygon(0 0, 100% 0, 50% 100%);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
.caret-up {
|
|
149
|
+
bottom: anchor(--vira-tooltip top);
|
|
150
|
+
top: calc(anchor(bottom) + 2px);
|
|
151
|
+
clip-path: polygon(50% 0, 100% 100%, 0 100%);
|
|
152
|
+
}
|
|
153
|
+
`,
|
|
154
|
+
init({ host }) {
|
|
155
|
+
/** The carets must open after the tooltip so they can anchor to it. */
|
|
156
|
+
listenTo(host, 'toggle', () => {
|
|
157
|
+
if (!host.matches(':popover-open')) {
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
host.shadowRoot.querySelectorAll('.caret').forEach((caret) => {
|
|
161
|
+
if (caret instanceof HTMLElement && !caret.matches(':popover-open')) {
|
|
162
|
+
caret.showPopover({
|
|
163
|
+
source: host.instanceInputs.anchor,
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
},
|
|
169
|
+
render() {
|
|
170
|
+
return html `
|
|
171
|
+
<slot></slot>
|
|
172
|
+
<div class="caret caret-down" popover="manual"></div>
|
|
173
|
+
<div class="caret caret-up" popover="manual"></div>
|
|
174
|
+
`;
|
|
175
|
+
},
|
|
176
|
+
});
|
|
@@ -11,7 +11,7 @@ import { viraShadows } from '../styles/shadows.js';
|
|
|
11
11
|
import { noUserSelect } from '../styles/user-select.js';
|
|
12
12
|
import { viraTheme } from '../styles/vira-color-theme.js';
|
|
13
13
|
import { defineViraElement } from '../util/define-vira-element.js';
|
|
14
|
-
import { isMouseEventOnScrollbar } from '../util/
|
|
14
|
+
import { isMouseEventOnScrollbar } from '../util/popover-manager.js';
|
|
15
15
|
import { ViraIcon } from './vira-icon.element.js';
|
|
16
16
|
const globalEventsToCloseDrawerOn = [
|
|
17
17
|
'pagehide',
|
|
@@ -1,23 +1,26 @@
|
|
|
1
1
|
import { type PartialWithUndefined } from '@augment-vir/common';
|
|
2
|
-
import { type HtmlInterpolation } from 'element-vir';
|
|
2
|
+
import { type AttributeValues, type HtmlInterpolation } from 'element-vir';
|
|
3
3
|
import { type ViraIconSvg } from '../icons/icon-svg.js';
|
|
4
|
-
import { type
|
|
5
|
-
import { type ViraSelectOption } from '../util/vira-select-option.js';
|
|
6
|
-
import { type PopUpTriggerPosition } from './pop-up/vira-pop-up-trigger.element.js';
|
|
4
|
+
import { type ViraDropdownOption, type ViraDropdownOptionGroup } from '../util/vira-dropdown-option.js';
|
|
7
5
|
/**
|
|
8
|
-
* A dropdown element that uses
|
|
6
|
+
* A dropdown element that uses popover menus.
|
|
9
7
|
*
|
|
10
8
|
* @category Dropdown
|
|
11
9
|
* @category Elements
|
|
12
10
|
* @see https://electrovir.github.io/vira/book/elements/dropdown/vira-dropdown
|
|
13
11
|
*/
|
|
14
12
|
export declare const ViraDropdown: import("element-vir").DeclarativeElementDefinition<"vira-dropdown", {
|
|
15
|
-
options: ReadonlyArray<Readonly<
|
|
13
|
+
options: ReadonlyArray<Readonly<ViraDropdownOption> | Readonly<ViraDropdownOptionGroup>>;
|
|
16
14
|
/** The selected id from the given options. */
|
|
17
15
|
selected: ReadonlyArray<PropertyKey>;
|
|
18
16
|
} & PartialWithUndefined<{
|
|
19
17
|
/** Text to show if nothing is selected. */
|
|
20
18
|
placeholder: string;
|
|
19
|
+
/**
|
|
20
|
+
* Text shown in the opened popover when `options` has no options. When omitted, opening a
|
|
21
|
+
* dropdown with no options shows no popover at all.
|
|
22
|
+
*/
|
|
23
|
+
noOptionsText: string;
|
|
21
24
|
/**
|
|
22
25
|
* If false, this will behave like a single select dropdown, otherwise you can select
|
|
23
26
|
* multiple.
|
|
@@ -26,12 +29,24 @@ export declare const ViraDropdown: import("element-vir").DeclarativeElementDefin
|
|
|
26
29
|
icon: ViraIconSvg;
|
|
27
30
|
selectionPrefix: string;
|
|
28
31
|
isDisabled: boolean;
|
|
32
|
+
/**
|
|
33
|
+
* When `true`, the currently selected options' labels are rendered as plain text with no
|
|
34
|
+
* trigger or menu.
|
|
35
|
+
*/
|
|
36
|
+
isReadonly: boolean;
|
|
37
|
+
hasError: boolean;
|
|
29
38
|
label: string | HtmlInterpolation;
|
|
30
|
-
/**
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
39
|
+
/** Attributes applied to the trigger element. */
|
|
40
|
+
attributePassthrough: Readonly<AttributeValues>;
|
|
41
|
+
/**
|
|
42
|
+
* Passed to {@link ViraMenu}'s `hoverScrollSpeed` input. How many menu items per second
|
|
43
|
+
* hovering a scroll arrow scrolls through.
|
|
44
|
+
*
|
|
45
|
+
* @default 24
|
|
46
|
+
*/
|
|
47
|
+
menuHoverScrollSpeed: number;
|
|
48
|
+
}>, {
|
|
49
|
+
isOpen: boolean;
|
|
35
50
|
shouldSelectOnMouseUp: boolean;
|
|
36
51
|
/**
|
|
37
52
|
* Used to couple the label and trigger together. This is not applied if no label is
|
|
@@ -41,5 +56,6 @@ export declare const ViraDropdown: import("element-vir").DeclarativeElementDefin
|
|
|
41
56
|
}, {
|
|
42
57
|
/** Emits all currently selected values. */
|
|
43
58
|
selectedValuesChange: import("element-vir").DefineElementEvent<string[]>;
|
|
44
|
-
|
|
59
|
+
/** `true` when the popover just opened, `false` when it just closed. */
|
|
60
|
+
openChange: import("element-vir").DefineElementEvent<boolean>;
|
|
45
61
|
}, "vira-dropdown-", "vira-dropdown-", readonly [], readonly ["leadingIcon", "prefixText", "trigger"]>;
|
|
@@ -1,18 +1,20 @@
|
|
|
1
1
|
import { check } from '@augment-vir/assert';
|
|
2
2
|
import { filterMap, randomString } from '@augment-vir/common';
|
|
3
3
|
import { extractEventTarget } from '@augment-vir/web';
|
|
4
|
-
import { classMap, css, defineElementEvent, html, ifDefined, listen, nothing, testId, } from 'element-vir';
|
|
4
|
+
import { attributes, classMap, css, defineElementEvent, html, ifDefined, listen, nothing, testId, } from 'element-vir';
|
|
5
5
|
import { ChevronUp16Icon } from '../icons/index.js';
|
|
6
6
|
import { viraFormCssVars } from '../styles/form-styles.js';
|
|
7
|
+
import { ViraSize, viraSizeHeights } from '../styles/form-variants.js';
|
|
7
8
|
import { noUserSelect, viraAnimationDurations } from '../styles/index.js';
|
|
8
9
|
import { defineViraElement } from '../util/define-vira-element.js';
|
|
9
|
-
import { renderMenuItemEntries } from '../util/
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
10
|
+
import { renderMenuItemEntries } from '../util/menu-helpers.js';
|
|
11
|
+
import { isViraDropdownOptionGroup, } from '../util/vira-dropdown-option.js';
|
|
12
|
+
import { ViraMenuItem } from './popover/vira-menu-item.element.js';
|
|
13
|
+
import { ViraMenu, ViraMenuCornerStyle } from './popover/vira-menu.element.js';
|
|
14
|
+
import { ViraPopoverTrigger } from './popover/vira-popover-trigger.element.js';
|
|
13
15
|
import { ViraIcon } from './vira-icon.element.js';
|
|
14
16
|
/**
|
|
15
|
-
* A dropdown element that uses
|
|
17
|
+
* A dropdown element that uses popover menus.
|
|
16
18
|
*
|
|
17
19
|
* @category Dropdown
|
|
18
20
|
* @category Elements
|
|
@@ -29,12 +31,12 @@ export const ViraDropdown = defineViraElement()({
|
|
|
29
31
|
:host {
|
|
30
32
|
display: inline-flex;
|
|
31
33
|
vertical-align: middle;
|
|
32
|
-
width:
|
|
34
|
+
width: 224px;
|
|
33
35
|
position: relative;
|
|
34
36
|
max-width: 100%;
|
|
35
37
|
}
|
|
36
38
|
|
|
37
|
-
${
|
|
39
|
+
${ViraPopoverTrigger} {
|
|
38
40
|
width: 100%;
|
|
39
41
|
}
|
|
40
42
|
|
|
@@ -60,18 +62,8 @@ export const ViraDropdown = defineViraElement()({
|
|
|
60
62
|
justify-content: flex-end;
|
|
61
63
|
}
|
|
62
64
|
|
|
63
|
-
.open {
|
|
64
|
-
|
|
65
|
-
transform: rotate(0);
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
&:not(.open-upwards).dropdown-trigger {
|
|
69
|
-
border-bottom-left-radius: 0;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
&.open-upwards.dropdown-trigger {
|
|
73
|
-
border-top-left-radius: 0;
|
|
74
|
-
}
|
|
65
|
+
.open .trigger-icon {
|
|
66
|
+
transform: rotate(0);
|
|
75
67
|
}
|
|
76
68
|
|
|
77
69
|
.dropdown-trigger {
|
|
@@ -84,13 +76,50 @@ export const ViraDropdown = defineViraElement()({
|
|
|
84
76
|
gap: 8px;
|
|
85
77
|
text-align: left;
|
|
86
78
|
align-items: center;
|
|
87
|
-
|
|
88
|
-
padding
|
|
79
|
+
min-height: ${viraSizeHeights[ViraSize.Medium]}px;
|
|
80
|
+
padding: 0 3px 0 10px;
|
|
89
81
|
border-radius: ${viraFormCssVars['vira-form-radius'].value};
|
|
90
82
|
background-color: ${viraFormCssVars['vira-form-background-color'].value};
|
|
91
83
|
color: ${viraFormCssVars['vira-form-foreground-color'].value};
|
|
92
84
|
}
|
|
93
85
|
|
|
86
|
+
.dropdown-wrapper {
|
|
87
|
+
display: flex;
|
|
88
|
+
width: 100%;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
.has-error .dropdown-trigger {
|
|
92
|
+
border-color: ${viraFormCssVars['vira-form-error-color'].value};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
.option-group-label,
|
|
96
|
+
.no-options {
|
|
97
|
+
${noUserSelect};
|
|
98
|
+
/* Aligns with the label text of each ViraMenuItem, past its check icon. */
|
|
99
|
+
padding: 8px 12px 4px
|
|
100
|
+
calc(${ViraMenuItem.cssVars['vira-menu-item-icon-gap'].value} * 3 + 16px);
|
|
101
|
+
opacity: 0.6;
|
|
102
|
+
text-align: left;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
.option-group-label {
|
|
106
|
+
font-weight: ${viraFormCssVars['vira-form-label-font-weight'].value};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
.no-options {
|
|
110
|
+
cursor: default;
|
|
111
|
+
padding-bottom: 8px;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
${ViraMenuItem}.option-group-label {
|
|
115
|
+
padding: 0 0 0 calc(${ViraMenuItem.cssVars['vira-menu-item-icon-gap'].value} * 2);
|
|
116
|
+
${ViraMenuItem.cssVars['vira-menu-item-padding'].name}: 8px 12px 4px 0;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
.readonly-value {
|
|
120
|
+
overflow-wrap: anywhere;
|
|
121
|
+
}
|
|
122
|
+
|
|
94
123
|
.using-placeholder {
|
|
95
124
|
opacity: 0.4;
|
|
96
125
|
}
|
|
@@ -114,12 +143,12 @@ export const ViraDropdown = defineViraElement()({
|
|
|
114
143
|
events: {
|
|
115
144
|
/** Emits all currently selected values. */
|
|
116
145
|
selectedValuesChange: defineElementEvent(),
|
|
146
|
+
/** `true` when the popover just opened, `false` when it just closed. */
|
|
117
147
|
openChange: defineElementEvent(),
|
|
118
148
|
},
|
|
119
149
|
state() {
|
|
120
150
|
return {
|
|
121
|
-
|
|
122
|
-
showPopUpResult: undefined,
|
|
151
|
+
isOpen: false,
|
|
123
152
|
shouldSelectOnMouseUp: false,
|
|
124
153
|
/**
|
|
125
154
|
* Used to couple the label and trigger together. This is not applied if no label is
|
|
@@ -129,7 +158,10 @@ export const ViraDropdown = defineViraElement()({
|
|
|
129
158
|
};
|
|
130
159
|
},
|
|
131
160
|
render({ state, inputs, dispatch, events, updateState, testIds }) {
|
|
132
|
-
const
|
|
161
|
+
const flatOptions = inputs.options.flatMap((entry) => {
|
|
162
|
+
return isViraDropdownOptionGroup(entry) ? entry.options : [entry];
|
|
163
|
+
});
|
|
164
|
+
const selectedOptions = filterMap(inputs.selected, (selectedValue) => flatOptions.find((option) => option.value === selectedValue), check.isTruthy);
|
|
133
165
|
const leadingIconTemplate = inputs.icon
|
|
134
166
|
? html `
|
|
135
167
|
<${ViraIcon.assign({
|
|
@@ -165,13 +197,78 @@ export const ViraDropdown = defineViraElement()({
|
|
|
165
197
|
detail: newSelectedValues,
|
|
166
198
|
}));
|
|
167
199
|
}
|
|
200
|
+
function toggleGroup(group) {
|
|
201
|
+
const enabledGroupOptions = group.options.filter((option) => !option.disabled);
|
|
202
|
+
const isGroupSelected = enabledGroupOptions.every((option) => {
|
|
203
|
+
return selectedOptions.includes(option);
|
|
204
|
+
});
|
|
205
|
+
dispatch(new events.selectedValuesChange({
|
|
206
|
+
detail: (isGroupSelected
|
|
207
|
+
? selectedOptions.filter((option) => !enabledGroupOptions.includes(option))
|
|
208
|
+
: [
|
|
209
|
+
...selectedOptions,
|
|
210
|
+
...enabledGroupOptions.filter((option) => {
|
|
211
|
+
return !selectedOptions.includes(option);
|
|
212
|
+
}),
|
|
213
|
+
]).map((option) => option.value),
|
|
214
|
+
}));
|
|
215
|
+
}
|
|
216
|
+
function renderGroupLabel(group) {
|
|
217
|
+
return inputs.isMultiSelect
|
|
218
|
+
? html `
|
|
219
|
+
<${ViraMenuItem.assign({
|
|
220
|
+
disablePointerStyles: true,
|
|
221
|
+
})}
|
|
222
|
+
class="option-group-label"
|
|
223
|
+
${listen('click', () => {
|
|
224
|
+
toggleGroup(group);
|
|
225
|
+
})}
|
|
226
|
+
>
|
|
227
|
+
${group.groupName}
|
|
228
|
+
</${ViraMenuItem}>
|
|
229
|
+
`
|
|
230
|
+
: html `
|
|
231
|
+
<div class="option-group-label">${group.groupName}</div>
|
|
232
|
+
`;
|
|
233
|
+
}
|
|
234
|
+
/** Drag-select actions in the same order as the rendered menu items. */
|
|
235
|
+
const menuItemActions = inputs.options.flatMap((entry) => {
|
|
236
|
+
const options = isViraDropdownOptionGroup(entry) ? entry.options : [entry];
|
|
237
|
+
const optionActions = options.map((option) => {
|
|
238
|
+
return option.disabled
|
|
239
|
+
? undefined
|
|
240
|
+
: () => {
|
|
241
|
+
selectOption(option);
|
|
242
|
+
};
|
|
243
|
+
});
|
|
244
|
+
return isViraDropdownOptionGroup(entry) && inputs.isMultiSelect
|
|
245
|
+
? [
|
|
246
|
+
() => {
|
|
247
|
+
toggleGroup(entry);
|
|
248
|
+
},
|
|
249
|
+
...optionActions,
|
|
250
|
+
]
|
|
251
|
+
: optionActions;
|
|
252
|
+
});
|
|
253
|
+
function renderOptionEntries(options) {
|
|
254
|
+
return renderMenuItemEntries(options.map((option) => {
|
|
255
|
+
return {
|
|
256
|
+
content: option.label,
|
|
257
|
+
onClick() {
|
|
258
|
+
selectOption(option);
|
|
259
|
+
},
|
|
260
|
+
disabled: option.disabled,
|
|
261
|
+
selected: selectedOptions.includes(option),
|
|
262
|
+
};
|
|
263
|
+
}));
|
|
264
|
+
}
|
|
265
|
+
const isMenuShown = state.isOpen && (!!flatOptions.length || !!inputs.noOptionsText);
|
|
168
266
|
const menuTemplate = html `
|
|
169
267
|
<${ViraMenu.assign({
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
: ViraMenuPopUpDirection.Upwards,
|
|
268
|
+
cornerStyle: ViraMenuCornerStyle.Round,
|
|
269
|
+
hoverScrollSpeed: inputs.menuHoverScrollSpeed,
|
|
173
270
|
})}
|
|
174
|
-
slot=${
|
|
271
|
+
slot=${ViraPopoverTrigger.slotNames['vira-popover-trigger-popover']}
|
|
175
272
|
${listen('mouseup', (event) => {
|
|
176
273
|
if (state.shouldSelectOnMouseUp) {
|
|
177
274
|
const menuItem = event
|
|
@@ -179,46 +276,44 @@ export const ViraDropdown = defineViraElement()({
|
|
|
179
276
|
.find((eventTarget) => {
|
|
180
277
|
return eventTarget instanceof ViraMenuItem;
|
|
181
278
|
});
|
|
182
|
-
const
|
|
183
|
-
?
|
|
279
|
+
const menuItemAction = menuItem
|
|
280
|
+
? menuItemActions[Array.from(extractEventTarget(event, ViraMenu).querySelectorAll(ViraMenuItem.tagName)).indexOf(menuItem)]
|
|
184
281
|
: undefined;
|
|
185
|
-
|
|
186
|
-
selectOption(option);
|
|
187
|
-
}
|
|
282
|
+
menuItemAction?.();
|
|
188
283
|
}
|
|
189
284
|
})}
|
|
190
285
|
>
|
|
191
|
-
${
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
286
|
+
${flatOptions.length
|
|
287
|
+
? inputs.options.map((entry) => {
|
|
288
|
+
return isViraDropdownOptionGroup(entry)
|
|
289
|
+
? html `
|
|
290
|
+
${renderGroupLabel(entry)} ${renderOptionEntries(entry.options)}
|
|
291
|
+
`
|
|
292
|
+
: renderOptionEntries([entry]);
|
|
293
|
+
})
|
|
294
|
+
: html `
|
|
295
|
+
<div class="no-options">${inputs.noOptionsText}</div>
|
|
296
|
+
`}
|
|
201
297
|
</${ViraMenu}>
|
|
202
298
|
`;
|
|
203
299
|
const triggerTemplate = html `
|
|
204
|
-
<${
|
|
300
|
+
<${ViraPopoverTrigger.assign({
|
|
205
301
|
...inputs,
|
|
206
302
|
keepOpenAfterInteraction: inputs.isMultiSelect,
|
|
207
303
|
focusOnClose: true,
|
|
208
|
-
|
|
304
|
+
popoverOffset: {
|
|
209
305
|
vertical: -1,
|
|
210
306
|
right: 24,
|
|
211
307
|
},
|
|
212
|
-
horizontalAnchor: inputs.horizontalAnchor || HorizontalAnchor.Both,
|
|
213
308
|
})}
|
|
214
|
-
${listen(
|
|
215
|
-
if (
|
|
309
|
+
${listen(ViraPopoverTrigger.events.openChange, (event) => {
|
|
310
|
+
if (state.isOpen !== event.detail) {
|
|
216
311
|
dispatch(new events.openChange({
|
|
217
312
|
detail: event.detail,
|
|
218
313
|
}));
|
|
219
314
|
}
|
|
220
315
|
updateState({
|
|
221
|
-
|
|
316
|
+
isOpen: event.detail,
|
|
222
317
|
});
|
|
223
318
|
})}
|
|
224
319
|
${listen('mouseup', () => {
|
|
@@ -231,15 +326,15 @@ export const ViraDropdown = defineViraElement()({
|
|
|
231
326
|
>
|
|
232
327
|
<div
|
|
233
328
|
class="dropdown-trigger ${classMap({
|
|
234
|
-
open:
|
|
235
|
-
'open-upwards': !state.showPopUpResult?.popDown,
|
|
329
|
+
open: state.isOpen,
|
|
236
330
|
})}"
|
|
237
|
-
slot=${
|
|
331
|
+
slot=${ViraPopoverTrigger.slotNames['vira-popover-trigger-trigger']}
|
|
238
332
|
id=${ifDefined(inputs.label ? state.randomId : undefined)}
|
|
239
333
|
aria-label=${ifDefined((check.isString(inputs.label) && inputs.label) || undefined)}
|
|
334
|
+
${attributes(inputs.attributePassthrough)}
|
|
240
335
|
${testId(testIds.trigger)}
|
|
241
336
|
${listen('mousedown', () => {
|
|
242
|
-
if (!state.
|
|
337
|
+
if (!state.isOpen) {
|
|
243
338
|
updateState({
|
|
244
339
|
shouldSelectOnMouseUp: true,
|
|
245
340
|
});
|
|
@@ -264,19 +359,37 @@ export const ViraDropdown = defineViraElement()({
|
|
|
264
359
|
></${ViraIcon}>
|
|
265
360
|
</span>
|
|
266
361
|
</div>
|
|
267
|
-
${
|
|
268
|
-
</${
|
|
362
|
+
${isMenuShown ? menuTemplate : nothing}
|
|
363
|
+
</${ViraPopoverTrigger}>
|
|
269
364
|
`;
|
|
365
|
+
const contentTemplate = inputs.isReadonly
|
|
366
|
+
? html `
|
|
367
|
+
<span class="readonly-value">${selectionDisplay}</span>
|
|
368
|
+
`
|
|
369
|
+
: triggerTemplate;
|
|
270
370
|
if (inputs.label) {
|
|
271
371
|
return html `
|
|
272
|
-
<label
|
|
372
|
+
<label
|
|
373
|
+
for=${ifDefined(inputs.isReadonly ? undefined : state.randomId)}
|
|
374
|
+
class=${classMap({
|
|
375
|
+
'has-error': !!inputs.hasError,
|
|
376
|
+
})}
|
|
377
|
+
>
|
|
273
378
|
<span class="dropdown-label">${inputs.label}</span>
|
|
274
|
-
${
|
|
379
|
+
${contentTemplate}
|
|
275
380
|
</label>
|
|
276
381
|
`;
|
|
277
382
|
}
|
|
278
383
|
else {
|
|
279
|
-
return
|
|
384
|
+
return html `
|
|
385
|
+
<span
|
|
386
|
+
class="dropdown-wrapper ${classMap({
|
|
387
|
+
'has-error': !!inputs.hasError,
|
|
388
|
+
})}"
|
|
389
|
+
>
|
|
390
|
+
${contentTemplate}
|
|
391
|
+
</span>
|
|
392
|
+
`;
|
|
280
393
|
}
|
|
281
394
|
},
|
|
282
395
|
});
|
|
@@ -6,8 +6,8 @@ import { defineViraElement } from '../util/define-vira-element.js';
|
|
|
6
6
|
import { applyRequiredLabel, areFormFieldsValid, ViraFormFieldType, } from '../util/vira-form-fields.js';
|
|
7
7
|
import { ViraCheckbox } from './vira-checkbox.element.js';
|
|
8
8
|
import { ViraDateInput } from './vira-date-input.element.js';
|
|
9
|
+
import { ViraDropdown } from './vira-dropdown.element.js';
|
|
9
10
|
import { ViraInput, ViraInputType } from './vira-input.element.js';
|
|
10
|
-
import { ViraSelect } from './vira-select.element.js';
|
|
11
11
|
import { ViraTextArea } from './vira-text-area.element.js';
|
|
12
12
|
/**
|
|
13
13
|
* A form element.
|
|
@@ -70,9 +70,9 @@ export const ViraForm = defineViraElement()({
|
|
|
70
70
|
&
|
|
71
71
|
> ${ViraDateInput},
|
|
72
72
|
&
|
|
73
|
-
> ${
|
|
73
|
+
> ${ViraDropdown},
|
|
74
74
|
&
|
|
75
|
-
> ${
|
|
75
|
+
> ${ViraInput},
|
|
76
76
|
&
|
|
77
77
|
> ${ViraTextArea} {
|
|
78
78
|
width: 100%;
|
|
@@ -159,34 +159,28 @@ export const ViraForm = defineViraElement()({
|
|
|
159
159
|
return wrapFormField({
|
|
160
160
|
label,
|
|
161
161
|
fieldTemplate: html `
|
|
162
|
-
<${
|
|
162
|
+
<${ViraDropdown.assign({
|
|
163
163
|
options: field.options,
|
|
164
|
-
|
|
164
|
+
selected: field.value ? [field.value] : [],
|
|
165
165
|
placeholder: field.placeholder,
|
|
166
|
-
|
|
166
|
+
isDisabled,
|
|
167
167
|
isReadonly: inputs.isReadonly,
|
|
168
168
|
label: childLabel,
|
|
169
169
|
hasError: field.hasError,
|
|
170
170
|
icon: field.icon,
|
|
171
|
-
|
|
172
|
-
? {
|
|
173
|
-
attributePassthrough: {
|
|
174
|
-
select: horizontalLabelAttributes,
|
|
175
|
-
},
|
|
176
|
-
}
|
|
177
|
-
: {}),
|
|
171
|
+
attributePassthrough: horizontalLabelAttributes,
|
|
178
172
|
})}
|
|
179
173
|
${field.testId ? testId(field.testId) : nothing}
|
|
180
|
-
${listen(
|
|
174
|
+
${listen(ViraDropdown.events.selectedValuesChange, (event) => {
|
|
181
175
|
dispatch(new events.valueChange({
|
|
182
176
|
detail: {
|
|
183
177
|
key,
|
|
184
178
|
...field,
|
|
185
|
-
value: event.detail,
|
|
179
|
+
value: event.detail[0],
|
|
186
180
|
},
|
|
187
181
|
}));
|
|
188
182
|
})}
|
|
189
|
-
></${
|
|
183
|
+
></${ViraDropdown}>
|
|
190
184
|
`,
|
|
191
185
|
});
|
|
192
186
|
}
|