uikit-react-public 0.14.21 → 0.17.4
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/README.md +4 -2
- package/dist/components/Accordion/Accordion.Heading.d.ts +4 -4
- package/dist/components/Accordion/Accordion.Panel.d.ts +2 -2
- package/dist/components/Accordion/Accordion.d.ts +1 -1
- package/dist/components/Accordion/Accordion.stories.d.ts +57 -0
- package/dist/components/Accordion/index.d.ts +2 -0
- package/dist/components/Avatar/Avatar.stories.d.ts +107 -1
- package/dist/components/Button/Button.d.ts +1 -0
- package/dist/components/Calendar/index.d.ts +1 -1
- package/dist/components/Datepicker/Datepicker.d.ts +1 -1
- package/dist/components/Datepicker/Datepicker.stories.d.ts +4 -3
- package/dist/components/Datepicker/Datepicker.types.d.ts +4 -5
- package/dist/components/Datepicker/subcomponents/CustomDatepicker.d.ts +4 -1
- package/dist/components/Datepicker/subcomponents/DatepickerInput.d.ts +15 -2
- package/dist/components/Datepicker/subcomponents/Panel.d.ts +1 -1
- package/dist/components/Datepicker/subcomponents/VisibleField.d.ts +6 -1
- package/dist/components/Datepicker/subcomponents/index.d.ts +0 -1
- package/dist/components/Datepicker/utils/index.d.ts +0 -1
- package/dist/components/Dialog/BaseDialog.d.ts +2 -1
- package/dist/components/Dialog/Dialog.d.ts +2 -0
- package/dist/components/Header/Header.d.ts +4 -1
- package/dist/components/Header/Header.stories.d.ts +40 -0
- package/dist/components/Main/Main.d.ts +21 -0
- package/dist/components/Main/Main.stories.d.ts +15 -0
- package/dist/components/Main/index.d.ts +2 -0
- package/dist/components/NativeDatepicker/NativeDatepicker.d.ts +3 -0
- package/dist/components/NativeDatepicker/NativeDatepicker.stories.d.ts +36 -0
- package/dist/components/NativeDatepicker/NativeDatepicker.types.d.ts +10 -0
- package/dist/components/NativeDatepicker/index.d.ts +2 -0
- package/dist/components/{Datepicker → NativeDatepicker}/utils/dateToLocaleISOString/dateToLocaleISOString.d.ts +1 -1
- package/dist/components/NativeDatepicker/utils/dateToLocaleISOString/dateToLocaleISOString.test.d.ts +1 -0
- package/dist/components/NativeDatepicker/utils/index.d.ts +1 -0
- package/dist/components/Select/Select.stories.d.ts +154 -2
- package/dist/components/Select/Select.types.d.ts +51 -22
- package/dist/components/Select/subcomponents/CustomOption.d.ts +1 -1
- package/dist/components/Select/subcomponents/CustomSelect.d.ts +3 -2
- package/dist/components/Select/subcomponents/FilterInput.d.ts +14 -0
- package/dist/components/Select/subcomponents/NativeSelect.d.ts +5 -1
- package/dist/components/Select/subcomponents/VisibleField.d.ts +3 -1
- package/dist/components/Select/subcomponents/index.d.ts +1 -0
- package/dist/components/WeekPicker/WeekPicker.d.ts +2 -2
- package/dist/components/WeekPicker/WeekPicker.stories.d.ts +41 -0
- package/dist/components/WeekPicker/WeekPicker.types.d.ts +16 -0
- package/dist/components/WeekPicker/index.d.ts +1 -0
- package/dist/components/WeekPicker/subcomponents/CustomDatepicker.d.ts +1 -1
- package/dist/components/index.d.ts +8 -0
- package/dist/hooks/useFocusTrap.d.ts +2 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +4366 -3768
- package/dist/utils/__tests__/announce.test.d.ts +1 -0
- package/dist/utils/announce.d.ts +6 -0
- package/dist/utils/index.d.ts +1 -0
- package/lib/components/Accordion/Accordion.Heading.tsx +27 -8
- package/lib/components/Accordion/Accordion.Panel.tsx +11 -3
- package/lib/components/Accordion/Accordion.stories.tsx +139 -0
- package/lib/components/Accordion/Accordion.tsx +10 -8
- package/lib/components/Accordion/__tests__/__snapshots__/Accordion.test.tsx.snap +7 -7
- package/lib/components/Accordion/index.ts +2 -0
- package/lib/components/Alert/Alert.stories.tsx +1 -1
- package/lib/components/Avatar/Avatar.mdx +117 -0
- package/lib/components/Avatar/Avatar.stories.tsx +110 -2
- package/lib/components/Blanket/Blanket.stories.tsx +1 -1
- package/lib/components/Button/Button.stories.tsx +1 -1
- package/lib/components/Button/Button.tsx +1 -0
- package/lib/components/Calendar/Calendar.stories.tsx +12 -32
- package/lib/components/Calendar/__tests__/Calendar.test.tsx +23 -15
- package/lib/components/Calendar/index.ts +1 -5
- package/lib/components/Calendar/subcomponents/AcademicWeeks.tsx +2 -1
- package/lib/components/Calendar/subcomponents/ColumnHeading.tsx +5 -1
- package/lib/components/Calendar/subcomponents/EventDot.tsx +2 -1
- package/lib/components/Calendar/subcomponents/index.ts +1 -1
- package/lib/components/Calendar/utils/getDatesForCalendarGrid/getDatesForCalendarGrid.ts +43 -11
- package/lib/components/Calendar/utils/normaliseMonth/normaliseMonth.test.ts +5 -5
- package/lib/components/Datepicker/Datepicker.lld.md +108 -0
- package/lib/components/Datepicker/Datepicker.stories.tsx +44 -5
- package/lib/components/Datepicker/Datepicker.tsx +14 -36
- package/lib/components/Datepicker/Datepicker.types.ts +5 -14
- package/lib/components/Datepicker/__tests__/Datepicker.test.tsx +150 -8
- package/lib/components/Datepicker/__tests__/__snapshots__/Datepicker.test.tsx.snap +10 -4
- package/lib/components/Datepicker/subcomponents/CustomDatepicker.tsx +39 -5
- package/lib/components/Datepicker/subcomponents/DatepickerInput.tsx +30 -17
- package/lib/components/Datepicker/subcomponents/Panel.tsx +6 -2
- package/lib/components/Datepicker/subcomponents/VisibleField.tsx +40 -3
- package/lib/components/Datepicker/subcomponents/index.ts +0 -1
- package/lib/components/Datepicker/utils/index.ts +0 -1
- package/lib/components/Dialog/BaseDialog.tsx +11 -0
- package/lib/components/Dialog/Dialog.tsx +8 -1
- package/lib/components/Dialog/DialogBody.tsx +5 -1
- package/lib/components/Dialog/DialogHeader.tsx +2 -1
- package/lib/components/Divider/Divider.stories.tsx +1 -1
- package/lib/components/Field/ErrorText.tsx +1 -0
- package/lib/components/Field/Field.stories.tsx +1 -1
- package/lib/components/Field/__tests__/Field.test.tsx +13 -0
- package/lib/components/FileInput/FileInput.stories.tsx +1 -1
- package/lib/components/Footer/Footer.stories.tsx +1 -1
- package/lib/components/Footer/__tests__/__snapshots__/Footer.test.tsx.snap +3 -3
- package/lib/components/Header/Header.mdx +52 -0
- package/lib/components/Header/Header.stories.tsx +98 -0
- package/lib/components/Header/Header.tsx +51 -6
- package/lib/components/Header/__tests__/Header.test.tsx +17 -1
- package/lib/components/Heading/Heading.stories.tsx +1 -1
- package/lib/components/Icon/Icon.stories.tsx +1 -1
- package/lib/components/IconButton/IconButton.stories.tsx +1 -1
- package/lib/components/Input/Input.stories.tsx +1 -1
- package/lib/components/Label/Label.stories.tsx +1 -1
- package/lib/components/Main/Main.stories.tsx +36 -0
- package/lib/components/Main/Main.tsx +46 -0
- package/lib/components/Main/__tests__/Main.test.tsx +80 -0
- package/lib/components/Main/__tests__/__snapshots__/Main.test.tsx.snap +33 -0
- package/lib/components/Main/index.ts +2 -0
- package/lib/components/NativeDatepicker/NativeDatepicker.stories.tsx +100 -0
- package/lib/components/{Datepicker/subcomponents → NativeDatepicker}/NativeDatepicker.tsx +14 -15
- package/lib/components/NativeDatepicker/NativeDatepicker.types.ts +19 -0
- package/lib/components/NativeDatepicker/index.ts +2 -0
- package/lib/components/{Datepicker → NativeDatepicker}/utils/dateToLocaleISOString/dateToLocaleISOString.ts +1 -1
- package/lib/components/NativeDatepicker/utils/index.ts +1 -0
- package/lib/components/Pagination/PaginationControls.tsx +55 -12
- package/lib/components/Pagination/PaginationInfo.tsx +5 -1
- package/lib/components/Paragraph/Paragraph.stories.tsx +1 -1
- package/lib/components/Search/Search.stories.tsx +1 -1
- package/lib/components/Search/Search.tsx +4 -1
- package/lib/components/Search/__tests__/Search.test.tsx +19 -1
- package/lib/components/Select/Select.mdx +169 -0
- package/lib/components/Select/Select.stories.tsx +191 -43
- package/lib/components/Select/Select.tsx +36 -12
- package/lib/components/Select/Select.types.ts +66 -48
- package/lib/components/Select/__tests__/Select.test.tsx +448 -7
- package/lib/components/Select/__tests__/__snapshots__/Select.test.tsx.snap +1 -1
- package/lib/components/Select/subcomponents/CustomOption.tsx +2 -1
- package/lib/components/Select/subcomponents/CustomSelect.tsx +303 -33
- package/lib/components/Select/subcomponents/FilterInput.tsx +80 -0
- package/lib/components/Select/subcomponents/NativeSelect.tsx +13 -1
- package/lib/components/Select/subcomponents/VisibleField.tsx +11 -3
- package/lib/components/Select/subcomponents/index.tsx +1 -0
- package/lib/components/Snackbar/Snackbar.stories.tsx +1 -1
- package/lib/components/Spinner/Spinner.stories.tsx +1 -1
- package/lib/components/Textarea/Textarea.stories.tsx +1 -1
- package/lib/components/Timepicker/Timepicker.tsx +4 -0
- package/lib/components/Timepicker/__tests__/__snapshots__/Timepicker.test.tsx.snap +2 -2
- package/lib/components/Toggle/Toggle.stories.tsx +1 -1
- package/lib/components/Tooltip/Tooltip.stories.tsx +1 -1
- package/lib/components/WeekPicker/WeekPicker.stories.tsx +147 -0
- package/lib/components/WeekPicker/WeekPicker.tsx +2 -2
- package/lib/components/WeekPicker/WeekPicker.types.ts +21 -0
- package/lib/components/WeekPicker/index.ts +1 -0
- package/lib/components/WeekPicker/subcomponents/CustomDatepicker.tsx +1 -1
- package/lib/components/common/Common.mdx +1 -1
- package/lib/components/index.ts +11 -2
- package/lib/hooks/useFocusTrap.ts +40 -4
- package/lib/index.ts +1 -0
- package/lib/utils/__tests__/announce.test.ts +121 -0
- package/lib/utils/announce.ts +134 -0
- package/lib/utils/index.ts +1 -0
- package/package.json +3 -6
- package/dist/components/Datepicker/subcomponents/NativeDatepicker.d.ts +0 -6
- package/lib/components/Accordion/Accordion.stories.tsx.NOT_READY +0 -93
- /package/dist/components/{Datepicker/utils/dateToLocaleISOString/dateToLocaleISOString.test.d.ts → Main/__tests__/Main.test.d.ts} +0 -0
- /package/lib/components/{Datepicker → NativeDatepicker}/utils/dateToLocaleISOString/dateToLocaleISOString.test.ts +0 -0
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react';
|
|
2
|
+
import { useArgs } from '@storybook/preview-api';
|
|
3
|
+
import NativeDatepicker from './NativeDatepicker';
|
|
4
|
+
|
|
5
|
+
const meta = {
|
|
6
|
+
title: 'Components/NativeDatepicker',
|
|
7
|
+
component: NativeDatepicker,
|
|
8
|
+
parameters: { layout: 'padded' },
|
|
9
|
+
argTypes: {
|
|
10
|
+
value: { control: { type: 'date' } },
|
|
11
|
+
minDate: { control: { type: 'date' } },
|
|
12
|
+
maxDate: { control: { type: 'date' } },
|
|
13
|
+
disabled: { control: { type: 'boolean' } },
|
|
14
|
+
},
|
|
15
|
+
tags: ['autodocs'],
|
|
16
|
+
} satisfies Meta<typeof NativeDatepicker>;
|
|
17
|
+
|
|
18
|
+
export default meta;
|
|
19
|
+
type Story = StoryObj<typeof meta>;
|
|
20
|
+
|
|
21
|
+
export const Default: Story = {
|
|
22
|
+
render: () => {
|
|
23
|
+
const [args, updateArgs] = useArgs();
|
|
24
|
+
|
|
25
|
+
// Storybook controls provide UNIX timestamps for dates, need to convert
|
|
26
|
+
// https://storybook.js.org/docs/essentials/controls#annotation
|
|
27
|
+
args.value = args.value ? new Date(args.value) : null;
|
|
28
|
+
args.minDate = args.minDate
|
|
29
|
+
? new Date(args.minDate).toLocaleDateString('sv-SE')
|
|
30
|
+
: null;
|
|
31
|
+
args.maxDate = args.maxDate
|
|
32
|
+
? new Date(args.maxDate).toLocaleDateString('sv-SE')
|
|
33
|
+
: null;
|
|
34
|
+
|
|
35
|
+
const onValueChange = (value: Date | null) => updateArgs({ value: value });
|
|
36
|
+
return (
|
|
37
|
+
<NativeDatepicker
|
|
38
|
+
{...args}
|
|
39
|
+
onValueChange={onValueChange}
|
|
40
|
+
/>
|
|
41
|
+
);
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export const Disabled: Story = {
|
|
46
|
+
name: 'Disabled',
|
|
47
|
+
args: {
|
|
48
|
+
disabled: true,
|
|
49
|
+
value: new Date(2025, 2, 10),
|
|
50
|
+
},
|
|
51
|
+
render: (args) => <NativeDatepicker {...args} />,
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
export const WithMinMaxDates: Story = {
|
|
55
|
+
name: 'With min and max dates',
|
|
56
|
+
args: {
|
|
57
|
+
// Initialise min date as 5 days into the current month
|
|
58
|
+
minDate: (() => {
|
|
59
|
+
const now = new Date();
|
|
60
|
+
return new Date(now.getFullYear(), now.getMonth(), 5).toLocaleDateString(
|
|
61
|
+
'sv-SE'
|
|
62
|
+
);
|
|
63
|
+
})(),
|
|
64
|
+
// Initialise max date as 5 days before the last day of the current month
|
|
65
|
+
maxDate: (() => {
|
|
66
|
+
const now = new Date();
|
|
67
|
+
const lastDay = new Date(
|
|
68
|
+
now.getFullYear(),
|
|
69
|
+
now.getMonth() + 1,
|
|
70
|
+
0
|
|
71
|
+
).getDate();
|
|
72
|
+
return new Date(
|
|
73
|
+
now.getFullYear(),
|
|
74
|
+
now.getMonth(),
|
|
75
|
+
lastDay - 5
|
|
76
|
+
).toLocaleDateString('sv-SE');
|
|
77
|
+
})(),
|
|
78
|
+
},
|
|
79
|
+
render: () => {
|
|
80
|
+
const [args, updateArgs] = useArgs();
|
|
81
|
+
|
|
82
|
+
// Storybook controls provide UNIX timestamps for dates, need to convert
|
|
83
|
+
// https://storybook.js.org/docs/essentials/controls#annotation
|
|
84
|
+
args.value = args.value ? new Date(args.value) : null;
|
|
85
|
+
args.minDate = args.minDate
|
|
86
|
+
? new Date(args.minDate).toLocaleDateString('sv-SE')
|
|
87
|
+
: null;
|
|
88
|
+
args.maxDate = args.maxDate
|
|
89
|
+
? new Date(args.maxDate).toLocaleDateString('sv-SE')
|
|
90
|
+
: null;
|
|
91
|
+
|
|
92
|
+
const onValueChange = (value: Date | null) => updateArgs({ value: value });
|
|
93
|
+
return (
|
|
94
|
+
<NativeDatepicker
|
|
95
|
+
{...args}
|
|
96
|
+
onValueChange={onValueChange}
|
|
97
|
+
/>
|
|
98
|
+
);
|
|
99
|
+
},
|
|
100
|
+
};
|
|
@@ -1,19 +1,15 @@
|
|
|
1
1
|
import { css, cx } from '@emotion/css';
|
|
2
|
-
import { theme } from '
|
|
2
|
+
import { theme } from '../../theme';
|
|
3
|
+
import { dateToLocaleISOString } from './utils';
|
|
4
|
+
import type { NativeDatepickerProps } from './NativeDatepicker.types';
|
|
3
5
|
|
|
4
|
-
|
|
5
|
-
extends React.InputHTMLAttributes<HTMLInputElement> {
|
|
6
|
-
testId?: string;
|
|
7
|
-
ref?: React.RefObject<HTMLInputElement>;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
const NAME = 'ucl-uikit-datepicker--native';
|
|
6
|
+
const NAME = 'ucl-uikit-native-datepicker';
|
|
11
7
|
|
|
12
8
|
const NativeDatepicker = ({
|
|
13
9
|
value,
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
10
|
+
onValueChange,
|
|
11
|
+
minDate,
|
|
12
|
+
maxDate,
|
|
17
13
|
className,
|
|
18
14
|
disabled,
|
|
19
15
|
testId = NAME,
|
|
@@ -54,10 +50,13 @@ const NativeDatepicker = ({
|
|
|
54
50
|
return (
|
|
55
51
|
<input
|
|
56
52
|
type='date'
|
|
57
|
-
value={value}
|
|
58
|
-
onChange={
|
|
59
|
-
|
|
60
|
-
|
|
53
|
+
value={dateToLocaleISOString(value) ?? ''}
|
|
54
|
+
onChange={(event) => {
|
|
55
|
+
const dateString = event.target.value;
|
|
56
|
+
onValueChange?.(dateString ? new Date(dateString) : null, event);
|
|
57
|
+
}}
|
|
58
|
+
min={minDate || undefined}
|
|
59
|
+
max={maxDate || undefined}
|
|
61
60
|
disabled={disabled}
|
|
62
61
|
className={style}
|
|
63
62
|
data-testid={testId}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { InputHTMLAttributes, RefObject } from 'react';
|
|
2
|
+
|
|
3
|
+
export type DatepickerValue = Date | null;
|
|
4
|
+
|
|
5
|
+
export interface NativeDatepickerProps
|
|
6
|
+
extends Omit<
|
|
7
|
+
InputHTMLAttributes<HTMLInputElement>,
|
|
8
|
+
'value' | 'onChange' | 'min' | 'max'
|
|
9
|
+
> {
|
|
10
|
+
value?: DatepickerValue;
|
|
11
|
+
onValueChange?: (
|
|
12
|
+
value: DatepickerValue,
|
|
13
|
+
event?: React.SyntheticEvent
|
|
14
|
+
) => void;
|
|
15
|
+
minDate?: string | null;
|
|
16
|
+
maxDate?: string | null;
|
|
17
|
+
testId?: string;
|
|
18
|
+
ref?: RefObject<HTMLInputElement>;
|
|
19
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Converts a Date object to a `YYYY-MM-DD` string based on the user's local timezone.
|
|
3
3
|
*
|
|
4
|
-
* Used for passing dates to <NativeDatepicker>
|
|
4
|
+
* Used for passing dates to <NativeDatepicker>
|
|
5
5
|
*
|
|
6
6
|
* This function uses the Swedish (`sv-SE`) locale with `toLocaleDateString`
|
|
7
7
|
* as it conveniently defaults to the ISO 8601 `YYYY-MM-DD` format, while respecting
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default as dateToLocaleISOString } from './dateToLocaleISOString/dateToLocaleISOString';
|
|
@@ -4,6 +4,7 @@ import useTheme from '../../theme/useTheme';
|
|
|
4
4
|
import { PaginationContext } from './Pagination';
|
|
5
5
|
import Button from '../Button';
|
|
6
6
|
import getPaginationButtons from './getPaginationButtons';
|
|
7
|
+
import { useMediaQuery } from '../../hooks';
|
|
7
8
|
|
|
8
9
|
export const NAME = 'ucl-ukit-pagination__controls';
|
|
9
10
|
export const DEFAULT_MAX_BUTTONS = 9;
|
|
@@ -21,6 +22,10 @@ const PaginationControls = ({
|
|
|
21
22
|
}: PaginationControlsProps) => {
|
|
22
23
|
const [theme] = useTheme();
|
|
23
24
|
|
|
25
|
+
const isTabletPlus = useMediaQuery(
|
|
26
|
+
`(min-width: ${theme.breakpoints.tablet}px)`
|
|
27
|
+
);
|
|
28
|
+
|
|
24
29
|
const contextValue = useContext(PaginationContext);
|
|
25
30
|
|
|
26
31
|
const handlePageChange = useCallback(
|
|
@@ -48,7 +53,7 @@ const PaginationControls = ({
|
|
|
48
53
|
const paginationButtons = getPaginationButtons(
|
|
49
54
|
currentPage,
|
|
50
55
|
totalPages,
|
|
51
|
-
maxNumberButtons
|
|
56
|
+
isTabletPlus ? maxNumberButtons : 5
|
|
52
57
|
);
|
|
53
58
|
|
|
54
59
|
const baseStyle = css``;
|
|
@@ -61,23 +66,58 @@ const PaginationControls = ({
|
|
|
61
66
|
display: flex;
|
|
62
67
|
justify-content: center;
|
|
63
68
|
align-items: center;
|
|
64
|
-
gap: ${theme.margin.m8};
|
|
65
69
|
list-style: none;
|
|
66
|
-
|
|
70
|
+
gap: ${theme.margin.m4};
|
|
71
|
+
flex-wrap: nowrap;
|
|
67
72
|
|
|
68
|
-
|
|
69
|
-
|
|
73
|
+
@media (min-width: ${theme.breakpoints.tablet}px) {
|
|
74
|
+
gap: ${theme.margin.m8};
|
|
75
|
+
}
|
|
70
76
|
`;
|
|
71
77
|
|
|
72
|
-
const
|
|
73
|
-
|
|
78
|
+
const buttonBaseStyle = css`
|
|
79
|
+
@media (max-width: ${theme.breakpoints.tablet}px) {
|
|
80
|
+
gap: ${theme.margin.m4};
|
|
81
|
+
}
|
|
74
82
|
`;
|
|
75
83
|
|
|
76
|
-
const
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
84
|
+
const previousButtonStyle = cx(
|
|
85
|
+
buttonBaseStyle,
|
|
86
|
+
css`
|
|
87
|
+
margin-right: ${theme.margin.m16};
|
|
88
|
+
display: none;
|
|
89
|
+
|
|
90
|
+
@media (min-width: ${theme.breakpoints.tablet}px) {
|
|
91
|
+
display: inline-block;
|
|
92
|
+
}
|
|
93
|
+
`
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
const nextButtonStyle = cx(
|
|
97
|
+
buttonBaseStyle,
|
|
98
|
+
css`
|
|
99
|
+
margin-left: ${theme.margin.m16};
|
|
100
|
+
display: none;
|
|
101
|
+
|
|
102
|
+
@media (min-width: ${theme.breakpoints.tablet}px) {
|
|
103
|
+
display: inline-block;
|
|
104
|
+
}
|
|
105
|
+
`
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
const pageNumberButtonBaseStyle = cx(
|
|
109
|
+
buttonBaseStyle,
|
|
110
|
+
css`
|
|
111
|
+
height: 40px;
|
|
112
|
+
min-width: 40px;
|
|
113
|
+
padding: 0;
|
|
114
|
+
|
|
115
|
+
@media (min-width: ${theme.breakpoints.tablet}px) {
|
|
116
|
+
height: 48px;
|
|
117
|
+
min-width: 48px;
|
|
118
|
+
}
|
|
119
|
+
`
|
|
120
|
+
);
|
|
81
121
|
|
|
82
122
|
const pageNumberButtonStyle = cx(
|
|
83
123
|
pageNumberButtonBaseStyle,
|
|
@@ -110,6 +150,7 @@ const PaginationControls = ({
|
|
|
110
150
|
<Button
|
|
111
151
|
className={previousButtonStyle}
|
|
112
152
|
variant='tertiary'
|
|
153
|
+
size={isTabletPlus ? 'default' : 'small'}
|
|
113
154
|
aria-label='Go to previous page'
|
|
114
155
|
onClick={() => handlePageChange(currentPage - 1)}
|
|
115
156
|
disabled={currentPage === 1}
|
|
@@ -128,6 +169,7 @@ const PaginationControls = ({
|
|
|
128
169
|
<Button
|
|
129
170
|
className={pageNumberButtonStyle}
|
|
130
171
|
variant='secondary'
|
|
172
|
+
size={isTabletPlus ? 'default' : 'small'}
|
|
131
173
|
aria-label={`Page ${page}`}
|
|
132
174
|
onClick={() => handlePageChange(page)}
|
|
133
175
|
>
|
|
@@ -138,6 +180,7 @@ const PaginationControls = ({
|
|
|
138
180
|
<Button
|
|
139
181
|
className={currentPageNumberButtonStyle}
|
|
140
182
|
variant='secondary'
|
|
183
|
+
size={isTabletPlus ? 'default' : 'small'}
|
|
141
184
|
aria-label={`Page ${page}, current page`}
|
|
142
185
|
aria-current='page'
|
|
143
186
|
disabled
|
|
@@ -40,11 +40,15 @@ const PaginationInfo = ({
|
|
|
40
40
|
}[format];
|
|
41
41
|
|
|
42
42
|
const baseStyle = css`
|
|
43
|
-
margin: ${theme.margin.
|
|
43
|
+
margin: ${theme.margin.m16} 0;
|
|
44
44
|
text-align: center;
|
|
45
45
|
color: ${theme.color.text.secondary};
|
|
46
46
|
font-family: ${theme.font.family.primary};
|
|
47
47
|
font-size: ${theme.font.size.f16};
|
|
48
|
+
|
|
49
|
+
@media (min-width: ${theme.breakpoints.tablet}px) {
|
|
50
|
+
margin: ${theme.margin.m24} 0;
|
|
51
|
+
}
|
|
48
52
|
`;
|
|
49
53
|
|
|
50
54
|
const style = cx(NAME, baseStyle, className);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { HTMLAttributes, useState } from 'react';
|
|
1
|
+
import { HTMLAttributes, useRef, useState } from 'react';
|
|
2
2
|
import { css, cx } from '@emotion/css';
|
|
3
3
|
import { Input, Icon, IconButton, InputProps, IconButtonProps } from '../..';
|
|
4
4
|
import useTheme from '../../theme/useTheme';
|
|
@@ -38,6 +38,7 @@ const Search = ({
|
|
|
38
38
|
...props
|
|
39
39
|
}: SearchProps) => {
|
|
40
40
|
const [searchTerms, setSearchTerms] = useState<string>('');
|
|
41
|
+
const inputRef = useRef<HTMLInputElement>(null);
|
|
41
42
|
const [theme] = useTheme();
|
|
42
43
|
|
|
43
44
|
const handleSearchTermsChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
@@ -46,6 +47,7 @@ const Search = ({
|
|
|
46
47
|
|
|
47
48
|
const handleClearButtonClick = () => {
|
|
48
49
|
setSearchTerms('');
|
|
50
|
+
inputRef.current?.focus();
|
|
49
51
|
};
|
|
50
52
|
|
|
51
53
|
const handleSearchButtonClick = () => {
|
|
@@ -125,6 +127,7 @@ const Search = ({
|
|
|
125
127
|
{...props}
|
|
126
128
|
>
|
|
127
129
|
<Input
|
|
130
|
+
ref={inputRef}
|
|
128
131
|
inputClassName={inputStyle}
|
|
129
132
|
placeholder={placeholder}
|
|
130
133
|
type='text'
|
|
@@ -34,7 +34,7 @@ describe('Search', () => {
|
|
|
34
34
|
<Search />
|
|
35
35
|
</ThemeContextProvider>
|
|
36
36
|
);
|
|
37
|
-
const search = screen.
|
|
37
|
+
const search = screen.getByRole('textbox');
|
|
38
38
|
expect(search).toBeInTheDocument();
|
|
39
39
|
});
|
|
40
40
|
|
|
@@ -91,4 +91,22 @@ describe('Search', () => {
|
|
|
91
91
|
|
|
92
92
|
expect(mockOnSearch).toHaveBeenCalledWith('test search');
|
|
93
93
|
});
|
|
94
|
+
test('clear button clears text and returns focus to input', async () => {
|
|
95
|
+
const user = userEvent.setup();
|
|
96
|
+
render(
|
|
97
|
+
<ThemeContextProvider>
|
|
98
|
+
<Search placeholder='Search products...' />
|
|
99
|
+
</ThemeContextProvider>
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
const input = screen.getByTestId('ucl-uikit-search');
|
|
103
|
+
const clearButton = screen.getByTestId('ucl-uikit-search-clear-search-btn');
|
|
104
|
+
|
|
105
|
+
await user.type(input, 'hello');
|
|
106
|
+
await user.tab();
|
|
107
|
+
await user.click(clearButton);
|
|
108
|
+
|
|
109
|
+
expect((input as HTMLInputElement).value).toBe('');
|
|
110
|
+
expect(document.activeElement).toBe(input);
|
|
111
|
+
});
|
|
94
112
|
});
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import * as SelectStories from "./Select.stories";
|
|
2
|
+
import { Meta, Title, Subtitle, Canvas, Controls, ArgTypes } from "@storybook/blocks";
|
|
3
|
+
|
|
4
|
+
export const usage = {
|
|
5
|
+
default: `<Select
|
|
6
|
+
options={[
|
|
7
|
+
{ label: 'Option one', value: '1' },
|
|
8
|
+
{ label: 'Option two', value: '2' },
|
|
9
|
+
{ label: 'Option three', value: '3' },
|
|
10
|
+
]}
|
|
11
|
+
value={value}
|
|
12
|
+
onValueChange={handleChange}
|
|
13
|
+
/>`,
|
|
14
|
+
native: `<Select
|
|
15
|
+
native
|
|
16
|
+
options={[
|
|
17
|
+
{ label: 'Option one', value: '1' },
|
|
18
|
+
{ label: 'Option two', value: '2' },
|
|
19
|
+
{ label: 'Option three', value: '3' },
|
|
20
|
+
]}
|
|
21
|
+
value={value}
|
|
22
|
+
nativeHtmlAttributes={{ onChange: onNativeChange }}
|
|
23
|
+
/>`,
|
|
24
|
+
filterable: `<Select
|
|
25
|
+
filterable
|
|
26
|
+
options={[
|
|
27
|
+
{ label: 'Option one', value: '1' },
|
|
28
|
+
{ label: 'Option two', value: '2' },
|
|
29
|
+
{ label: 'Option three', value: '3' },
|
|
30
|
+
]}
|
|
31
|
+
value={value}
|
|
32
|
+
onValueChange={handleChange}
|
|
33
|
+
placeholder="Type to filter..."
|
|
34
|
+
filterInputProps={{ 'aria-describedby': 'filter-hint' }}
|
|
35
|
+
/>`,
|
|
36
|
+
selectionBehaviourCommit: `<Select
|
|
37
|
+
selectionBehaviour="commit"
|
|
38
|
+
options={[
|
|
39
|
+
{ label: 'Option one', value: '1' },
|
|
40
|
+
{ label: 'Option two', value: '2' },
|
|
41
|
+
{ label: 'Option three', value: '3' },
|
|
42
|
+
]}
|
|
43
|
+
value={value}
|
|
44
|
+
onValueChange={handleChange}
|
|
45
|
+
/>`,
|
|
46
|
+
disabled: `<Select
|
|
47
|
+
disabled
|
|
48
|
+
options={[
|
|
49
|
+
{ label: 'Option one', value: '1' },
|
|
50
|
+
{ label: 'Option two', value: '2' },
|
|
51
|
+
{ label: 'Option three', value: '3' },
|
|
52
|
+
]}
|
|
53
|
+
value={value}
|
|
54
|
+
onValueChange={handleChange}
|
|
55
|
+
/>`,
|
|
56
|
+
placeholder: `<Select
|
|
57
|
+
placeholder="Please select an option"
|
|
58
|
+
options={[
|
|
59
|
+
{ label: 'Option one', value: '1' },
|
|
60
|
+
{ label: 'Option two', value: '2' },
|
|
61
|
+
{ label: 'Option three', value: '3' },
|
|
62
|
+
]}
|
|
63
|
+
value={value}
|
|
64
|
+
onValueChange={handleChange}
|
|
65
|
+
/>`,
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
<Meta of={SelectStories} />
|
|
69
|
+
<Title />
|
|
70
|
+
<Subtitle>
|
|
71
|
+
A custom select with an optional native fallback, opt-in filtering, and long-text handling.
|
|
72
|
+
</Subtitle>
|
|
73
|
+
|
|
74
|
+
Use `<Select>` when you need a styled dropdown that works like a standard form control but matches the UCL UI kit. The component renders a custom combobox by default and can fall back to the native `<select>` for environments where native behaviour is preferred (e.g. mobile accessibility).
|
|
75
|
+
|
|
76
|
+
Key features:
|
|
77
|
+
- Custom dropdown with keyboard support (Enter/Space to open, arrows to navigate, Escape to close)
|
|
78
|
+
- Configurable keyboard selection behaviour via `selectionBehaviour` (`focus` or `commit`)
|
|
79
|
+
- Optional native fallback (`native`)
|
|
80
|
+
- Optional filterable dropdown (`filterable`) that lets users type to narrow options
|
|
81
|
+
- `filterInputProps` to forward additional attributes (e.g. `aria-describedby`) to the filter input
|
|
82
|
+
- Support for long option text via `lineBreak`
|
|
83
|
+
- Disabled state and placeholder support
|
|
84
|
+
- Story previews set a fixed control width and wrapper height for better readability in docs.
|
|
85
|
+
|
|
86
|
+
## Variants
|
|
87
|
+
|
|
88
|
+
### Custom (default)
|
|
89
|
+
The default renders a fully styled combobox that handles focus, keyboard navigation, and selection. Use this for most desktop/web cases.
|
|
90
|
+
|
|
91
|
+
<Canvas
|
|
92
|
+
of={SelectStories.Default}
|
|
93
|
+
sourceState="shown"
|
|
94
|
+
source={{ code: usage.default }}
|
|
95
|
+
/>
|
|
96
|
+
|
|
97
|
+
### Native
|
|
98
|
+
Set `native` to render the browser `<select>`. This is useful when you want built-in mobile picker behaviour or the simplest possible semantics. Props like `onChange`, `value`, and standard `<select>` attributes can be passed via `nativeHtmlAttributes`.
|
|
99
|
+
|
|
100
|
+
<Canvas
|
|
101
|
+
of={SelectStories.Native}
|
|
102
|
+
sourceState="shown"
|
|
103
|
+
source={{ code: usage.native }}
|
|
104
|
+
/>
|
|
105
|
+
|
|
106
|
+
### Disabled
|
|
107
|
+
Applies the disabled state to both custom and native variants; interactions are blocked and styles are subdued.
|
|
108
|
+
|
|
109
|
+
<Canvas
|
|
110
|
+
of={SelectStories.Disabled}
|
|
111
|
+
sourceState="shown"
|
|
112
|
+
source={{ code: usage.disabled }}
|
|
113
|
+
/>
|
|
114
|
+
|
|
115
|
+
### Placeholder
|
|
116
|
+
Shows helper text until a value is selected. In the native variant it appears as a disabled option; in the custom variant it shows in the visible field.
|
|
117
|
+
|
|
118
|
+
<Canvas
|
|
119
|
+
of={SelectStories.WithPlaceholder}
|
|
120
|
+
sourceState="shown"
|
|
121
|
+
source={{ code: usage.placeholder }}
|
|
122
|
+
/>
|
|
123
|
+
|
|
124
|
+
### With filter
|
|
125
|
+
Adds an inline text input when the dropdown is open so users can type to filter options. Arrow keys and Escape still control the dropdown; the filter text is preserved while open and cleared on close.
|
|
126
|
+
|
|
127
|
+
Use `filterInputProps` to pass extra attributes to the input (e.g. `aria-describedby`, `inputMode`).
|
|
128
|
+
|
|
129
|
+
<Canvas
|
|
130
|
+
of={SelectStories.filterable}
|
|
131
|
+
sourceState="shown"
|
|
132
|
+
source={{ code: usage.filterable }}
|
|
133
|
+
/>
|
|
134
|
+
|
|
135
|
+
### Selection behaviour
|
|
136
|
+
Use `selectionBehaviour` to control how keyboard navigation commits value in the custom variant:
|
|
137
|
+
- `focus` (default): arrow keys move and commit immediately.
|
|
138
|
+
- `commit`: arrow keys only highlight; Enter or click commits.
|
|
139
|
+
|
|
140
|
+
<Canvas
|
|
141
|
+
of={SelectStories.SelectionBehaviourCommit}
|
|
142
|
+
sourceState="shown"
|
|
143
|
+
source={{ code: usage.selectionBehaviourCommit }}
|
|
144
|
+
/>
|
|
145
|
+
|
|
146
|
+
## Long text and wrapping
|
|
147
|
+
For very long labels, set `lineBreak` to allow breaking the text inside options. Without it, text is truncated/ellipsized to keep layout intact.
|
|
148
|
+
|
|
149
|
+
## Accessibility
|
|
150
|
+
- Custom variant exposes `role="combobox"` and `aria-expanded`, and listens for Enter/Space/Escape/arrow keys.
|
|
151
|
+
- In `selectionBehaviour="commit"`, arrow keys move highlight without changing value until Enter/click.
|
|
152
|
+
- Native variant leverages built-in browser semantics.
|
|
153
|
+
- The filter input (when `filterable` is true) is focusable when the panel opens; Escape and arrow keys bubble to the parent so the dropdown still closes/navigates.
|
|
154
|
+
- `filterInputProps` lets you forward ARIA hooks like `aria-describedby` to the filter input.
|
|
155
|
+
- Standard HTML attributes (`id`, `title`, `data-*`) are forwarded to the combobox/native select.
|
|
156
|
+
|
|
157
|
+
## Props
|
|
158
|
+
|
|
159
|
+
Full props specification for `<Select>` is below.
|
|
160
|
+
|
|
161
|
+
<Canvas
|
|
162
|
+
of={SelectStories.Default}
|
|
163
|
+
sourceState="hidden"
|
|
164
|
+
source={{ code: usage.default }}
|
|
165
|
+
/>
|
|
166
|
+
|
|
167
|
+
You can use the controls below to manipulate the `<Select>` component above.
|
|
168
|
+
|
|
169
|
+
<Controls />
|