@indico-data/design-system 3.20.0 → 3.22.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.
@@ -1,24 +1,53 @@
1
1
  import classNames from 'classnames';
2
2
 
3
- import { type PillProps } from './types';
3
+ import { type PillProps, type PillSize } from './types';
4
+ import { Icon } from '../icons/Icon';
5
+ import { type IconSizes } from '../icons/types';
6
+
7
+ const PILL_ICON_SIZE: Record<PillSize, IconSizes> = {
8
+ sm: 'xxs',
9
+ md: 'xs',
10
+ lg: 'xs',
11
+ };
4
12
 
5
13
  export const Pill = ({
6
14
  children,
7
- className,
8
15
  color = 'gray',
9
16
  size = 'sm',
10
- shade,
17
+ variant = 'solid',
18
+ type = 'pill',
19
+ iconLeft,
20
+ iconRight,
21
+ dot,
22
+ onClose,
23
+ closeAriaLabel = 'Remove',
24
+ className,
11
25
  ...rest
12
26
  }: PillProps) => {
13
- const pillClasses = classNames('pill', className, {
14
- [`pill--${color}`]: color && !shade,
15
- [`pill--${color}-${shade}`]: color && shade,
16
- [`pill--${size}`]: size,
17
- });
18
-
19
27
  return (
20
- <div className={pillClasses} {...rest}>
28
+ <div
29
+ className={classNames(
30
+ 'pill',
31
+ `pill--${type}`,
32
+ `pill--${size}`,
33
+ `pill--${variant}-${color}`,
34
+ className,
35
+ {
36
+ 'pill--icon-only': !children && (iconLeft || iconRight),
37
+ 'pill--closeable': !!onClose,
38
+ },
39
+ )}
40
+ {...rest}
41
+ >
42
+ {dot && <span className="pill__dot" />}
43
+ {iconLeft && <Icon name={iconLeft} size={PILL_ICON_SIZE[size]} />}
21
44
  {children}
45
+ {iconRight && <Icon name={iconRight} size={PILL_ICON_SIZE[size]} />}
46
+ {onClose && (
47
+ <button type="button" className="pill__close" onClick={onClose} aria-label={closeAriaLabel}>
48
+ <Icon name="fa-xmark" size="xs" />
49
+ </button>
50
+ )}
22
51
  </div>
23
52
  );
24
53
  };
@@ -1,61 +1,125 @@
1
+ import { faCheck, faArrowRight } from '@fortawesome/free-solid-svg-icons';
1
2
  import { render, screen } from '@testing-library/react';
3
+ import userEvent from '@testing-library/user-event';
2
4
 
3
5
  import { Pill } from '@/components/pill/Pill';
6
+ import { registerFontAwesomeIcons } from '@/setup/setupIcons';
4
7
 
5
- import { type PillColor, type PillShade, type PillSize } from '../types';
8
+ registerFontAwesomeIcons(faCheck, faArrowRight);
6
9
 
10
+ describe('Pill', () => {
11
+ it('renders children', () => {
12
+ render(<Pill>Hello</Pill>);
13
+ expect(screen.getByText('Hello')).toBeInTheDocument();
14
+ });
7
15
 
8
- const sizes = ['sm', 'md', 'lg'] as PillSize[];
9
- const colors = [
10
- 'blue',
11
- 'purple',
12
- 'red',
13
- 'yellow',
14
- 'gray',
15
- 'green',
16
- 'pink',
17
- 'orange',
18
- 'teal',
19
- ] as PillColor[];
16
+ it('applies default classes (gray, sm, solid, pill)', () => {
17
+ const { container } = render(<Pill>Default</Pill>);
18
+ const el = container.firstChild as HTMLElement;
19
+ expect(el).toHaveClass('pill', 'pill--pill', 'pill--sm', 'pill--solid-gray');
20
+ });
20
21
 
21
- const shades = [1, 2, 3, 4, 5] as PillShade[];
22
+ it('applies size class for non-default size', () => {
23
+ const { container } = render(<Pill size="lg">Large</Pill>);
24
+ expect(container.firstChild).toHaveClass('pill--lg');
25
+ });
22
26
 
23
- describe('Pill', () => {
24
- sizes.forEach((size) => {
25
- it(`checks that the proper size class is applied when size is ${size}`, () => {
26
- render(
27
- <Pill color="blue" size={size} data-testid={`pill-${size}`}>
28
- Success
27
+ it('applies badge type class', () => {
28
+ const { container } = render(<Pill type="badge">Badge</Pill>);
29
+ expect(container.firstChild).toHaveClass('pill--badge');
30
+ });
31
+
32
+ describe('colors x variants', () => {
33
+ it('applies solid color class', () => {
34
+ const { container } = render(
35
+ <Pill color="blue" variant="solid">
36
+ Status
29
37
  </Pill>,
30
38
  );
31
- const pill = screen.getByTestId(`pill-${size}`);
32
- expect(pill).toHaveClass(`pill pill--blue pill--${size}`);
39
+ expect(container.firstChild).toHaveClass('pill--solid-blue');
33
40
  });
34
- });
35
41
 
36
- colors.forEach((color) => {
37
- it(`checks that the proper style class has been applied when color is ${color}`, () => {
38
- render(
39
- <Pill color={color} size="md" data-testid={`pill-${color}`}>
42
+ it('applies outline color class', () => {
43
+ const { container } = render(
44
+ <Pill color="red" variant="outline">
45
+ Status
46
+ </Pill>,
47
+ );
48
+ expect(container.firstChild).toHaveClass('pill--outline-red');
49
+ });
50
+
51
+ it('applies soft color class', () => {
52
+ const { container } = render(
53
+ <Pill color="soft" variant="solid">
40
54
  Status
41
55
  </Pill>,
42
56
  );
43
- const pill = screen.getByTestId(`pill-${color}`);
44
- expect(pill).toHaveClass(`pill pill--${color} pill--md`);
57
+ expect(container.firstChild).toHaveClass('pill--solid-soft');
45
58
  });
46
59
  });
47
60
 
48
- colors.forEach((color) => {
49
- shades.forEach((shade) => {
50
- it(`checks that the proper style class has been applied when color is ${color} and shade is ${shade}`, () => {
51
- render(
52
- <Pill color={color} size="md" shade={shade} data-testid={`pill-${color}-${shade}`}>
53
- Status
54
- </Pill>,
55
- );
56
- const pill = screen.getByTestId(`pill-${color}-${shade}`);
57
- expect(pill).toHaveClass(`pill pill--${color}-${shade} pill--md`);
58
- });
61
+ it('renders a leading icon and sizes it based on pill size', () => {
62
+ const { container } = render(
63
+ <Pill iconLeft="fa-check" size="md">
64
+ With Icon
65
+ </Pill>,
66
+ );
67
+ expect(container.querySelector('.icon--xs')).toBeInTheDocument();
68
+ });
69
+
70
+ it('renders both leading and trailing icons', () => {
71
+ const { container } = render(
72
+ <Pill iconLeft="fa-check" iconRight="fa-arrow-right">
73
+ Both
74
+ </Pill>,
75
+ );
76
+ expect(container.querySelectorAll('.icon')).toHaveLength(2);
77
+ });
78
+
79
+ it('renders a dot element when dot is true', () => {
80
+ const { container } = render(<Pill dot>Dotted</Pill>);
81
+ expect(container.querySelector('.pill__dot')).toBeInTheDocument();
82
+ });
83
+
84
+ it('applies pill--icon-only when no children and icon is provided', () => {
85
+ const { container } = render(<Pill iconLeft="fa-check" />);
86
+ expect(container.firstChild).toHaveClass('pill--icon-only');
87
+ });
88
+
89
+ describe('onClose', () => {
90
+ it('renders close button and applies closeable class', () => {
91
+ const { container } = render(<Pill onClose={() => {}}>Closeable</Pill>);
92
+ expect(container.querySelector('.pill__close')).toBeInTheDocument();
93
+ expect(container.firstChild).toHaveClass('pill--closeable');
94
+ });
95
+
96
+ it('calls onClose when close button is clicked', async () => {
97
+ const user = userEvent.setup();
98
+ const handleClose = jest.fn();
99
+ render(<Pill onClose={handleClose}>Close Me</Pill>);
100
+ await user.click(screen.getByRole('button', { name: 'Remove' }));
101
+ expect(handleClose).toHaveBeenCalledTimes(1);
102
+ });
103
+
104
+ it('uses custom closeAriaLabel when provided', () => {
105
+ render(
106
+ <Pill onClose={() => {}} closeAriaLabel="Supprimer">
107
+ Close Me
108
+ </Pill>,
109
+ );
110
+ expect(screen.getByRole('button', { name: 'Supprimer' })).toBeInTheDocument();
59
111
  });
60
112
  });
113
+
114
+ it('renders dot + iconLeft + close together', () => {
115
+ const { container } = render(
116
+ <Pill dot iconLeft="fa-check" onClose={() => {}}>
117
+ All Features
118
+ </Pill>,
119
+ );
120
+ expect(container.querySelector('.pill__dot')).toBeInTheDocument();
121
+ expect(container.querySelector('.icon')).toBeInTheDocument();
122
+ expect(container.querySelector('.pill__close')).toBeInTheDocument();
123
+ expect(screen.getByText('All Features')).toBeInTheDocument();
124
+ });
61
125
  });
@@ -1,41 +1,138 @@
1
- @import '../../../styles/_colors.scss';
1
+ $pill-colors: 'red', 'purple', 'yellow', 'blue', 'green', 'gray', 'pink', 'orange', 'teal', 'soft';
2
+ $pill-variants: 'solid', 'outline';
2
3
 
3
4
  .pill {
4
- display: inline-block;
5
- border-radius: var(--pf-border-radius-xs);
6
- padding: var(--pf-spacing-xxs) var(--pf-spacing-sm);
7
- font-size: var(--pf-font-size-overline);
8
- font-weight: var(--pf-font-weight-medium);
5
+ display: inline-flex;
6
+ align-items: center;
7
+ justify-content: center;
9
8
  white-space: nowrap;
10
- line-height: 1;
11
- color: var(--pf-gray-color-50);
9
+ border: none;
10
+ font-family: var(--pf-font-family);
12
11
 
12
+ // ------- Types (border-radius) -------
13
+ &--badge {
14
+ border-radius: var(--pf-border-radius-sm);
15
+ }
16
+
17
+ &--pill {
18
+ border-radius: var(--pf-border-radius-full);
19
+ }
20
+
21
+ // ------- Sizes -------
13
22
  &--sm {
14
23
  padding: var(--pf-spacing-xxs) var(--pf-spacing-sm);
15
- font-size: var(--pf-font-size-overline);
24
+ font-size: var(--pf-font-size-sm);
25
+ font-weight: var(--pf-font-weight-semibold);
26
+ line-height: 16px;
27
+ gap: var(--pf-spacing-xxs);
16
28
  }
17
29
 
18
30
  &--md {
19
- padding: var(--pf-spacing-sm) var(--pf-spacing-lg);
20
- font-size: var(--pf-font-size-body);
31
+ padding: var(--pf-spacing-xxs) var(--pf-spacing-md);
32
+ font-size: var(--pf-font-size-md);
33
+ font-weight: var(--pf-font-weight-medium);
34
+ line-height: 20px;
35
+ gap: var(--pf-spacing-xs);
21
36
  }
22
37
 
23
38
  &--lg {
24
- padding: var(--pf-spacing-lg) var(--pf-spacing-2xl);
25
- font-size: var(--pf-font-size-h2);
39
+ padding: var(--pf-spacing-xs) var(--pf-spacing-lg);
40
+ font-size: var(--pf-font-size-md);
41
+ font-weight: var(--pf-font-weight-medium);
42
+ line-height: 20px;
43
+ gap: var(--pf-spacing-xs);
44
+ }
45
+
46
+ // LG badge uses border-md (8px) instead of border-sm (6px)
47
+ &--lg#{&}--badge {
48
+ border-radius: var(--pf-border-radius-md);
49
+ }
50
+
51
+ // ------- Icon-only (special padding) -------
52
+ &--icon-only {
53
+ &.pill--sm {
54
+ padding: var(--pf-spacing-xxs);
55
+ }
56
+
57
+ &.pill--md {
58
+ padding: var(--pf-spacing-xs);
59
+ }
60
+
61
+ &.pill--lg {
62
+ padding: var(--pf-spacing-sm);
63
+ }
26
64
  }
27
65
 
28
- @each $color in $chromatic-color-names {
29
- &--#{$color} {
30
- background-color: var(--pf-item-#{$color}-color);
31
- border-color: var(--pf-item-#{$color}-color);
66
+ // ------- Closeable (asymmetric padding) -------
67
+ &--closeable {
68
+ &.pill--sm {
69
+ padding: var(--pf-spacing-xxs) var(--pf-spacing-xs) var(--pf-spacing-xxs) var(--pf-spacing-sm);
32
70
  }
33
71
 
34
- @each $shade, $color-number in $item-shades-to-color-number {
35
- &--#{$color}-#{$shade} {
36
- background-color: var(--pf-item-#{$color}-#{$shade}-color);
37
- border-color: var(--pf-item-#{$color}-#{$shade}-color);
72
+ &.pill--md {
73
+ padding: var(--pf-spacing-xxs) var(--pf-spacing-xs) var(--pf-spacing-xxs) var(--pf-spacing-md);
74
+ gap: var(--pf-spacing-xxs);
75
+ }
76
+
77
+ &.pill--lg {
78
+ padding: var(--pf-spacing-xs) var(--pf-spacing-sm) var(--pf-spacing-xs) var(--pf-spacing-lg);
79
+ gap: var(--pf-spacing-xxs);
80
+ }
81
+ }
82
+
83
+ // ------- Color × Variant -------
84
+ @each $variant in $pill-variants {
85
+ @each $color in $pill-colors {
86
+ &--#{$variant}-#{$color} {
87
+ background-color: var(--pf-pill-#{$variant}-#{$color}-bg);
88
+ color: var(--pf-pill-#{$variant}-#{$color}-text);
89
+
90
+ @if $variant == 'outline' {
91
+ box-shadow: inset 0 0 0 1px var(--pf-pill-#{$variant}-#{$color}-border);
92
+ }
93
+
94
+ > .icon {
95
+ color: var(--pf-pill-#{$variant}-#{$color}-icon);
96
+ }
97
+
98
+ .pill__dot {
99
+ background-color: var(--pf-pill-#{$variant}-#{$color}-dot);
100
+ }
101
+
102
+ .pill__close {
103
+ color: var(--pf-pill-#{$variant}-#{$color}-close);
104
+
105
+ &:hover {
106
+ color: var(--pf-pill-#{$variant}-#{$color}-close-hover);
107
+ background-color: var(--pf-pill-#{$variant}-#{$color}-close-hover-bg);
108
+ }
109
+ }
38
110
  }
39
111
  }
40
112
  }
113
+
114
+ // ------- Sub-elements -------
115
+ &__dot {
116
+ width: 6px;
117
+ height: 6px;
118
+ border-radius: 50%;
119
+ flex-shrink: 0;
120
+ }
121
+
122
+ &__close {
123
+ display: inline-flex;
124
+ align-items: center;
125
+ justify-content: center;
126
+ flex-shrink: 0;
127
+ cursor: pointer;
128
+ border: none;
129
+ background: transparent;
130
+ padding: var(--pf-spacing-micro);
131
+ border-radius: var(--pf-border-radius-xs);
132
+ line-height: 1;
133
+ aspect-ratio: 1;
134
+ transition:
135
+ color 0.15s ease,
136
+ background-color 0.15s ease;
137
+ }
41
138
  }