@heliosgraphics/ui 2.0.0-alpha.95 → 2.0.0-alpha.96

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.
Files changed (109) hide show
  1. package/components/Alert/Alert.tsx +2 -0
  2. package/components/Breadcrumb/Breadcrumb.tsx +17 -1
  3. package/components/Browser/Browser.tsx +2 -0
  4. package/components/Button/Button.tsx +7 -3
  5. package/components/ButtonGroup/ButtonGroup.tsx +2 -0
  6. package/components/Checkbox/Checkbox.tsx +66 -55
  7. package/components/Clock/Clock.tsx +23 -19
  8. package/components/Column/Column.tsx +2 -0
  9. package/components/Confirm/Confirm.tsx +2 -0
  10. package/components/DatePicker/DatePicker.meta.ts +12 -5
  11. package/components/DatePicker/DatePicker.module.css +70 -1
  12. package/components/DatePicker/DatePicker.tsx +116 -4
  13. package/components/DatePicker/DatePicker.types.ts +6 -1
  14. package/components/DatePicker/DatePicker.utils.ts +53 -0
  15. package/components/Debug/Debug.tsx +2 -0
  16. package/components/Details/Details.tsx +2 -0
  17. package/components/Dialog/Dialog.module.css +4 -6
  18. package/components/Dialog/Dialog.tsx +25 -16
  19. package/components/Donut/Donut.tsx +2 -0
  20. package/components/Dot/Dot.tsx +2 -0
  21. package/components/Dropdown/Dropdown.module.css +5 -0
  22. package/components/Dropdown/Dropdown.tsx +21 -26
  23. package/components/Fieldset/Fieldset.tsx +2 -0
  24. package/components/Flex/Flex.meta.ts +1 -0
  25. package/components/Flex/Flex.tsx +22 -2
  26. package/components/Flex/Flex.types.ts +1 -0
  27. package/components/Flex/Flex.utils.spec.ts +4 -1
  28. package/components/Flex/Flex.utils.ts +4 -1
  29. package/components/Grid/Grid.tsx +2 -0
  30. package/components/Heading/Heading.meta.ts +5 -0
  31. package/components/Heading/Heading.tsx +15 -9
  32. package/components/Heading/Heading.types.ts +1 -0
  33. package/components/Heading/components/H0/H0.tsx +2 -0
  34. package/components/Heading/components/H1/H1.tsx +2 -0
  35. package/components/Heading/components/H2/H2.tsx +2 -0
  36. package/components/Heading/components/H3/H3.tsx +2 -0
  37. package/components/Heading/components/H4/H4.tsx +2 -0
  38. package/components/Heading/components/H5/H5.tsx +2 -0
  39. package/components/Heading/components/H6/H6.tsx +2 -0
  40. package/components/Icon/Icon.tsx +2 -0
  41. package/components/Input/Input.tsx +103 -95
  42. package/components/Layout/Layout.tsx +2 -0
  43. package/components/Layout/components/LayoutAside/LayoutAside.tsx +2 -0
  44. package/components/Layout/components/LayoutAside/components/LayoutAsideContent/LayoutAsideContent.tsx +2 -0
  45. package/components/Layout/components/LayoutAside/components/LayoutAsideFooter/LayoutAsideFooter.tsx +2 -0
  46. package/components/Layout/components/LayoutAside/components/LayoutAsideToggle/LayoutAsideToggle.tsx +2 -0
  47. package/components/Layout/components/LayoutMain/LayoutMain.tsx +2 -0
  48. package/components/Layout/components/LayoutMain/components/LayoutMainContent/LayoutMainContent.tsx +2 -0
  49. package/components/Layout/components/LayoutNavigation/LayoutNavigation.tsx +2 -0
  50. package/components/Layout/components/LayoutSubNavigation/LayoutSubNavigation.tsx +2 -0
  51. package/components/Loading/Loading.tsx +2 -0
  52. package/components/Markdown/Markdown.tsx +2 -0
  53. package/components/Masonry/Masonry.tsx +5 -1
  54. package/components/Menu/Menu.tsx +2 -0
  55. package/components/Menu/components/MenuCategory/MenuCategory.tsx +2 -0
  56. package/components/Menu/components/MenuFilter/MenuFilter.tsx +2 -0
  57. package/components/Menu/components/MenuItem/MenuItem.tsx +2 -0
  58. package/components/Overlay/Overlay.tsx +18 -2
  59. package/components/Pie/Pie.tsx +2 -0
  60. package/components/Pill/Pill.meta.ts +9 -1
  61. package/components/Pill/Pill.module.css +11 -0
  62. package/components/Pill/Pill.tsx +28 -3
  63. package/components/Pill/Pill.types.ts +2 -0
  64. package/components/Placeholder/Placeholder.tsx +2 -0
  65. package/components/Progress/Progress.tsx +2 -0
  66. package/components/Radio/Radio.tsx +2 -0
  67. package/components/Range/Range.tsx +2 -0
  68. package/components/Segments/Segments.context.ts +19 -0
  69. package/components/Segments/Segments.meta.ts +4 -0
  70. package/components/Segments/Segments.tsx +34 -42
  71. package/components/Segments/Segments.types.ts +1 -0
  72. package/components/Segments/components/SegmentButton/SegmentButton.meta.ts +0 -4
  73. package/components/Segments/components/SegmentButton/SegmentButton.tsx +28 -3
  74. package/components/Segments/components/SegmentButton/SegmentButton.types.ts +0 -2
  75. package/components/Select/Select.tsx +40 -43
  76. package/components/Separator/Separator.tsx +2 -0
  77. package/components/Separator/components/HRMarkup/HRMarkup.tsx +2 -0
  78. package/components/Separator/components/HorizontalSeparator/HorizontalSeparator.tsx +2 -0
  79. package/components/Separator/components/VerticalSeparator/VerticalSeparator.tsx +2 -0
  80. package/components/Setup/Setup.tsx +3 -0
  81. package/components/Shimmer/Shimmer.tsx +2 -0
  82. package/components/Slider/Slider.tsx +2 -0
  83. package/components/Spacer/Spacer.tsx +2 -0
  84. package/components/Table/Table.tsx +2 -0
  85. package/components/Tabs/Tabs.meta.ts +12 -12
  86. package/components/Tabs/Tabs.module.css +25 -9
  87. package/components/Tabs/Tabs.tsx +49 -53
  88. package/components/Tabs/Tabs.types.ts +10 -3
  89. package/components/Text/Text.tsx +2 -0
  90. package/components/Text/components/Div/Div.tsx +2 -0
  91. package/components/Text/components/Micro/Micro.tsx +2 -0
  92. package/components/Text/components/P/P.tsx +2 -0
  93. package/components/Text/components/Small/Small.tsx +2 -0
  94. package/components/Text/components/Tiny/Tiny.tsx +2 -0
  95. package/components/Textarea/Textarea.tsx +14 -13
  96. package/components/Tile/Tile.tsx +2 -0
  97. package/components/Timestamp/Timestamp.tsx +2 -0
  98. package/components/Toggle/Toggle.tsx +2 -0
  99. package/components/Tooltip/Tooltip.tsx +17 -9
  100. package/components/Tooltip/Tooltip.types.ts +0 -1
  101. package/components/Tooltip/components/TooltipContent/TooltipContent.tsx +2 -0
  102. package/components/Tooltip/components/TooltipTrigger/TooltipTrigger.tsx +4 -2
  103. package/components/shared/InputLabel/InputLabel.tsx +2 -0
  104. package/components/shared/ResultList/ResultList.tsx +6 -4
  105. package/contexts/LayoutContext/LayoutContext.tsx +15 -34
  106. package/contexts/LayoutContext/LayoutContext.types.ts +0 -1
  107. package/hooks/useLayoutContext.tsx +0 -1
  108. package/index.ts +5 -0
  109. package/package.json +1 -1
@@ -50,3 +50,5 @@ export const Alert: FC<AlertProps> = ({ children, onClose, title, icon, color, a
50
50
  </Flex>
51
51
  )
52
52
  }
53
+
54
+ Alert.displayName = "Alert"
@@ -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"
@@ -12,3 +12,5 @@ export const Browser: FC<BrowserProps> = ({ children }) => {
12
12
  </div>
13
13
  )
14
14
  }
15
+
16
+ Browser.displayName = "Browser"
@@ -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 || !buttonId) return
134
+ if (isDisabled || isLoading) return
134
135
 
135
136
  setIsActive(true)
136
- document.getElementById(buttonId)?.click()
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"
@@ -37,3 +37,5 @@ export const ButtonGroup: FC<ButtonGroupProps> = ({
37
37
  </Flex>
38
38
  )
39
39
  }
40
+
41
+ ButtonGroup.displayName = "ButtonGroup"
@@ -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: FC<CheckboxProps> = ({
14
- color = "gray",
15
- isChecked,
16
- isVertical,
17
- isLabelHidden = false,
18
- isSmall,
19
- description,
20
- isDisabled,
21
- isRequired,
22
- onChange,
23
- label,
24
- }) => {
25
- const checkboxId: string = useId()
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
- const colorClasses = getColorClasses(color, "dark")
28
- const checkboxClasses = getClasses(styles.checkbox, ...colorClasses, {
29
- [styles.checkboxDisabled]: isDisabled,
30
- [styles.checkboxSmall]: isSmall,
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
- const checkboxLabelClasses = getClasses(styles.checkbox__checkboxLabel, "flex gap-4", {
34
- "flex-x-center flex-column": isVertical,
35
- "flex-y-center": !isVertical,
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
- return (
39
- <div className={checkboxClasses} data-ui-component="Checkbox">
40
- <label className={checkboxLabelClasses} htmlFor={checkboxId}>
41
- <span className={styles.checkbox__mark}>
42
- <input
43
- type="checkbox"
44
- checked={isChecked}
45
- onChange={onChange}
46
- disabled={isDisabled}
47
- required={isRequired}
48
- aria-label={isLabelHidden ? label : undefined}
49
- id={checkboxId}
50
- />
51
- <Icon icon="check" size={isSmall ? 14 : 18} className={styles.checkbox__checkboxIcon} />
52
- <div className={styles.checkbox__checkboxMark} />
53
- </span>
54
- {!isLabelHidden && (
55
- <Flex isColumn={true}>
56
- <Text type={isSmall ? "tiny" : "small"} fontWeight="medium" emphasis={isDisabled ? "tertiary" : "inherit"}>
57
- {label}
58
- </Text>
59
- {description && (
60
- <Text type="tiny" emphasis="secondary">
61
- {description}
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
- </Flex>
65
- )}
66
- </label>
67
- </div>
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"
@@ -17,3 +17,5 @@ export const Column: FC<ColumnProps> = (props) => {
17
17
 
18
18
  return <div {...safeProps} style={columnStyle} className={columnClasses} data-ui-component="Column" />
19
19
  }
20
+
21
+ Column.displayName = "Column"
@@ -36,3 +36,5 @@ export const Confirm: FC<ConfirmProps> = ({
36
36
  </Dialog>
37
37
  )
38
38
  }
39
+
40
+ Confirm.displayName = "Confirm"
@@ -6,13 +6,20 @@ export const meta: HeliosAttributeMeta<DatePickerProps> = {
6
6
  {
7
7
  id: "ui-datepicker-default",
8
8
  description: "default",
9
- content: `<DatePicker/>`,
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
- date: {
15
- type: "string",
16
- isOptional: true,
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
- border: 1px solid gold;
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> = ({ date }) => {
7
- const datePickerClasses: string = getClasses(styles.datePicker)
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
- {date}
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"
@@ -1,3 +1,8 @@
1
1
  export interface DatePickerProps {
2
- date?: string
2
+ isDisabled?: boolean
3
+ isLabelHidden?: boolean
4
+ isRequired?: boolean
5
+ label: string
6
+ onChange?: (date: string) => void
7
+ value?: string
3
8
  }
@@ -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
+ }
@@ -44,3 +44,5 @@ export const Debug: FC<DebugProps> = ({ color = "gray", appearance = "dark" }) =
44
44
  </Flex>
45
45
  )
46
46
  }
47
+
48
+ Debug.displayName = "Debug"
@@ -17,3 +17,5 @@ export const Details: FC<DetailsProps> = ({ children, label, isOpen }) => {
17
17
  </details>
18
18
  )
19
19
  }
20
+
21
+ Details.displayName = "Details"
@@ -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
  }