@vygruppen/spor-react 1.3.3 → 2.0.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/.turbo/turbo-build.log +12 -10
- package/CHANGELOG.md +40 -0
- package/README.md +1 -1
- package/dist/index.d.ts +6718 -27
- package/dist/index.js +14139 -140
- package/dist/index.mjs +13818 -27
- package/package.json +19 -31
- package/src/accordion/Accordion.test.tsx +20 -0
- package/src/accordion/Accordion.tsx +62 -0
- package/src/accordion/AccordionContext.tsx +27 -0
- package/src/accordion/Expandable.tsx +157 -0
- package/src/accordion/index.tsx +2 -0
- package/src/alert/AlertIcon.tsx +75 -0
- package/src/alert/BaseAlert.test.tsx +37 -0
- package/src/alert/BaseAlert.tsx +21 -0
- package/src/alert/ClosableAlert.test.tsx +37 -0
- package/src/alert/ClosableAlert.tsx +75 -0
- package/src/alert/ExpandableAlert.test.tsx +84 -0
- package/src/alert/ExpandableAlert.tsx +84 -0
- package/src/alert/StaticAlert.tsx +25 -0
- package/src/alert/index.tsx +3 -0
- package/src/button/Button.test.tsx +23 -0
- package/src/button/Button.tsx +162 -0
- package/src/button/ButtonGroup.tsx +43 -0
- package/src/button/CloseButton.tsx +63 -0
- package/src/button/FloatingActionButton.tsx +113 -0
- package/src/button/IconButton.tsx +63 -0
- package/src/button/index.tsx +5 -0
- package/src/card/Card.tsx +59 -0
- package/src/card/index.tsx +1 -0
- package/src/datepicker/Calendar.tsx +32 -0
- package/src/datepicker/CalendarCell.tsx +74 -0
- package/src/datepicker/CalendarGrid.tsx +76 -0
- package/src/datepicker/CalendarHeader.tsx +153 -0
- package/src/datepicker/CalendarNavigationButton.tsx +26 -0
- package/src/datepicker/CalendarTriggerButton.tsx +36 -0
- package/src/datepicker/DateField.tsx +51 -0
- package/src/datepicker/DatePicker.tsx +153 -0
- package/src/datepicker/DateRangePicker.tsx +165 -0
- package/src/datepicker/DateTimeSegment.tsx +56 -0
- package/src/datepicker/RangeCalendar.tsx +35 -0
- package/src/datepicker/StyledField.tsx +31 -0
- package/src/datepicker/TimeField.tsx +46 -0
- package/src/datepicker/TimePicker.test.tsx +74 -0
- package/src/datepicker/TimePicker.tsx +196 -0
- package/src/datepicker/index.tsx +4 -0
- package/src/datepicker/utils.ts +33 -0
- package/src/i18n/index.tsx +38 -0
- package/src/image/index.tsx +2 -0
- package/src/index.tsx +25 -26
- package/src/input/CardSelect.tsx +165 -0
- package/src/input/Checkbox.tsx +24 -0
- package/src/input/CheckboxGroup.tsx +43 -0
- package/src/input/ChoiceChip.tsx +102 -0
- package/src/input/Dialog.tsx +29 -0
- package/src/input/FormControl.tsx +11 -0
- package/src/input/FormErrorMessage.tsx +91 -0
- package/src/input/FormLabel.tsx +11 -0
- package/src/input/InfoSelect.tsx +209 -0
- package/src/input/Input.tsx +59 -0
- package/src/input/InputElement.tsx +45 -0
- package/src/input/ListBox.tsx +123 -0
- package/src/input/NativeSelect.tsx +38 -0
- package/src/input/PasswordInput.tsx +70 -0
- package/src/input/Popover.tsx +70 -0
- package/src/input/Radio.tsx +34 -0
- package/src/input/RadioGroup.tsx +47 -0
- package/src/input/SearchInput.tsx +89 -0
- package/src/input/Switch.tsx +40 -0
- package/src/input/Textarea.tsx +98 -0
- package/src/input/index.tsx +20 -0
- package/src/layout/Divider.tsx +26 -0
- package/src/layout/Stack.tsx +42 -0
- package/src/layout/index.tsx +28 -0
- package/src/linjetag/InfoTag.tsx +54 -0
- package/src/linjetag/LineIcon.tsx +44 -0
- package/src/linjetag/TravelTag.tsx +121 -0
- package/src/linjetag/icons.tsx +80 -0
- package/src/linjetag/index.tsx +3 -0
- package/src/linjetag/types.d.ts +24 -0
- package/src/link/TextLink.tsx +45 -0
- package/src/link/index.tsx +1 -0
- package/src/loader/ClientOnly.tsx +29 -0
- package/src/loader/ColorInlineLoader.tsx +27 -0
- package/src/loader/ColorSpinner.tsx +44 -0
- package/src/loader/ContentLoader.tsx +27 -0
- package/src/loader/DarkFullScreenLoader.tsx +23 -0
- package/src/loader/DarkInlineLoader.tsx +25 -0
- package/src/loader/DarkSpinner.tsx +43 -0
- package/src/loader/LightFullScreenLoader.tsx +23 -0
- package/src/loader/LightInlineLoader.tsx +25 -0
- package/src/loader/LightSpinner.tsx +41 -0
- package/src/loader/Lottie.tsx +10 -0
- package/src/loader/ProgressBar.tsx +128 -0
- package/src/loader/ProgressLoader.tsx +140 -0
- package/src/loader/Skeleton.tsx +16 -0
- package/src/loader/SkeletonCircle.tsx +13 -0
- package/src/loader/SkeletonText.tsx +10 -0
- package/src/loader/index.tsx +14 -0
- package/src/loader/useHydrated.tsx +34 -0
- package/src/loader/useRotatingLabel.tsx +22 -0
- package/src/logo/VyLogo.tsx +101 -0
- package/src/logo/index.tsx +1 -0
- package/src/media-controller/JumpButton.tsx +69 -0
- package/src/media-controller/PlayPauseButton.tsx +67 -0
- package/src/media-controller/SkipButton.tsx +66 -0
- package/src/media-controller/icons.tsx +80 -0
- package/src/media-controller/index.test.tsx +59 -0
- package/src/media-controller/index.tsx +3 -0
- package/src/modal/Drawer.tsx +122 -0
- package/src/modal/Modal.tsx +15 -0
- package/src/modal/ModalHeader.tsx +31 -0
- package/src/modal/SimpleDrawer.tsx +44 -0
- package/src/modal/index.tsx +4 -0
- package/src/popover/PopoverWizardBody.tsx +91 -0
- package/src/popover/SimplePopover.tsx +75 -0
- package/src/popover/WizardPopover.tsx +61 -0
- package/src/popover/index.tsx +23 -0
- package/src/provider/SporProvider.tsx +67 -0
- package/src/provider/index.tsx +1 -0
- package/src/stepper/Stepper.tsx +115 -0
- package/src/stepper/StepperContext.tsx +55 -0
- package/src/stepper/StepperStep.tsx +48 -0
- package/src/stepper/index.tsx +2 -0
- package/src/tab/Tabs.tsx +20 -0
- package/src/tab/index.tsx +9 -0
- package/src/table/Table.tsx +58 -0
- package/src/table/index.tsx +19 -0
- package/src/theme/components/accordion.ts +143 -0
- package/src/theme/components/alert.ts +59 -0
- package/src/theme/components/badge.ts +109 -0
- package/src/theme/components/button.ts +217 -0
- package/src/theme/components/card-select.ts +158 -0
- package/src/theme/components/card.ts +174 -0
- package/src/theme/components/checkbox.ts +90 -0
- package/src/theme/components/choice-chip.ts +79 -0
- package/src/theme/components/close-button.ts +56 -0
- package/src/theme/components/code.ts +17 -0
- package/src/theme/components/datepicker.ts +194 -0
- package/src/theme/components/drawer.ts +92 -0
- package/src/theme/components/fab.ts +111 -0
- package/src/theme/components/form-label.ts +17 -0
- package/src/theme/components/form.ts +27 -0
- package/src/theme/components/index.ts +34 -0
- package/src/theme/components/info-select.ts +91 -0
- package/src/theme/components/info-tag.ts +49 -0
- package/src/theme/components/input.ts +97 -0
- package/src/theme/components/line-icon.ts +121 -0
- package/src/theme/components/link.ts +155 -0
- package/src/theme/components/listbox.ts +52 -0
- package/src/theme/components/media-controller-button.ts +134 -0
- package/src/theme/components/modal.ts +93 -0
- package/src/theme/components/popover.ts +63 -0
- package/src/theme/components/radio.ts +64 -0
- package/src/theme/components/select.ts +52 -0
- package/src/theme/components/skeleton.ts +40 -0
- package/src/theme/components/stepper.ts +230 -0
- package/src/theme/components/switch.ts +227 -0
- package/src/theme/components/table.ts +163 -0
- package/src/theme/components/tabs.ts +282 -0
- package/src/theme/components/textarea.ts +14 -0
- package/src/theme/components/toast.ts +28 -0
- package/src/theme/components/travel-tag.ts +267 -0
- package/src/theme/font-faces.ts +66 -0
- package/src/theme/foundations/borders.ts +11 -0
- package/src/theme/foundations/breakpoints.ts +9 -0
- package/src/theme/foundations/colors.ts +10 -0
- package/src/theme/foundations/config.ts +5 -0
- package/src/theme/foundations/fontSizes.ts +29 -0
- package/src/theme/foundations/fontWeights.ts +5 -0
- package/src/theme/foundations/fonts.ts +7 -0
- package/src/theme/foundations/index.ts +14 -0
- package/src/theme/foundations/lineHeights.ts +5 -0
- package/src/theme/foundations/radii.ts +12 -0
- package/src/theme/foundations/shadows.ts +8 -0
- package/src/theme/foundations/sizes.ts +34 -0
- package/src/theme/foundations/spacing.ts +30 -0
- package/src/theme/foundations/textStyles.ts +60 -0
- package/src/theme/foundations/zIndices.ts +17 -0
- package/src/theme/index.ts +14 -0
- package/src/theme/utils/box-shadow-utils.ts +44 -0
- package/src/theme/utils/focus-utils.ts +16 -0
- package/src/toast/ActionToast.test.tsx +22 -0
- package/src/toast/ActionToast.tsx +28 -0
- package/src/toast/BaseToast.test.tsx +27 -0
- package/src/toast/BaseToast.tsx +75 -0
- package/src/toast/ClosableToast.test.tsx +17 -0
- package/src/toast/ClosableToast.tsx +40 -0
- package/src/toast/index.tsx +1 -0
- package/src/toast/useToast.tsx +99 -0
- package/src/typography/Badge.tsx +68 -0
- package/src/typography/Code.tsx +32 -0
- package/src/typography/Heading.tsx +32 -0
- package/src/typography/Text.tsx +26 -0
- package/src/typography/index.tsx +4 -0
- package/src/util/externals.tsx +23 -0
- package/src/util/index.tsx +1 -0
package/package.json
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
{
|
2
2
|
"name": "@vygruppen/spor-react",
|
3
|
-
"version": "
|
3
|
+
"version": "2.0.0",
|
4
4
|
"main": "./dist/index.js",
|
5
5
|
"module": "./dist/index.mjs",
|
6
6
|
"types": "./dist/index.d.ts",
|
7
7
|
"license": "MIT",
|
8
8
|
"sideEffects": false,
|
9
9
|
"scripts": {
|
10
|
-
"build": "tsup src
|
10
|
+
"build": "tsup src/index.tsx --dts --treeshake --format cjs,esm",
|
11
11
|
"dev": "tsup src/index.tsx --watch --format esm"
|
12
12
|
},
|
13
13
|
"homepage": "https://github.com/nsbno/spor/tree/main/packages/spor-react",
|
@@ -17,42 +17,30 @@
|
|
17
17
|
"directory": "packages/spor-react"
|
18
18
|
},
|
19
19
|
"dependencies": {
|
20
|
-
"@leile/lobo-t": "^1.0.5",
|
21
|
-
"@vygruppen/spor-accordion-react": "*",
|
22
|
-
"@vygruppen/spor-alert-react": "*",
|
23
|
-
"@vygruppen/spor-button-react": "*",
|
24
|
-
"@vygruppen/spor-card-react": "*",
|
25
|
-
"@vygruppen/spor-design-tokens": "*",
|
26
|
-
"@vygruppen/spor-datepicker-react": "*",
|
27
|
-
"@vygruppen/spor-i18n-react": "*",
|
28
|
-
"@vygruppen/spor-icon-react": "*",
|
29
|
-
"@vygruppen/spor-image-react": "*",
|
30
|
-
"@vygruppen/spor-input-react": "*",
|
31
|
-
"@vygruppen/spor-layout-react": "*",
|
32
|
-
"@vygruppen/spor-linjetag-react": "*",
|
33
|
-
"@vygruppen/spor-link-react": "*",
|
34
|
-
"@vygruppen/spor-loader-react": "*",
|
35
|
-
"@vygruppen/spor-logo-react": "*",
|
36
|
-
"@vygruppen/spor-media-controller-react": "*",
|
37
|
-
"@vygruppen/spor-modal-react": "*",
|
38
|
-
"@vygruppen/spor-stepper-react": "*",
|
39
|
-
"@vygruppen/spor-table-react": "*",
|
40
|
-
"@vygruppen/spor-theme-react": "*",
|
41
|
-
"@vygruppen/spor-popover-react": "*",
|
42
|
-
"@vygruppen/spor-provider-react": "*",
|
43
|
-
"@vygruppen/spor-tab-react": "*",
|
44
|
-
"@vygruppen/spor-toast-react": "*",
|
45
|
-
"@vygruppen/spor-typography-react": "*",
|
46
|
-
"@vygruppen/spor-util-react": "*",
|
47
20
|
"@chakra-ui/react": "^2.3.5",
|
21
|
+
"@chakra-ui/theme-tools": "^2.0.12",
|
22
|
+
"@internationalized/date": "^3.0.1",
|
23
|
+
"@leile/lobo-t": "^1.0.5",
|
24
|
+
"@vygruppen/spor-design-tokens": "latest",
|
25
|
+
"@vygruppen/spor-icon-react": "latest",
|
26
|
+
"@vygruppen/spor-loader": "latest",
|
48
27
|
"@emotion/react": "^11.10.4",
|
49
28
|
"@emotion/styled": "^11.10.4",
|
50
|
-
"framer-motion": ">6.0.0"
|
29
|
+
"framer-motion": ">6.0.0",
|
30
|
+
"lottie-react": "^2.3.1",
|
31
|
+
"react-aria": "^3.23.0",
|
32
|
+
"react-stately": "^3.20.0",
|
33
|
+
"react-swipeable": "^7.0.0"
|
51
34
|
},
|
52
35
|
"devDependencies": {
|
53
36
|
"react": "^18.2.0",
|
54
37
|
"react-dom": "^18.2.0",
|
55
|
-
"
|
38
|
+
"@testing-library/jest-dom": "^5.16.5",
|
39
|
+
"@testing-library/react": "^13.4.0",
|
40
|
+
"tsup": "^6.2.2",
|
41
|
+
"vitest": "^0.26.3",
|
42
|
+
"vitest-axe": "^0.1.0",
|
43
|
+
"vitest-canvas-mock": "^0.2.0"
|
56
44
|
},
|
57
45
|
"peerDependencies": {
|
58
46
|
"react": "^18.2.0",
|
@@ -0,0 +1,20 @@
|
|
1
|
+
import { act, render } from "@testing-library/react";
|
2
|
+
import React from "react";
|
3
|
+
import { axe } from "vitest-axe";
|
4
|
+
import { Accordion } from "./Accordion";
|
5
|
+
import { ExpandableItem } from "./Expandable";
|
6
|
+
|
7
|
+
describe("<Accordion />", () => {
|
8
|
+
it("is accessible", async () => {
|
9
|
+
const { container, getByRole } = render(
|
10
|
+
<Accordion>
|
11
|
+
<ExpandableItem title="Title">Content</ExpandableItem>
|
12
|
+
</Accordion>
|
13
|
+
);
|
14
|
+
await act(async () => {
|
15
|
+
expect(await axe(container)).toHaveNoViolations();
|
16
|
+
getByRole("button").click();
|
17
|
+
expect(await axe(container)).toHaveNoViolations();
|
18
|
+
});
|
19
|
+
});
|
20
|
+
});
|
@@ -0,0 +1,62 @@
|
|
1
|
+
import {
|
2
|
+
Accordion as ChakraAccordion,
|
3
|
+
AccordionProps as ChakraAccordionProps,
|
4
|
+
As,
|
5
|
+
forwardRef,
|
6
|
+
} from "@chakra-ui/react";
|
7
|
+
import React from "react";
|
8
|
+
import { AccordionProvider } from "./AccordionContext";
|
9
|
+
export {
|
10
|
+
AccordionButton,
|
11
|
+
AccordionIcon,
|
12
|
+
AccordionItem,
|
13
|
+
AccordionPanel,
|
14
|
+
} from "@chakra-ui/react";
|
15
|
+
export type {
|
16
|
+
AccordionButtonProps,
|
17
|
+
AccordionItemProps,
|
18
|
+
AccordionPanelProps,
|
19
|
+
} from "@chakra-ui/react";
|
20
|
+
|
21
|
+
export type AccordionProps = Omit<ChakraAccordionProps, "variant" | "size"> & {
|
22
|
+
/**
|
23
|
+
* The display variant of the accordion items.
|
24
|
+
*
|
25
|
+
* - `list` renders a pretty unstyled expandable list without any borders
|
26
|
+
* - `outline` renders an outlined version
|
27
|
+
* - `card` renders a version with a drop shadow
|
28
|
+
*/
|
29
|
+
variant?: "list" | "outline" | "card";
|
30
|
+
size?: "sm" | "md" | "lg";
|
31
|
+
};
|
32
|
+
/**
|
33
|
+
* Wraps a set of ExpandableItem or AccordionItem components.
|
34
|
+
*
|
35
|
+
* ```tsx
|
36
|
+
* <Accordion variant="list" size="md">
|
37
|
+
* <ExpandableItem title="Is Spor easy?" headingLevel="h3">
|
38
|
+
* Yes
|
39
|
+
* </ExpandableItem>
|
40
|
+
* <ExpandableItem title="Is Spor lovable?" headingLevel="h3">
|
41
|
+
* 🥰
|
42
|
+
* </ExpandableItem>
|
43
|
+
* </Accordion>
|
44
|
+
* ```
|
45
|
+
*
|
46
|
+
* If you only have one expandable item, you can use the `<Expandable />` component instead.
|
47
|
+
*/
|
48
|
+
export const Accordion = forwardRef<AccordionProps, "div">((props, ref) => {
|
49
|
+
const defaultIndex =
|
50
|
+
typeof props.defaultIndex === "number" && props.allowMultiple
|
51
|
+
? [props.defaultIndex]
|
52
|
+
: props.defaultIndex;
|
53
|
+
return (
|
54
|
+
<AccordionProvider size={props.size}>
|
55
|
+
<ChakraAccordion
|
56
|
+
{...props}
|
57
|
+
ref={ref}
|
58
|
+
defaultIndex={defaultIndex as number[] | undefined}
|
59
|
+
/>
|
60
|
+
</AccordionProvider>
|
61
|
+
);
|
62
|
+
});
|
@@ -0,0 +1,27 @@
|
|
1
|
+
import React from "react";
|
2
|
+
import { AccordionProps } from "./Accordion";
|
3
|
+
|
4
|
+
type AccordionContextType = {
|
5
|
+
size: AccordionProps["size"];
|
6
|
+
};
|
7
|
+
const AccordionContext = React.createContext<AccordionContextType | null>(null);
|
8
|
+
type AccordionProviderProps = {
|
9
|
+
children: React.ReactNode;
|
10
|
+
size: AccordionProps["size"];
|
11
|
+
};
|
12
|
+
export const AccordionProvider = ({
|
13
|
+
size,
|
14
|
+
...props
|
15
|
+
}: AccordionProviderProps) => {
|
16
|
+
return <AccordionContext.Provider value={{ size }} {...props} />;
|
17
|
+
};
|
18
|
+
|
19
|
+
export const useAccordionContext = () => {
|
20
|
+
const context = React.useContext(AccordionContext);
|
21
|
+
if (context === null) {
|
22
|
+
throw new Error(
|
23
|
+
"useAccordionContext must be used within AccordionProvider"
|
24
|
+
);
|
25
|
+
}
|
26
|
+
return context;
|
27
|
+
};
|
@@ -0,0 +1,157 @@
|
|
1
|
+
import {
|
2
|
+
AccordionButton,
|
3
|
+
AccordionIcon,
|
4
|
+
AccordionItem,
|
5
|
+
AccordionItemProps,
|
6
|
+
AccordionPanel,
|
7
|
+
Box,
|
8
|
+
Flex,
|
9
|
+
} from "@chakra-ui/react";
|
10
|
+
import React from "react";
|
11
|
+
import { Accordion, AccordionProps } from "./Accordion";
|
12
|
+
import { useAccordionContext } from "./AccordionContext";
|
13
|
+
|
14
|
+
type HeadingLevel = "h2" | "h3" | "h4" | "h5" | "h6";
|
15
|
+
type ExpandableProps = AccordionProps & {
|
16
|
+
/** The hidden content */
|
17
|
+
children: React.ReactNode;
|
18
|
+
/** The title that's shown inside the toggle button */
|
19
|
+
title: React.ReactNode;
|
20
|
+
/** The semantic heading level of the toggle button */
|
21
|
+
headingLevel?: HeadingLevel;
|
22
|
+
/**
|
23
|
+
* Icon shown to the left of the title
|
24
|
+
*
|
25
|
+
* Make sure it's the outlined version of the icon.
|
26
|
+
*
|
27
|
+
* If the size is set to `sm` or `md` the icon should be 24px.
|
28
|
+
* If the size is set to `lg`, the icon should be 30px.
|
29
|
+
*/
|
30
|
+
leftIcon?: React.ReactNode;
|
31
|
+
};
|
32
|
+
/**
|
33
|
+
* A standalone expandable component.
|
34
|
+
*
|
35
|
+
* This one is great to use if you have a single expandable component by itself.
|
36
|
+
* If you want several expandables in a row, use the `Accordion` and `ExpandableItem` components instead.
|
37
|
+
*
|
38
|
+
* ```tsx
|
39
|
+
* <Expandable title="Click for more" variant="card" size="lg">
|
40
|
+
* <Text>MORE! 🎉</Text>
|
41
|
+
* </Expandable>
|
42
|
+
* ```
|
43
|
+
*/
|
44
|
+
export const Expandable = ({
|
45
|
+
children,
|
46
|
+
headingLevel,
|
47
|
+
title,
|
48
|
+
leftIcon,
|
49
|
+
size = "md",
|
50
|
+
...rest
|
51
|
+
}: ExpandableProps) => {
|
52
|
+
return (
|
53
|
+
<Accordion {...rest} size={size}>
|
54
|
+
<ExpandableItem
|
55
|
+
headingLevel={headingLevel}
|
56
|
+
title={title}
|
57
|
+
leftIcon={leftIcon}
|
58
|
+
>
|
59
|
+
{children}
|
60
|
+
</ExpandableItem>
|
61
|
+
</Accordion>
|
62
|
+
);
|
63
|
+
};
|
64
|
+
|
65
|
+
export type ExpandableItemProps = AccordionItemProps & {
|
66
|
+
/** The hidden content */
|
67
|
+
children: React.ReactNode;
|
68
|
+
/** The title that's shown inside the toggle button */
|
69
|
+
title: React.ReactNode;
|
70
|
+
/** The semantic heading level of the toggle button */
|
71
|
+
headingLevel?: HeadingLevel;
|
72
|
+
/**
|
73
|
+
* Icon shown to the left of the title
|
74
|
+
*
|
75
|
+
* Make sure it's the 30px outlined version of the icon
|
76
|
+
*/
|
77
|
+
leftIcon?: React.ReactNode;
|
78
|
+
};
|
79
|
+
/**
|
80
|
+
* An item in a set of Expandables. Must be wrapped in an `<Accordion>` component.
|
81
|
+
*
|
82
|
+
* ```tsx
|
83
|
+
* <Accordion variant="list" size="md">
|
84
|
+
* <ExpandableItem title="Is Spor easy?" headingLevel="h3">
|
85
|
+
* Yes
|
86
|
+
* </ExpandableItem>
|
87
|
+
* <ExpandableItem title="Do you love it?" headingLevel="h3">
|
88
|
+
* 🥰
|
89
|
+
* </ExpandableItem>
|
90
|
+
* </Accordion>
|
91
|
+
* ```
|
92
|
+
*
|
93
|
+
* If you need even more control, you can put together your own expandable with the `Accordion`, `AccordionItem`, `AccordionButton`, `AccordionIcon` and `AccordionPanel` components.
|
94
|
+
*/
|
95
|
+
export const ExpandableItem = ({
|
96
|
+
children,
|
97
|
+
title,
|
98
|
+
headingLevel = "h3",
|
99
|
+
leftIcon,
|
100
|
+
...rest
|
101
|
+
}: ExpandableItemProps) => {
|
102
|
+
const { size } = useAccordionContext();
|
103
|
+
warnAboutMismatchingIcon({ icon: leftIcon, size });
|
104
|
+
return (
|
105
|
+
<AccordionItem {...rest}>
|
106
|
+
<Box as={headingLevel}>
|
107
|
+
<AccordionButton>
|
108
|
+
<Flex alignItems="center">
|
109
|
+
{leftIcon && <Box mr={2}>{leftIcon}</Box>}
|
110
|
+
{title}
|
111
|
+
</Flex>
|
112
|
+
<AccordionIcon />
|
113
|
+
</AccordionButton>
|
114
|
+
</Box>
|
115
|
+
<AccordionPanel>{children}</AccordionPanel>
|
116
|
+
</AccordionItem>
|
117
|
+
);
|
118
|
+
};
|
119
|
+
|
120
|
+
type WarnAboutMismatchingIcon = {
|
121
|
+
icon: any;
|
122
|
+
size: AccordionProps["size"];
|
123
|
+
};
|
124
|
+
const warnAboutMismatchingIcon = ({ icon, size }: WarnAboutMismatchingIcon) => {
|
125
|
+
if (process.env.NODE_ENV !== "production") {
|
126
|
+
const displayName = icon?.type?.render?.displayName;
|
127
|
+
if (!displayName) {
|
128
|
+
return;
|
129
|
+
}
|
130
|
+
if (displayName.includes("Fill")) {
|
131
|
+
console.warn(
|
132
|
+
`You passed a filled icon. This component requires outlined icons. You passed ${displayName}, replace it with ${displayName.replace(
|
133
|
+
"Fill",
|
134
|
+
"Outline"
|
135
|
+
)}.`
|
136
|
+
);
|
137
|
+
return;
|
138
|
+
}
|
139
|
+
if (size === "lg" && !displayName.includes("30Icon")) {
|
140
|
+
console.warn(
|
141
|
+
`The icon you passed was of the wrong size for the lg size. You passed ${displayName}, replace it with ${displayName.replace(
|
142
|
+
/(\d{2})Icon/,
|
143
|
+
"30Icon"
|
144
|
+
)}.`
|
145
|
+
);
|
146
|
+
return;
|
147
|
+
}
|
148
|
+
if (["md" || "sm"].includes(size!) && !displayName.includes("24Icon")) {
|
149
|
+
console.warn(
|
150
|
+
`The icon you passed was of the wrong size for the ${size} size. You passed ${displayName}, replace it with ${displayName.replace(
|
151
|
+
/(\d{2})Icon/,
|
152
|
+
"24Icon"
|
153
|
+
)}.`
|
154
|
+
);
|
155
|
+
}
|
156
|
+
}
|
157
|
+
};
|
@@ -0,0 +1,75 @@
|
|
1
|
+
import {
|
2
|
+
AltTransportOutline24Icon,
|
3
|
+
ErrorOutline24Icon,
|
4
|
+
InformationOutline24Icon,
|
5
|
+
SuccessOutline24Icon,
|
6
|
+
WarningOutline24Icon,
|
7
|
+
} from "@vygruppen/spor-icon-react";
|
8
|
+
import React from "react";
|
9
|
+
import { createTexts, useTranslation } from "../i18n";
|
10
|
+
import { BaseAlertProps } from "./BaseAlert";
|
11
|
+
|
12
|
+
type AlertIconProps = { variant: BaseAlertProps["variant"] };
|
13
|
+
/**
|
14
|
+
* Internal component that shows the correct icon for the alert.
|
15
|
+
*/
|
16
|
+
export const AlertIcon = ({ variant }: AlertIconProps) => {
|
17
|
+
const Icon = getIcon(variant);
|
18
|
+
const { t } = useTranslation();
|
19
|
+
return (
|
20
|
+
<Icon
|
21
|
+
flexShrink={0}
|
22
|
+
aria-label={t(texts[variant])}
|
23
|
+
marginRight={1}
|
24
|
+
color="darkGrey"
|
25
|
+
/>
|
26
|
+
);
|
27
|
+
};
|
28
|
+
|
29
|
+
const getIcon = (variant: BaseAlertProps["variant"]) => {
|
30
|
+
switch (variant) {
|
31
|
+
case "info":
|
32
|
+
return InformationOutline24Icon;
|
33
|
+
case "success":
|
34
|
+
return SuccessOutline24Icon;
|
35
|
+
case "warning":
|
36
|
+
return WarningOutline24Icon;
|
37
|
+
case "alt-transport":
|
38
|
+
return AltTransportOutline24Icon;
|
39
|
+
case "error":
|
40
|
+
return ErrorOutline24Icon;
|
41
|
+
}
|
42
|
+
};
|
43
|
+
|
44
|
+
const texts = createTexts({
|
45
|
+
info: {
|
46
|
+
nb: "Informasjon",
|
47
|
+
nn: "Informasjon",
|
48
|
+
sv: "Information",
|
49
|
+
en: "Information",
|
50
|
+
},
|
51
|
+
success: {
|
52
|
+
nb: "Suksess",
|
53
|
+
nn: "Suksess",
|
54
|
+
sv: "Succé",
|
55
|
+
en: "Success",
|
56
|
+
},
|
57
|
+
warning: {
|
58
|
+
nb: "Advarsel",
|
59
|
+
nn: "Advarsel",
|
60
|
+
sv: "Varning",
|
61
|
+
en: "Warning",
|
62
|
+
},
|
63
|
+
error: {
|
64
|
+
nb: "Feil",
|
65
|
+
nn: "Feil",
|
66
|
+
sv: "Error",
|
67
|
+
en: "Error",
|
68
|
+
},
|
69
|
+
"alt-transport": {
|
70
|
+
nb: "Alternativ transport",
|
71
|
+
nn: "Alternativ transport",
|
72
|
+
sv: "Alternativ transport",
|
73
|
+
en: "Alternative transport",
|
74
|
+
},
|
75
|
+
});
|
@@ -0,0 +1,37 @@
|
|
1
|
+
import { render } from "@testing-library/react";
|
2
|
+
import React from "react";
|
3
|
+
import { axe } from "vitest-axe";
|
4
|
+
import { BaseAlert } from "./BaseAlert";
|
5
|
+
|
6
|
+
describe("<BaseAlert />", () => {
|
7
|
+
it("is accessible as variant='success'", async () => {
|
8
|
+
const { container } = render(
|
9
|
+
<BaseAlert variant="success">Test text</BaseAlert>
|
10
|
+
);
|
11
|
+
expect(await axe(container)).toHaveNoViolations();
|
12
|
+
});
|
13
|
+
it("is accessible as variant='info'", async () => {
|
14
|
+
const { container } = render(
|
15
|
+
<BaseAlert variant="info">Test text</BaseAlert>
|
16
|
+
);
|
17
|
+
expect(await axe(container)).toHaveNoViolations();
|
18
|
+
});
|
19
|
+
it("is accessible as variant='warning'", async () => {
|
20
|
+
const { container } = render(
|
21
|
+
<BaseAlert variant="warning">Test text</BaseAlert>
|
22
|
+
);
|
23
|
+
expect(await axe(container)).toHaveNoViolations();
|
24
|
+
});
|
25
|
+
it("is accessible as variant='error'", async () => {
|
26
|
+
const { container } = render(
|
27
|
+
<BaseAlert variant="error">Test text</BaseAlert>
|
28
|
+
);
|
29
|
+
expect(await axe(container)).toHaveNoViolations();
|
30
|
+
});
|
31
|
+
it("is accessible as variant='alt-transport'", async () => {
|
32
|
+
const { container } = render(
|
33
|
+
<BaseAlert variant="alt-transport">Test text</BaseAlert>
|
34
|
+
);
|
35
|
+
expect(await axe(container)).toHaveNoViolations();
|
36
|
+
});
|
37
|
+
});
|
@@ -0,0 +1,21 @@
|
|
1
|
+
import { Box, BoxProps, useMultiStyleConfig } from "@chakra-ui/react";
|
2
|
+
import React from "react";
|
3
|
+
|
4
|
+
export type BaseAlertProps = BoxProps & {
|
5
|
+
/** The color scheme and icon of the alert */
|
6
|
+
variant: "info" | "success" | "warning" | "alt-transport" | "error";
|
7
|
+
/** The body content of the alert */
|
8
|
+
children: React.ReactNode;
|
9
|
+
};
|
10
|
+
|
11
|
+
/**
|
12
|
+
* A base alert box component. Should only be composed by other alert components.
|
13
|
+
*/
|
14
|
+
export const BaseAlert = ({ variant, children, ...boxProps }: BaseAlertProps) => {
|
15
|
+
const styles = useMultiStyleConfig("Alert", { variant });
|
16
|
+
return (
|
17
|
+
<Box sx={styles.container} {...boxProps}>
|
18
|
+
{children}
|
19
|
+
</Box>
|
20
|
+
);
|
21
|
+
};
|
@@ -0,0 +1,37 @@
|
|
1
|
+
import { act, render } from "@testing-library/react";
|
2
|
+
import React from "react";
|
3
|
+
import { vi } from "vitest";
|
4
|
+
import { axe } from "vitest-axe";
|
5
|
+
import { ClosableAlert } from ".";
|
6
|
+
|
7
|
+
describe("<ClosableAlert />", () => {
|
8
|
+
it("closes when you click the close button", async () => {
|
9
|
+
const { getByRole, queryByRole } = render(
|
10
|
+
<ClosableAlert variant="info">Test text</ClosableAlert>
|
11
|
+
);
|
12
|
+
act(() => {
|
13
|
+
getByRole("button").click();
|
14
|
+
});
|
15
|
+
expect(queryByRole("button")).not.toBeInTheDocument();
|
16
|
+
});
|
17
|
+
|
18
|
+
it("calls the onClose prop when closed", () => {
|
19
|
+
const handleClick = vi.fn();
|
20
|
+
const { getByRole } = render(
|
21
|
+
<ClosableAlert variant="info" onClose={handleClick}>
|
22
|
+
Test text
|
23
|
+
</ClosableAlert>
|
24
|
+
);
|
25
|
+
act(() => {
|
26
|
+
getByRole("button").click();
|
27
|
+
});
|
28
|
+
expect(handleClick).toHaveBeenCalled();
|
29
|
+
});
|
30
|
+
|
31
|
+
it("is accessible", async () => {
|
32
|
+
const { container } = render(
|
33
|
+
<ClosableAlert variant="info">Test text</ClosableAlert>
|
34
|
+
);
|
35
|
+
expect(await axe(container)).toHaveNoViolations();
|
36
|
+
});
|
37
|
+
});
|
@@ -0,0 +1,75 @@
|
|
1
|
+
import { useDisclosure, useMultiStyleConfig } from "@chakra-ui/react";
|
2
|
+
import { CloseFill18Icon } from "@vygruppen/spor-icon-react";
|
3
|
+
import React from "react";
|
4
|
+
import { IconButton } from "../button";
|
5
|
+
import { createTexts, useTranslation } from "../i18n";
|
6
|
+
import { AlertIcon } from "./AlertIcon";
|
7
|
+
import { BaseAlert, BaseAlertProps } from "./BaseAlert";
|
8
|
+
|
9
|
+
type ClosableAlertProps = BaseAlertProps & {
|
10
|
+
/** Callback for when the close button is clicked */
|
11
|
+
onClose?: () => void;
|
12
|
+
};
|
13
|
+
/**
|
14
|
+
* A closable alert component.
|
15
|
+
*
|
16
|
+
* A regular alert with a close button that can be used to dismiss the alert.
|
17
|
+
*
|
18
|
+
* ```tsx
|
19
|
+
* <ClosableAlert variant="info" title="Nice to know">
|
20
|
+
* <Text>Some info here</Text>
|
21
|
+
* </ClosableAlert>
|
22
|
+
* ```
|
23
|
+
*
|
24
|
+
* You can also pass in an optional `onClose` callback, for things like analytics.
|
25
|
+
*
|
26
|
+
* ```tsx
|
27
|
+
* <ClosableAlert
|
28
|
+
* variant="info"
|
29
|
+
* title="Nice to know"
|
30
|
+
* onClose={() => analytics.track('alert-closed')}
|
31
|
+
* >
|
32
|
+
* <Text>Some info here</Text>
|
33
|
+
* </ClosableAlert>
|
34
|
+
*/
|
35
|
+
export const ClosableAlert = ({
|
36
|
+
variant,
|
37
|
+
children,
|
38
|
+
onClose: externalOnClose = () => {},
|
39
|
+
}: ClosableAlertProps) => {
|
40
|
+
const { isOpen, onClose } = useDisclosure({ defaultIsOpen: true });
|
41
|
+
const styles = useMultiStyleConfig("Alert", { variant });
|
42
|
+
const { t } = useTranslation();
|
43
|
+
if (!isOpen) {
|
44
|
+
return null;
|
45
|
+
}
|
46
|
+
|
47
|
+
const handleClose = () => {
|
48
|
+
externalOnClose();
|
49
|
+
onClose();
|
50
|
+
};
|
51
|
+
|
52
|
+
return (
|
53
|
+
<BaseAlert variant={variant}>
|
54
|
+
<IconButton
|
55
|
+
variant="ghost"
|
56
|
+
size="sm"
|
57
|
+
onClick={handleClose}
|
58
|
+
icon={<CloseFill18Icon />}
|
59
|
+
aria-label={t(texts.close)}
|
60
|
+
sx={styles.closeButton}
|
61
|
+
/>
|
62
|
+
<AlertIcon variant={variant} />
|
63
|
+
{children}
|
64
|
+
</BaseAlert>
|
65
|
+
);
|
66
|
+
};
|
67
|
+
|
68
|
+
const texts = createTexts({
|
69
|
+
close: {
|
70
|
+
nb: "Lukk",
|
71
|
+
nn: "Lukk",
|
72
|
+
sv: "Dölj",
|
73
|
+
en: "Close",
|
74
|
+
},
|
75
|
+
});
|
@@ -0,0 +1,84 @@
|
|
1
|
+
import { act, render, waitFor } from "@testing-library/react";
|
2
|
+
import React from "react";
|
3
|
+
import { vi } from "vitest";
|
4
|
+
import { axe } from "vitest-axe";
|
5
|
+
import { ExpandableAlert } from ".";
|
6
|
+
|
7
|
+
describe("<ExpandableAlert />", () => {
|
8
|
+
it("works as an expandable", async () => {
|
9
|
+
const { getByRole, getByText } = render(
|
10
|
+
<ExpandableAlert variant="info" title="Title">
|
11
|
+
Test text
|
12
|
+
</ExpandableAlert>
|
13
|
+
);
|
14
|
+
await waitFor(() => expect(getByText("Test text")).not.toBeVisible());
|
15
|
+
act(() => {
|
16
|
+
getByRole("button").click();
|
17
|
+
});
|
18
|
+
await waitFor(() => expect(getByText("Test text")).toBeVisible());
|
19
|
+
act(() => {
|
20
|
+
getByRole("button").click();
|
21
|
+
});
|
22
|
+
await waitFor(() => expect(getByText("Test text")).not.toBeVisible());
|
23
|
+
});
|
24
|
+
|
25
|
+
it("calls onToggle when toggled", async () => {
|
26
|
+
const handleClick = vi.fn();
|
27
|
+
const { getByRole } = render(
|
28
|
+
<ExpandableAlert variant="info" title="Title" onToggle={handleClick}>
|
29
|
+
Test text
|
30
|
+
</ExpandableAlert>
|
31
|
+
);
|
32
|
+
|
33
|
+
act(() => {
|
34
|
+
getByRole("button").click();
|
35
|
+
});
|
36
|
+
expect(handleClick).toHaveBeenCalledTimes(1);
|
37
|
+
expect(handleClick).lastCalledWith(true);
|
38
|
+
|
39
|
+
act(() => {
|
40
|
+
getByRole("button").click();
|
41
|
+
});
|
42
|
+
expect(handleClick).toHaveBeenCalledTimes(2);
|
43
|
+
expect(handleClick).lastCalledWith(false);
|
44
|
+
});
|
45
|
+
|
46
|
+
it("lets you set the heading level", async () => {
|
47
|
+
const { queryByRole, rerender } = render(
|
48
|
+
<ExpandableAlert variant="info" title="Title" headingLevel="h2">
|
49
|
+
Test text
|
50
|
+
</ExpandableAlert>
|
51
|
+
);
|
52
|
+
expect(queryByRole("heading", { level: 2 })).toBeInTheDocument();
|
53
|
+
rerender(
|
54
|
+
<ExpandableAlert variant="info" title="Title" headingLevel="h3">
|
55
|
+
Test text
|
56
|
+
</ExpandableAlert>
|
57
|
+
);
|
58
|
+
expect(queryByRole("heading", { level: 2 })).not.toBeInTheDocument();
|
59
|
+
expect(queryByRole("heading", { level: 3 })).toBeInTheDocument();
|
60
|
+
});
|
61
|
+
|
62
|
+
it("is accessible in all states", async () => {
|
63
|
+
const { container, getByRole } = render(
|
64
|
+
<ExpandableAlert variant="info" title="Title">
|
65
|
+
Test text
|
66
|
+
</ExpandableAlert>
|
67
|
+
);
|
68
|
+
await act(async () => {
|
69
|
+
expect(await axe(container)).toHaveNoViolations();
|
70
|
+
});
|
71
|
+
act(() => {
|
72
|
+
getByRole("button").click();
|
73
|
+
});
|
74
|
+
await act(async () => {
|
75
|
+
expect(await axe(container)).toHaveNoViolations();
|
76
|
+
});
|
77
|
+
act(() => {
|
78
|
+
getByRole("button").click();
|
79
|
+
});
|
80
|
+
await act(async () => {
|
81
|
+
expect(await axe(container)).toHaveNoViolations();
|
82
|
+
});
|
83
|
+
});
|
84
|
+
});
|