uikit-react-public 0.27.2 → 0.29.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 (32) hide show
  1. package/dist/components/BreadcrumbsNew/BaseBreadcrumbsItem.d.ts +12 -0
  2. package/dist/components/BreadcrumbsNew/Breadcrumbs.d.ts +18 -0
  3. package/dist/components/BreadcrumbsNew/BreadcrumbsCurrent.d.ts +8 -0
  4. package/dist/components/BreadcrumbsNew/BreadcrumbsLink.d.ts +7 -0
  5. package/dist/components/BreadcrumbsNew/__tests__/Breadcrumbs.test.d.ts +1 -0
  6. package/dist/components/BreadcrumbsNew/index.d.ts +2 -0
  7. package/dist/components/Chip/Chip.d.ts +11 -0
  8. package/dist/components/Chip/Chip.stories.d.ts +25 -0
  9. package/dist/components/Chip/__tests__/Chip.test.d.ts +1 -0
  10. package/dist/components/Chip/index.d.ts +2 -0
  11. package/dist/components/Link/BaseLink.d.ts +3 -0
  12. package/dist/components/index.d.ts +4 -0
  13. package/dist/index.js +5965 -5634
  14. package/lib/components/BaseCheckbox/BaseCheckbox.tsx +1 -5
  15. package/lib/components/BreadcrumbsNew/BaseBreadcrumbsItem.tsx +69 -0
  16. package/lib/components/BreadcrumbsNew/Breadcrumbs.tsx +118 -0
  17. package/lib/components/BreadcrumbsNew/BreadcrumbsCurrent.tsx +54 -0
  18. package/lib/components/BreadcrumbsNew/BreadcrumbsLink.tsx +67 -0
  19. package/lib/components/BreadcrumbsNew/__tests__/Breadcrumbs.test.tsx +53 -0
  20. package/lib/components/BreadcrumbsNew/index.ts +8 -0
  21. package/lib/components/Checkbox/__tests__/__snapshots__/Checkbox.test.tsx.snap +4 -4
  22. package/lib/components/Chip/Chip.stories.tsx +37 -0
  23. package/lib/components/Chip/Chip.tsx +191 -0
  24. package/lib/components/Chip/__tests__/Chip.test.tsx +287 -0
  25. package/lib/components/Chip/__tests__/__snapshots__/Chip.test.tsx.snap +255 -0
  26. package/lib/components/Chip/index.ts +2 -0
  27. package/lib/components/Link/BaseLink.tsx +40 -2
  28. package/lib/components/Link/__tests__/link.test.tsx +58 -1
  29. package/lib/components/Table/subcomponents/Cell/__tests__/__snapshots__/Cell.test.tsx.snap +1 -1
  30. package/lib/components/Table/subcomponents/HeadCell/__tests__/__snapshots__/HeadCell.test.tsx.snap +1 -1
  31. package/lib/components/index.ts +12 -0
  32. package/package.json +1 -1
@@ -116,11 +116,7 @@ const BaseCheckbox = ({
116
116
  grid-area: 1 / 1;
117
117
  line-height: 0;
118
118
  pointer-events: none;
119
-
120
- input:focus-visible + & {
121
- outline: 2px solid currentColor;
122
- outline-offset: 2px;
123
- }
119
+ outline: none;
124
120
  `;
125
121
 
126
122
  const style = cx(NAME, rootStyle, className);
@@ -0,0 +1,69 @@
1
+ import { LiHTMLAttributes, ReactNode } from 'react';
2
+ import { css, cx } from '@emotion/css';
3
+ import Icon from '../Icon';
4
+ import { useTheme } from '../../theme';
5
+ import type { BreadcrumbsSize } from './Breadcrumbs';
6
+
7
+ export const NAME = 'ucl-uikit-breadcrumbs__item';
8
+
9
+ export interface BaseBreadcrumbsItemProps extends LiHTMLAttributes<HTMLLIElement> {
10
+ separator?: boolean;
11
+ back?: boolean;
12
+ size?: BreadcrumbsSize;
13
+ testId?: string;
14
+ children?: ReactNode;
15
+ }
16
+
17
+ const BaseBreadcrumbsItem = ({
18
+ separator = false,
19
+ back = false,
20
+ size = 'medium',
21
+ testId = NAME,
22
+ className,
23
+ children,
24
+ ...props
25
+ }: BaseBreadcrumbsItemProps) => {
26
+ const [theme] = useTheme();
27
+
28
+ const style = cx(
29
+ NAME,
30
+ css`
31
+ display: inline-flex;
32
+ align-items: center;
33
+ gap: ${theme.margin.m8};
34
+ min-width: 0;
35
+ `,
36
+ className
37
+ );
38
+
39
+ const iconStyle = css`
40
+ flex: 0 0 auto;
41
+ color: ${theme.colour.icon.brandPrimary};
42
+ `;
43
+
44
+ return (
45
+ <li
46
+ className={style}
47
+ data-testid={testId}
48
+ {...props}
49
+ >
50
+ {back && (
51
+ <Icon.ArrowLeft
52
+ aria-hidden
53
+ className={iconStyle}
54
+ size={size === 'small' ? 16 : 20}
55
+ />
56
+ )}
57
+ {children}
58
+ {separator && (
59
+ <Icon.ChevronRight
60
+ aria-hidden
61
+ className={iconStyle}
62
+ size={size === 'small' ? 12 : 16}
63
+ />
64
+ )}
65
+ </li>
66
+ );
67
+ };
68
+
69
+ export default BaseBreadcrumbsItem;
@@ -0,0 +1,118 @@
1
+ import {
2
+ Children,
3
+ HTMLAttributes,
4
+ NamedExoticComponent,
5
+ ReactElement,
6
+ cloneElement,
7
+ isValidElement,
8
+ memo,
9
+ } from 'react';
10
+ import { css, cx } from '@emotion/css';
11
+ import { useTheme } from '../../theme';
12
+ import BaseBreadcrumbsItem, {
13
+ BaseBreadcrumbsItemProps,
14
+ } from './BaseBreadcrumbsItem';
15
+ import BreadcrumbsCurrent, {
16
+ BreadcrumbsCurrentProps,
17
+ } from './BreadcrumbsCurrent';
18
+ import BreadcrumbsLink, { BreadcrumbsLinkProps } from './BreadcrumbsLink';
19
+
20
+ export const NAME = 'ucl-uikit-breadcrumbs';
21
+
22
+ export type BreadcrumbsSize = 'small' | 'medium';
23
+
24
+ export interface BreadcrumbsProps extends HTMLAttributes<HTMLElement> {
25
+ size?: BreadcrumbsSize;
26
+ testId?: string;
27
+ }
28
+
29
+ type BreadcrumbChild = ReactElement<{
30
+ separator?: boolean;
31
+ back?: boolean;
32
+ size?: BreadcrumbsSize;
33
+ }>;
34
+
35
+ const Breadcrumbs = ({
36
+ size = 'medium',
37
+ testId = NAME,
38
+ className,
39
+ children,
40
+ 'aria-label': ariaLabel = 'Breadcrumb',
41
+ ...props
42
+ }: BreadcrumbsProps) => {
43
+ const [theme] = useTheme();
44
+ const items = Children.toArray(children).filter(isValidElement);
45
+ const parentItem = items[items.length - 2] as BreadcrumbChild | undefined;
46
+
47
+ const listStyle = css`
48
+ display: flex;
49
+ align-items: center;
50
+ gap: ${theme.margin.m8};
51
+ margin: 0;
52
+ padding: 0;
53
+ list-style: none;
54
+ `;
55
+
56
+ const desktopStyle = css`
57
+ display: none;
58
+
59
+ @media screen and (min-width: ${theme.breakpoints.tablet}px) {
60
+ display: flex;
61
+ }
62
+ `;
63
+
64
+ const mobileStyle = css`
65
+ @media screen and (min-width: ${theme.breakpoints.tablet}px) {
66
+ display: none;
67
+ }
68
+ `;
69
+
70
+ const style = cx(NAME, className);
71
+
72
+ return (
73
+ <nav
74
+ aria-label={ariaLabel}
75
+ className={style}
76
+ data-testid={testId}
77
+ {...props}
78
+ >
79
+ <ol className={cx(listStyle, desktopStyle)}>
80
+ {items.map((item, index) =>
81
+ cloneElement(item as BreadcrumbChild, {
82
+ separator: index < items.length - 1,
83
+ size,
84
+ })
85
+ )}
86
+ </ol>
87
+ {parentItem && (
88
+ <ol className={cx(listStyle, mobileStyle)}>
89
+ {cloneElement(parentItem, {
90
+ back: true,
91
+ separator: false,
92
+ size,
93
+ })}
94
+ </ol>
95
+ )}
96
+ </nav>
97
+ );
98
+ };
99
+
100
+ interface BreadcrumbsComponent extends NamedExoticComponent<BreadcrumbsProps> {
101
+ BaseItem: typeof BaseBreadcrumbsItem;
102
+ Link: typeof BreadcrumbsLink;
103
+ Current: typeof BreadcrumbsCurrent;
104
+ }
105
+
106
+ const MemoBreadcrumbs: BreadcrumbsComponent = Object.assign(memo(Breadcrumbs), {
107
+ BaseItem: BaseBreadcrumbsItem,
108
+ Link: BreadcrumbsLink,
109
+ Current: BreadcrumbsCurrent,
110
+ });
111
+
112
+ export type {
113
+ BaseBreadcrumbsItemProps,
114
+ BreadcrumbsCurrentProps,
115
+ BreadcrumbsLinkProps,
116
+ };
117
+
118
+ export default MemoBreadcrumbs;
@@ -0,0 +1,54 @@
1
+ import { HTMLAttributes } from 'react';
2
+ import { css, cx } from '@emotion/css';
3
+ import Text from '../Text';
4
+ import { useTheme } from '../../theme';
5
+ import BaseBreadcrumbsItem, {
6
+ BaseBreadcrumbsItemProps,
7
+ } from './BaseBreadcrumbsItem';
8
+
9
+ export const NAME = 'ucl-uikit-breadcrumbs__current';
10
+
11
+ export interface BreadcrumbsCurrentProps
12
+ extends
13
+ HTMLAttributes<HTMLSpanElement>,
14
+ Pick<BaseBreadcrumbsItemProps, 'separator' | 'size'> {
15
+ testId?: string;
16
+ }
17
+
18
+ const BreadcrumbsCurrent = ({
19
+ separator,
20
+ size = 'medium',
21
+ testId = NAME,
22
+ className,
23
+ children,
24
+ ...props
25
+ }: BreadcrumbsCurrentProps) => {
26
+ const [theme] = useTheme();
27
+
28
+ const style = cx(
29
+ NAME,
30
+ css`
31
+ color: ${theme.colour.text.secondary};
32
+ `,
33
+ className
34
+ );
35
+
36
+ return (
37
+ <BaseBreadcrumbsItem
38
+ separator={separator}
39
+ size={size}
40
+ >
41
+ <Text
42
+ aria-current='page'
43
+ className={style}
44
+ data-testid={testId}
45
+ level={size === 'small' ? 'sm-semibold' : 'md-semibold'}
46
+ {...props}
47
+ >
48
+ {children}
49
+ </Text>
50
+ </BaseBreadcrumbsItem>
51
+ );
52
+ };
53
+
54
+ export default BreadcrumbsCurrent;
@@ -0,0 +1,67 @@
1
+ import { css, cx } from '@emotion/css';
2
+ import Link, { LinkProps } from '../Link';
3
+ import { useTheme } from '../../theme';
4
+ import BaseBreadcrumbsItem, {
5
+ BaseBreadcrumbsItemProps,
6
+ } from './BaseBreadcrumbsItem';
7
+
8
+ export const NAME = 'ucl-uikit-breadcrumbs__link';
9
+
10
+ type BreadcrumbsLinkOwnProps = Pick<
11
+ BaseBreadcrumbsItemProps,
12
+ 'separator' | 'back' | 'size'
13
+ >;
14
+
15
+ export type BreadcrumbsLinkProps = BreadcrumbsLinkOwnProps &
16
+ Omit<LinkProps, 'size'>;
17
+
18
+ const BreadcrumbsLink = ({
19
+ separator,
20
+ back,
21
+ size = 'medium',
22
+ className,
23
+ children,
24
+ ...props
25
+ }: BreadcrumbsLinkProps) => {
26
+ const [theme] = useTheme();
27
+
28
+ const linkStyle = cx(
29
+ NAME,
30
+ css`
31
+ color: ${theme.colour.link.primary};
32
+ text-decoration: none;
33
+
34
+ &:visited {
35
+ color: ${theme.colour.link.primary};
36
+ }
37
+
38
+ &:hover {
39
+ text-decoration: underline;
40
+ }
41
+
42
+ &:active {
43
+ color: ${theme.colour.link.primaryHover};
44
+ }
45
+ `,
46
+ className
47
+ );
48
+
49
+ return (
50
+ <BaseBreadcrumbsItem
51
+ separator={separator}
52
+ back={back}
53
+ size={size}
54
+ >
55
+ <Link
56
+ className={linkStyle}
57
+ noVisited
58
+ size={size === 'small' ? 'small' : 'default'}
59
+ {...props}
60
+ >
61
+ {children}
62
+ </Link>
63
+ </BaseBreadcrumbsItem>
64
+ );
65
+ };
66
+
67
+ export default BreadcrumbsLink;
@@ -0,0 +1,53 @@
1
+ import { render, screen, within } from '@testing-library/react';
2
+ import { describe, expect, test } from 'vitest';
3
+ import { ThemeContextProvider } from '../../../theme/useTheme';
4
+ import Breadcrumbs from '../Breadcrumbs';
5
+
6
+ describe('BreadcrumbsNew', () => {
7
+ test('renders the full trail and a mobile parent link', () => {
8
+ render(
9
+ <ThemeContextProvider>
10
+ <Breadcrumbs>
11
+ <Breadcrumbs.Link asChild>
12
+ <a href='/'>Home</a>
13
+ </Breadcrumbs.Link>
14
+ <Breadcrumbs.Link asChild>
15
+ <a href='/parent'>Parent</a>
16
+ </Breadcrumbs.Link>
17
+ <Breadcrumbs.Current>Current page</Breadcrumbs.Current>
18
+ </Breadcrumbs>
19
+ </ThemeContextProvider>
20
+ );
21
+
22
+ const lists = screen.getAllByRole('list', { hidden: true });
23
+
24
+ expect(
25
+ within(lists[0]).getAllByRole('listitem', { hidden: true })
26
+ ).toHaveLength(3);
27
+ expect(
28
+ within(lists[1]).getAllByRole('listitem', { hidden: true })
29
+ ).toHaveLength(1);
30
+ expect(
31
+ within(lists[1]).getByRole('link', { hidden: true })
32
+ ).toHaveAttribute('href', '/parent');
33
+ expect(screen.getByText('Current page')).toHaveAttribute(
34
+ 'aria-current',
35
+ 'page'
36
+ );
37
+ });
38
+
39
+ test('supports small breadcrumbs', () => {
40
+ render(
41
+ <ThemeContextProvider>
42
+ <Breadcrumbs size='small'>
43
+ <Breadcrumbs.Link asChild>
44
+ <a href='/parent'>Parent</a>
45
+ </Breadcrumbs.Link>
46
+ <Breadcrumbs.Current>Current page</Breadcrumbs.Current>
47
+ </Breadcrumbs>
48
+ </ThemeContextProvider>
49
+ );
50
+
51
+ expect(screen.getByText('Current page')).toHaveClass('ucl-uikit-text');
52
+ });
53
+ });
@@ -0,0 +1,8 @@
1
+ export { default } from './Breadcrumbs';
2
+ export type {
3
+ BaseBreadcrumbsItemProps,
4
+ BreadcrumbsCurrentProps,
5
+ BreadcrumbsLinkProps,
6
+ BreadcrumbsProps,
7
+ BreadcrumbsSize,
8
+ } from './Breadcrumbs';
@@ -13,7 +13,7 @@ exports[`Checkbox > snapshot: checked prop 1`] = `
13
13
  />
14
14
  <span
15
15
  aria-hidden="true"
16
- class="css-1quq9cp"
16
+ class="css-g3adpt"
17
17
  >
18
18
  <svg
19
19
  class="ucl-uikit-icon css-1nacdsu"
@@ -49,7 +49,7 @@ exports[`Checkbox > snapshot: indeterminate prop 1`] = `
49
49
  />
50
50
  <span
51
51
  aria-hidden="true"
52
- class="css-1quq9cp"
52
+ class="css-g3adpt"
53
53
  >
54
54
  <svg
55
55
  class="ucl-uikit-icon css-2i3jn5"
@@ -87,7 +87,7 @@ exports[`Checkbox > snapshot: no props 1`] = `
87
87
  />
88
88
  <span
89
89
  aria-hidden="true"
90
- class="css-1quq9cp"
90
+ class="css-g3adpt"
91
91
  />
92
92
  </span>
93
93
  `;
@@ -104,7 +104,7 @@ exports[`Checkbox > snapshot: testId prop 1`] = `
104
104
  />
105
105
  <span
106
106
  aria-hidden="true"
107
- class="css-1quq9cp"
107
+ class="css-g3adpt"
108
108
  />
109
109
  </span>
110
110
  `;
@@ -0,0 +1,37 @@
1
+ import type { Meta, StoryObj } from '@storybook/react';
2
+ import { useArgs } from '@storybook/preview-api';
3
+
4
+ import Chip from './Chip';
5
+
6
+ const meta = {
7
+ title: 'Components/Chip',
8
+ component: Chip,
9
+ argTypes: {
10
+ label: { control: { type: 'text' } },
11
+ checked: { control: { type: 'boolean' } },
12
+ disabled: { control: { type: 'boolean' } },
13
+ },
14
+ } satisfies Meta<typeof Chip>;
15
+
16
+ export default meta;
17
+ type Story = StoryObj<typeof meta>;
18
+
19
+ export const Default: Story = {
20
+ args: {
21
+ label: 'Student',
22
+ checked: false,
23
+ disabled: false,
24
+ },
25
+ render: (args) => {
26
+ const [{ checked }, updateArgs] = useArgs();
27
+ const onChange = () => updateArgs({ checked: !checked });
28
+
29
+ return (
30
+ <Chip
31
+ {...args}
32
+ checked={checked}
33
+ onChange={onChange}
34
+ />
35
+ );
36
+ },
37
+ };
@@ -0,0 +1,191 @@
1
+ import { memo, forwardRef } from 'react';
2
+ import { css, cx } from '@emotion/css';
3
+ import useTheme from '../../theme/useTheme';
4
+ import BaseCheckbox, { BaseCheckboxProps } from '../BaseCheckbox';
5
+ import Icon from '../Icon';
6
+ import Text from '../Text';
7
+ import marginsStyle, { MarginProps } from '../common/marginsStyle';
8
+
9
+ export const NAME = 'ucl-uikit-chip';
10
+
11
+ export interface ChipBaseProps extends Omit<
12
+ BaseCheckboxProps,
13
+ | 'renderVisual'
14
+ | 'checkedComponent'
15
+ | 'uncheckedComponent'
16
+ | 'indeterminateComponent'
17
+ | 'indeterminate'
18
+ > {
19
+ label: string;
20
+ ariaLabel?: string;
21
+ }
22
+
23
+ export type ChipProps = ChipBaseProps & MarginProps;
24
+
25
+ export type Ref = HTMLInputElement;
26
+
27
+ const Chip = forwardRef<Ref, ChipProps>(
28
+ (
29
+ {
30
+ label,
31
+ checked,
32
+ defaultChecked,
33
+ disabled,
34
+ testId = NAME,
35
+ ariaLabel,
36
+ className,
37
+ ...props
38
+ },
39
+ ref
40
+ ) => {
41
+ const [theme] = useTheme();
42
+
43
+ const baseStyle = css`
44
+ position: relative;
45
+ display: inline-flex;
46
+ align-items: center;
47
+ justify-content: center;
48
+ min-height: 32px;
49
+ width: fit-content;
50
+ padding: ${theme.margin.m4} ${theme.margin.m16};
51
+ border: ${theme.border.b1} solid ${theme.colour.icon.brand};
52
+ border-radius: 16px;
53
+ outline: none;
54
+ box-sizing: border-box;
55
+ color: ${theme.colour.icon.inverse};
56
+ transition:
57
+ background-color 0.15s ease-out,
58
+ border-color 0.15s ease-out;
59
+ cursor: pointer;
60
+ background-color: ${theme.colour.icon.inverse};
61
+ color: ${theme.colour.icon.brand};
62
+
63
+ &:hover {
64
+ background-color: ${theme.colour.fill.brandSubtleHover};
65
+ }
66
+
67
+ &:has(input:focus-visible) {
68
+ box-shadow: ${theme.boxShadow.focus};
69
+ }
70
+
71
+ &:has(input:checked) {
72
+ background-color: ${theme.colour.icon.brandSelectedOnBgFill};
73
+ border-color: ${theme.colour.icon.brandSelectedOnBgFill};
74
+ color: ${theme.colour.icon.inverse};
75
+ }
76
+
77
+ &:has(input:checked):hover {
78
+ background-color: ${theme.colour.icon.brandSelectedOnBgFill};
79
+ }
80
+
81
+ &:has(input:disabled),
82
+ &:has(input:disabled):hover {
83
+ background-color: ${theme.colour.fill.disabled};
84
+ border-color: ${theme.colour.border.disabled};
85
+ color: ${theme.colour.text.disabled};
86
+ cursor: not-allowed;
87
+ }
88
+ `;
89
+
90
+ const style = cx(NAME, baseStyle, marginsStyle(props, theme), className);
91
+
92
+ const contentStyle = css`
93
+ display: inline-flex;
94
+ align-items: center;
95
+ justify-content: center;
96
+ `;
97
+
98
+ const iconBaseStyle = css`
99
+ flex-shrink: 0;
100
+ color: inherit;
101
+ user-select: none;
102
+ pointer-events: none;
103
+ transition:
104
+ opacity 0.15s ease-out,
105
+ margin-left 0.15s ease-out,
106
+ margin-right 0.15s ease-out;
107
+
108
+ @media (prefers-reduced-motion: reduce) {
109
+ transition: none;
110
+ }
111
+ `;
112
+
113
+ const iconVisibleStyle = css`
114
+ opacity: 1;
115
+ margin-left: 0;
116
+ margin-right: 0;
117
+ `;
118
+
119
+ const iconHiddenStyle = css`
120
+ opacity: 0;
121
+ margin-left: -4px;
122
+ margin-right: 4px;
123
+ `;
124
+
125
+ const labelBaseStyle = css`
126
+ color: inherit;
127
+ transition:
128
+ margin-left 0.15s ease-out,
129
+ margin-right 0.15s ease-out;
130
+
131
+ @media (prefers-reduced-motion: reduce) {
132
+ transition: none;
133
+ }
134
+ `;
135
+
136
+ const labelCheckedStyle = css`
137
+ margin-left: ${theme.margin.m4};
138
+ margin-right: 0;
139
+ `;
140
+
141
+ const labelUncheckedStyle = css`
142
+ margin-left: -6px;
143
+ margin-right: 10px;
144
+ `;
145
+
146
+ return (
147
+ <label className={style}>
148
+ <BaseCheckbox
149
+ ref={ref}
150
+ testId={`${testId}__root`}
151
+ data-testid={testId}
152
+ aria-label={ariaLabel}
153
+ checked={checked}
154
+ defaultChecked={defaultChecked}
155
+ disabled={disabled}
156
+ renderVisual={({ checked }) => {
157
+ const iconStyle = cx(
158
+ iconBaseStyle,
159
+ checked && iconVisibleStyle,
160
+ !checked && iconHiddenStyle
161
+ );
162
+
163
+ const labelStyle = cx(
164
+ labelBaseStyle,
165
+ checked && labelCheckedStyle,
166
+ !checked && labelUncheckedStyle
167
+ );
168
+
169
+ return (
170
+ <span className={contentStyle}>
171
+ <Icon.Check
172
+ className={iconStyle}
173
+ size={16}
174
+ />
175
+ <Text
176
+ level='sm-semibold'
177
+ className={labelStyle}
178
+ >
179
+ {label}
180
+ </Text>
181
+ </span>
182
+ );
183
+ }}
184
+ {...props}
185
+ />
186
+ </label>
187
+ );
188
+ }
189
+ );
190
+
191
+ export default memo(Chip);