@kaizen/components 1.80.2 → 1.80.4

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 (74) hide show
  1. package/codemods/README.md +24 -0
  2. package/codemods/migrateV2NextToCurrent/index.ts +40 -0
  3. package/codemods/migrateV2NextToCurrent/migrateV2NextToCurrent.spec.ts +555 -0
  4. package/codemods/migrateV2NextToCurrent/migrateV2NextToCurrent.ts +104 -0
  5. package/codemods/renameV2ComponentImportsAndUsages/index.ts +30 -0
  6. package/codemods/renameV2ComponentImportsAndUsages/renameV2ComponentImportsAndUsages.spec.ts +390 -0
  7. package/codemods/renameV2ComponentImportsAndUsages/renameV2ComponentImportsAndUsages.ts +151 -0
  8. package/codemods/utils/createModulePathTransformer.spec.ts +209 -0
  9. package/codemods/utils/createModulePathTransformer.ts +59 -0
  10. package/codemods/utils/createRenameMapFromGroups.ts +31 -0
  11. package/codemods/utils/index.ts +3 -0
  12. package/codemods/utils/updateJsxElementTagName.spec.ts +129 -0
  13. package/codemods/utils/updateJsxElementTagName.ts +56 -0
  14. package/codemods/utils/updateKaioImports.spec.ts +82 -0
  15. package/codemods/utils/updateKaioImports.ts +16 -7
  16. package/dist/cjs/src/__alpha__/SingleSelect/SingleSelect.cjs +69 -16
  17. package/dist/cjs/src/__alpha__/SingleSelect/context/SingleSelectContext.cjs +13 -0
  18. package/dist/cjs/src/__alpha__/SingleSelect/subcomponents/Popover/Popover.cjs +54 -0
  19. package/dist/cjs/src/__alpha__/SingleSelect/{SingleSelect.module.css.cjs → subcomponents/Popover/Popover.module.css.cjs} +1 -1
  20. package/dist/cjs/src/__alpha__/SingleSelect/subcomponents/Popover/utils/usePopoverPositioning.cjs +94 -0
  21. package/dist/cjs/src/__alpha__/SingleSelect/subcomponents/Popover/utils/usePositioningStyles.cjs +69 -0
  22. package/dist/cjs/src/__alpha__/SingleSelect/subcomponents/Popover/utils/useSupportsAnchorPositioning.cjs +12 -0
  23. package/dist/cjs/src/__alpha__/SingleSelect/subcomponents/Trigger/Trigger.cjs +41 -5
  24. package/dist/esm/src/__alpha__/SingleSelect/SingleSelect.mjs +60 -10
  25. package/dist/esm/src/__alpha__/SingleSelect/context/SingleSelectContext.mjs +10 -0
  26. package/dist/esm/src/__alpha__/SingleSelect/subcomponents/Popover/Popover.mjs +49 -0
  27. package/dist/esm/src/__alpha__/SingleSelect/subcomponents/Popover/Popover.module.css.mjs +4 -0
  28. package/dist/esm/src/__alpha__/SingleSelect/subcomponents/Popover/utils/usePopoverPositioning.mjs +92 -0
  29. package/dist/esm/src/__alpha__/SingleSelect/subcomponents/Popover/utils/usePositioningStyles.mjs +67 -0
  30. package/dist/esm/src/__alpha__/SingleSelect/subcomponents/Popover/utils/useSupportsAnchorPositioning.mjs +10 -0
  31. package/dist/esm/src/__alpha__/SingleSelect/subcomponents/Trigger/Trigger.mjs +43 -7
  32. package/dist/styles.css +43 -21
  33. package/dist/types/__alpha__/SingleSelect/SingleSelect.d.ts +7 -9
  34. package/dist/types/__alpha__/SingleSelect/context/SingleSelectContext.d.ts +12 -0
  35. package/dist/types/__alpha__/SingleSelect/context/index.d.ts +1 -0
  36. package/dist/types/__alpha__/SingleSelect/subcomponents/List/List.d.ts +2 -1
  37. package/dist/types/__alpha__/SingleSelect/subcomponents/ListItem/ListItem.d.ts +2 -1
  38. package/dist/types/__alpha__/SingleSelect/subcomponents/ListSection/ListSection.d.ts +2 -1
  39. package/dist/types/__alpha__/SingleSelect/subcomponents/Popover/Popover.d.ts +6 -0
  40. package/dist/types/__alpha__/SingleSelect/subcomponents/Popover/index.d.ts +1 -0
  41. package/dist/types/__alpha__/SingleSelect/subcomponents/Popover/utils/index.d.ts +2 -0
  42. package/dist/types/__alpha__/SingleSelect/subcomponents/Popover/utils/usePopoverPositioning.d.ts +4 -0
  43. package/dist/types/__alpha__/SingleSelect/subcomponents/Popover/utils/usePositioningStyles.d.ts +4 -0
  44. package/dist/types/__alpha__/SingleSelect/subcomponents/Popover/utils/useSupportsAnchorPositioning.d.ts +1 -0
  45. package/dist/types/__alpha__/SingleSelect/subcomponents/Trigger/Trigger.d.ts +2 -1
  46. package/dist/types/__alpha__/SingleSelect/subcomponents/index.d.ts +1 -0
  47. package/dist/types/__alpha__/SingleSelect/types.d.ts +45 -0
  48. package/package.json +4 -4
  49. package/src/__alpha__/SingleSelect/SingleSelect.tsx +79 -14
  50. package/src/__alpha__/SingleSelect/_docs/SingleSelect.mdx +5 -2
  51. package/src/__alpha__/SingleSelect/_docs/SingleSelect.spec.stories.tsx +100 -0
  52. package/src/__alpha__/SingleSelect/_docs/SingleSelect.stickersheet.stories.tsx +4 -4
  53. package/src/__alpha__/SingleSelect/_docs/SingleSelect.stories.tsx +21 -2
  54. package/src/__alpha__/SingleSelect/context/SingleSelectContext.tsx +21 -0
  55. package/src/__alpha__/SingleSelect/context/index.ts +1 -0
  56. package/src/__alpha__/SingleSelect/subcomponents/List/List.module.css +0 -1
  57. package/src/__alpha__/SingleSelect/subcomponents/List/List.tsx +2 -1
  58. package/src/__alpha__/SingleSelect/subcomponents/ListItem/ListItem.module.css +7 -0
  59. package/src/__alpha__/SingleSelect/subcomponents/ListItem/ListItem.tsx +2 -1
  60. package/src/__alpha__/SingleSelect/subcomponents/ListSection/ListSection.tsx +3 -1
  61. package/src/__alpha__/SingleSelect/subcomponents/Popover/Popover.module.css +24 -0
  62. package/src/__alpha__/SingleSelect/subcomponents/Popover/Popover.tsx +54 -0
  63. package/src/__alpha__/SingleSelect/subcomponents/Popover/index.ts +1 -0
  64. package/src/__alpha__/SingleSelect/subcomponents/Popover/utils/index.ts +2 -0
  65. package/src/__alpha__/SingleSelect/subcomponents/Popover/utils/usePopoverPositioning.ts +108 -0
  66. package/src/__alpha__/SingleSelect/subcomponents/Popover/utils/usePositioningStyles.ts +75 -0
  67. package/src/__alpha__/SingleSelect/subcomponents/Popover/utils/useSupportsAnchorPositioning.ts +13 -0
  68. package/src/__alpha__/SingleSelect/subcomponents/Trigger/Trigger.module.css +1 -0
  69. package/src/__alpha__/SingleSelect/subcomponents/Trigger/Trigger.tsx +29 -7
  70. package/src/__alpha__/SingleSelect/subcomponents/index.ts +1 -0
  71. package/src/__alpha__/SingleSelect/types.ts +58 -0
  72. package/dist/esm/src/__alpha__/SingleSelect/SingleSelect.module.css.mjs +0 -4
  73. package/src/__alpha__/SingleSelect/SingleSelect.module.css +0 -9
  74. package/src/__alpha__/SingleSelect/SingleSelect.spec.tsx +0 -26
@@ -0,0 +1,100 @@
1
+ import React from 'react'
2
+ import { type Meta, type StoryObj } from '@storybook/react'
3
+ import { expect, screen, userEvent, waitFor } from '@storybook/test'
4
+ import { SingleSelect } from '../SingleSelect'
5
+ import { singleMockItems } from './mockData'
6
+
7
+ const meta = {
8
+ title: 'Components/SingleSelect/SingleSelect (alpha)',
9
+ component: SingleSelect,
10
+ parameters: {
11
+ layout: 'centered',
12
+ },
13
+ } satisfies Meta<typeof SingleSelect>
14
+
15
+ export default meta
16
+
17
+ type Story = StoryObj<typeof meta>
18
+
19
+ const args = {
20
+ items: singleMockItems,
21
+ children: (
22
+ <SingleSelect.List>
23
+ {singleMockItems.map((item) => (
24
+ <SingleSelect.ListItem key={item.value} id={item.value}>
25
+ {item.label}
26
+ </SingleSelect.ListItem>
27
+ ))}
28
+ </SingleSelect.List>
29
+ ),
30
+ }
31
+
32
+ export const RendersButton: Story = {
33
+ args,
34
+ play: async () => {
35
+ expect(screen.getByRole('button')).toBeInTheDocument()
36
+ },
37
+ }
38
+
39
+ export const OpensPopoverOnClick: Story = {
40
+ args,
41
+ play: async () => {
42
+ const trigger = screen.getByRole('button')
43
+ await userEvent.click(trigger)
44
+ await waitFor(() => expect(trigger).toHaveAttribute('aria-expanded', 'true'))
45
+ const options = await screen.findAllByRole('option')
46
+ expect(options[0]).toBeVisible()
47
+ expect(options[0]).toHaveTextContent(singleMockItems[0].label)
48
+ },
49
+ }
50
+
51
+ export const ClosesPopoverOnSelect: Story = {
52
+ args,
53
+ play: async () => {
54
+ const trigger = screen.getByRole('button')
55
+ await userEvent.click(trigger)
56
+ await waitFor(() => expect(trigger).toHaveAttribute('aria-expanded', 'true'))
57
+ const options = await screen.findAllByRole('option')
58
+ await userEvent.click(options[0])
59
+ await waitFor(() => expect(screen.queryAllByRole('option')).toHaveLength(0))
60
+ },
61
+ }
62
+
63
+ export const KeyboardNavigation: Story = {
64
+ args,
65
+ play: async () => {
66
+ const trigger = screen.getByRole('button')
67
+ trigger.focus()
68
+ await userEvent.keyboard('{Enter}')
69
+ await waitFor(() => expect(trigger).toHaveAttribute('aria-expanded', 'true'))
70
+ const options = await screen.findAllByRole('option')
71
+ await userEvent.keyboard('{ArrowDown}')
72
+ expect(options[1]).toHaveAttribute('data-focused', 'true')
73
+ await userEvent.keyboard('{ArrowUp}')
74
+ expect(options[0]).toHaveAttribute('data-focused', 'true')
75
+ },
76
+ }
77
+
78
+ export const KeyboardSelectsItem: Story = {
79
+ args,
80
+ play: async () => {
81
+ const trigger = screen.getByRole('button')
82
+ trigger.focus()
83
+ await userEvent.keyboard('{Enter}')
84
+ await waitFor(() => expect(trigger).toHaveAttribute('aria-expanded', 'true'))
85
+ await userEvent.keyboard('{ArrowDown}')
86
+ await userEvent.keyboard('{Enter}')
87
+ await waitFor(() => expect(screen.queryAllByRole('option')).toHaveLength(0))
88
+ },
89
+ }
90
+
91
+ export const KeyboardEscapeClosesPopover: Story = {
92
+ args,
93
+ play: async () => {
94
+ const trigger = screen.getByRole('button')
95
+ await userEvent.click(trigger)
96
+ await waitFor(() => expect(trigger).toHaveAttribute('aria-expanded', 'true'))
97
+ await userEvent.keyboard('{Escape}')
98
+ await waitFor(() => expect(trigger).toHaveAttribute('aria-expanded', 'false'))
99
+ },
100
+ }
@@ -17,22 +17,22 @@ const StickerSheetTemplate: StickerSheetStory = {
17
17
  return (
18
18
  <StickerSheet isReversed={isReversed} title="SingleSelect" headers={['Items', 'Grouped']}>
19
19
  <StickerSheet.Row>
20
- <SingleSelect>
20
+ <SingleSelect items={singleMockItems}>
21
21
  <SingleSelect.List>
22
22
  {singleMockItems.map((item) => (
23
- <SingleSelect.ListItem key={item.value} value={{ value: item.value }}>
23
+ <SingleSelect.ListItem key={item.value} id={item.value}>
24
24
  {item.label}
25
25
  </SingleSelect.ListItem>
26
26
  ))}
27
27
  </SingleSelect.List>
28
28
  </SingleSelect>
29
29
 
30
- <SingleSelect>
30
+ <SingleSelect items={groupedMockItems}>
31
31
  <SingleSelect.List>
32
32
  {groupedMockItems.map((section) => (
33
33
  <SingleSelect.ListSection name={section.label} key={section.label}>
34
34
  {section.options.map((item) => (
35
- <SingleSelect.ListItem key={item.value} value={{ value: item.value }}>
35
+ <SingleSelect.ListItem key={item.value} id={item.value}>
36
36
  {item.label}
37
37
  </SingleSelect.ListItem>
38
38
  ))}
@@ -1,10 +1,30 @@
1
+ import React from 'react'
1
2
  import { type Meta, type StoryObj } from '@storybook/react'
2
3
  import { SingleSelect } from '../index'
4
+ import { singleMockItems } from './mockData'
3
5
 
4
6
  const meta = {
5
7
  title: 'Components/SingleSelect/SingleSelect (alpha)',
6
8
  component: SingleSelect,
7
- args: {},
9
+ args: {
10
+ items: singleMockItems,
11
+ children: (
12
+ <SingleSelect.List>
13
+ {singleMockItems.map((item) => (
14
+ <SingleSelect.ListItem key={item.value} id={item.value}>
15
+ {item.label}
16
+ </SingleSelect.ListItem>
17
+ ))}
18
+ </SingleSelect.List>
19
+ ),
20
+ },
21
+ decorators: [
22
+ (Story) => (
23
+ <div className="h-200 justify-center items-center position-relative flex">
24
+ <Story />
25
+ </div>
26
+ ),
27
+ ],
8
28
  } satisfies Meta<typeof SingleSelect>
9
29
 
10
30
  export default meta
@@ -12,7 +32,6 @@ export default meta
12
32
  type Story = StoryObj<typeof meta>
13
33
 
14
34
  export const Playground: Story = {
15
- args: {},
16
35
  parameters: {
17
36
  docs: {
18
37
  canvas: {
@@ -0,0 +1,21 @@
1
+ import { createContext, useContext } from 'react'
2
+ import { type Key } from '@react-types/shared'
3
+ import { type SelectItem, type SelectSection } from '../types'
4
+
5
+ type SingleSelectContextType = {
6
+ isOpen: boolean
7
+ setOpen: (open: boolean) => void
8
+ selectedKey: Key | null
9
+ items: (SelectItem | SelectSection)[]
10
+ anchorName: string
11
+ }
12
+
13
+ export const SingleSelectContext = createContext<SingleSelectContextType | undefined>(undefined)
14
+
15
+ export const useSingleSelectContext = (): SingleSelectContextType => {
16
+ const context = useContext(SingleSelectContext)
17
+ if (!context) {
18
+ throw new Error('useSingleSelectContext must be used within a SingleSelectContext.Provider')
19
+ }
20
+ return context
21
+ }
@@ -0,0 +1 @@
1
+ export * from './SingleSelectContext'
@@ -2,6 +2,5 @@
2
2
  .list {
3
3
  display: flex;
4
4
  flex-direction: column;
5
- gap: var(--spacing-16);
6
5
  }
7
6
  }
@@ -1,13 +1,14 @@
1
1
  import React, { type PropsWithChildren } from 'react'
2
2
  import classNames from 'classnames'
3
3
  import { ListBox as RACListBox, type ListBoxProps } from 'react-aria-components'
4
+ import { type SelectItem, type SelectSection } from '../../types'
4
5
  import styles from './List.module.css'
5
6
 
6
7
  export const List = ({
7
8
  children,
8
9
  className,
9
10
  ...props
10
- }: ListBoxProps<object> & PropsWithChildren): React.ReactElement => {
11
+ }: ListBoxProps<SelectItem | SelectSection> & PropsWithChildren): React.ReactElement => {
11
12
  return (
12
13
  <RACListBox className={classNames(styles.list, className)} {...props}>
13
14
  {children}
@@ -5,5 +5,12 @@
5
5
  font-size: var(--typography-paragraph-body-font-size);
6
6
  line-height: var(--typography-paragraph-body-line-height);
7
7
  letter-spacing: var(--typography-paragraph-body-letter-spacing);
8
+ padding: var(--spacing-8) var(--spacing-16);
9
+ }
10
+
11
+ .listItem:focus-visible {
12
+ background-color: var(--color-blue-200);
13
+ outline: none;
14
+ border-color: white;
8
15
  }
9
16
  }
@@ -1,13 +1,14 @@
1
1
  import React, { type PropsWithChildren } from 'react'
2
2
  import classNames from 'classnames'
3
3
  import { ListBoxItem as RACListBoxItem, type ListBoxItemProps } from 'react-aria-components'
4
+ import { type SelectItem } from '../../types'
4
5
  import styles from './ListItem.module.css'
5
6
 
6
7
  export const ListItem = ({
7
8
  children,
8
9
  className,
9
10
  ...props
10
- }: ListBoxItemProps<object> & PropsWithChildren): React.ReactElement => {
11
+ }: ListBoxItemProps<SelectItem> & PropsWithChildren): React.ReactElement => {
11
12
  return (
12
13
  <RACListBoxItem className={classNames(styles.listItem, className)} {...props}>
13
14
  {children}
@@ -5,6 +5,7 @@ import {
5
5
  ListBoxSection as RACListBoxSection,
6
6
  type ListBoxSectionProps,
7
7
  } from 'react-aria-components'
8
+ import { type SelectSection } from '../../types'
8
9
  import styles from './ListSection.module.css'
9
10
 
10
11
  export const ListSection = ({
@@ -12,7 +13,8 @@ export const ListSection = ({
12
13
  className,
13
14
  children,
14
15
  ...props
15
- }: ListBoxSectionProps<object> & PropsWithChildren & { name: string }): React.ReactElement => {
16
+ }: ListBoxSectionProps<SelectSection> &
17
+ PropsWithChildren & { name: string }): React.ReactElement => {
16
18
  return (
17
19
  <RACListBoxSection {...props}>
18
20
  <RACHeader className={classNames(styles.listSectionHeader, className)}>{name}</RACHeader>
@@ -0,0 +1,24 @@
1
+ @layer kz-components {
2
+ .popover {
3
+ position: absolute;
4
+ height: auto;
5
+ background-color: var(--color-white);
6
+ border-radius: var(--spacing-8);
7
+ padding: 0;
8
+ box-shadow: var(--shadow-small-box-shadow);
9
+ overflow: hidden auto;
10
+ margin: 0;
11
+ box-sizing: border-box;
12
+
13
+ /* TODO: update width based on design */
14
+ width: 200px;
15
+
16
+ @supports (anchor-name: --anchor) {
17
+ position-anchor: var(--position-anchor);
18
+ margin-block: var(--spacing-4);
19
+ position-area: var(--position-area) center;
20
+ /* stylelint-disable-next-line declaration-property-value-no-unknown */
21
+ width: anchor-size(width);
22
+ }
23
+ }
24
+ }
@@ -0,0 +1,54 @@
1
+ import React, { useLayoutEffect, useMemo, type PropsWithChildren } from 'react'
2
+
3
+ import { Popover as RACPopover } from 'react-aria-components'
4
+ import { useSingleSelectContext } from '../../context'
5
+ import { type PopoverProps } from '../../types'
6
+ import { usePositioningStyles } from './utils/usePositioningStyles'
7
+ import styles from './Popover.module.css'
8
+
9
+ export const Popover = ({
10
+ buttonRef,
11
+ popoverRef,
12
+ racPopoverRef,
13
+ children,
14
+ }: PopoverProps & PropsWithChildren): React.ReactElement => {
15
+ const { isOpen, setOpen, anchorName } = useSingleSelectContext()
16
+
17
+ const { popoverStyle, isPositioned } = usePositioningStyles(buttonRef, popoverRef, anchorName)
18
+
19
+ const shouldShowPopover = useMemo(() => isOpen && isPositioned, [isOpen, isPositioned])
20
+
21
+ useLayoutEffect(() => {
22
+ const popover = popoverRef.current
23
+ if (!popover?.showPopover || !popover?.hidePopover) return
24
+
25
+ if (shouldShowPopover) {
26
+ popover.showPopover()
27
+ } else {
28
+ popover.hidePopover()
29
+ }
30
+ // eslint-disable-next-line react-hooks/exhaustive-deps
31
+ }, [shouldShowPopover])
32
+
33
+ return (
34
+ <RACPopover
35
+ shouldUpdatePosition={false}
36
+ trigger="manual"
37
+ isOpen={isOpen}
38
+ onOpenChange={setOpen}
39
+ ref={racPopoverRef}
40
+ >
41
+ <div
42
+ // @ts-expect-error - popover attribute is not included in current ts version, ignore type error
43
+ popover="manual"
44
+ ref={popoverRef}
45
+ className={styles.popover}
46
+ style={popoverStyle}
47
+ >
48
+ {children}
49
+ </div>
50
+ </RACPopover>
51
+ )
52
+ }
53
+
54
+ Popover.displayName = 'SingleSelect.Popover'
@@ -0,0 +1 @@
1
+ export * from './Popover'
@@ -0,0 +1,2 @@
1
+ export * from './usePopoverPositioning'
2
+ export * from './useSupportsAnchorPositioning'
@@ -0,0 +1,108 @@
1
+ import { useCallback, useEffect, useRef, useState } from 'react'
2
+ import {
3
+ type LogicalPosition,
4
+ type Position,
5
+ type UsePopoverPositioningProps,
6
+ } from '../../../types'
7
+
8
+ export function usePopoverPositioning({
9
+ triggerRef,
10
+ popoverRef,
11
+ direction = 'ltr',
12
+ offset = 4,
13
+ preferredPlacement = 'bottom',
14
+ }: UsePopoverPositioningProps): Position & { isPositioned: boolean } {
15
+ const [position, setPosition] = useState<Position>({
16
+ top: preferredPlacement === 'bottom' ? offset : 'auto',
17
+ bottom: preferredPlacement === 'top' ? offset : 'auto',
18
+ insetInlineStart: 0,
19
+ maxHeight: 300, // TODO: update this based on designs
20
+ })
21
+
22
+ const [isPositioned, setIsPositioned] = useState(true)
23
+
24
+ const mountedRef = useRef<boolean>(false)
25
+ const isSSR = typeof window === 'undefined'
26
+
27
+ const updatePosition = useCallback(() => {
28
+ if (isSSR) return
29
+
30
+ const trigger = triggerRef.current
31
+ const popover = popoverRef.current
32
+
33
+ if (!mountedRef.current || !trigger || !popover?.isConnected) {
34
+ return
35
+ }
36
+
37
+ const triggerRect = trigger.getBoundingClientRect()
38
+ if (!triggerRect) return
39
+
40
+ const doc = trigger.ownerDocument
41
+ const win = doc?.defaultView ?? window
42
+ const isRTL = direction === 'rtl'
43
+
44
+ const inlineStart = isRTL ? win.innerWidth - triggerRect.right : triggerRect.left
45
+
46
+ const triggerTop = triggerRect.top
47
+ const triggerBottom = triggerRect.bottom
48
+ const viewportHeight = win.innerHeight
49
+
50
+ const spaceAbove = triggerTop
51
+ const spaceBelow = viewportHeight - triggerBottom
52
+
53
+ const shouldFlip =
54
+ preferredPlacement === 'bottom' && spaceBelow < 200 && spaceAbove > spaceBelow
55
+
56
+ let top: LogicalPosition
57
+ let bottom: LogicalPosition
58
+ let maxHeight: number | undefined
59
+
60
+ if (shouldFlip) {
61
+ top = 'auto'
62
+ bottom = viewportHeight - triggerTop + offset
63
+ maxHeight = Math.max(0, spaceAbove - offset)
64
+ } else {
65
+ top = triggerBottom + offset
66
+ bottom = 'auto'
67
+ maxHeight = Math.max(0, spaceBelow - offset)
68
+ }
69
+
70
+ const newPosition = {
71
+ top,
72
+ bottom,
73
+ insetInlineStart: inlineStart,
74
+ maxHeight,
75
+ }
76
+
77
+ setPosition(newPosition)
78
+ setIsPositioned(true)
79
+ }, [triggerRef, popoverRef, direction, offset, preferredPlacement, isSSR])
80
+
81
+ useEffect(() => {
82
+ if (typeof window === 'undefined') return
83
+
84
+ mountedRef.current = true
85
+
86
+ const triggerEl = triggerRef.current
87
+
88
+ updatePosition()
89
+
90
+ const resizeObserver = new ResizeObserver(() => {
91
+ updatePosition()
92
+ })
93
+
94
+ if (triggerEl) resizeObserver.observe(triggerEl)
95
+
96
+ const onWindowResize = (): void => updatePosition()
97
+ window.addEventListener('resize', onWindowResize, { passive: true })
98
+
99
+ return () => {
100
+ mountedRef.current = false
101
+ resizeObserver.disconnect()
102
+ window.removeEventListener('resize', onWindowResize)
103
+ setIsPositioned(false)
104
+ }
105
+ }, [updatePosition, triggerRef])
106
+
107
+ return { ...position, isPositioned }
108
+ }
@@ -0,0 +1,75 @@
1
+ import { useMemo } from 'react'
2
+ import { useLocale } from '@react-aria/i18n'
3
+ import { type PositionData } from '../../../types'
4
+ import { usePopoverPositioning } from './usePopoverPositioning'
5
+ import { useSupportsAnchorPositioning } from './useSupportsAnchorPositioning'
6
+
7
+ const CSS_PROPS = {
8
+ POSITION_ANCHOR: '--position-anchor',
9
+ POSITION_AREA: '--position-area',
10
+ } as const
11
+
12
+ const DEFAULTS = {
13
+ MAX_HEIGHT: '300px',
14
+ } as const
15
+
16
+ /**
17
+ * Generates manual positioning styles for browsers without anchor positioning support or SSR
18
+ */
19
+ const getManualPositioningStyles = (positionData: PositionData): React.CSSProperties => ({
20
+ top: positionData.top,
21
+ bottom: positionData.bottom,
22
+ insetInlineStart: positionData.insetInlineStart,
23
+ maxHeight: positionData.maxHeight,
24
+ left: 'auto',
25
+ right: 'auto',
26
+ position: 'fixed',
27
+ })
28
+
29
+ const getAnchorPositioningStyles = (
30
+ anchorName: string,
31
+ positionData: PositionData,
32
+ ): React.CSSProperties => {
33
+ const styles: React.CSSProperties = {
34
+ maxHeight: positionData.maxHeight ?? DEFAULTS.MAX_HEIGHT,
35
+ [CSS_PROPS.POSITION_ANCHOR]: anchorName,
36
+ [CSS_PROPS.POSITION_AREA]: positionData.top === 'auto' ? 'top' : 'bottom',
37
+ }
38
+ return styles
39
+ }
40
+
41
+ export const usePositioningStyles = (
42
+ buttonRef: React.RefObject<HTMLElement>,
43
+ popoverRef: React.RefObject<HTMLDivElement>,
44
+ anchorName: string,
45
+ ): { popoverStyle: React.CSSProperties; isPositioned: boolean } => {
46
+ const { direction } = useLocale()
47
+ const hasAnchorSupport = useSupportsAnchorPositioning()
48
+
49
+ const { top, bottom, insetInlineStart, maxHeight, isPositioned } = usePopoverPositioning({
50
+ triggerRef: buttonRef,
51
+ popoverRef,
52
+ direction,
53
+ preferredPlacement: 'bottom',
54
+ })
55
+
56
+ const positionData = useMemo(
57
+ () => ({
58
+ top,
59
+ bottom,
60
+ insetInlineStart,
61
+ maxHeight,
62
+ }),
63
+ [top, bottom, insetInlineStart, maxHeight],
64
+ )
65
+
66
+ const popoverStyle = useMemo(() => {
67
+ if (hasAnchorSupport === null || !hasAnchorSupport) {
68
+ return getManualPositioningStyles(positionData)
69
+ }
70
+
71
+ return getAnchorPositioningStyles(anchorName, positionData)
72
+ }, [hasAnchorSupport, anchorName, positionData])
73
+
74
+ return { popoverStyle, isPositioned }
75
+ }
@@ -0,0 +1,13 @@
1
+ import { useMemo } from 'react'
2
+
3
+ export const useSupportsAnchorPositioning = (): boolean => {
4
+ return useMemo(() => {
5
+ if (typeof window === 'undefined' || typeof CSS === 'undefined') {
6
+ return false
7
+ }
8
+
9
+ return (
10
+ CSS.supports('position-anchor', 'auto') || CSS.supports('position-try-fallbacks: flip-block')
11
+ )
12
+ }, [])
13
+ }
@@ -1,5 +1,6 @@
1
1
  @layer kz-components {
2
2
  .button {
3
+ anchor-name: var(--anchor-name);
3
4
  display: flex;
4
5
  align-items: center;
5
6
  justify-content: space-between;
@@ -1,13 +1,35 @@
1
- import React from 'react'
2
- import { Button as RACButton, SelectValue } from 'react-aria-components'
1
+ import React, { useMemo } from 'react'
2
+ import { Button as RACButton } from 'react-aria-components'
3
3
  import { Icon } from '~components/__next__/Icon'
4
+ import { useSingleSelectContext } from '../../context'
5
+ import { type SelectItem, type SelectSection, type TriggerProps } from '../../types'
4
6
  import styles from './Trigger.module.css'
5
7
 
6
- export const Trigger = (): JSX.Element => {
8
+ function flattenItems(items: (SelectItem | SelectSection)[]): SelectItem[] {
9
+ return items.flatMap((item) => ('options' in item ? item.options : item))
10
+ }
11
+
12
+ export const Trigger = ({ buttonRef }: TriggerProps): JSX.Element => {
13
+ const { isOpen, setOpen, selectedKey, items, anchorName } = useSingleSelectContext()
14
+ const flattenedItems = useMemo(() => flattenItems(items), [items])
15
+ const selectedLabel = useMemo(() => {
16
+ const key = selectedKey
17
+ const item = flattenedItems.find((i) => i.value === key)
18
+ return item?.label ?? <div></div>
19
+ }, [flattenedItems, selectedKey])
20
+
7
21
  return (
8
- <RACButton className={styles.button}>
9
- <SelectValue />
10
- <Icon name="keyboard_arrow_down" isPresentational />
11
- </RACButton>
22
+ <div style={{ position: 'relative' }}>
23
+ <RACButton
24
+ className={styles.button}
25
+ ref={buttonRef}
26
+ onPress={() => setOpen(!isOpen)}
27
+ aria-expanded={isOpen}
28
+ style={{ '--anchor-name': anchorName } as React.CSSProperties}
29
+ >
30
+ {selectedLabel}
31
+ <Icon name="keyboard_arrow_down" isPresentational />
32
+ </RACButton>
33
+ </div>
12
34
  )
13
35
  }
@@ -2,3 +2,4 @@ export * from './List'
2
2
  export * from './ListSection'
3
3
  export * from './ListItem'
4
4
  export * from './Trigger'
5
+ export * from './Popover'
@@ -0,0 +1,58 @@
1
+ import { type RefObject } from 'react'
2
+ import { type Key } from '@react-types/shared'
3
+
4
+ // Shared types
5
+ export type SelectItem = {
6
+ label: string
7
+ value: string
8
+ }
9
+
10
+ export type SelectSection = {
11
+ label: string
12
+ options: SelectItem[]
13
+ }
14
+
15
+ // SingleSelect related types
16
+ export type SingleSelectProps = {
17
+ children?: React.ReactNode
18
+ items: (SelectItem | SelectSection)[]
19
+ onSelectionChange?: (key: Key | null) => void
20
+ }
21
+
22
+ // Trigger related types
23
+ export type TriggerProps = {
24
+ buttonRef: React.RefObject<HTMLButtonElement>
25
+ }
26
+
27
+ // Popover related types
28
+ export type PopoverProps = {
29
+ buttonRef: React.RefObject<HTMLElement>
30
+ popoverRef: React.RefObject<HTMLDivElement>
31
+ racPopoverRef: React.Ref<any>
32
+ }
33
+
34
+ type PositionDataProp = number | string | undefined
35
+
36
+ export type PositionData = {
37
+ top: PositionDataProp
38
+ bottom: PositionDataProp
39
+ insetInlineStart: PositionDataProp
40
+ maxHeight: PositionDataProp
41
+ }
42
+
43
+ export type LogicalPosition = number | 'auto' | undefined
44
+
45
+ export type Position = {
46
+ top: LogicalPosition
47
+ bottom: LogicalPosition
48
+ insetInlineStart: number
49
+ maxHeight?: number
50
+ }
51
+
52
+ export type UsePopoverPositioningProps = {
53
+ triggerRef: RefObject<HTMLElement>
54
+ popoverRef: RefObject<HTMLElement>
55
+ direction?: 'ltr' | 'rtl'
56
+ offset?: number
57
+ preferredPlacement?: 'top' | 'bottom'
58
+ }
@@ -1,4 +0,0 @@
1
- var styles = {
2
- "popover": "SingleSelect-module_popover__ZjL9n"
3
- };
4
- export { styles as default };