vanilla-jui 1.5.11 → 1.5.13

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vanilla-jui",
3
- "version": "1.5.11",
3
+ "version": "1.5.13",
4
4
  "private": false,
5
5
  "description": "A lightweight, reactive, and fully customizable vanilla JS UI library.",
6
6
  "keywords": [
@@ -62,7 +62,7 @@
62
62
  },
63
63
  "dependencies": {
64
64
  "vanilla-create-storage": "^1.0.4",
65
- "vanilla-signal": "^1.1.14",
65
+ "vanilla-signal": "^1.1.15",
66
66
  "vanilla-signal-i18n": "^1.1.2"
67
67
  },
68
68
  "devDependencies": {
@@ -23,7 +23,10 @@ import {
23
23
  resolveProps,
24
24
  validateParam,
25
25
  } from '../utilities/types.ts';
26
- import { createValidator } from '../validation/validator.ts';
26
+ import {
27
+ createValidator,
28
+ type ValidatorClassNameConfig,
29
+ } from '../validation/validator.ts';
27
30
  import { createLoading } from '../primitives/loading.ts';
28
31
  import { t } from 'vanilla-signal-i18n';
29
32
  import locales from '../locales/index.ts';
@@ -137,6 +140,7 @@ export interface FormValidatorConfig {
137
140
  rules?: Record<string, Record<string, unknown>>;
138
141
  messages?: Record<string, Record<string, string>>;
139
142
  vanilla?: boolean;
143
+ className?: ValidatorClassNameConfig;
140
144
  onSubmit?: (() => void) | null;
141
145
  [key: string]: unknown;
142
146
  }
@@ -640,6 +644,7 @@ export function createForm(input: FormProps = {}): Form {
640
644
  if (!element) return;
641
645
  for (const control of Array.from(element.elements)) {
642
646
  control.classList.remove('is-valid', 'is-invalid');
647
+ control.removeAttribute('data-valid');
643
648
  }
644
649
  for (const help of all<HTMLElement>(
645
650
  '[data-field-help], [data-validator-help]',
@@ -75,6 +75,12 @@ interface TrackItem {
75
75
  key: string;
76
76
  }
77
77
 
78
+ interface NormalizedItemCacheEntry {
79
+ item: NormalizedSwiperDataItem;
80
+ keys: string[];
81
+ values: unknown[];
82
+ }
83
+
78
84
  export interface SwiperSlideContext {
79
85
  swiper: Swiper;
80
86
  item: NormalizedSwiperDataItem;
@@ -291,6 +297,8 @@ export function createSwiper(input: SwiperProps = {}): Swiper {
291
297
  }) as SwiperState;
292
298
  const events = createEventManager();
293
299
  const itemKeys = new WeakMap<object, string>();
300
+ const normalizedCache = new Map<string, NormalizedItemCacheEntry>();
301
+ const trackItemCache = new Map<string, TrackItem>();
294
302
  const imageCleanups = new Set<() => void>();
295
303
  let swiper!: Swiper;
296
304
  let wrapper: HTMLElement | null = null;
@@ -311,9 +319,31 @@ export function createSwiper(input: SwiperProps = {}): Swiper {
311
319
  if (!itemKeys.has(item)) itemKeys.set(item, randomId());
312
320
  return itemKeys.get(item) as string;
313
321
  };
322
+ const readItemEntries = (
323
+ item: SwiperDataItem
324
+ ): { keys: string[]; values: unknown[] } => {
325
+ const keys = Object.keys(item);
326
+ return {
327
+ keys,
328
+ values: keys.map((key) => item[key]),
329
+ };
330
+ };
331
+ const sameItemEntries = (
332
+ entry: NormalizedItemCacheEntry | undefined,
333
+ keys: string[],
334
+ values: unknown[]
335
+ ): boolean => {
336
+ if (!entry || entry.keys.length !== keys.length) return false;
337
+ for (let index = 0; index < keys.length; index += 1) {
338
+ if (entry.keys[index] !== keys[index]) return false;
339
+ if (!Object.is(entry.values[index], values[index])) return false;
340
+ }
341
+ return true;
342
+ };
314
343
  const normalizeData = (
315
344
  data: SwiperDataItem[]
316
345
  ): NormalizedSwiperDataItem[] => {
346
+ const activeKeys = new Set<string>();
317
347
  const items = data.map((item, index) => {
318
348
  validateParam(
319
349
  String(index),
@@ -321,13 +351,26 @@ export function createSwiper(input: SwiperProps = {}): Swiper {
321
351
  SWIPER_DATA_ITEM_RULE,
322
352
  'Swiper.state.data'
323
353
  );
324
- return {
354
+ const key = itemKey(item);
355
+ activeKeys.add(key);
356
+ const { keys, values } = readItemEntries(item);
357
+ const cached = normalizedCache.get(key);
358
+ if (cached && sameItemEntries(cached, keys, values)) {
359
+ cached.item.index = index;
360
+ return cached.item;
361
+ }
362
+ const normalized = {
325
363
  ...item,
326
364
  blank: item.blank !== false,
327
365
  index,
328
- key: itemKey(item),
366
+ key,
329
367
  };
368
+ normalizedCache.set(key, { item: normalized, keys, values });
369
+ return normalized;
330
370
  });
371
+ for (const key of normalizedCache.keys()) {
372
+ if (!activeKeys.has(key)) normalizedCache.delete(key);
373
+ }
331
374
  if (!items.some((item) => item.sort != null)) return items;
332
375
  return items.sort((a, b) => {
333
376
  if (a.sort == null && b.sort == null) return a.index - b.index;
@@ -336,31 +379,47 @@ export function createSwiper(input: SwiperProps = {}): Swiper {
336
379
  return a.sort - b.sort || a.index - b.index;
337
380
  });
338
381
  };
382
+ const trackItem = (
383
+ item: NormalizedSwiperDataItem,
384
+ index: number,
385
+ clone: TrackItem['clone']
386
+ ): TrackItem => {
387
+ const key =
388
+ clone === null
389
+ ? `real:${item.key}:${Boolean(item.url)}`
390
+ : `clone:${clone}:${item.key}:${Boolean(item.url)}`;
391
+ const cached = trackItemCache.get(key);
392
+ if (cached && cached.item === item && cached.clone === clone) {
393
+ cached.index = index;
394
+ return cached;
395
+ }
396
+ const next = { item, index, clone, key } satisfies TrackItem;
397
+ trackItemCache.set(key, next);
398
+ return next;
399
+ };
339
400
  const createTrackItems = (items: NormalizedSwiperDataItem[]): TrackItem[] => {
340
- const real = items.map((item, index) => ({
341
- item,
342
- index,
343
- clone: null,
344
- key: `real:${item.key}:${Boolean(item.url)}`,
345
- })) satisfies TrackItem[];
346
- if (!props.loop || items.length <= 1) return real;
401
+ const activeKeys = new Set<string>();
402
+ const real = items.map((item, index) => {
403
+ const current = trackItem(item, index, null);
404
+ activeKeys.add(current.key);
405
+ return current;
406
+ });
407
+ if (!props.loop || items.length <= 1) {
408
+ for (const key of trackItemCache.keys()) {
409
+ if (!activeKeys.has(key)) trackItemCache.delete(key);
410
+ }
411
+ return real;
412
+ }
347
413
  const first = items[0];
348
414
  const last = items[items.length - 1];
349
- return [
350
- {
351
- item: last,
352
- index: items.length - 1,
353
- clone: 'last',
354
- key: `clone:last:${last.key}:${Boolean(last.url)}`,
355
- },
356
- ...real,
357
- {
358
- item: first,
359
- index: 0,
360
- clone: 'first',
361
- key: `clone:first:${first.key}:${Boolean(first.url)}`,
362
- },
363
- ];
415
+ const lastClone = trackItem(last, items.length - 1, 'last');
416
+ const firstClone = trackItem(first, 0, 'first');
417
+ activeKeys.add(lastClone.key);
418
+ activeKeys.add(firstClone.key);
419
+ for (const key of trackItemCache.keys()) {
420
+ if (!activeKeys.has(key)) trackItemCache.delete(key);
421
+ }
422
+ return [lastClone, ...real, firstClone];
364
423
  };
365
424
  const realCount = (): number => normalizedItems().length;
366
425
  const trackCount = (): number => trackItems().length;
@@ -373,6 +432,10 @@ export function createSwiper(input: SwiperProps = {}): Swiper {
373
432
  if (index === trackCount() - 1) return 0;
374
433
  return index - 1;
375
434
  };
435
+ const toRealIndexFromTrackPosition = (index: number): number => {
436
+ if (!props.loop) return Math.max(0, index);
437
+ return toRealIndex(index);
438
+ };
376
439
  const trackIndexForRealIndex = (index: number): number => {
377
440
  const count = realCount();
378
441
  if (!count) return 0;
@@ -710,11 +773,22 @@ export function createSwiper(input: SwiperProps = {}): Swiper {
710
773
  });
711
774
  };
712
775
 
713
- const slideView = (itemAccessor: () => TrackItem): HTMLElement => {
776
+ const slideView = (
777
+ itemAccessor: () => TrackItem,
778
+ trackIndexAccessor: () => number
779
+ ): HTMLElement => {
780
+ const realIndex = (): number =>
781
+ toRealIndexFromTrackPosition(trackIndexAccessor());
714
782
  const slideContent = () => {
715
- const { item, index } = itemAccessor();
716
- const context = { swiper, item, index };
783
+ const { item } = itemAccessor();
717
784
  if (item.children != null) {
785
+ const context = {
786
+ swiper,
787
+ item,
788
+ get index() {
789
+ return realIndex();
790
+ },
791
+ } as SwiperSlideContext;
718
792
  return asRenderable(
719
793
  typeof item.children === 'function'
720
794
  ? item.children(context)
@@ -742,12 +816,12 @@ export function createSwiper(input: SwiperProps = {}): Swiper {
742
816
  };
743
817
  const commonProps = {
744
818
  className: props.className.slide,
745
- 'data-swiper-slide': () => String(itemAccessor().index),
819
+ 'data-swiper-slide': () => String(realIndex()),
746
820
  'data-swiper-index': () =>
747
- itemAccessor().clone ? null : String(itemAccessor().index),
821
+ itemAccessor().clone ? null : String(realIndex()),
748
822
  'data-clone': () => itemAccessor().clone,
749
823
  role: 'group',
750
- 'aria-label': () => `Slide ${itemAccessor().index + 1}`,
824
+ 'aria-label': () => `Slide ${realIndex() + 1}`,
751
825
  children: slideContent,
752
826
  };
753
827
  return itemAccessor().item.url
@@ -31,7 +31,7 @@
31
31
  margin: var(--content-heading-space) 0
32
32
  calc(var(--content-heading-space) * 0.75);
33
33
  font-weight: 600;
34
- line-height: 1.3;
34
+ /* line-height: 1.3; */
35
35
  color: var(--content-heading-fg);
36
36
  letter-spacing: -0.02em;
37
37
  }
@@ -46,7 +46,7 @@
46
46
  .j-content h2 {
47
47
  font-size: 1.625em;
48
48
  font-weight: 650;
49
- padding-bottom: 0.5em;
49
+ padding-bottom: 0.25em;
50
50
  border-bottom: 1px solid var(--content-border-subtle);
51
51
  }
52
52
 
@@ -458,9 +458,15 @@
458
458
  .hljs-comment,
459
459
  .hljs-code,
460
460
  .hljs-formula {
461
- color: var(--ui-gray-shade);
461
+ color: var(--ui-fg-soft);
462
462
  }
463
463
 
464
+ /* .dark .hljs-comment,
465
+ .dark .hljs-code,
466
+ .dark .hljs-formula {
467
+ color: var(--ui-gray-shade);
468
+ } */
469
+
464
470
  /* prettylights-syntax-entity-tag */
465
471
  .hljs-name,
466
472
  .hljs-quote,
@@ -581,6 +587,7 @@
581
587
  padding: 0.75em 1em;
582
588
  border-bottom: 1px solid var(--content-border-subtle);
583
589
  text-align: left;
590
+ vertical-align: top;
584
591
  }
585
592
 
586
593
  .j-content th {
package/src/css/form.css CHANGED
@@ -70,7 +70,7 @@
70
70
  margin: 0;
71
71
  }
72
72
 
73
- .j-form .form-field .field-control .help-block.is-invalid {
73
+ .j-form .form-field .field-control .help-block[data-valid='false'] {
74
74
  color: var(--state-danger);
75
75
  }
76
76
 
@@ -188,12 +188,12 @@
188
188
  }
189
189
 
190
190
  /* input validate */
191
- /* .j-input.is-valid,
192
- .j-select.is-valid,
193
- .j-textarea.is-valid,
194
- .j-input.is-invalid,
195
- .j-select.is-invalid,
196
- .j-textarea.is-invalid {
191
+ /* .j-input[data-valid='true'],
192
+ .j-select[data-valid='true'],
193
+ .j-textarea[data-valid='true'],
194
+ .j-input[data-valid='false'],
195
+ .j-select[data-valid='false'],
196
+ .j-textarea[data-valid='false'] {
197
197
  padding-right: calc(1.4285714286em + 0.875rem);
198
198
  background-repeat: no-repeat;
199
199
  background-position: right calc(0.3571428572em + 0.21875rem) center;
@@ -201,16 +201,16 @@
201
201
  calc(0.7142857143em + 0.4375rem);
202
202
  } */
203
203
 
204
- /* .j-input.is-valid,
205
- .j-select.is-valid,
206
- .j-textarea.is-valid {
204
+ /* .j-input[data-valid='true'],
205
+ .j-select[data-valid='true'],
206
+ .j-textarea[data-valid='true'] {
207
207
  border-color: var(--form-valid-border-color, var(--state-success));
208
208
  background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%232fb344' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3e%3cpolyline points='20 6 9 17 4 12'%3e%3c/polyline%3e%3c/svg%3e");
209
209
  } */
210
210
 
211
- .j-input.is-invalid,
212
- .j-select.is-invalid,
213
- .j-textarea.is-invalid {
211
+ .j-input[data-valid='false'],
212
+ .j-select[data-valid='false'],
213
+ .j-textarea[data-valid='false'] {
214
214
  border-color: var(--form-invalid-border-color, var(--state-danger));
215
215
  /* background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%23d63939' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3e%3cline x1='18' y1='6' x2='6' y2='18'%3e%3c/line%3e%3cline x1='6' y1='6' x2='18' y2='18'%3e%3c/line%3e%3c/svg%3e"); */
216
216
  }
@@ -861,14 +861,14 @@
861
861
  box-shadow: var(--input-focus-shadow, none);
862
862
  }
863
863
 
864
- /* .j-input.is-valid:focus-within,
865
- .j-select.is-valid:focus-within {
864
+ /* .j-input[data-valid='true']:focus-within,
865
+ .j-select[data-valid='true']:focus-within {
866
866
  border-color: transparent;
867
867
  outline-color: var(--form-valid-border-color, var(--state-success));
868
868
  } */
869
869
 
870
- .j-input.is-invalid:focus-within,
871
- .j-select.is-invalid:focus-within {
870
+ .j-input[data-valid='false']:focus-within,
871
+ .j-select[data-valid='false']:focus-within {
872
872
  border-color: transparent;
873
873
  outline-color: var(--form-invalid-border-color, var(--state-danger));
874
874
  }
package/src/css/menu.css CHANGED
@@ -363,6 +363,7 @@
363
363
  overflow-y: auto;
364
364
  scrollbar-width: thin;
365
365
  scrollbar-color: var(--ui-border) transparent;
366
+ height: 100%;
366
367
  }
367
368
 
368
369
  .j-menu[data-menu-type='mobile'] .menu {
@@ -416,13 +417,13 @@
416
417
  width: 100%;
417
418
  visibility: hidden;
418
419
  opacity: 0;
419
- transform: translateX(80%);
420
+ /* transform: translateX(80%); */
420
421
  transition:
421
- transform 0.3s ease-in-out,
422
- opacity 0.3s ease-in-out,
423
- visibility 0.3s ease-in-out;
422
+ /* transform 0.3s ease-in-out, */
423
+ /* visibility 0.3s ease-in-out, */ opacity 0.3s ease-in-out;
424
424
  max-width: 100%;
425
425
  background: var(--ui-bg);
426
+ padding-block: 0;
426
427
  }
427
428
 
428
429
  .j-menu[data-menu-type='mobile'] .menu .sub-menu .menu-item:hover a {
@@ -440,9 +441,9 @@
440
441
  > .sub-menu {
441
442
  visibility: visible;
442
443
  opacity: 1;
443
- transform: translateX(0);
444
- height: 100%;
444
+ /* transform: translateX(0); */
445
445
  z-index: var(--z-submenu);
446
+ height: 100%;
446
447
  }
447
448
 
448
449
  /* bottom menu */
package/src/css/table.css CHANGED
@@ -29,6 +29,7 @@
29
29
  table.j-table th:not([align]),
30
30
  table.j-table td:not([align]) {
31
31
  text-align: var(--table-cell-text-align);
32
+ vertical-align: var(--table-cell-vertical-align);
32
33
  }
33
34
 
34
35
  table.j-table a {
@@ -49,6 +49,7 @@
49
49
  --sub-menu-border-width: 0;
50
50
  --sub-menu-item-hover-outline-width: 0;
51
51
  --menu-link-gap: var(--s2);
52
+ --sub-menu-box-shadow: none;
52
53
  }
53
54
 
54
55
  .j-menu[data-menu-type='bottom'] {
@@ -10,6 +10,7 @@
10
10
  --table-border-color: var(--ui-border);
11
11
  --table-cell-padding: 0.5em 0.75em;
12
12
  --table-cell-text-align: left;
13
+ --table-cell-vertical-align: top;
13
14
  --table-link-fg: var(--tone-solid);
14
15
  --table-striped-bg: var(--tone-soft);
15
16
  --table-hover-bg: var(--tone-soft);
@@ -15,11 +15,11 @@
15
15
  --z-badge: 2;
16
16
  --z-toolbar: 2;
17
17
  --z-submenu: 5;
18
+ --z-overlay: 9500;
19
+ --z-offcanvas: 9600;
20
+ --z-popup: 9700;
21
+ --z-toast: 9800;
18
22
  --z-drop: 9900;
19
- --z-overlay: 9950;
20
- --z-offcanvas: 9960;
21
- --z-popup: 9970;
22
- --z-toast: 9980;
23
23
 
24
24
  /* --- Spacing --- */
25
25
  --space: 0.25rem;
@@ -317,7 +317,7 @@
317
317
  --ui-disabled-fg: #64748b;
318
318
  --ui-overlay: rgba(2, 6, 23, 0.7);
319
319
  --ui-overlay-muted: rgba(2, 6, 23, 0.45);
320
- --ui-popup-bg: rgba(17, 24, 39, 0.82);
320
+ --ui-popup-bg: rgba(17, 24, 39, 0.45);
321
321
  --ui-input-color: #f9fafb;
322
322
  --ui-input-border: rgba(148, 163, 184, 0.2);
323
323
  --ui-input-focus-border: #a78bfa;
@@ -1162,13 +1162,13 @@
1162
1162
  }
1163
1163
 
1164
1164
  .j-radius-round {
1165
- --radius-2xs: 9999px;
1166
- --radius-xs: 9999px;
1167
- --radius-sm: 9999px;
1168
- --radius-md: 9999px;
1169
- --radius-lg: 9999px;
1170
- --radius-xl: 9999px;
1171
- --radius-2xl: 9999px;
1165
+ --radius-2xs: 0.375rem;
1166
+ --radius-xs: 0.5rem;
1167
+ --radius-sm: 0.75rem;
1168
+ --radius-md: 1rem;
1169
+ --radius-lg: 999px;
1170
+ --radius-xl: 999px;
1171
+ --radius-2xl: 999px;
1172
1172
  }
1173
1173
 
1174
1174
  /* ============================================
@@ -1,7 +1,9 @@
1
1
  import { jsx } from 'vanilla-signal';
2
2
 
3
+ import { joinClasses } from '../utilities/class-name.ts';
3
4
  import { type DOMReference, all, resolveElement } from '../utilities/dom.ts';
4
5
  import { createEventManager } from '../utilities/events.ts';
6
+ import { isPlainObject } from '../utilities/object.ts';
5
7
  import {
6
8
  type ResolveSchema,
7
9
  resolveProps,
@@ -45,13 +47,21 @@ interface ValidatorProps extends Record<string, unknown> {
45
47
  rules?: Record<string, ValidatorRule>;
46
48
  messages?: ValidatorMessageMap;
47
49
  vanilla?: boolean;
50
+ className?: ValidatorClassNameConfig;
48
51
  onSubmit?: ((validator: ValidatorInstance) => void) | null;
49
52
  }
50
53
 
54
+ export interface ValidatorClassNames {
55
+ help: string;
56
+ }
57
+
58
+ export type ValidatorClassNameConfig = Partial<ValidatorClassNames>;
59
+
51
60
  interface ResolvedValidatorProps extends Record<string, unknown> {
52
61
  rules: Record<string, ValidatorRule>;
53
62
  messages: ValidatorMessageMap;
54
63
  vanilla: boolean;
64
+ className: ValidatorClassNames;
55
65
  onSubmit: ((validator: ValidatorInstance) => void) | null;
56
66
  }
57
67
 
@@ -75,19 +85,36 @@ export interface ValidatorInstance {
75
85
  destroy(): void;
76
86
  }
77
87
 
88
+ const DEFAULT_CLASS_NAMES: ValidatorClassNames = {
89
+ help: 'help-block',
90
+ };
91
+
78
92
  const VALIDATOR_PROPS_SCHEMA = {
79
93
  rules: { default: {}, type: 'object' },
80
94
  messages: { default: {}, type: 'object' },
81
95
  vanilla: { default: false, type: 'boolean' },
96
+ className: {
97
+ default: DEFAULT_CLASS_NAMES,
98
+ type: 'object',
99
+ normalize: (value: unknown) => resolveClassNames(value),
100
+ },
82
101
  onSubmit: { default: null, types: ['function', 'null'] },
83
102
  } satisfies ResolveSchema<ValidatorProps>;
84
103
 
104
+ function resolveClassNames(value: unknown): ValidatorClassNames {
105
+ return {
106
+ ...DEFAULT_CLASS_NAMES,
107
+ ...(isPlainObject(value) ? (value as ValidatorClassNameConfig) : {}),
108
+ } as ValidatorClassNames;
109
+ }
110
+
85
111
  function normalizeProps(input: ValidatorProps): ResolvedValidatorProps {
86
112
  const props = resolveProps(input, VALIDATOR_PROPS_SCHEMA, 'Validator.props');
87
113
  return {
88
114
  rules: props.rules as Record<string, ValidatorRule>,
89
115
  messages: props.messages as ValidatorMessageMap,
90
116
  vanilla: props.vanilla as boolean,
117
+ className: props.className as ValidatorClassNames,
91
118
  onSubmit: props.onSubmit as ResolvedValidatorProps['onSubmit'],
92
119
  };
93
120
  }
@@ -353,10 +380,7 @@ function showError(
353
380
  rule: string,
354
381
  customMessage = ''
355
382
  ): void {
356
- if (!(element instanceof HTMLInputElement) || element.type !== 'checkbox') {
357
- element.classList.remove('is-valid');
358
- element.classList.add('is-invalid');
359
- }
383
+ element.dataset.valid = 'false';
360
384
 
361
385
  const error = customMessage || props?.messages[name]?.[rule] || '';
362
386
  if (!error) return;
@@ -372,11 +396,13 @@ function showError(
372
396
  ) || null;
373
397
  if (!help) {
374
398
  help = jsx('div', {
375
- className: 'help-block is-invalid',
399
+ 'data-valid': 'false',
376
400
  'data-validator-help': name,
377
401
  }) as HTMLElement;
378
402
  formControl.appendChild(help);
379
403
  }
404
+ help.className = joinClasses(props?.className.help);
405
+ help.dataset.valid = 'false';
380
406
  help.textContent = error;
381
407
  }
382
408
 
@@ -388,9 +414,7 @@ function showSuccess(element: ValidatorElement): void {
388
414
  }
389
415
  }
390
416
 
391
- if (!(element instanceof HTMLInputElement) || element.type !== 'checkbox') {
392
- element.classList.remove('is-invalid');
393
- }
417
+ element.removeAttribute('data-valid');
394
418
  }
395
419
 
396
420
  export function createValidator(
@@ -543,7 +567,7 @@ export function createValidator(
543
567
  if (native) root.reset();
544
568
  for (const control of Array.from(root.elements)) {
545
569
  if (isValidatorElement(control)) {
546
- control.classList.remove('is-valid', 'is-invalid');
570
+ control.removeAttribute('data-valid');
547
571
  }
548
572
  }
549
573
  for (const help of all<HTMLElement>('[data-validator-help]', root)) {