@kaizen/components 1.79.9 → 1.80.0
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/src/Avatar/Avatar.cjs +2 -2
- package/dist/cjs/src/Filter/FilterMultiSelect/FilterMultiSelect.cjs +11 -4
- package/dist/cjs/src/Filter/FilterMultiSelect/subcomponents/ListBox/ListBox.cjs +1 -1
- package/dist/cjs/src/Filter/FilterMultiSelect/subcomponents/ListBox/ListBox.module.css.cjs +9 -0
- package/dist/cjs/src/Filter/FilterMultiSelect/subcomponents/MenuPopup/MenuPopup.cjs +1 -1
- package/dist/cjs/src/Filter/FilterMultiSelect/subcomponents/MenuPopup/{MenuPopup.module.scss.cjs → MenuPopup.module.css.cjs} +1 -1
- package/dist/cjs/src/Filter/FilterMultiSelect/subcomponents/MenuPopup/ResponsiveMenuPopup.cjs +91 -0
- package/dist/esm/src/Avatar/Avatar.mjs +2 -2
- package/dist/esm/src/Filter/FilterMultiSelect/FilterMultiSelect.mjs +12 -5
- package/dist/esm/src/Filter/FilterMultiSelect/subcomponents/ListBox/ListBox.mjs +1 -1
- package/dist/esm/src/Filter/FilterMultiSelect/subcomponents/ListBox/ListBox.module.css.mjs +7 -0
- package/dist/esm/src/Filter/FilterMultiSelect/subcomponents/MenuPopup/MenuPopup.mjs +1 -1
- package/dist/esm/src/Filter/FilterMultiSelect/subcomponents/MenuPopup/MenuPopup.module.css.mjs +4 -0
- package/dist/esm/src/Filter/FilterMultiSelect/subcomponents/MenuPopup/ResponsiveMenuPopup.mjs +85 -0
- package/dist/styles.css +9216 -9208
- package/dist/types/Filter/FilterMultiSelect/FilterMultiSelect.d.ts +7 -1
- package/dist/types/Filter/FilterMultiSelect/_docs/MockData.d.ts +1 -0
- package/dist/types/Filter/FilterMultiSelect/subcomponents/MenuPopup/ResponsiveMenuPopup.d.ts +22 -0
- package/dist/types/Filter/FilterMultiSelect/subcomponents/MenuPopup/index.d.ts +1 -0
- package/locales/de.json +17 -17
- package/package.json +1 -1
- package/src/Avatar/Avatar.spec.tsx +1 -0
- package/src/Avatar/Avatar.tsx +2 -2
- package/src/Filter/FilterBar/subcomponents/FilterBarMultiSelect/FilterBarMultiSelect.spec.tsx +1 -0
- package/src/Filter/FilterMultiSelect/FilterMultiSelect.tsx +10 -4
- package/src/Filter/FilterMultiSelect/_docs/FilterMultiSelect.mdx +9 -1
- package/src/Filter/FilterMultiSelect/_docs/FilterMultiSelect.stories.tsx +79 -2
- package/src/Filter/FilterMultiSelect/_docs/MockData.ts +39 -0
- package/src/Filter/FilterMultiSelect/context/MenuTriggerProvider/MenuTriggerProvider.spec.tsx +2 -18
- package/src/Filter/FilterMultiSelect/subcomponents/ListBox/ListBox.module.css +22 -0
- package/src/Filter/FilterMultiSelect/subcomponents/ListBox/ListBox.tsx +1 -1
- package/src/Filter/FilterMultiSelect/subcomponents/ListBoxSection/ListBoxSection.module.scss +1 -0
- package/src/Filter/FilterMultiSelect/subcomponents/MenuPopup/MenuPopup.module.css +22 -0
- package/src/Filter/FilterMultiSelect/subcomponents/MenuPopup/MenuPopup.tsx +1 -1
- package/src/Filter/FilterMultiSelect/subcomponents/MenuPopup/ResponsiveMenuPopup.tsx +115 -0
- package/src/Filter/FilterMultiSelect/subcomponents/MenuPopup/index.ts +1 -0
- package/dist/cjs/src/Filter/FilterMultiSelect/subcomponents/ListBox/ListBox.module.scss.cjs +0 -9
- package/dist/esm/src/Filter/FilterMultiSelect/subcomponents/ListBox/ListBox.module.scss.mjs +0 -7
- package/dist/esm/src/Filter/FilterMultiSelect/subcomponents/MenuPopup/MenuPopup.module.scss.mjs +0 -4
- package/src/Filter/FilterMultiSelect/subcomponents/ListBox/ListBox.module.scss +0 -25
- package/src/Filter/FilterMultiSelect/subcomponents/MenuPopup/MenuPopup.module.scss +0 -24
|
@@ -15,11 +15,13 @@ type SelectionProps = {
|
|
|
15
15
|
export type FilterMultiSelectProps = {
|
|
16
16
|
trigger: (value?: MenuTriggerProviderContextType) => React.ReactNode;
|
|
17
17
|
children: (value?: SelectionProviderContextType) => React.ReactNode;
|
|
18
|
+
/** Replaces the MenuPopup. Should only be used for changing how the floating element is positioned, ie: with the `<ResponsiveMenuPopup />` primitive. */
|
|
19
|
+
customMenuPopup?: React.ComponentType<MenuPopupProps>;
|
|
18
20
|
triggerRef?: React.RefObject<HTMLButtonElement>;
|
|
19
21
|
className?: string;
|
|
20
22
|
} & Omit<MenuPopupProps, 'children'> & Omit<MenuTriggerProviderProps, 'children'> & SelectionProps;
|
|
21
23
|
export declare const FilterMultiSelect: {
|
|
22
|
-
({ trigger, children, isOpen, defaultOpen, onOpenChange, isLoading, loadingSkeleton, label, items, selectedKeys, defaultSelectedKeys, onSelectionChange, selectionMode, onSearchInputChange, triggerRef, className, }: FilterMultiSelectProps): JSX.Element;
|
|
24
|
+
({ trigger, children, customMenuPopup, isOpen, defaultOpen, onOpenChange, isLoading, loadingSkeleton, label, items, selectedKeys, defaultSelectedKeys, onSelectionChange, selectionMode, onSearchInputChange, triggerRef, className, ...restProps }: FilterMultiSelectProps): JSX.Element;
|
|
23
25
|
displayName: string;
|
|
24
26
|
TriggerButton: {
|
|
25
27
|
({ selectedOptionLabels, label, classNameOverride, labelCharacterLimitBeforeTruncate, }: import("./subcomponents/Trigger").FilterTriggerButtonProps): JSX.Element;
|
|
@@ -70,5 +72,9 @@ export declare const FilterMultiSelect: {
|
|
|
70
72
|
({ children, ...restProps }: import("./subcomponents/NoResults").NoResultsProps): JSX.Element;
|
|
71
73
|
displayName: string;
|
|
72
74
|
};
|
|
75
|
+
ResponsiveMenuPopup: {
|
|
76
|
+
({ children, floatingConfig, classNameOverride, isLoading, loadingSkeleton, ...restProps }: import("./subcomponents/MenuPopup").ResponsiveMenuPopupProps): JSX.Element;
|
|
77
|
+
displayName: string;
|
|
78
|
+
};
|
|
73
79
|
};
|
|
74
80
|
export {};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { type HTMLAttributes } from 'react';
|
|
2
|
+
import { type UseFloatingOptions } from '@floating-ui/react-dom';
|
|
3
|
+
import { type OverrideClassName } from "../../../../types/OverrideClassName";
|
|
4
|
+
import { type MenuPopupProps } from './MenuPopup';
|
|
5
|
+
export type FloatingConfig = Pick<UseFloatingOptions, 'placement' | 'strategy' | 'whileElementsMounted'> & {
|
|
6
|
+
/** Whether the component should automatically resize based on the available window height.
|
|
7
|
+
* @default true
|
|
8
|
+
*/
|
|
9
|
+
shouldResize?: boolean;
|
|
10
|
+
/** Whether the component should automatically flip to the top of the input based on the available window height.
|
|
11
|
+
* @default true
|
|
12
|
+
*/
|
|
13
|
+
shouldFlip?: boolean;
|
|
14
|
+
};
|
|
15
|
+
export type ResponsiveMenuPopupProps = MenuPopupProps & {
|
|
16
|
+
floatingConfig?: FloatingConfig;
|
|
17
|
+
} & OverrideClassName<HTMLAttributes<HTMLDivElement>>;
|
|
18
|
+
/** This is a popup primitive that can be used with the FilterMultiSelect when there are overflow issues with the original implementation. This uses the floating-ui */
|
|
19
|
+
export declare const ResponsiveMenuPopup: {
|
|
20
|
+
({ children, floatingConfig, classNameOverride, isLoading, loadingSkeleton, ...restProps }: ResponsiveMenuPopupProps): JSX.Element;
|
|
21
|
+
displayName: string;
|
|
22
|
+
};
|
package/locales/de.json
CHANGED
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
},
|
|
24
24
|
"date.validation.unavailableDate" : {
|
|
25
25
|
"description" : "Error message when the user tries to select a date that is unavailable",
|
|
26
|
-
"message" : "{inputValue} ist nicht verfügbar.
|
|
26
|
+
"message" : "{inputValue} ist nicht verfügbar. Versuche es mit einem anderen Datum."
|
|
27
27
|
},
|
|
28
28
|
"dateInputDescription.inputFormat" : {
|
|
29
29
|
"description" : "Label for the 'Input format' field",
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
},
|
|
32
32
|
"datePicker.calendarLabelDescription" : {
|
|
33
33
|
"description" : "Label for the search input",
|
|
34
|
-
"message" : "
|
|
34
|
+
"message" : "Wähle im Kalender ein Datum für:"
|
|
35
35
|
},
|
|
36
36
|
"filterBar.addFiltersMenu.buttonLabel" : {
|
|
37
37
|
"description" : "Menu button label to show additional available filter options",
|
|
@@ -87,7 +87,7 @@
|
|
|
87
87
|
},
|
|
88
88
|
"kzErrorPage" : {
|
|
89
89
|
"description" : "Label for contact button",
|
|
90
|
-
"message" : "kontaktieren
|
|
90
|
+
"message" : "Support kontaktieren"
|
|
91
91
|
},
|
|
92
92
|
"kzErrorPage.400.message" : {
|
|
93
93
|
"description" : "Call to action instructions for the user",
|
|
@@ -99,23 +99,23 @@
|
|
|
99
99
|
},
|
|
100
100
|
"kzErrorPage.401.message" : {
|
|
101
101
|
"description" : "Call to action instructions for the user",
|
|
102
|
-
"message" : "Leider können wir nicht überprüfen, ob
|
|
102
|
+
"message" : "Leider können wir nicht überprüfen, ob du diese Seite anzeigen kannst. Gehe zurück und versuche es erneut oder gehe zur Startseite."
|
|
103
103
|
},
|
|
104
104
|
"kzErrorPage.401.title" : {
|
|
105
105
|
"description" : "Main title for page",
|
|
106
|
-
"message" : "
|
|
106
|
+
"message" : "Du kannst diese Seite nicht anzeigen"
|
|
107
107
|
},
|
|
108
108
|
"kzErrorPage.403.message" : {
|
|
109
109
|
"description" : "Call to action instructions for the user",
|
|
110
|
-
"message" : "Offenbar fehlt
|
|
110
|
+
"message" : "Offenbar fehlt dir leider die Berechtigung zum Anzeigen dieser Seite. Gehe zurück und versuche es erneut oder gehe zur Startseite."
|
|
111
111
|
},
|
|
112
112
|
"kzErrorPage.403.title" : {
|
|
113
113
|
"description" : "Main title for page",
|
|
114
|
-
"message" : "
|
|
114
|
+
"message" : "Du kannst diese Seite nicht anzeigen"
|
|
115
115
|
},
|
|
116
116
|
"kzErrorPage.404.message" : {
|
|
117
117
|
"description" : "Call to action instructions for the user",
|
|
118
|
-
"message" : "Leider können wir die von
|
|
118
|
+
"message" : "Leider können wir die von dir gesuchte Seite nicht finden. Gehe zurück und versuche es erneut oder gehe zur Startseite"
|
|
119
119
|
},
|
|
120
120
|
"kzErrorPage.404.title" : {
|
|
121
121
|
"description" : "Main title of page",
|
|
@@ -123,7 +123,7 @@
|
|
|
123
123
|
},
|
|
124
124
|
"kzErrorPage.413" : {
|
|
125
125
|
"description" : "Call to action instructions for the user",
|
|
126
|
-
"message" : "Leider liegt ein Problem mit unserem System vor und diese Seite kann nicht angezeigt werden.
|
|
126
|
+
"message" : "Leider liegt ein Problem mit unserem System vor und diese Seite kann nicht angezeigt werden. Gehe zurück und versuche es erneut oder gehe zur Startseite"
|
|
127
127
|
},
|
|
128
128
|
"kzErrorPage.413.title" : {
|
|
129
129
|
"description" : "Main title of page",
|
|
@@ -131,7 +131,7 @@
|
|
|
131
131
|
},
|
|
132
132
|
"kzErrorPage.422.message" : {
|
|
133
133
|
"description" : "Call to action instructions for the user",
|
|
134
|
-
"message" : "
|
|
134
|
+
"message" : "Deine Änderung konnte leider nicht vorgenommen werden. Gehe zurück und versuche es erneut oder gehe zur Startseite."
|
|
135
135
|
},
|
|
136
136
|
"kzErrorPage.422.title" : {
|
|
137
137
|
"description" : "Main title of page",
|
|
@@ -139,7 +139,7 @@
|
|
|
139
139
|
},
|
|
140
140
|
"kzErrorPage.500" : {
|
|
141
141
|
"description" : "Call to action instructions for the user",
|
|
142
|
-
"message" : "Leider liegt ein Problem mit unserem System vor und diese Seite kann nicht angezeigt werden.
|
|
142
|
+
"message" : "Leider liegt ein Problem mit unserem System vor und diese Seite kann nicht angezeigt werden. Gehe zurück und versuche es erneut oder gehe zur Startseite."
|
|
143
143
|
},
|
|
144
144
|
"kzErrorPage.500.title" : {
|
|
145
145
|
"description" : "Main title of page",
|
|
@@ -147,27 +147,27 @@
|
|
|
147
147
|
},
|
|
148
148
|
"kzErrorPage.502.message" : {
|
|
149
149
|
"description" : "Call to action instructions for the user",
|
|
150
|
-
"message" : "Das tut uns leid. Das Beste, was
|
|
150
|
+
"message" : "Das tut uns leid. Das Beste, was du tun kannst, ist, zurückzugehen und es erneut zu versuchen."
|
|
151
151
|
},
|
|
152
152
|
"kzErrorPage.502.title" : {
|
|
153
153
|
"description" : "Main title of page",
|
|
154
|
-
"message" : "
|
|
154
|
+
"message" : "Du kannst diese Seite nicht anzeigen"
|
|
155
155
|
},
|
|
156
156
|
"kzErrorPage.503.message" : {
|
|
157
157
|
"description" : "Call to action instructions for the user",
|
|
158
|
-
"message" : "Das tut uns leid. Das Beste, was
|
|
158
|
+
"message" : "Das tut uns leid. Das Beste, was du tun kannst, ist, zurückzugehen und es erneut zu versuchen."
|
|
159
159
|
},
|
|
160
160
|
"kzErrorPage.503.title" : {
|
|
161
161
|
"description" : "Main title of page",
|
|
162
|
-
"message" : "
|
|
162
|
+
"message" : "Du kannst diese Seite nicht anzeigen"
|
|
163
163
|
},
|
|
164
164
|
"kzErrorPage.504.message" : {
|
|
165
165
|
"description" : "Call to action instructions for the user",
|
|
166
|
-
"message" : "Das tut uns leid. Das Beste, was
|
|
166
|
+
"message" : "Das tut uns leid. Das Beste, was du tun kannst, ist, zurückzugehen und es erneut zu versuchen."
|
|
167
167
|
},
|
|
168
168
|
"kzErrorPage.504.title" : {
|
|
169
169
|
"description" : "Main title of page",
|
|
170
|
-
"message" : "
|
|
170
|
+
"message" : "Du kannst diese Seite nicht anzeigen"
|
|
171
171
|
},
|
|
172
172
|
"kzErrorPage.errorCode" : {
|
|
173
173
|
"message" : "Fehlercode {code}"
|
package/package.json
CHANGED
|
@@ -14,6 +14,7 @@ describe('<Avatar />', () => {
|
|
|
14
14
|
fireEvent.error(screen.getByRole('img'))
|
|
15
15
|
|
|
16
16
|
expect(screen.getByText('JD')).toBeInTheDocument()
|
|
17
|
+
expect(screen.queryByRole('img')).not.toBeInTheDocument()
|
|
17
18
|
})
|
|
18
19
|
|
|
19
20
|
describe('full name provided contains more than two names', () => {
|
package/src/Avatar/Avatar.tsx
CHANGED
|
@@ -156,13 +156,13 @@ export const Avatar = ({
|
|
|
156
156
|
)}
|
|
157
157
|
{...restProps}
|
|
158
158
|
>
|
|
159
|
-
{avatarState !== 'none' && (
|
|
159
|
+
{avatarState !== 'none' && avatarState !== 'error' && (
|
|
160
160
|
<img
|
|
161
161
|
ref={image}
|
|
162
162
|
className={classnames(
|
|
163
163
|
styles.avatarImage,
|
|
164
164
|
isCompany && styles.companyAvatarImage,
|
|
165
|
-
|
|
165
|
+
avatarState === 'loading' && styles.loading,
|
|
166
166
|
)}
|
|
167
167
|
src={avatarSrc}
|
|
168
168
|
onError={onImageFailure}
|
package/src/Filter/FilterBar/subcomponents/FilterBarMultiSelect/FilterBarMultiSelect.spec.tsx
CHANGED
|
@@ -180,6 +180,7 @@ describe('<FilterBarMultiSelect />', () => {
|
|
|
180
180
|
})
|
|
181
181
|
|
|
182
182
|
await user.click(getByRole('option', { name: 'Fruit Jelly' }))
|
|
183
|
+
await user.keyboard('{Escape}')
|
|
183
184
|
await waitFor(() => {
|
|
184
185
|
expect(getByRole('button', { name: 'Toppings : Pearls, Fruit Jelly' })).toBeInTheDocument()
|
|
185
186
|
})
|
|
@@ -13,7 +13,7 @@ import { ListBox } from './subcomponents/ListBox'
|
|
|
13
13
|
import { ListBoxSection } from './subcomponents/ListBoxSection'
|
|
14
14
|
import { LoadMoreButton } from './subcomponents/LoadMoreButton'
|
|
15
15
|
import { MenuFooter, MenuLoadingSkeleton } from './subcomponents/MenuLayout'
|
|
16
|
-
import { MenuPopup, type MenuPopupProps } from './subcomponents/MenuPopup'
|
|
16
|
+
import { MenuPopup, ResponsiveMenuPopup, type MenuPopupProps } from './subcomponents/MenuPopup'
|
|
17
17
|
import { MultiSelectOption } from './subcomponents/MultiSelectOption'
|
|
18
18
|
import { NoResults } from './subcomponents/NoResults'
|
|
19
19
|
import { SearchInput } from './subcomponents/SearchInput'
|
|
@@ -35,6 +35,8 @@ type SelectionProps = {
|
|
|
35
35
|
export type FilterMultiSelectProps = {
|
|
36
36
|
trigger: (value?: MenuTriggerProviderContextType) => React.ReactNode
|
|
37
37
|
children: (value?: SelectionProviderContextType) => React.ReactNode // the content of the menu
|
|
38
|
+
/** Replaces the MenuPopup. Should only be used for changing how the floating element is positioned, ie: with the `<ResponsiveMenuPopup />` primitive. */
|
|
39
|
+
customMenuPopup?: React.ComponentType<MenuPopupProps>
|
|
38
40
|
triggerRef?: React.RefObject<HTMLButtonElement>
|
|
39
41
|
className?: string
|
|
40
42
|
} & Omit<MenuPopupProps, 'children'> &
|
|
@@ -44,6 +46,7 @@ export type FilterMultiSelectProps = {
|
|
|
44
46
|
export const FilterMultiSelect = ({
|
|
45
47
|
trigger,
|
|
46
48
|
children,
|
|
49
|
+
customMenuPopup,
|
|
47
50
|
isOpen,
|
|
48
51
|
defaultOpen,
|
|
49
52
|
onOpenChange,
|
|
@@ -58,9 +61,11 @@ export const FilterMultiSelect = ({
|
|
|
58
61
|
onSearchInputChange,
|
|
59
62
|
triggerRef,
|
|
60
63
|
className,
|
|
64
|
+
...restProps
|
|
61
65
|
}: FilterMultiSelectProps): JSX.Element => {
|
|
66
|
+
const MenuComponent = customMenuPopup ?? MenuPopup
|
|
62
67
|
const menuTriggerProps = { isOpen, defaultOpen, onOpenChange, triggerRef }
|
|
63
|
-
const menuPopupProps = { isLoading, loadingSkeleton }
|
|
68
|
+
const menuPopupProps = { isLoading, loadingSkeleton, ...restProps }
|
|
64
69
|
const disabledKeys: Selection = new Set(
|
|
65
70
|
items?.filter((item) => item.isDisabled === true).map((disabledItem) => disabledItem.value),
|
|
66
71
|
)
|
|
@@ -79,11 +84,11 @@ export const FilterMultiSelect = ({
|
|
|
79
84
|
<MenuTriggerProvider {...menuTriggerProps}>
|
|
80
85
|
<div className={className}>
|
|
81
86
|
<MenuTriggerConsumer>{trigger}</MenuTriggerConsumer>
|
|
82
|
-
<
|
|
87
|
+
<MenuComponent aria-label={label} {...menuPopupProps}>
|
|
83
88
|
<SelectionProvider {...selectionProps}>
|
|
84
89
|
<SelectionConsumer>{children}</SelectionConsumer>
|
|
85
90
|
</SelectionProvider>
|
|
86
|
-
</
|
|
91
|
+
</MenuComponent>
|
|
87
92
|
</div>
|
|
88
93
|
</MenuTriggerProvider>
|
|
89
94
|
)
|
|
@@ -104,3 +109,4 @@ FilterMultiSelect.MenuFooter = MenuFooter // For layout
|
|
|
104
109
|
FilterMultiSelect.MenuLoadingSkeleton = MenuLoadingSkeleton // Menu Loading Skeleton example
|
|
105
110
|
FilterMultiSelect.LoadMoreButton = LoadMoreButton
|
|
106
111
|
FilterMultiSelect.NoResults = NoResults
|
|
112
|
+
FilterMultiSelect.ResponsiveMenuPopup = ResponsiveMenuPopup
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Controls, Meta, Canvas } from '@storybook/blocks'
|
|
1
|
+
import { Controls, Meta, Canvas, DocsStory } from '@storybook/blocks'
|
|
2
2
|
import { ResourceLinks, KAIOInstallation, NoClipCanvas } from '~storybook/components'
|
|
3
3
|
import * as FilterMultiSelectStories from './FilterMultiSelect.stories'
|
|
4
4
|
|
|
@@ -30,6 +30,14 @@ The FilterMultiSelect is a component relies heavily on consumer implemntation. I
|
|
|
30
30
|
|
|
31
31
|
<Canvas of={FilterMultiSelectStories.WithSectionHeaders} />
|
|
32
32
|
|
|
33
|
+
### With customMenuPopup component
|
|
34
|
+
|
|
35
|
+
You can replace the `MenuPopup` component within the FilterMultiSelect to allow flexibility in how the popup's placement is determined. While the default behavior should satisfy most scenarios, this can be used when there is limited vertical space available in the viewport.
|
|
36
|
+
|
|
37
|
+
<Canvas of={FilterMultiSelectStories.AboveIfAvailable} />
|
|
38
|
+
|
|
39
|
+
For convenience, a primitive called `ResponsiveMenuPopup` that can be accessed via dot notation that will automatically adjust the placement and size of the popup based on the available window height. This implementation uses `floating-ui` and the [Popover API](https://developer.mozilla.org/en-US/docs/Web/API/Popover_API) instead of `react-aria` hooks. It also locks scroll when the popup is active.
|
|
40
|
+
|
|
33
41
|
### Async
|
|
34
42
|
|
|
35
43
|
The following is an example of how you may create an async FilterMultiSelect using `@tanstack/react-query`.
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import React, { useState } from 'react'
|
|
2
2
|
import type { Selection } from '@react-types/shared'
|
|
3
3
|
import type { Meta, StoryObj } from '@storybook/react'
|
|
4
|
+
import { expect, userEvent, waitFor, within } from '@storybook/test'
|
|
4
5
|
import isChromatic from 'chromatic'
|
|
5
6
|
import { InlineNotification } from '~components/Notification'
|
|
6
7
|
import { TextField } from '~components/TextField'
|
|
7
8
|
import { FilterMultiSelect, getSelectedOptionLabels } from '..'
|
|
8
|
-
import { mockItems } from './MockData'
|
|
9
|
+
import { mockItems, mockManyItems } from './MockData'
|
|
9
10
|
|
|
10
11
|
const IS_CHROMATIC = isChromatic()
|
|
11
12
|
|
|
@@ -14,7 +15,7 @@ const meta = {
|
|
|
14
15
|
component: FilterMultiSelect,
|
|
15
16
|
parameters: {
|
|
16
17
|
docs: {
|
|
17
|
-
source: { type: '
|
|
18
|
+
source: { type: 'auto' },
|
|
18
19
|
},
|
|
19
20
|
},
|
|
20
21
|
args: {
|
|
@@ -178,6 +179,7 @@ export const WithSectionHeaders: Story = {
|
|
|
178
179
|
...FilterMultiSelectTemplate,
|
|
179
180
|
args: {
|
|
180
181
|
isOpen: IS_CHROMATIC || undefined,
|
|
182
|
+
items: mockManyItems,
|
|
181
183
|
children: (): JSX.Element => (
|
|
182
184
|
<>
|
|
183
185
|
<FilterMultiSelect.SearchInput />
|
|
@@ -308,3 +310,78 @@ export const WithSectionNotification: Story = {
|
|
|
308
310
|
chromatic: { disable: false },
|
|
309
311
|
},
|
|
310
312
|
}
|
|
313
|
+
|
|
314
|
+
const sourceCode = `
|
|
315
|
+
<FilterMultiSelect
|
|
316
|
+
{...filterMultiSelectProps}
|
|
317
|
+
customMenuPopup={(props): JSX.Element => (
|
|
318
|
+
// This will replace the default MenuPopup with a custom one. The rest of the component should still be implemented as the FilterMultiSelect pattern.
|
|
319
|
+
<FilterMultiSelect.ResponsiveMenuPopup {...props} />
|
|
320
|
+
)}
|
|
321
|
+
>
|
|
322
|
+
{/* FilterMultiSelect children */}
|
|
323
|
+
</FilterMultiSelect>
|
|
324
|
+
`
|
|
325
|
+
|
|
326
|
+
export const AboveIfAvailable: Story = {
|
|
327
|
+
...WithSectionNotification,
|
|
328
|
+
name: 'With customMenuPopup and vertical placement',
|
|
329
|
+
parameters: {
|
|
330
|
+
viewport: {
|
|
331
|
+
viewports: {
|
|
332
|
+
LimitedViewportAutoPlace: {
|
|
333
|
+
name: 'Limited vertical space',
|
|
334
|
+
styles: {
|
|
335
|
+
width: '1024px',
|
|
336
|
+
height: '650px',
|
|
337
|
+
},
|
|
338
|
+
},
|
|
339
|
+
},
|
|
340
|
+
defaultViewport: 'LimitedViewportAutoPlace',
|
|
341
|
+
},
|
|
342
|
+
docs: { source: { code: sourceCode } },
|
|
343
|
+
},
|
|
344
|
+
args: {
|
|
345
|
+
customMenuPopup: (props): JSX.Element => <FilterMultiSelect.ResponsiveMenuPopup {...props} />,
|
|
346
|
+
},
|
|
347
|
+
decorators: [
|
|
348
|
+
(Story) => (
|
|
349
|
+
<div>
|
|
350
|
+
<div style={{ height: '80vh', maxHeight: '500px' }}>Content above</div>
|
|
351
|
+
<Story />
|
|
352
|
+
</div>
|
|
353
|
+
),
|
|
354
|
+
],
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
export const ShouldResize: Story = {
|
|
358
|
+
...AboveIfAvailable,
|
|
359
|
+
name: 'With customMenuPopup, vertical placement and resized popup',
|
|
360
|
+
parameters: {
|
|
361
|
+
chromatic: {
|
|
362
|
+
disable: false,
|
|
363
|
+
},
|
|
364
|
+
viewport: {
|
|
365
|
+
viewports: {
|
|
366
|
+
LimitedViewportAutoPlace: {
|
|
367
|
+
name: 'Limited vertical space',
|
|
368
|
+
styles: {
|
|
369
|
+
width: '1024px',
|
|
370
|
+
height: '450px',
|
|
371
|
+
},
|
|
372
|
+
},
|
|
373
|
+
},
|
|
374
|
+
defaultViewport: 'LimitedViewportAutoPlace',
|
|
375
|
+
},
|
|
376
|
+
},
|
|
377
|
+
play: async ({ canvasElement, step }) => {
|
|
378
|
+
const canvas = within(canvasElement.parentElement!)
|
|
379
|
+
const triggerButton = await canvas.findByRole('button', {
|
|
380
|
+
name: /Engineer/i,
|
|
381
|
+
})
|
|
382
|
+
await step('Trigger opens the FilterMultiSelect dialog', async () => {
|
|
383
|
+
await userEvent.click(triggerButton)
|
|
384
|
+
await waitFor(() => expect(canvas.getByRole('dialog')).toBeVisible())
|
|
385
|
+
})
|
|
386
|
+
},
|
|
387
|
+
}
|
|
@@ -58,3 +58,42 @@ export const locationDemographicValues = [
|
|
|
58
58
|
label: 'London',
|
|
59
59
|
},
|
|
60
60
|
]
|
|
61
|
+
|
|
62
|
+
export const mockManyItems: ItemType[] = [
|
|
63
|
+
{ label: 'Front-End', value: 'id-fe', count: '1245' },
|
|
64
|
+
{ label: 'Back-End', value: 'id-be', count: '4', isDisabled: true },
|
|
65
|
+
{ label: 'SRE', value: 'id-sre', count: '4', isDisabled: true },
|
|
66
|
+
{ label: 'Dev-ops', value: 'id-devops' },
|
|
67
|
+
{ label: 'Others', value: 'id-others' },
|
|
68
|
+
{
|
|
69
|
+
label: 'Engineer-type-1 has a really really long label',
|
|
70
|
+
value: 'id-type-1',
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
label: 'Engineer-type-2 also has a really really long label',
|
|
74
|
+
value: 'id-type-2',
|
|
75
|
+
count: '156',
|
|
76
|
+
},
|
|
77
|
+
{ label: 'Engineer-type-3', value: 'id-type-3' },
|
|
78
|
+
{
|
|
79
|
+
label: 'Engineer-type-4',
|
|
80
|
+
value: 'id-type-4',
|
|
81
|
+
count: '4',
|
|
82
|
+
isDisabled: true,
|
|
83
|
+
},
|
|
84
|
+
{ label: 'Engineer-type-5', value: 'id-type-5' },
|
|
85
|
+
{ label: 'UI Designer', value: 'id-ui', count: '42' },
|
|
86
|
+
{ label: 'UX Researcher', value: 'id-ux', count: '15' },
|
|
87
|
+
{ label: 'Product Manager', value: 'id-pm', count: '28' },
|
|
88
|
+
{ label: 'Project Manager', value: 'id-project', count: '19', isDisabled: true },
|
|
89
|
+
{ label: 'Data Scientist', value: 'id-ds', count: '11' },
|
|
90
|
+
{ label: 'Machine Learning Engineer', value: 'id-ml', count: '7' },
|
|
91
|
+
{ label: 'QA Tester', value: 'id-qa', count: '22' },
|
|
92
|
+
{
|
|
93
|
+
label: 'Technical Writer with documentation expertise',
|
|
94
|
+
value: 'id-tech-writer',
|
|
95
|
+
count: '5',
|
|
96
|
+
},
|
|
97
|
+
{ label: 'DevSecOps Engineer', value: 'id-devsecops', count: '3', isDisabled: true },
|
|
98
|
+
{ label: 'Cloud Architect', value: 'id-cloud', count: '8' },
|
|
99
|
+
]
|
package/src/Filter/FilterMultiSelect/context/MenuTriggerProvider/MenuTriggerProvider.spec.tsx
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import React from 'react'
|
|
2
2
|
import { render, screen, waitFor } from '@testing-library/react'
|
|
3
3
|
import userEvent from '@testing-library/user-event'
|
|
4
|
-
import { vi } from 'vitest'
|
|
5
4
|
import { FilterTriggerButton } from '~components/Filter/FilterMultiSelect/subcomponents/Trigger'
|
|
6
5
|
import { MenuPopup } from '../../subcomponents/MenuPopup'
|
|
7
6
|
import { MenuTriggerProvider, type MenuTriggerProviderProps } from './MenuTriggerProvider'
|
|
@@ -53,15 +52,11 @@ describe('<MenuTriggerProvider /> - Visual content', () => {
|
|
|
53
52
|
rerender(<MenuTriggerProviderWrapper isOpen={false} />)
|
|
54
53
|
expect(screen.queryByText('menu-content-mock')).not.toBeInTheDocument()
|
|
55
54
|
})
|
|
56
|
-
|
|
57
|
-
it('fires the onOpenChange callback when the trigger is interacted', async () => {
|
|
55
|
+
it('fires the onOpenChange callback on user interaction to close the menu', async () => {
|
|
58
56
|
const onOpenChange = vi.fn()
|
|
59
57
|
render(<MenuTriggerProviderWrapper isOpen onOpenChange={onOpenChange} />)
|
|
60
58
|
|
|
61
|
-
|
|
62
|
-
name: 'trigger-display-label-mock',
|
|
63
|
-
})
|
|
64
|
-
await user.click(trigger)
|
|
59
|
+
await user.keyboard('{Escape}')
|
|
65
60
|
|
|
66
61
|
await waitFor(() => {
|
|
67
62
|
expect(onOpenChange).toBeCalledTimes(1)
|
|
@@ -86,17 +81,6 @@ describe('<MenuTriggerProvider /> - Mouse interaction', () => {
|
|
|
86
81
|
})
|
|
87
82
|
|
|
88
83
|
describe('Given the menu is opened', () => {
|
|
89
|
-
it('is closed when user clicks on the trigger', async () => {
|
|
90
|
-
render(<MenuTriggerProviderWrapper defaultOpen />)
|
|
91
|
-
const trigger = screen.getByRole('button', {
|
|
92
|
-
name: 'trigger-display-label-mock',
|
|
93
|
-
})
|
|
94
|
-
await user.click(trigger)
|
|
95
|
-
await waitFor(() => {
|
|
96
|
-
expect(screen.queryByText('menu-content-mock')).not.toBeInTheDocument()
|
|
97
|
-
})
|
|
98
|
-
})
|
|
99
|
-
|
|
100
84
|
it('is closed when user clicks outside of the menu', async () => {
|
|
101
85
|
render(<MenuTriggerProviderWrapper defaultOpen />)
|
|
102
86
|
await user.click(document.body)
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
@layer kz-components {
|
|
2
|
+
.listBox {
|
|
3
|
+
list-style: none;
|
|
4
|
+
padding: var(--spacing-12);
|
|
5
|
+
margin: 0 var(--spacing-12) 0 0;
|
|
6
|
+
display: grid;
|
|
7
|
+
max-height: 22rem;
|
|
8
|
+
overflow-y: auto;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
.overflown {
|
|
12
|
+
padding-right: var(--spacing-12);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
.hidden {
|
|
16
|
+
display: none;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
.noResultsWrapper {
|
|
20
|
+
list-style: none;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -3,7 +3,7 @@ import { type Collection, type Key } from '@react-types/shared'
|
|
|
3
3
|
import classnames from 'classnames'
|
|
4
4
|
import { useSelectionContext } from '../../context/SelectionProvider'
|
|
5
5
|
import { type MultiSelectItem } from '../../types'
|
|
6
|
-
import styles from './ListBox.module.
|
|
6
|
+
import styles from './ListBox.module.css'
|
|
7
7
|
|
|
8
8
|
export type ListBoxItems = {
|
|
9
9
|
selectedItems: MultiSelectItem[]
|
package/src/Filter/FilterMultiSelect/subcomponents/ListBoxSection/ListBoxSection.module.scss
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
}
|
|
11
11
|
|
|
12
12
|
.listBoxSectionHeader {
|
|
13
|
+
position: relative; // this is needed to ensure the VisuallyHidden element doesn't impact the scroll height of the list
|
|
13
14
|
font-family: $typography-heading-6-font-family;
|
|
14
15
|
font-size: $typography-heading-6-font-size;
|
|
15
16
|
font-weight: $typography-heading-6-font-weight;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
@layer kz-components {
|
|
2
|
+
.menuPopup {
|
|
3
|
+
/* from $ca-z-index-dropdown */
|
|
4
|
+
z-index: 1000;
|
|
5
|
+
box-sizing: border-box;
|
|
6
|
+
background: var(--color-white);
|
|
7
|
+
color: var(--color-purple-800);
|
|
8
|
+
border-radius: var(--border-solid-border-radius);
|
|
9
|
+
box-shadow: var(--shadow-large-box-shadow);
|
|
10
|
+
padding: var(--spacing-12) 0;
|
|
11
|
+
margin-top: var(--spacing-6);
|
|
12
|
+
text-align: start;
|
|
13
|
+
width: var(--menu-container-width, 294px);
|
|
14
|
+
max-height: var(--menu-container-height, 500px);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
.menuPopup[popover]:popover-open {
|
|
18
|
+
z-index: unset;
|
|
19
|
+
margin: 0;
|
|
20
|
+
inset: unset;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -2,7 +2,7 @@ import React from 'react'
|
|
|
2
2
|
import { FocusScope } from '@react-aria/focus'
|
|
3
3
|
import { DismissButton, useOverlay } from '@react-aria/overlays'
|
|
4
4
|
import { useMenuTriggerContext } from '../../context'
|
|
5
|
-
import styles from './MenuPopup.module.
|
|
5
|
+
import styles from './MenuPopup.module.css'
|
|
6
6
|
|
|
7
7
|
export type MenuPopupProps = {
|
|
8
8
|
isLoading?: boolean
|