@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.
- package/codemods/README.md +24 -0
- package/codemods/migrateV2NextToCurrent/index.ts +40 -0
- package/codemods/migrateV2NextToCurrent/migrateV2NextToCurrent.spec.ts +555 -0
- package/codemods/migrateV2NextToCurrent/migrateV2NextToCurrent.ts +104 -0
- package/codemods/renameV2ComponentImportsAndUsages/index.ts +30 -0
- package/codemods/renameV2ComponentImportsAndUsages/renameV2ComponentImportsAndUsages.spec.ts +390 -0
- package/codemods/renameV2ComponentImportsAndUsages/renameV2ComponentImportsAndUsages.ts +151 -0
- package/codemods/utils/createModulePathTransformer.spec.ts +209 -0
- package/codemods/utils/createModulePathTransformer.ts +59 -0
- package/codemods/utils/createRenameMapFromGroups.ts +31 -0
- package/codemods/utils/index.ts +3 -0
- package/codemods/utils/updateJsxElementTagName.spec.ts +129 -0
- package/codemods/utils/updateJsxElementTagName.ts +56 -0
- package/codemods/utils/updateKaioImports.spec.ts +82 -0
- package/codemods/utils/updateKaioImports.ts +16 -7
- package/dist/cjs/src/__alpha__/SingleSelect/SingleSelect.cjs +69 -16
- package/dist/cjs/src/__alpha__/SingleSelect/context/SingleSelectContext.cjs +13 -0
- package/dist/cjs/src/__alpha__/SingleSelect/subcomponents/Popover/Popover.cjs +54 -0
- package/dist/cjs/src/__alpha__/SingleSelect/{SingleSelect.module.css.cjs → subcomponents/Popover/Popover.module.css.cjs} +1 -1
- package/dist/cjs/src/__alpha__/SingleSelect/subcomponents/Popover/utils/usePopoverPositioning.cjs +94 -0
- package/dist/cjs/src/__alpha__/SingleSelect/subcomponents/Popover/utils/usePositioningStyles.cjs +69 -0
- package/dist/cjs/src/__alpha__/SingleSelect/subcomponents/Popover/utils/useSupportsAnchorPositioning.cjs +12 -0
- package/dist/cjs/src/__alpha__/SingleSelect/subcomponents/Trigger/Trigger.cjs +41 -5
- package/dist/esm/src/__alpha__/SingleSelect/SingleSelect.mjs +60 -10
- package/dist/esm/src/__alpha__/SingleSelect/context/SingleSelectContext.mjs +10 -0
- package/dist/esm/src/__alpha__/SingleSelect/subcomponents/Popover/Popover.mjs +49 -0
- package/dist/esm/src/__alpha__/SingleSelect/subcomponents/Popover/Popover.module.css.mjs +4 -0
- package/dist/esm/src/__alpha__/SingleSelect/subcomponents/Popover/utils/usePopoverPositioning.mjs +92 -0
- package/dist/esm/src/__alpha__/SingleSelect/subcomponents/Popover/utils/usePositioningStyles.mjs +67 -0
- package/dist/esm/src/__alpha__/SingleSelect/subcomponents/Popover/utils/useSupportsAnchorPositioning.mjs +10 -0
- package/dist/esm/src/__alpha__/SingleSelect/subcomponents/Trigger/Trigger.mjs +43 -7
- package/dist/styles.css +43 -21
- package/dist/types/__alpha__/SingleSelect/SingleSelect.d.ts +7 -9
- package/dist/types/__alpha__/SingleSelect/context/SingleSelectContext.d.ts +12 -0
- package/dist/types/__alpha__/SingleSelect/context/index.d.ts +1 -0
- package/dist/types/__alpha__/SingleSelect/subcomponents/List/List.d.ts +2 -1
- package/dist/types/__alpha__/SingleSelect/subcomponents/ListItem/ListItem.d.ts +2 -1
- package/dist/types/__alpha__/SingleSelect/subcomponents/ListSection/ListSection.d.ts +2 -1
- package/dist/types/__alpha__/SingleSelect/subcomponents/Popover/Popover.d.ts +6 -0
- package/dist/types/__alpha__/SingleSelect/subcomponents/Popover/index.d.ts +1 -0
- package/dist/types/__alpha__/SingleSelect/subcomponents/Popover/utils/index.d.ts +2 -0
- package/dist/types/__alpha__/SingleSelect/subcomponents/Popover/utils/usePopoverPositioning.d.ts +4 -0
- package/dist/types/__alpha__/SingleSelect/subcomponents/Popover/utils/usePositioningStyles.d.ts +4 -0
- package/dist/types/__alpha__/SingleSelect/subcomponents/Popover/utils/useSupportsAnchorPositioning.d.ts +1 -0
- package/dist/types/__alpha__/SingleSelect/subcomponents/Trigger/Trigger.d.ts +2 -1
- package/dist/types/__alpha__/SingleSelect/subcomponents/index.d.ts +1 -0
- package/dist/types/__alpha__/SingleSelect/types.d.ts +45 -0
- package/package.json +4 -4
- package/src/__alpha__/SingleSelect/SingleSelect.tsx +79 -14
- package/src/__alpha__/SingleSelect/_docs/SingleSelect.mdx +5 -2
- package/src/__alpha__/SingleSelect/_docs/SingleSelect.spec.stories.tsx +100 -0
- package/src/__alpha__/SingleSelect/_docs/SingleSelect.stickersheet.stories.tsx +4 -4
- package/src/__alpha__/SingleSelect/_docs/SingleSelect.stories.tsx +21 -2
- package/src/__alpha__/SingleSelect/context/SingleSelectContext.tsx +21 -0
- package/src/__alpha__/SingleSelect/context/index.ts +1 -0
- package/src/__alpha__/SingleSelect/subcomponents/List/List.module.css +0 -1
- package/src/__alpha__/SingleSelect/subcomponents/List/List.tsx +2 -1
- package/src/__alpha__/SingleSelect/subcomponents/ListItem/ListItem.module.css +7 -0
- package/src/__alpha__/SingleSelect/subcomponents/ListItem/ListItem.tsx +2 -1
- package/src/__alpha__/SingleSelect/subcomponents/ListSection/ListSection.tsx +3 -1
- package/src/__alpha__/SingleSelect/subcomponents/Popover/Popover.module.css +24 -0
- package/src/__alpha__/SingleSelect/subcomponents/Popover/Popover.tsx +54 -0
- package/src/__alpha__/SingleSelect/subcomponents/Popover/index.ts +1 -0
- package/src/__alpha__/SingleSelect/subcomponents/Popover/utils/index.ts +2 -0
- package/src/__alpha__/SingleSelect/subcomponents/Popover/utils/usePopoverPositioning.ts +108 -0
- package/src/__alpha__/SingleSelect/subcomponents/Popover/utils/usePositioningStyles.ts +75 -0
- package/src/__alpha__/SingleSelect/subcomponents/Popover/utils/useSupportsAnchorPositioning.ts +13 -0
- package/src/__alpha__/SingleSelect/subcomponents/Trigger/Trigger.module.css +1 -0
- package/src/__alpha__/SingleSelect/subcomponents/Trigger/Trigger.tsx +29 -7
- package/src/__alpha__/SingleSelect/subcomponents/index.ts +1 -0
- package/src/__alpha__/SingleSelect/types.ts +58 -0
- package/dist/esm/src/__alpha__/SingleSelect/SingleSelect.module.css.mjs +0 -4
- package/src/__alpha__/SingleSelect/SingleSelect.module.css +0 -9
- 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}
|
|
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}
|
|
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'
|
|
@@ -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<
|
|
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<
|
|
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<
|
|
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,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
|
+
}
|
package/src/__alpha__/SingleSelect/subcomponents/Popover/utils/useSupportsAnchorPositioning.ts
ADDED
|
@@ -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,13 +1,35 @@
|
|
|
1
|
-
import React from 'react'
|
|
2
|
-
import { Button as RACButton
|
|
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
|
-
|
|
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
|
-
<
|
|
9
|
-
<
|
|
10
|
-
|
|
11
|
-
|
|
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
|
}
|
|
@@ -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
|
+
}
|