@arbor-education/design-system.components 4.1.0 → 4.3.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/.pre-commit-config.yaml +4 -0
- package/CHANGELOG.md +12 -0
- package/dist/components/formField/FormField.d.ts +15 -0
- package/dist/components/formField/FormField.d.ts.map +1 -1
- package/dist/components/formField/FormField.js +2 -1
- package/dist/components/formField/FormField.js.map +1 -1
- package/dist/components/formField/FormField.stories.d.ts +1 -0
- package/dist/components/formField/FormField.stories.d.ts.map +1 -1
- package/dist/components/formField/FormField.stories.js +71 -5
- package/dist/components/formField/FormField.stories.js.map +1 -1
- package/dist/components/formField/FormField.test.js +19 -1
- package/dist/components/formField/FormField.test.js.map +1 -1
- package/dist/components/formField/label/Label.js +2 -2
- package/dist/components/formField/label/Label.js.map +1 -1
- package/dist/components/widgetCalendar/WidgetCalendar.stories.d.ts +1 -0
- package/dist/components/widgetCalendar/WidgetCalendar.stories.d.ts.map +1 -1
- package/dist/components/widgetCalendar/WidgetCalendar.stories.js +77 -4
- package/dist/components/widgetCalendar/WidgetCalendar.stories.js.map +1 -1
- package/dist/components/widgetCalendar/WidgetCalendar.test.js +31 -0
- package/dist/components/widgetCalendar/WidgetCalendar.test.js.map +1 -1
- package/dist/components/widgetCalendar/WidgetCalendarItem.d.ts +1 -1
- package/dist/components/widgetCalendar/WidgetCalendarItem.d.ts.map +1 -1
- package/dist/components/widgetCalendar/WidgetCalendarItem.js +1 -3
- package/dist/components/widgetCalendar/WidgetCalendarItem.js.map +1 -1
- package/dist/index.css +30 -0
- package/dist/index.css.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/components/formField/FormField.stories.tsx +100 -5
- package/src/components/formField/FormField.test.tsx +36 -1
- package/src/components/formField/FormField.tsx +21 -2
- package/src/components/formField/label/Label.tsx +2 -2
- package/src/components/widgetCalendar/WidgetCalendar.stories.tsx +81 -4
- package/src/components/widgetCalendar/WidgetCalendar.test.tsx +48 -0
- package/src/components/widgetCalendar/WidgetCalendarItem.tsx +10 -4
- package/src/components/widgetCalendar/widgetCalendar.scss +24 -0
- package/src/index.ts +1 -0
- package/src/tokens.scss +6 -0
|
@@ -18,7 +18,7 @@ import { FormField } from './FormField.js';
|
|
|
18
18
|
|
|
19
19
|
const DESCRIPTION_INTRO = [
|
|
20
20
|
'FormField is the **complete form field assembly**: it wires together a label, an optional',
|
|
21
|
-
'description, any of
|
|
21
|
+
'description, any of fourteen input controls (or your own via `custom`), an optional helper link, and an error message — all',
|
|
22
22
|
'with the correct accessibility connections built in automatically.',
|
|
23
23
|
'',
|
|
24
24
|
'Think of it as a filing cabinet drawer with everything pre-labelled: you tell it what kind of',
|
|
@@ -63,6 +63,7 @@ const USAGE_GUIDANCE = [
|
|
|
63
63
|
'| `"toggle"` | `Toggle` | Instant on/off settings embedded in a form |',
|
|
64
64
|
'| `"radio"` | `RadioButtonGroup` | One choice from a short, mutually-exclusive list |',
|
|
65
65
|
'| `"singleFile"` | `SingleFileInput` | One attachment submitted with a native form POST |',
|
|
66
|
+
'| `"custom"` | Your own element, via `inputProps.renderCustomInput` | A control the DS does not ship, such as a rich-text editor |',
|
|
66
67
|
].join('\n');
|
|
67
68
|
|
|
68
69
|
const DEVELOPER_NOTES = [
|
|
@@ -235,7 +236,7 @@ const meta = {
|
|
|
235
236
|
},
|
|
236
237
|
inputType: {
|
|
237
238
|
control: 'select',
|
|
238
|
-
options: ['text', 'textarea', 'number', 'time', 'colourPicker', 'selectDropdown', 'datePicker', 'combobox', 'checkbox', 'checkboxGroup', 'toggle', 'radio', 'singleFile'],
|
|
239
|
+
options: ['text', 'textarea', 'number', 'time', 'colourPicker', 'selectDropdown', 'datePicker', 'combobox', 'checkbox', 'checkboxGroup', 'toggle', 'radio', 'singleFile', 'custom'],
|
|
239
240
|
description: [
|
|
240
241
|
'Determines which input component is rendered.',
|
|
241
242
|
'Defaults to `"text"` (renders `TextInput`).',
|
|
@@ -249,11 +250,12 @@ const meta = {
|
|
|
249
250
|
'`"checkboxGroup"` → `CheckboxGroup` (several independent selections);',
|
|
250
251
|
'`"toggle"` → `Toggle` (instant on/off switch);',
|
|
251
252
|
'`"radio"` → `RadioButtonGroup` (one choice from a short list);',
|
|
252
|
-
'`"singleFile"` → `SingleFileInput` (one file for a native form POST)
|
|
253
|
+
'`"singleFile"` → `SingleFileInput` (one file for a native form POST);',
|
|
254
|
+
'`"custom"` → the element returned by `inputProps.renderCustomInput` (a control the DS does not ship).',
|
|
253
255
|
'`"checkbox"`, `"checkboxGroup"`, `"toggle"` and `"radio"` require `inputProps` — see the developer notes.',
|
|
254
256
|
].join(' '),
|
|
255
257
|
table: {
|
|
256
|
-
type: { summary: "'text' | 'textarea' | 'number' | 'time' | 'colourPicker' | 'selectDropdown' | 'datePicker' | 'combobox' | 'checkbox' | 'checkboxGroup' | 'toggle' | 'radio' | 'singleFile'" },
|
|
258
|
+
type: { summary: "'text' | 'textarea' | 'number' | 'time' | 'colourPicker' | 'selectDropdown' | 'datePicker' | 'combobox' | 'checkbox' | 'checkboxGroup' | 'toggle' | 'radio' | 'singleFile' | 'custom'" },
|
|
257
259
|
defaultValue: { summary: "'text'" },
|
|
258
260
|
},
|
|
259
261
|
},
|
|
@@ -328,7 +330,7 @@ const meta = {
|
|
|
328
330
|
'**Required, not optional, for `"checkbox"`, `"checkboxGroup"`, `"toggle"`, and `"radio"`.**',
|
|
329
331
|
].join(' '),
|
|
330
332
|
table: {
|
|
331
|
-
type: { summary: 'TextInputProps | TextAreaProps | NumberInputProps | TimeInputProps | ColourPickerDropdownProps | SelectDropdownInputProps | DatePickerProps | DateTimePickerProps | ComboboxProps | CheckboxInputProps | CheckboxGroupProps | Toggle.Props | RadioButtonGroupProps | SingleFileInputProps' },
|
|
333
|
+
type: { summary: 'TextInputProps | TextAreaProps | NumberInputProps | TimeInputProps | ColourPickerDropdownProps | SelectDropdownInputProps | DatePickerProps | DateTimePickerProps | ComboboxProps | CheckboxInputProps | CheckboxGroupProps | Toggle.Props | RadioButtonGroupProps | SingleFileInputProps | CustomInputProps' },
|
|
332
334
|
},
|
|
333
335
|
},
|
|
334
336
|
},
|
|
@@ -1990,3 +1992,96 @@ export default WithRadioExample;
|
|
|
1990
1992
|
'convention as `checkboxGroup`.',
|
|
1991
1993
|
].join('\n'),
|
|
1992
1994
|
);
|
|
1995
|
+
|
|
1996
|
+
export const WithCustomInput: Story = withDescription(
|
|
1997
|
+
{
|
|
1998
|
+
parameters: {
|
|
1999
|
+
docs: {
|
|
2000
|
+
source: {
|
|
2001
|
+
language: 'tsx',
|
|
2002
|
+
code: `
|
|
2003
|
+
import { FormField } from '@arbor-education/design-system.components';
|
|
2004
|
+
|
|
2005
|
+
function WithCustomInputExample() {
|
|
2006
|
+
return (
|
|
2007
|
+
<FormField
|
|
2008
|
+
id="report-notes"
|
|
2009
|
+
label="Report notes"
|
|
2010
|
+
inputType="custom"
|
|
2011
|
+
fieldDescription="Shown to parents on the termly report"
|
|
2012
|
+
helperLinkText="Formatting help"
|
|
2013
|
+
helperLinkUrl="https://example.com/formatting"
|
|
2014
|
+
errorText="Report notes are required"
|
|
2015
|
+
inputProps={{
|
|
2016
|
+
renderCustomInput: ({ id, labelId, hasError, 'aria-describedby': describedBy, 'aria-invalid': invalid }) => (
|
|
2017
|
+
<div
|
|
2018
|
+
id={id}
|
|
2019
|
+
role="textbox"
|
|
2020
|
+
aria-multiline="true"
|
|
2021
|
+
aria-labelledby={labelId}
|
|
2022
|
+
aria-describedby={describedBy}
|
|
2023
|
+
aria-invalid={invalid}
|
|
2024
|
+
contentEditable
|
|
2025
|
+
suppressContentEditableWarning
|
|
2026
|
+
className={hasError ? 'my-editor my-editor--error' : 'my-editor'}
|
|
2027
|
+
/>
|
|
2028
|
+
),
|
|
2029
|
+
}}
|
|
2030
|
+
/>
|
|
2031
|
+
);
|
|
2032
|
+
}
|
|
2033
|
+
export default WithCustomInputExample;
|
|
2034
|
+
`.trim(),
|
|
2035
|
+
},
|
|
2036
|
+
},
|
|
2037
|
+
},
|
|
2038
|
+
render: () => (
|
|
2039
|
+
<div style={{ maxWidth: '28rem', padding: 'var(--spacing-xlarge)' }}>
|
|
2040
|
+
<FormField
|
|
2041
|
+
id="report-notes"
|
|
2042
|
+
label="Report notes"
|
|
2043
|
+
inputType="custom"
|
|
2044
|
+
fieldDescription="Shown to parents on the termly report"
|
|
2045
|
+
helperLinkText="Formatting help"
|
|
2046
|
+
helperLinkUrl="https://example.com/formatting"
|
|
2047
|
+
errorText="Report notes are required"
|
|
2048
|
+
inputProps={{
|
|
2049
|
+
renderCustomInput: ({ id, labelId, hasError, 'aria-describedby': describedBy, 'aria-invalid': invalid }) => (
|
|
2050
|
+
<div
|
|
2051
|
+
id={id}
|
|
2052
|
+
role="textbox"
|
|
2053
|
+
aria-multiline="true"
|
|
2054
|
+
aria-labelledby={labelId}
|
|
2055
|
+
aria-describedby={describedBy}
|
|
2056
|
+
aria-invalid={invalid}
|
|
2057
|
+
contentEditable
|
|
2058
|
+
suppressContentEditableWarning
|
|
2059
|
+
style={{
|
|
2060
|
+
minHeight: '6rem',
|
|
2061
|
+
padding: 'var(--spacing-small)',
|
|
2062
|
+
border: `1px solid ${hasError ? 'var(--color-border-error, red)' : 'var(--color-border-default, grey)'}`,
|
|
2063
|
+
borderRadius: 'var(--border-radius-small, 4px)',
|
|
2064
|
+
}}
|
|
2065
|
+
/>
|
|
2066
|
+
),
|
|
2067
|
+
}}
|
|
2068
|
+
/>
|
|
2069
|
+
</div>
|
|
2070
|
+
),
|
|
2071
|
+
},
|
|
2072
|
+
[
|
|
2073
|
+
'`inputType="custom"` hosts an input the DS does not ship, such as a rich-text editor. `FormField`',
|
|
2074
|
+
'still renders the label, `fieldDescription`, helper link and `errorText`; your',
|
|
2075
|
+
'`inputProps.renderCustomInput` function returns the control. `inputProps` is **required**.',
|
|
2076
|
+
'',
|
|
2077
|
+
'**`renderCustomInput` receives the same wiring the built-in inputs get:** `id`, `aria-describedby`,',
|
|
2078
|
+
'`aria-invalid` and `hasError`. Apply them to the element that takes focus, otherwise the',
|
|
2079
|
+
'description and error are not announced.',
|
|
2080
|
+
'',
|
|
2081
|
+
'**Use `labelId` with `aria-labelledby` when the control is not a native input.** The label\'s',
|
|
2082
|
+
'`htmlFor` only names `input`, `textarea` and `select`. A `contentEditable` element or an iframe',
|
|
2083
|
+
'needs `aria-labelledby={labelId}` to get its accessible name.',
|
|
2084
|
+
'',
|
|
2085
|
+
'**The DS styles none of the custom control.** Use `hasError` to apply your own error treatment.',
|
|
2086
|
+
].join('\n'),
|
|
2087
|
+
);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import '@testing-library/jest-dom/vitest';
|
|
2
2
|
import { render, screen } from '@testing-library/react';
|
|
3
|
-
import { describe, expect, test } from 'vitest';
|
|
3
|
+
import { describe, expect, test, vi } from 'vitest';
|
|
4
4
|
import { FormField } from './FormField.js';
|
|
5
5
|
|
|
6
6
|
describe('FormField component', () => {
|
|
@@ -229,4 +229,39 @@ describe('FormField component', () => {
|
|
|
229
229
|
expect(field).toHaveClass('ds-input--error');
|
|
230
230
|
expect(field).toHaveAccessibleDescription('File is too large');
|
|
231
231
|
});
|
|
232
|
+
|
|
233
|
+
test('renders the consumer element inside the field chrome when inputType is custom', () => {
|
|
234
|
+
render(
|
|
235
|
+
<FormField
|
|
236
|
+
id="notes"
|
|
237
|
+
label="Notes"
|
|
238
|
+
inputType="custom"
|
|
239
|
+
fieldDescription="Shown on the report"
|
|
240
|
+
helperLinkText="Formatting help"
|
|
241
|
+
helperLinkUrl="https://example.com/help"
|
|
242
|
+
errorText="Notes are required"
|
|
243
|
+
inputProps={{
|
|
244
|
+
renderCustomInput: ({ id, labelId, 'aria-describedby': describedBy, 'aria-invalid': invalid }) => (
|
|
245
|
+
<div role="textbox" id={id} aria-multiline="true" aria-labelledby={labelId} aria-describedby={describedBy} aria-invalid={invalid} />
|
|
246
|
+
),
|
|
247
|
+
}}
|
|
248
|
+
/>,
|
|
249
|
+
);
|
|
250
|
+
|
|
251
|
+
const editor = screen.getByRole('textbox', { name: 'Notes' });
|
|
252
|
+
expect(editor).toHaveAccessibleDescription('Shown on the report Notes are required');
|
|
253
|
+
expect(editor).toBeInvalid();
|
|
254
|
+
expect(screen.getByText('Notes are required')).toBeInTheDocument();
|
|
255
|
+
expect(screen.getByRole('link', { name: 'Notes helper link' })).toBeInTheDocument();
|
|
256
|
+
expect(editor.closest('.ds-form-field')).not.toBeNull();
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
test('tells a custom input whether the field has an error', () => {
|
|
260
|
+
const renderCustomInput = vi.fn(() => <div />);
|
|
261
|
+
const { rerender } = render(<FormField id="notes" inputType="custom" inputProps={{ renderCustomInput }} />);
|
|
262
|
+
expect(renderCustomInput).toHaveBeenLastCalledWith(expect.objectContaining({ 'hasError': false, 'aria-invalid': false }));
|
|
263
|
+
|
|
264
|
+
rerender(<FormField id="notes" inputType="custom" errorText="Required" inputProps={{ renderCustomInput }} />);
|
|
265
|
+
expect(renderCustomInput).toHaveBeenLastCalledWith(expect.objectContaining({ 'hasError': true, 'aria-invalid': true }));
|
|
266
|
+
});
|
|
232
267
|
});
|
|
@@ -16,6 +16,21 @@ import { CheckboxGroup } from './inputs/checkbox/CheckboxGroup.js';
|
|
|
16
16
|
import { RadioButtonGroup } from './inputs/radio/RadioButtonGroup.js';
|
|
17
17
|
import { Toggle } from 'Components/toggle/Toggle.js';
|
|
18
18
|
|
|
19
|
+
export type FormFieldSharedProps = {
|
|
20
|
+
'id'?: string;
|
|
21
|
+
'aria-describedby': string;
|
|
22
|
+
'aria-invalid': boolean;
|
|
23
|
+
'hasError': boolean;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export type CustomInputRenderProps = FormFieldSharedProps & {
|
|
27
|
+
labelId?: string;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export type CustomInputProps = {
|
|
31
|
+
renderCustomInput: (renderProps: CustomInputRenderProps) => React.ReactNode;
|
|
32
|
+
};
|
|
33
|
+
|
|
19
34
|
export type FormFieldProps = {
|
|
20
35
|
className?: string;
|
|
21
36
|
label?: string;
|
|
@@ -39,6 +54,7 @@ export type FormFieldProps = {
|
|
|
39
54
|
| { inputType?: 'toggle'; inputProps: Toggle.Props }
|
|
40
55
|
| { inputType?: 'radio'; inputProps: RadioButtonGroup.Props }
|
|
41
56
|
| { inputType?: 'singleFile'; inputProps?: SingleFileInput.Props }
|
|
57
|
+
| { inputType?: 'custom'; inputProps: CustomInputProps }
|
|
42
58
|
);
|
|
43
59
|
|
|
44
60
|
export const FormField = (props: FormFieldProps) => {
|
|
@@ -52,7 +68,9 @@ export const FormField = (props: FormFieldProps) => {
|
|
|
52
68
|
describedByIds.push(`${id}-error`);
|
|
53
69
|
}
|
|
54
70
|
|
|
55
|
-
const
|
|
71
|
+
const labelId = inputType === 'custom' && id ? `${id}-label` : undefined;
|
|
72
|
+
|
|
73
|
+
const sharedProps: FormFieldSharedProps = {
|
|
56
74
|
id,
|
|
57
75
|
'aria-describedby': describedByIds.join(' '),
|
|
58
76
|
'hasError': !!errorText,
|
|
@@ -62,7 +80,7 @@ export const FormField = (props: FormFieldProps) => {
|
|
|
62
80
|
return (
|
|
63
81
|
<div className={classes}>
|
|
64
82
|
{label && (
|
|
65
|
-
<Label htmlFor={id}>
|
|
83
|
+
<Label htmlFor={id} id={labelId}>
|
|
66
84
|
{label}
|
|
67
85
|
</Label>
|
|
68
86
|
)}
|
|
@@ -111,6 +129,7 @@ export const FormField = (props: FormFieldProps) => {
|
|
|
111
129
|
{inputType === 'singleFile' && (
|
|
112
130
|
<SingleFileInput {...sharedProps} {...(inputProps as SingleFileInput.Props)} />
|
|
113
131
|
)}
|
|
132
|
+
{inputType === 'custom' && (inputProps as CustomInputProps).renderCustomInput({ ...sharedProps, labelId })}
|
|
114
133
|
{((helperLinkText && helperLinkUrl) || errorText) && (
|
|
115
134
|
<div className="ds-form-field__message">
|
|
116
135
|
{errorText && (
|
|
@@ -2,7 +2,7 @@ import classNames from 'classnames';
|
|
|
2
2
|
import type { HTMLProps } from 'react';
|
|
3
3
|
|
|
4
4
|
export const Label = (props: HTMLProps<HTMLLabelElement>) => {
|
|
5
|
-
const { className, htmlFor, children } = props;
|
|
5
|
+
const { className, htmlFor, id, children } = props;
|
|
6
6
|
const classes = classNames('ds-label', className);
|
|
7
|
-
return <label className={classes} htmlFor={htmlFor}>{children}</label>;
|
|
7
|
+
return <label className={classes} htmlFor={htmlFor} id={id}>{children}</label>;
|
|
8
8
|
};
|
|
@@ -166,7 +166,7 @@ const groupedItems: WidgetCalendar.ItemData[] = [
|
|
|
166
166
|
|
|
167
167
|
const DESCRIPTION_INTRO = [
|
|
168
168
|
'`WidgetCalendar` is a pre-composed dashboard widget for date/time-anchored calendar events.',
|
|
169
|
-
'It wraps [`Widget`](?path=/docs/components-widget--docs) with an event list,
|
|
169
|
+
'It wraps [`Widget`](?path=/docs/components-widget--docs) with an event list, an eight-value event type indicator, and',
|
|
170
170
|
'optional heading navigation. Each item renders a semantic `<time>` element from an ISO `eventDateTime` and a',
|
|
171
171
|
'human-readable `eventTimeLabel`.',
|
|
172
172
|
].join('\n');
|
|
@@ -214,9 +214,12 @@ const DEVELOPER_NOTES = [
|
|
|
214
214
|
'fall into the ungrouped bucket, which renders above all groups to prevent ambiguity about which group',
|
|
215
215
|
'an item belongs to. Groups appear in the order their labels are first declared.',
|
|
216
216
|
'',
|
|
217
|
-
'
|
|
218
|
-
'
|
|
219
|
-
'
|
|
217
|
+
'**`eventType` selects one of eight type-bar colours.** The accepted values are `primary` (the default),',
|
|
218
|
+
'`secondary`, `destructive`, `caution`, `info`, `warning`, `green`, and `purple`, matching the eight index',
|
|
219
|
+
'colours in the design. The colour is chosen by the school per event and carries no meaning of its own —',
|
|
220
|
+
'`WidgetCalendar` attaches no semantics to the value, so the same colour can represent different event',
|
|
221
|
+
'categories in different schools. Pass the value the data source supplies rather than inferring one from',
|
|
222
|
+
'the event content.',
|
|
220
223
|
'',
|
|
221
224
|
'**No loading, disabled, or error state exists on this component.** Handle those states in the consumer',
|
|
222
225
|
'(e.g. render a Skeleton in place of `WidgetCalendar` while data loads). `isLoading` and `loadingLabel` are',
|
|
@@ -688,6 +691,80 @@ export const SecondaryEventType: Story = withDescription(
|
|
|
688
691
|
'Isolates `eventType="secondary"` — the neutral grey type bar, used for lower-priority or generic events.',
|
|
689
692
|
);
|
|
690
693
|
|
|
694
|
+
export const AllEventTypes: Story = withDescription(
|
|
695
|
+
{
|
|
696
|
+
args: {
|
|
697
|
+
heading: 'Calendar',
|
|
698
|
+
items: [
|
|
699
|
+
{
|
|
700
|
+
id: 'type-primary',
|
|
701
|
+
eventType: 'primary',
|
|
702
|
+
eventPrimaryTerm: 'Class',
|
|
703
|
+
eventPrimaryDefinition: 'Year 10: 10G',
|
|
704
|
+
...classEventTime,
|
|
705
|
+
},
|
|
706
|
+
{
|
|
707
|
+
id: 'type-secondary',
|
|
708
|
+
eventType: 'secondary',
|
|
709
|
+
eventPrimaryTerm: 'Class',
|
|
710
|
+
eventPrimaryDefinition: 'Year 8: 8B',
|
|
711
|
+
...classEventTime,
|
|
712
|
+
},
|
|
713
|
+
{
|
|
714
|
+
id: 'type-destructive',
|
|
715
|
+
eventType: 'destructive',
|
|
716
|
+
eventPrimaryTerm: 'Exam',
|
|
717
|
+
eventPrimaryDefinition: 'GCSE Maths: Paper 1',
|
|
718
|
+
...classEventTime,
|
|
719
|
+
},
|
|
720
|
+
{
|
|
721
|
+
id: 'type-caution',
|
|
722
|
+
eventType: 'caution',
|
|
723
|
+
eventPrimaryTerm: 'Trip',
|
|
724
|
+
eventPrimaryDefinition: 'Geography field trip',
|
|
725
|
+
...classEventTime,
|
|
726
|
+
},
|
|
727
|
+
{
|
|
728
|
+
id: 'type-info',
|
|
729
|
+
eventType: 'info',
|
|
730
|
+
eventPrimaryTerm: 'Meeting',
|
|
731
|
+
eventPrimaryDefinition: 'Parents\' evening',
|
|
732
|
+
...classEventTime,
|
|
733
|
+
},
|
|
734
|
+
{
|
|
735
|
+
id: 'type-warning',
|
|
736
|
+
eventType: 'warning',
|
|
737
|
+
eventPrimaryTerm: 'Deadline',
|
|
738
|
+
eventPrimaryDefinition: 'Reports due to head of year',
|
|
739
|
+
...classEventTime,
|
|
740
|
+
},
|
|
741
|
+
{
|
|
742
|
+
id: 'type-green',
|
|
743
|
+
eventType: 'green',
|
|
744
|
+
eventPrimaryTerm: 'Club',
|
|
745
|
+
eventPrimaryDefinition: 'Football: Year 9',
|
|
746
|
+
...classEventTime,
|
|
747
|
+
},
|
|
748
|
+
{
|
|
749
|
+
id: 'type-purple',
|
|
750
|
+
eventType: 'purple',
|
|
751
|
+
eventPrimaryTerm: 'Assembly',
|
|
752
|
+
eventPrimaryDefinition: 'Whole-school assembly',
|
|
753
|
+
...classEventTime,
|
|
754
|
+
},
|
|
755
|
+
],
|
|
756
|
+
onItemClick: fn(),
|
|
757
|
+
},
|
|
758
|
+
render: args => <WidgetCalendar {...args} />,
|
|
759
|
+
parameters: { controls: { disable: true } },
|
|
760
|
+
},
|
|
761
|
+
[
|
|
762
|
+
'All eight `eventType` values side by side, so the full set of type-bar colours can be compared in one view.',
|
|
763
|
+
'The colour is chosen by the school per event — `WidgetCalendar` attaches no semantics to any of the eight',
|
|
764
|
+
'values. See the Developer notes above for the full list.',
|
|
765
|
+
].join(' '),
|
|
766
|
+
);
|
|
767
|
+
|
|
691
768
|
export const NonInteractiveItems: Story = withDescription(
|
|
692
769
|
{
|
|
693
770
|
args: {
|
|
@@ -60,6 +60,54 @@ describe('WidgetCalendar', () => {
|
|
|
60
60
|
expect(container.querySelector('.ds-widget-calendar__item--secondary')).toBeInTheDocument();
|
|
61
61
|
});
|
|
62
62
|
|
|
63
|
+
const eventTypes: WidgetCalendar.EventType[] = [
|
|
64
|
+
'primary',
|
|
65
|
+
'secondary',
|
|
66
|
+
'destructive',
|
|
67
|
+
'caution',
|
|
68
|
+
'info',
|
|
69
|
+
'warning',
|
|
70
|
+
'green',
|
|
71
|
+
'purple',
|
|
72
|
+
];
|
|
73
|
+
|
|
74
|
+
test.each(eventTypes)('applies the %s modifier class and no other event type class', (eventType) => {
|
|
75
|
+
const { container } = render(
|
|
76
|
+
<WidgetCalendar
|
|
77
|
+
heading="Calendar"
|
|
78
|
+
items={[{ ...mockItems[0]!, eventType }]}
|
|
79
|
+
/>,
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
const item = container.querySelector('.ds-widget-calendar__item');
|
|
83
|
+
|
|
84
|
+
expect(item).toHaveClass(`ds-widget-calendar__item--${eventType}`);
|
|
85
|
+
|
|
86
|
+
eventTypes
|
|
87
|
+
.filter(otherType => otherType !== eventType)
|
|
88
|
+
.forEach((otherType) => {
|
|
89
|
+
expect(item).not.toHaveClass(`ds-widget-calendar__item--${otherType}`);
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test('defaults to the primary modifier class when eventType is omitted', () => {
|
|
94
|
+
const { container } = render(
|
|
95
|
+
<WidgetCalendar
|
|
96
|
+
heading="Calendar"
|
|
97
|
+
items={[{
|
|
98
|
+
id: 'no-type',
|
|
99
|
+
eventPrimaryTerm: 'Class',
|
|
100
|
+
eventPrimaryDefinition: 'Year 10: 10G',
|
|
101
|
+
eventSecondaryTerm: 'Time and location',
|
|
102
|
+
eventDateTime: '2026-06-08T08:45/2026-06-08T09:00',
|
|
103
|
+
eventTimeLabel: '08:45 – 09:00',
|
|
104
|
+
}]}
|
|
105
|
+
/>,
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
expect(container.querySelector('.ds-widget-calendar__item')).toHaveClass('ds-widget-calendar__item--primary');
|
|
109
|
+
});
|
|
110
|
+
|
|
63
111
|
test('calls onItemClick with item id', async () => {
|
|
64
112
|
const user = userEvent.setup();
|
|
65
113
|
const onItemClick = vi.fn();
|
|
@@ -3,7 +3,15 @@ import { Icon } from 'Components/icon/Icon';
|
|
|
3
3
|
import { TooltipWrapper } from 'Components/tooltip/TooltipWrapper';
|
|
4
4
|
import { ENTER_KEY, SPACE_KEY } from 'Utils/keyboardConstants';
|
|
5
5
|
|
|
6
|
-
export type WidgetCalendarEventType
|
|
6
|
+
export type WidgetCalendarEventType
|
|
7
|
+
= 'primary'
|
|
8
|
+
| 'secondary'
|
|
9
|
+
| 'destructive'
|
|
10
|
+
| 'caution'
|
|
11
|
+
| 'info'
|
|
12
|
+
| 'warning'
|
|
13
|
+
| 'green'
|
|
14
|
+
| 'purple';
|
|
7
15
|
|
|
8
16
|
export type WidgetCalendarItemProps = {
|
|
9
17
|
id: string;
|
|
@@ -68,9 +76,7 @@ export const WidgetCalendarItem = (props: WidgetCalendarItemProps): React.JSX.El
|
|
|
68
76
|
<li
|
|
69
77
|
className={classNames(
|
|
70
78
|
'ds-widget-calendar__item',
|
|
71
|
-
eventType
|
|
72
|
-
? 'ds-widget-calendar__item--secondary'
|
|
73
|
-
: 'ds-widget-calendar__item--primary',
|
|
79
|
+
`ds-widget-calendar__item--${eventType}`,
|
|
74
80
|
isInteractive && 'ds-widget-calendar__item--interactive',
|
|
75
81
|
className,
|
|
76
82
|
)}
|
|
@@ -75,6 +75,30 @@
|
|
|
75
75
|
background-color: var(--widget-calendar-type-secondary-color);
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
+
.ds-widget-calendar__item--destructive .ds-widget-calendar__content::before {
|
|
79
|
+
background-color: var(--widget-calendar-type-destructive-color);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
.ds-widget-calendar__item--caution .ds-widget-calendar__content::before {
|
|
83
|
+
background-color: var(--widget-calendar-type-caution-color);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
.ds-widget-calendar__item--info .ds-widget-calendar__content::before {
|
|
87
|
+
background-color: var(--widget-calendar-type-info-color);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
.ds-widget-calendar__item--warning .ds-widget-calendar__content::before {
|
|
91
|
+
background-color: var(--widget-calendar-type-warning-color);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
.ds-widget-calendar__item--green .ds-widget-calendar__content::before {
|
|
95
|
+
background-color: var(--widget-calendar-type-green-color);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
.ds-widget-calendar__item--purple .ds-widget-calendar__content::before {
|
|
99
|
+
background-color: var(--widget-calendar-type-purple-color);
|
|
100
|
+
}
|
|
101
|
+
|
|
78
102
|
.ds-widget-calendar__details {
|
|
79
103
|
margin: 0;
|
|
80
104
|
}
|
package/src/index.ts
CHANGED
|
@@ -28,6 +28,7 @@ export {
|
|
|
28
28
|
} from 'Components/filterBar/FilterBar';
|
|
29
29
|
export { Fieldset } from 'Components/formField/fieldset/Fieldset';
|
|
30
30
|
export { FormField } from 'Components/formField/FormField';
|
|
31
|
+
export type { CustomInputProps, CustomInputRenderProps, FormFieldSharedProps } from 'Components/formField/FormField';
|
|
31
32
|
export { CheckboxGroup } from 'Components/formField/inputs/checkbox/CheckboxGroup';
|
|
32
33
|
export { CheckboxInput } from 'Components/formField/inputs/checkbox/CheckboxInput';
|
|
33
34
|
export { ColourPickerDropdown } from 'Components/formField/inputs/colourPickerDropdown/ColourPickerDropdown';
|
package/src/tokens.scss
CHANGED
|
@@ -841,6 +841,12 @@
|
|
|
841
841
|
--widget-calendar-type-bar-radius: var(--border-radius-xsmall);
|
|
842
842
|
--widget-calendar-type-primary-color: var(--color-brand-500);
|
|
843
843
|
--widget-calendar-type-secondary-color: var(--color-grey-300);
|
|
844
|
+
--widget-calendar-type-destructive-color: var(--color-semantic-destructive-500);
|
|
845
|
+
--widget-calendar-type-caution-color: var(--color-semantic-caution-500);
|
|
846
|
+
--widget-calendar-type-info-color: var(--color-semantic-info-500);
|
|
847
|
+
--widget-calendar-type-warning-color: var(--color-semantic-warning-500);
|
|
848
|
+
--widget-calendar-type-green-color: var(--color-ramp-gradient-sequential-500);
|
|
849
|
+
--widget-calendar-type-purple-color: var(--color-extended-colours-purple-500);
|
|
844
850
|
--widget-calendar-color-title: var(--color-grey-900);
|
|
845
851
|
--widget-list-row-hover-hover-bg: var(--color-grey-050);
|
|
846
852
|
--widget-list-row-hover-color-icon-arrow: var(--color-grey-900);
|