diff --git a/UpgradeGuide.md b/UpgradeGuide.md index f2aaf953d..361722a07 100644 --- a/UpgradeGuide.md +++ b/UpgradeGuide.md @@ -11,6 +11,8 @@ - Check your sizes, standalone props have been replaced by the `size` prop, such as `size=medium` instead of `medium`. - `Checkbox` - The old `theme` prop functionality has been replaced by the [global theme object](https://faithlife.github.io/styled-ui/#/theme) and Styled System props. +- `DatePicker` + - `DatePicker` is now wrapped in a single `div` in the DOM, instead of being three separate DOM elements "wrapped" in a `React.Fragment`. - `DatePickerInput` - The `styleOverrides` prop has been removed in favor of Styled System props. Use style props for the input directly on the `DatePickerInput` component, and use style props for the calendar popover on a `DatePickerInput.Popover` child config component. - The `placement` prop has been removed—use `placement` on `DatePickerInput.Popover` instead. diff --git a/components/Paragraph.js b/components/Paragraph.js index 32e83ceaf..ed2a28853 100644 --- a/components/Paragraph.js +++ b/components/Paragraph.js @@ -9,11 +9,11 @@ export const Paragraph = styled.p` display: block; margin: 0; color: ${themeGet('colors.foregroundPrimary')}; - ${themeGet('textStyles.c.16')}; + ${themeGet('textStyles.c.16')} - ${textStyle}; - ${box}; - ${typography}; + ${textStyle} + ${box} + ${typography} `; Paragraph.defaultProps = { theme }; diff --git a/components/Text.js b/components/Text.js index a11ca081f..8d237aaaa 100644 --- a/components/Text.js +++ b/components/Text.js @@ -1,4 +1,3 @@ -import PropTypes from 'prop-types'; import systemPropTypes from '@styled-system/prop-types'; import styled from 'styled-components'; import { textStyle, layout, border } from 'styled-system'; @@ -10,20 +9,20 @@ export const Text = styled.span` display: inline-flex; align-items: baseline; color: ${themeGet('colors.foregroundPrimary')}; - ${themeGet('textStyles.c.16')}; + ${themeGet('textStyles.c.16')} - ${textStyle}; - ${common}; - ${layout}; - ${typography}; - ${border}; + ${textStyle} + ${common} + ${layout} + ${typography} + ${border} `; Text.defaultProps = { theme }; Text.propTypes = { ...common.propTypes, ...typography.propTypes, + ...systemPropTypes.textStyle, ...systemPropTypes.layout, ...systemPropTypes.border, - textStyle: PropTypes.string, }; diff --git a/components/accordion/accordion-item.jsx b/components/accordion/accordion-item.jsx index 7135ea1b4..fab3e199c 100644 --- a/components/accordion/accordion-item.jsx +++ b/components/accordion/accordion-item.jsx @@ -4,7 +4,7 @@ import { useId } from '../shared-hooks'; import { Box } from '../Box'; import { useAccordionContext, AccordionItemContextProvider } from './accordion-util'; -export function AccordionItem({ children, index, pinned: isPinned }) { +export function AccordionItem({ children, index, pinned: isPinned, ...otherProps }) { const { expandedSections, onExpansion } = useAccordionContext(); const isExpanded = expandedSections.includes(index); @@ -35,6 +35,7 @@ export function AccordionItem({ children, index, pinned: isPinned }) { 'header' 'panel' `} + {...otherProps} > {children} @@ -46,4 +47,7 @@ AccordionItem.propTypes = { children: PropTypes.node.isRequired, /** This is supplied by the Accordion component. */ index: PropTypes.number, + /** If `true`, the item will remain permanenty expanded. */ + pinned: PropTypes.bool, + ...Box.propTypes, }; diff --git a/components/button/Button.js b/components/button/Button.js index 8bd92a481..0699e2584 100644 --- a/components/button/Button.js +++ b/components/button/Button.js @@ -1,12 +1,15 @@ import React from 'react'; +import PropTypes from 'prop-types'; import styled, { css } from 'styled-components'; import { variant, layout, flexbox, position, textStyle, border, background } from 'styled-system'; +import styledSystemPropTypes from '@styled-system/prop-types'; import 'focus-visible'; import { Box } from '../Box'; import { common, typography } from '../../theme/system'; import { buttonSizes, buttons } from '../../theme/buttons'; import { theme } from '../../theme'; import { LoadingSpinner } from '../loading-spinner'; +import { elementOfType } from '../utils'; const sizeVariant = variant({ prop: 'size', @@ -113,6 +116,33 @@ const Button = React.forwardRef( ), ); +Button.propTypes = { + children: PropTypes.node, + icon: PropTypes.element, + disabled: PropTypes.bool, + loading: PropTypes.bool, + active: PropTypes.bool, + size: PropTypes.oneOf(['small', 'medium', 'large']), + variant: PropTypes.oneOf([ + 'primary', + 'secondary', + 'minor', + 'transparent', + 'minorTransparent', + 'link', + 'danger', + 'dangerSpecial', + ]), + ...common.propTypes, + ...typography.propTypes, + ...styledSystemPropTypes.textStyle, + ...styledSystemPropTypes.layout, + ...styledSystemPropTypes.flexbox, + ...styledSystemPropTypes.position, + ...styledSystemPropTypes.border, + ...styledSystemPropTypes.background, +}; + const SegmentedButtonGroup = styled(Box).attrs(({ border }) => ({ border: border ?? 1, borderColor: 'button.segmentedButtonGroupBorder', @@ -143,4 +173,9 @@ const SegmentedButtonGroup = styled(Box).attrs(({ border }) => ({ } `; +SegmentedButtonGroup.propTypes = { + children: PropTypes.arrayOf(elementOfType(Button)).isRequired, + ...Box.propTypes, +}; + export { Button, SegmentedButtonGroup }; diff --git a/components/check-box/checkbox-content.jsx b/components/check-box/checkbox-content.jsx index 96f11c868..aec118a51 100644 --- a/components/check-box/checkbox-content.jsx +++ b/components/check-box/checkbox-content.jsx @@ -4,6 +4,7 @@ import styledSystemPropTypes from '@styled-system/prop-types'; import styled from 'styled-components'; import { getConfigChild } from '../utils'; import { Text } from '../Text'; +import { common } from '../../theme/system'; import * as Styled from './styled'; export function CheckboxContent({ isChecked, title, disabled, children, ...otherProps }) { @@ -30,8 +31,8 @@ CheckboxContent.propTypes = { title: PropTypes.string, isChecked: PropTypes.oneOf([true, false, 'mixed']), disabled: PropTypes.bool, + ...common.propTypes, ...styledSystemPropTypes.position, - ...styledSystemPropTypes.space, ...styledSystemPropTypes.layout, }; @@ -41,8 +42,8 @@ CheckboxContent.propTypes = { */ export const CheckboxBox = props => null; CheckboxBox.propTypes = { + ...common.propTypes, ...styledSystemPropTypes.position, - ...styledSystemPropTypes.space, ...styledSystemPropTypes.layout, }; CheckboxBox.childConfigComponent = 'CheckboxBox'; diff --git a/components/check-box/component.jsx b/components/check-box/component.jsx index d4f47d86b..b0bb62bca 100644 --- a/components/check-box/component.jsx +++ b/components/check-box/component.jsx @@ -3,6 +3,7 @@ import PropTypes from 'prop-types'; import { CheckboxContent } from './checkbox-content'; import * as Styled from './styled'; import { DefaultThemeProvider } from '../DefaultThemeProvider'; +import { common } from '../../theme/system'; /** Styled checkbox control with consistent styling across platforms */ export const Checkbox = function Checkbox({ @@ -67,6 +68,7 @@ Checkbox.propTypes = { /** Disables automatic blur. */ disableAutoBlur: PropTypes.bool, disabled: PropTypes.bool, + ...common.propTypes, }; Checkbox.defaultProps = { diff --git a/components/check-box/styled.jsx b/components/check-box/styled.jsx index 42edf3de7..355fd8c32 100644 --- a/components/check-box/styled.jsx +++ b/components/check-box/styled.jsx @@ -1,5 +1,6 @@ import styled, { css } from 'styled-components'; -import { position, space, layout } from 'styled-system'; +import { position, layout } from 'styled-system'; +import { common } from '../../theme/system'; import { resetStyles } from '../utils'; import { Box } from '../Box'; @@ -19,8 +20,8 @@ export const CheckboxDiv = styled(Box)` background-color: ${theme.colors.checkbox.disabledBackground}; `} + ${common} ${position} - ${space} ${layout} `; @@ -64,6 +65,8 @@ export const CheckboxContainer = styled.button` box-shadow: 0 0 0 2px ${({ theme }) => theme.colors.checkbox.shadowFocused}; } } + + ${common} `; export const isCheckedStyles = css` diff --git a/components/collapse/component.jsx b/components/collapse/component.jsx index c5b0e4d22..b93d29e14 100644 --- a/components/collapse/component.jsx +++ b/components/collapse/component.jsx @@ -8,6 +8,7 @@ const propTypes = { ...Transition.propTypes, isOpen: PropTypes.bool, children: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.node), PropTypes.node]), + ...Box.propTypes, }; const defaultProps = { diff --git a/components/date-period-picker/component.jsx b/components/date-period-picker/component.jsx index 996c52540..d57249c66 100644 --- a/components/date-period-picker/component.jsx +++ b/components/date-period-picker/component.jsx @@ -4,6 +4,9 @@ import debounce from 'lodash.debounce'; import { DatePicker } from '../date-picker'; import { Input } from '../input'; import { dateFunctionProps } from '../date-picker/date-function-props'; +import { filterProps } from '../utils'; +import { common } from '../../theme/system'; +import { DefaultThemeProvider } from '../DefaultThemeProvider'; import * as Styled from './styled'; const DATE_FORMAT_STRING = 'M/d/yyyy'; @@ -40,6 +43,7 @@ export class DatePeriodPicker extends PureComponent { dateFunctions: dateFunctionProps, /** Debounce value for date inputs. Defaults to 500ms */ debounce: PropTypes.number, + ...common.propTypes, }; static defaultProps = { @@ -181,44 +185,45 @@ export class DatePeriodPicker extends PureComponent { render() { const { setSelectedDate, validate, dateFunctions } = this.props; + const { matchingProps: styleProps } = filterProps(this.props, common.propTypes); const { inputValues: { start, end }, selectedDateRange, } = this.state; return ( - - {this.getUniqueDatePeriods().map(({ displayName, dateRange, originalIndex }) => ( - { - setSelectedDate(dateRange, originalIndex); - }} - > - {displayName} - - ))} - - - From - this.handleInputValueChange(event.target.value, 'start')} - small - /> - - - To - this.handleInputValueChange(event.target.value, 'end')} - small - /> - - - + + + {this.getUniqueDatePeriods().map(({ displayName, dateRange, originalIndex }) => ( + { + setSelectedDate(dateRange, originalIndex); + }} + > + {displayName} + + ))} + + + From + this.handleInputValueChange(event.target.value, 'start')} + small + /> + + + To + this.handleInputValueChange(event.target.value, 'end')} + small + /> + + - - + + ); } } diff --git a/components/date-period-picker/styled.jsx b/components/date-period-picker/styled.jsx index 199a298c0..bd5be6714 100644 --- a/components/date-period-picker/styled.jsx +++ b/components/date-period-picker/styled.jsx @@ -1,5 +1,6 @@ import styled from 'styled-components'; -import { colors, thickness, fonts } from '../shared-styles'; +import { themeGet } from '@styled-system/theme-get'; +import { common } from '../../theme/system'; export const Container = styled.div` display: flex; @@ -12,25 +13,25 @@ export const Container = styled.div` max-width: 324px; } - padding: ${thickness.four} 0 ${thickness.twelve} 0; + padding: ${themeGet('space.2')} 0 ${themeGet('space.4')} 0; overflow: hidden; -`; -export const DatePickerContainer = styled.div` - padding: ${thickness.eight}; + ${common} `; export const DatePeriod = styled.div` - font: ${fonts.ui14}; - padding: ${thickness.eight}; - background: ${colors.white}; + ${themeGet('textStyles.ui.14')} + + padding: ${themeGet('space.3')}; + background: ${themeGet('colors.datePeriodPicker.background')}; cursor: pointer; width: 100%; + box-sizing: border-box; text-align: initial; &:hover { - background: ${colors.blueBase}; - color: ${colors.white}; + background: ${themeGet('colors.datePeriodPicker.hoverBackground')}; + color: ${themeGet('colors.datePeriodPicker.hoverText')}; transition: background 0.2s ease-out, color 0.2s ease-out; } `; @@ -38,8 +39,8 @@ export const DatePeriod = styled.div` export const DateInputContainer = styled.div` display: flex; flex-direction: row; - border-top: 1px solid ${colors.gray14}; - padding: ${thickness.twelve} ${thickness.eight}; + border-top: 1px solid ${themeGet('colors.datePeriodPicker.inputBorder')}; + padding: ${themeGet('space.4')} ${themeGet('space.3')}; `; export const Label = styled.label` @@ -47,13 +48,14 @@ export const Label = styled.label` text-align: initial; display: flex; flex-direction: column; - font: ${fonts.ui14}; + + ${themeGet('textStyles.ui.14')} &:first-of-type { - padding-right: ${thickness.eight}; + padding-right: ${themeGet('space.3')}; } `; export const LabelText = styled.span` - padding-bottom: ${thickness.four}; + padding-bottom: ${themeGet('space.2')}; `; diff --git a/components/date-picker-input/component.jsx b/components/date-picker-input/component.jsx index 8421ae327..ccb10bb4c 100644 --- a/components/date-picker-input/component.jsx +++ b/components/date-picker-input/component.jsx @@ -5,7 +5,7 @@ import { Popover } from '../popover-v6'; import { Calendar as CalendarIcon } from '../icons'; import { Input } from '../input'; import { dateFunctionProps } from '../date-picker/date-function-props'; -import { DatePicker } from '../date-picker/component'; +import { DatePicker } from '../date-picker'; import { UtilityButton } from '../button'; import * as Styled from './styled'; import { DefaultThemeProvider } from '../DefaultThemeProvider'; @@ -139,16 +139,15 @@ export function DatePickerInput({ zIndex={3} {...popoverProps} > - - - + )} diff --git a/components/date-picker-input/legacy-component.jsx b/components/date-picker-input/legacy-component.jsx index 63d0bb41b..03833a86b 100644 --- a/components/date-picker-input/legacy-component.jsx +++ b/components/date-picker-input/legacy-component.jsx @@ -6,7 +6,7 @@ import { PlacementOptionsProps } from '../popover/popper-helpers'; import { Calendar as CalendarIcon } from '../icons'; import { Input } from '../input'; import { dateFunctionProps } from '../date-picker/date-function-props'; -import { DatePicker } from '../date-picker/component'; +import { DatePicker } from '../date-picker'; import * as Styled from './styled'; /** Flexible date picker input (with support for many date parsing libraries) */ @@ -145,16 +145,15 @@ export function DatePickerInput({ }} {...popoverProps} > - - - + )} diff --git a/components/date-picker-input/styled.jsx b/components/date-picker-input/styled.jsx index b32e26837..de3b4a0a7 100644 --- a/components/date-picker-input/styled.jsx +++ b/components/date-picker-input/styled.jsx @@ -26,7 +26,3 @@ export const CalendarButton = styled(UtilityButton).attrs(({ theme, color }) => export const CalendarIconContainer = styled.div` height: 18px; /* matches CalendarIcon */ `; - -export const DateTime = styled.div` - width: 204px; -`; diff --git a/components/date-picker/component.jsx b/components/date-picker/component.jsx index 5f9ecaeba..db5f1a9fe 100644 --- a/components/date-picker/component.jsx +++ b/components/date-picker/component.jsx @@ -1,8 +1,12 @@ -import React, { Component, Fragment } from 'react'; +import React, { Component } from 'react'; import PropTypes from 'prop-types'; +import styledSystemPropTypes from '@styled-system/prop-types'; import { Caret } from '../icons'; -import { colors } from '../shared-styles'; +import { theme } from '../../theme'; +import { DefaultThemeProvider } from '../DefaultThemeProvider'; import { dateFunctionProps } from './date-function-props'; +import { common } from '../../theme/system'; +import { filterProps } from '../utils'; import * as Styled from './styled'; import { CalendarWeek } from './calendar-week'; @@ -55,6 +59,8 @@ export class DatePicker extends Component { dateFunctions: dateFunctionProps, minDate: PropTypes.instanceOf(Date), maxDate: PropTypes.instanceOf(Date), + ...common.propTypes, + ...styledSystemPropTypes.layout, }; UNSAFE_componentWillMount() { @@ -123,66 +129,72 @@ export class DatePicker extends Component { minDate, maxDate, } = this.props; + const { matchingProps: styleProps } = filterProps(this.props, { + ...common.propTypes, + ...styledSystemPropTypes.layout, + }); const { currentMonth, weeks } = this.state; return ( - - - - - - - {dateFunctions.format(currentMonth, 'MMMM yyyy')} - - - - - - S - M - T - W - T - F - S - - - {weeks.map(week => ( - - ))} - - + + + + + + + + {dateFunctions.format(currentMonth, 'MMMM yyyy')} + + + + + + S + M + T + W + T + F + S + + + {weeks.map(week => ( + + ))} + + + ); } } diff --git a/components/date-picker/index.js b/components/date-picker/index.js index 5012bff02..3d959c10f 100644 --- a/components/date-picker/index.js +++ b/components/date-picker/index.js @@ -1 +1,4 @@ export { DatePicker } from './component'; + +/** @todo Remove legacy export upon v6 release. */ +export { DatePicker as LegacyDatePicker } from './legacy-component'; diff --git a/components/date-picker/legacy-component.jsx b/components/date-picker/legacy-component.jsx new file mode 100644 index 000000000..74f74dec3 --- /dev/null +++ b/components/date-picker/legacy-component.jsx @@ -0,0 +1,188 @@ +import React, { Component, Fragment } from 'react'; +import PropTypes from 'prop-types'; +import { Caret } from '../icons'; +import { colors } from '../shared-styles'; +import { dateFunctionProps } from './date-function-props'; +import * as Styled from './legacy-styled'; +import { CalendarWeek } from './calendar-week'; + +function generateWeek(sunday, { getYear, getMonth, getDate }) { + const week = []; + const year = getYear(sunday); + const month = getMonth(sunday); + const date = getDate(sunday); + + for (let index = 0; index < 7; index++) { + week.push(new Date(year, month, date + index)); + } + + return week; +} + +function generateWeeks(month, dateFunctions) { + const { startOfWeek, startOfMonth, endOfWeek, endOfMonth, isBefore, addWeeks } = dateFunctions; + const firstDay = startOfWeek(startOfMonth(month)); + const lastDay = endOfWeek(endOfMonth(month)); + const weeks = []; + + for ( + let walker = new Date(firstDay.getTime()); + isBefore(walker, lastDay); + walker = addWeeks(walker, 1) + ) { + weeks.push(generateWeek(walker, dateFunctions)); + } + + return weeks; +} + +/** Standard date picker control (with support for many different date parsing libraries) */ +export class DatePicker extends Component { + static propTypes = { + /** Sets the selected date */ + selectedDate: PropTypes.instanceOf(Date), + /** Sets the selected date range (use with asDateRange prop) */ + selectedDateRange: PropTypes.shape({ + start: PropTypes.instanceOf(Date), + end: PropTypes.instanceOf(Date), + }), + /** A callback that retrieves the currently selected date or date range whenever the the selected dates change. */ + setSelectedDate: PropTypes.func.isRequired, + /** Specifies that the component should function as a date range picker */ + asDateRangePicker: PropTypes.bool, + /** Takes a date as a parameter and returns false if that date is invalid */ + validate: PropTypes.func, + dateFunctions: dateFunctionProps, + minDate: PropTypes.instanceOf(Date), + maxDate: PropTypes.instanceOf(Date), + }; + + UNSAFE_componentWillMount() { + const selectedDate = this.props.selectedDate; + const date = selectedDate ? this.props.selectedDate : new Date(); + this.setMonth(date); + } + + setMonth = currentMonth => { + this.setState({ + currentMonth, + weeks: generateWeeks(currentMonth, this.props.dateFunctions), + }); + }; + + setSelectedDate = date => { + const { dateFunctions, selectedDateRange, asDateRangePicker, setSelectedDate } = this.props; + if (asDateRangePicker) { + let newDateRange; + if (selectedDateRange && selectedDateRange.start && !selectedDateRange.end) { + if (dateFunctions.isBefore(selectedDateRange.start, date)) { + newDateRange = { start: selectedDateRange.start, end: date }; + } else { + newDateRange = { start: date, end: selectedDateRange.start }; + } + } else { + newDateRange = { start: date }; + } + setSelectedDate(newDateRange); + } else { + setSelectedDate(date); + } + }; + + decrementMonth = () => { + if (!this.canDecrementMonth(this.state.weeks)) { + return; + } + this.setMonth(this.props.dateFunctions.subMonths(this.state.currentMonth, 1)); + }; + + incrementMonth = () => { + if (!this.canIncrementMonth(this.state.weeks)) { + return; + } + this.setMonth(this.props.dateFunctions.addMonths(this.state.currentMonth, 1)); + }; + + canDecrementMonth = weeks => { + const firstDay = weeks[0][0]; + return !this.props.minDate || this.props.minDate < firstDay; + }; + + canIncrementMonth = weeks => { + const lastDay = weeks[weeks.length - 1][weeks[weeks.length - 1].length - 1]; + return !this.props.maxDate || this.props.maxDate > lastDay; + }; + + render() { + const { + selectedDate, + dateFunctions, + selectedDateRange, + asDateRangePicker, + validate, + minDate, + maxDate, + } = this.props; + const { currentMonth, weeks } = this.state; + + return ( + + + + + + + {dateFunctions.format(currentMonth, 'MMMM yyyy')} + + + + + + S + M + T + W + T + F + S + + + {weeks.map(week => ( + + ))} + + + ); + } +} diff --git a/components/date-picker/legacy-styled.jsx b/components/date-picker/legacy-styled.jsx new file mode 100644 index 000000000..c470e8238 --- /dev/null +++ b/components/date-picker/legacy-styled.jsx @@ -0,0 +1,61 @@ +import styled from 'styled-components'; +import { colors } from '../shared-styles'; + +export const ChangeMonth = styled.button` + display: flex; + justify-content: space-around; + align-items: center; + border: none; + margin: 0; + padding: 0; + height: 34px; + width: 22px; + background: none; + cursor: pointer; + + &:focus { + outline: ${({ visuallyDisabled }) => visuallyDisabled && `${colors.gray22} auto 1px`}; + } +`; + +export const Header = styled.div` + display: flex; + justify-content: space-between; + align-items: center; + white-space: nowrap; + border-bottom: none; + background: ${colors.white}; + color: ${colors.gray66}; + line-height: 32px; + font-weight: bold; +`; + +export const MonthLabel = styled.div` + display: inline-block; +`; + +export const Week = styled.ul` + display: flex; + border-bottom: 1px solid ${colors.gray14}; + padding: 8px 0; + background: ${colors.white}; + list-style: none; + margin: 0; + color: ${colors.gray22}; + font-size: 12px; +`; + +export const WeekDay = styled.li` + flex: 1; + text-align: center; + text-transform: uppercase; +`; + +export const Month = styled.div` + background: ${colors.white}; + font-size: 14px; + + @media (hover: none) { + max-width: 308px; + } +`; diff --git a/components/date-picker/styled.jsx b/components/date-picker/styled.jsx index c470e8238..be6843fbf 100644 --- a/components/date-picker/styled.jsx +++ b/components/date-picker/styled.jsx @@ -1,5 +1,9 @@ import styled from 'styled-components'; -import { colors } from '../shared-styles'; +import { layout } from 'styled-system'; +import { themeGet } from '@styled-system/theme-get'; +import { common } from '../../theme/system'; + +export const Container = styled.div(common, layout); export const ChangeMonth = styled.button` display: flex; @@ -14,7 +18,8 @@ export const ChangeMonth = styled.button` cursor: pointer; &:focus { - outline: ${({ visuallyDisabled }) => visuallyDisabled && `${colors.gray22} auto 1px`}; + outline: ${({ visuallyDisabled }) => + visuallyDisabled && `${themeGet('colors.datePicker.disabledOutline')} auto 1px`}; } `; @@ -24,8 +29,8 @@ export const Header = styled.div` align-items: center; white-space: nowrap; border-bottom: none; - background: ${colors.white}; - color: ${colors.gray66}; + background: ${themeGet('colors.datePicker.background')}; + color: ${themeGet('colors.datePicker.header')}; line-height: 32px; font-weight: bold; `; @@ -36,12 +41,12 @@ export const MonthLabel = styled.div` export const Week = styled.ul` display: flex; - border-bottom: 1px solid ${colors.gray14}; + border-bottom: 1px solid ${themeGet('colors.datePicker.weekBorder')}; padding: 8px 0; - background: ${colors.white}; + background: ${themeGet('colors.datePicker.background')}; list-style: none; margin: 0; - color: ${colors.gray22}; + color: ${themeGet('colors.datePicker.week')}; font-size: 12px; `; @@ -52,7 +57,7 @@ export const WeekDay = styled.li` `; export const Month = styled.div` - background: ${colors.white}; + background: ${themeGet('colors.datePicker.background')}; font-size: 14px; @media (hover: none) { diff --git a/components/drop-zone/component.jsx b/components/drop-zone/component.jsx index e57b5de65..bf346d33f 100644 --- a/components/drop-zone/component.jsx +++ b/components/drop-zone/component.jsx @@ -13,6 +13,7 @@ export class DropZone extends PureComponent { onDrop: PropTypes.func.isRequired, /** The contents of the drop zone. Children will be rendered into a flex container with align-items set to center. */ children: PropTypes.node, + ...Paragraph.propTypes, }; state = { @@ -48,7 +49,7 @@ export class DropZone extends PureComponent { }; render() { - const { children } = this.props; + const { children, onDrop, ...otherProps } = this.props; const { showHighlight } = this.state; return ( @@ -71,6 +72,7 @@ export class DropZone extends PureComponent { alignItems="center" backgroundColor={showHighlight ? 'blue2' : null} color={showHighlight ? 'blue4' : null} + {...otherProps} > {children} diff --git a/components/dropdown/dropdown-children.jsx b/components/dropdown/dropdown-children.jsx index b48ff1e47..08614d82d 100644 --- a/components/dropdown/dropdown-children.jsx +++ b/components/dropdown/dropdown-children.jsx @@ -1,8 +1,10 @@ import React, { useCallback } from 'react'; +import PropTypes from 'prop-types'; import { getConfigChild } from '../utils'; import { Box } from '../Box'; import { Text } from '../Text'; import { Checkbox } from '../check-box'; +import { UtilityButton } from '../button'; import { handledKeys, useDropdownContext } from './utils'; import * as Styled from './styled'; @@ -25,6 +27,12 @@ export function MenuItemIcon({ src, variant, children, ...iconProps }) { MenuItemIcon.defaultProps = { variant: 'icon', }; +MenuItemIcon.propTypes = { + src: PropTypes.string, + variant: PropTypes.oneOf(['thumbnail', 'icon', 'avatar']), + children: PropTypes.node, + ...Box.propTypes, +}; MenuItemIcon.childConfigComponent = 'MenuItemIcon'; export function MenuItemPrimaryText({ hasSecondaryText, children, ...textProps }) { @@ -40,6 +48,11 @@ export function MenuItemPrimaryText({ hasSecondaryText, children, ...textProps } ); } +MenuItemPrimaryText.propTypes = { + hasSecondaryText: PropTypes.bool, + children: PropTypes.node.isRequired, + ...Text.propTypes, +}; MenuItemPrimaryText.childConfigComponent = 'MenuItemPrimaryText'; export function MenuItemSecondaryText({ children, ...textProps }) { @@ -49,6 +62,10 @@ export function MenuItemSecondaryText({ children, ...textProps }) { ); } +MenuItemSecondaryText.propTypes = { + children: PropTypes.node.isRequired, + ...Text.propTypes, +}; MenuItemSecondaryText.childConfigComponent = 'MenuItemSecondaryText'; export const MenuItem = React.forwardRef(function MenuItem( @@ -123,6 +140,17 @@ export const MenuItem = React.forwardRef(function MenuItem( ); }); +MenuItem.propTypes = { + keyboardFocused: PropTypes.bool, + /** @type {(event: React.MouseEvent) => void} */ + onClick: PropTypes.func, + preventDefaultOnClick: PropTypes.bool, + /** @type {(event: React.KeyboardEvent) => void} */ + onKeyDown: PropTypes.func, + disabled: PropTypes.bool, + children: PropTypes.node.isRequired, + ...UtilityButton.propTypes, +}; MenuItem.isFocusableChild = true; export const MenuItemCheckbox = React.forwardRef(function MenuItemCheckbox( @@ -155,6 +183,14 @@ export const MenuItemCheckbox = React.forwardRef(function MenuItemCheckbox( ); }); +MenuItemCheckbox.propTypes = { + isChecked: PropTypes.bool, + /** @type {() => void} */ + onToggle: PropTypes.func, + disabled: PropTypes.bool, + children: PropTypes.node.isRequired, + ...MenuItem.propTypes, +}; MenuItemCheckbox.isFocusableChild = true; export const MenuItemLink = React.forwardRef(function MenuItemLink( @@ -189,6 +225,12 @@ export const MenuItemLink = React.forwardRef(function MenuItemLink( ); }); +MenuItemLink.propTypes = { + disabled: PropTypes.bool, + href: PropTypes.string, + children: PropTypes.node.isRequired, + ...MenuItem.propTypes, +}; MenuItemLink.isFocusableChild = true; export function MenuItemSeparator(hrProps) { @@ -206,6 +248,7 @@ export function MenuItemSeparator(hrProps) { /> ); } +MenuItemSeparator.propTypes = Box.propTypes; export function MenuItemTitle({ children, ...textProps }) { return ( @@ -214,3 +257,7 @@ export function MenuItemTitle({ children, ...textProps }) { ); } +MenuItemTitle.propTypes = { + children: PropTypes.node.isRequired, + ...Text.propTypes, +}; diff --git a/components/dropdown/dropdown-menu.jsx b/components/dropdown/dropdown-menu.jsx index 99861b825..10fddbe5c 100644 --- a/components/dropdown/dropdown-menu.jsx +++ b/components/dropdown/dropdown-menu.jsx @@ -1,6 +1,18 @@ import React, { useCallback } from 'react'; +import PropTypes from 'prop-types'; import { Popover } from '../popover-v6'; import { useDropdownContext, useKeyboardNavigate } from './utils'; +import { elementOfType } from '../utils'; +import { + MenuItem, + MenuItemCheckbox, + MenuItemLink, + MenuItemSeparator, + MenuItemIcon, + MenuItemPrimaryText, + MenuItemSecondaryText, + MenuItemTitle, +} from './dropdown-children'; import * as Styled from './styled'; export function DropdownMenu({ children, ...popoverProps }) { @@ -53,3 +65,19 @@ export function DropdownMenu({ children, ...popoverProps }) { ) ); } + +DropdownMenu.propTypes = { + children: PropTypes.arrayOf( + PropTypes.oneOfType([ + elementOfType(MenuItem), + elementOfType(MenuItemCheckbox), + elementOfType(MenuItemLink), + elementOfType(MenuItemSeparator), + elementOfType(MenuItemIcon), + elementOfType(MenuItemPrimaryText), + elementOfType(MenuItemSecondaryText), + elementOfType(MenuItemTitle), + ]), + ).isRequired, + ...Popover.propTypes, +}; diff --git a/components/dropdown/dropdown-toggle.jsx b/components/dropdown/dropdown-toggle.jsx index 2d49a29f3..aca38d4ae 100644 --- a/components/dropdown/dropdown-toggle.jsx +++ b/components/dropdown/dropdown-toggle.jsx @@ -1,4 +1,5 @@ import React, { useMemo } from 'react'; +import PropTypes from 'prop-types'; import { getConfigChild } from '../utils'; import { Button, SegmentedButtonGroup } from '../button'; import { useDropdownContext, useKeyboardActivate } from './utils'; @@ -69,6 +70,23 @@ DropdownToggle.defaultProps = { size: 'small', variant: 'primary', }; +DropdownToggle.propTypes = { + hideCarrot: PropTypes.bool, + size: PropTypes.oneOf(['small', 'medium', 'large']), + variant: PropTypes.oneOf([ + 'primary', + 'secondary', + 'minor', + 'transparent', + 'minorTransparent', + 'link', + 'danger', + 'dangerSpecial', + ]), + disabled: PropTypes.bool, + children: PropTypes.node.isRequired, + ...Button.propTypes, +}; export function DropdownActionButton({ defaultSize, @@ -84,3 +102,19 @@ export function DropdownActionButton({ ); } DropdownActionButton.childConfigComponent = 'DropdownActionButton'; +DropdownActionButton.propTypes = { + defaultSize: PropTypes.oneOf(['small', 'medium', 'large']), + defaultVariant: PropTypes.oneOf([ + 'primary', + 'secondary', + 'minor', + 'transparent', + 'minorTransparent', + 'link', + 'danger', + 'dangerSpecial', + ]), + defaultDisabled: PropTypes.bool, + children: PropTypes.element.isRequired, + ...Button.propTypes, +}; diff --git a/components/popover-v6/index.jsx b/components/popover-v6/index.jsx index 7380e434a..8740088e9 100644 --- a/components/popover-v6/index.jsx +++ b/components/popover-v6/index.jsx @@ -4,6 +4,7 @@ import PropTypes from 'prop-types'; import { usePopper } from 'faithlife-react-popper'; import { useFocusAwayHandler } from '../shared-hooks/use-focus-away-handler'; import { mergeRefs } from '../utils/merge-refs'; +import { box } from '../../theme/system'; import { PopoverContainer, PopoverArrow } from './styled'; export function usePopover(reference, options) { @@ -99,4 +100,6 @@ Popover.propTypes = { hideArrow: PropTypes.bool, /** handler useful for closing popover when clicking away */ onFocusAway: PropTypes.func, + children: PropTypes.node.required, + ...box.propTypes, }; diff --git a/components/utils/filter-props.js b/components/utils/filter-props.js index 845a5df25..83668ef48 100644 --- a/components/utils/filter-props.js +++ b/components/utils/filter-props.js @@ -1,15 +1,28 @@ -export function filterChildProps(props, childPropTypes) { - const parentProps = {}; - const childProps = {}; - const childPropNames = Object.keys(childPropTypes); +/** + * Filters a collection of props by a list of names (or a prop types object). + * + * @param {object} props - A props object. + * @param {(string[] | object)} filterPropNames - An array of prop names to filter by (or a prop + * types object with keys to filter by). + * @returns {{ matchingProps: object, remainingProps: object }} An object containing two props + * objects: one containing the props that match the filter and the other containing the props that + * don't match. + */ +export function filterProps(props, filterPropNames) { + const filterList = Array.isArray(filterPropNames) + ? filterPropNames + : Object.keys(filterPropNames); - for (const [propName, prop] of Object.entries(props)) { - if (childPropNames.includes(propName)) { - childProps[propName] = prop; + const matchingProps = {}; + const remainingProps = {}; + + for (const [propName, propValue] of Object.entries(props)) { + if (filterList.includes(propName)) { + matchingProps[propName] = propValue; } else { - parentProps[propName] = prop; + remainingProps[propName] = propValue; } } - return [parentProps, childProps]; + return { matchingProps, remainingProps }; } diff --git a/components/utils/index.js b/components/utils/index.js index b3887bfc5..5232d95c3 100644 --- a/components/utils/index.js +++ b/components/utils/index.js @@ -1,9 +1,10 @@ export { BootstrapContainer, wrapBootstrap } from './bootstrap-container'; export { forwardClassRef } from './forwardref-wrapper'; export { TransitionStatuses, TransitionTimeouts } from './transition-group-utils'; -export { filterChildProps } from './filter-props'; +export { filterProps } from './filter-props'; export { deprecate, deprecateComponent, deprecateProp } from './deprecate'; export { getConfigProps, getConfigChild } from './get-config-props'; +export { elementOfType } from './prop-types'; /** * Chooses the correct variant from a main variant prop and a set of boolean shortcut props. diff --git a/components/utils/prop-types.js b/components/utils/prop-types.js new file mode 100644 index 000000000..bc165b690 --- /dev/null +++ b/components/utils/prop-types.js @@ -0,0 +1,15 @@ +import PropTypes from 'prop-types'; + +/** + * Creates a prop-type-checking function that validates whether the prop value is an element of a + * given component type. + * + * @param {React.ElementType} Component - The component type to check for. + * @returns A PropTypes validation function to use in a component's `propTypes` object. + */ +export function elementOfType(Component) { + // Not very intuitive, but this is the best way to check with PropTypes (see https://github.com/facebook/react/issues/2979) + return PropTypes.shape({ + type: PropTypes.oneOf([Component]).isRequired, + }); +} diff --git a/index-v6.js b/index-v6.js index 6397d67b7..f3e974eda 100644 --- a/index-v6.js +++ b/index-v6.js @@ -1,6 +1,7 @@ export { Accordion } from './components/accordion'; export { Button, SegmentedButtonGroup } from './components/button'; export { Checkbox } from './components/check-box'; +export { DatePicker } from './components/date-picker'; export { DatePickerInput } from './components/date-picker-input'; export { Dropdown } from './components/dropdown'; export { HelpBox } from './components/help-box'; diff --git a/index.js b/index.js index a26cf8ffa..f1a6fba3d 100644 --- a/index.js +++ b/index.js @@ -9,7 +9,7 @@ export { Bootstrap } from './components/bootstrap'; export { LegacyButton as Button, UtilityButton } from './components/button'; export { LegacyCheckbox as Checkbox } from './components/check-box'; export { Collapse } from './components/collapse'; -export { DatePicker } from './components/date-picker'; +export { LegacyDatePicker as DatePicker } from './components/date-picker'; export { LegacyDatePickerInput as DatePickerInput } from './components/date-picker-input'; export { DatePeriodPicker } from './components/date-period-picker'; export { DropZone } from './components/drop-zone'; diff --git a/theme/core.js b/theme/core.js index e2a9428cb..ce3ac5753 100644 --- a/theme/core.js +++ b/theme/core.js @@ -201,6 +201,21 @@ colors.checkbox = { shadowFocused: colors.input.shadowFocused, }; +colors.datePicker = { + disabledOutline: colors.gray22, + background: colors.white, + header: colors.gray66, + week: colors.gray22, + weekBorder: colors.gray14, +}; + +colors.datePeriodPicker = { + background: colors.datePicker.background, + hoverBackground: colors.blue4, + hoverText: colors.white, + inputBorder: colors.datePicker.weekBorder, +}; + colors.datePickerInput = { iconColor: colors.gray52, }; diff --git a/theme/textStyles.js b/theme/textStyles.js index 023105694..e0859dba5 100644 --- a/theme/textStyles.js +++ b/theme/textStyles.js @@ -52,59 +52,59 @@ export const textStyles = { '12': { fontSize: '12px', lineHeight: '16px', - fontWeight: fontWeights.normal, + fontWeight: fontWeights.regular, }, '13': { fontSize: '13px', lineHeight: '18px', - fontWeight: fontWeights.normal, + fontWeight: fontWeights.regular, }, '14': { fontSize: '14px', lineHeight: '20px', - fontWeight: fontWeights.normal, + fontWeight: fontWeights.regular, }, '16': { fontSize: '16px', lineHeight: '22px', - fontWeight: fontWeights.normal, + fontWeight: fontWeights.regular, }, '18': { fontSize: '18px', lineHeight: '24px', - fontWeight: fontWeights.normal, + fontWeight: fontWeights.regular, }, }, ui: { '12': { fontSize: '12px', lineHeight: '12px', - fontWeight: fontWeights.normal, + fontWeight: fontWeights.regular, }, '13': { fontSize: '13px', lineHeight: '13px', - fontWeight: fontWeights.normal, + fontWeight: fontWeights.regular, }, '14': { fontSize: '14px', lineHeight: '14px', - fontWeight: fontWeights.normal, + fontWeight: fontWeights.regular, }, '16': { fontSize: '16px', lineHeight: '16px', - fontWeight: fontWeights.normal, + fontWeight: fontWeights.regular, }, '18': { fontSize: '18px', lineHeight: '18px', - fontWeight: fontWeights.normal, + fontWeight: fontWeights.regular, }, '24': { fontSize: '24px', lineHeight: '24px', - fontWeight: fontWeights.normal, + fontWeight: fontWeights.regular, }, }, };