Migrate to a monorepo structure (#2909)
This commit is contained in:
@ -0,0 +1,253 @@
|
||||
import React from 'react';
|
||||
import ReactDatePicker from 'react-datepicker';
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import { overlayBackground } from '@/ui/theme/constants/effects';
|
||||
|
||||
import 'react-datepicker/dist/react-datepicker.css';
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
& .react-datepicker {
|
||||
border-color: ${({ theme }) => theme.border.color.light};
|
||||
background: transparent;
|
||||
font-family: 'Inter';
|
||||
font-size: ${({ theme }) => theme.font.size.md};
|
||||
border: none;
|
||||
display: block;
|
||||
font-weight: ${({ theme }) => theme.font.weight.regular};
|
||||
}
|
||||
|
||||
& .react-datepicker-popper {
|
||||
position: relative !important;
|
||||
inset: auto !important;
|
||||
transform: none !important;
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
& .react-datepicker__triangle::after {
|
||||
display: none;
|
||||
}
|
||||
|
||||
& .react-datepicker__triangle::before {
|
||||
display: none;
|
||||
}
|
||||
|
||||
& .react-datepicker-wrapper {
|
||||
display: none;
|
||||
}
|
||||
|
||||
// Header
|
||||
|
||||
& .react-datepicker__header {
|
||||
background: transparent;
|
||||
border: none;
|
||||
}
|
||||
|
||||
& .react-datepicker__header__dropdown {
|
||||
display: flex;
|
||||
color: ${({ theme }) => theme.font.color.primary};
|
||||
margin-left: ${({ theme }) => theme.spacing(1)};
|
||||
margin-bottom: ${({ theme }) => theme.spacing(1)};
|
||||
}
|
||||
|
||||
& .react-datepicker__month-dropdown-container,
|
||||
& .react-datepicker__year-dropdown-container {
|
||||
text-align: left;
|
||||
border-radius: ${({ theme }) => theme.border.radius.sm};
|
||||
margin-left: ${({ theme }) => theme.spacing(1)};
|
||||
margin-right: 0;
|
||||
padding: ${({ theme }) => theme.spacing(2)};
|
||||
padding-right: ${({ theme }) => theme.spacing(4)};
|
||||
background-color: ${({ theme }) => theme.background.tertiary};
|
||||
}
|
||||
|
||||
& .react-datepicker__month-read-view--down-arrow,
|
||||
& .react-datepicker__year-read-view--down-arrow {
|
||||
height: 5px;
|
||||
width: 5px;
|
||||
border-width: 1px 1px 0 0;
|
||||
border-color: ${({ theme }) => theme.border.color.light};
|
||||
top: 3px;
|
||||
right: -6px;
|
||||
}
|
||||
|
||||
& .react-datepicker__year-read-view,
|
||||
& .react-datepicker__month-read-view {
|
||||
padding-right: ${({ theme }) => theme.spacing(2)};
|
||||
}
|
||||
|
||||
& .react-datepicker__month-dropdown-container {
|
||||
width: 80px;
|
||||
}
|
||||
|
||||
& .react-datepicker__year-dropdown-container {
|
||||
width: 50px;
|
||||
}
|
||||
|
||||
& .react-datepicker__month-dropdown,
|
||||
& .react-datepicker__year-dropdown {
|
||||
border: ${({ theme }) => theme.border.color.light};
|
||||
${overlayBackground}
|
||||
overflow-y: scroll;
|
||||
top: ${({ theme }) => theme.spacing(2)};
|
||||
}
|
||||
& .react-datepicker__month-dropdown {
|
||||
left: ${({ theme }) => theme.spacing(2)};
|
||||
width: 160px;
|
||||
height: 260px;
|
||||
}
|
||||
|
||||
& .react-datepicker__year-dropdown {
|
||||
left: calc(${({ theme }) => theme.spacing(9)} + 80px);
|
||||
width: 100px;
|
||||
height: 260px;
|
||||
}
|
||||
|
||||
& .react-datepicker__navigation--years {
|
||||
display: none;
|
||||
}
|
||||
|
||||
& .react-datepicker__month-option--selected,
|
||||
& .react-datepicker__year-option--selected {
|
||||
display: none;
|
||||
}
|
||||
|
||||
& .react-datepicker__year-option,
|
||||
& .react-datepicker__month-option {
|
||||
text-align: left;
|
||||
padding: ${({ theme }) => theme.spacing(2)}
|
||||
calc(${({ theme }) => theme.spacing(2)} - 2px);
|
||||
width: calc(100% - ${({ theme }) => theme.spacing(4)});
|
||||
border-radius: ${({ theme }) => theme.border.radius.xs};
|
||||
color: ${({ theme }) => theme.font.color.secondary};
|
||||
cursor: pointer;
|
||||
margin: 2px;
|
||||
|
||||
&:hover {
|
||||
background: ${({ theme }) => theme.background.transparent.light};
|
||||
}
|
||||
}
|
||||
|
||||
& .react-datepicker__year-option {
|
||||
&:first-of-type,
|
||||
&:last-of-type {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
& .react-datepicker__current-month {
|
||||
display: none;
|
||||
}
|
||||
|
||||
& .react-datepicker__day-name {
|
||||
color: ${({ theme }) => theme.font.color.secondary};
|
||||
width: 34px;
|
||||
height: 40px;
|
||||
line-height: 40px;
|
||||
}
|
||||
|
||||
& .react-datepicker__month-container {
|
||||
float: none;
|
||||
}
|
||||
|
||||
// Days
|
||||
|
||||
& .react-datepicker__month {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
& .react-datepicker__day {
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
line-height: 34px;
|
||||
}
|
||||
|
||||
& .react-datepicker__navigation--previous,
|
||||
& .react-datepicker__navigation--next {
|
||||
height: 34px;
|
||||
border-radius: ${({ theme }) => theme.border.radius.sm};
|
||||
padding-top: 6px;
|
||||
&:hover {
|
||||
background: ${({ theme }) => theme.background.transparent.light};
|
||||
}
|
||||
}
|
||||
& .react-datepicker__navigation--previous {
|
||||
right: 38px;
|
||||
top: 8px;
|
||||
left: auto;
|
||||
|
||||
& > span {
|
||||
margin-left: -6px;
|
||||
}
|
||||
}
|
||||
|
||||
& .react-datepicker__navigation--next {
|
||||
right: 6px;
|
||||
top: 8px;
|
||||
|
||||
& > span {
|
||||
margin-left: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
& .react-datepicker__navigation-icon::before {
|
||||
height: 7px;
|
||||
width: 7px;
|
||||
border-width: 1px 1px 0 0;
|
||||
border-color: ${({ theme }) => theme.font.color.tertiary};
|
||||
}
|
||||
|
||||
& .react-datepicker__day--keyboard-selected {
|
||||
background-color: inherit;
|
||||
}
|
||||
|
||||
& .react-datepicker__day,
|
||||
.react-datepicker__time-name {
|
||||
color: ${({ theme }) => theme.font.color.primary};
|
||||
}
|
||||
|
||||
& .react-datepicker__day--selected {
|
||||
background-color: ${({ theme }) => theme.color.blue};
|
||||
color: ${({ theme }) => theme.font.color.inverted};
|
||||
}
|
||||
|
||||
& .react-datepicker__day--outside-month {
|
||||
color: ${({ theme }) => theme.font.color.tertiary};
|
||||
}
|
||||
|
||||
& .react-datepicker__day:hover {
|
||||
color: ${({ theme }) => theme.font.color.tertiary};
|
||||
}
|
||||
`;
|
||||
|
||||
export type InternalDatePickerProps = {
|
||||
date: Date;
|
||||
onMouseSelect?: (date: Date) => void;
|
||||
onChange?: (date: Date) => void;
|
||||
};
|
||||
|
||||
export const InternalDatePicker = ({
|
||||
date,
|
||||
onChange,
|
||||
onMouseSelect,
|
||||
}: InternalDatePickerProps) => (
|
||||
<StyledContainer>
|
||||
<ReactDatePicker
|
||||
open={true}
|
||||
selected={date}
|
||||
showMonthDropdown
|
||||
showYearDropdown
|
||||
onChange={() => {
|
||||
// We need to use onSelect here but onChange is almost redundant with onSelect but is required
|
||||
}}
|
||||
customInput={<></>}
|
||||
onSelect={(date: Date, event) => {
|
||||
if (event?.type === 'click') {
|
||||
onMouseSelect?.(date);
|
||||
} else {
|
||||
onChange?.(date);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</StyledContainer>
|
||||
);
|
||||
@ -0,0 +1,48 @@
|
||||
import { Meta, StoryObj } from '@storybook/react';
|
||||
import { expect, userEvent, within } from '@storybook/test';
|
||||
|
||||
import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator';
|
||||
|
||||
import { InternalDatePicker } from '../InternalDatePicker';
|
||||
|
||||
const meta: Meta<typeof InternalDatePicker> = {
|
||||
title: 'UI/Input/Internal/InternalDatePicker',
|
||||
component: InternalDatePicker,
|
||||
decorators: [ComponentDecorator],
|
||||
argTypes: {
|
||||
date: { control: 'date' },
|
||||
},
|
||||
args: { date: new Date('January 1, 2023 00:00:00') },
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof InternalDatePicker>;
|
||||
|
||||
export const Default: Story = {};
|
||||
|
||||
export const WithOpenMonthSelect: Story = {
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
const monthSelect = canvas.getByText('January');
|
||||
|
||||
await userEvent.click(monthSelect);
|
||||
|
||||
expect(canvas.getAllByText('January')).toHaveLength(2);
|
||||
[
|
||||
'February',
|
||||
'March',
|
||||
'April',
|
||||
'May',
|
||||
'June',
|
||||
'July',
|
||||
'August',
|
||||
'September',
|
||||
'October',
|
||||
'November',
|
||||
'December',
|
||||
].forEach((monthLabel) =>
|
||||
expect(canvas.getByText(monthLabel)).toBeInTheDocument(),
|
||||
);
|
||||
},
|
||||
};
|
||||
@ -0,0 +1,148 @@
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { getCountries, getCountryCallingCode } from 'react-phone-number-input';
|
||||
import { useTheme } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
import { hasFlag } from 'country-flag-icons';
|
||||
import * as Flags from 'country-flag-icons/react/3x2';
|
||||
import { CountryCallingCode } from 'libphonenumber-js';
|
||||
|
||||
import { IconChevronDown } from '@/ui/display/icon';
|
||||
import { IconWorld } from '@/ui/input/constants/icons';
|
||||
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
|
||||
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
|
||||
import { DropdownScope } from '@/ui/layout/dropdown/scopes/DropdownScope';
|
||||
|
||||
import { CountryPickerHotkeyScope } from '../types/CountryPickerHotkeyScope';
|
||||
|
||||
import { CountryPickerDropdownSelect } from './CountryPickerDropdownSelect';
|
||||
|
||||
import 'react-phone-number-input/style.css';
|
||||
|
||||
type StyledDropdownButtonProps = {
|
||||
isUnfolded: boolean;
|
||||
};
|
||||
|
||||
export const StyledDropdownButtonContainer = styled.div<StyledDropdownButtonProps>`
|
||||
align-items: center;
|
||||
background: ${({ theme }) => theme.background.primary};
|
||||
border-radius: ${({ theme }) => theme.border.radius.xs};
|
||||
color: ${({ color }) => color ?? 'none'};
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
|
||||
height: 32px;
|
||||
|
||||
padding-left: ${({ theme }) => theme.spacing(2)};
|
||||
padding-right: ${({ theme }) => theme.spacing(2)};
|
||||
|
||||
padding-right: ${({ theme }) => theme.spacing(2)};
|
||||
user-select: none;
|
||||
|
||||
&:hover {
|
||||
filter: brightness(0.95);
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledIconContainer = styled.div`
|
||||
align-items: center;
|
||||
color: ${({ theme }) => theme.font.color.tertiary};
|
||||
display: flex;
|
||||
gap: ${({ theme }) => theme.spacing(1)};
|
||||
justify-content: center;
|
||||
|
||||
svg {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
height: 16px;
|
||||
justify-content: center;
|
||||
}
|
||||
`;
|
||||
|
||||
export type Country = {
|
||||
countryCode: string;
|
||||
countryName: string;
|
||||
callingCode: CountryCallingCode;
|
||||
Flag: Flags.FlagComponent;
|
||||
};
|
||||
|
||||
export const CountryPickerDropdownButton = ({
|
||||
value,
|
||||
onChange,
|
||||
}: {
|
||||
value: string;
|
||||
onChange: (countryCode: string) => void;
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
|
||||
const [selectedCountry, setSelectedCountry] = useState<Country>();
|
||||
|
||||
const { isDropdownOpen, closeDropdown } = useDropdown({
|
||||
dropdownScopeId: 'country-picker',
|
||||
});
|
||||
|
||||
const handleChange = (countryCode: string) => {
|
||||
onChange(countryCode);
|
||||
closeDropdown();
|
||||
};
|
||||
|
||||
const countries = useMemo<Country[]>(() => {
|
||||
const regionNamesInEnglish = new Intl.DisplayNames(['en'], {
|
||||
type: 'region',
|
||||
});
|
||||
|
||||
const countryCodes = getCountries();
|
||||
|
||||
return countryCodes.reduce<Country[]>((result, countryCode) => {
|
||||
const countryName = regionNamesInEnglish.of(countryCode);
|
||||
|
||||
if (!countryName) return result;
|
||||
|
||||
if (!hasFlag(countryCode)) return result;
|
||||
|
||||
const Flag = Flags[countryCode];
|
||||
|
||||
const callingCode = getCountryCallingCode(countryCode);
|
||||
|
||||
result.push({
|
||||
countryCode,
|
||||
countryName,
|
||||
callingCode,
|
||||
Flag,
|
||||
});
|
||||
|
||||
return result;
|
||||
}, []);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const country = countries.find(({ countryCode }) => countryCode === value);
|
||||
if (country) {
|
||||
setSelectedCountry(country);
|
||||
}
|
||||
}, [countries, value]);
|
||||
|
||||
return (
|
||||
<DropdownScope dropdownScopeId="country-picker">
|
||||
<Dropdown
|
||||
dropdownHotkeyScope={{ scope: CountryPickerHotkeyScope.CountryPicker }}
|
||||
clickableComponent={
|
||||
<StyledDropdownButtonContainer isUnfolded={isDropdownOpen}>
|
||||
<StyledIconContainer>
|
||||
{selectedCountry ? <selectedCountry.Flag /> : <IconWorld />}
|
||||
<IconChevronDown size={theme.icon.size.sm} />
|
||||
</StyledIconContainer>
|
||||
</StyledDropdownButtonContainer>
|
||||
}
|
||||
dropdownComponents={
|
||||
<CountryPickerDropdownSelect
|
||||
countries={countries}
|
||||
selectedCountry={selectedCountry}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
}
|
||||
dropdownPlacement="bottom-start"
|
||||
dropdownOffset={{ x: 0, y: 4 }}
|
||||
/>
|
||||
</DropdownScope>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,98 @@
|
||||
import { useMemo, useState } from 'react';
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import { DropdownMenu } from '@/ui/layout/dropdown/components/DropdownMenu';
|
||||
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
|
||||
import { DropdownMenuSearchInput } from '@/ui/layout/dropdown/components/DropdownMenuSearchInput';
|
||||
import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
|
||||
import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem';
|
||||
import { MenuItemSelectAvatar } from '@/ui/navigation/menu-item/components/MenuItemSelectAvatar';
|
||||
|
||||
import { Country } from './CountryPickerDropdownButton';
|
||||
|
||||
import 'react-phone-number-input/style.css';
|
||||
|
||||
const StyledIconContainer = styled.div`
|
||||
align-items: center;
|
||||
color: ${({ theme }) => theme.font.color.tertiary};
|
||||
display: flex;
|
||||
padding-right: ${({ theme }) => theme.spacing(1)};
|
||||
|
||||
svg {
|
||||
align-items: center;
|
||||
border-radius: ${({ theme }) => theme.border.radius.xs};
|
||||
display: flex;
|
||||
height: 12px;
|
||||
justify-content: center;
|
||||
}
|
||||
`;
|
||||
|
||||
export const CountryPickerDropdownSelect = ({
|
||||
countries,
|
||||
selectedCountry,
|
||||
onChange,
|
||||
}: {
|
||||
countries: Country[];
|
||||
selectedCountry?: Country;
|
||||
onChange: (countryCode: string) => void;
|
||||
}) => {
|
||||
const [searchFilter, setSearchFilter] = useState<string>('');
|
||||
|
||||
const filteredCountries = useMemo(
|
||||
() =>
|
||||
countries.filter(({ countryName }) =>
|
||||
countryName
|
||||
.toLocaleLowerCase()
|
||||
.includes(searchFilter.toLocaleLowerCase()),
|
||||
),
|
||||
[countries, searchFilter],
|
||||
);
|
||||
|
||||
return (
|
||||
<DropdownMenu width="200px" disableBlur>
|
||||
<DropdownMenuSearchInput
|
||||
value={searchFilter}
|
||||
onChange={(event) => setSearchFilter(event.currentTarget.value)}
|
||||
autoFocus
|
||||
/>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItemsContainer hasMaxHeight>
|
||||
{filteredCountries?.length === 0 ? (
|
||||
<MenuItem text="No result" />
|
||||
) : (
|
||||
<>
|
||||
{selectedCountry && (
|
||||
<MenuItemSelectAvatar
|
||||
key={selectedCountry.countryCode}
|
||||
selected={true}
|
||||
onClick={() => onChange(selectedCountry.countryCode)}
|
||||
text={`${selectedCountry.countryName} (+${selectedCountry.callingCode})`}
|
||||
avatar={
|
||||
<StyledIconContainer>
|
||||
<selectedCountry.Flag />
|
||||
</StyledIconContainer>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{filteredCountries.map(
|
||||
({ countryCode, countryName, callingCode, Flag }) =>
|
||||
selectedCountry?.countryCode === countryCode ? null : (
|
||||
<MenuItemSelectAvatar
|
||||
key={countryCode}
|
||||
selected={selectedCountry?.countryCode === countryCode}
|
||||
onClick={() => onChange(countryCode)}
|
||||
text={`${countryName} (+${callingCode})`}
|
||||
avatar={
|
||||
<StyledIconContainer>
|
||||
<Flag />
|
||||
</StyledIconContainer>
|
||||
}
|
||||
/>
|
||||
),
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</DropdownMenuItemsContainer>
|
||||
</DropdownMenu>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,3 @@
|
||||
export enum CountryPickerHotkeyScope {
|
||||
CountryPicker = 'country-picker',
|
||||
}
|
||||
Reference in New Issue
Block a user