vira 28.5.1 → 28.7.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.
@@ -13,9 +13,11 @@ export * from './vira-checkbox.element.js';
13
13
  export * from './vira-collapsible-wrapper.element.js';
14
14
  export * from './vira-dropdown.element.js';
15
15
  export * from './vira-error.element.js';
16
+ export * from './vira-form.element.js';
16
17
  export * from './vira-icon.element.js';
17
18
  export * from './vira-image.element.js';
18
19
  export * from './vira-input.element.js';
19
20
  export * from './vira-link.element.js';
20
21
  export * from './vira-modal.element.js';
21
22
  export * from './vira-progress.element.js';
23
+ export * from './vira-select.element.js';
@@ -13,9 +13,11 @@ export * from './vira-checkbox.element.js';
13
13
  export * from './vira-collapsible-wrapper.element.js';
14
14
  export * from './vira-dropdown.element.js';
15
15
  export * from './vira-error.element.js';
16
+ export * from './vira-form.element.js';
16
17
  export * from './vira-icon.element.js';
17
18
  export * from './vira-image.element.js';
18
19
  export * from './vira-input.element.js';
19
20
  export * from './vira-link.element.js';
20
21
  export * from './vira-modal.element.js';
21
22
  export * from './vira-progress.element.js';
23
+ export * from './vira-select.element.js';
@@ -1,3 +1,4 @@
1
+ import { type PartialWithUndefined } from '@augment-vir/common';
1
2
  import { type AttributeValues } from 'element-vir';
2
3
  /**
3
4
  * Inputs shared between the multiple input elements.
@@ -6,10 +7,11 @@ import { type AttributeValues } from 'element-vir';
6
7
  */
7
8
  export type SharedTextInputElementInputs = {
8
9
  value: string;
10
+ } & PartialWithUndefined<{
9
11
  /** Shown when no other text is present. Input restrictions do not apply to this property. */
10
- placeholder?: string;
12
+ placeholder: string;
11
13
  /** Set to true to trigger disabled styles and to block all user input. */
12
- disabled?: boolean;
14
+ disabled: boolean;
13
15
  /**
14
16
  * Only letters in the given string or matches to the given RegExp will be allowed.
15
17
  * blockedInputs takes precedence over this input.
@@ -17,16 +19,16 @@ export type SharedTextInputElementInputs = {
17
19
  * For example: if allowedInputs is set to "abcd" and blockedInputs is set to "d", only "a",
18
20
  * "b", or "c" letters will be allowed.
19
21
  */
20
- allowedInputs?: string | RegExp;
22
+ allowedInputs: string | RegExp;
21
23
  /** Any letters in the given string or matches to the given RegExp will be blocked. */
22
- blockedInputs?: string | RegExp;
24
+ blockedInputs: string | RegExp;
23
25
  /** Disable all browser helps like spellchecking, autocomplete, etc. */
24
- disableBrowserHelps?: boolean;
26
+ disableBrowserHelps: boolean;
25
27
  /** Set this to true to make the whole element size to only fit the input text. */
26
- fitText?: boolean;
28
+ fitText: boolean;
27
29
  /** A set of attributes that will be applied to the inner native text element. */
28
- attributePassthrough?: AttributeValues | undefined;
29
- };
30
+ attributePassthrough: AttributeValues;
31
+ }>;
30
32
  /**
31
33
  * Inputs used to check if the current input element value is allowed.
32
34
  *
@@ -17,6 +17,7 @@ export type ViraCheckboxInputs = PartialWithUndefined<{
17
17
  attributePassthrough: Record<ViraCheckboxInnerElements, AttributeValues>;
18
18
  disabled: boolean;
19
19
  label: string;
20
+ hasError: boolean;
20
21
  }> & {
21
22
  value: boolean;
22
23
  };
@@ -34,22 +34,22 @@ export const ViraCheckbox = defineViraElement()({
34
34
 
35
35
  label {
36
36
  display: inline-flex;
37
- gap: 8px;
37
+ flex-direction: column;
38
+ align-items: flex-start;
39
+ gap: 4px;
38
40
 
39
41
  &.disabled {
40
42
  cursor: not-allowed;
41
43
  }
42
44
 
43
- & .text {
45
+ & .label-text {
44
46
  cursor: pointer;
45
- &::first-line {
46
- line-height: 24px;
47
- }
47
+ font-weight: ${viraFormCssVars['vira-form-label-font-weight'].value};
48
48
  }
49
49
  }
50
50
 
51
51
  ${ViraIcon} {
52
- ${viraIconCssVars['vira-icon-stroke-width'].name}: 3px;
52
+ ${viraIconCssVars['vira-icon-stroke-width'].name}: 2px;
53
53
  opacity: 0;
54
54
  }
55
55
 
@@ -71,6 +71,10 @@ export const ViraCheckbox = defineViraElement()({
71
71
  }
72
72
  }
73
73
 
74
+ &.error {
75
+ border-color: ${viraFormCssVars['vira-form-error-foreground-color'].value};
76
+ }
77
+
74
78
  &:hover {
75
79
  background-color: ${viraFormCssVars['vira-form-selection-hover-background-color']
76
80
  .value};
@@ -98,7 +102,7 @@ export const ViraCheckbox = defineViraElement()({
98
102
  const textLabel = inputs.label
99
103
  ? html `
100
104
  <span
101
- class="text"
105
+ class="label-text"
102
106
  ${attributes(inputs.attributePassthrough?.['text'])}
103
107
  style=${ifDefined(inputs.stylePassthrough?.['text'])}
104
108
  >
@@ -113,12 +117,14 @@ export const ViraCheckbox = defineViraElement()({
113
117
  })}
114
118
  ${attributes(inputs.attributePassthrough?.label)}
115
119
  style=${ifDefined(inputs.stylePassthrough?.label)}
116
- ${listen('click', updateValue)}
120
+ ${listen('mousedown', updateValue)}
117
121
  >
122
+ ${textLabel}
118
123
  <span
119
124
  class="custom-checkbox ${classMap({
120
125
  checked: inputs.value,
121
126
  disabled: !!inputs.disabled,
127
+ error: !!inputs.hasError,
122
128
  })}"
123
129
  role="checkbox"
124
130
  aria-checked=${inputs.value ? 'true' : 'false'}
@@ -136,7 +142,6 @@ export const ViraCheckbox = defineViraElement()({
136
142
  style=${ifDefined(inputs.stylePassthrough?.[ViraIcon.tagName])}
137
143
  ></${ViraIcon}>
138
144
  </span>
139
- ${textLabel}
140
145
  </label>
141
146
  `;
142
147
  },
@@ -0,0 +1,69 @@
1
+ import { type PartialWithUndefined } from '@augment-vir/common';
2
+ import { type ViraIconSvg } from '../icons/icon-svg.js';
3
+ import { type ViraSelectOption } from './vira-select.element.js';
4
+ /**
5
+ * Form field types for {@link ViraFormField}.
6
+ *
7
+ * @category Internal
8
+ */
9
+ export declare enum ViraFormFieldType {
10
+ Text = "text",
11
+ /** Allows auto complete for _existing_ passwords used on this website (for login). */
12
+ ExistingPassword = "existing-password",
13
+ /** Allows auto complete for _new_ passwords used on this website (for login). */
14
+ NewPassword = "new-password",
15
+ Email = "email",
16
+ Select = "select",
17
+ Checkbox = "checkbox"
18
+ }
19
+ /**
20
+ * An individual form field for {@link ViraFormFields}.
21
+ *
22
+ * @category Internal
23
+ */
24
+ export type ViraFormField = {
25
+ type: ViraFormFieldType.Text | ViraFormFieldType.ExistingPassword | ViraFormFieldType.NewPassword | ViraFormFieldType.Email;
26
+ label: string;
27
+ value: string;
28
+ placeholder?: string | undefined;
29
+ disabled?: boolean | undefined;
30
+ icon?: ViraIconSvg | undefined;
31
+ hasError?: boolean | undefined;
32
+ isUsername?: boolean | undefined;
33
+ } | {
34
+ type: ViraFormFieldType.Select;
35
+ label: string;
36
+ value: string | undefined;
37
+ options: ReadonlyArray<Readonly<ViraSelectOption>>;
38
+ placeholder?: string | undefined;
39
+ disabled?: boolean | undefined;
40
+ icon?: ViraIconSvg | undefined;
41
+ hasError?: boolean | undefined;
42
+ } | {
43
+ type: ViraFormFieldType.Checkbox;
44
+ label: string;
45
+ value: boolean;
46
+ disabled?: boolean | undefined;
47
+ hasError?: boolean | undefined;
48
+ };
49
+ /**
50
+ * A collection of form fields for {@link ViraForm}.
51
+ *
52
+ * @category Internal
53
+ */
54
+ export type ViraFormFields = Record<string, ViraFormField>;
55
+ /**
56
+ * A form element.
57
+ *
58
+ * @category Elements
59
+ * @see https://electrovir.github.io/vira/book/elements/vira-form
60
+ */
61
+ export declare const ViraForm: import("element-vir").DeclarativeElementDefinition<"vira-form", Readonly<{
62
+ fields: Readonly<ViraFormFields>;
63
+ } & PartialWithUndefined<{
64
+ showClearButtons: boolean;
65
+ }>>, {}, {
66
+ valueChange: import("element-vir").DefineEvent<{
67
+ key: string;
68
+ } & ViraFormField>;
69
+ }, "vira-form-", "vira-form-", readonly ["actionButtons"], readonly []>;
@@ -0,0 +1,137 @@
1
+ import { getObjectTypedEntries } from '@augment-vir/common';
2
+ import { css, defineElementEvent, html, listen } from 'element-vir';
3
+ import { defineViraElement } from './define-vira-element.js';
4
+ import { ViraCheckbox } from './vira-checkbox.element.js';
5
+ import { ViraInput, ViraInputType } from './vira-input.element.js';
6
+ import { ViraSelect } from './vira-select.element.js';
7
+ /**
8
+ * Form field types for {@link ViraFormField}.
9
+ *
10
+ * @category Internal
11
+ */
12
+ export var ViraFormFieldType;
13
+ (function (ViraFormFieldType) {
14
+ ViraFormFieldType["Text"] = "text";
15
+ /** Allows auto complete for _existing_ passwords used on this website (for login). */
16
+ ViraFormFieldType["ExistingPassword"] = "existing-password";
17
+ /** Allows auto complete for _new_ passwords used on this website (for login). */
18
+ ViraFormFieldType["NewPassword"] = "new-password";
19
+ ViraFormFieldType["Email"] = "email";
20
+ ViraFormFieldType["Select"] = "select";
21
+ ViraFormFieldType["Checkbox"] = "checkbox";
22
+ })(ViraFormFieldType || (ViraFormFieldType = {}));
23
+ /**
24
+ * A form element.
25
+ *
26
+ * @category Elements
27
+ * @see https://electrovir.github.io/vira/book/elements/vira-form
28
+ */
29
+ export const ViraForm = defineViraElement()({
30
+ tagName: 'vira-form',
31
+ slotNames: ['actionButtons'],
32
+ events: {
33
+ valueChange: defineElementEvent(),
34
+ },
35
+ styles: css `
36
+ form {
37
+ display: flex;
38
+ flex-direction: column;
39
+ gap: 10px;
40
+ }
41
+ `,
42
+ render({ inputs, slotNames, dispatch, events }) {
43
+ const formFields = getObjectTypedEntries(inputs.fields).map(([key, field,]) => {
44
+ if (field.type === ViraFormFieldType.Checkbox) {
45
+ return html `
46
+ <${ViraCheckbox.assign({
47
+ value: field.value,
48
+ disabled: field.disabled,
49
+ hasError: field.hasError,
50
+ label: field.label,
51
+ })}
52
+ ${listen(ViraCheckbox.events.valueChange, (event) => {
53
+ dispatch(new events.valueChange({
54
+ key,
55
+ ...field,
56
+ value: event.detail,
57
+ }));
58
+ })}
59
+ ></${ViraCheckbox}>
60
+ `;
61
+ }
62
+ else if (field.type === ViraFormFieldType.Select) {
63
+ return html `
64
+ <${ViraSelect.assign({
65
+ options: field.options,
66
+ value: field.value,
67
+ placeholder: field.placeholder,
68
+ disabled: field.disabled,
69
+ label: field.label,
70
+ hasError: field.hasError,
71
+ icon: field.icon,
72
+ })}
73
+ ${listen(ViraSelect.events.valueChange, (event) => {
74
+ dispatch(new events.valueChange({
75
+ key,
76
+ ...field,
77
+ value: event.detail,
78
+ }));
79
+ })}
80
+ ></${ViraSelect}>
81
+ `;
82
+ }
83
+ else {
84
+ return html `
85
+ <${ViraInput.assign({
86
+ value: field.value,
87
+ disabled: field.disabled,
88
+ hasError: field.hasError,
89
+ icon: field.icon,
90
+ label: field.label,
91
+ placeholder: field.placeholder,
92
+ showClearButton: inputs.showClearButtons,
93
+ attributePassthrough: field.isUsername
94
+ ? {
95
+ autocomplete: 'username',
96
+ }
97
+ : field.type === ViraFormFieldType.NewPassword
98
+ ? {
99
+ autocomplete: 'new-password',
100
+ }
101
+ : field.type === ViraFormFieldType.ExistingPassword
102
+ ? {
103
+ autocomplete: 'password',
104
+ }
105
+ : field.type === ViraFormFieldType.Email
106
+ ? {
107
+ autocomplete: 'email',
108
+ }
109
+ : {},
110
+ type: [
111
+ ViraFormFieldType.NewPassword,
112
+ ViraFormFieldType.ExistingPassword,
113
+ ].includes(field.type)
114
+ ? ViraInputType.Password
115
+ : field.type === ViraFormFieldType.Email
116
+ ? ViraInputType.Email
117
+ : ViraInputType.Default,
118
+ })}
119
+ ${listen(ViraInput.events.valueChange, (event) => {
120
+ dispatch(new events.valueChange({
121
+ key,
122
+ ...field,
123
+ value: event.detail,
124
+ }));
125
+ })}
126
+ ></${ViraInput}>
127
+ `;
128
+ }
129
+ });
130
+ return html `
131
+ <form ${listen('submit', (event) => event.preventDefault())}>
132
+ ${formFields}
133
+ <slot name=${slotNames.actionButtons}></slot>
134
+ </form>
135
+ `;
136
+ },
137
+ });
@@ -1,6 +1,5 @@
1
1
  import { type PartialWithUndefined } from '@augment-vir/common';
2
2
  import { type ViraIconSvg } from '../icons/index.js';
3
- import { type SharedTextInputElementInputs } from './shared-text-input-logic.js';
4
3
  export * from './shared-text-input-logic.js';
5
4
  /**
6
5
  * Input types for {@link ViraInput}.
@@ -30,7 +29,17 @@ export declare const ViraInput: import("element-vir").DeclarativeElementDefiniti
30
29
  hasError: boolean;
31
30
  showClearButton: boolean;
32
31
  type: ViraInputType;
33
- }> & SharedTextInputElementInputs>, {
32
+ }> & {
33
+ value: string;
34
+ } & PartialWithUndefined<{
35
+ placeholder: string;
36
+ disabled: boolean;
37
+ allowedInputs: string | RegExp;
38
+ blockedInputs: string | RegExp;
39
+ disableBrowserHelps: boolean;
40
+ fitText: boolean;
41
+ attributePassthrough: import("element-vir").AttributeValues;
42
+ }>>, {
34
43
  forcedInputWidth: number;
35
44
  showPassword: boolean;
36
45
  /**
@@ -50,4 +59,4 @@ export declare const ViraInput: import("element-vir").DeclarativeElementDefiniti
50
59
  * that was blocked out of programmatic "value" property assignments.
51
60
  */
52
61
  inputBlocked: import("element-vir").DefineEvent<string>;
53
- }, "vira-input-disabled" | "vira-input-fit-text" | "vira-input-clear-button-shown" | "vira-input-error", "vira-input-background-color" | "vira-input-placeholder-color" | "vira-input-text-color" | "vira-input-border-color" | "vira-input-text-selection-color" | "vira-input-action-button-color" | "vira-input-clear-button-hover-color" | "vira-input-clear-button-active-color" | "vira-input-show-password-button-hover-color" | "vira-input-show-password-button-active-color" | "vira-input-padding-horizontal" | "vira-input-padding-vertical" | "vira-input-label-font-weight", readonly [], readonly []>;
62
+ }, "vira-input-disabled" | "vira-input-fit-text" | "vira-input-clear-button-shown" | "vira-input-error", "vira-input-action-button-color" | "vira-input-clear-button-hover-color" | "vira-input-clear-button-active-color" | "vira-input-show-password-button-hover-color" | "vira-input-show-password-button-active-color" | "vira-input-padding-horizontal" | "vira-input-padding-vertical", readonly [], readonly []>;
@@ -34,11 +34,6 @@ export var ViraInputType;
34
34
  export const ViraInput = defineViraElement()({
35
35
  tagName: 'vira-input',
36
36
  cssVars: {
37
- 'vira-input-background-color': 'white',
38
- 'vira-input-placeholder-color': '#cccccc',
39
- 'vira-input-text-color': '#000000',
40
- 'vira-input-border-color': '#cccccc',
41
- 'vira-input-text-selection-color': '#cfe9ff',
42
37
  'vira-input-action-button-color': '#aaaaaa',
43
38
  'vira-input-clear-button-hover-color': '#ff0000',
44
39
  'vira-input-clear-button-active-color': '#b30000',
@@ -48,7 +43,6 @@ export const ViraInput = defineViraElement()({
48
43
  'vira-input-show-password-button-active-color': '#0261ba',
49
44
  'vira-input-padding-horizontal': '10px',
50
45
  'vira-input-padding-vertical': '6px',
51
- 'vira-input-label-font-weight': 'bold',
52
46
  },
53
47
  styles: ({ hostClasses, cssVars }) => {
54
48
  return css `
@@ -57,7 +51,7 @@ export const ViraInput = defineViraElement()({
57
51
  display: inline-flex;
58
52
  width: 224px;
59
53
  box-sizing: border-box;
60
- color: ${cssVars['vira-input-text-color'].value};
54
+ color: ${viraFormCssVars['vira-form-foreground-color'].value};
61
55
  }
62
56
 
63
57
  label {
@@ -67,19 +61,16 @@ export const ViraInput = defineViraElement()({
67
61
  gap: 2px;
68
62
  width: 100%;
69
63
  max-width: 100%;
64
+ cursor: text;
70
65
 
71
66
  & .input-label {
72
- font-weight: ${cssVars['vira-input-label-font-weight'].value};
67
+ font-weight: ${viraFormCssVars['vira-form-label-font-weight'].value};
73
68
  text-align: left;
74
69
  flex-shrink: 0;
75
70
  flex-wrap: wrap;
76
71
  }
77
72
  }
78
73
 
79
- ${hostClasses['vira-input-disabled'].selector} {
80
- ${viraDisabledStyles};
81
- }
82
-
83
74
  ${hostClasses['vira-input-fit-text'].selector} {
84
75
  width: unset;
85
76
  }
@@ -144,9 +135,7 @@ export const ViraInput = defineViraElement()({
144
135
  .wrapper-border {
145
136
  top: -1px;
146
137
  left: -1px;
147
- border: 1px solid ${cssVars['vira-input-border-color'].value};
148
- transition: border
149
- ${viraAnimationDurations['vira-interaction-animation-duration'].value};
138
+ border: 1px solid ${viraFormCssVars['vira-form-border-color'].value};
150
139
  }
151
140
 
152
141
  .input-wrapper {
@@ -159,7 +148,7 @@ export const ViraInput = defineViraElement()({
159
148
  position: relative;
160
149
  padding: 0 ${cssVars['vira-input-padding-horizontal'].value};
161
150
  border-radius: ${viraBorders['vira-form-input-radius'].value};
162
- background-color: ${cssVars['vira-input-background-color'].value};
151
+ background-color: ${viraFormCssVars['vira-form-background-color'].value};
163
152
  /*
164
153
  Border colors are actually applied via the .wrapper-border class. However, we must
165
154
  apply a border here still so that it takes up space.
@@ -195,11 +184,11 @@ export const ViraInput = defineViraElement()({
195
184
  }
196
185
 
197
186
  ::selection {
198
- background: ${cssVars['vira-input-text-selection-color']
187
+ background: ${viraFormCssVars['vira-form-text-selection-color']
199
188
  .value}; /* WebKit/Blink Browsers */
200
189
  }
201
190
  ::-moz-selection {
202
- background: ${cssVars['vira-input-text-selection-color']
191
+ background: ${viraFormCssVars['vira-form-text-selection-color']
203
192
  .value}; /* Gecko Browsers */
204
193
  }
205
194
 
@@ -209,7 +198,7 @@ export const ViraInput = defineViraElement()({
209
198
  }
210
199
 
211
200
  input::placeholder {
212
- color: ${cssVars['vira-input-placeholder-color'].value};
201
+ color: ${viraFormCssVars['vira-form-placeholder-color'].value};
213
202
  }
214
203
 
215
204
  .suffix {
@@ -251,6 +240,25 @@ export const ViraInput = defineViraElement()({
251
240
  border-color: ${viraFormCssVars['vira-form-error-foreground-color'].value};
252
241
  }
253
242
  }
243
+
244
+ ${hostClasses['vira-input-disabled'].selector} {
245
+ cursor: not-allowed;
246
+
247
+ & label,
248
+ & .input-wrapper {
249
+ cursor: not-allowed;
250
+ }
251
+
252
+ & input,
253
+ & .wrapper-border,
254
+ & input::placeholder {
255
+ ${viraDisabledStyles};
256
+ }
257
+
258
+ & .focus-border {
259
+ display: none;
260
+ }
261
+ }
254
262
  `;
255
263
  },
256
264
  events: {
@@ -299,16 +307,7 @@ export const ViraInput = defineViraElement()({
299
307
  width: ${state.forcedInputWidth}px;
300
308
  `
301
309
  : nothing;
302
- const shouldBlockBrowserHelps = inputs.disableBrowserHelps ||
303
- /**
304
- * Some browsers leaks passwords with their browser helps (like Chrome with
305
- * spellchecking).
306
- */
307
- inputs.type === ViraInputType.Password;
308
- const inputTemplate = html `
309
- <span
310
- class="input-wrapper"
311
- ${listen('mousedown', (event) => {
310
+ const mousedownListener = listen('mousedown', (event) => {
312
311
  const eventTarget = extractEventTarget(event, HTMLElement, {
313
312
  useOriginalTarget: true,
314
313
  });
@@ -317,8 +316,15 @@ export const ViraInput = defineViraElement()({
317
316
  event.preventDefault();
318
317
  inputElement.focus();
319
318
  }
320
- })}
321
- >
319
+ });
320
+ const shouldBlockBrowserHelps = inputs.disableBrowserHelps ||
321
+ /**
322
+ * Some browsers leaks passwords with their browser helps (like Chrome with
323
+ * spellchecking).
324
+ */
325
+ inputs.type === ViraInputType.Password;
326
+ const inputTemplate = html `
327
+ <span class="input-wrapper" ${inputs.label ? nothing : mousedownListener}>
322
328
  ${iconTemplate}
323
329
  ${renderIf(!!inputs.fitText, html `
324
330
  <span
@@ -334,6 +340,7 @@ export const ViraInput = defineViraElement()({
334
340
  <input
335
341
  id=${ifDefined(inputs.label ? state.randomId : undefined)}
336
342
  aria-label=${ifDefined(inputs.label || undefined)}
343
+ autofocus=${false}
337
344
  type=${calculateEffectiveInputType(inputs.type, state.showPassword)}
338
345
  style=${forcedInputWidthStyles}
339
346
  autocomplete=${ifDefined(shouldBlockBrowserHelps ? 'off' : undefined)}
@@ -408,7 +415,7 @@ export const ViraInput = defineViraElement()({
408
415
  `;
409
416
  if (inputs.label) {
410
417
  return html `
411
- <label for=${state.randomId}>
418
+ <label for=${state.randomId} ${mousedownListener}>
412
419
  <span class="input-label">${inputs.label}</span>
413
420
  ${inputTemplate}
414
421
  </label>
@@ -0,0 +1,48 @@
1
+ import { type PartialWithUndefined } from '@augment-vir/common';
2
+ import { type AttributeValues } from 'element-vir';
3
+ import { type ViraIconSvg } from '../icons/index.js';
4
+ /**
5
+ * Options for {@link ViraSelect}.
6
+ *
7
+ * @category Dropdown
8
+ * @category Elements
9
+ * @see https://electrovir.github.io/vira/book/elements/vira-select
10
+ */
11
+ export type ViraSelectOption = {
12
+ /** A value or id, used to keep track of which option is selected. */
13
+ value: string;
14
+ label: string;
15
+ } & PartialWithUndefined<{
16
+ disabled: boolean;
17
+ }>;
18
+ /**
19
+ * Similar to {@link ViraDropdown} but is, instead, simply a wrapper for `<select>` and nothing more.
20
+ *
21
+ * @category Dropdown
22
+ * @category Elements
23
+ * @see https://electrovir.github.io/vira/book/elements/vira-select
24
+ */
25
+ export declare const ViraSelect: import("element-vir").DeclarativeElementDefinition<"vira-select", Readonly<{
26
+ options: ReadonlyArray<Readonly<ViraSelectOption>>;
27
+ /** The currently selected option value. */
28
+ value: undefined | string;
29
+ } & PartialWithUndefined<{
30
+ icon: Readonly<ViraIconSvg>;
31
+ placeholder: string;
32
+ label: string;
33
+ disabled: boolean;
34
+ attributePassthrough: Readonly<PartialWithUndefined<{
35
+ label: AttributeValues;
36
+ select: AttributeValues;
37
+ option: AttributeValues;
38
+ }>>;
39
+ hasError: boolean;
40
+ }>>, {
41
+ /**
42
+ * Used to couple the label and select together. This is not applied if no label is
43
+ * provided.
44
+ */
45
+ randomId: string;
46
+ }, {
47
+ valueChange: import("element-vir").DefineEvent<string>;
48
+ }, "vira-select-disabled" | "vira-select-error", "vira-select-padding-horizontal" | "vira-select-padding-vertical" | "vira-select-icon-padding", readonly [], readonly []>;
@@ -0,0 +1,240 @@
1
+ import { randomString } from '@augment-vir/common';
2
+ import { extractEventTarget } from '@augment-vir/web';
3
+ import { attributes, classMap, css, defineElementEvent, html, ifDefined, listen, nothing, } from 'element-vir';
4
+ import { ChevronUp24Icon } from '../icons/index.js';
5
+ import { viraDisabledStyles } from '../styles/disabled.js';
6
+ import { createFocusStyles } from '../styles/focus.js';
7
+ import { viraFormCssVars } from '../styles/form-styles.js';
8
+ import { viraAnimationDurations, viraBorders } from '../styles/index.js';
9
+ import { noNativeFormStyles } from '../styles/native-styles.js';
10
+ import { defineViraElement } from './define-vira-element.js';
11
+ import { ViraIcon } from './vira-icon.element.js';
12
+ /**
13
+ * Similar to {@link ViraDropdown} but is, instead, simply a wrapper for `<select>` and nothing more.
14
+ *
15
+ * @category Dropdown
16
+ * @category Elements
17
+ * @see https://electrovir.github.io/vira/book/elements/vira-select
18
+ */
19
+ export const ViraSelect = defineViraElement()({
20
+ tagName: 'vira-select',
21
+ state() {
22
+ return {
23
+ /**
24
+ * Used to couple the label and select together. This is not applied if no label is
25
+ * provided.
26
+ */
27
+ randomId: randomString(32),
28
+ };
29
+ },
30
+ events: {
31
+ valueChange: defineElementEvent(),
32
+ },
33
+ cssVars: {
34
+ 'vira-select-padding-horizontal': '10px',
35
+ 'vira-select-padding-vertical': '6px',
36
+ 'vira-select-icon-padding': '44px',
37
+ },
38
+ hostClasses: {
39
+ 'vira-select-disabled': ({ inputs }) => !!inputs.disabled,
40
+ 'vira-select-error': ({ inputs }) => !!inputs.hasError,
41
+ },
42
+ styles: ({ hostClasses, cssVars }) => css `
43
+ :host {
44
+ position: relative;
45
+ display: inline-flex;
46
+ width: 223px;
47
+ box-sizing: border-box;
48
+ color: ${viraFormCssVars['vira-form-foreground-color'].value};
49
+ }
50
+
51
+ .select-wrapper {
52
+ ${noNativeFormStyles};
53
+ max-width: 100%;
54
+ flex-grow: 1;
55
+ display: inline-flex;
56
+ box-sizing: border-box;
57
+ align-items: center;
58
+ position: relative;
59
+ border-radius: ${viraBorders['vira-form-input-radius'].value};
60
+ background-color: ${viraFormCssVars['vira-form-background-color'].value};
61
+ /*
62
+ Border colors are actually applied via the .wrapper-border class. However, we must
63
+ apply a border here still so that it takes up space.
64
+ */
65
+ border: 1px solid transparent;
66
+ cursor: pointer;
67
+
68
+ & select {
69
+ appearance: none;
70
+ -webkit-appearance: none;
71
+ -moz-appearance: none;
72
+ font: inherit;
73
+ outline: none;
74
+ width: 100%;
75
+ border: none;
76
+ background: none;
77
+ border-radius: inherit;
78
+ padding: ${cssVars['vira-select-padding-vertical'].value} 31px
79
+ ${cssVars['vira-select-padding-vertical'].value}
80
+ ${cssVars['vira-select-padding-horizontal'].value};
81
+ cursor: pointer;
82
+ overflow: hidden;
83
+ text-overflow: ellipsis;
84
+
85
+ &:focus:focus-visible:not([aria-disabled='true']) ~ .focus-border {
86
+ ${createFocusStyles({
87
+ elementBorderSize: 0,
88
+ noNesting: true,
89
+ })}
90
+ }
91
+
92
+ &.placeholder {
93
+ color: ${viraFormCssVars['vira-form-placeholder-color'].value};
94
+ }
95
+
96
+ &.with-icon {
97
+ padding-left: ${cssVars['vira-select-icon-padding'].value};
98
+ }
99
+ }
100
+
101
+ & ${ViraIcon} {
102
+ position: absolute;
103
+ pointer-events: none;
104
+
105
+ &.trigger-icon {
106
+ transform: rotate(180deg);
107
+ right: 3px;
108
+ }
109
+
110
+ &.input-icon {
111
+ left: 10px;
112
+ }
113
+ }
114
+
115
+ & .border-style {
116
+ position: absolute;
117
+ top: 0;
118
+ left: 0;
119
+ width: 100%;
120
+ height: 100%;
121
+ border-radius: ${viraBorders['vira-form-input-radius'].value};
122
+ z-index: 0;
123
+ pointer-events: none;
124
+ }
125
+
126
+ & .wrapper-border {
127
+ top: -1px;
128
+ left: -1px;
129
+ border: 1px solid ${viraFormCssVars['vira-form-border-color'].value};
130
+ transition: border
131
+ ${viraAnimationDurations['vira-interaction-animation-duration'].value};
132
+ }
133
+ }
134
+
135
+ label {
136
+ display: flex;
137
+ flex-direction: column;
138
+ justify-content: flex-start;
139
+ gap: 2px;
140
+ width: 100%;
141
+ max-width: 100%;
142
+
143
+ & .select-label {
144
+ font-weight: ${viraFormCssVars['vira-form-label-font-weight'].value};
145
+ text-align: left;
146
+ flex-shrink: 0;
147
+ flex-wrap: wrap;
148
+ }
149
+ }
150
+
151
+ ${hostClasses['vira-select-disabled'].selector} {
152
+ cursor: not-allowed;
153
+
154
+ & label {
155
+ cursor: not-allowed;
156
+ }
157
+
158
+ & select,
159
+ & .wrapper-border {
160
+ ${viraDisabledStyles}
161
+ }
162
+ }
163
+
164
+ ${hostClasses['vira-select-error'].selector} {
165
+ & .wrapper-border {
166
+ border-color: ${viraFormCssVars['vira-form-error-foreground-color'].value};
167
+ }
168
+ }
169
+ `,
170
+ render({ inputs, state, dispatch, events }) {
171
+ const value = inputs.value || undefined;
172
+ const placeholderOptionTemplate = inputs.placeholder || value == undefined
173
+ ? html `
174
+ <option value="" disabled ?selected=${value == undefined}>
175
+ ${inputs.placeholder}
176
+ </option>
177
+ `
178
+ : nothing;
179
+ const selectTemplate = html `
180
+ <span class="select-wrapper">
181
+ <select
182
+ .value=${ifDefined(value)}
183
+ class=${classMap({
184
+ placeholder: !value && !!inputs.placeholder,
185
+ 'with-icon': !!inputs.icon,
186
+ })}
187
+ tabindex=${inputs.disabled ? -1 : 0}
188
+ id=${ifDefined(inputs.label ? state.randomId : undefined)}
189
+ aria-label=${ifDefined(inputs.label || undefined)}
190
+ aria-disabled=${ifDefined(inputs.disabled ? 'true' : undefined)}
191
+ ${listen('input', (event) => {
192
+ const selectElement = extractEventTarget(event, HTMLSelectElement);
193
+ const newValue = selectElement.value;
194
+ if (selectElement.value !== value) {
195
+ selectElement.selectedIndex = inputs.options.findIndex((option) => option.value === value);
196
+ }
197
+ dispatch(new events.valueChange(newValue));
198
+ })}
199
+ ${attributes(inputs.attributePassthrough?.select)}
200
+ >
201
+ ${placeholderOptionTemplate}
202
+ ${inputs.options.map((option) => {
203
+ return html `
204
+ <option
205
+ ?selected=${option.value === value}
206
+ aria-label=${option.label}
207
+ ?disabled=${option.disabled}
208
+ value=${option.value}
209
+ >
210
+ ${option.label}
211
+ </option>
212
+ `;
213
+ })}
214
+ </select>
215
+ <!--
216
+ These separate style elements are necessary so that we can select them as
217
+ siblings of the focused <select> element.
218
+ -->
219
+
220
+ <div class="border-style focus-border"></div>
221
+
222
+ <div class="border-style wrapper-border"></div>
223
+
224
+ <${ViraIcon.assign({ icon: inputs.icon })} class="input-icon"></${ViraIcon}>
225
+ <${ViraIcon.assign({ icon: ChevronUp24Icon })} class="trigger-icon"></${ViraIcon}>
226
+ </span>
227
+ `;
228
+ if (inputs.label) {
229
+ return html `
230
+ <label for=${state.randomId} ${attributes(inputs.attributePassthrough?.label)}>
231
+ <span class="select-label">${inputs.label}</span>
232
+ ${selectTemplate}
233
+ </label>
234
+ `;
235
+ }
236
+ else {
237
+ return selectTemplate;
238
+ }
239
+ },
240
+ });
@@ -10,7 +10,7 @@ import { viraBorders } from './border.js';
10
10
  */
11
11
  export const viraFocusCssVars = defineCssVars({
12
12
  'vira-focus-outline-color': '#59b1ff',
13
- 'vira-focus-outline-border-radius': css `calc(${viraBorders['vira-form-input-radius'].value} + 4px)`,
13
+ 'vira-focus-outline-border-radius': css `calc(${viraBorders['vira-form-input-radius'].value} + 2px)`,
14
14
  });
15
15
  /**
16
16
  * Create styles that look like an outline for the given selector.
@@ -6,12 +6,15 @@
6
6
  */
7
7
  export declare const viraFormCssVars: import("lit-css-vars").CssVarDefinitions<{
8
8
  readonly 'vira-form-border-color': "#cccccc";
9
+ readonly 'vira-form-placeholder-color': "#cccccc";
9
10
  readonly 'vira-form-background-color': "white";
10
11
  readonly 'vira-form-foreground-color': "black";
11
- readonly 'vira-form-selection-hover-background-color': "#d2eaff";
12
+ readonly 'vira-form-text-selection-color': "#cfe9ff";
13
+ readonly 'vira-form-selection-hover-background-color': "#e6f9fe";
12
14
  readonly 'vira-form-selection-hover-foreground-color': "black";
13
- readonly 'vira-form-selection-active-background-color': "#d2eaff";
15
+ readonly 'vira-form-selection-active-background-color': "#e6f9fe";
14
16
  readonly 'vira-form-selection-active-foreground-color': "black";
15
17
  readonly 'vira-form-error-foreground-color': "red";
16
18
  readonly 'vira-form-success-foreground-color': "green";
19
+ readonly 'vira-form-label-font-weight': "bold";
17
20
  }>;
@@ -7,12 +7,15 @@ import { defineCssVars } from 'lit-css-vars';
7
7
  */
8
8
  export const viraFormCssVars = defineCssVars({
9
9
  'vira-form-border-color': '#cccccc',
10
+ 'vira-form-placeholder-color': '#cccccc',
10
11
  'vira-form-background-color': 'white',
11
12
  'vira-form-foreground-color': 'black',
12
- 'vira-form-selection-hover-background-color': '#d2eaff',
13
+ 'vira-form-text-selection-color': '#cfe9ff',
14
+ 'vira-form-selection-hover-background-color': '#e6f9fe',
13
15
  'vira-form-selection-hover-foreground-color': 'black',
14
- 'vira-form-selection-active-background-color': '#d2eaff',
16
+ 'vira-form-selection-active-background-color': '#e6f9fe',
15
17
  'vira-form-selection-active-foreground-color': 'black',
16
18
  'vira-form-error-foreground-color': 'red',
17
19
  'vira-form-success-foreground-color': 'green',
20
+ 'vira-form-label-font-weight': 'bold',
18
21
  });
@@ -20,6 +20,7 @@ export const noNativeFormStyles = css `
20
20
  background: none;
21
21
  border: none;
22
22
  font: inherit;
23
+ line-height: inherit;
23
24
  color: inherit;
24
25
  text-transform: inherit;
25
26
  text-decoration: inherit;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vira",
3
- "version": "28.5.1",
3
+ "version": "28.7.0",
4
4
  "description": "A simple and highly versatile design system using element-vir.",
5
5
  "keywords": [
6
6
  "design",