Skip to content
Closed
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
1 change: 0 additions & 1 deletion packages/neuron-ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,6 @@
"immer": "9.0.16",
"jsqr": "1.4.0",
"office-ui-fabric-react": "7.204.0",
"primereact": "8.7.1",
"qr.js": "0.0.0",
"react": "17.0.2",
"react-dom": "17.0.2",
Expand Down
90 changes: 90 additions & 0 deletions packages/neuron-ui/src/widgets/Calendar/calendar.module.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
@import '../../styles/mixin.scss';

@mixin button {
@include medium-text;
appearance: none;
cursor: pointer;
font-size: 0.75rem;
line-height: 1rem;
font-weight: 500;
padding: 7px 7px;
border: none;
border-radius: 2px;
margin: 0;
box-sizing: border-box;
border-radius: 2px;
background-color: transparent;
&:hover {
@include semi-bold-text;
background-color: #efefef;
}
&[disabled] {
cursor: none;
opacity: 0.5;
box-shadow: none !important;
pointer-events: none;
&:hover {
background-color: transparent;
}
}
}

.calendar {
width: 374px;
.calOptions {
width: 100%;
display: flex;
flex-wrap: wrap;
justify-content: space-between;
list-style-type: none;
list-style: none;
padding: 0;
button {
@include button;
width: 100px;
}
}
}

.calendarHeader {
display: flex;
justify-content: space-between;
border-bottom: 1px solid #eee;
margin-top: 10px;
margin-bottom: 10px;

.calPrev, .calNext {
@include button;
width: 30px;
}
.calTitle {
button {
@include button;
@include semi-bold-text;
height: 100%;
min-width: 100px;
margin-right: 10px;
}
}
}

.calendarTable {
width: 100%;

.calTableHeader {
@include semi-bold-text;
font-size: 13px;
}

.calDateItem {
@include button;
width: 100%;
&:global(.today) {
border: 1px solid var(--nervos-green-light);
margin: -1px;
}
&:global(.active) {
background-color: var(--nervos-green);
}
}
}
186 changes: 186 additions & 0 deletions packages/neuron-ui/src/widgets/Calendar/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
import React, { useState } from 'react'
import { getMonthCalendar, useLocalNames, monthInRange, yearInRange, dateEqual } from './utils'
import styles from './calendar.module.scss'

interface Option {
value: number
title: string
selectable: boolean
}
const Selector = ({ options, onChange }: { options: Option[]; onChange: (option: Option) => void }) => (
<ol className={styles.calOptions} role="menu">
{options.map(option => (
<li key={option.value} role="presentation">
<button
type="button"
aria-label={option.title}
title={option.title}
role="menuitem"
onClick={() => onChange(option)}
disabled={!option.selectable}
>
{option.title}
</button>
</li>
))}
</ol>
)

export interface CalendarProps {
value: Date | undefined
onChange: (value: Date) => void
minDate?: Date
maxDate?: Date
}
const Calendar: React.FC<CalendarProps> = ({ value, onChange, minDate = null, maxDate = null }) => {
const today = new Date()

const [year, setYear] = useState(value === undefined ? today.getFullYear() : value.getFullYear())
const [month, setMonth] = useState(value === undefined ? today.getMonth() + 1 : value.getMonth() + 1)
const [status, setStatus] = useState<'year' | 'month' | 'date'>('date')

React.useEffect(() => {
setYear(value === undefined ? today.getFullYear() : value.getFullYear())
setMonth(value === undefined ? today.getMonth() + 1 : value.getMonth() + 1)
}, [value])

const locale = useLocalNames()
const weeknames = [...Array(7).keys()].map(_ => locale.dayNamesMin[(_ + locale.firstDayOfWeek) % 7])
const monthname = locale.monthNames[month - 1]

const calendar = getMonthCalendar(year, month, { minDate, maxDate })
const calendarTable = (
<table className={styles.calendarTable}>
<thead>
<tr>
{weeknames.map(weekname => (
<th className={styles.calTableHeader} scope="col" key={weekname}>
{weekname}
</th>
))}
</tr>
</thead>
<tbody>
{calendar.map(week => (
<tr key={week[0].label}>
{week.map(date => (
<td key={`${date.month}${date.date}`}>
<button
type="button"
data-type="button"
aria-label={date.label}
aria-pressed={dateEqual(date.instance, value)}
title={date.label}
className={`
${styles.calDateItem}
${date.isToday ? 'today' : ''}
${dateEqual(date.instance, value) ? 'active' : ''}
`}
disabled={!date.curMonth || !date.selectable}
onClick={() => onChange(date.instance)}
>
{date.date}
</button>
</td>
))}
</tr>
))}
</tbody>
</table>
)

const monthOptions: Option[] = [...Array(12).keys()].map(index => ({
value: index + 1,
title: locale.monthNames[index],
selectable: monthInRange(year, index, { minDate, maxDate }),
}))
const yearOptions: Option[] = [...Array(12).keys()].map(index => ({
value: year - 6 + index,
title: `${year - 6 + index}`,
selectable: yearInRange(year - 6 + index, { minDate, maxDate }),
}))

const prevMonth = () => {
if (month > 1) {
setMonth(m => m - 1)
} else {
setYear(y => y - 1)
setMonth(12)
}
}
const nextMonth = () => {
if (month < 12) {
setMonth(m => m + 1)
} else {
setYear(y => y + 1)
setMonth(1)
}
}

const calendarHeader = (
<div className={styles.calendarHeader}>
<button type="button" aria-label="prev" title="prev" className={styles.calPrev} onClick={prevMonth}>
{'<'}
</button>
<div className={styles.calTitle}>
<button
type="button"
aria-label={monthname}
title={monthname}
aria-haspopup="true"
aria-controls="menu"
onClick={() => setStatus('month')}
>
{monthname}
</button>
<button
type="button"
aria-label={`${year}`}
title={`${year}`}
aria-haspopup="true"
aria-controls="menu"
onClick={() => setStatus('year')}
>
{year}
</button>
</div>
<button type="button" aria-label="next" title="next" className={styles.calNext} onClick={nextMonth}>
{'>'}
</button>
</div>
)

const onChangeMonth = (monthOptionItem: Option) => {
setMonth(monthOptionItem.value)
setStatus('date')
}
const onChangeYear = (yearOptionItem: Option) => {
setYear(yearOptionItem.value)
setStatus('month')
}

return (
<div className={styles.calendar}>
{calendarHeader}
{status === 'date' && calendarTable}
{status === 'year' && <Selector options={yearOptions} onChange={onChangeYear} />}
{status === 'month' && <Selector options={monthOptions} onChange={onChangeMonth} />}
</div>
)
}

export default React.memo(Calendar, (prevProps, nextProps) => {
if (prevProps.value?.toDateString() !== nextProps.value?.toDateString()) {
return false
}
if (prevProps.minDate?.toDateString() !== nextProps.minDate?.toDateString()) {
return false
}
if (prevProps.maxDate?.toDateString() !== nextProps.maxDate?.toDateString()) {
return false
}
if (prevProps.onChange !== nextProps.onChange) {
return false
}
return true
})
Loading