@iota/apps-ui-kit 0.9.3 → 0.9.5

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 (39) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/dist/index.d.ts +30 -21
  3. package/dist/index.es.js +2173 -2066
  4. package/dist/index.es.js.map +1 -1
  5. package/dist/index.umd.js +12 -12
  6. package/dist/index.umd.js.map +1 -1
  7. package/dist/style.css +1 -1
  8. package/package.json +2 -2
  9. package/src/lib/components/atoms/badge/Badge.tsx +13 -6
  10. package/src/lib/components/atoms/badge/badge.classes.ts +0 -2
  11. package/src/lib/components/atoms/badge/badge.enums.ts +5 -0
  12. package/src/lib/components/atoms/button/Button.tsx +4 -2
  13. package/src/lib/components/atoms/button-segment/ButtonSegment.tsx +3 -1
  14. package/src/lib/components/atoms/checkbox/Checkbox.tsx +3 -1
  15. package/src/lib/components/atoms/header/Header.tsx +2 -0
  16. package/src/lib/components/atoms/key-value-info/KeyValueInfo.tsx +1 -1
  17. package/src/lib/components/atoms/list-item/ListItem.tsx +3 -1
  18. package/src/lib/components/atoms/radio-button/RadioButton.tsx +3 -1
  19. package/src/lib/components/atoms/snackbar/Snackbar.tsx +4 -4
  20. package/src/lib/components/atoms/toggle/Toggle.tsx +4 -1
  21. package/src/lib/components/molecules/account/Account.tsx +2 -2
  22. package/src/lib/components/molecules/address/Address.tsx +4 -2
  23. package/src/lib/components/molecules/card/Card.tsx +6 -2
  24. package/src/lib/components/molecules/chip/Chip.tsx +11 -5
  25. package/src/lib/components/molecules/display-stats/DisplayStats.tsx +1 -1
  26. package/src/lib/components/molecules/input/Input.tsx +2 -0
  27. package/src/lib/components/molecules/navbar-item/NavbarItem.tsx +1 -1
  28. package/src/lib/components/molecules/navbar-item/NavbarItemHorizontal.tsx +3 -0
  29. package/src/lib/components/molecules/navbar-item/NavbarItemVertical.tsx +3 -0
  30. package/src/lib/components/molecules/search/Search.tsx +11 -1
  31. package/src/lib/components/molecules/select/Select.tsx +32 -24
  32. package/src/lib/components/molecules/table-header-cell/TableHeaderCell.tsx +12 -1
  33. package/src/lib/components/molecules/visual-asset-card/VisualAssetCard.tsx +2 -1
  34. package/src/lib/components/organisms/accordion/Accordion.tsx +2 -0
  35. package/src/lib/components/organisms/dialog/Dialog.tsx +9 -2
  36. package/src/lib/components/organisms/navbar/Navbar.tsx +2 -0
  37. package/src/lib/components/organisms/navbar/NavbarSlideout.tsx +1 -0
  38. package/src/lib/components/organisms/table/Table.tsx +4 -0
  39. package/src/lib/tailwind/base.presets.ts +42 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@iota/apps-ui-kit",
3
- "version": "0.9.3",
3
+ "version": "0.9.5",
4
4
  "sideEffects": false,
5
5
  "repository": "https://github.com/iotaledger/iota",
6
6
  "license": "Apache-2.0",
@@ -27,7 +27,7 @@
27
27
  "classnames": "^2.5.1",
28
28
  "lodash.merge": "^4.6.2",
29
29
  "react-number-format": "^5.2.2",
30
- "@iota/apps-ui-icons": "0.7.3"
30
+ "@iota/apps-ui-icons": "0.7.5"
31
31
  },
32
32
  "peerDependencies": {
33
33
  "react": "^18 || ^19",
@@ -2,8 +2,9 @@
2
2
  // SPDX-License-Identifier: Apache-2.0
3
3
 
4
4
  import cx from 'classnames';
5
+ import { BadgeSize } from './badge.enums';
5
6
  import type { BadgeType } from './badge.enums';
6
- import { BACKGROUND_COLORS, BADGE_TEXT_CLASS, BORDER_COLORS, TEXT_COLORS } from './badge.classes';
7
+ import { BACKGROUND_COLORS, BORDER_COLORS, TEXT_COLORS } from './badge.classes';
7
8
 
8
9
  interface BadgeProps {
9
10
  /**
@@ -14,24 +15,30 @@ interface BadgeProps {
14
15
  * The label of the badge.
15
16
  */
16
17
  label?: string;
18
+ /**
19
+ * The size of the badge
20
+ */
21
+ size?: BadgeSize;
17
22
  }
18
23
 
19
- export function Badge({ type, label }: BadgeProps): React.JSX.Element {
24
+ export function Badge({ type, label, size = BadgeSize.Medium }: BadgeProps): React.JSX.Element {
20
25
  const backgroundClasses = BACKGROUND_COLORS[type];
21
26
  const textClasses = TEXT_COLORS[type];
22
- const borderClasses = BORDER_COLORS[type];
27
+ const isSmall = size === BadgeSize.Small;
23
28
  const labelClasses = label ? 'px-xs py-xxs' : 'h-1.5 w-1.5';
29
+ const textSizeClass = isSmall ? 'text-label-sm' : 'text-label-md';
24
30
 
25
31
  return (
26
32
  <div
27
33
  className={cx(
28
- 'inline-flex items-center space-x-2 rounded-full border disabled:opacity-30',
34
+ 'inline-flex items-center space-x-2 rounded-full disabled:opacity-30',
35
+ { border: !isSmall },
29
36
  backgroundClasses,
30
- borderClasses,
37
+ { [BORDER_COLORS[type]]: !isSmall },
31
38
  labelClasses,
32
39
  )}
33
40
  >
34
- <span className={cx(BADGE_TEXT_CLASS, textClasses)}>{label}</span>
41
+ <span className={cx(textSizeClass, textClasses)}>{label}</span>
35
42
  </div>
36
43
  );
37
44
  }
@@ -29,5 +29,3 @@ export const BORDER_COLORS: Record<BadgeType, string> = {
29
29
  [BadgeType.Warning]: 'border-warning-surface',
30
30
  [BadgeType.Error]: 'border-error-surface',
31
31
  };
32
-
33
- export const BADGE_TEXT_CLASS = 'text-label-md';
@@ -9,3 +9,8 @@ export enum BadgeType {
9
9
  Warning = 'warning',
10
10
  Error = 'error',
11
11
  }
12
+
13
+ export enum BadgeSize {
14
+ Medium = 'medium',
15
+ Small = 'small',
16
+ }
@@ -14,7 +14,7 @@ import {
14
14
  } from './button.classes';
15
15
  import cx from 'classnames';
16
16
 
17
- export interface ButtonProps {
17
+ export interface ButtonProps extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, 'type'> {
18
18
  /**
19
19
  * The size of the button.
20
20
  */
@@ -75,6 +75,7 @@ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
75
75
  iconAfterText = false,
76
76
  tabIndex = 0,
77
77
  testId,
78
+ ...buttonProps
78
79
  },
79
80
  ref,
80
81
  ): React.JSX.Element => {
@@ -87,9 +88,10 @@ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
87
88
 
88
89
  return (
89
90
  <button
91
+ type={htmlType}
92
+ {...buttonProps}
90
93
  ref={ref}
91
94
  onClick={onClick}
92
- type={htmlType}
93
95
  className={cx(
94
96
  'state-layer relative flex items-center justify-center gap-2 rounded-full transition-all duration-150 ease-in disabled:cursor-not-allowed disabled:opacity-40',
95
97
  paddingClasses,
@@ -13,7 +13,7 @@ import cx from 'classnames';
13
13
  import { ButtonSegmentType } from './buttonSegment.enums';
14
14
  import { ButtonUnstyled } from '../button';
15
15
 
16
- interface ButtonSegmentProps {
16
+ interface ButtonSegmentProps extends React.AriaAttributes {
17
17
  /**
18
18
  * The label of the button.
19
19
  */
@@ -52,6 +52,7 @@ export function ButtonSegment({
52
52
  onClick,
53
53
  type = ButtonSegmentType.Rounded,
54
54
  isNested = false,
55
+ ...ariaProps
55
56
  }: ButtonSegmentProps): React.JSX.Element {
56
57
  const isUnderlined = type === ButtonSegmentType.Underlined;
57
58
 
@@ -88,6 +89,7 @@ export function ButtonSegment({
88
89
  },
89
90
  )}
90
91
  disabled={disabled}
92
+ {...ariaProps}
91
93
  >
92
94
  <div className={cx('flex flex-row items-center justify-center gap-2', textSize)}>
93
95
  {icon && <span>{icon}</span>}
@@ -5,7 +5,7 @@ import { forwardRef, useEffect, useRef } from 'react';
5
5
  import cx from 'classnames';
6
6
  import { Dash, Checkmark } from '@iota/apps-ui-icons';
7
7
 
8
- interface CheckboxProps {
8
+ interface CheckboxProps extends React.AriaAttributes {
9
9
  /**
10
10
  * The label of the checkbox.
11
11
  */
@@ -46,6 +46,7 @@ export const Checkbox = forwardRef<HTMLInputElement, CheckboxProps>(
46
46
  isDisabled,
47
47
  onCheckedChange,
48
48
  name,
49
+ ...ariaProps
49
50
  }: CheckboxProps,
50
51
  ref,
51
52
  ) => {
@@ -92,6 +93,7 @@ export const Checkbox = forwardRef<HTMLInputElement, CheckboxProps>(
92
93
  onChange={(e) => {
93
94
  onCheckedChange?.(e);
94
95
  }}
96
+ {...ariaProps}
95
97
  />
96
98
  <span
97
99
  onClick={() => inputRef.current?.click()}
@@ -46,6 +46,7 @@ export function Header({
46
46
  type={ButtonType.Ghost}
47
47
  onClick={onBack}
48
48
  icon={<ArrowBack />}
49
+ aria-label="Back"
49
50
  />
50
51
  ) : (
51
52
  keepSpaceForIcon && <div className="w-9" />
@@ -64,6 +65,7 @@ export function Header({
64
65
  onClick={onClose}
65
66
  icon={<Close />}
66
67
  testId={`close-icon`}
68
+ aria-label="Close"
67
69
  />
68
70
  ) : (
69
71
  keepSpaceForIcon && <div className="w-9" />
@@ -146,7 +146,7 @@ export function KeyValueInfo({
146
146
  )}
147
147
  <div className="self-center">
148
148
  {copyText && (
149
- <ButtonUnstyled onClick={handleCopyClick}>
149
+ <ButtonUnstyled onClick={handleCopyClick} aria-label="Copy to clipboard">
150
150
  <Copy className="key-supporting-text-color" />
151
151
  </ButtonUnstyled>
152
152
  )}
@@ -6,7 +6,7 @@ import cx from 'classnames';
6
6
  import { ArrowRight } from '@iota/apps-ui-icons';
7
7
  import { Button, ButtonSize, ButtonType } from '../button';
8
8
 
9
- export interface ListItemProps {
9
+ export interface ListItemProps extends React.AriaAttributes {
10
10
  /**
11
11
  * Has right icon (optional).
12
12
  */
@@ -36,6 +36,7 @@ export function ListItem({
36
36
  isDisabled,
37
37
  children,
38
38
  isHighlighted,
39
+ ...ariaProps
39
40
  }: PropsWithChildren<ListItemProps>): React.JSX.Element {
40
41
  function handleKeyDown(event: React.KeyboardEvent<HTMLDivElement>) {
41
42
  if ((event.key === 'Enter' || event.key === ' ') && !isDisabled && onClick) {
@@ -64,6 +65,7 @@ export function ListItem({
64
65
  role="button"
65
66
  tabIndex={0}
66
67
  onKeyDown={handleKeyDown}
68
+ {...ariaProps}
67
69
  className={cx(
68
70
  'list-item-color relative flex flex-row items-center justify-between px-md py-sm',
69
71
  !isDisabled && onClick ? 'cursor-pointer' : 'cursor-default',
@@ -4,7 +4,7 @@
4
4
  import cx from 'classnames';
5
5
  import { RadioOn, RadioOff } from '@iota/apps-ui-icons';
6
6
 
7
- interface RadioButtonProps {
7
+ interface RadioButtonProps extends React.AriaAttributes {
8
8
  /**
9
9
  * The id of the radio button.
10
10
  */
@@ -48,6 +48,7 @@ function RadioButton({
48
48
  isChecked,
49
49
  isDisabled,
50
50
  onChange,
51
+ ...ariaProps
51
52
  }: RadioButtonProps): React.JSX.Element {
52
53
  const RadioIcon = isChecked ? RadioOn : RadioOff;
53
54
 
@@ -77,6 +78,7 @@ function RadioButton({
77
78
  onChange={onChange}
78
79
  disabled={isDisabled}
79
80
  className={cx('peer appearance-none')}
81
+ {...ariaProps}
80
82
  />
81
83
  <span className="radio-icon-color peer-checked:radio-icon-checked-color peer-checked:peer-disabled:radio-icon-checked-disabled-color absolute size-6">
82
84
  <RadioIcon className="size-full" />
@@ -6,6 +6,7 @@ import cx from 'classnames';
6
6
  import { Close } from '@iota/apps-ui-icons';
7
7
  import { SnackbarType } from './snackbar.enums';
8
8
  import { BACKGROUND_COLOR, TEXT_COLOR } from './snackbar.classes';
9
+ import { ButtonUnstyled } from '../button';
9
10
 
10
11
  type Renderable = JSX.Element | string | null;
11
12
  export interface SnackbarProps {
@@ -59,10 +60,9 @@ export function Snackbar({
59
60
  >
60
61
  <div className={cx('w-full text-left text-body-md', TEXT_COLOR[type])}>{text}</div>
61
62
  {showClose && (
62
- <Close
63
- className={cx('h-5 w-5 cursor-pointer', TEXT_COLOR[type])}
64
- onClick={onClose}
65
- />
63
+ <ButtonUnstyled onClick={onClose} aria-label="Close">
64
+ <Close className={cx('h-5 w-5', TEXT_COLOR[type])} />
65
+ </ButtonUnstyled>
66
66
  )}
67
67
  </div>
68
68
  );
@@ -16,7 +16,7 @@ import {
16
16
  } from './toggle.classes';
17
17
  import cx from 'classnames';
18
18
 
19
- interface ToggleProps {
19
+ interface ToggleProps extends React.AriaAttributes {
20
20
  /**
21
21
  * The label for the toggle.
22
22
  */
@@ -62,6 +62,7 @@ export const Toggle = forwardRef<HTMLInputElement, ToggleProps>(
62
62
  name,
63
63
  size = ToggleSize.Default,
64
64
  testId,
65
+ ...ariaProps
65
66
  }: ToggleProps,
66
67
  ref,
67
68
  ) => {
@@ -123,8 +124,10 @@ export const Toggle = forwardRef<HTMLInputElement, ToggleProps>(
123
124
 
124
125
  <span
125
126
  role="switch"
127
+ aria-checked={isToggled}
126
128
  onClick={() => inputRef.current?.click()}
127
129
  className={toggleClasses}
130
+ {...ariaProps}
128
131
  >
129
132
  <span className={thumbClasses} />
130
133
  </span>
@@ -134,13 +134,13 @@ export function Account({
134
134
  <div className="ml-auto flex items-center space-x-2 [&_button]:h-5 [&_button]:w-5 [&_svg]:h-5 [&_svg]:w-5">
135
135
  <div className="account-icon-color flex items-center space-x-2 [&_button:not(.locked)]:invisible group-hover:[&_button:not(.locked)]:visible">
136
136
  {onOptionsClick && (
137
- <ButtonUnstyled onClick={onOptionsClick}>
137
+ <ButtonUnstyled onClick={onOptionsClick} aria-label="More options">
138
138
  <MoreHoriz />
139
139
  </ButtonUnstyled>
140
140
  )}
141
141
  </div>
142
142
  {showSelected && (
143
- <ButtonUnstyled>
143
+ <ButtonUnstyled aria-label="Checkmark">
144
144
  {isSelected ? (
145
145
  <CheckmarkFilled className="account-check-active-color h-5 w-5" />
146
146
  ) : (
@@ -82,7 +82,8 @@ export function Address({
82
82
  {isCopyable && (
83
83
  <ButtonUnstyled
84
84
  onClick={handleCopyClick}
85
- className="opacity-0 group-hover:opacity-100"
85
+ className="opacity-100"
86
+ aria-label="Copy to clipboard"
86
87
  >
87
88
  <Copy />
88
89
  </ButtonUnstyled>
@@ -90,7 +91,8 @@ export function Address({
90
91
  {isExternal && (
91
92
  <ButtonUnstyled
92
93
  onClick={handleOpenClick}
93
- className="opacity-0 group-hover:opacity-100"
94
+ className="opacity-100"
95
+ aria-label="Open in new tab"
94
96
  >
95
97
  <ArrowTopRight />
96
98
  </ButtonUnstyled>
@@ -5,7 +5,7 @@ import cx from 'classnames';
5
5
  import { CARD_DISABLED_CLASSES, CARD_TYPE_CLASSES } from './card.classes';
6
6
  import { CardType } from './card.enums';
7
7
 
8
- export interface CardProps {
8
+ export interface CardProps extends React.AriaAttributes {
9
9
  /**
10
10
  * If `true`, the card will be disabled.
11
11
  */
@@ -44,25 +44,29 @@ export function Card({
44
44
  onClick,
45
45
  children,
46
46
  testId,
47
+ ...ariaProps
47
48
  }: CardProps) {
48
49
  function handleOnClick() {
49
50
  if (!isDisabled) {
50
51
  onClick?.();
51
52
  }
52
53
  }
54
+ const isClickable = !isDisabled && onClick;
53
55
  return (
54
56
  <div
55
57
  onClick={handleOnClick}
58
+ role={isClickable ? 'button' : undefined}
56
59
  className={cx(
57
60
  'relative inline-flex w-full items-center gap-3 rounded-xl px-sm py-xs',
58
61
  CARD_TYPE_CLASSES[type],
59
62
  {
60
- 'state-layer': isHoverable || (!isDisabled && onClick),
63
+ 'state-layer': isHoverable || isClickable,
61
64
  [CARD_DISABLED_CLASSES]: isDisabled,
62
65
  'cursor-pointer': onClick,
63
66
  },
64
67
  )}
65
68
  data-testid={testId}
69
+ {...ariaProps}
66
70
  >
67
71
  {children}
68
72
  </div>
@@ -18,7 +18,7 @@ import {
18
18
  } from './chip.classes';
19
19
  import { ChipSize, ChipType } from './chip.enums';
20
20
 
21
- interface ChipProps {
21
+ interface ChipProps extends React.AriaAttributes {
22
22
  /**
23
23
  * The label of the chip
24
24
  */
@@ -77,6 +77,7 @@ export function Chip({
77
77
  trailingElement,
78
78
  disabled,
79
79
  size = ChipSize.Default,
80
+ ...ariaProps
80
81
  }: ChipProps) {
81
82
  const isOutlineSelected = type === ChipType.Outline && selected;
82
83
  const outlineStyle = selected
@@ -104,6 +105,7 @@ export function Chip({
104
105
  selected ? 'border-transparent' : BORDER_CLASSES[type],
105
106
  )}
106
107
  disabled={disabled}
108
+ {...ariaProps}
107
109
  >
108
110
  <span
109
111
  className={cx(
@@ -128,9 +130,13 @@ export function Chip({
128
130
  </span>
129
131
  {trailingElement}
130
132
  {showClose && (
131
- <div
132
- onClick={onClose}
133
- className={cx(disabled ? 'cursor-default' : 'cursor-pointer')}
133
+ <ButtonUnstyled
134
+ onClick={(e) => {
135
+ e.stopPropagation();
136
+ onClose?.();
137
+ }}
138
+ disabled={disabled}
139
+ aria-label="Remove"
134
140
  >
135
141
  <Close
136
142
  className={cx(
@@ -138,7 +144,7 @@ export function Chip({
138
144
  disabled ? '' : selected ? 'opacity-100' : CLOSE_ICON_INTERACTIVE,
139
145
  )}
140
146
  />
141
- </div>
147
+ </ButtonUnstyled>
142
148
  )}
143
149
  </span>
144
150
  </ButtonUnstyled>
@@ -131,7 +131,7 @@ export function DisplayStats({
131
131
  )}
132
132
  {copyText && (
133
133
  <div className="self-center">
134
- <ButtonUnstyled onClick={handleCopyClick}>
134
+ <ButtonUnstyled onClick={handleCopyClick} aria-label="Copy to clipboard">
135
135
  <Copy className="display-stats-copy-icon-color" />
136
136
  </ButtonUnstyled>
137
137
  </div>
@@ -241,6 +241,7 @@ function InputTrailingElement({
241
241
  className="input-icon-color [&_svg]:h-5 [&_svg]:w-5"
242
242
  onClick={onClearInput}
243
243
  tabIndex={-1}
244
+ aria-label="Clear input"
244
245
  >
245
246
  <Close />
246
247
  </ButtonUnstyled>
@@ -251,6 +252,7 @@ function InputTrailingElement({
251
252
  onClick={onToggleButtonClick}
252
253
  className="input-icon-color [&_svg]:h-5 [&_svg]:w-5"
253
254
  tabIndex={-1}
255
+ aria-label="Toggle visibility"
254
256
  >
255
257
  {isContentVisible ? <VisibilityOn /> : <VisibilityOff />}
256
258
  </ButtonUnstyled>
@@ -5,7 +5,7 @@ import { NavbarItemHorizontal } from './NavbarItemHorizontal';
5
5
  import { NavbarItemVertical } from './NavbarItemVertical';
6
6
  import { NavbarItemType } from './navbarItem.enums';
7
7
 
8
- export interface NavbarItemProps {
8
+ export interface NavbarItemProps extends React.AriaAttributes {
9
9
  /**
10
10
  * The type of the navbar item.
11
11
  */
@@ -23,6 +23,7 @@ export function NavbarItemHorizontal({
23
23
  hasBadge,
24
24
  onClick,
25
25
  isDisabled = false,
26
+ ...ariaProps
26
27
  }: Omit<NavbarItemProps, 'type'>): React.JSX.Element {
27
28
  const fillClasses = isSelected ? SELECTED_ICON : UNSELECTED_ICON;
28
29
  const paddingClasses = text ? PADDING_WITH_TEXT : PADDING_WITHOUT_TEXT;
@@ -40,9 +41,11 @@ export function NavbarItemHorizontal({
40
41
  return (
41
42
  <div
42
43
  onClick={handleClick}
44
+ role="button"
43
45
  className={cx(
44
46
  'inline-flex cursor-pointer flex-col items-center justify-center space-y-1',
45
47
  )}
48
+ {...ariaProps}
46
49
  >
47
50
  <div
48
51
  className={cx(
@@ -19,6 +19,7 @@ export function NavbarItemVertical({
19
19
  hasBadge,
20
20
  onClick,
21
21
  isDisabled = false,
22
+ ...ariaProps
22
23
  }: Omit<NavbarItemProps, 'type'>): React.JSX.Element {
23
24
  const fillClasses = isSelected ? SELECTED_ICON : UNSELECTED_ICON;
24
25
  const backgroundColors = isSelected && SELECTED_BACKGROUND;
@@ -36,11 +37,13 @@ export function NavbarItemVertical({
36
37
  return (
37
38
  <div
38
39
  onClick={handleClick}
40
+ role="button"
39
41
  className={cx(
40
42
  'relative inline-flex w-full flex-row items-center justify-between rounded-full p-sm',
41
43
  backgroundColors,
42
44
  disabledClasses,
43
45
  )}
46
+ {...ariaProps}
44
47
  >
45
48
  <div className="flex items-center gap-3">
46
49
  <div className={cx('inline-flex [&_svg]:h-6 [&_svg]:w-6', fillClasses)}>{icon}</div>
@@ -19,7 +19,7 @@ export interface Suggestion {
19
19
  type?: string;
20
20
  }
21
21
 
22
- export interface SearchProps {
22
+ export interface SearchProps extends React.AriaAttributes {
23
23
  /**
24
24
  * The value of the search input.
25
25
  */
@@ -63,6 +63,7 @@ export function Search({
63
63
  isLoading = false,
64
64
  type = SearchBarType.Outlined,
65
65
  renderSuggestion,
66
+ ...ariaProps
66
67
  }: SearchProps): React.JSX.Element {
67
68
  const inputRef = useRef<HTMLInputElement>(null);
68
69
  const suggestionsListRef = useRef<HTMLDivElement>(null);
@@ -181,6 +182,11 @@ export function Search({
181
182
  'search-placeholder-color w-full flex-1 outline-none',
182
183
  backgroundColorClass,
183
184
  )}
185
+ role="combobox"
186
+ aria-expanded={showSuggestions}
187
+ aria-haspopup="listbox"
188
+ aria-autocomplete="list"
189
+ {...ariaProps}
184
190
  />
185
191
  <SearchIcon />
186
192
  </div>
@@ -191,6 +197,7 @@ export function Search({
191
197
  'absolute left-0 top-full flex w-full flex-col items-center overflow-hidden',
192
198
  suggestionsStyle,
193
199
  )}
200
+ role="listbox"
194
201
  >
195
202
  <Divider width="w-11/12" />
196
203
  {isLoading ? (
@@ -207,6 +214,9 @@ export function Search({
207
214
  'w-full cursor-pointer px-md py-sm',
208
215
  selectedIndex === index ? 'search-selected-index-bg-color' : '',
209
216
  )}
217
+ role="option"
218
+ aria-selected={selectedIndex === index}
219
+ aria-label={suggestion.label}
210
220
  >
211
221
  {renderSuggestion(suggestion, index)}
212
222
  </div>
@@ -16,7 +16,9 @@ export type SelectOption =
16
16
  | { id: string; renderLabel: () => React.JSX.Element }
17
17
  | { id: string; label: React.ReactNode };
18
18
 
19
- interface SelectProps extends Pick<React.HTMLProps<HTMLSelectElement>, 'disabled'> {
19
+ interface SelectProps
20
+ extends Pick<React.HTMLProps<HTMLSelectElement>, 'disabled'>,
21
+ React.AriaAttributes {
20
22
  /**
21
23
  * The selected option value.
22
24
  */
@@ -83,6 +85,7 @@ export const Select = forwardRef<HTMLButtonElement, SelectProps>(
83
85
  value,
84
86
  dropdownPosition = DropdownPosition.Bottom,
85
87
  size = SelectSize.Default,
88
+ ...ariaProps
86
89
  },
87
90
  ref,
88
91
  ) => {
@@ -156,10 +159,13 @@ export const Select = forwardRef<HTMLButtonElement, SelectProps>(
156
159
  ref={ref}
157
160
  onClick={onSelectorClick}
158
161
  disabled={disabled}
162
+ aria-expanded={isOpen}
163
+ aria-haspopup="listbox"
159
164
  className={cx(
160
165
  'select-container select-border-color focus-visible:enabled:select-border-focus-color active:enabled:select-border-focus-color group-[.errored]:select-border-error-color group-[.opened]:select-border-focus-color [&:is(:focus,_:focus-visible,_:active)]:enabled:select-border-focus-color hover:enabled:select-border-hover-color flex flex-row items-center gap-x-3 rounded-lg border py-sm disabled:cursor-not-allowed [&_svg]:h-5 [&_svg]:w-5',
161
166
  selectPadding,
162
167
  )}
168
+ {...ariaProps}
163
169
  >
164
170
  {leadingIcon && <span className="select-icon-color">{leadingIcon}</span>}
165
171
 
@@ -195,29 +201,31 @@ export const Select = forwardRef<HTMLButtonElement, SelectProps>(
195
201
  onClick={closeDropdown}
196
202
  />
197
203
  )}
198
- <div
199
- className={cx('absolute z-50 min-w-full', {
200
- hidden: !isOpen,
201
- 'top-full':
202
- !dropdownPosition || dropdownPosition === DropdownPosition.Bottom,
203
- 'bottom-full': dropdownPosition === DropdownPosition.Top,
204
- })}
205
- >
206
- <Dropdown>
207
- {options.map((option) => {
208
- const optionIsString = typeof option === 'string';
209
- return (
210
- <ListItem
211
- onClick={() => handleOptionClick(option)}
212
- hideBottomBorder
213
- key={optionIsString ? option : option.id}
214
- >
215
- <OptionLabel option={option} />
216
- </ListItem>
217
- );
218
- })}
219
- </Dropdown>
220
- </div>
204
+ {isOpen && (
205
+ <div
206
+ className={cx(
207
+ 'absolute z-50 min-w-full animate-dropdown-show',
208
+ dropdownPosition === DropdownPosition.Top
209
+ ? 'bottom-full origin-bottom'
210
+ : 'top-full origin-top',
211
+ )}
212
+ >
213
+ <Dropdown>
214
+ {options.map((option) => {
215
+ const optionIsString = typeof option === 'string';
216
+ return (
217
+ <ListItem
218
+ onClick={() => handleOptionClick(option)}
219
+ hideBottomBorder
220
+ key={optionIsString ? option : option.id}
221
+ >
222
+ <OptionLabel option={option} />
223
+ </ListItem>
224
+ );
225
+ })}
226
+ </Dropdown>
227
+ </div>
228
+ )}
221
229
  </div>
222
230
  </InputWrapper>
223
231
  );