@indico-data/design-system 2.53.0 → 2.55.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/lib/components/forms/date/inputDateTimePicker/SingleInputDateTimePicker.d.ts +24 -0
- package/lib/components/forms/date/inputDateTimePicker/SingleInputDateTimePicker.stories.d.ts +6 -0
- package/lib/components/forms/date/inputDateTimePicker/helpers.d.ts +1 -0
- package/lib/components/forms/date/inputDateTimePicker/index.d.ts +1 -0
- package/lib/components/forms/subcomponents/DisplayFormError.d.ts +2 -1
- package/lib/components/forms/timePicker/TimePicker.d.ts +3 -1
- package/lib/components/forms/timePicker/helpers.d.ts +2 -5
- package/lib/components/index.d.ts +3 -0
- package/lib/components/loading-indicators/BarSpinner/BarSpinner.d.ts +8 -0
- package/lib/components/loading-indicators/BarSpinner/BarSpinner.stories.d.ts +7 -0
- package/lib/components/loading-indicators/BarSpinner/__tests__/BarSpinner.test.d.ts +1 -0
- package/lib/components/loading-indicators/CirclePulse/CirclePulse.d.ts +7 -0
- package/lib/components/loading-indicators/CirclePulse/CirclePulse.stories.d.ts +6 -0
- package/lib/components/tooltip/Tooltip.d.ts +13 -0
- package/lib/components/tooltip/Tooltip.stories.d.ts +6 -0
- package/lib/index.css +149 -90
- package/lib/index.d.ts +53 -47
- package/lib/index.esm.css +149 -90
- package/lib/index.esm.js +5964 -6155
- package/lib/index.esm.js.map +1 -1
- package/lib/index.js +5962 -6153
- package/lib/index.js.map +1 -1
- package/package.json +1 -1
- package/src/components/forms/date/datePicker/DatePicker.stories.tsx +4 -4
- package/src/components/forms/date/datePicker/DatePicker.tsx +0 -2
- package/src/components/forms/date/inputDateTimePicker/SingleInputDateTimePicker.mdx +20 -0
- package/src/components/forms/date/inputDateTimePicker/SingleInputDateTimePicker.stories.tsx +262 -0
- package/src/components/forms/date/inputDateTimePicker/SingleInputDateTimePicker.tsx +141 -0
- package/src/components/forms/date/inputDateTimePicker/helpers.ts +3 -0
- package/src/components/forms/date/inputDateTimePicker/index.ts +1 -0
- package/src/components/forms/input/Input.tsx +1 -1
- package/src/components/forms/passwordInput/PasswordInput.stories.tsx +1 -1
- package/src/components/forms/subcomponents/DisplayFormError.tsx +7 -2
- package/src/components/forms/timePicker/TimePicker.mdx +3 -27
- package/src/components/forms/timePicker/TimePicker.stories.tsx +19 -1
- package/src/components/forms/timePicker/TimePicker.tsx +37 -80
- package/src/components/forms/timePicker/__tests__/TimePicker.test.tsx +33 -11
- package/src/components/forms/timePicker/helpers.ts +61 -13
- package/src/components/index.ts +3 -0
- package/src/components/loading-indicators/BarSpinner/BarSpinner.mdx +21 -0
- package/src/components/loading-indicators/BarSpinner/BarSpinner.stories.tsx +89 -0
- package/src/components/loading-indicators/BarSpinner/BarSpinner.tsx +25 -0
- package/src/components/loading-indicators/BarSpinner/__tests__/BarSpinner.test.tsx +16 -0
- package/src/{legacy/components/loading-indicators/BarSpinner/BarSpinner.styles.ts → components/loading-indicators/BarSpinner/styles/BarSpinner.scss} +11 -11
- package/src/components/loading-indicators/CirclePulse/CirclePulse.mdx +20 -0
- package/src/components/loading-indicators/CirclePulse/CirclePulse.scss +83 -0
- package/src/components/loading-indicators/CirclePulse/CirclePulse.stories.tsx +78 -0
- package/src/components/loading-indicators/CirclePulse/CirclePulse.tsx +15 -0
- package/src/components/tanstackTable/components/TableBody/TableBody.tsx +1 -1
- package/src/components/tooltip/Tooltip.mdx +25 -0
- package/src/components/tooltip/Tooltip.stories.tsx +113 -0
- package/src/components/tooltip/Tooltip.tsx +38 -0
- package/src/components/tooltip/styles/Tooltip.scss +8 -0
- package/src/index.ts +4 -2
- package/src/styles/globals.scss +9 -0
- package/src/styles/index.scss +3 -2
- package/lib/components/forms/timePicker/constants.d.ts +0 -13
- package/lib/legacy/components/Tooltip/Tooltip.d.ts +0 -15
- package/lib/legacy/components/Tooltip/Tooltip.stories.d.ts +0 -9
- package/lib/legacy/components/Tooltip/Tooltip.styles.d.ts +0 -1
- package/lib/legacy/components/index.d.ts +0 -2
- package/lib/legacy/components/loading-indicators/BarSpinner/BarSpinner.d.ts +0 -7
- package/lib/legacy/components/loading-indicators/BarSpinner/BarSpinner.stories.d.ts +0 -10
- package/lib/legacy/components/loading-indicators/BarSpinner/BarSpinner.styles.d.ts +0 -1
- package/lib/legacy/components/loading-indicators/CirclePulse/CirclePulse.d.ts +0 -10
- package/lib/legacy/components/loading-indicators/CirclePulse/CirclePulse.stories.d.ts +0 -12
- package/lib/legacy/components/loading-indicators/CirclePulse/CirclePulse.styles.d.ts +0 -7
- package/lib/legacy/components/loading-indicators/CircleSpinner/CircleSpinner.d.ts +0 -10
- package/lib/legacy/components/loading-indicators/CircleSpinner/CircleSpinner.stories.d.ts +0 -10
- package/lib/legacy/components/loading-indicators/CircleSpinner/index.d.ts +0 -1
- package/lib/legacy/components/loading-indicators/index.d.ts +0 -3
- package/src/components/forms/timePicker/constants.ts +0 -21
- package/src/components/forms/timePicker/styles/TimePicker.scss +0 -51
- package/src/legacy/components/Tooltip/Tooltip.stories.tsx +0 -107
- package/src/legacy/components/Tooltip/Tooltip.styles.ts +0 -64
- package/src/legacy/components/Tooltip/Tooltip.tsx +0 -70
- package/src/legacy/components/index.ts +0 -2
- package/src/legacy/components/loading-indicators/BarSpinner/BarSpinner.stories.tsx +0 -14
- package/src/legacy/components/loading-indicators/BarSpinner/BarSpinner.tsx +0 -21
- package/src/legacy/components/loading-indicators/CirclePulse/CirclePulse.stories.tsx +0 -22
- package/src/legacy/components/loading-indicators/CirclePulse/CirclePulse.styles.ts +0 -77
- package/src/legacy/components/loading-indicators/CirclePulse/CirclePulse.tsx +0 -57
- package/src/legacy/components/loading-indicators/CircleSpinner/CircleSpinner.stories.tsx +0 -16
- package/src/legacy/components/loading-indicators/CircleSpinner/CircleSpinner.tsx +0 -36
- package/src/legacy/components/loading-indicators/CircleSpinner/index.ts +0 -1
- package/src/legacy/components/loading-indicators/index.ts +0 -3
- /package/lib/{legacy/components → components}/loading-indicators/BarSpinner/index.d.ts +0 -0
- /package/lib/{legacy/components → components}/loading-indicators/CirclePulse/index.d.ts +0 -0
- /package/lib/{legacy/components/Tooltip → components/tooltip}/index.d.ts +0 -0
- /package/src/{legacy/components → components}/loading-indicators/BarSpinner/index.ts +0 -0
- /package/src/{legacy/components → components}/loading-indicators/CirclePulse/index.ts +0 -0
- /package/src/{legacy/components/Tooltip → components/tooltip}/index.ts +0 -0
|
@@ -1,104 +1,61 @@
|
|
|
1
|
-
import { useState } from 'react';
|
|
2
|
-
import { Select } from '../select/Select';
|
|
1
|
+
import { ChangeEvent, useState, FocusEvent } from 'react';
|
|
3
2
|
import { Input } from '../input/Input';
|
|
4
|
-
import {
|
|
5
|
-
import { FloatUI } from '../..';
|
|
6
|
-
import { hourOptions, minuteOptions, periodOptions } from './constants';
|
|
7
|
-
import { parseTimeValue } from './helpers';
|
|
8
|
-
import { SelectOption } from '../select/types';
|
|
9
|
-
import { SingleValue, MultiValue } from 'react-select';
|
|
3
|
+
import { formatTimeValue, validateInputValue } from './helpers';
|
|
10
4
|
|
|
11
5
|
interface TimePickerProps {
|
|
12
6
|
timeValue?: string;
|
|
13
7
|
label?: string;
|
|
8
|
+
name?: string;
|
|
9
|
+
hasHiddenLabel?: boolean;
|
|
14
10
|
onTimeChange?: (time: string) => void;
|
|
15
11
|
}
|
|
16
12
|
|
|
17
13
|
export const TimePicker = ({
|
|
18
14
|
timeValue = '',
|
|
19
15
|
label = 'Time Picker',
|
|
16
|
+
name = 'time-picker',
|
|
17
|
+
hasHiddenLabel = false,
|
|
20
18
|
onTimeChange,
|
|
21
19
|
}: TimePickerProps) => {
|
|
22
|
-
const
|
|
23
|
-
const [
|
|
24
|
-
const [minutes, setMinutes] = useState(initialTime.minutes);
|
|
25
|
-
const [period, setPeriod] = useState(initialTime.period);
|
|
20
|
+
const [validationError, setValidationError] = useState('');
|
|
21
|
+
const [inputValue, setInputValue] = useState(timeValue);
|
|
26
22
|
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
23
|
+
const handleTimeChange = (e: ChangeEvent<HTMLInputElement>) => {
|
|
24
|
+
const newValue = e.target.value;
|
|
25
|
+
const error = validateInputValue(newValue);
|
|
26
|
+
setInputValue(newValue);
|
|
27
|
+
setValidationError(error);
|
|
31
28
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
29
|
+
// Only send valid values to parent component
|
|
30
|
+
if (!error || error === '') {
|
|
31
|
+
onTimeChange?.(newValue);
|
|
32
|
+
}
|
|
35
33
|
};
|
|
36
34
|
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
|
|
35
|
+
const handleBlur = (e: FocusEvent<HTMLInputElement>) => {
|
|
36
|
+
const currentValue = e.target.value;
|
|
37
|
+
|
|
38
|
+
// Only format if there's no validation error
|
|
39
|
+
if (validationError === '' && currentValue.trim() !== '') {
|
|
40
|
+
const formattedValue = formatTimeValue(currentValue);
|
|
41
|
+
setInputValue(formattedValue);
|
|
42
|
+
onTimeChange?.(formattedValue);
|
|
43
|
+
}
|
|
40
44
|
};
|
|
41
45
|
|
|
42
46
|
return (
|
|
43
47
|
<div className="time-input-wrapper">
|
|
44
|
-
<
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
<Col xs="content">
|
|
56
|
-
<Select
|
|
57
|
-
menuIsOpen
|
|
58
|
-
className="hour-menu"
|
|
59
|
-
components={{ DropdownIndicator: () => null, IndicatorSeparator: () => null }}
|
|
60
|
-
name="hours"
|
|
61
|
-
value={{ label: hours, value: hours }}
|
|
62
|
-
onChange={
|
|
63
|
-
handleHourChange as (
|
|
64
|
-
newValue: SingleValue<SelectOption> | MultiValue<SelectOption>,
|
|
65
|
-
) => void
|
|
66
|
-
}
|
|
67
|
-
options={hourOptions}
|
|
68
|
-
/>
|
|
69
|
-
</Col>
|
|
70
|
-
<Col xs="content">
|
|
71
|
-
<Select
|
|
72
|
-
className="minute-menu"
|
|
73
|
-
components={{ DropdownIndicator: () => null, IndicatorSeparator: () => null }}
|
|
74
|
-
name="minutes"
|
|
75
|
-
options={minuteOptions}
|
|
76
|
-
menuIsOpen
|
|
77
|
-
value={{ label: minutes.padStart(2, '0'), value: minutes }}
|
|
78
|
-
onChange={
|
|
79
|
-
handleMinuteChange as (
|
|
80
|
-
newValue: SingleValue<SelectOption> | MultiValue<SelectOption>,
|
|
81
|
-
) => void
|
|
82
|
-
}
|
|
83
|
-
/>
|
|
84
|
-
</Col>
|
|
85
|
-
<Col xs="content">
|
|
86
|
-
<Select
|
|
87
|
-
menuIsOpen
|
|
88
|
-
className="period-menu"
|
|
89
|
-
components={{ DropdownIndicator: () => null, IndicatorSeparator: () => null }}
|
|
90
|
-
name="period"
|
|
91
|
-
options={periodOptions}
|
|
92
|
-
value={{ label: period.toUpperCase(), value: period }}
|
|
93
|
-
onChange={
|
|
94
|
-
handlePeriodChange as (
|
|
95
|
-
newValue: SingleValue<SelectOption> | MultiValue<SelectOption>,
|
|
96
|
-
) => void
|
|
97
|
-
}
|
|
98
|
-
/>
|
|
99
|
-
</Col>
|
|
100
|
-
</Row>
|
|
101
|
-
</FloatUI>
|
|
48
|
+
<Input
|
|
49
|
+
data-testid={`${name}-input`}
|
|
50
|
+
label={label}
|
|
51
|
+
hasHiddenLabel={hasHiddenLabel}
|
|
52
|
+
value={inputValue}
|
|
53
|
+
maxLength={8}
|
|
54
|
+
onChange={handleTimeChange}
|
|
55
|
+
onBlur={handleBlur}
|
|
56
|
+
name={name}
|
|
57
|
+
errorMessage={validationError}
|
|
58
|
+
/>
|
|
102
59
|
</div>
|
|
103
60
|
);
|
|
104
61
|
};
|
|
@@ -3,24 +3,46 @@ import { TimePicker } from '@/components/forms/timePicker/TimePicker';
|
|
|
3
3
|
import userEvent from '@testing-library/user-event';
|
|
4
4
|
|
|
5
5
|
describe('TimePicker', () => {
|
|
6
|
-
it('
|
|
7
|
-
render(
|
|
6
|
+
it('renders an error when the input is invalid', async () => {
|
|
7
|
+
render(
|
|
8
|
+
<TimePicker
|
|
9
|
+
name="time-picker"
|
|
10
|
+
label="Time Picker"
|
|
11
|
+
hasHiddenLabel
|
|
12
|
+
data-testid="time-picker-input"
|
|
13
|
+
/>,
|
|
14
|
+
);
|
|
15
|
+
|
|
8
16
|
const timePickerInput = screen.getByTestId('time-picker-input');
|
|
9
17
|
await userEvent.click(timePickerInput);
|
|
10
18
|
|
|
11
|
-
|
|
12
|
-
|
|
19
|
+
await userEvent.type(timePickerInput, '13:00');
|
|
20
|
+
|
|
21
|
+
const timePickerError = screen.getByTestId('time-picker-error');
|
|
22
|
+
expect(timePickerError).toBeInTheDocument();
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('formats the time input when the value is valid and a user clicks away from the input', async () => {
|
|
26
|
+
render(
|
|
27
|
+
<TimePicker
|
|
28
|
+
name="time-picker"
|
|
29
|
+
label="Time Picker"
|
|
30
|
+
hasHiddenLabel
|
|
31
|
+
data-testid="time-picker-input"
|
|
32
|
+
/>,
|
|
33
|
+
);
|
|
13
34
|
|
|
14
|
-
|
|
35
|
+
const timePickerInput = screen.getByTestId('time-picker-input');
|
|
36
|
+
await userEvent.click(timePickerInput);
|
|
15
37
|
|
|
16
|
-
|
|
17
|
-
await userEvent.click(minuteOption);
|
|
38
|
+
await userEvent.type(timePickerInput, '1:00pm');
|
|
18
39
|
|
|
19
|
-
|
|
40
|
+
const timePickerError = screen.queryByTestId('time-picker-error');
|
|
41
|
+
expect(timePickerError).not.toBeInTheDocument();
|
|
20
42
|
|
|
21
|
-
|
|
22
|
-
await userEvent.click(periodOption);
|
|
43
|
+
await userEvent.click(document.body);
|
|
23
44
|
|
|
24
|
-
expect(timePickerInput).toHaveValue('1:
|
|
45
|
+
expect(timePickerInput).toHaveValue('1:00 PM');
|
|
46
|
+
expect(timePickerError).not.toBeInTheDocument();
|
|
25
47
|
});
|
|
26
48
|
});
|
|
@@ -1,14 +1,62 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
1
|
+
export const formatTimeValue = (value: string): string => {
|
|
2
|
+
if (!value || value.trim() === '') {
|
|
3
|
+
return '';
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
// Normalize the input
|
|
7
|
+
const normalizedValue = value.trim().toLowerCase();
|
|
8
|
+
|
|
9
|
+
// Extract time components using regex
|
|
10
|
+
const timeRegex = /^(0?[1-9]|1[0-2]):([0-5][0-9])(\s*)([ap]m)$/i;
|
|
11
|
+
const matches = normalizedValue.match(timeRegex);
|
|
12
|
+
|
|
13
|
+
if (matches) {
|
|
14
|
+
const hours = parseInt(matches[1], 10);
|
|
15
|
+
const minutes = parseInt(matches[2], 10);
|
|
16
|
+
const period = matches[4].toUpperCase(); // Convert am/pm to AM/PM
|
|
17
|
+
|
|
18
|
+
// Format as hh:mm AM/PM
|
|
19
|
+
return `${hours}:${minutes < 10 ? '0' + minutes : minutes} ${period}`;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return value; // Return original if no match (shouldn't happen with valid inputs)
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export const validateInputValue = (value: string): string => {
|
|
26
|
+
if (!value || value.trim() === '') {
|
|
27
|
+
return ''; // Empty input is allowed
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Normalize the input (remove extra spaces, convert to lowercase)
|
|
31
|
+
const normalizedValue = value.trim().toLowerCase();
|
|
32
|
+
|
|
33
|
+
// Regular expression for valid time formats: 1:10 pm, 01:10 pm, 1:10pm, 01:10pm
|
|
34
|
+
const timeRegex = /^(0?[1-9]|1[0-2]):([0-5][0-9])(\s*)([ap]m)$/i;
|
|
35
|
+
|
|
36
|
+
if (!timeRegex.test(normalizedValue)) {
|
|
37
|
+
// Check if the issue might be a 24-hour format
|
|
38
|
+
const hour24Regex = /^([13-9]|1[3-9]|2[0-3]):/i;
|
|
39
|
+
if (hour24Regex.test(normalizedValue)) {
|
|
40
|
+
return 'Please enter time in 12-hour format (e.g., 1:10 pm)';
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return 'Please enter a valid time format (e.g., 1:10 pm)';
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Extract hours and minutes for additional validation
|
|
47
|
+
const matches = normalizedValue.match(timeRegex);
|
|
48
|
+
if (matches) {
|
|
49
|
+
const hours = parseInt(matches[1], 10);
|
|
50
|
+
const minutes = parseInt(matches[2], 10);
|
|
51
|
+
|
|
52
|
+
if (hours < 1 || hours > 12) {
|
|
53
|
+
return 'Hours must be between 1 and 12';
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (minutes < 0 || minutes > 59) {
|
|
57
|
+
return 'Minutes must be between 0 and 59';
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return ''; // Valid time format
|
|
14
62
|
};
|
package/src/components/index.ts
CHANGED
|
@@ -22,3 +22,6 @@ export { Modal } from './modal';
|
|
|
22
22
|
export { Badge } from './badge';
|
|
23
23
|
export { Pagination } from './pagination';
|
|
24
24
|
export { TanstackTable } from './tanstackTable';
|
|
25
|
+
export { Tooltip } from './tooltip';
|
|
26
|
+
export { BarSpinner } from './loading-indicators/BarSpinner/BarSpinner';
|
|
27
|
+
export { CirclePulse } from './loading-indicators/CirclePulse/CirclePulse';
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { Canvas, Meta, Controls, Story } from '@storybook/blocks';
|
|
2
|
+
import * as BarSpinner from './BarSpinner.stories';
|
|
3
|
+
|
|
4
|
+
<Meta title="Utilities/BarSpinner" name="BarSpinner" of={BarSpinner} />
|
|
5
|
+
|
|
6
|
+
# BarSpinner
|
|
7
|
+
|
|
8
|
+
This is a simple looping animation component that can indicate a loading state
|
|
9
|
+
|
|
10
|
+
<Canvas of={BarSpinner.Default} />
|
|
11
|
+
|
|
12
|
+
### The following props are available for the BarSpinner component:
|
|
13
|
+
|
|
14
|
+
<Controls of={BarSpinner.Default} />
|
|
15
|
+
|
|
16
|
+
## Usage
|
|
17
|
+
Simple usage of the BarSpinner component:
|
|
18
|
+
<Canvas of={BarSpinner.WithWidthAndHeight} />
|
|
19
|
+
```tsx
|
|
20
|
+
<BarSpinner id="bar-spinner-optional-id" className="optional-class-name" width={200} height={10} />
|
|
21
|
+
```
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { Meta, StoryObj } from '@storybook/react';
|
|
2
|
+
import { BarSpinner } from './BarSpinner';
|
|
3
|
+
import { Col, Container, Row } from '../../grid';
|
|
4
|
+
|
|
5
|
+
const meta: Meta = {
|
|
6
|
+
title: 'Utilities/BarSpinner',
|
|
7
|
+
component: BarSpinner,
|
|
8
|
+
argTypes: {
|
|
9
|
+
className: {
|
|
10
|
+
control: false,
|
|
11
|
+
description: 'The class name of the bar spinner',
|
|
12
|
+
table: {
|
|
13
|
+
category: 'Props',
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
'data-testid': {
|
|
17
|
+
table: {
|
|
18
|
+
disable: true,
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
width: {
|
|
22
|
+
control: false,
|
|
23
|
+
description: 'The width of the bar spinner',
|
|
24
|
+
table: {
|
|
25
|
+
category: 'Props',
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
height: {
|
|
29
|
+
control: false,
|
|
30
|
+
description: 'The height of the bar spinner',
|
|
31
|
+
table: {
|
|
32
|
+
category: 'Props',
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
id: {
|
|
36
|
+
control: false,
|
|
37
|
+
description:
|
|
38
|
+
"The id of the tooltip. This needs to match the element that holds it's data attribute",
|
|
39
|
+
table: {
|
|
40
|
+
category: 'Props',
|
|
41
|
+
type: {
|
|
42
|
+
summary: 'string',
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export default meta;
|
|
50
|
+
|
|
51
|
+
type Story = StoryObj<typeof BarSpinner>;
|
|
52
|
+
|
|
53
|
+
export const Default: Story = {
|
|
54
|
+
args: {
|
|
55
|
+
id: 'bar-spinner-1',
|
|
56
|
+
},
|
|
57
|
+
|
|
58
|
+
render: (args) => {
|
|
59
|
+
return (
|
|
60
|
+
<Container>
|
|
61
|
+
<Row>
|
|
62
|
+
<Col sm={4}>
|
|
63
|
+
<BarSpinner id={args.id} />
|
|
64
|
+
</Col>
|
|
65
|
+
</Row>
|
|
66
|
+
</Container>
|
|
67
|
+
);
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
export const WithWidthAndHeight: Story = {
|
|
72
|
+
args: {
|
|
73
|
+
id: 'bar-spinner-1',
|
|
74
|
+
width: 200,
|
|
75
|
+
height: 10,
|
|
76
|
+
},
|
|
77
|
+
|
|
78
|
+
render: (args) => {
|
|
79
|
+
return (
|
|
80
|
+
<Container>
|
|
81
|
+
<Row>
|
|
82
|
+
<Col sm={4}>
|
|
83
|
+
<BarSpinner id={args.id} width={args.width} height={args.height} />
|
|
84
|
+
</Col>
|
|
85
|
+
</Row>
|
|
86
|
+
</Container>
|
|
87
|
+
);
|
|
88
|
+
},
|
|
89
|
+
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export function BarSpinner({
|
|
2
|
+
width,
|
|
3
|
+
id,
|
|
4
|
+
height,
|
|
5
|
+
className,
|
|
6
|
+
...rest
|
|
7
|
+
}: {
|
|
8
|
+
'data-testid'?: string;
|
|
9
|
+
width?: number;
|
|
10
|
+
id?: string;
|
|
11
|
+
height?: number;
|
|
12
|
+
className?: string;
|
|
13
|
+
[key: string]: any;
|
|
14
|
+
}) {
|
|
15
|
+
// Create style object only if width or height exist
|
|
16
|
+
const style: React.CSSProperties = {};
|
|
17
|
+
if (width !== undefined) style.width = `${width}px`;
|
|
18
|
+
if (height !== undefined) style.height = `${height}px`;
|
|
19
|
+
|
|
20
|
+
return (
|
|
21
|
+
<div className={`bar-spinner ${className}`} id={id} style={style} {...rest}>
|
|
22
|
+
<span />
|
|
23
|
+
</div>
|
|
24
|
+
);
|
|
25
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { render, screen } from '@testing-library/react';
|
|
2
|
+
import { BarSpinner } from '../BarSpinner';
|
|
3
|
+
|
|
4
|
+
describe('BarSpinner Component', () => {
|
|
5
|
+
it('Renders the bar spinner without style props', () => {
|
|
6
|
+
render(<BarSpinner id="bar-spinner-1" data-testid="bar-spinner-1" />);
|
|
7
|
+
expect(screen.getByTestId('bar-spinner-1')).toBeInTheDocument();
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
it('Renders the bar spinner with prop width and height', () => {
|
|
11
|
+
render(<BarSpinner id="bar-spinner-1" width={200} height={10} data-testid="bar-spinner-1" />);
|
|
12
|
+
expect(screen.getByTestId('bar-spinner-1')).toBeInTheDocument();
|
|
13
|
+
expect(screen.getByTestId('bar-spinner-1')).toHaveStyle('width: 200px');
|
|
14
|
+
expect(screen.getByTestId('bar-spinner-1')).toHaveStyle('height: 10px');
|
|
15
|
+
});
|
|
16
|
+
});
|
|
@@ -1,20 +1,20 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
const moveBg = keyframes`
|
|
4
|
-
from {
|
|
1
|
+
@keyframes moveBg {
|
|
2
|
+
0% {
|
|
5
3
|
transform: translateX(0);
|
|
6
4
|
}
|
|
7
|
-
|
|
5
|
+
100% {
|
|
8
6
|
transform: translateX(46px);
|
|
9
7
|
}
|
|
10
|
-
|
|
8
|
+
}
|
|
11
9
|
|
|
12
|
-
|
|
10
|
+
.bar-spinner {
|
|
13
11
|
display: inline-block;
|
|
14
|
-
width: 200px;
|
|
15
|
-
height: 14px; /* Can be anything */
|
|
16
12
|
position: relative;
|
|
13
|
+
width: 100%;
|
|
14
|
+
height: 100%;
|
|
15
|
+
min-height: 14px;
|
|
17
16
|
border-radius: 25px;
|
|
17
|
+
background-color: var(--pf-primary-color-800);
|
|
18
18
|
|
|
19
19
|
> span {
|
|
20
20
|
display: block;
|
|
@@ -39,7 +39,7 @@ export const StyledBarSpinner = styled.div`
|
|
|
39
39
|
transparent 12px,
|
|
40
40
|
transparent 20px
|
|
41
41
|
);
|
|
42
|
-
animation-name:
|
|
42
|
+
animation-name: moveBg;
|
|
43
43
|
animation-duration: 1s;
|
|
44
44
|
animation-timing-function: linear;
|
|
45
45
|
animation-iteration-count: infinite;
|
|
@@ -47,4 +47,4 @@ export const StyledBarSpinner = styled.div`
|
|
|
47
47
|
overflow: hidden;
|
|
48
48
|
}
|
|
49
49
|
}
|
|
50
|
-
|
|
50
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { Canvas, Meta, Controls, Story } from '@storybook/blocks';
|
|
2
|
+
import * as CirclePulse from './CirclePulse.stories';
|
|
3
|
+
|
|
4
|
+
<Meta title="Utilities/CirclePulse" name="CirclePulse" of={CirclePulse} />
|
|
5
|
+
|
|
6
|
+
# CirclePulse
|
|
7
|
+
|
|
8
|
+
This is a simple looping animation component that can indicate a loading state
|
|
9
|
+
|
|
10
|
+
<Canvas of={CirclePulse.Default} />
|
|
11
|
+
|
|
12
|
+
### The following props are available for the CirclePulse component:
|
|
13
|
+
|
|
14
|
+
<Controls of={CirclePulse.Default} />
|
|
15
|
+
|
|
16
|
+
## Usage
|
|
17
|
+
|
|
18
|
+
```tsx
|
|
19
|
+
<CirclePulse size="md" id="circle-pulse-optional-id" className="optional-class-name" />
|
|
20
|
+
```
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
:root {
|
|
2
|
+
--circle-pulse-animation-speed: 1.4s;
|
|
3
|
+
--circle-pulse-border-width: var(--pf-border-xl);
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
// Size map with increments of 20px
|
|
7
|
+
$circle-pulse-sizes: (
|
|
8
|
+
'xxs': 20px,
|
|
9
|
+
'xs': 40px,
|
|
10
|
+
'sm': 60px,
|
|
11
|
+
'md': 80px,
|
|
12
|
+
'lg': 100px,
|
|
13
|
+
'xl': 120px,
|
|
14
|
+
'xxl': 140px,
|
|
15
|
+
);
|
|
16
|
+
|
|
17
|
+
// Generate size classes for the pulse only
|
|
18
|
+
@each $size, $value in $circle-pulse-sizes {
|
|
19
|
+
.circle-pulse-#{$size} {
|
|
20
|
+
--circle-pulse-size: #{$value};
|
|
21
|
+
--circle-pulse-ripple-size: calc(#{$value} * 1.7);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
@keyframes ripple {
|
|
26
|
+
0% {
|
|
27
|
+
transform: translate(-50%, -50%) scale(0);
|
|
28
|
+
opacity: 0;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
20% {
|
|
32
|
+
opacity: 1;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
100% {
|
|
36
|
+
transform: translate(-50%, -50%) scale(1);
|
|
37
|
+
opacity: 0;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
.circle-pulse {
|
|
42
|
+
display: grid;
|
|
43
|
+
place-items: center;
|
|
44
|
+
position: relative;
|
|
45
|
+
|
|
46
|
+
width: var(--circle-pulse-size);
|
|
47
|
+
height: var(--circle-pulse-size);
|
|
48
|
+
|
|
49
|
+
margin-left: auto;
|
|
50
|
+
margin-right: auto;
|
|
51
|
+
|
|
52
|
+
&:before,
|
|
53
|
+
&:after {
|
|
54
|
+
content: '';
|
|
55
|
+
position: absolute;
|
|
56
|
+
border: var(--circle-pulse-border-width) solid var(--pf-primary-color-300);
|
|
57
|
+
border-radius: 50%;
|
|
58
|
+
|
|
59
|
+
/* Set initial dimensions */
|
|
60
|
+
width: var(--circle-pulse-ripple-size);
|
|
61
|
+
height: var(--circle-pulse-ripple-size);
|
|
62
|
+
|
|
63
|
+
/* Position at center */
|
|
64
|
+
top: 50%;
|
|
65
|
+
left: 50%;
|
|
66
|
+
transform: translate(-50%, -50%) scale(0);
|
|
67
|
+
transform-origin: center;
|
|
68
|
+
|
|
69
|
+
animation: ripple var(--circle-pulse-animation-speed) cubic-bezier(0, 0.2, 0.8, 1) infinite;
|
|
70
|
+
|
|
71
|
+
@media (prefers-reduced-motion) {
|
|
72
|
+
animation-duration: calc(var(--circle-pulse-animation-speed) * 2);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
&:after {
|
|
77
|
+
animation-delay: calc(var(--circle-pulse-animation-speed) * -0.5);
|
|
78
|
+
|
|
79
|
+
@media (prefers-reduced-motion) {
|
|
80
|
+
animation-delay: calc(var(--circle-pulse-animation-speed) * -1);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { Meta, StoryObj } from '@storybook/react';
|
|
2
|
+
import { CirclePulse } from './CirclePulse';
|
|
3
|
+
import { Col, Container, Row } from '../../grid';
|
|
4
|
+
|
|
5
|
+
const meta: Meta = {
|
|
6
|
+
title: 'Utilities/CirclePulse',
|
|
7
|
+
component: CirclePulse,
|
|
8
|
+
decorators: [
|
|
9
|
+
(Story) => (
|
|
10
|
+
<div
|
|
11
|
+
style={{
|
|
12
|
+
width: '300px',
|
|
13
|
+
height: '300px',
|
|
14
|
+
display: 'flex',
|
|
15
|
+
justifyContent: 'center',
|
|
16
|
+
alignItems: 'center',
|
|
17
|
+
}}
|
|
18
|
+
>
|
|
19
|
+
<Story />
|
|
20
|
+
</div>
|
|
21
|
+
),
|
|
22
|
+
],
|
|
23
|
+
argTypes: {
|
|
24
|
+
size: {
|
|
25
|
+
control: 'select',
|
|
26
|
+
description: 'The size of the circle',
|
|
27
|
+
options: ['xxs', 'xs', 'sm', 'md', 'lg', 'xl', 'xxl'],
|
|
28
|
+
table: {
|
|
29
|
+
category: 'Props',
|
|
30
|
+
type: {
|
|
31
|
+
summary: 'xxs | xs | sm | md | lg | xl | xxl',
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
id: {
|
|
36
|
+
control: false,
|
|
37
|
+
description: 'The id of the circle pulse',
|
|
38
|
+
table: {
|
|
39
|
+
category: 'Props',
|
|
40
|
+
type: {
|
|
41
|
+
summary: 'string',
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
className: {
|
|
46
|
+
control: false,
|
|
47
|
+
description: 'The class name of the circle pulse',
|
|
48
|
+
table: {
|
|
49
|
+
category: 'Props',
|
|
50
|
+
type: {
|
|
51
|
+
summary: 'string',
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
export default meta;
|
|
59
|
+
|
|
60
|
+
type Story = StoryObj<typeof CirclePulse>;
|
|
61
|
+
|
|
62
|
+
export const Default: Story = {
|
|
63
|
+
args: {
|
|
64
|
+
size: 'md',
|
|
65
|
+
},
|
|
66
|
+
|
|
67
|
+
render: (args) => {
|
|
68
|
+
return (
|
|
69
|
+
<Container>
|
|
70
|
+
<Row>
|
|
71
|
+
<Col sm={4}>
|
|
72
|
+
<CirclePulse {...args} />
|
|
73
|
+
</Col>
|
|
74
|
+
</Row>
|
|
75
|
+
</Container>
|
|
76
|
+
);
|
|
77
|
+
},
|
|
78
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
interface CirclePulseProps {
|
|
2
|
+
size?: 'xxs' | 'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'xxl';
|
|
3
|
+
className?: string;
|
|
4
|
+
[key: string]: any;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function CirclePulse({ size = 'md', className, ...rest }: CirclePulseProps) {
|
|
8
|
+
return (
|
|
9
|
+
<div
|
|
10
|
+
className={`circle-pulse circle-pulse-${size} ${className}`}
|
|
11
|
+
aria-hidden={true}
|
|
12
|
+
{...rest}
|
|
13
|
+
/>
|
|
14
|
+
);
|
|
15
|
+
}
|