@altinn/altinn-components 0.2.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (85) hide show
  1. package/.github/workflows/ci-cd-main.yml +5 -2
  2. package/.github/workflows/workflow-deploy-storybook.yml +35 -0
  3. package/.storybook/StoryDecorator.tsx +10 -8
  4. package/CHANGELOG.md +14 -0
  5. package/README.md +20 -2
  6. package/lib/components/Attachment/AttachmentLink.stories.ts +0 -2
  7. package/lib/components/Avatar/avatar.stories.tsx +1 -0
  8. package/lib/components/Button/IconButton.tsx +21 -0
  9. package/lib/components/Button/buttonBase.module.css +17 -2
  10. package/lib/components/Button/iconButton.module.css +14 -0
  11. package/lib/components/Button/index.ts +1 -0
  12. package/lib/components/ContextMenu/ContextMenu.tsx +2 -2
  13. package/lib/components/Dialog/Dialog.stories.ts +6 -2
  14. package/lib/components/Dialog/Dialog.tsx +13 -5
  15. package/lib/components/Dialog/DialogAttachments.stories.ts +0 -2
  16. package/lib/components/Dialog/DialogContent.tsx +0 -1
  17. package/lib/components/Dialog/DialogHeader.stories.ts +0 -2
  18. package/lib/components/Dialog/DialogListItem.tsx +3 -0
  19. package/lib/components/Dialog/DialogMetadata.stories.ts +0 -2
  20. package/lib/components/Dialog/DialogNav.stories.ts +0 -2
  21. package/lib/components/Dialog/DialogNav.tsx +6 -6
  22. package/lib/components/Dialog/DialogSeenBy.stories.tsx +5 -7
  23. package/lib/components/Dialog/DialogTitle.stories.ts +0 -2
  24. package/lib/components/Dialog/dialogListItemBase.module.css +1 -1
  25. package/lib/components/Dropdown/Backdrop.tsx +11 -0
  26. package/lib/components/Dropdown/DrawerBase.tsx +17 -0
  27. package/lib/components/Dropdown/DropdownBase.tsx +17 -0
  28. package/lib/components/Dropdown/backdrop.module.css +8 -0
  29. package/lib/components/Dropdown/drawerBase.module.css +8 -0
  30. package/lib/components/Dropdown/dropdownBase.module.css +18 -0
  31. package/lib/components/Dropdown/index.ts +3 -0
  32. package/lib/components/{Header → GlobalMenu}/GlobalMenu.stories.tsx +1 -13
  33. package/lib/components/{Header → GlobalMenu}/GlobalMenu.tsx +14 -23
  34. package/lib/components/GlobalMenu/index.tsx +1 -0
  35. package/lib/components/Header/Header.stories.ts +61 -33
  36. package/lib/components/Header/Header.tsx +43 -17
  37. package/lib/components/Header/HeaderBase.tsx +10 -3
  38. package/lib/components/Header/HeaderButton.stories.ts +16 -0
  39. package/lib/components/Header/HeaderButton.tsx +12 -0
  40. package/lib/components/Header/HeaderMenu.tsx +17 -0
  41. package/lib/components/Header/header.module.css +51 -13
  42. package/lib/components/Header/headerBase.module.css +8 -0
  43. package/lib/components/Header/headerButton.module.css +1 -0
  44. package/lib/components/Header/headerMenu.module.css +3 -0
  45. package/lib/components/Header/index.tsx +0 -1
  46. package/lib/components/Layout/Layout.stories.tsx +236 -0
  47. package/lib/components/Layout/Layout.tsx +9 -7
  48. package/lib/components/Layout/LayoutBase.tsx +1 -1
  49. package/lib/components/Layout/LayoutBody.tsx +1 -1
  50. package/lib/components/Layout/LayoutContent.tsx +1 -1
  51. package/lib/components/Layout/LayoutSidebar.tsx +11 -4
  52. package/lib/components/Layout/layoutBase.module.css +23 -0
  53. package/lib/components/Layout/layoutBody.module.css +14 -0
  54. package/lib/components/Layout/layoutContent.module.css +8 -0
  55. package/lib/components/Layout/layoutSidebar.module.css +19 -0
  56. package/lib/components/LayoutAction/ActionFooter.stories.tsx +70 -0
  57. package/lib/components/LayoutAction/ActionFooter.tsx +15 -0
  58. package/lib/components/LayoutAction/ActionHeader.stories.ts +20 -0
  59. package/lib/components/LayoutAction/ActionHeader.tsx +19 -0
  60. package/lib/components/LayoutAction/ActionMenu.stories.tsx +39 -0
  61. package/lib/components/LayoutAction/ActionMenu.tsx +22 -0
  62. package/lib/components/LayoutAction/actionFooter.module.css +28 -0
  63. package/lib/components/LayoutAction/actionHeader.module.css +55 -0
  64. package/lib/components/LayoutAction/actionMenu.module.css +25 -0
  65. package/lib/components/LayoutAction/index.ts +3 -0
  66. package/lib/components/Menu/MenuBase.tsx +4 -2
  67. package/lib/components/Menu/MenuItemBase.tsx +5 -3
  68. package/lib/components/RootProvider/RootProvider.tsx +19 -0
  69. package/lib/components/RootProvider/index.ts +1 -0
  70. package/lib/components/Snackbar/Snackbar.stories.tsx +21 -0
  71. package/lib/components/Snackbar/Snackbar.tsx +32 -0
  72. package/lib/components/Snackbar/SnackbarBase.tsx +39 -0
  73. package/lib/components/Snackbar/SnackbarLabel.tsx +10 -0
  74. package/lib/components/Snackbar/SnackbarMedia.tsx +14 -0
  75. package/lib/components/Snackbar/index.ts +4 -0
  76. package/lib/components/Snackbar/snackbarBase.module.css +55 -0
  77. package/lib/components/Snackbar/snackbarLabel.module.css +6 -0
  78. package/lib/components/Snackbar/snackbarMedia.module.css +10 -0
  79. package/lib/components/Toolbar/ToolbarAdd.stories.ts +0 -2
  80. package/lib/components/Toolbar/ToolbarFilter.stories.ts +0 -2
  81. package/lib/components/index.ts +4 -0
  82. package/package.json +1 -1
  83. package/lib/components/Layout/Layout.stories.ts +0 -124
  84. package/lib/components/Layout/layout.module.css +0 -63
  85. /package/lib/components/Header/{globalMenu.module.css → mobileMenu.module.css} +0 -0
@@ -1,10 +1,7 @@
1
1
  'use client';
2
- import cx from 'classnames';
3
2
  import { type MouseEventHandler, useState } from 'react';
4
3
  import type { AvatarType } from '../Avatar';
5
4
  import { Menu, type MenuGroups, MenuItem, type MenuItemProps, type MenuSearchProps } from '../Menu';
6
- import { HeaderButton } from './HeaderButton';
7
- import styles from './globalMenu.module.css';
8
5
 
9
6
  export type Account = {
10
7
  type: AvatarType;
@@ -18,7 +15,10 @@ export interface AccountSearch extends MenuSearchProps {
18
15
  hidden?: boolean;
19
16
  }
20
17
 
18
+ export type MobileMenuType = 'dropdown' | 'drawer';
19
+
21
20
  export interface GlobalMenuProps {
21
+ variant: MobileMenuType;
22
22
  expanded: boolean;
23
23
  onToggle: MouseEventHandler;
24
24
  items: MenuItemProps[];
@@ -34,15 +34,11 @@ export interface GlobalMenuProps {
34
34
  const defaultResultLabel = (hits: number) => `${hits} hits`;
35
35
 
36
36
  export const GlobalMenu = ({
37
- className,
38
- expanded,
39
- onToggle,
40
37
  accounts = [],
41
38
  accountGroups = {},
42
39
  accountSearch,
43
40
  items = [],
44
41
  groups,
45
- menuLabel = 'Menu',
46
42
  backLabel = 'Back',
47
43
  }: GlobalMenuProps) => {
48
44
  const accountMenu: MenuItemProps[] = accounts.map((account) => ({
@@ -72,7 +68,7 @@ export const GlobalMenu = ({
72
68
  onClick: onToggleAccounts,
73
69
  };
74
70
 
75
- const globalMenu = selectedAccount ? [accountMenuItem, ...items] : items;
71
+ const MobileMenu = selectedAccount ? [accountMenuItem, ...items] : items;
76
72
 
77
73
  const filteredAccountMenu = filterString
78
74
  ? accountMenu
@@ -113,19 +109,14 @@ export const GlobalMenu = ({
113
109
  ...(filteredAccountMenu.length > 0 ? filteredAccountMenu : [{ id: 'search', group: 'search', hidden: true }]),
114
110
  ];
115
111
 
116
- return (
117
- <div className={cx(styles.button, className)}>
118
- <HeaderButton as="div" avatar={accountMenuItem.avatar} onClick={onToggle} expanded={expanded} label={menuLabel} />
119
- <div className={styles.dropdown} aria-expanded={expanded}>
120
- {selectAccount ? (
121
- <>
122
- <MenuItem {...backItem} />
123
- <Menu theme="global" search={accountSearchItem} groups={filterAccountGroups} items={accountSwitcher} />
124
- </>
125
- ) : (
126
- <Menu theme="global" groups={groups} items={globalMenu} />
127
- )}
128
- </div>
129
- </div>
130
- );
112
+ if (selectAccount) {
113
+ return (
114
+ <>
115
+ <MenuItem {...backItem} />
116
+ <Menu theme="global" search={accountSearchItem} groups={filterAccountGroups} items={accountSwitcher} />
117
+ </>
118
+ );
119
+ }
120
+
121
+ return <Menu theme="global" groups={groups} items={MobileMenu} />;
131
122
  };
@@ -0,0 +1 @@
1
+ export * from './GlobalMenu.tsx';
@@ -6,39 +6,79 @@ const meta = {
6
6
  component: Header,
7
7
  tags: ['autodocs'],
8
8
  parameters: {},
9
- args: {},
10
- } satisfies Meta<typeof Header>;
11
-
12
- export default meta;
13
- type Story = StoryObj<typeof meta>;
14
-
15
- export const Default: Story = {
16
- args: {},
17
- };
18
-
19
- export const Person: Story = {
20
9
  args: {
21
10
  expanded: true,
22
- globalMenu: {
11
+ search: {
12
+ placeholder: 'Søk i Altinn',
13
+ },
14
+ menu: {
15
+ accountGroups: {
16
+ primary: {
17
+ title: 'Deg selv og favoritter',
18
+ },
19
+ secondary: {
20
+ title: 'Andre kontoer',
21
+ },
22
+ },
23
23
  accounts: [
24
24
  {
25
+ group: 'primary',
25
26
  type: 'person',
26
27
  name: 'Aurora Mikalsen',
27
28
  selected: true,
28
29
  },
30
+ {
31
+ group: 'favourites',
32
+ type: 'person',
33
+ name: 'Rakel Engelsvik',
34
+ selected: false,
35
+ },
36
+ {
37
+ group: 'favourites',
38
+ type: 'company',
39
+ name: 'Auroras keeperskole',
40
+ selected: false,
41
+ },
42
+ {
43
+ group: 'secondary',
44
+ type: 'company',
45
+ name: 'Keeperhansker AS',
46
+ selected: false,
47
+ },
48
+ {
49
+ group: 'secondary',
50
+ type: 'company',
51
+ name: 'Stadion drift AS',
52
+ selected: false,
53
+ },
54
+ {
55
+ group: 'secondary',
56
+ type: 'company',
57
+ name: 'Sportsklubben Brann',
58
+ selected: false,
59
+ },
60
+ {
61
+ group: 'secondary',
62
+ type: 'company',
63
+ name: 'Landslaget',
64
+ selected: false,
65
+ },
29
66
  ],
30
- menu: [
67
+ items: [
31
68
  {
69
+ id: '1',
32
70
  icon: 'airplane',
33
71
  size: 'lg',
34
72
  label: 'Section 1',
35
73
  },
36
74
  {
75
+ id: '2',
37
76
  icon: 'briefcase',
38
77
  size: 'lg',
39
78
  label: 'Section 2',
40
79
  },
41
80
  {
81
+ id: '3',
42
82
  size: 'lg',
43
83
  label: 'Section 3',
44
84
  icon: 'camera',
@@ -46,29 +86,17 @@ export const Person: Story = {
46
86
  ],
47
87
  },
48
88
  },
49
- };
89
+ } satisfies Meta<typeof Header>;
50
90
 
51
- export const Company: Story = {
52
- args: {
53
- globalMenu: {
54
- accounts: [
55
- {
56
- type: 'company',
57
- name: 'Bergen bar',
58
- selected: true,
59
- },
60
- {
61
- type: 'person',
62
- name: 'Aurora Mikalsen',
63
- },
64
- ],
65
- },
66
- },
67
- };
91
+ export default meta;
92
+ type Story = StoryObj<typeof meta>;
93
+
94
+ export const Default: Story = {};
68
95
 
69
- export const CompanyAndMenu: Story = {
96
+ export const Company: Story = {
70
97
  args: {
71
- globalMenu: {
98
+ menu: {
99
+ ...meta.args?.menu,
72
100
  accounts: [
73
101
  {
74
102
  type: 'company',
@@ -1,29 +1,36 @@
1
1
  'use client';
2
+ import cx from 'classnames';
2
3
  import { useState } from 'react';
3
- import { GlobalMenu, type GlobalMenuProps } from './GlobalMenu';
4
+ import { Backdrop, DrawerBase, DropdownBase } from '../Dropdown';
5
+ import { GlobalMenu, type GlobalMenuProps } from '../GlobalMenu';
6
+ import { HeaderBase } from './HeaderBase';
7
+ import { HeaderButton } from './HeaderButton';
4
8
  import { HeaderLogo } from './HeaderLogo';
9
+ import { HeaderMenu } from './HeaderMenu';
5
10
  import { HeaderSearch, type HeaderSearchProps } from './HeaderSearch';
6
11
  import styles from './header.module.css';
7
12
 
8
- export type HeaderColor = 'default' | 'dark' | 'light';
13
+ export type HeaderExpandedType = 'search' | 'menu' | null;
9
14
 
10
15
  export interface HeaderAccountProps {
11
16
  type?: string;
12
17
  name?: string;
13
18
  }
14
19
 
15
- type ExpandedType = 'search' | 'account';
16
-
17
20
  export interface HeaderProps {
18
- color?: HeaderColor;
19
21
  menu: GlobalMenuProps;
20
22
  search?: HeaderSearchProps;
21
23
  }
22
24
 
23
- export const Header = ({ color, search, menu }: HeaderProps) => {
24
- const [expandedType, setExpandedType] = useState<ExpandedType | null>(null);
25
+ export const Header = ({ search, menu }: HeaderProps) => {
26
+ const [expandedType, setExpandedType] = useState<HeaderExpandedType | null>(null);
27
+ const selectedAccount = menu?.accounts?.find((account) => account.selected);
28
+ const selectedAvatar = selectedAccount && {
29
+ type: selectedAccount.type,
30
+ name: selectedAccount.name,
31
+ };
25
32
 
26
- const onToggle = (type: ExpandedType) => {
33
+ const onToggle = (type: HeaderExpandedType) => {
27
34
  if (expandedType === type) {
28
35
  setExpandedType(null);
29
36
  } else {
@@ -40,16 +47,30 @@ export const Header = ({ color, search, menu }: HeaderProps) => {
40
47
  };
41
48
 
42
49
  return (
43
- <header className={styles.header} data-color={color}>
50
+ <HeaderBase
51
+ className={cx(
52
+ styles.header,
53
+ expandedType === 'menu' && styles.menuExpanded,
54
+ expandedType === 'search' && styles.searchExpanded,
55
+ )}
56
+ expanded={(expandedType && true) || false}
57
+ >
58
+ <Backdrop className={styles.backdrop} />
44
59
  <HeaderLogo className={styles.logo} />
45
- {menu && (
46
- <GlobalMenu
47
- {...menu}
48
- expanded={expandedType === 'account'}
49
- onToggle={() => onToggle('account')}
50
- className={styles.button}
60
+ <HeaderMenu className={styles.menu}>
61
+ <HeaderButton
62
+ avatar={selectedAvatar}
63
+ onClick={() => onToggle('menu')}
64
+ expanded={expandedType === 'menu'}
65
+ label={menu?.menuLabel}
51
66
  />
52
- )}
67
+ {menu && (
68
+ <DropdownBase expanded={expandedType === 'menu'} className={styles.dropdown}>
69
+ <GlobalMenu {...menu} variant="dropdown" />
70
+ </DropdownBase>
71
+ )}
72
+ </HeaderMenu>
73
+
53
74
  {search && (
54
75
  <HeaderSearch
55
76
  {...search}
@@ -59,6 +80,11 @@ export const Header = ({ color, search, menu }: HeaderProps) => {
59
80
  onFocus={onSearchFocus}
60
81
  />
61
82
  )}
62
- </header>
83
+ {menu && (
84
+ <DrawerBase expanded={expandedType === 'menu'} className={styles.drawer}>
85
+ <GlobalMenu {...menu} variant="drawer" expanded={expandedType === 'menu'} />
86
+ </DrawerBase>
87
+ )}
88
+ </HeaderBase>
63
89
  );
64
90
  };
@@ -1,10 +1,17 @@
1
+ import cx from 'classnames';
1
2
  import type { ReactNode } from 'react';
2
- import styles from './header.module.css';
3
+ import styles from './headerBase.module.css';
3
4
 
4
5
  export interface HeaderBaseProps {
6
+ expanded?: boolean;
7
+ className?: string;
5
8
  children?: ReactNode;
6
9
  }
7
10
 
8
- export const HeaderBase = ({ children }: HeaderBaseProps) => {
9
- return <header className={styles.header}>{children}</header>;
11
+ export const HeaderBase = ({ expanded, className, children }: HeaderBaseProps) => {
12
+ return (
13
+ <header className={cx(styles.header, className)} aria-expanded={expanded}>
14
+ {children}
15
+ </header>
16
+ );
10
17
  };
@@ -43,6 +43,22 @@ export const Company: Story = {
43
43
  },
44
44
  };
45
45
 
46
+ export const CompanyGroup: Story = {
47
+ args: {
48
+ avatarGroup: {
49
+ defaultType: 'company',
50
+ items: [
51
+ {
52
+ name: 'Bergen bar',
53
+ },
54
+ {
55
+ name: 'Sportsklubben Brann',
56
+ },
57
+ ],
58
+ },
59
+ },
60
+ };
61
+
46
62
  export const Expanded: Story = {
47
63
  args: {
48
64
  expanded: true,
@@ -1,6 +1,7 @@
1
1
  import cx from 'classnames';
2
2
  import type { ElementType } from 'react';
3
3
  import { Avatar, type AvatarProps } from '../Avatar';
4
+ import { AvatarGroup, type AvatarGroupProps } from '../Avatar';
4
5
  import { ButtonBase, type ButtonProps } from '../Button';
5
6
  import { Icon, type IconName } from '../Icon';
6
7
 
@@ -9,6 +10,7 @@ import styles from './headerButton.module.css';
9
10
  export interface HeaderButtonProps extends ButtonProps {
10
11
  label?: string;
11
12
  avatar?: AvatarProps;
13
+ avatarGroup?: AvatarGroupProps;
12
14
  as?: ElementType;
13
15
  className?: string;
14
16
  expanded?: boolean;
@@ -19,6 +21,7 @@ export const HeaderButton = ({
19
21
  className,
20
22
  as = 'button',
21
23
  avatar,
24
+ avatarGroup,
22
25
  icon = 'padlock-locked',
23
26
  expanded,
24
27
  label = 'Menu',
@@ -35,6 +38,15 @@ export const HeaderButton = ({
35
38
  );
36
39
  }
37
40
 
41
+ if (avatarGroup) {
42
+ return (
43
+ <ButtonBase {...buttonProps} as={as} className={cx(styles.button, className)}>
44
+ <span className={styles.label}>{label}</span>
45
+ <AvatarGroup {...avatarGroup} size="sm" />
46
+ </ButtonBase>
47
+ );
48
+ }
49
+
38
50
  if (avatar) {
39
51
  return (
40
52
  <ButtonBase {...buttonProps} as={as} className={cx(styles.button, className)}>
@@ -0,0 +1,17 @@
1
+ import cx from 'classnames';
2
+ import type { ReactNode } from 'react';
3
+ import styles from './headerMenu.module.css';
4
+
5
+ export interface HeaderMenuProps {
6
+ className?: string;
7
+ expanded?: boolean;
8
+ children?: ReactNode;
9
+ }
10
+
11
+ export const HeaderMenu = ({ expanded = false, className, children }: HeaderMenuProps) => {
12
+ return (
13
+ <div className={cx(styles.menu, className)} aria-expanded={expanded}>
14
+ {children}
15
+ </div>
16
+ );
17
+ };
@@ -1,21 +1,35 @@
1
- .header {
2
- display: flex;
3
- align-items: center;
4
- justify-content: space-between;
5
- flex-wrap: wrap;
6
- gap: 1rem;
7
- padding: 1rem;
1
+ .backdrop {
2
+ display: none;
3
+ z-index: 2;
8
4
  }
9
5
 
10
- /* color */
6
+ .header .drawer[aria-expanded="true"] {
7
+ display: block;
8
+ }
11
9
 
12
- .header[data-color="light"] {
13
- background-color: var(--global-background-default);
10
+ .header .dropdown[aria-expanded="true"] {
11
+ width: 320px;
12
+ display: none;
14
13
  }
15
14
 
16
- .header[data-color="dark"] {
17
- background-color: var(--global-base-default);
18
- color: #fff;
15
+ /* menu */
16
+
17
+ .header.menuExpanded {
18
+ background-color: white;
19
+ }
20
+
21
+ @media (min-width: 1024px) {
22
+ .header.menuExpanded {
23
+ background-color: transparent;
24
+ }
25
+
26
+ .header.menuExpanded .backdrop {
27
+ display: block;
28
+ }
29
+
30
+ .header.menuExpanded .menu {
31
+ z-index: 2;
32
+ }
19
33
  }
20
34
 
21
35
  /* search */
@@ -24,7 +38,31 @@
24
38
  width: 100%;
25
39
  }
26
40
 
41
+ .header.searchExpanded {
42
+ margin-top: -72px;
43
+ }
44
+
45
+ .header.searchExpanded .search {
46
+ z-index: 2;
47
+ }
48
+
49
+ .header.searchExpanded .backdrop {
50
+ display: block;
51
+ }
52
+
27
53
  @media (min-width: 1024px) {
54
+ .header.searchExpanded {
55
+ margin-top: 0;
56
+ }
57
+
58
+ .header .drawer[aria-expanded="true"] {
59
+ display: none;
60
+ }
61
+
62
+ .header .dropdown[aria-expanded="true"] {
63
+ display: block;
64
+ }
65
+
28
66
  .search {
29
67
  position: absolute;
30
68
  left: 0;
@@ -0,0 +1,8 @@
1
+ .header {
2
+ display: flex;
3
+ align-items: center;
4
+ justify-content: space-between;
5
+ flex-wrap: wrap;
6
+ gap: 1rem;
7
+ padding: 1rem;
8
+ }
@@ -6,6 +6,7 @@
6
6
  background-color: var(--global-base-default);
7
7
  color: white;
8
8
  padding: 0.625rem;
9
+ border: none;
9
10
  border-radius: 4px;
10
11
  }
11
12
 
@@ -0,0 +1,3 @@
1
+ .menu {
2
+ position: relative;
3
+ }
@@ -2,4 +2,3 @@ export * from './Header';
2
2
  export * from './HeaderBase';
3
3
  export * from './HeaderLogo';
4
4
  export * from './HeaderButton';
5
- export * from './GlobalMenu.tsx';