@shipfox/react-ui 0.10.0 → 0.12.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/.turbo/turbo-build.log +6 -6
- package/.turbo/turbo-check.log +2 -2
- package/.turbo/turbo-type.log +1 -1
- package/CHANGELOG.md +12 -0
- package/dist/components/button/button.js +14 -11
- package/dist/components/button/button.js.map +1 -1
- package/dist/components/calendar/calendar.d.ts +5 -0
- package/dist/components/calendar/calendar.d.ts.map +1 -0
- package/dist/components/calendar/calendar.js +46 -0
- package/dist/components/calendar/calendar.js.map +1 -0
- package/dist/components/calendar/index.d.ts +2 -0
- package/dist/components/calendar/index.d.ts.map +1 -0
- package/dist/components/calendar/index.js +3 -0
- package/dist/components/calendar/index.js.map +1 -0
- package/dist/components/date-picker/date-picker.d.ts +19 -0
- package/dist/components/date-picker/date-picker.d.ts.map +1 -0
- package/dist/components/date-picker/date-picker.js +114 -0
- package/dist/components/date-picker/date-picker.js.map +1 -0
- package/dist/components/date-picker/date-picker.stories.js +333 -0
- package/dist/components/date-picker/date-picker.stories.js.map +1 -0
- package/dist/components/date-picker/index.d.ts +2 -0
- package/dist/components/date-picker/index.d.ts.map +1 -0
- package/dist/components/date-picker/index.js +3 -0
- package/dist/components/date-picker/index.js.map +1 -0
- package/dist/components/date-time-range-picker/date-time-range-picker.d.ts +24 -0
- package/dist/components/date-time-range-picker/date-time-range-picker.d.ts.map +1 -0
- package/dist/components/date-time-range-picker/date-time-range-picker.js +130 -0
- package/dist/components/date-time-range-picker/date-time-range-picker.js.map +1 -0
- package/dist/components/date-time-range-picker/index.d.ts +2 -0
- package/dist/components/date-time-range-picker/index.d.ts.map +1 -0
- package/dist/components/date-time-range-picker/index.js +3 -0
- package/dist/components/date-time-range-picker/index.js.map +1 -0
- package/dist/components/dropdown-menu/dropdown-menu.d.ts +58 -0
- package/dist/components/dropdown-menu/dropdown-menu.d.ts.map +1 -0
- package/dist/components/dropdown-menu/dropdown-menu.js +280 -0
- package/dist/components/dropdown-menu/dropdown-menu.js.map +1 -0
- package/dist/components/dropdown-menu/dropdown-menu.stories.js +462 -0
- package/dist/components/dropdown-menu/dropdown-menu.stories.js.map +1 -0
- package/dist/components/dropdown-menu/index.d.ts +3 -0
- package/dist/components/dropdown-menu/index.d.ts.map +1 -0
- package/dist/components/dropdown-menu/index.js +3 -0
- package/dist/components/dropdown-menu/index.js.map +1 -0
- package/dist/components/dynamic-item/dynamic-item.d.ts +1 -1
- package/dist/components/dynamic-item/dynamic-item.d.ts.map +1 -1
- package/dist/components/dynamic-item/dynamic-item.js +4 -4
- package/dist/components/dynamic-item/dynamic-item.js.map +1 -1
- package/dist/components/dynamic-item/dynamic-item.stories.js +11 -1
- package/dist/components/dynamic-item/dynamic-item.stories.js.map +1 -1
- package/dist/components/index.d.ts +5 -0
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/index.js +5 -0
- package/dist/components/index.js.map +1 -1
- package/dist/components/inline-tips/inline-tips.stories.js +5 -0
- package/dist/components/inline-tips/inline-tips.stories.js.map +1 -1
- package/dist/components/item/item.stories.js +15 -8
- package/dist/components/item/item.stories.js.map +1 -1
- package/dist/components/modal/modal.stories.js +14 -6
- package/dist/components/modal/modal.stories.js.map +1 -1
- package/dist/components/popover/index.d.ts +2 -0
- package/dist/components/popover/index.d.ts.map +1 -0
- package/dist/components/popover/index.js +3 -0
- package/dist/components/popover/index.js.map +1 -0
- package/dist/components/popover/popover.d.ts +10 -0
- package/dist/components/popover/popover.d.ts.map +1 -0
- package/dist/components/popover/popover.js +47 -0
- package/dist/components/popover/popover.js.map +1 -0
- package/dist/components/textarea/textarea.stories.js +8 -2
- package/dist/components/textarea/textarea.stories.js.map +1 -1
- package/dist/styles.css +1 -1
- package/package.json +3 -3
- package/src/components/button/button.tsx +12 -12
- package/src/components/calendar/calendar.tsx +90 -0
- package/src/components/calendar/index.ts +1 -0
- package/src/components/date-picker/date-picker.stories.tsx +230 -0
- package/src/components/date-picker/date-picker.tsx +179 -0
- package/src/components/date-picker/index.ts +1 -0
- package/src/components/date-time-range-picker/date-time-range-picker.tsx +211 -0
- package/src/components/date-time-range-picker/index.ts +1 -0
- package/src/components/dropdown-menu/dropdown-menu.stories.tsx +384 -0
- package/src/components/dropdown-menu/dropdown-menu.tsx +416 -0
- package/src/components/dropdown-menu/index.ts +29 -0
- package/src/components/dynamic-item/dynamic-item.stories.tsx +6 -1
- package/src/components/dynamic-item/dynamic-item.tsx +9 -3
- package/src/components/index.ts +5 -0
- package/src/components/inline-tips/inline-tips.stories.tsx +5 -0
- package/src/components/item/item.stories.tsx +65 -56
- package/src/components/modal/modal.stories.tsx +16 -6
- package/src/components/popover/index.ts +1 -0
- package/src/components/popover/popover.tsx +60 -0
- package/src/components/textarea/textarea.stories.tsx +8 -2
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import {cva, type VariantProps} from 'class-variance-authority';
|
|
2
|
+
import {Calendar} from 'components/calendar';
|
|
3
|
+
import {Icon} from 'components/icon';
|
|
4
|
+
import {Popover, PopoverContent, PopoverTrigger} from 'components/popover';
|
|
5
|
+
import {format} from 'date-fns';
|
|
6
|
+
import type {ComponentProps, ReactNode} from 'react';
|
|
7
|
+
import {forwardRef, useState} from 'react';
|
|
8
|
+
import type {DateRange as DayPickerDateRange} from 'react-day-picker';
|
|
9
|
+
import {cn} from 'utils/cn';
|
|
10
|
+
|
|
11
|
+
export const dateTimeRangePickerVariants = cva(
|
|
12
|
+
'min-w-240 relative flex items-center rounded-6 shadow-button-neutral transition-[background-color,box-shadow] outline-none',
|
|
13
|
+
{
|
|
14
|
+
variants: {
|
|
15
|
+
variant: {
|
|
16
|
+
base: 'bg-background-field-base hover:bg-background-field-hover',
|
|
17
|
+
component: 'bg-background-field-component hover:bg-background-field-component-hover',
|
|
18
|
+
},
|
|
19
|
+
size: {
|
|
20
|
+
base: 'h-32',
|
|
21
|
+
small: 'h-28',
|
|
22
|
+
},
|
|
23
|
+
state: {
|
|
24
|
+
default: '',
|
|
25
|
+
error: 'shadow-border-error',
|
|
26
|
+
disabled:
|
|
27
|
+
'bg-background-neutral-disabled shadow-none pointer-events-none cursor-not-allowed',
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
defaultVariants: {
|
|
31
|
+
variant: 'base',
|
|
32
|
+
size: 'base',
|
|
33
|
+
state: 'default',
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
export type DateRange = {
|
|
39
|
+
start?: Date;
|
|
40
|
+
end?: Date;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export type DateTimeRangePickerProps = Omit<ComponentProps<'input'>, 'size' | 'type'> &
|
|
44
|
+
VariantProps<typeof dateTimeRangePickerVariants> & {
|
|
45
|
+
dateRange?: DateRange;
|
|
46
|
+
onDateRangeSelect?: (range: DateRange | undefined) => void;
|
|
47
|
+
placeholder?: string;
|
|
48
|
+
dateFormat?: string;
|
|
49
|
+
leftIcon?: ReactNode;
|
|
50
|
+
rightIcon?: ReactNode;
|
|
51
|
+
onClear?: () => void;
|
|
52
|
+
numberOfMonths?: number;
|
|
53
|
+
closeOnSelect?: boolean;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
export const DateTimeRangePicker = forwardRef<HTMLInputElement, DateTimeRangePickerProps>(
|
|
57
|
+
(
|
|
58
|
+
{
|
|
59
|
+
className,
|
|
60
|
+
variant,
|
|
61
|
+
size,
|
|
62
|
+
state,
|
|
63
|
+
dateRange,
|
|
64
|
+
onDateRangeSelect,
|
|
65
|
+
placeholder = 'DD/MM/YYYY - DD/MM/YYYY',
|
|
66
|
+
dateFormat = 'dd/MM/yyyy',
|
|
67
|
+
leftIcon,
|
|
68
|
+
rightIcon,
|
|
69
|
+
onClear,
|
|
70
|
+
disabled,
|
|
71
|
+
numberOfMonths = 2,
|
|
72
|
+
closeOnSelect = false,
|
|
73
|
+
...props
|
|
74
|
+
},
|
|
75
|
+
ref,
|
|
76
|
+
) => {
|
|
77
|
+
const [open, setOpen] = useState(false);
|
|
78
|
+
const isDisabled = disabled || state === 'disabled';
|
|
79
|
+
const hasRange = dateRange?.start && dateRange?.end;
|
|
80
|
+
const displayValue =
|
|
81
|
+
hasRange && dateRange.start && dateRange.end
|
|
82
|
+
? `${format(dateRange.start, dateFormat)} - ${format(dateRange.end, dateFormat)}`
|
|
83
|
+
: '';
|
|
84
|
+
|
|
85
|
+
// Convert our DateRange to react-day-picker's DateRange format
|
|
86
|
+
const dayPickerRange: DayPickerDateRange | undefined =
|
|
87
|
+
dateRange?.start || dateRange?.end
|
|
88
|
+
? {
|
|
89
|
+
from: dateRange?.start,
|
|
90
|
+
to: dateRange?.end,
|
|
91
|
+
}
|
|
92
|
+
: undefined;
|
|
93
|
+
|
|
94
|
+
const handleSelect = (selectedRange: DayPickerDateRange | undefined) => {
|
|
95
|
+
if (selectedRange) {
|
|
96
|
+
onDateRangeSelect?.({
|
|
97
|
+
start: selectedRange.from,
|
|
98
|
+
end: selectedRange.to,
|
|
99
|
+
});
|
|
100
|
+
// Only close if both dates are selected and closeOnSelect is true
|
|
101
|
+
if (closeOnSelect && selectedRange.from && selectedRange.to) {
|
|
102
|
+
setOpen(false);
|
|
103
|
+
}
|
|
104
|
+
} else {
|
|
105
|
+
onDateRangeSelect?.(undefined);
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
const handleClear = (e: React.MouseEvent) => {
|
|
110
|
+
e.stopPropagation();
|
|
111
|
+
onClear?.();
|
|
112
|
+
setOpen(false);
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
return (
|
|
116
|
+
<Popover open={open} onOpenChange={setOpen}>
|
|
117
|
+
<div
|
|
118
|
+
className={cn(
|
|
119
|
+
open && 'shadow-border-interactive-with-active',
|
|
120
|
+
dateTimeRangePickerVariants({variant, size, state: isDisabled ? 'disabled' : state}),
|
|
121
|
+
className,
|
|
122
|
+
)}
|
|
123
|
+
>
|
|
124
|
+
{/* Calendar Icon Button */}
|
|
125
|
+
<PopoverTrigger asChild>
|
|
126
|
+
<button
|
|
127
|
+
type="button"
|
|
128
|
+
disabled={isDisabled}
|
|
129
|
+
className={cn(
|
|
130
|
+
'flex items-center justify-center shrink-0 transition-colors',
|
|
131
|
+
size === 'small' ? 'size-28' : 'size-32',
|
|
132
|
+
isDisabled && 'text-foreground-neutral-disabled',
|
|
133
|
+
)}
|
|
134
|
+
aria-label="Open calendar"
|
|
135
|
+
>
|
|
136
|
+
{leftIcon || (
|
|
137
|
+
<Icon
|
|
138
|
+
name="calendar2Line"
|
|
139
|
+
className={cn(
|
|
140
|
+
'size-16 text-foreground-neutral-muted',
|
|
141
|
+
isDisabled && 'text-foreground-neutral-disabled',
|
|
142
|
+
)}
|
|
143
|
+
/>
|
|
144
|
+
)}
|
|
145
|
+
</button>
|
|
146
|
+
</PopoverTrigger>
|
|
147
|
+
|
|
148
|
+
{/* Divider */}
|
|
149
|
+
<div className="h-full w-px bg-border-neutral-base shrink-0" />
|
|
150
|
+
|
|
151
|
+
{/* Input Field */}
|
|
152
|
+
<input
|
|
153
|
+
ref={ref}
|
|
154
|
+
type="text"
|
|
155
|
+
disabled={isDisabled}
|
|
156
|
+
placeholder={placeholder}
|
|
157
|
+
value={displayValue}
|
|
158
|
+
readOnly
|
|
159
|
+
className={cn(
|
|
160
|
+
'flex-1 min-w-0 px-8 text-sm leading-20 bg-transparent outline-none border-none cursor-pointer',
|
|
161
|
+
'placeholder:text-foreground-neutral-muted',
|
|
162
|
+
'text-foreground-neutral-base',
|
|
163
|
+
'disabled:text-foreground-neutral-disabled disabled:cursor-not-allowed',
|
|
164
|
+
size === 'small' ? 'py-4' : 'py-6',
|
|
165
|
+
)}
|
|
166
|
+
onClick={() => !isDisabled && setOpen(true)}
|
|
167
|
+
{...props}
|
|
168
|
+
/>
|
|
169
|
+
|
|
170
|
+
{/* Clear Button (shown when date range is selected) */}
|
|
171
|
+
<button
|
|
172
|
+
type="button"
|
|
173
|
+
onClick={handleClear}
|
|
174
|
+
className={cn(
|
|
175
|
+
'flex items-center justify-center shrink-0 transition-colors hover:text-foreground-neutral-base',
|
|
176
|
+
size === 'small' ? 'size-28' : 'size-32',
|
|
177
|
+
hasRange && onClear && !isDisabled ? 'visible' : 'invisible',
|
|
178
|
+
)}
|
|
179
|
+
aria-label="Clear date range"
|
|
180
|
+
>
|
|
181
|
+
<Icon name="closeLine" className="size-16 text-foreground-neutral-muted" />
|
|
182
|
+
</button>
|
|
183
|
+
|
|
184
|
+
{/* Custom Right Icon */}
|
|
185
|
+
{rightIcon && !hasRange && (
|
|
186
|
+
<div
|
|
187
|
+
className={cn(
|
|
188
|
+
'flex items-center justify-center shrink-0',
|
|
189
|
+
size === 'small' ? 'size-28' : 'size-32',
|
|
190
|
+
)}
|
|
191
|
+
>
|
|
192
|
+
{rightIcon}
|
|
193
|
+
</div>
|
|
194
|
+
)}
|
|
195
|
+
</div>
|
|
196
|
+
|
|
197
|
+
<PopoverContent className="w-auto p-0" align="start">
|
|
198
|
+
<Calendar
|
|
199
|
+
mode="range"
|
|
200
|
+
defaultMonth={dayPickerRange?.from}
|
|
201
|
+
selected={dayPickerRange}
|
|
202
|
+
onSelect={handleSelect}
|
|
203
|
+
numberOfMonths={numberOfMonths}
|
|
204
|
+
/>
|
|
205
|
+
</PopoverContent>
|
|
206
|
+
</Popover>
|
|
207
|
+
);
|
|
208
|
+
},
|
|
209
|
+
);
|
|
210
|
+
|
|
211
|
+
DateTimeRangePicker.displayName = 'DateTimeRangePicker';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './date-time-range-picker';
|
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
import {argosScreenshot} from '@argos-ci/storybook/vitest';
|
|
2
|
+
import type {Meta, StoryObj} from '@storybook/react';
|
|
3
|
+
import {screen, within} from '@testing-library/react';
|
|
4
|
+
import type {UserEvent} from '@testing-library/user-event';
|
|
5
|
+
import userEvent from '@testing-library/user-event';
|
|
6
|
+
import {useState} from 'react';
|
|
7
|
+
import {Avatar} from '../avatar';
|
|
8
|
+
import {Button} from '../button';
|
|
9
|
+
import {
|
|
10
|
+
DropdownMenu,
|
|
11
|
+
DropdownMenuCheckboxItem,
|
|
12
|
+
DropdownMenuContent,
|
|
13
|
+
DropdownMenuGroup,
|
|
14
|
+
DropdownMenuItem,
|
|
15
|
+
DropdownMenuLabel,
|
|
16
|
+
DropdownMenuRadioGroup,
|
|
17
|
+
DropdownMenuRadioItem,
|
|
18
|
+
DropdownMenuSeparator,
|
|
19
|
+
DropdownMenuSub,
|
|
20
|
+
DropdownMenuSubContent,
|
|
21
|
+
DropdownMenuSubTrigger,
|
|
22
|
+
DropdownMenuTrigger,
|
|
23
|
+
} from './dropdown-menu';
|
|
24
|
+
|
|
25
|
+
const OPEN_MENU_REGEX = /open menu/i;
|
|
26
|
+
const ACTIONS_REGEX = /actions/i;
|
|
27
|
+
const ORGANIZATION_REGEX = /organization/i;
|
|
28
|
+
const COMPLETE_MENU_REGEX = /complete menu/i;
|
|
29
|
+
const SWITCH_ORGANIZATION_REGEX = /switch organization/i;
|
|
30
|
+
|
|
31
|
+
const isTestEnvironment = () => typeof navigator !== 'undefined' && navigator.webdriver === true;
|
|
32
|
+
|
|
33
|
+
type StoryContext = Parameters<NonNullable<Story['play']>>[0];
|
|
34
|
+
type AdditionalStepsCallback = (ctx: StoryContext, user: UserEvent) => Promise<void>;
|
|
35
|
+
|
|
36
|
+
async function openMenuAndScreenshot(
|
|
37
|
+
ctx: StoryContext,
|
|
38
|
+
triggerRegex: RegExp,
|
|
39
|
+
screenshotName: string,
|
|
40
|
+
additionalSteps?: AdditionalStepsCallback,
|
|
41
|
+
): Promise<void> {
|
|
42
|
+
const {canvasElement, step} = ctx;
|
|
43
|
+
const canvas = within(canvasElement);
|
|
44
|
+
const user = userEvent.setup();
|
|
45
|
+
|
|
46
|
+
let triggerButton: HTMLElement | null = null;
|
|
47
|
+
|
|
48
|
+
await step('Open the dropdown menu', async () => {
|
|
49
|
+
triggerButton = canvas.getByRole('button', {name: triggerRegex});
|
|
50
|
+
await user.click(triggerButton);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
await step('Wait for menu to appear and render', async () => {
|
|
54
|
+
await screen.findByRole('menu');
|
|
55
|
+
await new Promise((resolve) => setTimeout(resolve, 300));
|
|
56
|
+
|
|
57
|
+
if (isTestEnvironment() && triggerButton instanceof HTMLElement) {
|
|
58
|
+
triggerButton.style.display = 'none';
|
|
59
|
+
}
|
|
60
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
await argosScreenshot(ctx, screenshotName);
|
|
64
|
+
|
|
65
|
+
if (additionalSteps) {
|
|
66
|
+
await additionalSteps(ctx, user);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const meta = {
|
|
71
|
+
title: 'Components/DropdownMenu',
|
|
72
|
+
component: DropdownMenuContent,
|
|
73
|
+
subcomponents: {
|
|
74
|
+
DropdownMenu,
|
|
75
|
+
DropdownMenuTrigger,
|
|
76
|
+
DropdownMenuItem,
|
|
77
|
+
DropdownMenuCheckboxItem,
|
|
78
|
+
DropdownMenuRadioGroup,
|
|
79
|
+
DropdownMenuRadioItem,
|
|
80
|
+
DropdownMenuLabel,
|
|
81
|
+
DropdownMenuSeparator,
|
|
82
|
+
DropdownMenuGroup,
|
|
83
|
+
DropdownMenuSub,
|
|
84
|
+
DropdownMenuSubTrigger,
|
|
85
|
+
DropdownMenuSubContent,
|
|
86
|
+
},
|
|
87
|
+
parameters: {
|
|
88
|
+
layout: 'centered',
|
|
89
|
+
},
|
|
90
|
+
tags: ['autodocs'],
|
|
91
|
+
argTypes: {
|
|
92
|
+
side: {
|
|
93
|
+
control: 'select',
|
|
94
|
+
options: ['top', 'right', 'bottom', 'left'],
|
|
95
|
+
description: 'The preferred side of the trigger to render against',
|
|
96
|
+
table: {defaultValue: {summary: 'bottom'}},
|
|
97
|
+
},
|
|
98
|
+
align: {
|
|
99
|
+
control: 'select',
|
|
100
|
+
options: ['start', 'center', 'end'],
|
|
101
|
+
description: 'The preferred alignment against the trigger',
|
|
102
|
+
table: {defaultValue: {summary: 'start'}},
|
|
103
|
+
},
|
|
104
|
+
sideOffset: {
|
|
105
|
+
control: {type: 'number', min: 0, max: 20, step: 1},
|
|
106
|
+
description: 'Distance in pixels from the trigger',
|
|
107
|
+
table: {defaultValue: {summary: '4'}},
|
|
108
|
+
},
|
|
109
|
+
alignOffset: {
|
|
110
|
+
control: {type: 'number', min: -20, max: 20, step: 1},
|
|
111
|
+
description: 'Offset in pixels from the alignment edge',
|
|
112
|
+
table: {defaultValue: {summary: '0'}},
|
|
113
|
+
},
|
|
114
|
+
size: {
|
|
115
|
+
control: 'select',
|
|
116
|
+
options: ['sm', 'md', 'lg'],
|
|
117
|
+
description: 'Size variant of the dropdown content',
|
|
118
|
+
table: {defaultValue: {summary: 'md'}},
|
|
119
|
+
},
|
|
120
|
+
},
|
|
121
|
+
args: {
|
|
122
|
+
side: 'bottom',
|
|
123
|
+
align: 'center',
|
|
124
|
+
sideOffset: 8,
|
|
125
|
+
alignOffset: 0,
|
|
126
|
+
size: 'md',
|
|
127
|
+
},
|
|
128
|
+
} satisfies Meta<typeof DropdownMenuContent>;
|
|
129
|
+
|
|
130
|
+
export default meta;
|
|
131
|
+
type Story = StoryObj<typeof meta>;
|
|
132
|
+
|
|
133
|
+
export const Default: Story = {
|
|
134
|
+
play: (ctx) => openMenuAndScreenshot(ctx, OPEN_MENU_REGEX, 'DropdownMenu Default Open'),
|
|
135
|
+
render: function DefaultStory(args) {
|
|
136
|
+
const [container, setContainer] = useState<HTMLDivElement | null>(null);
|
|
137
|
+
|
|
138
|
+
return (
|
|
139
|
+
<div
|
|
140
|
+
ref={setContainer}
|
|
141
|
+
className="relative flex h-500 w-500 items-center justify-center rounded-16 bg-background-subtle-base shadow-tooltip overflow-visible"
|
|
142
|
+
>
|
|
143
|
+
{container && (
|
|
144
|
+
<DropdownMenu>
|
|
145
|
+
<DropdownMenuTrigger asChild>
|
|
146
|
+
<Button variant="secondary">Open Menu</Button>
|
|
147
|
+
</DropdownMenuTrigger>
|
|
148
|
+
<DropdownMenuContent {...args} container={container} side="bottom" align="center">
|
|
149
|
+
<DropdownMenuItem icon="editLine">Edit</DropdownMenuItem>
|
|
150
|
+
<DropdownMenuItem icon="addLine">Add</DropdownMenuItem>
|
|
151
|
+
<DropdownMenuSeparator />
|
|
152
|
+
<DropdownMenuItem icon="deleteBinLine">Delete</DropdownMenuItem>
|
|
153
|
+
</DropdownMenuContent>
|
|
154
|
+
</DropdownMenu>
|
|
155
|
+
)}
|
|
156
|
+
</div>
|
|
157
|
+
);
|
|
158
|
+
},
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
export const WithShortcuts: Story = {
|
|
162
|
+
play: (ctx) => openMenuAndScreenshot(ctx, ACTIONS_REGEX, 'DropdownMenu With Shortcuts Open'),
|
|
163
|
+
render: function WithShortcutsStory(args) {
|
|
164
|
+
const [container, setContainer] = useState<HTMLDivElement | null>(null);
|
|
165
|
+
|
|
166
|
+
return (
|
|
167
|
+
<div
|
|
168
|
+
ref={setContainer}
|
|
169
|
+
className="relative flex h-500 w-500 items-center justify-center rounded-16 bg-background-subtle-base shadow-tooltip overflow-visible"
|
|
170
|
+
>
|
|
171
|
+
{container && (
|
|
172
|
+
<DropdownMenu>
|
|
173
|
+
<DropdownMenuTrigger asChild>
|
|
174
|
+
<Button variant="secondary">Actions</Button>
|
|
175
|
+
</DropdownMenuTrigger>
|
|
176
|
+
<DropdownMenuContent {...args} container={container} side="bottom" align="center">
|
|
177
|
+
<DropdownMenuItem icon="fileCopyLine" shortcut="⌘C">
|
|
178
|
+
Copy
|
|
179
|
+
</DropdownMenuItem>
|
|
180
|
+
<DropdownMenuItem icon="clipboardLine" shortcut="⌘V">
|
|
181
|
+
Paste
|
|
182
|
+
</DropdownMenuItem>
|
|
183
|
+
<DropdownMenuSeparator />
|
|
184
|
+
<DropdownMenuItem icon="deleteBinLine" shortcut="⌘⌫">
|
|
185
|
+
Delete
|
|
186
|
+
</DropdownMenuItem>
|
|
187
|
+
</DropdownMenuContent>
|
|
188
|
+
</DropdownMenu>
|
|
189
|
+
)}
|
|
190
|
+
</div>
|
|
191
|
+
);
|
|
192
|
+
},
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
function UserProfileSection() {
|
|
196
|
+
return (
|
|
197
|
+
<div className="flex items-center gap-8 px-8 py-6">
|
|
198
|
+
<Avatar
|
|
199
|
+
size="sm"
|
|
200
|
+
content="image"
|
|
201
|
+
src="https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?w=100&h=100&fit=crop&crop=faces"
|
|
202
|
+
fallback="John Doe"
|
|
203
|
+
/>
|
|
204
|
+
<div className="flex flex-col">
|
|
205
|
+
<span className="text-sm font-medium leading-20 text-foreground-neutral-base">
|
|
206
|
+
John Doe
|
|
207
|
+
</span>
|
|
208
|
+
<span className="text-xs leading-16 text-foreground-neutral-muted">john@example.com</span>
|
|
209
|
+
</div>
|
|
210
|
+
</div>
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function OrganizationItem() {
|
|
215
|
+
return (
|
|
216
|
+
<div className="flex items-center gap-8 px-8 py-6">
|
|
217
|
+
<Avatar size="3xs" content="logo" logoName="stripe" radius="rounded" />
|
|
218
|
+
<span className="text-sm leading-20 text-foreground-neutral-subtle">
|
|
219
|
+
Stripe's organization
|
|
220
|
+
</span>
|
|
221
|
+
</div>
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
export const OrganizationMenu: Story = {
|
|
226
|
+
args: {
|
|
227
|
+
size: 'md',
|
|
228
|
+
},
|
|
229
|
+
play: (ctx) =>
|
|
230
|
+
openMenuAndScreenshot(
|
|
231
|
+
ctx,
|
|
232
|
+
ORGANIZATION_REGEX,
|
|
233
|
+
'DropdownMenu Organization Menu Open',
|
|
234
|
+
async ({step}, user) => {
|
|
235
|
+
await step('Hover over submenu trigger', async () => {
|
|
236
|
+
const submenuTrigger = screen.getByRole('menuitem', {name: SWITCH_ORGANIZATION_REGEX});
|
|
237
|
+
await user.hover(submenuTrigger);
|
|
238
|
+
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
await argosScreenshot(ctx, 'DropdownMenu Organization Menu Submenu Open');
|
|
242
|
+
},
|
|
243
|
+
),
|
|
244
|
+
render: function OrganizationMenuStory(args) {
|
|
245
|
+
const [container, setContainer] = useState<HTMLDivElement | null>(null);
|
|
246
|
+
|
|
247
|
+
return (
|
|
248
|
+
<div
|
|
249
|
+
ref={setContainer}
|
|
250
|
+
className="relative flex h-600 w-600 items-center justify-center rounded-16 bg-background-subtle-base shadow-tooltip overflow-visible"
|
|
251
|
+
>
|
|
252
|
+
{container && (
|
|
253
|
+
<DropdownMenu>
|
|
254
|
+
<DropdownMenuTrigger asChild>
|
|
255
|
+
<Button variant="secondary" iconLeft="buildingLine">
|
|
256
|
+
Organization
|
|
257
|
+
</Button>
|
|
258
|
+
</DropdownMenuTrigger>
|
|
259
|
+
<DropdownMenuContent {...args} container={container} side="bottom" align="start">
|
|
260
|
+
<OrganizationItem />
|
|
261
|
+
<DropdownMenuSeparator />
|
|
262
|
+
<DropdownMenuItem icon="settings3Line">Settings</DropdownMenuItem>
|
|
263
|
+
<DropdownMenuSub>
|
|
264
|
+
<DropdownMenuSubTrigger icon="arrowLeftRightLine">
|
|
265
|
+
Switch organization
|
|
266
|
+
</DropdownMenuSubTrigger>
|
|
267
|
+
<DropdownMenuSubContent>
|
|
268
|
+
<DropdownMenuItem
|
|
269
|
+
icon="shipfox"
|
|
270
|
+
iconStyle="text-foreground-neutral-base"
|
|
271
|
+
className="text-foreground-neutral-base"
|
|
272
|
+
>
|
|
273
|
+
Shipfox
|
|
274
|
+
</DropdownMenuItem>
|
|
275
|
+
<DropdownMenuItem
|
|
276
|
+
icon="github"
|
|
277
|
+
iconStyle="text-foreground-neutral-base"
|
|
278
|
+
className="text-foreground-neutral-base"
|
|
279
|
+
>
|
|
280
|
+
Github
|
|
281
|
+
</DropdownMenuItem>
|
|
282
|
+
<DropdownMenuItem
|
|
283
|
+
icon="google"
|
|
284
|
+
iconStyle="text-foreground-neutral-base"
|
|
285
|
+
className="text-foreground-neutral-base"
|
|
286
|
+
>
|
|
287
|
+
Google
|
|
288
|
+
</DropdownMenuItem>
|
|
289
|
+
</DropdownMenuSubContent>
|
|
290
|
+
</DropdownMenuSub>
|
|
291
|
+
<DropdownMenuItem icon="addLine">New organization</DropdownMenuItem>
|
|
292
|
+
</DropdownMenuContent>
|
|
293
|
+
</DropdownMenu>
|
|
294
|
+
)}
|
|
295
|
+
</div>
|
|
296
|
+
);
|
|
297
|
+
},
|
|
298
|
+
};
|
|
299
|
+
|
|
300
|
+
export const CompleteExample: Story = {
|
|
301
|
+
args: {
|
|
302
|
+
size: 'lg',
|
|
303
|
+
side: 'bottom',
|
|
304
|
+
},
|
|
305
|
+
play: (ctx) =>
|
|
306
|
+
openMenuAndScreenshot(ctx, COMPLETE_MENU_REGEX, 'DropdownMenu Complete Example Open'),
|
|
307
|
+
render: function CompleteExampleStory(args) {
|
|
308
|
+
const [container, setContainer] = useState<HTMLDivElement | null>(null);
|
|
309
|
+
const [showNotifications, setShowNotifications] = useState(true);
|
|
310
|
+
const [showBadges, setShowBadges] = useState(false);
|
|
311
|
+
const [theme, setTheme] = useState('dark');
|
|
312
|
+
|
|
313
|
+
return (
|
|
314
|
+
<div
|
|
315
|
+
ref={setContainer}
|
|
316
|
+
className="relative flex h-600 w-500 items-center justify-center rounded-16 bg-background-subtle-base shadow-tooltip overflow-visible"
|
|
317
|
+
>
|
|
318
|
+
{container && (
|
|
319
|
+
<DropdownMenu>
|
|
320
|
+
<DropdownMenuTrigger asChild>
|
|
321
|
+
<Button variant="secondary" iconLeft="menu3Line">
|
|
322
|
+
Complete Menu
|
|
323
|
+
</Button>
|
|
324
|
+
</DropdownMenuTrigger>
|
|
325
|
+
<DropdownMenuContent
|
|
326
|
+
{...args}
|
|
327
|
+
className="w-260"
|
|
328
|
+
container={container}
|
|
329
|
+
side="bottom"
|
|
330
|
+
align="start"
|
|
331
|
+
>
|
|
332
|
+
<UserProfileSection />
|
|
333
|
+
<DropdownMenuSeparator />
|
|
334
|
+
|
|
335
|
+
<DropdownMenuLabel>Actions</DropdownMenuLabel>
|
|
336
|
+
<DropdownMenuGroup>
|
|
337
|
+
<DropdownMenuItem icon="sparklingLine">Getting started</DropdownMenuItem>
|
|
338
|
+
<DropdownMenuItem icon="userLine">Profile settings</DropdownMenuItem>
|
|
339
|
+
</DropdownMenuGroup>
|
|
340
|
+
|
|
341
|
+
<DropdownMenuSeparator />
|
|
342
|
+
|
|
343
|
+
<DropdownMenuLabel>Preferences</DropdownMenuLabel>
|
|
344
|
+
<DropdownMenuGroup>
|
|
345
|
+
<DropdownMenuCheckboxItem
|
|
346
|
+
checked={showNotifications}
|
|
347
|
+
onCheckedChange={setShowNotifications}
|
|
348
|
+
closeOnSelect={false}
|
|
349
|
+
>
|
|
350
|
+
Show notifications
|
|
351
|
+
</DropdownMenuCheckboxItem>
|
|
352
|
+
<DropdownMenuCheckboxItem
|
|
353
|
+
checked={showBadges}
|
|
354
|
+
onCheckedChange={setShowBadges}
|
|
355
|
+
closeOnSelect={false}
|
|
356
|
+
>
|
|
357
|
+
Show badges
|
|
358
|
+
</DropdownMenuCheckboxItem>
|
|
359
|
+
</DropdownMenuGroup>
|
|
360
|
+
|
|
361
|
+
<DropdownMenuSeparator />
|
|
362
|
+
|
|
363
|
+
<DropdownMenuLabel>Theme</DropdownMenuLabel>
|
|
364
|
+
<DropdownMenuRadioGroup value={theme} onValueChange={setTheme}>
|
|
365
|
+
<DropdownMenuRadioItem value="light" closeOnSelect={false}>
|
|
366
|
+
Light
|
|
367
|
+
</DropdownMenuRadioItem>
|
|
368
|
+
<DropdownMenuRadioItem value="dark" closeOnSelect={false}>
|
|
369
|
+
Dark
|
|
370
|
+
</DropdownMenuRadioItem>
|
|
371
|
+
<DropdownMenuRadioItem value="system" closeOnSelect={false}>
|
|
372
|
+
System
|
|
373
|
+
</DropdownMenuRadioItem>
|
|
374
|
+
</DropdownMenuRadioGroup>
|
|
375
|
+
|
|
376
|
+
<DropdownMenuSeparator />
|
|
377
|
+
<DropdownMenuItem icon="logoutCircleLine">Log out</DropdownMenuItem>
|
|
378
|
+
</DropdownMenuContent>
|
|
379
|
+
</DropdownMenu>
|
|
380
|
+
)}
|
|
381
|
+
</div>
|
|
382
|
+
);
|
|
383
|
+
},
|
|
384
|
+
};
|