@heliosgraphics/ui 2.0.0-alpha.95 → 2.0.0-alpha.97
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/components/Alert/Alert.tsx +2 -0
- package/components/Breadcrumb/Breadcrumb.tsx +17 -1
- package/components/Browser/Browser.tsx +2 -0
- package/components/Button/Button.tsx +7 -3
- package/components/ButtonGroup/ButtonGroup.tsx +2 -0
- package/components/Checkbox/Checkbox.tsx +66 -55
- package/components/Clock/Clock.tsx +23 -19
- package/components/Column/Column.tsx +2 -0
- package/components/Confirm/Confirm.tsx +2 -0
- package/components/DatePicker/DatePicker.meta.ts +12 -5
- package/components/DatePicker/DatePicker.module.css +70 -1
- package/components/DatePicker/DatePicker.tsx +116 -4
- package/components/DatePicker/DatePicker.types.ts +6 -1
- package/components/DatePicker/DatePicker.utils.ts +53 -0
- package/components/Debug/Debug.tsx +2 -0
- package/components/Details/Details.tsx +2 -0
- package/components/Dialog/Dialog.module.css +4 -6
- package/components/Dialog/Dialog.tsx +33 -24
- package/components/Donut/Donut.tsx +2 -0
- package/components/Dot/Dot.tsx +2 -0
- package/components/Dropdown/Dropdown.module.css +5 -0
- package/components/Dropdown/Dropdown.tsx +21 -26
- package/components/Fieldset/Fieldset.tsx +2 -0
- package/components/Flex/Flex.meta.ts +1 -0
- package/components/Flex/Flex.tsx +22 -2
- package/components/Flex/Flex.types.ts +1 -0
- package/components/Flex/Flex.utils.spec.ts +4 -1
- package/components/Flex/Flex.utils.ts +4 -1
- package/components/Grid/Grid.tsx +2 -0
- package/components/Heading/Heading.meta.ts +5 -0
- package/components/Heading/Heading.tsx +15 -9
- package/components/Heading/Heading.types.ts +1 -0
- package/components/Heading/components/H0/H0.tsx +2 -0
- package/components/Heading/components/H1/H1.tsx +2 -0
- package/components/Heading/components/H2/H2.tsx +2 -0
- package/components/Heading/components/H3/H3.tsx +2 -0
- package/components/Heading/components/H4/H4.tsx +2 -0
- package/components/Heading/components/H5/H5.tsx +2 -0
- package/components/Heading/components/H6/H6.tsx +2 -0
- package/components/Icon/Icon.tsx +2 -0
- package/components/Input/Input.tsx +103 -95
- package/components/Layout/Layout.tsx +2 -0
- package/components/Layout/components/LayoutAside/LayoutAside.tsx +2 -0
- package/components/Layout/components/LayoutAside/components/LayoutAsideContent/LayoutAsideContent.tsx +2 -0
- package/components/Layout/components/LayoutAside/components/LayoutAsideFooter/LayoutAsideFooter.tsx +2 -0
- package/components/Layout/components/LayoutAside/components/LayoutAsideToggle/LayoutAsideToggle.tsx +2 -0
- package/components/Layout/components/LayoutMain/LayoutMain.tsx +2 -0
- package/components/Layout/components/LayoutMain/components/LayoutMainContent/LayoutMainContent.tsx +2 -0
- package/components/Layout/components/LayoutNavigation/LayoutNavigation.tsx +2 -0
- package/components/Layout/components/LayoutSubNavigation/LayoutSubNavigation.tsx +2 -0
- package/components/Loading/Loading.tsx +2 -0
- package/components/Markdown/Markdown.tsx +2 -0
- package/components/Masonry/Masonry.tsx +5 -1
- package/components/Menu/Menu.tsx +2 -0
- package/components/Menu/components/MenuCategory/MenuCategory.tsx +2 -0
- package/components/Menu/components/MenuFilter/MenuFilter.tsx +2 -0
- package/components/Menu/components/MenuItem/MenuItem.tsx +2 -0
- package/components/Overlay/Overlay.tsx +18 -2
- package/components/Pie/Pie.tsx +2 -0
- package/components/Pill/Pill.meta.ts +9 -1
- package/components/Pill/Pill.module.css +11 -0
- package/components/Pill/Pill.tsx +28 -3
- package/components/Pill/Pill.types.ts +2 -0
- package/components/Placeholder/Placeholder.tsx +2 -0
- package/components/Progress/Progress.tsx +2 -0
- package/components/Radio/Radio.tsx +2 -0
- package/components/Range/Range.tsx +2 -0
- package/components/Segments/Segments.context.ts +19 -0
- package/components/Segments/Segments.meta.ts +4 -0
- package/components/Segments/Segments.tsx +34 -42
- package/components/Segments/Segments.types.ts +1 -0
- package/components/Segments/components/SegmentButton/SegmentButton.meta.ts +0 -4
- package/components/Segments/components/SegmentButton/SegmentButton.tsx +28 -3
- package/components/Segments/components/SegmentButton/SegmentButton.types.ts +0 -2
- package/components/Select/Select.tsx +40 -43
- package/components/Separator/Separator.tsx +2 -0
- package/components/Separator/components/HRMarkup/HRMarkup.tsx +2 -0
- package/components/Separator/components/HorizontalSeparator/HorizontalSeparator.tsx +2 -0
- package/components/Separator/components/VerticalSeparator/VerticalSeparator.tsx +2 -0
- package/components/Setup/Setup.tsx +3 -0
- package/components/Shimmer/Shimmer.tsx +2 -0
- package/components/Slider/Slider.tsx +2 -0
- package/components/Spacer/Spacer.tsx +2 -0
- package/components/Table/Table.tsx +2 -0
- package/components/Tabs/Tabs.meta.ts +12 -12
- package/components/Tabs/Tabs.module.css +25 -9
- package/components/Tabs/Tabs.tsx +49 -53
- package/components/Tabs/Tabs.types.ts +10 -3
- package/components/Text/Text.tsx +2 -0
- package/components/Text/components/Div/Div.tsx +2 -0
- package/components/Text/components/Micro/Micro.tsx +2 -0
- package/components/Text/components/P/P.tsx +2 -0
- package/components/Text/components/Small/Small.tsx +2 -0
- package/components/Text/components/Tiny/Tiny.tsx +2 -0
- package/components/Textarea/Textarea.tsx +14 -13
- package/components/Tile/Tile.tsx +2 -0
- package/components/Timestamp/Timestamp.tsx +2 -0
- package/components/Toggle/Toggle.tsx +2 -0
- package/components/Tooltip/Tooltip.tsx +17 -9
- package/components/Tooltip/Tooltip.types.ts +0 -1
- package/components/Tooltip/components/TooltipContent/TooltipContent.tsx +2 -0
- package/components/Tooltip/components/TooltipTrigger/TooltipTrigger.tsx +4 -2
- package/components/shared/InputLabel/InputLabel.tsx +2 -0
- package/components/shared/ResultList/ResultList.tsx +6 -4
- package/contexts/LayoutContext/LayoutContext.tsx +15 -34
- package/contexts/LayoutContext/LayoutContext.types.ts +0 -1
- package/hooks/useLayoutContext.tsx +0 -1
- package/hooks/useResizeObserver.tsx +2 -2
- package/index.ts +5 -0
- package/package.json +1 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Fragment, type FC, type ElementType } from "react"
|
|
1
|
+
import { Fragment, type FC, type ElementType, type KeyboardEvent } from "react"
|
|
2
2
|
import { Text } from "../Text"
|
|
3
3
|
import { Flex } from "../Flex"
|
|
4
4
|
import { Icon } from "../Icon"
|
|
@@ -13,12 +13,26 @@ export const Breadcrumb: FC<BreadcrumbProps> = ({ items }) => {
|
|
|
13
13
|
{items?.map((item, index) => {
|
|
14
14
|
const isLast: boolean = Boolean(index + 1 === items?.length)
|
|
15
15
|
const AComponent: ElementType = item.as || "a"
|
|
16
|
+
const isCustomElement: boolean = !!item.as && item.as !== "a" && item.as !== "button"
|
|
17
|
+
const keyboardProps =
|
|
18
|
+
isCustomElement && item.onClick
|
|
19
|
+
? {
|
|
20
|
+
role: "link" as const,
|
|
21
|
+
tabIndex: 0,
|
|
22
|
+
onKeyDown: (event: KeyboardEvent): void => {
|
|
23
|
+
if (event.key === "Enter") {
|
|
24
|
+
item.onClick?.(event as unknown as React.MouseEvent<HTMLAnchorElement>)
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
}
|
|
28
|
+
: {}
|
|
16
29
|
|
|
17
30
|
return (
|
|
18
31
|
<Fragment key={item.href ?? item.name}>
|
|
19
32
|
<AComponent
|
|
20
33
|
href={item.href}
|
|
21
34
|
onClick={item.onClick}
|
|
35
|
+
{...keyboardProps}
|
|
22
36
|
{...(item.isActive && { "aria-current": "page" as const })}
|
|
23
37
|
>
|
|
24
38
|
<Text
|
|
@@ -37,3 +51,5 @@ export const Breadcrumb: FC<BreadcrumbProps> = ({ items }) => {
|
|
|
37
51
|
</nav>
|
|
38
52
|
)
|
|
39
53
|
}
|
|
54
|
+
|
|
55
|
+
Breadcrumb.displayName = "Breadcrumb"
|
|
@@ -6,7 +6,7 @@ import { Flex } from "../Flex"
|
|
|
6
6
|
import { Icon } from "../Icon"
|
|
7
7
|
import { Loading } from "../Loading"
|
|
8
8
|
import { Text } from "../Text"
|
|
9
|
-
import { useState, useId, type KeyboardEvent, type MouseEvent, type FC, useMemo } from "react"
|
|
9
|
+
import { useState, useId, useRef, type KeyboardEvent, type MouseEvent, type FC, useMemo } from "react"
|
|
10
10
|
import { getColorClasses } from "../../utils/colors"
|
|
11
11
|
import { INTENTION_COLOR_MAP } from "../../constants/intentions"
|
|
12
12
|
import type { HeliosIconType } from "../../types/icons"
|
|
@@ -52,6 +52,7 @@ export const Button: FC<ButtonProps> = ({
|
|
|
52
52
|
}) => {
|
|
53
53
|
const [isActive, setIsActive] = useState<boolean>(false)
|
|
54
54
|
|
|
55
|
+
const inputRef = useRef<HTMLInputElement>(null)
|
|
55
56
|
const buttonId = useId()
|
|
56
57
|
const isIconOnlyLoading: boolean = !!isIconOnly && !!isLoading
|
|
57
58
|
|
|
@@ -130,10 +131,10 @@ export const Button: FC<ButtonProps> = ({
|
|
|
130
131
|
if (event.key !== "Enter" && event.key !== " ") return
|
|
131
132
|
event.preventDefault()
|
|
132
133
|
|
|
133
|
-
if (isDisabled || isLoading
|
|
134
|
+
if (isDisabled || isLoading) return
|
|
134
135
|
|
|
135
136
|
setIsActive(true)
|
|
136
|
-
|
|
137
|
+
inputRef.current?.click()
|
|
137
138
|
}
|
|
138
139
|
|
|
139
140
|
return (
|
|
@@ -159,6 +160,7 @@ export const Button: FC<ButtonProps> = ({
|
|
|
159
160
|
</Flex>
|
|
160
161
|
)}
|
|
161
162
|
<input
|
|
163
|
+
ref={inputRef}
|
|
162
164
|
id={buttonId}
|
|
163
165
|
aria-disabled={isDisabled || isLoading}
|
|
164
166
|
disabled={isDisabled || isLoading}
|
|
@@ -201,3 +203,5 @@ export const Button: FC<ButtonProps> = ({
|
|
|
201
203
|
</Flex>
|
|
202
204
|
)
|
|
203
205
|
}
|
|
206
|
+
|
|
207
|
+
Button.displayName = "Button"
|
|
@@ -1,69 +1,80 @@
|
|
|
1
1
|
"use client"
|
|
2
2
|
|
|
3
|
-
import { useId } from "react"
|
|
3
|
+
import { useId, forwardRef } from "react"
|
|
4
4
|
import { getClasses } from "@heliosgraphics/utils"
|
|
5
5
|
import { getColorClasses } from "../../utils/colors"
|
|
6
6
|
import { Flex } from "../Flex"
|
|
7
7
|
import { Icon } from "../Icon"
|
|
8
8
|
import { Text } from "../Text"
|
|
9
9
|
import styles from "./Checkbox.module.css"
|
|
10
|
-
import type { FC } from "react"
|
|
11
10
|
import type { CheckboxProps } from "./Checkbox.types"
|
|
12
11
|
|
|
13
|
-
export const Checkbox
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
12
|
+
export const Checkbox = forwardRef<HTMLInputElement, CheckboxProps>(
|
|
13
|
+
(
|
|
14
|
+
{
|
|
15
|
+
color = "gray",
|
|
16
|
+
isChecked,
|
|
17
|
+
isVertical,
|
|
18
|
+
isLabelHidden = false,
|
|
19
|
+
isSmall,
|
|
20
|
+
description,
|
|
21
|
+
isDisabled,
|
|
22
|
+
isRequired,
|
|
23
|
+
onChange,
|
|
24
|
+
label,
|
|
25
|
+
},
|
|
26
|
+
ref,
|
|
27
|
+
) => {
|
|
28
|
+
const checkboxId: string = useId()
|
|
26
29
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
30
|
+
const colorClasses = getColorClasses(color, "dark")
|
|
31
|
+
const checkboxClasses = getClasses(styles.checkbox, ...colorClasses, {
|
|
32
|
+
[styles.checkboxDisabled]: isDisabled,
|
|
33
|
+
[styles.checkboxSmall]: isSmall,
|
|
34
|
+
})
|
|
32
35
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
36
|
+
const checkboxLabelClasses = getClasses(styles.checkbox__checkboxLabel, "flex gap-4", {
|
|
37
|
+
"flex-x-center flex-column": isVertical,
|
|
38
|
+
"flex-y-center": !isVertical,
|
|
39
|
+
})
|
|
37
40
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
<
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
41
|
+
return (
|
|
42
|
+
<div className={checkboxClasses} data-ui-component="Checkbox">
|
|
43
|
+
<label className={checkboxLabelClasses} htmlFor={checkboxId}>
|
|
44
|
+
<span className={styles.checkbox__mark}>
|
|
45
|
+
<input
|
|
46
|
+
ref={ref}
|
|
47
|
+
type="checkbox"
|
|
48
|
+
checked={isChecked}
|
|
49
|
+
onChange={onChange}
|
|
50
|
+
disabled={isDisabled}
|
|
51
|
+
required={isRequired}
|
|
52
|
+
aria-label={isLabelHidden ? label : undefined}
|
|
53
|
+
id={checkboxId}
|
|
54
|
+
/>
|
|
55
|
+
<Icon icon="check" size={isSmall ? 14 : 18} className={styles.checkbox__checkboxIcon} />
|
|
56
|
+
<div className={styles.checkbox__checkboxMark} />
|
|
57
|
+
</span>
|
|
58
|
+
{!isLabelHidden && (
|
|
59
|
+
<Flex isColumn={true}>
|
|
60
|
+
<Text
|
|
61
|
+
type={isSmall ? "tiny" : "small"}
|
|
62
|
+
fontWeight="medium"
|
|
63
|
+
emphasis={isDisabled ? "tertiary" : "inherit"}
|
|
64
|
+
>
|
|
65
|
+
{label}
|
|
62
66
|
</Text>
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
}
|
|
67
|
+
{description && (
|
|
68
|
+
<Text type="tiny" emphasis="secondary">
|
|
69
|
+
{description}
|
|
70
|
+
</Text>
|
|
71
|
+
)}
|
|
72
|
+
</Flex>
|
|
73
|
+
)}
|
|
74
|
+
</label>
|
|
75
|
+
</div>
|
|
76
|
+
)
|
|
77
|
+
},
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
Checkbox.displayName = "Checkbox"
|
|
@@ -1,11 +1,29 @@
|
|
|
1
1
|
"use client"
|
|
2
2
|
|
|
3
|
-
import { useState, useEffect } from "react"
|
|
3
|
+
import { useId, useState, useEffect } from "react"
|
|
4
4
|
import styles from "./Clock.module.css"
|
|
5
5
|
import type { FC } from "react"
|
|
6
6
|
import type { ClockProps } from "./Clock.types"
|
|
7
7
|
|
|
8
|
+
const getRotation = (time: Date, type: "hours" | "minutes" | "seconds"): string => {
|
|
9
|
+
const hours = time.getHours() % 12
|
|
10
|
+
const minutes = time.getMinutes()
|
|
11
|
+
const seconds = time.getSeconds()
|
|
12
|
+
|
|
13
|
+
switch (type) {
|
|
14
|
+
case "hours":
|
|
15
|
+
return `rotate(${(hours + minutes / 60) * 30 - 180}, 0, 0)`
|
|
16
|
+
case "minutes":
|
|
17
|
+
return `rotate(${minutes * 6 - 180}, 0, 0)`
|
|
18
|
+
case "seconds":
|
|
19
|
+
return `rotate(${seconds * 6 - 180}, 0, 0)`
|
|
20
|
+
default:
|
|
21
|
+
return ""
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
8
25
|
export const Clock: FC<ClockProps> = () => {
|
|
26
|
+
const titleId: string = useId()
|
|
9
27
|
const [time, setTime] = useState(() => new Date())
|
|
10
28
|
|
|
11
29
|
useEffect(() => {
|
|
@@ -16,26 +34,10 @@ export const Clock: FC<ClockProps> = () => {
|
|
|
16
34
|
return (): void => globalThis.clearInterval(timerId)
|
|
17
35
|
}, [])
|
|
18
36
|
|
|
19
|
-
const getRotation = (time: Date, type: "hours" | "minutes" | "seconds"): string => {
|
|
20
|
-
const hours = time.getHours() % 12
|
|
21
|
-
const minutes = time.getMinutes()
|
|
22
|
-
const seconds = time.getSeconds()
|
|
23
|
-
|
|
24
|
-
switch (type) {
|
|
25
|
-
case "hours":
|
|
26
|
-
return `rotate(${(hours + minutes / 60) * 30 - 180}, 0, 0)`
|
|
27
|
-
case "minutes":
|
|
28
|
-
return `rotate(${minutes * 6 - 180}, 0, 0)`
|
|
29
|
-
case "seconds":
|
|
30
|
-
return `rotate(${seconds * 6 - 180}, 0, 0)`
|
|
31
|
-
default:
|
|
32
|
-
return ""
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
|
|
36
37
|
return (
|
|
37
38
|
<div className={styles.clock} data-ui-component="Clock">
|
|
38
|
-
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256">
|
|
39
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" role="img" aria-labelledby={titleId}>
|
|
40
|
+
<title id={titleId}>Clock</title>
|
|
39
41
|
<g transform="translate(128,128)">
|
|
40
42
|
<circle r="124" fill="none" strokeWidth="8" className={styles.clock__border} />
|
|
41
43
|
<g transform="rotate(180)">
|
|
@@ -76,3 +78,5 @@ export const Clock: FC<ClockProps> = () => {
|
|
|
76
78
|
</div>
|
|
77
79
|
)
|
|
78
80
|
}
|
|
81
|
+
|
|
82
|
+
Clock.displayName = "Clock"
|
|
@@ -6,13 +6,20 @@ export const meta: HeliosAttributeMeta<DatePickerProps> = {
|
|
|
6
6
|
{
|
|
7
7
|
id: "ui-datepicker-default",
|
|
8
8
|
description: "default",
|
|
9
|
-
content:
|
|
9
|
+
content: '<DatePicker label="Date" />',
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
id: "ui-datepicker-with-value",
|
|
13
|
+
description: "with value",
|
|
14
|
+
content: '<DatePicker label="Birthday" value="2024-03-15" onChange={ON_CHANGE} />',
|
|
10
15
|
},
|
|
11
16
|
],
|
|
12
17
|
_status: "experimental",
|
|
13
18
|
_category: "core",
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
},
|
|
19
|
+
isDisabled: { type: "boolean", isOptional: true },
|
|
20
|
+
isLabelHidden: { type: "boolean", isOptional: true },
|
|
21
|
+
isRequired: { type: "boolean", isOptional: true },
|
|
22
|
+
label: { type: "string" },
|
|
23
|
+
onChange: { type: "(date: string) => void", isOptional: true, description: "Returns YYYY-MM-DD format" },
|
|
24
|
+
value: { type: "string", isOptional: true, description: "YYYY-MM-DD format" },
|
|
18
25
|
}
|
|
@@ -1,3 +1,72 @@
|
|
|
1
1
|
.datePicker {
|
|
2
|
-
|
|
2
|
+
display: flex;
|
|
3
|
+
flex-direction: column;
|
|
4
|
+
width: 280px;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
.datePicker__header {
|
|
8
|
+
display: flex;
|
|
9
|
+
align-items: center;
|
|
10
|
+
justify-content: space-between;
|
|
11
|
+
padding: 8px 4px;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
.datePicker__grid {
|
|
15
|
+
display: grid;
|
|
16
|
+
grid-template-columns: repeat(7, 1fr);
|
|
17
|
+
gap: 2px;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
.datePicker__dayLabel {
|
|
21
|
+
display: flex;
|
|
22
|
+
align-items: center;
|
|
23
|
+
justify-content: center;
|
|
24
|
+
height: 32px;
|
|
25
|
+
font-size: 11px;
|
|
26
|
+
font-weight: 500;
|
|
27
|
+
color: var(--ui-text-tertiary);
|
|
28
|
+
user-select: none;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
.datePicker__day {
|
|
32
|
+
display: flex;
|
|
33
|
+
align-items: center;
|
|
34
|
+
justify-content: center;
|
|
35
|
+
height: 32px;
|
|
36
|
+
border-radius: var(--radius-sm);
|
|
37
|
+
font-size: 13px;
|
|
38
|
+
cursor: pointer;
|
|
39
|
+
user-select: none;
|
|
40
|
+
color: var(--ui-text-primary);
|
|
41
|
+
border: none;
|
|
42
|
+
background: none;
|
|
43
|
+
padding: 0;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
.datePicker__day:hover {
|
|
47
|
+
background: var(--ui-bg-tertiary);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
.datePicker__dayToday {
|
|
51
|
+
font-weight: 600;
|
|
52
|
+
box-shadow: inset 0 0 0 1px var(--ui-border-secondary);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
.datePicker__daySelected {
|
|
56
|
+
background: var(--ui-text-primary);
|
|
57
|
+
color: var(--ui-bg-primary);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
.datePicker__daySelected:hover {
|
|
61
|
+
background: var(--ui-text-primary);
|
|
62
|
+
opacity: 0.9;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
.datePicker__dayEmpty {
|
|
66
|
+
pointer-events: none;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
.datePicker__dayDisabled {
|
|
70
|
+
opacity: 0.3;
|
|
71
|
+
pointer-events: none;
|
|
3
72
|
}
|
|
@@ -1,14 +1,126 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
1
3
|
import { getClasses } from "@heliosgraphics/utils"
|
|
4
|
+
import { useMemo, useState, type FC } from "react"
|
|
5
|
+
import { Button } from "../Button"
|
|
6
|
+
import { ButtonGroup } from "../ButtonGroup"
|
|
7
|
+
import { InputLabel } from "../shared/InputLabel"
|
|
8
|
+
import { Text } from "../Text"
|
|
9
|
+
import {
|
|
10
|
+
DAYS_OF_WEEK,
|
|
11
|
+
MONTH_NAMES,
|
|
12
|
+
getDaysInMonth,
|
|
13
|
+
getFirstDayOfMonth,
|
|
14
|
+
formatDate,
|
|
15
|
+
parseDate,
|
|
16
|
+
isToday,
|
|
17
|
+
} from "./DatePicker.utils"
|
|
2
18
|
import styles from "./DatePicker.module.css"
|
|
3
19
|
import type { DatePickerProps } from "./DatePicker.types"
|
|
4
|
-
import type { FC } from "react"
|
|
5
20
|
|
|
6
|
-
export const DatePicker: FC<DatePickerProps> = ({
|
|
7
|
-
const
|
|
21
|
+
export const DatePicker: FC<DatePickerProps> = ({ isDisabled, isLabelHidden, label, onChange, value }) => {
|
|
22
|
+
const parsed = value ? parseDate(value) : null
|
|
23
|
+
const now: Date = new Date()
|
|
24
|
+
|
|
25
|
+
const [viewYear, setViewYear] = useState<number>(parsed?.year ?? now.getFullYear())
|
|
26
|
+
const [viewMonth, setViewMonth] = useState<number>(parsed?.month ?? now.getMonth())
|
|
27
|
+
|
|
28
|
+
const daysInMonth: number = useMemo(() => getDaysInMonth(viewYear, viewMonth), [viewYear, viewMonth])
|
|
29
|
+
const firstDay: number = useMemo(() => getFirstDayOfMonth(viewYear, viewMonth), [viewYear, viewMonth])
|
|
30
|
+
|
|
31
|
+
const onPreviousMonth = (): void => {
|
|
32
|
+
if (viewMonth === 0) {
|
|
33
|
+
setViewMonth(11)
|
|
34
|
+
setViewYear(viewYear - 1)
|
|
35
|
+
} else {
|
|
36
|
+
setViewMonth(viewMonth - 1)
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const onNextMonth = (): void => {
|
|
41
|
+
if (viewMonth === 11) {
|
|
42
|
+
setViewMonth(0)
|
|
43
|
+
setViewYear(viewYear + 1)
|
|
44
|
+
} else {
|
|
45
|
+
setViewMonth(viewMonth + 1)
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const onSelectDay = (day: number): void => {
|
|
50
|
+
const formatted: string = formatDate(viewYear, viewMonth, day)
|
|
51
|
+
|
|
52
|
+
onChange?.(formatted)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const datePickerClasses: string = getClasses(styles.datePicker, {
|
|
56
|
+
[styles.datePicker__dayDisabled]: !!isDisabled,
|
|
57
|
+
})
|
|
8
58
|
|
|
9
59
|
return (
|
|
10
60
|
<div className={datePickerClasses} data-ui-component="DatePicker">
|
|
11
|
-
{
|
|
61
|
+
{!isLabelHidden && <InputLabel label={label} id="datepicker" isDisabled={!!isDisabled} />}
|
|
62
|
+
<div className={styles.datePicker__header}>
|
|
63
|
+
<ButtonGroup>
|
|
64
|
+
<Button
|
|
65
|
+
size="tiny"
|
|
66
|
+
icon="chevron-left"
|
|
67
|
+
isIconOnly={true}
|
|
68
|
+
onClick={onPreviousMonth}
|
|
69
|
+
isDisabled={!!isDisabled}
|
|
70
|
+
value=""
|
|
71
|
+
/>
|
|
72
|
+
</ButtonGroup>
|
|
73
|
+
<Text type="small" fontWeight="medium">
|
|
74
|
+
{MONTH_NAMES[viewMonth]} {viewYear}
|
|
75
|
+
</Text>
|
|
76
|
+
<ButtonGroup>
|
|
77
|
+
<Button
|
|
78
|
+
size="tiny"
|
|
79
|
+
icon="chevron-right"
|
|
80
|
+
isIconOnly={true}
|
|
81
|
+
onClick={onNextMonth}
|
|
82
|
+
isDisabled={!!isDisabled}
|
|
83
|
+
value=""
|
|
84
|
+
/>
|
|
85
|
+
</ButtonGroup>
|
|
86
|
+
</div>
|
|
87
|
+
<div className={styles.datePicker__grid}>
|
|
88
|
+
{DAYS_OF_WEEK.map((day: string) => (
|
|
89
|
+
<div key={day} className={styles.datePicker__dayLabel}>
|
|
90
|
+
{day}
|
|
91
|
+
</div>
|
|
92
|
+
))}
|
|
93
|
+
{Array.from({ length: firstDay }).map((_, i: number) => (
|
|
94
|
+
<div key={`empty-${i}`} className={styles.datePicker__dayEmpty} />
|
|
95
|
+
))}
|
|
96
|
+
{Array.from({ length: daysInMonth }).map((_, i: number) => {
|
|
97
|
+
const day: number = i + 1
|
|
98
|
+
const dateStr: string = formatDate(viewYear, viewMonth, day)
|
|
99
|
+
const isSelected: boolean = value === dateStr
|
|
100
|
+
const isTodayDate: boolean = isToday(viewYear, viewMonth, day)
|
|
101
|
+
|
|
102
|
+
const dayClasses: string = getClasses(styles.datePicker__day, {
|
|
103
|
+
[styles.datePicker__daySelected]: isSelected,
|
|
104
|
+
[styles.datePicker__dayToday]: isTodayDate && !isSelected,
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
return (
|
|
108
|
+
<button
|
|
109
|
+
key={day}
|
|
110
|
+
type="button"
|
|
111
|
+
className={dayClasses}
|
|
112
|
+
onClick={() => onSelectDay(day)}
|
|
113
|
+
disabled={isDisabled}
|
|
114
|
+
aria-label={`${MONTH_NAMES[viewMonth]} ${day}, ${viewYear}`}
|
|
115
|
+
aria-pressed={isSelected}
|
|
116
|
+
>
|
|
117
|
+
{day}
|
|
118
|
+
</button>
|
|
119
|
+
)
|
|
120
|
+
})}
|
|
121
|
+
</div>
|
|
12
122
|
</div>
|
|
13
123
|
)
|
|
14
124
|
}
|
|
125
|
+
|
|
126
|
+
DatePicker.displayName = "DatePicker"
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
export const DAYS_OF_WEEK: Array<string> = ["Mo", "Tu", "We", "Th", "Fr", "Sa", "Su"]
|
|
2
|
+
|
|
3
|
+
export const MONTH_NAMES: Array<string> = [
|
|
4
|
+
"January",
|
|
5
|
+
"February",
|
|
6
|
+
"March",
|
|
7
|
+
"April",
|
|
8
|
+
"May",
|
|
9
|
+
"June",
|
|
10
|
+
"July",
|
|
11
|
+
"August",
|
|
12
|
+
"September",
|
|
13
|
+
"October",
|
|
14
|
+
"November",
|
|
15
|
+
"December",
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
export const getDaysInMonth = (year: number, month: number): number => {
|
|
19
|
+
return new Date(year, month + 1, 0).getDate()
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export const getFirstDayOfMonth = (year: number, month: number): number => {
|
|
23
|
+
const day: number = new Date(year, month, 1).getDay()
|
|
24
|
+
|
|
25
|
+
return day === 0 ? 6 : day - 1
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export const formatDate = (year: number, month: number, day: number): string => {
|
|
29
|
+
const m: string = String(month + 1).padStart(2, "0")
|
|
30
|
+
const d: string = String(day).padStart(2, "0")
|
|
31
|
+
|
|
32
|
+
return `${year}-${m}-${d}`
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export const parseDate = (value: string): { year: number; month: number; day: number } | null => {
|
|
36
|
+
const parts: Array<string> = value.split("-")
|
|
37
|
+
|
|
38
|
+
if (parts.length !== 3) return null
|
|
39
|
+
|
|
40
|
+
const year: number = parseInt(parts[0] ?? "", 10)
|
|
41
|
+
const month: number = parseInt(parts[1] ?? "", 10) - 1
|
|
42
|
+
const day: number = parseInt(parts[2] ?? "", 10)
|
|
43
|
+
|
|
44
|
+
if (isNaN(year) || isNaN(month) || isNaN(day)) return null
|
|
45
|
+
|
|
46
|
+
return { year, month, day }
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export const isToday = (year: number, month: number, day: number): boolean => {
|
|
50
|
+
const now: Date = new Date()
|
|
51
|
+
|
|
52
|
+
return now.getFullYear() === year && now.getMonth() === month && now.getDate() === day
|
|
53
|
+
}
|
|
@@ -99,13 +99,11 @@
|
|
|
99
99
|
width: calc(100% - 16px);
|
|
100
100
|
}
|
|
101
101
|
|
|
102
|
-
.dialog__content {
|
|
103
|
-
padding: 12px;
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
@media (max-width: 576px) {
|
|
108
102
|
.dialogCentered {
|
|
109
103
|
width: calc(100% - 16px);
|
|
110
104
|
}
|
|
105
|
+
|
|
106
|
+
.dialog__content {
|
|
107
|
+
padding: 12px;
|
|
108
|
+
}
|
|
111
109
|
}
|