Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Calendar component #1325

Merged
merged 5 commits into from
Dec 5, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions packages/components/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,6 @@
"@vtex/shoreline-cli": "workspace",
"@vtex/shoreline-preset-admin": "workspace",
"@vtex/shoreline-theme": "workspace",
"@vtex/shoreline-utils": "^0.0.1-dev.0",
"match-sorter": "6.3.1",
"postcss": "8.4.31",
"postcss-preset-env": "9.3.0",
Expand All @@ -58,11 +57,12 @@
"@react-aria/checkbox": "3.11.2",
"@react-aria/focus": "3.14.3",
"@react-aria/interactions": "3.19.1",
"@react-aria/i18n": "3.9.0",
"@react-stately/toggle": "3.6.3",
"@tanstack/react-table": "8.10.7",
"@tanstack/react-virtual": "3.0.0-beta.68",
"@vtex/shoreline-icons": "^0.2.3",
"@vtex/shoreline-utils": "^0.5.1",
"@vtex/shoreline-icons": "workspace",
"@vtex/shoreline-utils": "workspace",
"react-hot-toast": "2.4.1",
"react-virtual": "^2.10.4",
"tiny-invariant": "1.3.1"
Expand Down
6 changes: 5 additions & 1 deletion packages/components/src/locale/locale-provider.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { ReactNode } from 'react'
import React from 'react'
import { I18nProvider } from '@react-aria/i18n'
import { LocaleContext } from './locale-context'

/**
Expand All @@ -13,7 +14,10 @@ export function LocaleProvider(props: LocaleProviderProps) {
const { locale = 'en-US', children } = props

return (
<LocaleContext.Provider value={locale}>{children}</LocaleContext.Provider>
<LocaleContext.Provider value={locale}>
{/** Some react-aria components require this to translate */}
<I18nProvider locale={locale}>{children}</I18nProvider>
Comment on lines +18 to +19
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't we use their provider instead then? Or maybe port whatever makes this required to our own locale provider? It seems to me that it does the same job we're doing here.

Or maybe just use the provider from react-aria on the components that require it.

Ref: https://github.com/adobe/react-spectrum/blob/main/packages/%40react-aria/i18n/src/context.tsx

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've tried using it on each component, but it caused DX issues since it must wrap the hooks. Though in using the locale instead of wrapping it - buuut it would make the lib stuck on react-aria to do intl even in non-react-aria components. So, I've decided to plug into the LocaleProvider :)

</LocaleContext.Provider>
)
}

Expand Down
2 changes: 0 additions & 2 deletions packages/date/.gitignore

This file was deleted.

6 changes: 5 additions & 1 deletion packages/date/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,9 +46,13 @@
"postcss-preset-env": "9.3.0"
},
"dependencies": {
"@vtex/shoreline-icons": "workspace",
"@vtex/shoreline-utils": "workspace",
"@vtex/shoreline-store": "workspace",
"@internationalized/date": "3.5.0",
"@react-aria/calendar": "3.5.3",
"@react-aria/datepicker": "3.9.0",
"@react-stately/datepicker": "3.9.0",
"@vtex/shoreline-utils": "workspace"
"@react-stately/calendar": "3.4.2"
}
}
26 changes: 26 additions & 0 deletions packages/date/src/calendar/calendar-cell.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
@layer sl-extended-components {
[data-sl-calendar-cell-button] {
width: 100%;
height: 100%;

&[data-selected='true'] {
background: var(--sl-bg-accent-strong);
color: var(--sl-fg-inverted);

&:hover,
&:focus {
background: var(--sl-bg-accent-strong-hover);
color: var(--sl-fg-inverted);
}

&:active {
background: var(--sl-bg-accent-strong-pressed);
color: var(--sl-fg-inverted);
}

&:focus-visible {
box-shadow: var(--sl-focus-ring-accent);
}
}
}
}
53 changes: 53 additions & 0 deletions packages/date/src/calendar/calendar-cell.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import React, { useRef } from 'react'
import { useCalendarCell } from '@react-aria/calendar'
import { IconButton } from '@vtex/shoreline-components'
import type { CalendarDate } from '@internationalized/date'

import './calendar-cell.css'
import { useCalendarContext } from './calendar-provider'

/**
* Cell of a calendar grid
*/
export function CalendarCell(props: CalendarCellProps) {
const { date } = props
const ref = useRef(null)
const store = useCalendarContext()

const {
cellProps,
buttonProps,
isSelected,
isOutsideVisibleRange,
isDisabled,
isUnavailable,
formattedDate,
isFocused,
} = useCalendarCell({ date }, store.state, ref)

return (
<td data-sl-calendar-cell {...cellProps}>
<IconButton
{...buttonProps}
ref={ref}
variant="tertiary"
data-sl-calendar-cell-button
hidden={isOutsideVisibleRange}
data-selected={isSelected}
data-disabled={isDisabled}
data-unavailable={isUnavailable}
data-focused={isFocused}
label={formattedDate}
>
<span>{formattedDate}</span>
</IconButton>
</td>
)
}

interface CalendarCellProps {
/**
* Date that the cell represents
*/
date: CalendarDate
marcelovicentegc marked this conversation as resolved.
Show resolved Hide resolved
}
17 changes: 17 additions & 0 deletions packages/date/src/calendar/calendar-grid.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
@layer sl-components {
[data-sl-calendar-grid] {
display: grid;
grid-template-columns: repeat(7, 1fr);

& > thead,
tbody,
tr {
display: contents;
}
}

[data-sl-calendar-grid-header] {
font: var(--sl-text-body-font);
letter-spacing: var(--sl-text-body-letter-spacing);
}
}
52 changes: 52 additions & 0 deletions packages/date/src/calendar/calendar-grid.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import React from 'react'
import type { AriaCalendarGridProps } from '@react-aria/calendar'
import { useCalendarGrid } from '@react-aria/calendar'
import { useLocale } from '@vtex/shoreline-components'

import { getWeeksInMonth } from '../utils'
import { CalendarCell } from './calendar-cell'
import { useCalendarContext } from './calendar-provider'
import './calendar-grid.css'

/**
* Grid of a calendar
*/
export function CalendarGrid(props: CalendarGridProps) {
const locale = useLocale()
const store = useCalendarContext()
const { gridProps, headerProps, weekDays } = useCalendarGrid(
props,
store.state
)

const weeksInMonth = getWeeksInMonth(store.state.visibleRange.start, locale)

return (
<table data-sl-calendar-grid {...gridProps}>
<thead data-sl-calendar-grid-header {...headerProps}>
<tr>
{weekDays.map((day, index) => (
<th key={index}>{day}</th>
))}
</tr>
</thead>
<tbody>
{[...new Array(weeksInMonth).keys()].map((weekIndex) => (
<tr key={weekIndex}>
{store.state
.getDatesInWeek(weekIndex)
.map((date: any, i: number) =>
date ? (
<CalendarCell key={i} date={date} />
) : (
<td data-sl-calendar-cell key={i} />
)
)}
</tr>
))}
</tbody>
</table>
)
}

export type CalendarGridProps = AriaCalendarGridProps
22 changes: 22 additions & 0 deletions packages/date/src/calendar/calendar-provider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import type { CalendarState } from '@react-stately/calendar'
import React, { createContext, useContext } from 'react'
import type { Store } from '@vtex/shoreline-store'
import { invariant } from '@vtex/shoreline-utils'

export const CalendarContext = createContext<Store<CalendarState> | null>(null)

export function CalendarProvider({ store, children }: any) {
return (
<CalendarContext.Provider value={store}>
{children}
</CalendarContext.Provider>
)
}

export function useCalendarContext() {
const context = useContext(CalendarContext)

invariant(context, 'Calendar components must be wrapped by CalendarProvider')

return context
}
23 changes: 23 additions & 0 deletions packages/date/src/calendar/calendar-store.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import type { CalendarDate } from '@internationalized/date'
import { createCalendar } from '@internationalized/date'
import type { CalendarStateOptions } from '@react-stately/calendar'
import { useCalendarState } from '@react-stately/calendar'
import { Store } from '@vtex/shoreline-store'
import { useMemo } from 'react'

/**
* Returns a calendar store
*/
export function useCalendarStore(props: UseCalendarStoreProps) {
const state = useCalendarState({
...props,
createCalendar,
})

return useMemo(() => new Store(state), [state])
}

export type UseCalendarStoreProps = Omit<
CalendarStateOptions<CalendarDate>,
'createCalendar'
>
19 changes: 19 additions & 0 deletions packages/date/src/calendar/calendar.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
@layer sl-components {
[data-sl-calendar] {
width: fit-content;
display: grid;
grid-template-rows: repeat(3, auto);
gap: var(--sl-space-gap);
}

[data-sl-calendar-header] {
display: flex;
align-items: center;
justify-content: space-between;
}

[data-sl-calendar-title] {
font: var(--sl-text-display-4-font);
letter-spacing: var(--sl-text-display-4-letter-spacing);
}
}
62 changes: 62 additions & 0 deletions packages/date/src/calendar/calendar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import React from 'react'
import type { AriaCalendarProps } from '@react-aria/calendar'
import { useCalendar } from '@react-aria/calendar'
import type { CalendarDate } from '@internationalized/date'
import { useLocale, IconButton } from '@vtex/shoreline-components'
import { IconCaretLeft, IconCaretRight } from '@vtex/shoreline-icons'

import { CalendarGrid } from './calendar-grid'
import { CalendarProvider } from './calendar-provider'
import { useCalendarStore } from './calendar-store'

import './calendar.css'

/**
* Allow users to select a date
* @example
* <Calendar />
*/
export function Calendar(props: CalendarProps) {
const locale = useLocale()
const store = useCalendarStore({
...props,
locale,
})

const { calendarProps, prevButtonProps, nextButtonProps, title } =
useCalendar(props, store.state)

return (
<CalendarProvider store={store}>
<div data-sl-calendar {...calendarProps}>
<div data-sl-calendar-header>
<IconButton
label={prevButtonProps['aria-label']}
variant="tertiary"
disabled={prevButtonProps.isDisabled}
onClick={prevButtonProps.onPress as any}
onFocus={prevButtonProps.onFocusChange as any}
>
<IconCaretLeft />
</IconButton>
<h2 data-sl-calendar-title>{title}</h2>
<IconButton
label={nextButtonProps['aria-label']}
variant="tertiary"
disabled={nextButtonProps.isDisabled}
onClick={nextButtonProps.onPress as any}
onFocus={nextButtonProps.onFocusChange as any}
>
<IconCaretRight />
</IconButton>
</div>
<CalendarGrid />
</div>
</CalendarProvider>
)
}

export type CalendarProps = Omit<
AriaCalendarProps<CalendarDate>,
'createCalendar' | 'locale'
>
1 change: 1 addition & 0 deletions packages/date/src/calendar/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './calendar'
49 changes: 49 additions & 0 deletions packages/date/src/calendar/stories/calendar.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import React, { useState } from 'react'
import { LocaleProvider } from '@vtex/shoreline-components'

import { Calendar } from '../index'
import { getLocalTimeZone, today } from '../../utils'

export default {
title: 'date/calendar',
}

export function Default() {
return <Calendar />
}

export function Controlled() {
const now = today(getLocalTimeZone())
const [value, setValue] = useState(now)
const [focusedValue, setFocusedValue] = useState(now)

return (
<>
<p>Selected Date: {value.toString()}</p>
<p>Focused Date: {focusedValue.toString()}</p>

<button
onClick={() => {
setValue(now)
setFocusedValue(now)
}}
>
Today
</button>
<Calendar
value={value}
onChange={setValue}
focusedValue={focusedValue}
onFocusChange={setFocusedValue}
/>
</>
)
}

export function Locale() {
return (
<LocaleProvider locale="ja-JP">
<Calendar />
</LocaleProvider>
)
}
Loading