@transferwise/components 0.0.0-experimental-ea80215 → 0.0.0-experimental-e4e09f5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/build/dateLookup/dateTrigger/DateTrigger.js +8 -4
  2. package/build/dateLookup/dateTrigger/DateTrigger.js.map +1 -1
  3. package/build/dateLookup/dateTrigger/DateTrigger.mjs +8 -4
  4. package/build/dateLookup/dateTrigger/DateTrigger.mjs.map +1 -1
  5. package/build/field/Field.js +9 -2
  6. package/build/field/Field.js.map +1 -1
  7. package/build/field/Field.mjs +9 -2
  8. package/build/field/Field.mjs.map +1 -1
  9. package/build/i18n/en.json +3 -1
  10. package/build/i18n/en.json.js +3 -1
  11. package/build/i18n/en.json.js.map +1 -1
  12. package/build/i18n/en.json.mjs +3 -1
  13. package/build/i18n/en.json.mjs.map +1 -1
  14. package/build/inputs/SelectInput.js +36 -100
  15. package/build/inputs/SelectInput.js.map +1 -1
  16. package/build/inputs/SelectInput.mjs +38 -102
  17. package/build/inputs/SelectInput.mjs.map +1 -1
  18. package/build/label/Label.js +29 -1
  19. package/build/label/Label.js.map +1 -1
  20. package/build/label/Label.messages.js +15 -0
  21. package/build/label/Label.messages.js.map +1 -0
  22. package/build/label/Label.messages.mjs +13 -0
  23. package/build/label/Label.messages.mjs.map +1 -0
  24. package/build/label/Label.mjs +30 -2
  25. package/build/label/Label.mjs.map +1 -1
  26. package/build/main.css +0 -18
  27. package/build/styles/dateLookup/dateTrigger/DateTrigger.css +0 -8
  28. package/build/styles/inputs/SelectInput.css +0 -10
  29. package/build/styles/main.css +0 -18
  30. package/build/types/dateLookup/dateTrigger/DateTrigger.d.ts.map +1 -1
  31. package/build/types/field/Field.d.ts +4 -2
  32. package/build/types/field/Field.d.ts.map +1 -1
  33. package/build/types/index.d.ts +1 -1
  34. package/build/types/index.d.ts.map +1 -1
  35. package/build/types/inputs/SelectInput.d.ts.map +1 -1
  36. package/build/types/label/Label.d.ts +10 -1
  37. package/build/types/label/Label.d.ts.map +1 -1
  38. package/build/types/label/Label.messages.d.ts +12 -0
  39. package/build/types/label/Label.messages.d.ts.map +1 -0
  40. package/build/types/label/index.d.ts +3 -0
  41. package/build/types/label/index.d.ts.map +1 -0
  42. package/package.json +5 -6
  43. package/src/dateInput/DateInput.tests.story.tsx +8 -32
  44. package/src/dateLookup/DateLookup.rtl.spec.tsx +1 -1
  45. package/src/dateLookup/dateTrigger/DateTrigger.css +0 -8
  46. package/src/dateLookup/dateTrigger/DateTrigger.less +0 -8
  47. package/src/dateLookup/dateTrigger/DateTrigger.spec.js +1 -1
  48. package/src/dateLookup/dateTrigger/DateTrigger.tsx +9 -4
  49. package/src/field/Field.spec.tsx +3 -3
  50. package/src/field/Field.story.tsx +81 -3
  51. package/src/field/Field.tsx +10 -4
  52. package/src/i18n/en.json +3 -1
  53. package/src/index.ts +1 -1
  54. package/src/inlineAlert/InlineAlert.story.tsx +8 -21
  55. package/src/inputs/InputGroup.spec.tsx +1 -1
  56. package/src/inputs/SearchInput.spec.tsx +1 -1
  57. package/src/inputs/SelectInput.css +0 -10
  58. package/src/inputs/SelectInput.less +0 -12
  59. package/src/inputs/SelectInput.spec.tsx +1 -1
  60. package/src/inputs/SelectInput.story.tsx +0 -20
  61. package/src/inputs/SelectInput.tsx +46 -139
  62. package/src/label/Label.messages.tsx +12 -0
  63. package/src/label/Label.story.tsx +30 -21
  64. package/src/label/Label.tsx +43 -2
  65. package/src/label/index.ts +2 -0
  66. package/src/main.css +0 -18
  67. package/src/radioGroup/RadioGroup.rtl.spec.tsx +1 -1
  68. package/src/select/Select.rtl.spec.tsx +1 -1
  69. package/src/switch/Switch.spec.tsx +1 -1
  70. package/src/field/Field.tests.story.tsx +0 -33
@@ -322,26 +322,6 @@ export const Advanced: Story<Month> = {
322
322
  },
323
323
  };
324
324
 
325
- export const ManyItems: Story<string, true> = {
326
- args: {
327
- multiple: true,
328
- items: Array.from({ length: 1000 }, (_, index) => ({
329
- type: 'option',
330
- value: String(index + 1),
331
- })),
332
- renderValue: (value, withinTrigger) =>
333
- withinTrigger ? (
334
- value
335
- ) : (
336
- <SelectInputOptionContent
337
- title={value}
338
- description={Number(value) % 10 === 0 ? 'Divisible by 10' : undefined}
339
- />
340
- ),
341
- filterable: true,
342
- },
343
- };
344
-
345
325
  export const WithinDrawer: Story<Currency> = {
346
326
  args: CurrenciesArgs,
347
327
  decorators: [
@@ -6,7 +6,6 @@ import {
6
6
  createContext,
7
7
  forwardRef,
8
8
  useContext,
9
- useDeferredValue,
10
9
  useEffect,
11
10
  useId,
12
11
  useMemo,
@@ -14,7 +13,6 @@ import {
14
13
  useState,
15
14
  } from 'react';
16
15
  import { useIntl } from 'react-intl';
17
- import { Virtualizer } from 'virtua';
18
16
 
19
17
  import { useEffectEvent } from '../common/hooks/useEffectEvent';
20
18
  import { useScreenSize } from '../common/hooks/useScreenSize';
@@ -31,8 +29,6 @@ import { InputGroup } from './InputGroup';
31
29
  import { SearchInput } from './SearchInput';
32
30
  import messages from './SelectInput.messages';
33
31
 
34
- const MAX_ITEMS_WITHOUT_VIRTUALIZATION = 50;
35
-
36
32
  function searchableString(value: string) {
37
33
  return value.trim().replace(/\s+/gu, ' ').normalize('NFKC').toLowerCase();
38
34
  }
@@ -44,13 +40,22 @@ function inferSearchableStrings(value: unknown) {
44
40
 
45
41
  if (typeof value === 'object' && value != null) {
46
42
  return Object.values(value)
47
- .filter((innerValue) => typeof innerValue === 'string')
43
+ .filter((innerValue): innerValue is string => typeof innerValue === 'string')
48
44
  .map((innerValue) => searchableString(innerValue));
49
45
  }
50
46
 
51
47
  return [];
52
48
  }
53
49
 
50
+ const SelectInputTriggerButtonPropsContext = createContext<{
51
+ ref?: React.ForwardedRef<HTMLButtonElement | null>;
52
+ id?: string;
53
+ onClick?: (event: React.MouseEvent) => void;
54
+ onKeyDown?: (event: React.KeyboardEvent) => void;
55
+ [key: string]: unknown;
56
+ }>({});
57
+ const SelectInputOptionContentWithinTriggerContext = createContext(false);
58
+
54
59
  export interface SelectInputOptionItem<T = string> {
55
60
  type: 'option';
56
61
  value: T;
@@ -84,11 +89,6 @@ function dedupeSelectInputOptionItem<T>(
84
89
  return { ...item, value: undefined };
85
90
  }
86
91
 
87
- /**
88
- * Sets the `value` of duplicate option items to `undefined`, hiding them when
89
- * rendered. Indexes are kept intact within groups to preserve the active item
90
- * between filter changes when possible.
91
- */
92
92
  function dedupeSelectInputItems<T>(
93
93
  items: readonly SelectInputItem<T>[],
94
94
  ): SelectInputItem<T | undefined>[] {
@@ -112,23 +112,20 @@ function dedupeSelectInputItems<T>(
112
112
  });
113
113
  }
114
114
 
115
- function selectInputOptionItemIncludesNeedle<T>(item: SelectInputOptionItem<T>, needle: string) {
115
+ function filterSelectInputOptionItem<T>(item: SelectInputOptionItem<T>, needle: string) {
116
116
  return inferSearchableStrings(item.filterMatchers ?? item.value).some((haystack) =>
117
117
  haystack.includes(needle),
118
118
  );
119
119
  }
120
120
 
121
- function filterSelectInputItems<T>(
122
- items: readonly SelectInputItem<T>[],
123
- predicate: (item: SelectInputOptionItem<T>) => boolean,
124
- ) {
121
+ function filterSelectInputItems<T>(items: readonly SelectInputItem<T>[], needle: string) {
125
122
  return items.filter((item) => {
126
123
  switch (item.type) {
127
124
  case 'option': {
128
- return predicate(item);
125
+ return filterSelectInputOptionItem(item, needle);
129
126
  }
130
127
  case 'group': {
131
- return item.options.some((option) => predicate(option));
128
+ return item.options.some((option) => filterSelectInputOptionItem(option, needle));
132
129
  }
133
130
  default:
134
131
  }
@@ -274,15 +271,12 @@ export function SelectInput<T = string, M extends boolean = false>({
274
271
  }, [handleClose, open]);
275
272
 
276
273
  const [filterQuery, _setFilterQuery] = useState('');
277
- const deferredFilterQuery = useDeferredValue(filterQuery);
278
274
  const setFilterQuery = useEffectEvent((query: string) => {
279
275
  _setFilterQuery(query);
280
- if (query !== filterQuery) {
281
- onFilterChange({
282
- query,
283
- queryNormalized: query ? searchableString(query) : null,
284
- });
285
- }
276
+ onFilterChange({
277
+ query,
278
+ queryNormalized: query ? searchableString(query) : null,
279
+ });
286
280
  });
287
281
 
288
282
  const triggerRef = useRef<HTMLButtonElement | null>(null);
@@ -300,7 +294,9 @@ export function SelectInput<T = string, M extends boolean = false>({
300
294
  multiple={multiple}
301
295
  defaultValue={defaultValue}
302
296
  value={controlledValue}
303
- by={compareValues}
297
+ // TODO: Remove assertion when upgrading TypeScript to v5
298
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
299
+ by={compareValues as any}
304
300
  disabled={disabled}
305
301
  onChange={
306
302
  ((value) => {
@@ -353,8 +349,8 @@ export function SelectInput<T = string, M extends boolean = false>({
353
349
  content: !placeholderShown ? (
354
350
  <SelectInputOptionContentWithinTriggerContext.Provider value>
355
351
  {multiple && Array.isArray(value)
356
- ? (value as readonly NonNullable<T>[])
357
- .map((option) => renderValue(option, true))
352
+ ? value
353
+ .map((option: NonNullable<T>) => renderValue(option, true))
358
354
  .filter((node) => node != null)
359
355
  .join(', ')
360
356
  : renderValue(value as NonNullable<T>, true)}
@@ -383,7 +379,9 @@ export function SelectInput<T = string, M extends boolean = false>({
383
379
  setOpen(false);
384
380
  }}
385
381
  onCloseEnd={() => {
386
- setFilterQuery('');
382
+ if (filterQuery !== '') {
383
+ setFilterQuery('');
384
+ }
387
385
  }}
388
386
  >
389
387
  <SelectInputOptions
@@ -394,7 +392,7 @@ export function SelectInput<T = string, M extends boolean = false>({
394
392
  filterPlaceholder={filterPlaceholder}
395
393
  searchInputRef={searchInputRef}
396
394
  listboxRef={listboxRef}
397
- filterQuery={deferredFilterQuery}
395
+ filterQuery={filterQuery}
398
396
  onFilterChange={setFilterQuery}
399
397
  />
400
398
  </OptionsOverlay>
@@ -404,14 +402,6 @@ export function SelectInput<T = string, M extends boolean = false>({
404
402
  );
405
403
  }
406
404
 
407
- const SelectInputTriggerButtonPropsContext = createContext<{
408
- ref?: React.ForwardedRef<HTMLButtonElement | null>;
409
- id?: string;
410
- onClick?: (event: React.MouseEvent) => void;
411
- onKeyDown?: (event: React.KeyboardEvent) => void;
412
- [key: string]: unknown;
413
- }>({});
414
-
415
405
  type SelectInputTriggerButtonElementType = 'button' | React.ComponentType;
416
406
 
417
407
  export type SelectInputTriggerButtonProps<
@@ -516,48 +506,7 @@ function SelectInputOptions<T = string>({
516
506
  }
517
507
  return undefined;
518
508
  }, [filterQuery, filterable]);
519
- useEffect(() => {
520
- if (needle) {
521
- // Ensure having an active option while filtering
522
- requestAnimationFrame(() => {
523
- if (
524
- controllerRef.current != null &&
525
- !controllerRef.current.hasAttribute('aria-activedescendant')
526
- ) {
527
- // Activate first option via synthetic key press
528
- controllerRef.current.dispatchEvent(
529
- new KeyboardEvent('keydown', { key: 'Home', bubbles: true }),
530
- );
531
- }
532
- });
533
- }
534
- }, [controllerRef, needle]);
535
-
536
- const filteredItems: readonly SelectInputItem<NonNullable<T> | undefined>[] =
537
- needle != null
538
- ? filterSelectInputItems(dedupeSelectInputItems(items), (item) =>
539
- selectInputOptionItemIncludesNeedle(item, needle),
540
- )
541
- : items;
542
- const resultsEmpty = needle != null && filteredItems.length === 0;
543
-
544
- const virtualized = filteredItems.length > MAX_ITEMS_WITHOUT_VIRTUALIZATION;
545
-
546
- // Items shown once shall be kept mounted until the needle changes, otherwise
547
- // the scroll position may jump around inadvertently. Pattern adopted from:
548
- // https://inokawa.github.io/virtua/?path=/story/advanced-keep-offscreen-items--append-only
549
- const [mountedIndexes, setMountedIndexes] = useState<number[]>([]);
550
- useEffect(() => {
551
- // Ensure the 'End' key works as intended by keeping the last item mounted
552
- setMountedIndexes((prevMountedIndexes) => {
553
- const indexes = new Set(prevMountedIndexes);
554
- indexes.add(filteredItems.length - 1);
555
- return [...indexes]; // Sorting is redundant by nature here
556
- });
557
- }, [
558
- needle, // Needed as `filteredItems.length` may be equal between two updates
559
- filteredItems.length,
560
- ]);
509
+ const resultsEmpty = needle != null && filterSelectInputItems(items, needle).length === 0;
561
510
 
562
511
  const listboxContainerRef = useRef<HTMLDivElement>(null);
563
512
  useEffect(() => {
@@ -573,19 +522,6 @@ function SelectInputOptions<T = string>({
573
522
  const statusId = useId();
574
523
  const listboxId = useId();
575
524
 
576
- const getItemNode = (index: number) => {
577
- const item = filteredItems[index];
578
- return (
579
- <SelectInputItemView
580
- // eslint-disable-next-line react/no-array-index-key
581
- key={index}
582
- item={item}
583
- renderValue={renderValue}
584
- needle={needle}
585
- />
586
- );
587
- };
588
-
589
525
  return (
590
526
  <ListboxBase.Options
591
527
  as={SelectInputOptionsContainer}
@@ -597,6 +533,12 @@ function SelectInputOptions<T = string>({
597
533
  controllerRef.current.setAttribute('aria-activedescendant', value);
598
534
  } else {
599
535
  controllerRef.current.removeAttribute('aria-activedescendant');
536
+ if (filterQuery) {
537
+ // Ensure having an active option while filtering
538
+ controllerRef.current.dispatchEvent(
539
+ new KeyboardEvent('keydown', { key: 'Home', bubbles: true }),
540
+ );
541
+ }
600
542
  }
601
543
  }
602
544
  }}
@@ -607,7 +549,7 @@ function SelectInputOptions<T = string>({
607
549
  ref={searchInputRef}
608
550
  shape="rectangle"
609
551
  placeholder={filterPlaceholder}
610
- defaultValue={filterQuery}
552
+ value={filterQuery}
611
553
  aria-controls={listboxId}
612
554
  aria-describedby={showStatus ? statusId : undefined}
613
555
  onKeyDown={(event) => {
@@ -618,9 +560,6 @@ function SelectInputOptions<T = string>({
618
560
  }
619
561
  }}
620
562
  onChange={(event) => {
621
- // Free up resources and ensure not to go out of bounds when the
622
- // resulting item count is less than before
623
- setMountedIndexes([]);
624
563
  onFilterChange(event.currentTarget.value);
625
564
  }}
626
565
  />
@@ -632,9 +571,7 @@ function SelectInputOptions<T = string>({
632
571
  tabIndex={-1}
633
572
  className={clsx(
634
573
  'np-select-input-listbox-container',
635
- virtualized && 'np-select-input-listbox-container--virtualized',
636
- needle == null && // Groups aren't shown when filtering
637
- items.some((item) => item.type === 'group') &&
574
+ items.some((item) => item.type === 'group') &&
638
575
  'np-select-input-listbox-container--has-group',
639
576
  )}
640
577
  >
@@ -653,33 +590,15 @@ function SelectInputOptions<T = string>({
653
590
  tabIndex={0}
654
591
  className="np-select-input-listbox"
655
592
  >
656
- {!virtualized ? (
657
- filteredItems.map((_, index) => getItemNode(index))
658
- ) : (
659
- <Virtualizer
660
- key={needle}
661
- count={filteredItems.length}
662
- keepMounted={mountedIndexes}
663
- scrollRef={listboxRef} // `VList` doesn't expose this
664
- onRangeChange={(startIndex, endIndex) => {
665
- setMountedIndexes((prevMountedIndexes) => {
666
- const indexes = new Set(prevMountedIndexes);
667
- for (let index = startIndex; index <= endIndex; index += 1) {
668
- indexes.add(index);
669
- }
670
- return [...indexes].sort((a, b) => a - b);
671
- });
672
- }}
673
- >
674
- {(index) => (
675
- <SelectInputItemsCountContext.Provider value={filteredItems.length}>
676
- <SelectInputItemPositionContext.Provider value={index + 1}>
677
- {getItemNode(index)}
678
- </SelectInputItemPositionContext.Provider>
679
- </SelectInputItemsCountContext.Provider>
680
- )}
681
- </Virtualizer>
682
- )}
593
+ {(needle != null ? dedupeSelectInputItems(items) : items).map((item, index) => (
594
+ <SelectInputItemView
595
+ // eslint-disable-next-line react/no-array-index-key
596
+ key={index}
597
+ item={item}
598
+ renderValue={renderValue}
599
+ needle={needle}
600
+ />
601
+ ))}
683
602
  </div>
684
603
 
685
604
  {renderFooter != null ? (
@@ -720,10 +639,7 @@ function SelectInputItemView<T = string>({
720
639
  }: SelectInputItemViewProps<T>) {
721
640
  switch (item.type) {
722
641
  case 'option': {
723
- if (
724
- item.value != null &&
725
- (needle == null || selectInputOptionItemIncludesNeedle(item, needle))
726
- ) {
642
+ if (item.value != null && (needle == null || filterSelectInputOptionItem(item, needle))) {
727
643
  return (
728
644
  <SelectInputOption value={item.value} disabled={item.disabled}>
729
645
  {renderValue(item.value, false)}
@@ -785,9 +701,6 @@ function SelectInputGroupItemView<T = string>({
785
701
  );
786
702
  }
787
703
 
788
- const SelectInputItemsCountContext = createContext<number | undefined>(undefined);
789
- const SelectInputItemPositionContext = createContext<number | undefined>(undefined);
790
-
791
704
  interface SelectInputOptionProps<T = string> {
792
705
  value: T;
793
706
  disabled?: boolean;
@@ -795,14 +708,10 @@ interface SelectInputOptionProps<T = string> {
795
708
  }
796
709
 
797
710
  function SelectInputOption<T = string>({ value, disabled, children }: SelectInputOptionProps<T>) {
798
- const itemsCount = useContext(SelectInputItemsCountContext);
799
- const itemPosition = useContext(SelectInputItemPositionContext);
800
711
  return (
801
712
  <ListboxBase.Option
802
713
  as="div"
803
714
  value={value}
804
- aria-setsize={itemsCount}
805
- aria-posinset={itemPosition}
806
715
  disabled={disabled}
807
716
  className={({ active, disabled: uiDisabled }) =>
808
717
  clsx(
@@ -828,8 +737,6 @@ function SelectInputOption<T = string>({ value, disabled, children }: SelectInpu
828
737
  );
829
738
  }
830
739
 
831
- const SelectInputOptionContentWithinTriggerContext = createContext(false);
832
-
833
740
  export interface SelectInputOptionContentProps {
834
741
  title: string;
835
742
  note?: string;
@@ -0,0 +1,12 @@
1
+ import { defineMessages } from 'react-intl';
2
+
3
+ export default defineMessages({
4
+ optionalLabel: {
5
+ id: 'neptune.Label.optional',
6
+ defaultMessage: '(Optional)',
7
+ },
8
+ optionalAriaLabel: {
9
+ id: 'neptune.aria.Label.optional',
10
+ defaultMessage: 'This field is optional',
11
+ },
12
+ });
@@ -1,37 +1,46 @@
1
1
  import { useState } from 'react';
2
2
 
3
- import Info from '../info/Info';
4
3
  import { Input } from '../inputs/Input';
5
4
  import { Label } from './Label';
5
+ import InlineAlert from '../inlineAlert/InlineAlert';
6
+ import { lorem10 } from '../test-utils';
6
7
 
7
8
  export default {
8
9
  component: Label,
9
10
  title: 'Label',
11
+ tags: ['autodocs'],
10
12
  };
11
13
 
12
14
  export const Basic = () => {
13
15
  const [value, setValue] = useState<string | undefined>('This is some text');
14
16
  return (
15
- <Label>
16
- Phone number
17
- <Input value={value} id="input" onChange={({ target }) => setValue(target.value)} />
18
- </Label>
19
- );
20
- };
17
+ <>
18
+ <Label className="m-b-2">
19
+ Phone number
20
+ <Input value={value} id="input" onChange={({ target }) => setValue(target.value)} />
21
+ </Label>
21
22
 
22
- export const WithInfo = () => {
23
- const [value, setValue] = useState<string | undefined>('This is some text');
24
- return (
25
- <Label>
26
- <span className="d-flex">
27
- Phone number{' '}
28
- <Info
29
- content="This is some help in popover"
30
- aria-label="The aria label"
31
- className="m-l-1"
32
- />
33
- </span>
34
- <Input value={value} id="input" onChange={({ target }) => setValue(target.value)} />
35
- </Label>
23
+ <Label className="m-b-2">
24
+ <Label.Optional>Phone number</Label.Optional>
25
+ <Input value={value} id="input" onChange={({ target }) => setValue(target.value)} />
26
+ </Label>
27
+
28
+ <Label className="m-b-2">
29
+ <Label.Optional>Phone number</Label.Optional>
30
+ <Label.Description>This an field Description</Label.Description>
31
+ <Input value={value} id="input" onChange={({ target }) => setValue(target.value)} />
32
+ </Label>
33
+
34
+ <Label htmlFor="phone-number-1">
35
+ <Label.Optional>Phone number</Label.Optional>
36
+ <Label.Description>This an field Description</Label.Description>
37
+ </Label>
38
+ <Input
39
+ id="phone-number-1"
40
+ className="m-b-2"
41
+ value={value}
42
+ onChange={({ target }) => setValue(target.value)}
43
+ />
44
+ </>
36
45
  );
37
46
  };
@@ -1,4 +1,9 @@
1
1
  import { clsx } from 'clsx';
2
+ import messages from './Label.messages';
3
+ import { useIntl } from 'react-intl';
4
+ import Body from '../body';
5
+ import { CommonProps } from '../common';
6
+ import { PropsWithChildren } from 'react';
2
7
 
3
8
  export type LabelProps = {
4
9
  id?: string;
@@ -7,14 +12,50 @@ export type LabelProps = {
7
12
  children?: React.ReactNode;
8
13
  };
9
14
 
10
- export const Label = ({ id, htmlFor, className, children }: LabelProps) => {
15
+ const Label = ({ id, htmlFor, className, children }: LabelProps) => {
11
16
  return (
12
17
  <label
13
18
  id={id}
14
19
  htmlFor={htmlFor}
15
- className={clsx('control-label d-flex flex-column gap-y-1 m-b-0', className)}
20
+ className={clsx(
21
+ 'd-flex',
22
+ 'flex-column',
23
+ 'gap-y-4',
24
+ 'm-b-0',
25
+ 'np-text-body-default-bold',
26
+ 'text-primary',
27
+ className,
28
+ )}
16
29
  >
17
30
  {children}
18
31
  </label>
19
32
  );
20
33
  };
34
+
35
+ export type LabelOptionalProps = PropsWithChildren<CommonProps>;
36
+
37
+ const Optional = ({ children, className }: LabelOptionalProps) => {
38
+ const { formatMessage } = useIntl();
39
+ return (
40
+ <div>
41
+ {children}
42
+ <Body
43
+ as="span"
44
+ aria-label={formatMessage(messages.optionalAriaLabel)}
45
+ className={clsx('text-secondary', 'm-l-1', className)}
46
+ >
47
+ {formatMessage(messages.optionalLabel)}
48
+ </Body>
49
+ </div>
50
+ );
51
+ };
52
+
53
+ export type LabelDescriptionProps = PropsWithChildren<CommonProps>;
54
+
55
+ const Description = ({ children, className }: LabelDescriptionProps) =>
56
+ children ? <Body className={clsx('text-secondary', className)}>{children}</Body> : null;
57
+
58
+ Label.Optional = Optional;
59
+ Label.Description = Description;
60
+
61
+ export { Label };
@@ -0,0 +1,2 @@
1
+ export { Label } from './Label';
2
+ export type { LabelProps, LabelOptionalProps, LabelDescriptionProps } from './Label';
package/src/main.css CHANGED
@@ -1719,18 +1719,10 @@ button.np-option {
1719
1719
  white-space: nowrap;
1720
1720
  width: 100%;
1721
1721
  }
1722
- .np-date-trigger .control-label {
1723
- font-weight: 400;
1724
- font-weight: var(--font-weight-regular);
1725
- }
1726
1722
  .np-theme-personal .np-date-trigger {
1727
1723
  padding-left: 16px;
1728
1724
  padding-left: var(--size-16);
1729
1725
  }
1730
- .np-theme-personal .np-date-trigger .control-label + span {
1731
- font-weight: 400;
1732
- font-weight: var(--font-weight-regular);
1733
- }
1734
1726
  .clear-btn {
1735
1727
  transition: color 0.15s ease-in-out;
1736
1728
  color: #c9cbce;
@@ -2655,10 +2647,6 @@ html:not([dir="rtl"]) .np-flow-navigation--sm .np-flow-navigation__stepper {
2655
2647
  height: auto;
2656
2648
  }
2657
2649
  }
2658
- .np-select-input-listbox-container--virtualized {
2659
- /* The wrapping element shrinks this as needed */
2660
- height: 100vh;
2661
- }
2662
2650
  .np-select-input-listbox-container--has-group {
2663
2651
  scroll-padding-top: 32px;
2664
2652
  scroll-padding-top: var(--size-32);
@@ -2677,12 +2665,6 @@ html:not([dir="rtl"]) .np-flow-navigation--sm .np-flow-navigation__stepper {
2677
2665
  outline: var(--ring-outline-color) solid var(--ring-outline-width);
2678
2666
  outline-offset: var(--ring-outline-offset);
2679
2667
  }
2680
- .np-select-input-listbox-container--virtualized .np-select-input-listbox {
2681
- /* Adopted from `VList` in virtua: https://github.com/inokawa/virtua/blob/7f6ed5b37df6b480d4ff350f3960067c5b3519d2/src/react/VList.tsx#L113-L116 */
2682
- overflow-y: auto;
2683
- contain: strict;
2684
- height: 100%;
2685
- }
2686
2668
  .np-select-input-separator-item {
2687
2669
  margin: 8px;
2688
2670
  margin: var(--size-8);
@@ -1,4 +1,4 @@
1
- import { render, screen } from '@testing-library/react';
1
+ import { render, screen } from '../test-utils';
2
2
 
3
3
  import RadioGroup from '.';
4
4
  import { Field } from '../field/Field';
@@ -12,6 +12,6 @@ describe('Select', () => {
12
12
  <Select options={options} selected={options[0]} onChange={() => {}} />
13
13
  </Field>,
14
14
  );
15
- expect(screen.getByLabelText('Currency')).toHaveTextContent('USD');
15
+ expect(screen.getByLabelText(/Currency/)).toHaveTextContent('USD');
16
16
  });
17
17
  });
@@ -85,7 +85,7 @@ describe('Switch', () => {
85
85
 
86
86
  it('supports `Field` for labeling', () => {
87
87
  render(
88
- <Field label="Dark mode">
88
+ <Field label="Dark mode" required>
89
89
  <Switch checked onClick={props.onClick} />
90
90
  </Field>,
91
91
  );
@@ -1,33 +0,0 @@
1
- import { useState } from 'react';
2
-
3
- import { Input } from '../inputs/Input';
4
- import { Field } from './Field';
5
- import { Sentiment } from '../common';
6
-
7
- export default {
8
- component: Field,
9
- title: 'Field/Tests',
10
- };
11
-
12
- export const WithHelpAndErrorOnBlur = () => {
13
- const [value, setValue] = useState('This is some text');
14
- const [error, setError] = useState<string | undefined>(undefined);
15
- return (
16
- <Field
17
- label="Phone number"
18
- sentiment={error ? Sentiment.NEGATIVE : Sentiment.NEUTRAL}
19
- message={error || 'Please include country code'}
20
- >
21
- <Input
22
- value={value}
23
- onChange={({ target }) => {
24
- setValue(target.value);
25
- setError(undefined);
26
- }}
27
- onBlur={() => {
28
- setError('Something went wrong');
29
- }}
30
- />
31
- </Field>
32
- );
33
- };