@altinn/altinn-components 0.4.1 → 0.5.1

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 (194) hide show
  1. package/.storybook/StoryDecorator.tsx +1 -1
  2. package/.storybook/main.ts +3 -4
  3. package/.storybook/preview.tsx +28 -0
  4. package/CHANGELOG.md +14 -0
  5. package/biome.jsonc +1 -1
  6. package/lib/components/Attachment/AttachmentLink.stories.ts +1 -1
  7. package/lib/components/Attachment/AttachmentList.stories.ts +1 -1
  8. package/lib/components/Button/Button.tsx +13 -10
  9. package/lib/components/Button/ButtonBase.tsx +1 -1
  10. package/lib/components/Button/ButtonIcon.tsx +16 -0
  11. package/lib/components/Button/ButtonLabel.tsx +18 -0
  12. package/lib/components/Button/Buttons.stories.tsx +64 -0
  13. package/lib/components/Button/ComboButton.tsx +9 -7
  14. package/lib/components/Button/IconButton.stories.tsx +47 -0
  15. package/lib/components/Button/IconButton.tsx +15 -5
  16. package/lib/components/Button/button.module.css +5 -46
  17. package/lib/components/Button/buttonBase.module.css +55 -23
  18. package/lib/components/Button/buttonIcon.module.css +17 -0
  19. package/lib/components/Button/buttonLabel.module.css +17 -0
  20. package/lib/components/Button/comboButton.module.css +15 -65
  21. package/lib/components/Button/iconButton.module.css +21 -4
  22. package/lib/components/Button/index.ts +2 -0
  23. package/lib/components/ContextMenu/ContextMenu.stories.ts +49 -0
  24. package/lib/components/ContextMenu/ContextMenu.tsx +12 -20
  25. package/lib/components/ContextMenu/ContextMenuBase.tsx +33 -0
  26. package/lib/components/Dialog/Dialog.stories.ts +12 -5
  27. package/lib/components/Dialog/Dialog.tsx +2 -0
  28. package/lib/components/Dialog/DialogGroup.tsx +24 -0
  29. package/lib/components/Dialog/DialogList.stories.ts +14 -10
  30. package/lib/components/Dialog/DialogList.tsx +26 -11
  31. package/lib/components/Dialog/DialogListItem.tsx +12 -2
  32. package/lib/components/Dialog/DialogListItemBase.tsx +4 -2
  33. package/lib/components/Dialog/DialogNav.stories.ts +5 -5
  34. package/lib/components/Dialog/DialogNav.tsx +2 -6
  35. package/lib/components/Dialog/DialogSelect.tsx +1 -1
  36. package/lib/components/Dialog/dialogGroup.module.css +35 -0
  37. package/lib/components/Dropdown/Backdrop.tsx +4 -3
  38. package/lib/components/Dropdown/DrawerBase.tsx +5 -2
  39. package/lib/components/Dropdown/DrawerBody.tsx +12 -0
  40. package/lib/components/Dropdown/DrawerButton.tsx +17 -0
  41. package/lib/components/Dropdown/DrawerFooter.tsx +12 -0
  42. package/lib/components/Dropdown/DrawerHeader.tsx +19 -0
  43. package/lib/components/Dropdown/DrawerOrDropdown.tsx +29 -0
  44. package/lib/components/Dropdown/DropdownBase.tsx +17 -2
  45. package/lib/components/Dropdown/backdrop.module.css +3 -0
  46. package/lib/components/Dropdown/drawerBase.module.css +9 -0
  47. package/lib/components/Dropdown/drawerBody.module.css +5 -0
  48. package/lib/components/Dropdown/drawerButton.module.css +6 -0
  49. package/lib/components/Dropdown/drawerFooter.module.css +13 -0
  50. package/lib/components/Dropdown/drawerHeader.module.css +17 -0
  51. package/lib/components/Dropdown/drawerOrDropdown.module.css +19 -0
  52. package/lib/components/Dropdown/dropdownBase.module.css +20 -4
  53. package/lib/components/Dropdown/index.ts +7 -1
  54. package/lib/components/Footer/footerMenu.module.css +5 -0
  55. package/lib/components/GlobalMenu/AccountButton.tsx +29 -0
  56. package/lib/components/GlobalMenu/AccountMenu.stories.tsx +65 -0
  57. package/lib/components/GlobalMenu/AccountMenu.tsx +73 -0
  58. package/lib/components/GlobalMenu/BackButton.tsx +10 -0
  59. package/lib/components/GlobalMenu/GlobalMenu.stories.tsx +112 -121
  60. package/lib/components/GlobalMenu/GlobalMenu.tsx +41 -89
  61. package/lib/components/GlobalMenu/GlobalMenuBase.tsx +22 -0
  62. package/lib/components/GlobalMenu/LogoutButton.tsx +19 -0
  63. package/lib/components/GlobalMenu/globalMenuBase.module.css +39 -0
  64. package/lib/components/GlobalMenu/index.tsx +1 -1
  65. package/lib/components/GlobalMenu/logoutButton.module.css +9 -0
  66. package/lib/components/Header/{Header.stories.ts → Header.stories.tsx} +79 -20
  67. package/lib/components/Header/Header.tsx +25 -38
  68. package/lib/components/Header/HeaderBase.tsx +7 -3
  69. package/lib/components/Header/header.module.css +10 -42
  70. package/lib/components/Header/headerBase.module.css +43 -0
  71. package/lib/components/Header/headerButton.module.css +1 -0
  72. package/lib/components/Layout/Layout.stories.tsx +77 -38
  73. package/lib/components/Layout/Layout.tsx +5 -3
  74. package/lib/components/Layout/LayoutBase.tsx +3 -2
  75. package/lib/components/Layout/layoutBase.module.css +11 -0
  76. package/lib/components/Layout/layoutBody.module.css +1 -0
  77. package/lib/components/LayoutAction/ActionHeader.tsx +1 -1
  78. package/lib/components/LayoutAction/ActionMenu.tsx +2 -4
  79. package/lib/components/LayoutAction/actionMenu.module.css +3 -0
  80. package/lib/components/List/List.stories.tsx +43 -0
  81. package/lib/components/List/List.tsx +6 -6
  82. package/lib/components/List/ListBase.tsx +6 -6
  83. package/lib/components/List/ListItem.tsx +4 -1
  84. package/lib/components/List/ListItemBase.tsx +20 -4
  85. package/lib/components/List/listBase.module.css +3 -3
  86. package/lib/components/List/listItemBase.module.css +4 -0
  87. package/lib/components/Menu/Menu.stories.ts +46 -46
  88. package/lib/components/Menu/Menu.tsx +3 -102
  89. package/lib/components/Menu/MenuBase.tsx +47 -3
  90. package/lib/components/Menu/MenuItem.tsx +8 -4
  91. package/lib/components/Menu/MenuItemBase.tsx +15 -2
  92. package/lib/components/Menu/MenuItems.stories.ts +438 -0
  93. package/lib/components/Menu/MenuItems.tsx +96 -0
  94. package/lib/components/Menu/MenuOption.tsx +4 -1
  95. package/lib/components/Menu/index.ts +1 -1
  96. package/lib/components/Menu/menu.module.css +2 -3
  97. package/lib/components/Menu/menuBase.module.css +25 -0
  98. package/lib/components/Menu/menuItemBase.module.css +11 -5
  99. package/lib/components/Menu/menuItemLabel.module.css +11 -1
  100. package/lib/components/Menu/menuSearch.module.css +1 -0
  101. package/lib/components/Meta/MetaItemBase.tsx +1 -1
  102. package/lib/components/Meta/MetaItemLabel.tsx +1 -1
  103. package/lib/components/Meta/MetaItemMedia.tsx +1 -1
  104. package/lib/components/Page/PageBase.tsx +14 -0
  105. package/lib/components/Page/PageHeader.tsx +21 -0
  106. package/lib/components/Page/PageHeaderMedia.tsx +25 -0
  107. package/lib/components/Page/SectionBase.tsx +52 -0
  108. package/lib/components/Page/SectionFooter.tsx +15 -0
  109. package/lib/components/Page/SectionHeader.tsx +16 -0
  110. package/lib/components/Page/index.ts +5 -0
  111. package/lib/components/Page/pageHeader.module.css +5 -0
  112. package/lib/components/Page/sectionBase.module.css +82 -0
  113. package/lib/components/Page/sectionFooter.module.css +8 -0
  114. package/lib/components/Page/sectionHeader.module.css +9 -0
  115. package/lib/components/RootProvider/RootProvider.tsx +43 -7
  116. package/lib/components/Searchbar/Autocomplete.stories.tsx +77 -0
  117. package/lib/components/Searchbar/Autocomplete.tsx +44 -0
  118. package/lib/components/Searchbar/AutocompleteBase.tsx +16 -0
  119. package/lib/components/Searchbar/AutocompleteGroup.tsx +17 -0
  120. package/lib/components/Searchbar/AutocompleteItem.tsx +23 -0
  121. package/lib/components/Searchbar/SearchField.tsx +78 -0
  122. package/lib/components/Searchbar/Searchbar.stories.tsx +151 -0
  123. package/lib/components/Searchbar/Searchbar.tsx +18 -0
  124. package/lib/components/Searchbar/SearchbarBase.tsx +23 -0
  125. package/lib/components/Searchbar/autocompleteBase.module.css +17 -0
  126. package/lib/components/Searchbar/autocompleteGroup.module.css +3 -0
  127. package/lib/components/Searchbar/autocompleteItem.module.css +19 -0
  128. package/lib/components/Searchbar/index.ts +1 -0
  129. package/lib/components/Searchbar/searchField.module.css +54 -0
  130. package/lib/components/Searchbar/searchbarBase.module.css +20 -0
  131. package/lib/components/Toolbar/Toolbar.stories.tsx +10 -10
  132. package/lib/components/Toolbar/Toolbar.tsx +28 -10
  133. package/lib/components/Toolbar/ToolbarAdd.tsx +6 -5
  134. package/lib/components/Toolbar/ToolbarBase.tsx +5 -20
  135. package/lib/components/Toolbar/ToolbarButton.tsx +1 -1
  136. package/lib/components/Toolbar/ToolbarFilter.tsx +11 -6
  137. package/lib/components/Toolbar/ToolbarMenu.tsx +8 -7
  138. package/lib/components/Toolbar/ToolbarOptions.stories.ts +5 -5
  139. package/lib/components/Toolbar/ToolbarOptions.tsx +34 -21
  140. package/lib/components/Toolbar/toolbarAdd.module.css +7 -0
  141. package/lib/components/Toolbar/toolbarBase.module.css +19 -0
  142. package/lib/components/Toolbar/toolbarButton.module.css +1 -1
  143. package/lib/components/Toolbar/toolbarFilter.module.css +25 -0
  144. package/lib/components/Toolbar/toolbarMenu.module.css +7 -0
  145. package/lib/components/Typography/Heading.tsx +23 -0
  146. package/lib/components/Typography/Typography.tsx +8 -5
  147. package/lib/components/Typography/heading.module.css +21 -0
  148. package/lib/components/Typography/index.ts +1 -0
  149. package/lib/components/Typography/typography.module.css +8 -0
  150. package/lib/components/index.ts +2 -0
  151. package/lib/hooks/index.ts +3 -0
  152. package/lib/{components/Menu → hooks}/useEscapeKey.ts +2 -2
  153. package/lib/hooks/useMenu.tsx +80 -0
  154. package/lib/index.ts +1 -0
  155. package/lib/stories/Color/MenuItem.stories.tsx +43 -0
  156. package/lib/stories/Color/Swatches.stories.tsx +19 -0
  157. package/lib/stories/Color/Swatches.tsx +42 -0
  158. package/lib/stories/Color/colors.json +62 -0
  159. package/lib/stories/Color/swatches.module.css +14 -0
  160. package/lib/stories/Inbox/BookmarksPage.tsx +52 -0
  161. package/lib/stories/Inbox/DialogPage.tsx +15 -0
  162. package/lib/stories/Inbox/Inbox.stories.tsx +55 -0
  163. package/lib/stories/Inbox/Inbox.tsx +12 -0
  164. package/lib/stories/Inbox/InboxLayout.tsx +50 -0
  165. package/lib/stories/Inbox/InboxPage.tsx +50 -0
  166. package/lib/stories/Inbox/InboxProvider.tsx +136 -0
  167. package/lib/stories/Inbox/InboxSection.tsx +39 -0
  168. package/lib/stories/Inbox/InboxToolbar.tsx +94 -0
  169. package/lib/stories/Inbox/ProfilePage.tsx +35 -0
  170. package/lib/stories/Inbox/SettingsPage.tsx +19 -0
  171. package/lib/stories/Inbox/accounts/accounts.ts +24 -0
  172. package/lib/stories/Inbox/accounts/index.ts +1 -0
  173. package/lib/stories/Inbox/actionMenu.ts +24 -0
  174. package/lib/stories/Inbox/dialogs/brreg-completed.json +35 -0
  175. package/lib/stories/Inbox/dialogs/brreg-draft.json +45 -0
  176. package/lib/stories/Inbox/dialogs/index.ts +10 -0
  177. package/lib/stories/Inbox/dialogs/skatt-2023.json +33 -0
  178. package/lib/stories/Inbox/groupBy.ts +19 -0
  179. package/lib/stories/Inbox/inboxSection.module.css +19 -0
  180. package/lib/stories/Inbox/index.ts +15 -0
  181. package/lib/stories/Inbox/layout/footer.ts +27 -0
  182. package/lib/stories/Inbox/layout/header.ts +11 -0
  183. package/lib/stories/Inbox/layout/index.ts +3 -0
  184. package/lib/stories/Inbox/layout/menu.ts +64 -0
  185. package/package.json +15 -13
  186. package/tsconfig.json +7 -2
  187. package/lib/components/Header/HeaderSearch.stories.ts +0 -20
  188. package/lib/components/Header/HeaderSearch.tsx +0 -44
  189. package/lib/components/Header/headerSearch.module.css +0 -30
  190. package/lib/components/Menu/MenuGroup.tsx +0 -18
  191. package/lib/components/Menu/__menuItem.module.css +0 -130
  192. package/lib/components/Toolbar/toolbar.module.css +0 -43
  193. /package/lib/components/ContextMenu/{contextMenu.module.css → contextMenuBase.module.css} +0 -0
  194. /package/lib/{components/Menu → hooks}/useClickOutside.ts +0 -0
@@ -4,12 +4,22 @@
4
4
  padding: 0 0.25rem;
5
5
  }
6
6
 
7
+ .title {
8
+ color: var(--theme-text-default);
9
+ }
10
+
7
11
  .title[data-size="lg"] {
8
12
  font-size: 1.125rem;
9
13
  line-height: 1.25;
10
14
  font-weight: 500;
11
15
  }
12
16
 
17
+ .title[data-size="md"] {
18
+ font-size: 1rem;
19
+ line-height: 1.25;
20
+ font-weight: 500;
21
+ }
22
+
13
23
  .title[data-size="sm"] {
14
24
  font-size: 1rem;
15
25
  line-height: 1.25;
@@ -17,6 +27,6 @@
17
27
  }
18
28
 
19
29
  .description {
20
- font-size: 14px;
30
+ font-size: 0.875rem;
21
31
  color: var(--theme-text-subtle);
22
32
  }
@@ -1,5 +1,6 @@
1
1
  .input {
2
2
  width: 100%;
3
+ height: 2.75rem;
3
4
  font-size: 0.875rem;
4
5
  line-height: 1rem;
5
6
  font-weight: normal;
@@ -3,7 +3,7 @@ import type { ElementType, ReactNode } from 'react';
3
3
  import styles from './metaItem.module.css';
4
4
 
5
5
  export type MetaItemVariant = 'solid' | 'outline' | 'dotted' | 'text';
6
- export type MetaItemSize = 'xs' | 'sm' | 'md';
6
+ export type MetaItemSize = 'xs'; // | 'sm' | 'md';
7
7
  export type MetaItemColor = 'subtle';
8
8
 
9
9
  export interface MetaItemBaseProps {
@@ -11,7 +11,7 @@ export interface MetaItemLabelProps {
11
11
  children?: ReactNode;
12
12
  }
13
13
 
14
- export const MetaItemLabel = ({ size = 'sm', variant = 'text', children }: MetaItemLabelProps) => {
14
+ export const MetaItemLabel = ({ size = 'xs', variant = 'text', children }: MetaItemLabelProps) => {
15
15
  return (
16
16
  <span className={styles.label} data-variant={variant} data-size={size}>
17
17
  {children}
@@ -8,7 +8,7 @@ interface MetaItemMediaProps {
8
8
  icon?: IconName;
9
9
  }
10
10
 
11
- export const MetaItemMedia = ({ size = 'sm', icon, progress }: MetaItemMediaProps) => {
11
+ export const MetaItemMedia = ({ size = 'xs', icon, progress }: MetaItemMediaProps) => {
12
12
  if (!icon && typeof progress !== 'number') {
13
13
  return false;
14
14
  }
@@ -0,0 +1,14 @@
1
+ import type { ReactNode } from 'react';
2
+ import { SectionBase, type SectionBaseProps } from './SectionBase';
3
+
4
+ export interface PageBaseProps extends SectionBaseProps {
5
+ children?: ReactNode;
6
+ }
7
+
8
+ export const PageBase = ({ children, ...props }: PageBaseProps) => {
9
+ return (
10
+ <SectionBase as="main" {...props}>
11
+ {children}
12
+ </SectionBase>
13
+ );
14
+ };
@@ -0,0 +1,21 @@
1
+ import { Heading } from '../';
2
+ import { PageHeaderMedia, type PageHeaderMediaProps } from './PageHeaderMedia';
3
+ import { SectionBase, type SectionBaseProps } from './SectionBase';
4
+ import styles from './pageHeader.module.css';
5
+
6
+ export interface PageHeaderProps extends SectionBaseProps, PageHeaderMediaProps {
7
+ title?: string;
8
+ }
9
+
10
+ export const PageHeader = ({ title, icon, avatar, avatarGroup, children, ...props }: PageHeaderProps) => {
11
+ return (
12
+ <SectionBase as="header" {...props}>
13
+ <div className={styles.title}>
14
+ <PageHeaderMedia icon={icon} avatar={avatar} avatarGroup={avatarGroup} />
15
+ <Heading size="md">{title}</Heading>
16
+ </div>
17
+
18
+ {children}
19
+ </SectionBase>
20
+ );
21
+ };
@@ -0,0 +1,25 @@
1
+ import type { ReactNode } from 'react';
2
+ import { Avatar, AvatarGroup, type AvatarGroupProps, type AvatarProps } from '../Avatar';
3
+ import { Icon, type IconName } from '../Icon';
4
+ import styles from './pageHeader.module.css';
5
+
6
+ export interface PageHeaderMediaProps {
7
+ icon?: IconName;
8
+ avatar?: AvatarProps;
9
+ avatarGroup?: AvatarGroupProps;
10
+ children?: ReactNode;
11
+ }
12
+
13
+ export const PageHeaderMedia = ({ icon, avatar, avatarGroup }: PageHeaderMediaProps) => {
14
+ if (!icon && !avatar && !avatarGroup) {
15
+ return false;
16
+ }
17
+
18
+ return (
19
+ <div className={styles.media}>
20
+ {(icon && <Icon name={icon} variant="outline" className={styles.icon} />) ||
21
+ (avatar && <Avatar {...avatar} size="xl" className={styles.avatar} />) ||
22
+ (avatarGroup && <AvatarGroup {...avatarGroup} size="lg" className={styles.avatarGroup} />)}
23
+ </div>
24
+ );
25
+ };
@@ -0,0 +1,52 @@
1
+ import cx from 'classnames';
2
+ import type { CSSProperties, ReactNode } from 'react';
3
+ import styles from './sectionBase.module.css';
4
+
5
+ export type SectionElement = 'section' | 'main' | 'header' | 'footer' | 'div';
6
+ export type SectionColor = 'transparent' | 'white' | 'subtle' | 'accent';
7
+ export type SectionSpacing = 'none' | 'sm' | 'md' | 'lg';
8
+ export type SectionMargin = 'none' | 'sm' | 'md' | 'lg';
9
+ export type SectionShadow = 'none' | 'xs' | 'sm' | 'md' | 'lg' | 'xl';
10
+
11
+ export interface SectionBaseProps {
12
+ as?: SectionElement;
13
+ color?: SectionColor;
14
+ padding?: boolean;
15
+ spacing?: SectionSpacing;
16
+ margin?: SectionMargin;
17
+ shadow?: SectionShadow;
18
+ inset?: boolean;
19
+ children?: ReactNode;
20
+ className?: string;
21
+ style?: CSSProperties;
22
+ }
23
+
24
+ export const SectionBase = ({
25
+ as = 'section',
26
+ color = 'transparent',
27
+ padding = false,
28
+ spacing = 'none',
29
+ margin = 'none',
30
+ shadow = 'none',
31
+ inset = false,
32
+ className,
33
+ style,
34
+ children,
35
+ }: SectionBaseProps) => {
36
+ const Component = as;
37
+
38
+ return (
39
+ <Component
40
+ className={cx(styles.section, className)}
41
+ style={style}
42
+ data-color={color}
43
+ data-inset={inset}
44
+ data-padding={padding}
45
+ data-spacing={spacing}
46
+ data-margin={margin}
47
+ data-shadow={shadow}
48
+ >
49
+ {children}
50
+ </Component>
51
+ );
52
+ };
@@ -0,0 +1,15 @@
1
+ import type { ReactNode } from 'react';
2
+ import styles from './sectionFooter.module.css';
3
+
4
+ export interface SectionFooterProps {
5
+ margin?: boolean;
6
+ children?: ReactNode;
7
+ }
8
+
9
+ export const SectionFooter = ({ margin = false, children }: SectionFooterProps) => {
10
+ return (
11
+ <footer data-margin={margin} className={styles.footer}>
12
+ {children}
13
+ </footer>
14
+ );
15
+ };
@@ -0,0 +1,16 @@
1
+ import type { ReactNode } from 'react';
2
+ import styles from './sectionHeader.module.css';
3
+
4
+ export interface SectionHeaderProps {
5
+ padding?: boolean;
6
+ margin?: boolean;
7
+ children?: ReactNode;
8
+ }
9
+
10
+ export const SectionHeader = ({ margin = false, children }: SectionHeaderProps) => {
11
+ return (
12
+ <header data-margin={margin} className={styles.header}>
13
+ {children}
14
+ </header>
15
+ );
16
+ };
@@ -0,0 +1,5 @@
1
+ export * from './PageBase';
2
+ export * from './PageHeader';
3
+ export * from './SectionBase';
4
+ export * from './SectionHeader';
5
+ export * from './SectionFooter';
@@ -0,0 +1,5 @@
1
+ .title {
2
+ display: flex;
3
+ align-items: center;
4
+ column-gap: 0.5rem;
5
+ }
@@ -0,0 +1,82 @@
1
+ .section {
2
+ display: flex;
3
+ flex-direction: column;
4
+ }
5
+
6
+ /* inset on small screens */
7
+
8
+ @media (max-width: 1024px) {
9
+ .section[data-inset="true"] {
10
+ margin-left: -1rem;
11
+ margin-right: -1rem;
12
+ }
13
+ }
14
+
15
+ /* spacing */
16
+
17
+ .section[data-spacing="sm"] {
18
+ row-gap: 0.25rem;
19
+ }
20
+
21
+ .section[data-spacing="md"] {
22
+ row-gap: 0.5rem;
23
+ }
24
+
25
+ .section[data-spacing="lg"] {
26
+ row-gap: 1rem;
27
+ }
28
+
29
+ /* margin */
30
+
31
+ .section[data-margin="sm"] {
32
+ margin: 0.25rem 0;
33
+ }
34
+
35
+ .section[data-margin="md"] {
36
+ margin: 0.5rem 0;
37
+ }
38
+
39
+ .section[data-margin="lg"] {
40
+ margin: 1rem 0;
41
+ }
42
+
43
+ /* padding */
44
+
45
+ .section[data-padding="true"] {
46
+ padding: 1rem;
47
+ }
48
+
49
+ /* color */
50
+
51
+ .section[data-color="white"] {
52
+ background-color: #fff;
53
+ }
54
+
55
+ .section[data-color="subtle"] {
56
+ background-color: var(--theme-surface-default);
57
+ }
58
+
59
+ .section[data-color="accent"] {
60
+ background-color: var(--theme-base-default);
61
+ }
62
+
63
+ /* size */
64
+
65
+ .section[data-shadow="xs"] {
66
+ box-shadow: var(--ds-shadow-xs);
67
+ }
68
+
69
+ .section[data-shadow="sm"] {
70
+ box-shadow: var(--ds-shadow-sm);
71
+ }
72
+ .section[data-shadow="md"] {
73
+ box-shadow: var(--ds-shadow-md);
74
+ }
75
+
76
+ .section[data-shadow="lg"] {
77
+ box-shadow: var(--ds-shadow-lg);
78
+ }
79
+
80
+ .section[data-shadow="xl"] {
81
+ box-shadow: var(--ds-shadow-xl);
82
+ }
@@ -0,0 +1,8 @@
1
+ .footer {
2
+ display: flex;
3
+ justify-content: space-between;
4
+ }
5
+
6
+ .header[data-margin="true"] {
7
+ margin: 0.5rem 0;
8
+ }
@@ -0,0 +1,9 @@
1
+ .header {
2
+ display: flex;
3
+ align-items: center;
4
+ justify-content: space-between;
5
+ }
6
+
7
+ .header[data-margin="true"] {
8
+ margin: 0.5rem 0;
9
+ }
@@ -1,19 +1,55 @@
1
- import { type ReactNode, createContext } from 'react';
1
+ import { type ReactNode, createContext, useContext, useState } from 'react';
2
+
3
+ type OpenElementId = 'search' | 'menu' | string;
2
4
 
3
5
  interface RootContextProvider {
4
- showBackdrop: boolean;
6
+ currentId: OpenElementId;
7
+ toggleId: (elementId: OpenElementId) => void;
8
+ closeAll: () => void;
9
+ openId: (elementId: OpenElementId) => void;
5
10
  }
6
11
 
7
12
  const initialValue = {
8
- showBackdrop: false,
13
+ currentId: '',
9
14
  };
10
- const RootContext = createContext<RootContextProvider>(initialValue);
15
+
16
+ interface RootContextInitialValue {
17
+ currentId: OpenElementId;
18
+ setCurrentId?: (elementId: OpenElementId) => void;
19
+ }
20
+
21
+ const RootContext = createContext<RootContextInitialValue>(initialValue);
11
22
 
12
23
  interface ProviderProps {
13
24
  children: ReactNode;
14
- value?: RootContextProvider;
25
+ initialValue?: RootContextInitialValue;
15
26
  }
16
27
 
17
- export const RootProvider = ({ children, value }: ProviderProps) => {
18
- return <RootContext.Provider value={value ?? initialValue}>{children}</RootContext.Provider>;
28
+ export const RootProvider = ({ children, initialValue }: ProviderProps) => {
29
+ const [currentId, setCurrentId] = useState<OpenElementId>(initialValue?.currentId || '');
30
+ return (
31
+ <RootContext.Provider
32
+ value={{
33
+ currentId,
34
+ setCurrentId,
35
+ }}
36
+ >
37
+ {children}
38
+ </RootContext.Provider>
39
+ );
40
+ };
41
+
42
+ export const useRootContext = (): RootContextProvider => {
43
+ const { currentId, setCurrentId } = useContext(RootContext);
44
+ const toggleId = (elementId: OpenElementId) => setCurrentId!(currentId === elementId ? '' : elementId);
45
+ const closeAll = () => {
46
+ setCurrentId!('');
47
+ };
48
+ const openId = (elementId: OpenElementId) => setCurrentId!(elementId);
49
+ return {
50
+ currentId,
51
+ toggleId,
52
+ closeAll,
53
+ openId,
54
+ };
19
55
  };
@@ -0,0 +1,77 @@
1
+ import type { Meta, StoryObj } from '@storybook/react';
2
+ import { Autocomplete } from './Autocomplete';
3
+
4
+ const meta = {
5
+ title: 'Header/Autocomplete',
6
+ component: Autocomplete,
7
+ tags: ['autodocs'],
8
+ parameters: {},
9
+ args: {},
10
+ } satisfies Meta<typeof Autocomplete>;
11
+
12
+ export default meta;
13
+ type Story = StoryObj<typeof meta>;
14
+
15
+ export const Scopes: Story = {
16
+ args: {
17
+ groups: {},
18
+ items: [
19
+ {
20
+ id: '1a',
21
+ groupId: '1',
22
+ href: '#',
23
+ label: 'Alt i innboks',
24
+ },
25
+ {
26
+ id: '1b',
27
+ groupId: '1',
28
+ href: '#',
29
+ label: 'Alt i hele Altinn',
30
+ },
31
+ ],
32
+ },
33
+ };
34
+
35
+ export const ScopesAndSuggestions: Story = {
36
+ args: {
37
+ groups: {
38
+ '2': {
39
+ title: '2 treff i innboksen',
40
+ },
41
+ },
42
+ items: [
43
+ {
44
+ id: '1a',
45
+ groupId: '1',
46
+ href: '#',
47
+ label: () => (
48
+ <span>
49
+ <mark>skatt</mark> i innboks
50
+ </span>
51
+ ),
52
+ },
53
+ {
54
+ id: '1b',
55
+ groupId: '1',
56
+ href: '#',
57
+ label: () => (
58
+ <span>
59
+ <mark>skatt</mark> i hele Altinn
60
+ </span>
61
+ ),
62
+ },
63
+ {
64
+ id: '2a',
65
+ groupId: '2',
66
+ href: '#',
67
+ label: 'Skattemeldingen 2023',
68
+ },
69
+ {
70
+ id: '2b',
71
+ groupId: '2',
72
+ href: '#',
73
+ label: 'Skattemeldingen 2022',
74
+ },
75
+ ],
76
+ },
77
+ };
@@ -0,0 +1,44 @@
1
+ import { useMenu } from '../../hooks';
2
+ import { AutocompleteBase } from './AutocompleteBase';
3
+ import { AutocompleteGroup, type AutocompleteGroupProps } from './AutocompleteGroup';
4
+ import { AutocompleteItem, type AutocompleteItemProps } from './AutocompleteItem';
5
+
6
+ export interface AutocompleteProps {
7
+ items: AutocompleteItemProps[];
8
+ groups?: Record<string, AutocompleteGroupProps>;
9
+ expanded?: boolean;
10
+ className?: string;
11
+ }
12
+
13
+ export const Autocomplete = ({ className, items, groups = {}, expanded }: AutocompleteProps) => {
14
+ const { menu, setActiveIndex } = useMenu<AutocompleteItemProps, AutocompleteGroupProps>({
15
+ items,
16
+ groups,
17
+ groupByKey: 'groupId',
18
+ keyboardEvents: true,
19
+ });
20
+ return (
21
+ <AutocompleteBase className={className} expanded={expanded}>
22
+ {menu.map((group, index) => {
23
+ return (
24
+ <AutocompleteGroup {...group.props} key={index}>
25
+ <ul>
26
+ {group.items.map((item, index) => {
27
+ const {
28
+ active,
29
+ menuIndex,
30
+ props: { groupId, ...itemProps },
31
+ } = item;
32
+ return (
33
+ <li key={index} tabIndex={-1} onMouseEnter={() => setActiveIndex(menuIndex)}>
34
+ <AutocompleteItem {...itemProps} active={active} />
35
+ </li>
36
+ );
37
+ })}
38
+ </ul>
39
+ </AutocompleteGroup>
40
+ );
41
+ })}
42
+ </AutocompleteBase>
43
+ );
44
+ };
@@ -0,0 +1,16 @@
1
+ import cx from 'classnames';
2
+ import type { ReactNode } from 'react';
3
+ import styles from './autocompleteBase.module.css';
4
+ export interface AutocompleteBaseProps {
5
+ className?: string;
6
+ expanded?: boolean;
7
+ children?: ReactNode;
8
+ }
9
+
10
+ export const AutocompleteBase = ({ expanded, children, className }: AutocompleteBaseProps) => {
11
+ return (
12
+ <nav className={cx(styles.autocomplete, className)} aria-expanded={expanded}>
13
+ {children}
14
+ </nav>
15
+ );
16
+ };
@@ -0,0 +1,17 @@
1
+ import type { ReactNode } from 'react';
2
+ import { MenuHeader } from '../Menu';
3
+ import styles from './autocompleteGroup.module.css';
4
+
5
+ export interface AutocompleteGroupProps {
6
+ title?: string;
7
+ children?: ReactNode;
8
+ }
9
+
10
+ export const AutocompleteGroup = ({ title, children }: AutocompleteGroupProps) => {
11
+ return (
12
+ <section className={styles.group}>
13
+ {title && <MenuHeader title={title} />}
14
+ {children}
15
+ </section>
16
+ );
17
+ };
@@ -0,0 +1,23 @@
1
+ import type { ElementType, ReactNode } from 'react';
2
+ import { ListItemBase, ListItemLabel } from '../List/';
3
+ import styles from './autocompleteItem.module.css';
4
+
5
+ export interface AutocompleteItemProps {
6
+ as?: ElementType;
7
+ href?: string;
8
+ onClick?: () => void;
9
+ loading?: boolean;
10
+ active?: boolean;
11
+ disabled?: boolean;
12
+ label?: string | (() => ReactNode);
13
+ groupId?: string;
14
+ style?: React.CSSProperties;
15
+ }
16
+
17
+ export const AutocompleteItem = ({ as = 'a', label, active, ...rest }: AutocompleteItemProps) => {
18
+ return (
19
+ <ListItemBase className={styles.item} as={as} size="sm" active={active} linkIcon="chevron-right" {...rest}>
20
+ <ListItemLabel size="sm">{typeof label === 'function' ? label() : label}</ListItemLabel>
21
+ </ListItemBase>
22
+ );
23
+ };
@@ -0,0 +1,78 @@
1
+ import cx from 'classnames';
2
+ import { type ChangeEventHandler, type FocusEventHandler, useRef } from 'react';
3
+ import { IconButton } from '../Button';
4
+ import { Icon } from '../Icon';
5
+ import styles from './searchField.module.css';
6
+
7
+ export interface SearchFieldProps {
8
+ name: string;
9
+ // TODO: Should be required?
10
+ value?: string;
11
+ className?: string;
12
+ expanded?: boolean;
13
+ placeholder?: string;
14
+ onFocus?: FocusEventHandler;
15
+ onBlur?: FocusEventHandler;
16
+ onChange?: ChangeEventHandler;
17
+ onClear?: () => void;
18
+ onClose?: () => void;
19
+ onEnter?: () => void;
20
+ }
21
+
22
+ export const SearchField = ({
23
+ className,
24
+ expanded,
25
+ name = 'q',
26
+ value,
27
+ placeholder = 'Søk',
28
+ onFocus,
29
+ onBlur,
30
+ onChange,
31
+ onClear,
32
+ onClose,
33
+ onEnter,
34
+ }: SearchFieldProps) => {
35
+ const ref = useRef<HTMLInputElement>(null);
36
+
37
+ const handleOnKeyUp = (event: React.KeyboardEvent<HTMLInputElement>) => {
38
+ if (event.key === 'Escape') {
39
+ ref.current?.blur();
40
+ }
41
+ if (event.key === 'Enter') {
42
+ onEnter?.();
43
+ }
44
+ };
45
+
46
+ return (
47
+ <div className={cx(styles.field, className)} aria-expanded={expanded}>
48
+ <input
49
+ ref={ref}
50
+ onFocus={onFocus}
51
+ onBlur={onBlur}
52
+ name={name}
53
+ value={value}
54
+ onChange={onChange}
55
+ placeholder={placeholder}
56
+ className={styles.input}
57
+ onKeyUp={handleOnKeyUp}
58
+ type="search"
59
+ aria-haspopup
60
+ autoComplete="off"
61
+ aria-autocomplete="list"
62
+ aria-expanded={expanded}
63
+ />
64
+ <Icon name="magnifying-glass" className={styles.icon} />
65
+ {(value && (
66
+ <IconButton
67
+ icon="x-mark"
68
+ color="secondary"
69
+ variant="solid"
70
+ size="custom"
71
+ className={styles.clear}
72
+ onClick={onClear}
73
+ />
74
+ )) ||
75
+ (expanded && <IconButton icon="x-mark" variant="text" className={styles.dismiss} onClick={onClose} />)}
76
+ </div>
77
+ );
78
+ };