agroptima-design-system 0.14.0 → 0.15.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agroptima-design-system",
3
- "version": "0.14.0",
3
+ "version": "0.15.0",
4
4
  "scripts": {
5
5
  "dev": "npm run storybook",
6
6
  "storybook": "storybook dev -p 6006 --ci",
@@ -1,4 +1,4 @@
1
- import { IconButton, IconButtonProps } from './IconButton'
1
+ import { IconButton, IconButtonProps } from './Button'
2
2
  import { Icon } from './Icon'
3
3
  import './Alert.scss'
4
4
  import { classNames } from '../utils/classNames'
@@ -0,0 +1,28 @@
1
+ import NextLink from 'next/link'
2
+
3
+ interface CommonProps {
4
+ disabled?: boolean
5
+ }
6
+
7
+ type HtmlButtonProps = React.ButtonHTMLAttributes<HTMLButtonElement>
8
+
9
+ type AnchorProps = React.AnchorHTMLAttributes<HTMLAnchorElement>
10
+
11
+ export type BaseButtonProps =
12
+ | (HtmlButtonProps & CommonProps)
13
+ | (AnchorProps & CommonProps)
14
+
15
+ const hasHref = (props: HtmlButtonProps | AnchorProps): props is AnchorProps =>
16
+ 'href' in props
17
+
18
+ export function BaseButton({ children, ...props }: BaseButtonProps) {
19
+ if (hasHref(props)) {
20
+ return (
21
+ <NextLink href={props.href || ''} {...props}>
22
+ {children}
23
+ </NextLink>
24
+ )
25
+ }
26
+
27
+ return <button {...(props as HtmlButtonProps)}>{children}</button>
28
+ }
@@ -1,6 +1,6 @@
1
- @use '../settings/color_alias';
2
- @use '../settings/typography/content' as typography;
3
- @use '../settings/config';
1
+ @use '../../settings/color_alias';
2
+ @use '../../settings/typography/content' as typography;
3
+ @use '../../settings/config';
4
4
 
5
5
  @mixin button-style($main-color, $secondary-color, $hover-color) {
6
6
  background: $main-color;
@@ -1,9 +1,9 @@
1
- import NextLink from 'next/link'
2
1
  import './Button.scss'
3
- import { Icon, IconType } from './Icon'
4
- import { classNames } from '../utils/classNames'
2
+ import { Icon, IconType } from '../Icon'
3
+ import { classNames } from '../../utils/classNames'
4
+ import { BaseButtonProps, BaseButton } from './BaseButton'
5
5
 
6
- export interface BaseButtonProps {
6
+ interface CustomProps {
7
7
  label: string
8
8
  accessibilityLabel?: string
9
9
  leftIcon?: IconType
@@ -13,16 +13,7 @@ export interface BaseButtonProps {
13
13
  disabled?: boolean
14
14
  }
15
15
 
16
- type HtmlButtonProps = React.ButtonHTMLAttributes<HTMLButtonElement>
17
-
18
- type AnchorProps = React.AnchorHTMLAttributes<HTMLAnchorElement>
19
-
20
- export type ButtonProps =
21
- | (HtmlButtonProps & BaseButtonProps)
22
- | (AnchorProps & BaseButtonProps)
23
-
24
- const hasHref = (props: HtmlButtonProps | AnchorProps): props is AnchorProps =>
25
- 'href' in props
16
+ export type ButtonProps = CustomProps & BaseButtonProps
26
17
 
27
18
  export type ButtonVariant =
28
19
  | 'primary'
@@ -59,23 +50,8 @@ export function Button({
59
50
  }
60
51
  const cssClasses = classNames('button', variant, props.className)
61
52
 
62
- if (hasHref(props)) {
63
- return (
64
- <NextLink
65
- href={props.href || ''}
66
- aria-label={accessibilityLabel || label}
67
- {...props}
68
- className={cssClasses}
69
- >
70
- {leftIcon && <Icon name={leftIcon} />}
71
- {label}
72
- {rightIcon && <Icon name={rightIcon} />}
73
- </NextLink>
74
- )
75
- }
76
-
77
53
  return (
78
- <button
54
+ <BaseButton
79
55
  disabled={loading || disabled}
80
56
  aria-label={accessibilityLabel || label}
81
57
  {...props}
@@ -84,6 +60,6 @@ export function Button({
84
60
  {leftIcon && <Icon name={leftIcon} />}
85
61
  {label}
86
62
  {rightIcon && <Icon name={rightIcon} />}
87
- </button>
63
+ </BaseButton>
88
64
  )
89
65
  }
@@ -0,0 +1,51 @@
1
+ @use '../../settings/color_alias';
2
+ @use '../../settings/config';
3
+
4
+ .floating-button {
5
+ display: inline-flex;
6
+ justify-content: center;
7
+ align-items: center;
8
+ flex-shrink: 0;
9
+ gap: config.$space-1x;
10
+ cursor: default;
11
+ height: fit-content;
12
+ padding: config.$space-5x;
13
+ border-radius: config.$corner-radius-m;
14
+ text-decoration: none;
15
+ border: 1px solid transparent;
16
+ border-radius: 50%;
17
+
18
+ > .icon {
19
+ width: config.$icon-size-5x;
20
+ height: config.$icon-size-5x;
21
+ > svg {
22
+ width: 100%;
23
+ height: 100%;
24
+ }
25
+ }
26
+
27
+ &.primary {
28
+ background: color_alias.$primary-color-600;
29
+
30
+ > .icon {
31
+ > svg {
32
+ fill: color_alias.$neutral-white;
33
+ path {
34
+ fill: color_alias.$neutral-white;
35
+ }
36
+ }
37
+ }
38
+
39
+ &:disabled {
40
+ background: color_alias.$neutral-color-400;
41
+ > .icon {
42
+ > svg {
43
+ fill: color_alias.$neutral-white;
44
+ path {
45
+ fill: color_alias.$neutral-white;
46
+ }
47
+ }
48
+ }
49
+ }
50
+ }
51
+ }
@@ -0,0 +1,36 @@
1
+ import './FloatingButton.scss'
2
+ import { Icon, IconType } from '../Icon'
3
+ import { classNames } from '../../utils/classNames'
4
+ import { BaseButtonProps, BaseButton } from './BaseButton'
5
+
6
+ export type Variant = 'primary'
7
+
8
+ interface CustomProps {
9
+ icon: IconType
10
+ variant?: Variant
11
+ disabled?: boolean
12
+ accessibilityLabel: string
13
+ }
14
+
15
+ export type FloatingButtonProps = CustomProps & BaseButtonProps
16
+
17
+ export function FloatingButton({
18
+ accessibilityLabel,
19
+ icon,
20
+ disabled,
21
+ variant = 'primary',
22
+ ...props
23
+ }: FloatingButtonProps) {
24
+ const cssClasses = classNames('floating-button', variant, props.className)
25
+
26
+ return (
27
+ <BaseButton
28
+ disabled={disabled}
29
+ aria-label={accessibilityLabel}
30
+ {...props}
31
+ className={cssClasses}
32
+ >
33
+ <Icon name={icon} />
34
+ </BaseButton>
35
+ )
36
+ }
@@ -1,10 +1,11 @@
1
- @use '../settings/color_alias';
2
- @use '../settings/config';
1
+ @use '../../settings/color_alias';
2
+ @use '../../settings/config';
3
3
 
4
4
  .icon-button {
5
5
  border: none;
6
6
  background: none;
7
7
  cursor: default;
8
+ padding: 0;
8
9
 
9
10
  > .icon {
10
11
  width: config.$icon-size-5x;
@@ -0,0 +1,36 @@
1
+ import './IconButton.scss'
2
+ import { Icon, IconType } from '../Icon'
3
+ import { classNames } from '../../utils/classNames'
4
+ import { BaseButtonProps, BaseButton } from './BaseButton'
5
+
6
+ export type Variant = 'primary'
7
+
8
+ interface CustomProps {
9
+ icon: IconType
10
+ variant?: Variant
11
+ disabled?: boolean
12
+ accessibilityLabel: string
13
+ }
14
+
15
+ export type IconButtonProps = CustomProps & BaseButtonProps
16
+
17
+ export function IconButton({
18
+ accessibilityLabel,
19
+ icon,
20
+ disabled,
21
+ variant = 'primary',
22
+ ...props
23
+ }: IconButtonProps) {
24
+ const cssClasses = classNames('icon-button', variant, props.className)
25
+
26
+ return (
27
+ <BaseButton
28
+ disabled={disabled}
29
+ aria-label={accessibilityLabel}
30
+ {...props}
31
+ className={cssClasses}
32
+ >
33
+ <Icon name={icon} />
34
+ </BaseButton>
35
+ )
36
+ }
@@ -0,0 +1,9 @@
1
+ import { Button } from './Button'
2
+ import { IconButton } from './IconButton'
3
+ import { FloatingButton } from './FloatingButton'
4
+
5
+ export type { ButtonProps } from './Button'
6
+ export type { IconButtonProps } from './IconButton'
7
+ export type { FloatingButtonProps } from './FloatingButton'
8
+
9
+ export { Button, IconButton, FloatingButton }
@@ -4,15 +4,19 @@
4
4
  @use '../../settings/depth';
5
5
 
6
6
  .menu {
7
+ @include typography.body-regular-primary;
7
8
  list-style-type: none;
8
9
  display: flex;
9
10
  flex-direction: column;
10
11
  padding: 0;
11
12
 
12
- .menu-option {
13
+ &-item {
13
14
  display: flex;
14
- flex-direction: column;
15
15
  gap: config.$space-2x;
16
+ padding: config.$space-3x;
17
+ text-decoration: none;
18
+ color: inherit;
19
+ align-items: center;
16
20
  cursor: default;
17
21
 
18
22
  .icon {
@@ -24,46 +28,10 @@
24
28
  }
25
29
  }
26
30
 
27
- details {
28
- &[open] {
29
- > .container .right .icon {
30
- transform: rotate(180deg);
31
- }
32
- }
33
- &:has(> .dropdown) {
34
- > .container .right {
35
- display: inline-block;
36
- }
37
-
38
- > .dropdown {
39
- > .menu-option details .container {
40
- padding-left: config.$space-8x;
41
- }
42
- }
43
- }
44
-
45
- .container {
46
- display: flex;
47
- padding: config.$space-3x;
48
-
49
- .left {
50
- display: flex;
51
- width: 100%;
52
- gap: config.$space-2x;
53
- justify-content: flex-start;
54
- align-items: baseline;
55
- }
56
-
57
- .right {
58
- display: none;
59
- margin-top: auto;
60
- margin-bottom: auto;
61
- }
62
- }
31
+ .title {
32
+ flex: 1;
63
33
  }
64
34
 
65
- @include typography.body-regular-primary;
66
-
67
35
  &.primary {
68
36
  color: color_alias.$neutral-white;
69
37
  background: color_alias.$neutral-color-900;
@@ -81,29 +49,30 @@
81
49
  background: color_alias.$primary-color-600;
82
50
  }
83
51
 
84
- &.selected {
52
+ &.active {
85
53
  background: color_alias.$primary-color-600;
86
54
  }
55
+ }
56
+ }
87
57
 
88
- details {
89
- &:has(> .dropdown) {
90
- > .dropdown {
91
- .menu-option {
92
- background: color_alias.$neutral-color-100;
93
- color: color_alias.$neutral-color-1000;
94
-
95
- &:hover {
96
- background: color_alias.$primary-color-100;
97
- }
98
- }
58
+ details[open] {
59
+ .arrow {
60
+ transform: rotate(180deg);
61
+ }
62
+ }
63
+ .menu-dropdown .menu {
64
+ .menu-item {
65
+ padding-left: config.$space-8x;
66
+ background: color_alias.$neutral-color-100;
67
+ color: color_alias.$neutral-color-1000;
99
68
 
100
- .selected {
101
- background: color_alias.$primary-color-100;
102
- box-shadow: inset -3px 0px 0px 0px color_alias.$primary-color-600;
103
- }
104
- }
105
- }
69
+ &:hover {
70
+ background: color_alias.$primary-color-100;
106
71
  }
107
72
  }
73
+ .active {
74
+ background: color_alias.$primary-color-100;
75
+ box-shadow: inset -3px 0px 0px 0px color_alias.$primary-color-600;
76
+ }
108
77
  }
109
78
  }
@@ -6,19 +6,15 @@ export type Variant = 'primary'
6
6
 
7
7
  export interface MenuProps extends React.ComponentPropsWithoutRef<'ul'> {
8
8
  variant?: Variant
9
- isDropdown?: boolean
10
9
  }
11
10
 
12
11
  export function Menu({
13
12
  variant = 'primary',
14
13
  className,
15
- isDropdown = false,
16
14
  children,
17
15
  ...props
18
16
  }: MenuProps): React.JSX.Element {
19
- const cssClasses = classNames('menu', variant, className, {
20
- dropdown: isDropdown,
21
- })
17
+ const cssClasses = classNames('menu', variant, className)
22
18
 
23
19
  return (
24
20
  <ul className={cssClasses} role="menu" {...props}>
@@ -0,0 +1,42 @@
1
+ import { classNames } from '../../utils/classNames'
2
+ import { Icon, IconType } from '../Icon'
3
+ import './Menu.scss'
4
+
5
+ export type Variant = 'primary'
6
+
7
+ export interface MenuDropdownProps
8
+ extends React.ComponentPropsWithoutRef<'li'> {
9
+ title: string
10
+ variant?: Variant
11
+ icon?: IconType
12
+ isOpen?: boolean
13
+ name?: string
14
+ }
15
+
16
+ export function MenuDropdown({
17
+ variant = 'primary',
18
+ className,
19
+ icon,
20
+ title,
21
+ children,
22
+ isOpen,
23
+ name,
24
+ ...props
25
+ }: MenuDropdownProps): React.JSX.Element {
26
+ const cssClasses = classNames('menu-item', variant, className)
27
+
28
+ return (
29
+ <li tabIndex={0} role="menuitem" className="menu-dropdown" {...props}>
30
+ <details open={isOpen} name={name}>
31
+ <summary className={cssClasses}>
32
+ {icon && <Icon name={icon} />}
33
+ <span className="title">{title}</span>
34
+ <Icon className="arrow" name="AngleDown" />
35
+ </summary>
36
+ <ul className="menu" role="menu">
37
+ {children}
38
+ </ul>
39
+ </details>
40
+ </li>
41
+ )
42
+ }
@@ -0,0 +1,39 @@
1
+ import './Menu.scss'
2
+ import React from 'react'
3
+ import { Icon, IconType } from '../Icon'
4
+ import { classNames } from '../../utils/classNames'
5
+ import Link from 'next/link'
6
+
7
+ export type Variant = 'primary'
8
+
9
+ export interface MenuLinkProps
10
+ extends React.AnchorHTMLAttributes<HTMLAnchorElement> {
11
+ title: string
12
+ variant?: Variant
13
+ icon?: IconType
14
+ isActive?: boolean
15
+ href: string
16
+ }
17
+
18
+ export function MenuLink({
19
+ variant = 'primary',
20
+ isActive = false,
21
+ className,
22
+ icon,
23
+ title,
24
+ href,
25
+ ...props
26
+ }: MenuLinkProps): React.JSX.Element {
27
+ const cssClasses = classNames('menu-item', variant, className, {
28
+ active: isActive,
29
+ })
30
+
31
+ return (
32
+ <li tabIndex={0} role="menuitem">
33
+ <Link href={href} {...props} className={cssClasses}>
34
+ {icon && <Icon name={icon} />}
35
+ <span className="title">{title}</span>
36
+ </Link>
37
+ </li>
38
+ )
39
+ }
@@ -1,4 +1,5 @@
1
1
  import { Menu } from './Menu'
2
- import { MenuOption } from './MenuOption'
2
+ import { MenuLink } from './MenuLink'
3
+ import { MenuDropdown } from './MenuDropdown'
3
4
 
4
- export { Menu, MenuOption }
5
+ export { Menu, MenuLink, MenuDropdown }
@@ -0,0 +1 @@
1
+ <svg width="20" height="20" fill="#444" xmlns="http://www.w3.org/2000/svg"><path d="M20 11.429h-8.571V20H8.57v-8.571H0V8.57h8.571V0h2.858v8.571H20v2.858Z" fill="#444"/><defs><clipPath id="add__a"><path fill="#fff" d="M0 0h20v20H0z"/></clipPath></defs></svg>
@@ -1,3 +1,4 @@
1
+ import Add from './add.svg'
1
2
  import AddCircle from './add-circle.svg'
2
3
  import AngleDown from './angle-down.svg'
3
4
  import AngleLeft from './angle-left.svg'
@@ -23,6 +24,7 @@ import Sorter from './sorter.svg'
23
24
  import Warning from './warning.svg'
24
25
 
25
26
  export {
27
+ Add,
26
28
  AddCircle,
27
29
  AngleDown,
28
30
  AngleLeft,
@@ -1,4 +1,4 @@
1
- import { IconButton } from '../atoms/IconButton'
1
+ import { IconButton } from '../atoms/Button'
2
2
  import { Card, CardHeader, CardContent, CardFooter } from '../atoms/Card'
3
3
  import { Button } from '../atoms/Button'
4
4
 
@@ -1,7 +1,7 @@
1
1
  import React from 'react'
2
2
 
3
3
  import { CardsTable, CardsTableHead, CardsTableHeader, CardsTableRow, CardsTableBody, CardsTableCell } from '../atoms/CardsTable'
4
- import { IconButton } from '../atoms/IconButton'
4
+ import { IconButton } from '../atoms/Button'
5
5
  import { Badge } from '../atoms/Badge'
6
6
 
7
7
  const figmaPrimaryDesign = {
@@ -3,6 +3,15 @@ import { Meta } from "@storybook/addon-docs";
3
3
  <Meta title="Changelog" />
4
4
  # Changelog
5
5
 
6
+ ## 0.15.0
7
+ - Added FloatingButton component to Storybook
8
+
9
+ BREAKING CHANGES
10
+ - All button components have been moved to a common folder
11
+
12
+ ## 0.14.1
13
+ - Added MenuLink and MenuDropdown components to Storybook and remove MenuOption component.
14
+
6
15
  ## 0.14.0
7
16
  - Added Menu component to Storybook.
8
17
 
@@ -0,0 +1,58 @@
1
+ import { StoryObj } from '@storybook/react'
2
+ import { FloatingButton } from '../atoms/Button'
3
+
4
+ const meta = {
5
+ title: 'Design System/Atoms/FloatingButton',
6
+ component: FloatingButton,
7
+ tags: ['autodocs'],
8
+ argTypes: {
9
+ accessibilityLabel: {
10
+ description: 'Accessible name & description of the element',
11
+ },
12
+ variant: {
13
+ description: 'Component variant used from a list of values',
14
+ },
15
+ disabled: {
16
+ description: 'Is the button in disabled state?',
17
+ },
18
+ icon: {
19
+ description: 'Icon from a list of values',
20
+ control: { type: 'select' },
21
+ },
22
+ href: {
23
+ description:
24
+ 'If a link is provided, the component will be rendered as NextLink, otherwise as button',
25
+ },
26
+ },
27
+ }
28
+
29
+ const figmaPrimaryDesign = {
30
+ design: {
31
+ type: 'figma',
32
+ url: 'https://www.figma.com/file/DN2ova21vWqCRvPspBXgI1/Design-System?type=design&node-id=1873-922&mode=dev',
33
+ },
34
+ }
35
+
36
+ export default meta
37
+ type Story = StoryObj<typeof meta>
38
+
39
+ export const Link: Story = {
40
+ args: {
41
+ icon: 'Add',
42
+ variant: 'primary',
43
+ accessibilityLabel: 'Edit game',
44
+ href: 'link.com',
45
+ disabled: false,
46
+ },
47
+ parameters: figmaPrimaryDesign,
48
+ }
49
+
50
+ export const Button: Story = {
51
+ args: {
52
+ icon: 'Add',
53
+ variant: 'primary',
54
+ accessibilityLabel: 'Delete game',
55
+ disabled: false,
56
+ },
57
+ parameters: figmaPrimaryDesign,
58
+ }
@@ -1,5 +1,5 @@
1
1
  import { StoryObj } from '@storybook/react'
2
- import { IconButton } from '../atoms/IconButton'
2
+ import { IconButton } from '../atoms/Button'
3
3
 
4
4
  const meta = {
5
5
  title: 'Design System/Atoms/IconButton',
@@ -1,6 +1,6 @@
1
1
  import React from 'react'
2
2
 
3
- import { Menu, MenuOption } from '../atoms/Menu'
3
+ import { Menu, MenuLink, MenuDropdown } from '../atoms/Menu'
4
4
 
5
5
  const figmaPrimaryDesign = {
6
6
  design: {
@@ -23,11 +23,14 @@ const meta = {
23
23
  title: {
24
24
  description: 'Component title text',
25
25
  },
26
- isSelected: {
27
- description: 'Is the element selected?',
26
+ isActive: {
27
+ description: 'Is the element active?',
28
28
  },
29
- onClick: {
30
- description: 'Event triggered when the component is clicked',
29
+ href: {
30
+ description: 'link to the page',
31
+ },
32
+ isOpen: {
33
+ description: 'Is the dropdown open?',
31
34
  },
32
35
  },
33
36
  parameters: figmaPrimaryDesign,
@@ -35,116 +38,29 @@ const meta = {
35
38
 
36
39
  export default meta
37
40
 
38
- export const MenuWithSecondLevelDropdown = {
39
- render: () => (
40
- <Menu>
41
- <MenuOption title="Tekken 8" icon="Edit">
42
- <Menu isDropdown>
43
- <MenuOption title="Walkthrough" onClick={() => alert('click')} />
44
- <MenuOption title="Characters" onClick={() => alert('click')} />
45
- <MenuOption title="Story" onClick={() => alert('click')} />
46
- </Menu>
47
- </MenuOption>
48
- <MenuOption
49
- title="The Legend of Zelda: Tears of the Kingdom"
50
- icon="Delete"
51
- onClick={() => alert('click')}
52
- />
53
- <MenuOption
54
- title="Metal Gear Solid 5: Ground Zeroes + The Phantom Pain"
55
- icon="Show"
56
- >
57
- <Menu isDropdown>
58
- <MenuOption title="Walkthrough" onClick={() => alert('click')} />
59
- <MenuOption title="Characters" onClick={() => alert('click')} />
60
- <MenuOption title="Story" onClick={() => alert('click')} />
61
- </Menu>
62
- </MenuOption>
63
- <MenuOption title="Stray" icon="Info" onClick={() => alert('click')} />
64
- </Menu>
65
- ),
66
- }
67
-
68
- export const FirstLevelMenu = {
69
- render: () => (
70
- <Menu>
71
- <MenuOption title="Tekken 8" icon="Edit" onClick={() => alert('click')} />
72
- <MenuOption
73
- title="The Legend of Zelda: Tears of the Kingdom"
74
- icon="Delete"
75
- onClick={() => alert('click')}
76
- />
77
- <MenuOption
78
- title="Metal Gear Solid 5: Ground Zeroes + The Phantom Pain"
79
- icon="Show"
80
- onClick={() => alert('click')}
81
- />
82
- <MenuOption title="Stray" icon="Info" onClick={() => alert('click')} />
83
- </Menu>
84
- ),
85
- }
86
-
87
- export const MenuWithSecondLevelPreselectedOption = {
41
+ export const MenuWithLinks = {
88
42
  render: () => (
89
43
  <Menu>
90
- <MenuOption isSelected title="Tekken 8" icon="Edit">
91
- <Menu isDropdown>
92
- <MenuOption title="Walkthrough" onClick={() => alert('click')} />
93
- <MenuOption
94
- isSelected
95
- title="Characters"
96
- onClick={() => alert('click')}
97
- />
98
- <MenuOption title="Story" onClick={() => alert('click')} />
99
- </Menu>
100
- </MenuOption>
101
- <MenuOption
44
+ <MenuLink title="Tekken 8" href="some-link" />
45
+ <MenuLink
102
46
  title="The Legend of Zelda: Tears of the Kingdom"
103
47
  icon="Delete"
104
- onClick={() => alert('click')}
48
+ href="some-link"
49
+ isActive
105
50
  />
106
- <MenuOption
51
+ <MenuLink
107
52
  title="Metal Gear Solid 5: Ground Zeroes + The Phantom Pain"
108
53
  icon="Show"
109
- >
110
- <Menu isDropdown>
111
- <MenuOption title="Walkthrough" onClick={() => alert('click')} />
112
- <MenuOption title="Characters" onClick={() => alert('click')} />
113
- <MenuOption title="Story" onClick={() => alert('click')} />
114
- </Menu>
115
- </MenuOption>
116
- <MenuOption title="Stray" icon="Info" onClick={() => alert('click')} />
117
- </Menu>
118
- ),
119
- }
120
-
121
- export const MenuWithFirstLevelPreselectedOption = {
122
- render: () => (
123
- <Menu>
124
- <MenuOption title="Tekken 8" icon="Edit">
125
- <Menu isDropdown>
126
- <MenuOption title="Walkthrough" onClick={() => alert('click')} />
127
- <MenuOption title="Characters" onClick={() => alert('click')} />
128
- <MenuOption title="Story" onClick={() => alert('click')} />
129
- </Menu>
130
- </MenuOption>
131
- <MenuOption
132
- isSelected
133
- title="The Legend of Zelda: Tears of the Kingdom"
134
- icon="Delete"
135
- onClick={() => alert('click')}
54
+ href="some-link"
136
55
  />
137
- <MenuOption
138
- title="Metal Gear Solid 5: Ground Zeroes + The Phantom Pain"
139
- icon="Show"
140
- >
141
- <Menu isDropdown>
142
- <MenuOption title="Walkthrough" onClick={() => alert('click')} />
143
- <MenuOption title="Characters" onClick={() => alert('click')} />
144
- <MenuOption title="Story" onClick={() => alert('click')} />
145
- </Menu>
146
- </MenuOption>
147
- <MenuOption title="Stray" icon="Info" onClick={() => alert('click')} />
56
+ <MenuDropdown title="Open" icon="AddCircle" name="menu" isOpen>
57
+ <MenuLink title="Stray" href="some-link" isActive />
58
+ <MenuLink title="Fallout 3" href="some-link" />
59
+ </MenuDropdown>
60
+ <MenuDropdown title="Close" name="menu">
61
+ <MenuLink title="Dark souls" href="some-link" />
62
+ <MenuLink title="Elder ring" href="some-link" />
63
+ </MenuDropdown>
148
64
  </Menu>
149
65
  ),
150
66
  }
@@ -9,7 +9,7 @@ import {
9
9
  CardsTableCell,
10
10
  Alignment,
11
11
  } from '@/atoms/CardsTable'
12
- import { IconButton } from '@/atoms/IconButton'
12
+ import { IconButton } from '@/atoms/Button'
13
13
  import { Badge } from '@/atoms/Badge'
14
14
 
15
15
  describe('CardsTable', () => {
@@ -1,59 +1,57 @@
1
1
  import React from 'react'
2
2
  import { render } from '@testing-library/react'
3
3
  import { Menu } from '@/atoms/Menu/Menu'
4
- import { MenuOption } from '@/atoms/Menu/MenuOption'
4
+ import { MenuDropdown, MenuLink } from '@/atoms/Menu'
5
5
 
6
6
  describe('Menu', () => {
7
7
  it('renders first-level menu', () => {
8
- const { getByRole, getByText, getAllByRole } = render(
8
+ const { getByRole, getByText } = render(
9
9
  <Menu>
10
- <MenuOption title="Tekken 8" icon="Edit" />
11
- <MenuOption
12
- isSelected
10
+ <MenuLink title="Tekken 8" icon="Edit" href="#" />
11
+ <MenuLink
12
+ isActive
13
13
  title="The Legend of Zelda: Tears of the Kingdom"
14
14
  icon="Delete"
15
+ href="#"
15
16
  />
16
17
  </Menu>,
17
18
  )
18
19
 
19
20
  expect(getByRole('menu')).toHaveClass(`menu primary`)
20
- expect(getAllByRole('menuitem')[1]).toHaveClass(`selected`)
21
+ expect(
22
+ getByRole('link', { name: 'The Legend of Zelda: Tears of the Kingdom' }),
23
+ ).toHaveClass(`active`)
21
24
  expect(getByText(/Tekken/i)).toBeInTheDocument()
22
25
  expect(getByText(/Zelda/i)).toBeInTheDocument()
23
- expect(getAllByRole('img')[0].title).toBe('Edit')
24
- expect(getAllByRole('img')[2].title).toBe('Delete')
26
+ expect(getByRole('img', { name: 'Edit' })).toBeInTheDocument()
27
+ expect(getByRole('img', { name: 'Delete' })).toBeInTheDocument()
25
28
  })
26
29
 
27
30
  it('renders second-level menu', () => {
28
- const { getByText, getAllByRole } = render(
31
+ const { getByText, getAllByRole, getByRole } = render(
29
32
  <Menu>
30
- <MenuOption title="Tekken 8" icon="Edit">
31
- <Menu isDropdown>
32
- <MenuOption title="Walkthrough" onClick={() => alert('click')} />
33
- <MenuOption
34
- isSelected
35
- title="Characters"
36
- onClick={() => alert('click')}
37
- />
38
- <MenuOption title="Story" onClick={() => alert('click')} />
39
- </Menu>
40
- </MenuOption>
41
- <MenuOption
33
+ <MenuDropdown title="Tekken 8" icon="Edit">
34
+ <MenuLink title="Walkthrough" href="#" />
35
+ <MenuLink isActive title="Characters" href="#" />
36
+ <MenuLink title="Story" href="#" />
37
+ </MenuDropdown>
38
+ <MenuLink
42
39
  title="The Legend of Zelda: Tears of the Kingdom"
43
40
  icon="Delete"
41
+ href="#"
44
42
  />
45
43
  </Menu>,
46
44
  )
47
45
 
48
46
  expect(getAllByRole('menu').length).toBe(2)
49
- expect(getAllByRole('menuitem')[2]).toHaveClass(`selected`)
47
+ expect(getByRole('link', { name: 'Characters' })).toHaveClass(`active`)
50
48
  expect(getByText(/Tekken/i)).toBeInTheDocument()
51
49
  expect(getByText(/Walkthrough/i)).toBeInTheDocument()
52
50
  expect(getByText(/Characters/i)).toBeInTheDocument()
53
51
  expect(getByText(/Story/i)).toBeInTheDocument()
54
52
  expect(getByText(/Zelda/i)).toBeInTheDocument()
55
- expect(getAllByRole('img')[0].title).toBe('Edit')
56
- expect(getAllByRole('img')[2].title).toBe('AngleDown')
57
- expect(getAllByRole('img')[5].title).toBe('Delete')
53
+ expect(getByRole('img', { name: 'Edit' })).toBeInTheDocument()
54
+ expect(getByRole('img', { name: 'AngleDown' })).toBeInTheDocument()
55
+ expect(getByRole('img', { name: 'Delete' })).toBeInTheDocument()
58
56
  })
59
57
  })
@@ -1,58 +0,0 @@
1
- import NextLink from 'next/link'
2
- import './IconButton.scss'
3
- import { Icon, IconType } from './Icon'
4
- import { classNames } from '../utils/classNames'
5
-
6
- export type Variant = 'primary'
7
-
8
- export interface BaseIconButtonProps {
9
- icon: IconType
10
- variant?: Variant
11
- disabled?: boolean
12
- accessibilityLabel: string
13
- }
14
-
15
- type HtmlButtonProps = React.ButtonHTMLAttributes<HTMLButtonElement>
16
-
17
- type AnchorProps = React.AnchorHTMLAttributes<HTMLAnchorElement>
18
-
19
- export type IconButtonProps =
20
- | (HtmlButtonProps & BaseIconButtonProps)
21
- | (AnchorProps & BaseIconButtonProps)
22
-
23
- const hasHref = (props: HtmlButtonProps | AnchorProps): props is AnchorProps =>
24
- 'href' in props
25
-
26
- export function IconButton({
27
- accessibilityLabel,
28
- icon,
29
- disabled,
30
- variant = 'primary',
31
- ...props
32
- }: IconButtonProps) {
33
- const cssClasses = classNames('icon-button', variant, props.className)
34
-
35
- if (hasHref(props)) {
36
- return (
37
- <NextLink
38
- href={props.href || ''}
39
- aria-label={accessibilityLabel}
40
- {...props}
41
- className={cssClasses}
42
- >
43
- <Icon name={icon} />
44
- </NextLink>
45
- )
46
- }
47
-
48
- return (
49
- <button
50
- disabled={disabled}
51
- aria-label={accessibilityLabel}
52
- {...props}
53
- className={cssClasses}
54
- >
55
- <Icon name={icon} />
56
- </button>
57
- )
58
- }
@@ -1,79 +0,0 @@
1
- import './Menu.scss'
2
- import React from 'react'
3
- import { Icon, IconType } from '../Icon'
4
- import { classNames } from '../../utils/classNames'
5
-
6
- export type Variant = 'primary'
7
-
8
- export interface MenuOptionProps extends React.ComponentPropsWithoutRef<'li'> {
9
- variant?: Variant
10
- icon?: IconType
11
- title: string
12
- isSelected?: boolean
13
- }
14
-
15
- export function MenuOption({
16
- variant = 'primary',
17
- className,
18
- isSelected = false,
19
- icon,
20
- title,
21
- children,
22
- onClick,
23
- ...props
24
- }: MenuOptionProps): React.JSX.Element {
25
- const cssClasses = classNames('menu-option', variant, className, {
26
- selected: isSelected,
27
- })
28
-
29
- function closePreviousSelectedDropdown(currentTarget: HTMLLIElement) {
30
- document.querySelectorAll('details[open]').forEach((detailElement) => {
31
- const firstLevelMenuOption = detailElement?.closest('li')
32
- const currentFirstLevelMenuOption = currentTarget
33
- ?.closest('details')
34
- ?.closest('li')
35
-
36
- if (firstLevelMenuOption !== currentFirstLevelMenuOption)
37
- detailElement.removeAttribute('open')
38
- })
39
- }
40
-
41
- function unselectPreviousOption() {
42
- document
43
- .querySelectorAll('.selected')
44
- .forEach((option) => option.classList.remove('selected'))
45
- }
46
-
47
- function setOptionSelected(event: React.MouseEvent<HTMLLIElement>) {
48
- event.stopPropagation()
49
-
50
- closePreviousSelectedDropdown(event.currentTarget)
51
- unselectPreviousOption()
52
-
53
- event.currentTarget.classList.add('selected')
54
- if (onClick) onClick(event)
55
- }
56
-
57
- return (
58
- <li
59
- className={cssClasses}
60
- tabIndex={0}
61
- role="menuitem"
62
- onClick={setOptionSelected}
63
- {...props}
64
- >
65
- <details open={isSelected}>
66
- <summary className="container">
67
- <div className="left">
68
- {icon && <Icon name={icon} />}
69
- <span className="title">{title}</span>
70
- </div>
71
- <div className="right">
72
- <Icon name="AngleDown" />
73
- </div>
74
- </summary>
75
- {children}
76
- </details>
77
- </li>
78
- )
79
- }