@kaizen/components 0.0.0-canary-fix-filter-select-jump-issue-useHasStableYPosition-20250120225815 → 0.0.0-canary-fix-filter-select-jump-issue-useHasStableYPosition-test-20250129223936
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/dist/cjs/__rc__/Select/hooks/{useHasStableYPosition.cjs → useHasCalculatedListboxPosition.cjs} +15 -9
- package/dist/cjs/__rc__/Select/subcomponents/ListBox/ListBox.cjs +22 -9
- package/dist/esm/__rc__/Select/hooks/{useHasStableYPosition.mjs → useHasCalculatedListboxPosition.mjs} +15 -9
- package/dist/esm/__rc__/Select/subcomponents/ListBox/ListBox.mjs +22 -9
- package/dist/styles.css +8759 -8758
- package/dist/types/__rc__/Select/hooks/{useHasStableYPosition.d.ts → useHasCalculatedListboxPosition.d.ts} +1 -1
- package/package.json +1 -1
- package/src/Content/Content.module.scss +3 -2
- package/src/Filter/FilterDateRangePicker/_docs/FilterDateRangePicker.mdx +1 -1
- package/src/Filter/FilterMultiSelect/_docs/FilterMultiSelect.mdx +1 -1
- package/src/Filter/FilterSelect/_docs/FilterSelect.stories.tsx +24 -27
- package/src/__rc__/Select/Select.spec.tsx +14 -4
- package/src/__rc__/Select/hooks/{useHasStableYPosition.ts → useHasCalculatedListboxPosition.ts} +13 -7
- package/src/__rc__/Select/subcomponents/ListBox/ListBox.tsx +31 -10
|
@@ -2,4 +2,4 @@
|
|
|
2
2
|
* Due to the floating element's position starting as a negative value on render and then jumping to the correct position, this caused the focus to jump to the top of the page.
|
|
3
3
|
* This now polls to check if the element's position is stable by comparing the first and last position.
|
|
4
4
|
*/
|
|
5
|
-
export declare const
|
|
5
|
+
export declare const useHasCalculatedListboxPosition: (ref: React.RefObject<HTMLElement>) => boolean;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kaizen/components",
|
|
3
|
-
"version": "0.0.0-canary-fix-filter-select-jump-issue-useHasStableYPosition-
|
|
3
|
+
"version": "0.0.0-canary-fix-filter-select-jump-issue-useHasStableYPosition-test-20250129223936",
|
|
4
4
|
"description": "Kaizen component library",
|
|
5
5
|
"author": "Geoffrey Chong <geoff.chong@cultureamp.com>",
|
|
6
6
|
"homepage": "https://cultureamp.design",
|
|
@@ -3,10 +3,11 @@
|
|
|
3
3
|
|
|
4
4
|
.content {
|
|
5
5
|
max-width: $layout-content-max-width;
|
|
6
|
-
|
|
6
|
+
margin: 0 $layout-content-side-margin;
|
|
7
7
|
width: 100%;
|
|
8
8
|
|
|
9
9
|
@media (max-width: calc(#{$layout-breakpoints-large} - 1px)) {
|
|
10
|
-
|
|
10
|
+
margin: 0 $content-margin-width-on-medium-and-small;
|
|
11
|
+
width: calc(100% - 2 * #{$content-margin-width-on-medium-and-small});
|
|
11
12
|
}
|
|
12
13
|
}
|
|
@@ -4,7 +4,7 @@ import * as FilterDRPStories from './FilterDateRangePicker.stories'
|
|
|
4
4
|
|
|
5
5
|
<Meta of={FilterDRPStories} />
|
|
6
6
|
|
|
7
|
-
#
|
|
7
|
+
# FilterDateRangePicker
|
|
8
8
|
|
|
9
9
|
<ResourceLinks
|
|
10
10
|
sourceCode="https://github.com/cultureamp/kaizen-design-system/tree/main/packages/components/src/FilterDateRangePicker"
|
|
@@ -4,7 +4,7 @@ import * as FilterMultiSelectStories from './FilterMultiSelect.stories'
|
|
|
4
4
|
|
|
5
5
|
<Meta of={FilterMultiSelectStories} />
|
|
6
6
|
|
|
7
|
-
#
|
|
7
|
+
# FilterMultiSelect
|
|
8
8
|
|
|
9
9
|
<ResourceLinks
|
|
10
10
|
sourceCode="https://github.com/cultureamp/kaizen-design-system/tree/main/packages/components/src/FilterMultiSelect"
|
|
@@ -2,6 +2,7 @@ import React, { useState } from 'react'
|
|
|
2
2
|
import { type Meta, type StoryObj } from '@storybook/react'
|
|
3
3
|
import { fn } from '@storybook/test'
|
|
4
4
|
import { renderTriggerControls } from '~components/Filter/_docs/controls/renderTriggerControls'
|
|
5
|
+
import { Well } from '~components/Well'
|
|
5
6
|
import { FilterButton } from '../../FilterButton'
|
|
6
7
|
import { FilterSelect } from '../FilterSelect'
|
|
7
8
|
import { type SelectOption } from '../types'
|
|
@@ -104,44 +105,40 @@ export const AdditionalProperties: Story = {
|
|
|
104
105
|
/**
|
|
105
106
|
* Extend the option type to have additional properties to use for rendering.
|
|
106
107
|
*/
|
|
107
|
-
export const
|
|
108
|
-
render: (
|
|
108
|
+
export const FilterSelectBelowPageContent: Story = {
|
|
109
|
+
render: () => {
|
|
109
110
|
const [isOpen, setIsOpen] = useState<boolean>(false)
|
|
110
111
|
|
|
111
112
|
return (
|
|
112
113
|
<div>
|
|
113
|
-
<
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
114
|
+
<Well color="gray" style={{ height: '1500px' }}>
|
|
115
|
+
Page content above the FilterSelect
|
|
116
|
+
</Well>
|
|
117
|
+
<FilterSelect
|
|
118
|
+
label="Label"
|
|
117
119
|
isOpen={isOpen}
|
|
118
120
|
setIsOpen={setIsOpen}
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
{ label: 'Strawberry', value: 'strawberry', isFruit: true },
|
|
122
|
-
{ label: 'Chocolate', value: 'chocolate', isFruit: false },
|
|
123
|
-
{ label: 'Apple', value: 'apple', isFruit: true },
|
|
124
|
-
{ label: 'Lemon', value: 'lemon', isFruit: true },
|
|
125
|
-
]}
|
|
121
|
+
renderTrigger={(triggerProps) => <FilterButton {...triggerProps} />}
|
|
122
|
+
items={groupedMockItems}
|
|
126
123
|
>
|
|
127
124
|
{({ items }): JSX.Element[] =>
|
|
128
|
-
items.map((item) =>
|
|
129
|
-
item.type === 'item'
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
)
|
|
125
|
+
items.map((item) => {
|
|
126
|
+
if (item.type === 'item') {
|
|
127
|
+
return (
|
|
128
|
+
<FilterSelect.Option
|
|
129
|
+
key={item.key}
|
|
130
|
+
item={{
|
|
131
|
+
...item,
|
|
132
|
+
}}
|
|
133
|
+
/>
|
|
134
|
+
)
|
|
135
|
+
}
|
|
136
|
+
return <FilterSelect.ItemDefaultRender key={item.key} item={item} />
|
|
137
|
+
})
|
|
141
138
|
}
|
|
142
139
|
</FilterSelect>
|
|
143
140
|
</div>
|
|
144
141
|
)
|
|
145
142
|
},
|
|
146
|
-
name: '
|
|
143
|
+
name: 'FilterSelect below page content',
|
|
147
144
|
}
|
|
@@ -188,14 +188,24 @@ describe('<Select />', () => {
|
|
|
188
188
|
})
|
|
189
189
|
})
|
|
190
190
|
it('is closed when hits the escape key', async () => {
|
|
191
|
-
const { getByRole } = render(<SelectWrapper
|
|
192
|
-
const
|
|
191
|
+
const { getByRole, queryByRole } = render(<SelectWrapper />)
|
|
192
|
+
const trigger = getByRole('combobox', {
|
|
193
|
+
name: 'Mock Label',
|
|
194
|
+
})
|
|
195
|
+
await user.tab()
|
|
196
|
+
await waitFor(() => {
|
|
197
|
+
expect(trigger).toHaveFocus()
|
|
198
|
+
})
|
|
199
|
+
await user.keyboard('{Enter}')
|
|
200
|
+
|
|
193
201
|
await waitFor(() => {
|
|
194
|
-
expect(
|
|
202
|
+
expect(queryByRole('listbox')).toBeVisible()
|
|
195
203
|
})
|
|
204
|
+
|
|
196
205
|
await user.keyboard('{Escape}')
|
|
206
|
+
|
|
197
207
|
await waitFor(() => {
|
|
198
|
-
expect(
|
|
208
|
+
expect(queryByRole('listbox')).toBe(null)
|
|
199
209
|
})
|
|
200
210
|
})
|
|
201
211
|
})
|
package/src/__rc__/Select/hooks/{useHasStableYPosition.ts → useHasCalculatedListboxPosition.ts}
RENAMED
|
@@ -4,8 +4,8 @@ import { useEffect, useState } from 'react'
|
|
|
4
4
|
* Due to the floating element's position starting as a negative value on render and then jumping to the correct position, this caused the focus to jump to the top of the page.
|
|
5
5
|
* This now polls to check if the element's position is stable by comparing the first and last position.
|
|
6
6
|
*/
|
|
7
|
-
export const
|
|
8
|
-
const [
|
|
7
|
+
export const useHasCalculatedListboxPosition = (ref: React.RefObject<HTMLElement>): boolean => {
|
|
8
|
+
const [hasStablePosition, setHasStablePosition] = useState(false)
|
|
9
9
|
const [lastYPosition, setLastYPosition] = useState<number | null>(null)
|
|
10
10
|
|
|
11
11
|
useEffect(() => {
|
|
@@ -14,18 +14,24 @@ export const useHasStableYPosition = (ref: React.RefObject<HTMLElement>): boolea
|
|
|
14
14
|
const { y } = ref.current.getBoundingClientRect()
|
|
15
15
|
if (lastYPosition === null) {
|
|
16
16
|
setLastYPosition(y)
|
|
17
|
-
} else if (y === lastYPosition) {
|
|
18
|
-
|
|
17
|
+
} else if (y === lastYPosition && y >= 0) {
|
|
18
|
+
setHasStablePosition(true)
|
|
19
19
|
} else {
|
|
20
20
|
setLastYPosition(y)
|
|
21
21
|
}
|
|
22
22
|
}
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
const intervalId = setInterval(
|
|
25
|
+
const intervalId = setInterval(() => {
|
|
26
|
+
if (hasStablePosition) {
|
|
27
|
+
clearInterval(intervalId)
|
|
28
|
+
return
|
|
29
|
+
}
|
|
30
|
+
checkPosition()
|
|
31
|
+
}, 1)
|
|
26
32
|
|
|
27
33
|
return () => clearInterval(intervalId)
|
|
28
|
-
}, [ref, lastYPosition])
|
|
34
|
+
}, [ref, lastYPosition, hasStablePosition])
|
|
29
35
|
|
|
30
|
-
return
|
|
36
|
+
return hasStablePosition
|
|
31
37
|
}
|
|
@@ -4,8 +4,8 @@ import { type SelectState } from '@react-stately/select'
|
|
|
4
4
|
import classnames from 'classnames'
|
|
5
5
|
import { type OverrideClassName } from '~components/types/OverrideClassName'
|
|
6
6
|
import { useSelectContext } from '../../context'
|
|
7
|
-
import {
|
|
8
|
-
import { type SelectItem, type SelectOption } from '../../types'
|
|
7
|
+
import { useHasCalculatedListboxPosition } from '../../hooks/useHasCalculatedListboxPosition'
|
|
8
|
+
import { type SelectItem, type SelectItemNode, type SelectOption } from '../../types'
|
|
9
9
|
import styles from './ListBox.module.scss'
|
|
10
10
|
|
|
11
11
|
export type SingleListBoxProps<Option extends SelectOption> = OverrideClassName<
|
|
@@ -16,17 +16,38 @@ export type SingleListBoxProps<Option extends SelectOption> = OverrideClassName<
|
|
|
16
16
|
menuProps: AriaListBoxOptions<SelectItem<Option>>
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
/**
|
|
19
|
+
/** determines is the first or last key passed in is a section. If not it will return the key, otherwise will return the first option key of that section */
|
|
20
|
+
const getOptionOrSectionKey = (
|
|
21
|
+
optionKey: SelectOption['value'] | null,
|
|
22
|
+
state: SelectState<SelectItem<any>>,
|
|
23
|
+
): Key | null => {
|
|
24
|
+
if (!optionKey) return null
|
|
25
|
+
|
|
26
|
+
const option = state.collection.getItem(optionKey) as SelectItemNode | null
|
|
27
|
+
const optionType = option?.type
|
|
28
|
+
|
|
29
|
+
if (optionType === 'section') {
|
|
30
|
+
const sectionOptions = option?.value?.options
|
|
31
|
+
|
|
32
|
+
return sectionOptions ? Array.from(sectionOptions)[0]?.value : null
|
|
33
|
+
}
|
|
34
|
+
return optionKey
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** A util to retrieve the key of the correct focusable option based of the focus strategy
|
|
20
38
|
* This is used to determine which element from the collection to focus to on open base on the keyboard event
|
|
21
39
|
* ie: UpArrow will set the focusStrategy to "last"
|
|
22
40
|
*/
|
|
23
41
|
const getOptionKeyFromCollection = (state: SelectState<SelectItem<any>>): Key | null => {
|
|
24
42
|
if (state.selectedItem) {
|
|
25
43
|
return state.selectedItem.key
|
|
26
|
-
} else if (state.focusStrategy === 'last') {
|
|
27
|
-
return state.collection.getLastKey()
|
|
28
44
|
}
|
|
29
|
-
|
|
45
|
+
|
|
46
|
+
if (state.focusStrategy === 'last') {
|
|
47
|
+
return getOptionOrSectionKey(state.collection.getLastKey(), state)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return getOptionOrSectionKey(state.collection.getFirstKey(), state)
|
|
30
51
|
}
|
|
31
52
|
|
|
32
53
|
/** This makes the use of query selector less brittle in instances where a failed selector is passed in
|
|
@@ -49,7 +70,7 @@ export const ListBox = <Option extends SelectOption>({
|
|
|
49
70
|
}: SingleListBoxProps<Option>): JSX.Element => {
|
|
50
71
|
const { state } = useSelectContext<Option>()
|
|
51
72
|
const ref = useRef<HTMLUListElement>(null)
|
|
52
|
-
const
|
|
73
|
+
const hasCalculatedListboxPosition = useHasCalculatedListboxPosition(ref)
|
|
53
74
|
const { listBoxProps } = useListBox(
|
|
54
75
|
{
|
|
55
76
|
...menuProps,
|
|
@@ -62,10 +83,10 @@ export const ListBox = <Option extends SelectOption>({
|
|
|
62
83
|
)
|
|
63
84
|
|
|
64
85
|
/**
|
|
65
|
-
*
|
|
86
|
+
* When the Listbox is opened the initial position starts above the window, which can cause the out of the box behaviour in react-aria's listbox to jump a user to the top of the page.
|
|
66
87
|
*/
|
|
67
88
|
useEffect(() => {
|
|
68
|
-
if (
|
|
89
|
+
if (hasCalculatedListboxPosition) {
|
|
69
90
|
const optionKey = getOptionKeyFromCollection(state)
|
|
70
91
|
const focusToElement = safeQuerySelector(`[data-key='${optionKey}']`)
|
|
71
92
|
|
|
@@ -77,7 +98,7 @@ export const ListBox = <Option extends SelectOption>({
|
|
|
77
98
|
}
|
|
78
99
|
// Only run this effect for checking the first successful render
|
|
79
100
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
80
|
-
}, [
|
|
101
|
+
}, [hasCalculatedListboxPosition])
|
|
81
102
|
|
|
82
103
|
return (
|
|
83
104
|
<ul
|