-
Notifications
You must be signed in to change notification settings - Fork 1.7k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[UI v2] feat: Adds timezone component (#17101)
- Loading branch information
1 parent
039693f
commit ea7262e
Showing
5 changed files
with
180 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export { TimezoneSelect } from "./timezone-select"; |
17 changes: 17 additions & 0 deletions
17
ui-v2/src/components/ui/timezone-select/timezone-select.stories.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
import type { Meta, StoryObj } from "@storybook/react"; | ||
|
||
import { useState } from "react"; | ||
import { TimezoneSelect } from "./timezone-select"; | ||
|
||
const meta: Meta<typeof TimezoneSelect> = { | ||
title: "UI/TimezoneSelect", | ||
component: () => <TimezoneSelectStory />, | ||
}; | ||
export default meta; | ||
|
||
export const story: StoryObj = { name: "TimezoneSelect" }; | ||
|
||
const TimezoneSelectStory = () => { | ||
const [value, setValue] = useState("America/Los_Angeles"); | ||
return <TimezoneSelect selectedValue={value} onSelect={setValue} />; | ||
}; |
27 changes: 27 additions & 0 deletions
27
ui-v2/src/components/ui/timezone-select/timezone-select.test.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
import { render, screen } from "@testing-library/react"; | ||
import userEvent from "@testing-library/user-event"; | ||
import { mockPointerEvents } from "@tests/utils/browser"; | ||
import { expect, test, vi } from "vitest"; | ||
|
||
import { TimezoneSelect } from "./timezone-select"; | ||
|
||
test("TimezoneSelect can select an option", async () => { | ||
mockPointerEvents(); | ||
const user = userEvent.setup(); | ||
|
||
// ------------ Setup | ||
const mockOnSelectFn = vi.fn(); | ||
|
||
render(<TimezoneSelect onSelect={mockOnSelectFn} selectedValue="" />); | ||
|
||
// ------------ Act | ||
await user.click(screen.getByRole("combobox", { name: /select timezone/i })); | ||
|
||
expect(screen.getByText(/suggested timezones/i)).toBeVisible(); | ||
expect(screen.getByText(/all timezones/i)).toBeVisible(); | ||
|
||
await user.click(screen.getByRole("option", { name: /africa \/ abidjan/i })); | ||
|
||
// ------------ Assert | ||
expect(mockOnSelectFn).toBeCalledWith("Africa/Abidjan"); | ||
}); |
133 changes: 133 additions & 0 deletions
133
ui-v2/src/components/ui/timezone-select/timezone-select.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,133 @@ | ||
import { | ||
Combobox, | ||
ComboboxCommandEmtpy, | ||
ComboboxCommandGroup, | ||
ComboboxCommandInput, | ||
ComboboxCommandItem, | ||
ComboboxCommandList, | ||
ComboboxContent, | ||
ComboboxTrigger, | ||
} from "@/components/ui/combobox"; | ||
import { | ||
DropdownMenuLabel, | ||
DropdownMenuSeparator, | ||
} from "@/components/ui/dropdown-menu"; | ||
import { useDeferredValue, useMemo, useState } from "react"; | ||
|
||
function getTimezoneLabel(value: string): string { | ||
return value.replaceAll("/", " / ").replaceAll("_", " "); | ||
} | ||
|
||
const localTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone; | ||
|
||
const SUGGESTED_TIMEZONES = [ | ||
{ label: "UTC", value: "Etc/UTC" }, | ||
{ | ||
label: getTimezoneLabel(localTimezone), | ||
value: localTimezone, | ||
}, | ||
]; | ||
|
||
const TIMEZONES = Intl.supportedValuesOf("timeZone") | ||
.map((timezone) => ({ | ||
label: getTimezoneLabel(timezone), | ||
value: timezone, | ||
})) | ||
.slice(0, 5); | ||
|
||
const ALL_TIMEZONES = [...SUGGESTED_TIMEZONES, ...TIMEZONES]; | ||
|
||
type TimezoneSelectProps = { | ||
selectedValue: string | undefined; | ||
onSelect: (value: string) => void; | ||
}; | ||
|
||
export const TimezoneSelect = ({ | ||
selectedValue = "", | ||
onSelect, | ||
}: TimezoneSelectProps) => { | ||
const [search, setSearch] = useState(""); | ||
|
||
const deferredSearch = useDeferredValue(search); | ||
|
||
const filteredSuggestedTimezones = useMemo(() => { | ||
return SUGGESTED_TIMEZONES.filter( | ||
(timeZone) => | ||
timeZone.label.toLowerCase().includes(deferredSearch.toLowerCase()) || | ||
timeZone.value.toLowerCase().includes(deferredSearch.toLowerCase()), | ||
); | ||
}, [deferredSearch]); | ||
|
||
const filteredTimezones = useMemo(() => { | ||
return TIMEZONES.filter( | ||
(timeZone) => | ||
timeZone.label.toLowerCase().includes(deferredSearch.toLowerCase()) || | ||
timeZone.value.toLowerCase().includes(deferredSearch.toLowerCase()), | ||
); | ||
}, [deferredSearch]); | ||
|
||
const selectedTimezone = useMemo( | ||
() => ALL_TIMEZONES.find(({ value }) => value === selectedValue), | ||
[selectedValue], | ||
); | ||
|
||
return ( | ||
<Combobox> | ||
<ComboboxTrigger | ||
selected={Boolean(selectedValue)} | ||
aria-label="Select timezone" | ||
> | ||
{selectedTimezone?.label ?? "Select timezone"} | ||
</ComboboxTrigger> | ||
<ComboboxContent> | ||
<ComboboxCommandInput | ||
value={search} | ||
onValueChange={setSearch} | ||
placeholder="Search" | ||
/> | ||
<ComboboxCommandEmtpy>No timezone found</ComboboxCommandEmtpy> | ||
<ComboboxCommandList> | ||
<ComboboxCommandGroup> | ||
{filteredSuggestedTimezones.length > 0 && ( | ||
<DropdownMenuLabel>Suggested timezones</DropdownMenuLabel> | ||
)} | ||
{filteredSuggestedTimezones.map(({ label, value }) => { | ||
return ( | ||
<ComboboxCommandItem | ||
key={value} | ||
selected={selectedValue === value} | ||
onSelect={(value) => { | ||
onSelect(value); | ||
setSearch(""); | ||
}} | ||
value={value} | ||
> | ||
{label} | ||
</ComboboxCommandItem> | ||
); | ||
})} | ||
<DropdownMenuSeparator /> | ||
{filteredTimezones.length > 0 && ( | ||
<DropdownMenuLabel>All timezones</DropdownMenuLabel> | ||
)} | ||
{filteredTimezones.map(({ label, value }) => { | ||
return ( | ||
<ComboboxCommandItem | ||
key={value} | ||
selected={selectedValue === value} | ||
onSelect={(value) => { | ||
onSelect(value); | ||
setSearch(""); | ||
}} | ||
value={value} | ||
> | ||
{label} | ||
</ComboboxCommandItem> | ||
); | ||
})} | ||
</ComboboxCommandGroup> | ||
</ComboboxCommandList> | ||
</ComboboxContent> | ||
</Combobox> | ||
); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters