diff --git a/.storybook/config.js b/.storybook/config.js index e19b1087c..89886d607 100644 --- a/.storybook/config.js +++ b/.storybook/config.js @@ -24,5 +24,5 @@ addDecorator(withKnobs); addDecorator(wrapContent({ assetRoot })); configure(() => { - require('../stories/index.js'); + require('../stories/index.ts'); }, module); diff --git a/package.json b/package.json index 72796fdfe..5de71e022 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,7 @@ "storybook": "start-storybook -s ./node_modules/@salesforce-ux/design-system -p 9001 -c .storybook", "test": "npm-run-all type-check test:jest test:storyshots", "test:storyshots": "NODE_ENV=test jest test/storyshots/*.test.js", - "test:jest": "jest test/*-spec.js", + "test:jest": "jest test/*-spec.js test/*-spec.tsx", "prepublish": "npm run build", "type-check": "tsc --noEmit", "type-check:watch": "npm run type-check -- --watch", @@ -72,10 +72,14 @@ "@types/classnames": "^2.2.7", "@types/enzyme": "^3.9.1", "@types/jest": "^24.0.11", + "@types/power-assert": "^1.5.0", "@types/react": "^16.8.12", + "@types/react-dom": "^16.9.0", "@types/storybook__addon-actions": "^3.4.2", "@types/storybook__addon-knobs": "^5.0.0", "@types/storybook__react": "^4.0.1", + "@types/svg4everybody": "^2.1.1", + "@types/uuid": "^3.4.4", "@typescript-eslint/eslint-plugin": "^1.5.0", "babel-core": "^7.0.0-bridge.0", "babel-eslint": "^10.0.1", @@ -107,7 +111,7 @@ "@types/react": "^16.8.12" }, "jest": { - "testRegex": "(/test/.*|\\.(test|spec))\\.js$", + "testRegex": "(/test/.*|\\.(test|spec))\\.(js|tsx)$", "setupFilesAfterEnv": [ "/test/setupTests.js" ], diff --git a/src/scripts/AutoAlign.js b/src/scripts/AutoAlign.tsx similarity index 78% rename from src/scripts/AutoAlign.js rename to src/scripts/AutoAlign.tsx index 9885d26d4..9ab479fc9 100644 --- a/src/scripts/AutoAlign.js +++ b/src/scripts/AutoAlign.tsx @@ -1,28 +1,36 @@ -import React from 'react'; +import React, { ComponentType } from 'react'; import PropTypes from 'prop-types'; import classnames from 'classnames'; import RelativePortal from 'react-relative-portal'; +import { ComponentSettingsContext } from './ComponentSettings'; -function delay(ms) { +function delay(ms: number) { return new Promise((resolve) => { setTimeout(resolve, ms); }); } -function getViewportRect() { +function getViewportRect(): Rect { const { innerHeight: height = Infinity, innerWidth: width = Infinity } = window || {}; return { top: 0, left: 0, width, height }; } -function getCenterPoint(rect) { +type Rect = { + top: number; + left: number; + width: number; + height: number; +}; + +function getCenterPoint(rect: Rect) { return { x: rect.left + 0.5 * rect.width, y: rect.top + 0.5 * rect.height, }; } -function getPreferAlignment(rect) { +function getPreferAlignment(rect: Rect) { const { x: rx, y: ry } = getCenterPoint(rect); const { x: vx, y: vy } = getCenterPoint(getViewportRect()); return { @@ -31,7 +39,12 @@ function getPreferAlignment(rect) { }; } -function calcAlignmentRect(target, rect, vertAlign, horizAlign) { +function calcAlignmentRect( + target: Rect, + rect: { width: number; height: number }, + vertAlign: string, + horizAlign: string +) { return { ...rect, top: @@ -53,7 +66,7 @@ function calcAlignmentRect(target, rect, vertAlign, horizAlign) { }; } -function hasViewportIntersection({ top, left, width, height }) { +function hasViewportIntersection({ top, left, width, height }: Rect) { const { width: viewportWidth, height: viewportHeight } = getViewportRect(); return ( top < 0 || @@ -63,7 +76,7 @@ function hasViewportIntersection({ top, left, width, height }) { ); } -function isEqualRect(aRect, bRect) { +function isEqualRect(aRect: Rect, bRect: Rect) { return ( aRect.top === bRect.top && aRect.left === bRect.left && @@ -72,9 +85,9 @@ function isEqualRect(aRect, bRect) { ); } -function throttle(func, ms) { +function throttle(func: Function, ms: number) { let last = 0; - return (...args) => { + return (...args: any) => { const now = Date.now(); if (last + ms < now) { func(...args); @@ -83,9 +96,9 @@ function throttle(func, ms) { }; } -function ignoreFirstCall(func) { +function ignoreFirstCall(func: Function) { let called = false; - return (...args) => { + return (...args: any) => { if (called) { func(...args); } @@ -93,30 +106,59 @@ function ignoreFirstCall(func) { }; } +export type AutoAlignOptions = { + triggerSelector: string; +}; + +export type AutoAlignProps = { + portalClassName?: string; + portalStyle?: object; + size?: 'small' | 'medium' | 'large'; + preventPortalize?: boolean; +} & Partial; + +export type InjectedProps = { + align: 'left' | 'right'; + vertAlign: 'top' | 'bottom'; +}; + +export type AutoAlignState = { + triggerRect: Rect; + horizAlign: string; + vertAlign: string; +}; + /** * */ -export default function autoAlign(options) { +export function autoAlign(options: AutoAlignOptions) { const { triggerSelector } = options; - return (Cmp) => - class extends React.Component { - static propTypes = { - portalClassName: PropTypes.string, - portalStyle: PropTypes.object, // eslint-disable-line react/forbid-prop-types - size: PropTypes.oneOf(['small', 'medium', 'large']), - align: PropTypes.oneOf(['left', 'right']), - vertAlign: PropTypes.oneOf(['top', 'bottom']), - preventPortalize: PropTypes.bool, - children: PropTypes.node, - }; + return ( + Cmp: ComponentType + ) => { + type ResultProps = TOriginalProps & AutoAlignProps; + + return class extends React.Component { + private pid: number | null = null; + + /* eslint-disable react/sort-comp */ + node: any; + + content: any; + /* eslint-enable react/sort-comp */ + + context!: Pick< + ComponentSettingsContext, + 'portalClassName' | 'portalStyle' + >; static contextTypes = { portalClassName: PropTypes.string, portalStyle: PropTypes.object, // eslint-disable-line react/forbid-prop-types }; - state = { + state: AutoAlignState = { triggerRect: { top: 0, left: 0, width: 0, height: 0 }, horizAlign: 'left', vertAlign: 'top', @@ -182,7 +224,7 @@ export default function autoAlign(options) { } }; - updateAlignment(triggerRect) { + updateAlignment(triggerRect: Rect) { if (this.content && this.content.node) { const { horizAlign: oldHorizAlign, @@ -273,10 +315,10 @@ export default function autoAlign(options) { : 0; const content = ( (this.content = cmp)} - {...pprops} + align={align.split('-')[0] as InjectedProps['align']} + vertAlign={vertAlign.split('-')[0] as InjectedProps['vertAlign']} + ref={(cmp: any) => (this.content = cmp)} + {...pprops as TOriginalProps} > {children} @@ -301,4 +343,5 @@ export default function autoAlign(options) { ); } }; + }; } diff --git a/src/scripts/Badge.tsx b/src/scripts/Badge.tsx index bea07a967..044440d96 100644 --- a/src/scripts/Badge.tsx +++ b/src/scripts/Badge.tsx @@ -4,13 +4,9 @@ import classnames from 'classnames'; export type BadgeProps = { type?: 'default' | 'shade' | 'inverse'; label?: string; -}; +} & HTMLAttributes; -export const Badge: React.FC> = ({ - type, - label, - ...props -}) => { +export const Badge: React.FC = ({ type, label, ...props }) => { const typeClassName = type ? `slds-theme--${type}` : null; const badgeClassNames = classnames('slds-badge', typeClassName); return ( diff --git a/src/scripts/BreadCrumbs.tsx b/src/scripts/BreadCrumbs.tsx index 22b62c88f..467b69f2d 100644 --- a/src/scripts/BreadCrumbs.tsx +++ b/src/scripts/BreadCrumbs.tsx @@ -4,9 +4,9 @@ import classnames from 'classnames'; export type CrumbProps = { className?: string; href?: string; -}; +} & HTMLAttributes; -export const Crumb: React.FC> = ({ +export const Crumb: React.FC = ({ className, href, children, @@ -28,11 +28,14 @@ export const Crumb: React.FC> = ({ export type BreadCrumbsProps = { label?: string; className?: string; -}; +} & HTMLAttributes; -export const BreadCrumbs: React.FC< - BreadCrumbsProps & HTMLAttributes -> = ({ label, className, children, ...props }) => { +export const BreadCrumbs: React.FC = ({ + label, + className, + children, + ...props +}) => { const oClassName = classnames( 'slds-breadcrumb slds-list--horizontal', className diff --git a/src/scripts/Button.tsx b/src/scripts/Button.tsx index 843dba9ef..954661f97 100644 --- a/src/scripts/Button.tsx +++ b/src/scripts/Button.tsx @@ -1,7 +1,7 @@ import React, { Component, ReactNode, ButtonHTMLAttributes } from 'react'; import classnames from 'classnames'; -import Icon from './Icon'; -import Spinner from './Spinner'; +import { Icon } from './Icon'; +import { Spinner } from './Spinner'; type Omit = Pick>; @@ -16,7 +16,8 @@ export type ButtonType = | 'icon-inverse' | 'icon-more' | 'icon-border' - | 'icon-border-filled'; + | 'icon-border-filled' + | 'icon-border-inverse'; const ICON_SIZES = ['x-small', 'small', 'medium', 'large'] as const; const ICON_ALIGNS = ['left', 'right'] as const; @@ -42,21 +43,15 @@ export type ButtonProps = { iconMore?: string; iconMoreSize?: ButtonIconMoreSize; onClick?: (e: React.MouseEvent) => void; - buttonRef?: (node?: HTMLButtonElement) => void; -}; + buttonRef?: (node: HTMLButtonElement) => void; +} & Omit, 'type'>; -export class Button extends Component< - ButtonProps & Omit, 'type'>, - {} -> { - // eslint-disable-next-line react/sort-comp - private node: HTMLButtonElement | null; +export class Button extends Component { + node: HTMLButtonElement | null = null; constructor(props: Readonly) { super(props); - this.node = null; - this.onClick = this.onClick.bind(this); } @@ -74,7 +69,7 @@ export class Button extends Component< const inverse = inv || /-?inverse$/.test(type || ''); return ( ; + return ; } render() { @@ -146,7 +141,7 @@ export class Button extends Component< export type ButtonIconProps = { className?: string; - icon?: string; + icon: string; align?: ButtonIconAlign; size?: ButtonIconSize; inverse?: boolean; @@ -176,13 +171,13 @@ export const ButtonIcon: React.FC = ({ inverseClassName, className ); - const iconStyle = { ...style, pointerEvents: 'none' }; return ( ); diff --git a/src/scripts/ButtonGroup.js b/src/scripts/ButtonGroup.tsx similarity index 68% rename from src/scripts/ButtonGroup.js rename to src/scripts/ButtonGroup.tsx index b28b5a8eb..c1ef50421 100644 --- a/src/scripts/ButtonGroup.js +++ b/src/scripts/ButtonGroup.tsx @@ -1,15 +1,18 @@ import React, { Component, Children } from 'react'; -import PropTypes from 'prop-types'; import classnames from 'classnames'; -import DropdownButton from './DropdownButton'; +import { DropdownButton } from './DropdownButton'; -export default class ButtonGroup extends Component { - constructor() { - super(); +export type ButtonGroupProps = { + className?: string; +}; + +export class ButtonGroup extends Component { + constructor(props: Readonly) { + super(props); this.renderButton = this.renderButton.bind(this); } - renderButton(button, index) { + renderButton(button: any, index: number) { const cnt = React.Children.count(this.props.children); if ( button.type && @@ -29,9 +32,6 @@ export default class ButtonGroup extends Component { render() { const { className, children, ...props } = this.props; const btnGrpClassNames = classnames(className, 'slds-button-group'); - const pprops = Object.assign({}, props); - delete pprops.component; - delete pprops.items; return (
{Children.map(children, this.renderButton)} @@ -39,8 +39,3 @@ export default class ButtonGroup extends Component { ); } } - -ButtonGroup.propTypes = { - className: PropTypes.string, - children: PropTypes.node, -}; diff --git a/src/scripts/Checkbox.js b/src/scripts/Checkbox.js deleted file mode 100644 index deda89cd4..000000000 --- a/src/scripts/Checkbox.js +++ /dev/null @@ -1,60 +0,0 @@ -import React, { Component } from 'react'; -import PropTypes from 'prop-types'; -import classnames from 'classnames'; -import FormElement from './FormElement'; - -export default class Checkbox extends Component { - componentWillReceiveProps(nextProps) { - const input = this.node.getElementsByTagName('input')[0]; - if (nextProps.defaultChecked !== input.checked) { - input.checked = nextProps.defaultChecked; - } - } - - renderCheckbox({ className, label, checkboxRef, ...props }) { - const checkClassNames = classnames(className, 'slds-checkbox'); - return ( - - ); - } - - render() { - const { grouped, required, error, totalCols, cols, ...props } = this.props; - const formElemProps = { required, error, totalCols, cols }; - return grouped ? ( - this.renderCheckbox(props) - ) : ( - (this.node = node)} - {...formElemProps} - > - {this.renderCheckbox(props)} - - ); - } -} - -Checkbox.propTypes = { - className: PropTypes.string, - label: PropTypes.string, - required: PropTypes.bool, - error: FormElement.propTypes.error, - totalCols: PropTypes.number, - cols: PropTypes.number, - grouped: PropTypes.bool, - checkboxRef: PropTypes.func, - name: PropTypes.string, - value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), - checked: PropTypes.bool, - defaultChecked: PropTypes.bool, -}; diff --git a/src/scripts/Checkbox.tsx b/src/scripts/Checkbox.tsx new file mode 100644 index 000000000..53fd949b8 --- /dev/null +++ b/src/scripts/Checkbox.tsx @@ -0,0 +1,66 @@ +import React, { Component, InputHTMLAttributes } from 'react'; +import classnames from 'classnames'; +import { FormElement, FormElementProps } from './FormElement'; + +export type CheckboxProps = { + className?: string; + label?: string; + required?: boolean; + error?: FormElementProps['error']; + totalCols?: number; + cols?: number; + grouped?: boolean; + name?: string; + value?: string | number; + checked?: boolean; + defaultChecked?: boolean; + checkboxRef?: (node: HTMLLabelElement | null) => void; +} & InputHTMLAttributes; + +export class Checkbox extends Component { + node: HTMLDivElement | HTMLLabelElement | null = null; + + componentWillReceiveProps(nextProps: Readonly) { + if (this.node) { + const input = this.node.getElementsByTagName('input')[0]; + if ( + nextProps.defaultChecked !== undefined && + nextProps.defaultChecked !== input.checked + ) { + input.checked = nextProps.defaultChecked; + } + } + } + + renderCheckbox({ className, label, checkboxRef, ...props }: CheckboxProps) { + const checkClassNames = classnames(className, 'slds-checkbox'); + return ( + + ); + } + + render() { + const { grouped, required, error, totalCols, cols, ...props } = this.props; + const formElemProps = { required, error, totalCols, cols }; + return grouped ? ( + this.renderCheckbox(props) + ) : ( + (this.node = node)} + {...formElemProps} + > + {this.renderCheckbox(props)} + + ); + } +} diff --git a/src/scripts/CheckboxGroup.js b/src/scripts/CheckboxGroup.tsx similarity index 65% rename from src/scripts/CheckboxGroup.js rename to src/scripts/CheckboxGroup.tsx index f799b70ff..8684dac19 100644 --- a/src/scripts/CheckboxGroup.js +++ b/src/scripts/CheckboxGroup.tsx @@ -1,21 +1,39 @@ -import React from 'react'; -import PropTypes from 'prop-types'; +import React, { FieldsetHTMLAttributes } from 'react'; import classnames from 'classnames'; -import FormElement from './FormElement'; +import { FormElementProps } from './FormElement'; -export default class CheckboxGroup extends React.Component { - constructor() { - super(); +export type CheckboxGroupProps = { + className?: string; + label?: string; + required?: boolean; + error?: FormElementProps['error']; + name?: string; + totalCols?: number; + cols?: number; + style?: object; + onChange?: ( + e: React.FormEvent, + values: (string | number)[] + ) => void; +} & FieldsetHTMLAttributes; + +export class CheckboxGroup extends React.Component { + static isFormElement = true; + + private nodes: { [key: string]: any } = {}; + + constructor(props: Readonly) { + super(props); this.onChange = this.onChange.bind(this); this.renderControl = this.renderControl.bind(this); } - onChange(e) { + onChange(e: React.FormEvent) { if (this.props.onChange) { - const values = []; - React.Children.forEach(this.props.children, (check, i) => { - const el = check.props.ref || this[`check${i + 1}`]; + const values: (string | number)[] = []; + React.Children.forEach(this.props.children, (check: any, i) => { + const el = check.props.ref || this.nodes[`check${i + 1}`]; const checkEl = el && el.querySelector('input[type=checkbox]'); if (checkEl && checkEl.checked) { values.push(check.props.value); @@ -25,12 +43,12 @@ export default class CheckboxGroup extends React.Component { } } - renderControl(checkbox, i) { - const props = { grouped: true }; + renderControl(checkbox: any, i: number) { + const props: any = { grouped: true }; if (checkbox.props.ref) { props.ref = checkbox.props.ref; } else { - props.checkboxRef = (node) => (this[`check${i + 1}`] = node); + props.checkboxRef = (node: any) => (this.nodes[`check${i + 1}`] = node); } if (this.props.name) { props.name = this.props.name; @@ -97,19 +115,3 @@ export default class CheckboxGroup extends React.Component { ); } } - -CheckboxGroup.propTypes = { - className: PropTypes.string, - label: PropTypes.string, - required: PropTypes.bool, - error: FormElement.propTypes.error, - name: PropTypes.string, - totalCols: PropTypes.number, - cols: PropTypes.number, - onChange: PropTypes.func, - children: PropTypes.node, - /* eslint-disable react/forbid-prop-types */ - style: PropTypes.object, -}; - -CheckboxGroup.isFormElement = true; diff --git a/src/scripts/ComponentSettings.js b/src/scripts/ComponentSettings.tsx similarity index 54% rename from src/scripts/ComponentSettings.js rename to src/scripts/ComponentSettings.tsx index 7d7997eff..c1bb3e695 100644 --- a/src/scripts/ComponentSettings.js +++ b/src/scripts/ComponentSettings.tsx @@ -1,24 +1,32 @@ import React from 'react'; import PropTypes from 'prop-types'; +export type ComponentSettingsProps = { + assetRoot?: string; + portalClassName?: string; + portalStyle?: object; +}; + +export type ComponentSettingsContext = { + assetRoot?: string; + portalClassName?: string; + portalStyle?: object; +}; + /** * */ -export default class ComponentSettings extends React.Component { - static propTypes = { - assetRoot: PropTypes.string, - portalClassName: PropTypes.string, - portalStyle: PropTypes.object, // eslint-disable-line react/forbid-prop-types - children: PropTypes.node, - }; - +export class ComponentSettings extends React.Component< + ComponentSettingsProps, + {} +> { static childContextTypes = { assetRoot: PropTypes.string, portalClassName: PropTypes.string, portalStyle: PropTypes.object, // eslint-disable-line react/forbid-prop-types }; - getChildContext() { + getChildContext(): ComponentSettingsContext { const { assetRoot, portalClassName, portalStyle } = this.props; return { assetRoot, portalClassName, portalStyle }; } diff --git a/src/scripts/Container.js b/src/scripts/Container.js deleted file mode 100644 index 498b4824e..000000000 --- a/src/scripts/Container.js +++ /dev/null @@ -1,29 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import classnames from 'classnames'; - -const Container = ({ className, size, align, children, ...props }) => { - const ctClassNames = classnames( - className, - `slds-container--${size || 'fluid'}`, - align ? `slds-container--${align}` : null - ); - return ( -
- {children} -
- ); -}; - -const CONTAINER_SIZES = ['small', 'medium', 'large']; - -const CONTAINER_ALIGNS = ['left', 'center', 'right']; - -Container.propTypes = { - className: PropTypes.string, - size: PropTypes.oneOf(CONTAINER_SIZES), - align: PropTypes.oneOf(CONTAINER_ALIGNS), - children: PropTypes.element, -}; - -export default Container; diff --git a/src/scripts/Container.tsx b/src/scripts/Container.tsx new file mode 100644 index 000000000..b307560d2 --- /dev/null +++ b/src/scripts/Container.tsx @@ -0,0 +1,27 @@ +import React, { HTMLAttributes } from 'react'; +import classnames from 'classnames'; + +export type ContainerProps = { + className: string; + size: 'small' | 'medium' | 'large'; + align: 'left' | 'center' | 'right'; +} & HTMLAttributes; + +export const Container: React.FC = ({ + className, + size, + align, + children, + ...props +}) => { + const ctClassNames = classnames( + className, + `slds-container--${size || 'fluid'}`, + align ? `slds-container--${align}` : null + ); + return ( +
+ {children} +
+ ); +}; diff --git a/src/scripts/DateInput.js b/src/scripts/DateInput.tsx similarity index 71% rename from src/scripts/DateInput.js rename to src/scripts/DateInput.tsx index d87953b20..8a67baeaf 100644 --- a/src/scripts/DateInput.js +++ b/src/scripts/DateInput.tsx @@ -1,31 +1,30 @@ import React, { Component } from 'react'; -import PropTypes from 'prop-types'; import classnames from 'classnames'; import moment from 'moment'; -import autoAlign from './AutoAlign'; -import FormElement from './FormElement'; -import Input from './Input'; -import Icon from './Icon'; -import Datepicker from './Datepicker'; +import { autoAlign, InjectedProps } from './AutoAlign'; +import { FormElement, FormElementProps } from './FormElement'; +import { Input, InputProps } from './Input'; +import { Icon } from './Icon'; +import { Datepicker } from './Datepicker'; import { uuid, isElInChildren, registerStyle } from './util'; +export type DatepickerDropdownProps = { + className?: string; + dateValue?: string; + minDate?: string; + maxDate?: string; + elementRef?: (node: HTMLDivElement) => void; + extensionRenderer?: (...props: any[]) => JSX.Element; + onSelect?: (date: string) => void; + onBlur?: (e: React.FocusEvent) => void; + onClose?: () => void; +} & InjectedProps; + /** * */ -class DatepickerDropdown extends Component { - static propTypes = { - className: PropTypes.string, - align: PropTypes.oneOf(['left', 'right']), - vertAlign: PropTypes.oneOf(['top', 'bottom']), - dateValue: PropTypes.string, - minDate: PropTypes.string, - maxDate: PropTypes.string, - elementRef: PropTypes.func, - extensionRenderer: PropTypes.func, - onSelect: PropTypes.func, - onBlur: PropTypes.func, - onClose: PropTypes.func, - }; +class DatepickerDropdown extends Component { + node: HTMLDivElement | null = null; render() { const { @@ -47,7 +46,7 @@ class DatepickerDropdown extends Component { align ? `slds-dropdown--${align}` : undefined, vertAlign ? `slds-dropdown--${vertAlign}` : undefined ); - const handleDOMRef = (node) => { + const handleDOMRef = (node: HTMLDivElement) => { this.node = node; if (elementRef) { elementRef(node); @@ -74,12 +73,53 @@ const DatepickerDropdownPortal = autoAlign({ triggerSelector: '.slds-dropdown-trigger', })(DatepickerDropdown); +export type DateInputProps = { + id?: string; + className?: string; + label?: string; + required?: boolean; + error?: FormElementProps['error']; + totalCols?: number; + cols?: number; + value?: string; + defaultValue?: string; + defaultOpened?: boolean; + dateFormat?: string; + includeTime?: boolean; + minDate?: string; + maxDate?: string; + menuAlign?: 'left' | 'right'; + onKeyDown?: (e: React.KeyboardEvent) => void; + onBlur?: () => void; + onChange?: (e: React.ChangeEvent, value: string) => void; + onValueChange?: ( + value: string | undefined, + prevValue: string | undefined + ) => void; + onComplete?: () => void; + extensionRenderer?: (...props: any[]) => JSX.Element; +} & InputProps; + +export type DateInputState = { + id: string; + opened: boolean; + inputValue?: string; + value?: string; +}; /** * */ -export default class DateInput extends Component { - constructor(props) { - super(); +export class DateInput extends Component { + static isFormElement = true; + + node: HTMLDivElement | null = null; + + datepicker: HTMLDivElement | null = null; + + input: HTMLInputElement | null = null; + + constructor(props: Readonly) { + super(props); this.state = { id: `form-element-${uuid()}`, opened: props.defaultOpened || false, @@ -102,7 +142,7 @@ export default class DateInput extends Component { ]); } - componentDidUpdate(prevProps, prevState) { + componentDidUpdate(prevProps: DateInputProps, prevState: DateInputState) { if (this.props.onValueChange && prevState.value !== this.state.value) { this.props.onValueChange(this.state.value, prevState.value); } @@ -114,15 +154,19 @@ export default class DateInput extends Component { }, 10); } - onInputKeyDown(e) { + onInputKeyDown(e: React.KeyboardEvent) { if (e.keyCode === 13) { // return key e.preventDefault(); e.stopPropagation(); - this.setValueFromInput(e.target.value); + if ((e.target as any).value !== undefined) { + this.setValueFromInput((e.target as any).value); + } if (this.props.onComplete) { setTimeout(() => { - this.props.onComplete(); + if (this.props.onComplete) { + this.props.onComplete(); + } }, 10); } } else if (e.keyCode === 40) { @@ -136,7 +180,7 @@ export default class DateInput extends Component { } } - onInputChange(e) { + onInputChange(e: React.ChangeEvent) { const inputValue = e.target.value; this.setState({ inputValue }); if (this.props.onChange) { @@ -144,7 +188,7 @@ export default class DateInput extends Component { } } - onInputBlur(e) { + onInputBlur(e: React.FocusEvent) { this.setValueFromInput(e.target.value); setTimeout(() => { if (!this.isFocusedInComponent()) { @@ -158,7 +202,7 @@ export default class DateInput extends Component { }, 10); } - onDatepickerSelect(dvalue) { + onDatepickerSelect(dvalue: string) { const value = moment(dvalue).format(this.getValueFormat()); this.setState({ value, inputValue: undefined }); setTimeout(() => { @@ -205,14 +249,14 @@ export default class DateInput extends Component { return this.props.dateFormat || (this.props.includeTime ? 'L HH:mm' : 'L'); } - setValueFromInput(inputValue) { + setValueFromInput(inputValue: string) { let { value } = this.state; if (!inputValue) { value = ''; } else { - value = moment(inputValue, this.getInputValueFormat()); - if (value.isValid()) { - value = value.format(this.getValueFormat()); + const mvalue = moment(inputValue, this.getInputValueFormat()); + if (mvalue.isValid()) { + value = mvalue.format(this.getValueFormat()); } else { value = ''; } @@ -231,9 +275,9 @@ export default class DateInput extends Component { showDatepicker() { let { value } = this.state; if (typeof this.state.inputValue !== 'undefined') { - value = moment(this.state.inputValue, this.getInputValueFormat()); - if (value.isValid()) { - value = value.format(this.getValueFormat()); + const mvalue = moment(this.state.inputValue, this.getInputValueFormat()); + if (mvalue.isValid()) { + value = mvalue.format(this.getValueFormat()); } else { // eslint-disable-next-line prefer-destructuring value = this.state.value; @@ -242,7 +286,7 @@ export default class DateInput extends Component { this.setState({ opened: true, value }); } - renderInput({ inputValue, ...props }) { + renderInput({ inputValue, ...props }: any) { const pprops = props; delete pprops.onValueChange; return ( @@ -316,7 +360,7 @@ export default class DateInput extends Component { {this.state.opened ? ( (this.datepicker = node)} + elementRef={(node: HTMLDivElement) => (this.datepicker = node)} dateValue={ mvalue.isValid() ? mvalue.format('YYYY-MM-DD') : undefined } @@ -336,31 +380,3 @@ export default class DateInput extends Component { ); } } - -const MENU_ALIGN = ['left', 'right']; - -DateInput.propTypes = { - id: PropTypes.string, - className: PropTypes.string, - label: PropTypes.string, - required: PropTypes.bool, - error: FormElement.propTypes.error, - totalCols: PropTypes.number, - cols: PropTypes.number, - value: PropTypes.string, - defaultValue: PropTypes.string, - defaultOpened: PropTypes.bool, - dateFormat: PropTypes.string, - includeTime: PropTypes.bool, - onKeyDown: PropTypes.func, - onBlur: PropTypes.func, - onChange: PropTypes.func, - onValueChange: PropTypes.func, - onComplete: PropTypes.func, - menuAlign: PropTypes.oneOf(MENU_ALIGN), - minDate: PropTypes.string, - maxDate: PropTypes.string, - extensionRenderer: PropTypes.func, -}; - -DateInput.isFormElement = true; diff --git a/src/scripts/Datepicker.js b/src/scripts/Datepicker.tsx similarity index 79% rename from src/scripts/Datepicker.js rename to src/scripts/Datepicker.tsx index 095c0ce19..5a9d9e658 100644 --- a/src/scripts/Datepicker.js +++ b/src/scripts/Datepicker.tsx @@ -1,12 +1,26 @@ import React, { Component } from 'react'; -import PropTypes from 'prop-types'; import classnames from 'classnames'; import moment from 'moment'; import { Button } from './Button'; -import Select, { Option } from './Select'; +import { Select, Option } from './Select'; import { getToday, isElInChildren } from './util'; -function createCalendarObject(date, mnDate, mxDate) { +type Date = { + year: number; + month: number; + date: number; + value: string; +}; + +type Calendar = { + year: number; + month: number; + weeks: Date[][]; + minDate?: Date; + maxDate?: Date; +}; + +function createCalendarObject(date?: string, mnDate?: string, mxDate?: string) { let minDate; let maxDate; let d = moment(date, 'YYYY-MM-DD'); @@ -57,7 +71,7 @@ function createCalendarObject(date, mnDate, mxDate) { days = []; } } - const cal = { year, month, weeks }; + const cal: Calendar = { year, month, weeks }; if (minDate) { cal.minDate = minDate; } @@ -67,14 +81,36 @@ function createCalendarObject(date, mnDate, mxDate) { return cal; } -function cancelEvent(e) { +function cancelEvent(e: React.FocusEvent) { e.preventDefault(); e.stopPropagation(); } -export default class Datepicker extends Component { - constructor() { - super(); +export type DatepickerProps = { + className?: string; + selectedDate?: string; + autoFocus?: boolean; + minDate?: string; + maxDate?: string; + extensionRenderer?: (...props: any[]) => JSX.Element; + elementRef?: (node: HTMLDivElement) => void; + onSelect?: (date: string) => void; + onBlur?: (e: React.FocusEvent) => void; + onClose?: () => void; +}; + +export type DatepickerState = { + focusDate?: boolean; + targetDate?: string; +}; + +export class Datepicker extends Component { + node: HTMLDivElement | null = null; + + month: HTMLTableElement | null = null; + + constructor(props: Readonly) { + super(props); this.state = {}; this.onBlur = this.onBlur.bind(this); @@ -101,8 +137,8 @@ export default class Datepicker extends Component { } } - onDateKeyDown(date, e) { - let targetDate = this.state.targetDate || this.props.selectedDate; + onDateKeyDown(date: string, e: React.KeyboardEvent) { + let targetDate: any = this.state.targetDate || this.props.selectedDate; if (e.keyCode === 13 || e.keyCode === 32) { // return / space this.onDateClick(date); @@ -129,13 +165,13 @@ export default class Datepicker extends Component { } } - onDateClick(date) { + onDateClick(date: string) { if (this.props.onSelect) { this.props.onSelect(date); } } - onDateFocus(date) { + onDateFocus(date: string) { if (this.state.targetDate !== date) { setTimeout(() => { this.setState({ targetDate: date }); @@ -143,16 +179,16 @@ export default class Datepicker extends Component { } } - onYearChange(e, item) { + onYearChange(e: React.ChangeEvent) { // eslint-disable-next-line react/no-access-state-in-setstate let targetDate = this.state.targetDate || this.props.selectedDate; targetDate = moment(targetDate) - .year(item) + .year(Number(e.target.value)) .format('YYYY-MM-DD'); this.setState({ targetDate }); } - onMonthChange(month) { + onMonthChange(month: number) { // eslint-disable-next-line react/no-access-state-in-setstate let targetDate = this.state.targetDate || this.props.selectedDate; targetDate = moment(targetDate) @@ -161,7 +197,7 @@ export default class Datepicker extends Component { this.setState({ targetDate }); } - onBlur(e) { + onBlur(e: React.FocusEvent) { setTimeout(() => { if (!this.isFocusedInComponent()) { if (this.props.onBlur) { @@ -171,7 +207,7 @@ export default class Datepicker extends Component { }, 10); } - onKeyDown(e) { + onKeyDown(e: React.KeyboardEvent) { if (e.keyCode === 27) { // ESC if (this.props.onClose) { @@ -180,12 +216,14 @@ export default class Datepicker extends Component { } } - focusDate(date) { + focusDate(date: string | undefined) { const el = this.month; if (!el) { return; } - const dateEl = el.querySelector(`.slds-day[data-date-value="${date}"]`); + const dateEl: HTMLSpanElement | null = el.querySelector( + `.slds-day[data-date-value="${date}"]` + ); if (dateEl) { dateEl.focus(); } @@ -195,8 +233,7 @@ export default class Datepicker extends Component { return isElInChildren(this.node, document.activeElement); } - renderFilter(cal) { - /* eslint-disable max-len */ + renderFilter(cal: Calendar) { return (
@@ -239,7 +276,7 @@ export default class Datepicker extends Component { ); } - renderMonth(cal, selectedDate, today) { + renderMonth(cal: Calendar, selectedDate: string | undefined, today: string) { return ( @@ -330,7 +377,7 @@ export default class Datepicker extends Component { const targetDate = this.state.targetDate || selectedDate; const cal = createCalendarObject(targetDate, minDate, maxDate); const datepickerClassNames = classnames('slds-datepicker', className); - const handleDOMRef = (node) => { + const handleDOMRef = (node: HTMLDivElement) => { this.node = node; if (elementRef) { elementRef(node); @@ -352,16 +399,3 @@ export default class Datepicker extends Component { ); } } - -Datepicker.propTypes = { - className: PropTypes.string, - selectedDate: PropTypes.string, - autoFocus: PropTypes.bool, - minDate: PropTypes.string, - maxDate: PropTypes.string, - extensionRenderer: PropTypes.func, - elementRef: PropTypes.func, - onSelect: PropTypes.func, - onBlur: PropTypes.func, - onClose: PropTypes.func, -}; diff --git a/src/scripts/DropdownButton.js b/src/scripts/DropdownButton.tsx similarity index 75% rename from src/scripts/DropdownButton.js rename to src/scripts/DropdownButton.tsx index bbb88cf66..47f98fa33 100644 --- a/src/scripts/DropdownButton.js +++ b/src/scripts/DropdownButton.tsx @@ -1,13 +1,45 @@ import React, { Component } from 'react'; -import PropTypes from 'prop-types'; import classnames from 'classnames'; -import { Button } from './Button'; -import DropdownMenu from './DropdownMenu'; -import { registerStyle, isElInChildren, offset } from './util'; +import { Button, ButtonProps } from './Button'; +import { DropdownMenu } from './DropdownMenu'; +import { registerStyle, isElInChildren } from './util'; + +export type DropdownMenuAlign = 'left' | 'right'; +export type DropdownMenuSize = 'small' | 'medium' | 'large'; +export type DropdownButtonProps = { + className?: string; + label?: React.ReactNode; + menuAlign?: DropdownMenuAlign; + menuSize?: DropdownMenuSize; + menuHeader?: string; + nubbinTop?: boolean; + hoverPopup?: boolean; + grouped?: boolean; + isFirstInGroup?: boolean; + isLastInGroup?: boolean; + style?: object; + menuStyle?: object; + onBlur?: (...args: any[]) => any; + onClick?: (...args: any[]) => any; + onMenuItemClick?: (...args: any[]) => any; +} & ButtonProps; + +type DropdownButtonState = { + opened: boolean; +}; + +export class DropdownButton extends Component< + DropdownButtonProps, + DropdownButtonState +> { + node: HTMLDivElement | null = null; + + trigger: HTMLButtonElement | null = null; -export default class DropdownButton extends Component { - constructor() { - super(); + dropdown: HTMLDivElement | null = null; + + constructor(props: Readonly) { + super(props); this.state = { opened: false }; registerStyle('no-hover-popup', [ [ @@ -32,7 +64,7 @@ export default class DropdownButton extends Component { }, 10); } - onKeyDown(e) { + onKeyDown(e: React.KeyboardEvent) { if (e.keyCode === 40) { // down e.preventDefault(); @@ -56,7 +88,7 @@ export default class DropdownButton extends Component { } } - onTriggerClick(...args) { + onTriggerClick(...args: any[]) { if (!this.props.hoverPopup) { this.setState((prevState) => ({ opened: !prevState.opened })); } @@ -65,7 +97,7 @@ export default class DropdownButton extends Component { } } - onMenuItemClick(...args) { + onMenuItemClick(...args: any[]) { if (!this.props.hoverPopup) { setTimeout(() => { const triggerElem = this.trigger; @@ -79,28 +111,12 @@ export default class DropdownButton extends Component { } onMenuClose() { - this.trigger.focus(); + if (this.trigger) { + this.trigger.focus(); + } this.setState({ opened: false }); } - getStyles() { - const triggerOffset = offset(this.trigger); - const dropdownOffset = offset(this.dropdown); - const triggerPadding = 5; - const nubbinHeight = 8; - const top = - -1 * - (dropdownOffset.top - - triggerOffset.top - - this.trigger.offsetHeight - - triggerPadding); - return { - dropdownOffset: { - marginTop: `${top + (this.props.nubbinTop ? nubbinHeight : 0)}px`, - }, - }; - } - isFocusedInComponent() { const targetEl = document.activeElement; return ( @@ -114,7 +130,7 @@ export default class DropdownButton extends Component { if (!dropdownEl) { return; } - const firstItemEl = + const firstItemEl: HTMLAnchorElement | null = dropdownEl.querySelector( '.slds-is-selected > .react-slds-menuitem[tabIndex]' ) || dropdownEl.querySelector('.react-slds-menuitem[tabIndex]'); @@ -123,7 +139,7 @@ export default class DropdownButton extends Component { } } - renderButton({ grouped, isFirstInGroup, isLastInGroup, ...props }) { + renderButton({ grouped, isFirstInGroup, isLastInGroup, ...props }: any) { const pprops = props; delete pprops.onMenuItemClick; const button = ( @@ -213,25 +229,3 @@ export default class DropdownButton extends Component { ); } } - -DropdownButton.propTypes = { - className: PropTypes.string, - label: PropTypes.node, - type: PropTypes.string, - icon: PropTypes.string, - menuAlign: PropTypes.oneOf(['left', 'center', 'right']), - menuSize: PropTypes.oneOf(['small', 'medium', 'large']), - menuHeader: PropTypes.string, - nubbinTop: PropTypes.bool, - hoverPopup: PropTypes.bool, - onBlur: PropTypes.func, - onClick: PropTypes.func, - onMenuItemClick: PropTypes.func, - grouped: PropTypes.bool, - isFirstInGroup: PropTypes.bool, - isLastInGroup: PropTypes.bool, - children: PropTypes.node, - /* eslint-disable react/forbid-prop-types */ - style: PropTypes.object, - menuStyle: PropTypes.object, -}; diff --git a/src/scripts/DropdownMenu.js b/src/scripts/DropdownMenu.tsx similarity index 60% rename from src/scripts/DropdownMenu.js rename to src/scripts/DropdownMenu.tsx index 2440cf7fc..8643d9735 100644 --- a/src/scripts/DropdownMenu.js +++ b/src/scripts/DropdownMenu.tsx @@ -1,11 +1,19 @@ -import React, { Component } from 'react'; -import PropTypes from 'prop-types'; +import React, { Component, ComponentType, AnchorHTMLAttributes } from 'react'; import classnames from 'classnames'; -import Icon from './Icon'; -import autoAlign from './AutoAlign'; +import { Icon } from './Icon'; +import { autoAlign, InjectedProps, AutoAlignProps } from './AutoAlign'; import { PicklistItem } from './Picklist'; -export const DropdownMenuHeader = ({ divider, className, children }) => { +export type DropdownMenuHeaderProps = { + className?: string; + divider?: 'top' | 'bottom'; +}; + +export const DropdownMenuHeader: React.FC = ({ + divider, + className, + children, +}) => { const menuHeaderClass = classnames( 'slds-dropdown__header', { [`slds-has-divider--${divider}-space`]: divider }, @@ -18,22 +26,30 @@ export const DropdownMenuHeader = ({ divider, className, children }) => { ); }; -DropdownMenuHeader.propTypes = { - className: PropTypes.string, - divider: PropTypes.oneOf(['top', 'bottom']), - children: PropTypes.node, -}; - export const MenuHeader = DropdownMenuHeader; -export class DropdownMenuItem extends Component { - onKeyDown(e, ...args) { +export type DropdownMenuItemProps = { + className?: string; + label?: string; + icon?: string; + iconRight?: string; + disabled?: boolean; + divider?: 'top' | 'bottom'; + tabIndex?: number; + selected?: boolean; + onClick?: (e: React.MouseEvent) => void; + onBlur?: (e: React.FocusEvent) => void; + onFocus?: (e: React.FocusEvent) => void; +} & AnchorHTMLAttributes; + +export class DropdownMenuItem extends Component { + onKeyDown(e: any) { if (e.keyCode === 13 || e.keyCode === 32) { // return or space e.preventDefault(); e.stopPropagation(); if (this.props.onClick) { - this.props.onClick(e, ...args); + this.props.onClick(e); } } else if (e.keyCode === 40 || e.keyCode === 38) { e.preventDefault(); @@ -52,13 +68,13 @@ export class DropdownMenuItem extends Component { } } - onBlur(e) { + onBlur(e: React.FocusEvent) { if (this.props.onBlur) { this.props.onBlur(e); } } - onFocus(e) { + onFocus(e: React.FocusEvent) { if (this.props.onFocus) { this.props.onFocus(e); } @@ -87,16 +103,17 @@ export class DropdownMenuItem extends Component { className ); return ( -
  • +
  • + {/* eslint-disable-next-line jsx-a11y/interactive-supports-focus */}

    @@ -112,37 +129,46 @@ export class DropdownMenuItem extends Component { } } -DropdownMenuItem.propTypes = { - className: PropTypes.string, - label: PropTypes.string, - icon: PropTypes.string, - iconRight: PropTypes.string, - disabled: PropTypes.bool, - divider: PropTypes.oneOf(['top', 'bottom']), - tabIndex: PropTypes.number, - selected: PropTypes.bool, - onClick: PropTypes.func, - onBlur: PropTypes.func, - onFocus: PropTypes.func, - children: PropTypes.node, +export const MenuItem = DropdownMenuItem; + +export type DropdownMenuProps = { + className?: string; + size?: 'small' | 'medium' | 'large'; + header?: string; + nubbin?: + | 'top' + | 'top left' + | 'top right' + | 'bottom' + | 'bottom left' + | 'bottom right' + | 'auto'; + nubbinTop?: boolean; // for backward compatibility. use nubbin instead + hoverPopup?: boolean; + onMenuItemClick?: (props: any, ...args: any[]) => void; + onMenuClose?: () => void; + onBlur?: (e: any) => void; + onFocus?: (e: any) => void; + dropdownMenuRef?: (node: HTMLDivElement) => void; + style?: object; }; -export const MenuItem = DropdownMenuItem; +class WrappedDropdownMenu extends Component { + node: HTMLDivElement | null = null; -class DropdownMenu extends Component { - onMenuItemBlur(e) { + onMenuItemBlur(e: any) { if (this.props.onBlur) { this.props.onBlur(e); } } - onMenuItemFocus(e) { + onMenuItemFocus(e: any) { if (this.props.onFocus) { this.props.onFocus(e); } } - onKeyDown(e) { + onKeyDown(e: React.KeyboardEvent) { if (e.keyCode === 27) { // ESC if (this.props.onMenuClose) { @@ -151,9 +177,9 @@ class DropdownMenu extends Component { } } - renderMenuItem(menuItem) { + renderMenuItem(menuItem: any) { const { onClick, onBlur, onFocus, ...props } = menuItem.props; - const onMenuItemClick = (...args) => { + const onMenuItemClick = (...args: any[]) => { if (onClick) { onClick(...args); } @@ -161,13 +187,13 @@ class DropdownMenu extends Component { this.props.onMenuItemClick(props, ...args); } }; - const onMenuItemFocus = (e) => { + const onMenuItemFocus = (e: any) => { if (onFocus) { onFocus(e); } this.onMenuItemFocus(e); }; - const onMenuItemBlur = (e) => { + const onMenuItemBlur = (e: any) => { if (onBlur) { onBlur(e); } @@ -208,7 +234,7 @@ class DropdownMenu extends Component { : undefined, { 'react-slds-no-hover-popup': !hoverPopup } ); - const handleDOMRef = (node) => { + const handleDOMRef = (node: HTMLDivElement) => { this.node = node; if (dropdownMenuRef) { dropdownMenuRef(node); @@ -220,13 +246,13 @@ class DropdownMenu extends Component { ref={handleDOMRef} style={{ outline: 'none', ...style }} onKeyDown={this.onKeyDown.bind(this)} - tabIndex='-1' + tabIndex={-1} onFocus={onFocus} onBlur={onBlur} > {header ? {header} : null}

      - {React.Children.map(children, (item) => + {React.Children.map(children, (item: any) => item.type === MenuItem || item.type === PicklistItem ? this.renderMenuItem(item) : item @@ -237,40 +263,18 @@ class DropdownMenu extends Component { } } -DropdownMenu.propTypes = { - className: PropTypes.string, - align: PropTypes.oneOf(['left', 'right']), - vertAlign: PropTypes.oneOf(['top', 'bottom']), - size: PropTypes.oneOf(['small', 'medium', 'large']), - header: PropTypes.string, - nubbin: PropTypes.oneOf([ - 'top', - 'top left', - 'top right', - 'bottom', - 'bottom left', - 'bottom right', - 'auto', - ]), - nubbinTop: PropTypes.bool, // for backward compatibility. use nubbin instead - hoverPopup: PropTypes.bool, - onMenuItemClick: PropTypes.func, - onMenuClose: PropTypes.func, - onBlur: PropTypes.func, - onFocus: PropTypes.func, - children: PropTypes.node, - dropdownMenuRef: PropTypes.func, - /* eslint-disable react/forbid-prop-types */ - style: PropTypes.object, -}; - -function preventPortalizeOnHoverPopup(Cmp) { - // eslint-disable-next-line react/prop-types - return (props) => ; +function preventPortalizeOnHoverPopup( + Cmp: ComponentType +) { + type ResultProps = DropdownMenuProps & AutoAlignProps; + const Result: React.FC = (props) => ( + + ); + return Result; } -export default preventPortalizeOnHoverPopup( +export const DropdownMenu = preventPortalizeOnHoverPopup( autoAlign({ triggerSelector: '.slds-dropdown-trigger', - })(DropdownMenu) + })(WrappedDropdownMenu) ); diff --git a/src/scripts/FieldSet.js b/src/scripts/FieldSet.tsx similarity index 71% rename from src/scripts/FieldSet.js rename to src/scripts/FieldSet.tsx index d5e0cc1b5..0e0d8d503 100644 --- a/src/scripts/FieldSet.js +++ b/src/scripts/FieldSet.tsx @@ -1,10 +1,20 @@ import React, { Component } from 'react'; -import PropTypes from 'prop-types'; import classnames from 'classnames'; -import FormElement from './FormElement'; +import { FormElement } from './FormElement'; import { uuid } from './util'; -const FieldSet = ({ className, label, children, ...props }) => { +export type FieldSetProps = { + className?: string; + label?: string; + children?: React.ReactNode; +}; + +export function FieldSet({ + className, + label, + children, + ...props +}: FieldSetProps) { const fsClassNames = classnames(className, 'slds-form--compound'); return (
      @@ -14,18 +24,19 @@ const FieldSet = ({ className, label, children, ...props }) => {
      {children}
      ); -}; +} + +FieldSet.isFormElement = true; -FieldSet.propTypes = { - className: PropTypes.string, - label: PropTypes.string, - children: PropTypes.node, +type RowProps = { + className?: string; + cols?: number; }; -FieldSet.isFormElement = true; +class Row extends Component { + static isFormElement = true; -class Row extends Component { - renderChild(totalCols, child) { + renderChild(totalCols: number, child: any) { if (child && !child.type.isFormElement) { const { id = `form-element-${uuid()}` } = child.props; const formElemProps = { id, totalCols, cols: 1 }; @@ -50,14 +61,4 @@ class Row extends Component { } } -Row.propTypes = { - className: PropTypes.string, - cols: PropTypes.number, - children: PropTypes.node, -}; - -Row.isFormElement = true; - FieldSet.Row = Row; - -export default FieldSet; diff --git a/src/scripts/Form.js b/src/scripts/Form.tsx similarity index 61% rename from src/scripts/Form.js rename to src/scripts/Form.tsx index 5cdcfb86d..255bacfe3 100644 --- a/src/scripts/Form.js +++ b/src/scripts/Form.tsx @@ -1,17 +1,25 @@ -import React, { Component } from 'react'; -import PropTypes from 'prop-types'; +import React, { Component, FormHTMLAttributes } from 'react'; import classnames from 'classnames'; -import FormElement from './FormElement'; +import { FormElement } from './FormElement'; import { uuid } from './util'; -export default class Form extends Component { - constructor() { - super(); +export type FormProps = { + className?: string; + type?: 'stacked' | 'horizontal' | 'inline' | 'compound'; +} & FormHTMLAttributes; + +export class Form extends Component { + static defaultProps: Pick = { + type: 'stacked', + }; + + constructor(props: Readonly) { + super(props); this.renderFormElement = this.renderFormElement.bind(this); } - renderFormElement(element) { + renderFormElement(element: any) { if (element && !element.type.isFormElement) { const { id = `form-element-${uuid()}` } = element.props; const formElemProps = { id }; @@ -34,15 +42,3 @@ export default class Form extends Component { ); } } - -const FORM_TYPES = ['stacked', 'horizontal', 'inline', 'compound']; - -Form.propTypes = { - className: PropTypes.string, - type: PropTypes.oneOf(FORM_TYPES), - children: PropTypes.node, -}; - -Form.defaultProps = { - type: 'stacked', -}; diff --git a/src/scripts/FormElement.js b/src/scripts/FormElement.tsx similarity index 73% rename from src/scripts/FormElement.js rename to src/scripts/FormElement.tsx index dd4896757..0987f7114 100644 --- a/src/scripts/FormElement.js +++ b/src/scripts/FormElement.tsx @@ -1,9 +1,24 @@ import React from 'react'; -import PropTypes from 'prop-types'; import classnames from 'classnames'; -export default class FormElement extends React.Component { - renderFormElement(props) { +export type FormElementProps = { + id?: string; + className?: string; + label?: string; + required?: boolean; + error?: boolean | string | { message: string }; + readOnly?: boolean; + cols?: number; + totalCols?: number; + dropdown?: JSX.Element; + formElementRef?: (node: HTMLDivElement) => void; + style?: object; +}; + +export class FormElement extends React.Component { + static isFormElement = true; + + renderFormElement(props: any) { const { className, error, @@ -47,7 +62,7 @@ export default class FormElement extends React.Component { ); } - renderControl(props) { + renderControl(props: { children: any; dropdown: any; error: any }) { const { children, dropdown, error } = props; const { readOnly } = this.props; const formElementControlClassNames = classnames( @@ -63,7 +78,7 @@ export default class FormElement extends React.Component { ); } - renderError(error) { + renderError(error: any) { const errorMessage = error ? typeof error === 'string' ? error @@ -105,30 +120,3 @@ export default class FormElement extends React.Component { }); } } - -FormElement.propTypes = { - id: PropTypes.string, - className: PropTypes.string, - label: PropTypes.string, - required: PropTypes.bool, - error: PropTypes.oneOfType([ - PropTypes.bool, - PropTypes.string, - PropTypes.shape({ - message: PropTypes.string, - }), - ]), - readOnly: PropTypes.bool, - cols: PropTypes.number, - totalCols: PropTypes.number, - dropdown: PropTypes.element, - children: PropTypes.oneOfType([ - PropTypes.element, - PropTypes.arrayOf(PropTypes.element), - ]), - formElementRef: PropTypes.func, - /* eslint-disable react/forbid-prop-types */ - style: PropTypes.object, -}; - -FormElement.isFormElement = true; diff --git a/src/scripts/Grid.js b/src/scripts/Grid.tsx similarity index 62% rename from src/scripts/Grid.js rename to src/scripts/Grid.tsx index 944ee2217..4481f05e9 100644 --- a/src/scripts/Grid.js +++ b/src/scripts/Grid.tsx @@ -1,8 +1,21 @@ -import React, { Component } from 'react'; -import PropTypes from 'prop-types'; +import React, { Component, ReactHTML } from 'react'; import classnames from 'classnames'; -const Grid = ({ className, frame, vertical, children, tag, ...props }) => { +export type GridProps = { + className?: string; + tag?: keyof ReactHTML; + frame?: boolean; + vertical?: boolean; +} & React.HTMLAttributes; + +export const Grid: React.FC = ({ + className, + frame, + vertical, + children, + tag, + ...props +}) => { const gridClassNames = classnames( className, 'slds-grid', @@ -17,26 +30,37 @@ const Grid = ({ className, frame, vertical, children, tag, ...props }) => { ); }; -Grid.propTypes = { - tag: PropTypes.string, - className: PropTypes.string, - frame: PropTypes.bool, - children: PropTypes.node, - vertical: PropTypes.bool, -}; - Grid.defaultProps = { vertical: true, }; -function adjustCols(colNum, large) { +function adjustCols(colNum: number, large?: boolean) { if (colNum > 6) { return large ? 12 : 6; } return colNum; } -export const Col = (props) => { +export type ColProps = { + className?: string; + padded?: boolean | 'medium' | 'large'; + align?: 'top' | 'medium' | 'bottom'; + noFlex?: boolean; + order?: number; + orderSmall?: number; + orderMedium?: number; + orderLarge?: number; + cols?: number; + colsSmall?: number; + colsMedium?: number; + colsLarge?: number; + totalCols?: number; + totalColsSmall?: number; + totalColsMedium?: number; + totalColsLarge?: number; +} & React.HTMLAttributes; + +export const Col: React.FC = (props) => { const { className, padded, @@ -60,7 +84,11 @@ export const Col = (props) => { const rowClassNames = classnames( className, padded - ? `slds-col--padded${/^(medium|large)$/.test(padded) ? `-${padded}` : ''}` + ? `slds-col--padded${ + typeof padded === 'string' && /^(medium|large)$/.test(padded) + ? `-${padded}` + : '' + }` : 'slds-col', align ? `slds-align-${align}` : null, noFlex ? 'slds-no-flex' : null, @@ -77,7 +105,7 @@ export const Col = (props) => { colsMedium && totalColsMedium ? `slds-medium-size--${colsMedium}-of-${adjustCols(totalColsMedium)}` : null, - colsLarge && totalColsMedium + colsLarge && totalColsLarge ? `slds-large-size--${colsLarge}-of-${adjustCols(totalColsLarge, true)}` : null ); @@ -88,42 +116,28 @@ export const Col = (props) => { ); }; -const COL_ALIGNS = ['top', 'medium', 'bottom']; - -Col.propTypes = { - className: PropTypes.string, - padded: PropTypes.oneOfType([PropTypes.bool, PropTypes.string]), - align: PropTypes.oneOf(COL_ALIGNS), - noFlex: PropTypes.bool, - order: PropTypes.number, - orderSmall: PropTypes.number, - orderMedium: PropTypes.number, - orderLarge: PropTypes.number, - cols: PropTypes.number, - colsSmall: PropTypes.number, - colsMedium: PropTypes.number, - colsLarge: PropTypes.number, - totalCols: PropTypes.number, - totalColsSmall: PropTypes.number, - totalColsMedium: PropTypes.number, - totalColsLarge: PropTypes.number, - children: PropTypes.node, -}; - -Grid.propTypes = { - className: PropTypes.string, - frame: PropTypes.bool, - children: PropTypes.node, -}; +export type RowProps = { + className?: string; + align?: 'center' | 'space' | 'spread'; + nowrap?: boolean; + nowrapSmall?: boolean; + nowrapMedium?: boolean; + nowrapLarge?: boolean; + pullPadded?: boolean; + cols?: number; + colsSmall?: number; + colsMedium?: number; + colsLarge?: number; +} & React.HTMLAttributes; -export class Row extends Component { - renderColumn(colProps, child) { +export class Row extends Component { + renderColumn(colProps: any, child: any) { if (child.type !== Col) { return
  • {child}; } /* eslint-disable no-param-reassign */ - const childProps = Object.keys(colProps).reduce((cprops, key) => { + const childProps = Object.keys(colProps).reduce((cprops: any, key) => { cprops[key] = child.props[key] || colProps[key]; return cprops; }, {}); @@ -162,7 +176,7 @@ export class Row extends Component { let cnt = 0; React.Children.forEach(children, (child) => { if (!React.isValidElement(child)) return; - cnt += child.props.cols || 1; + cnt += (child as any).props.cols || 1; }); return cnt; })(); @@ -179,22 +193,3 @@ export class Row extends Component { ); } } - -const ROW_ALIGNS = ['center', 'space', 'spread']; - -Row.propTypes = { - className: PropTypes.string, - align: PropTypes.oneOf(ROW_ALIGNS), - nowrap: PropTypes.bool, - nowrapSmall: PropTypes.bool, - nowrapMedium: PropTypes.bool, - nowrapLarge: PropTypes.bool, - pullPadded: PropTypes.bool, - cols: PropTypes.number, - colsSmall: PropTypes.number, - colsMedium: PropTypes.number, - colsLarge: PropTypes.number, - children: PropTypes.node, -}; - -export default Grid; diff --git a/src/scripts/Icon.js b/src/scripts/Icon.tsx similarity index 83% rename from src/scripts/Icon.js rename to src/scripts/Icon.tsx index 851523289..ad41cbf6b 100644 --- a/src/scripts/Icon.js +++ b/src/scripts/Icon.tsx @@ -1,8 +1,9 @@ -import React, { Component } from 'react'; +import React, { Component, SVGAttributes } from 'react'; import PropTypes from 'prop-types'; import classnames from 'classnames'; import svg4everybody from 'svg4everybody'; import { registerStyle, getAssetRoot } from './util'; +import { ComponentSettingsContext } from './ComponentSettings'; svg4everybody(); @@ -89,18 +90,68 @@ weeklyview,world,zoomin,zoomout .split(/[\s,]+/); /* eslint-enable max-len */ -export default class Icon extends Component { - constructor(props) { +export type IconCategory = + | 'action' + | 'custom' + | 'doctype' + | 'standard' + | 'utility'; +export type IconSize = 'x-small' | 'small' | 'medium' | 'large'; +export type IconContainer = boolean | 'default' | 'circle'; +export type IconTextColor = 'default' | 'warning' | 'error' | null; + +export type IconProps = { + className?: string; + containerClassName?: string; + category?: IconCategory; + icon: string; + size?: IconSize; + align?: 'left' | 'right'; + container?: IconContainer; + color?: string; + textColor?: IconTextColor; + tabIndex?: number; + fillColor?: string; +}; + +export type IconState = { + iconColor?: string; +}; + +export class Icon extends Component< + IconProps & SVGAttributes, + IconState +> { + static contextTypes = { assetRoot: PropTypes.string }; + + static ICONS = { + STANDARD_ICONS, + CUSTOM_ICONS, + ACTION_ICONS, + DOCTYPE_ICONS, + UTILITY_ICONS, + }; + + // eslint-disable-next-line react/sort-comp + context!: Pick; + + iconContainer: HTMLSpanElement | null; + + svgIcon: SVGElement | null; + + constructor(props: Readonly>) { super(props); this.state = {}; + this.iconContainer = null; + this.svgIcon = null; registerStyle('icon', [['.slds-icon use', '{ pointer-events: none; }']]); } componentDidMount() { this.checkIconColor(); const svgEl = this.svgIcon; - if (svgEl) { - svgEl.setAttribute('focusable', this.props.tabIndex >= 0); + if (svgEl && this.props.tabIndex !== undefined) { + svgEl.setAttribute('focusable', (this.props.tabIndex >= 0).toString()); } } @@ -108,9 +159,12 @@ export default class Icon extends Component { this.checkIconColor(); } - getIconColor(fillColor, category, icon) { + getIconColor( + fillColor: string | undefined, + category: string | undefined, + icon: string + ) { /* eslint-disable no-unneeded-ternary */ - /* eslint-disable max-len */ return this.state.iconColor ? this.state.iconColor : category === 'doctype' @@ -126,6 +180,7 @@ export default class Icon extends Component { : category === 'action' && /^new_custom/.test(icon) ? icon.replace(/^new_custom/, 'custom-') : `${category}-${(icon || '').replace(/_/g, '-')}`; + /* eslint-enable no-unneeded-ternary */ } checkIconColor() { @@ -143,9 +198,12 @@ export default class Icon extends Component { if (!el) { return; } - const bgColorStyle = getComputedStyle(el)['background-color']; + const bgColorStyle = getComputedStyle(el).backgroundColor; // if no background color set to the icon - if (/^(transparent|rgba\(0,\s*0,\s*0,\s*0\))$/.test(bgColorStyle)) { + if ( + bgColorStyle && + /^(transparent|rgba\(0,\s*0,\s*0,\s*0\))$/.test(bgColorStyle) + ) { this.setState({ iconColor: 'standard-default' }); } } @@ -162,7 +220,7 @@ export default class Icon extends Component { style, assetRoot, ...props - }) { + }: any) { const iconColor = this.getIconColor(fillColor, category, icon); const iconClassNames = classnames( { @@ -201,7 +259,7 @@ export default class Icon extends Component { let { category, icon } = props; if (icon.indexOf(':') > 0) { - [category, icon] = icon.split(':'); + [category, icon] = icon.split(':') as [IconProps['category'], string]; } if (container) { const { containerClassName, fillColor, ...pprops } = props; @@ -232,37 +290,3 @@ export default class Icon extends Component { return this.renderSVG({ ...props, category, icon, assetRoot }); } } - -Icon.propTypes = { - className: PropTypes.string, - containerClassName: PropTypes.string, - category: PropTypes.oneOf([ - 'action', - 'custom', - 'doctype', - 'standard', - 'utility', - ]), - icon: PropTypes.string, - size: PropTypes.oneOf(['x-small', 'small', 'medium', 'large']), - container: PropTypes.oneOfType([ - PropTypes.bool, - PropTypes.oneOf(['default', 'circle']), - ]), - color: PropTypes.string, - textColor: PropTypes.oneOf(['default', 'warning', 'error']), - tabIndex: PropTypes.number, - fillColor: PropTypes.string, -}; - -Icon.contextTypes = { - assetRoot: PropTypes.string, -}; - -Icon.ICONS = { - STANDARD_ICONS, - CUSTOM_ICONS, - ACTION_ICONS, - DOCTYPE_ICONS, - UTILITY_ICONS, -}; diff --git a/src/scripts/Input.js b/src/scripts/Input.tsx similarity index 73% rename from src/scripts/Input.js rename to src/scripts/Input.tsx index 5bae63dcd..6e533cd6e 100644 --- a/src/scripts/Input.js +++ b/src/scripts/Input.tsx @@ -1,28 +1,55 @@ -import React, { Component } from 'react'; -import PropTypes from 'prop-types'; +import React, { Component, InputHTMLAttributes } from 'react'; import classnames from 'classnames'; import keycoder from 'keycoder'; -import Icon from './Icon'; -import FormElement from './FormElement'; -import Text from './Text'; +import { Icon } from './Icon'; +import { FormElement, FormElementProps } from './FormElement'; +import { Text } from './Text'; import { uuid, registerStyle } from './util'; -export default class Input extends Component { - constructor() { - super(); +type Omit = Pick>; + +export type InputProps = { + id?: string; + className?: string; + label?: string; + required?: boolean; + error?: FormElementProps['error']; + totalCols?: number; + cols?: number; + value?: string; + defaultValue?: string; + placeholder?: string; + bare?: boolean; + symbolPattern?: string; + readOnly?: boolean; + htmlReadOnly?: boolean; + iconLeft?: string | JSX.Element; + iconRight?: string | JSX.Element; + addonLeft?: string; + addonRight?: string; + onChange?: (e: React.ChangeEvent, value: string) => void; + onKeyDown?: (e: React.KeyboardEvent) => void; + inputRef?: (node: HTMLInputElement) => void; +} & Omit, 'onChange'>; + +export class Input extends Component { + static isFormElement = true; + + constructor(props: Readonly) { + super(props); this.onChange = this.onChange.bind(this); this.onKeyDown = this.onKeyDown.bind(this); this.registerIconStyle(); } - onChange(e) { + onChange(e: React.ChangeEvent) { const { value } = e.target; if (this.props.onChange) { this.props.onChange(e, value); } } - onKeyDown(e) { + onKeyDown(e: React.KeyboardEvent) { const { symbolPattern, onKeyDown } = this.props; if (symbolPattern) { const { keyCode, shiftKey } = e; @@ -47,7 +74,7 @@ export default class Input extends Component { ]); } - renderAddon(content) { + renderAddon(content: string) { return ( void; + lookupSelectionRef?: (node: HTMLDivElement) => void; +}; /** * */ -export class LookupSelection extends Component { - static propTypes = { - id: PropTypes.string, - selected: LookupEntryType, - hidden: PropTypes.bool, - onResetSelection: PropTypes.func, - lookupSelectionRef: PropTypes.func, - }; +export class LookupSelection extends Component { + pill: HTMLElement | null = null; - onKeyDown(e) { + onKeyDown(e: any) { if (e.keyCode === 8 || e.keyCode === 46) { // Bacspace / DEL e.preventDefault(); @@ -45,8 +47,8 @@ export class LookupSelection extends Component { } } - renderPill(selected) { - const onPillClick = (e) => { + renderPill(selected: LookupEntry) { + const onPillClick = (e: any) => { e.target.focus(); e.preventDefault(); e.stopPropagation(); @@ -86,41 +88,41 @@ export class LookupSelection extends Component { } } -/** - * - */ -const ICON_ALIGNS = ['left', 'right']; +export type LookupScope = { + label: string; + value: string; + icon: string; +}; + +export type LookupSearchProps = { + id?: string; + className?: string; + hidden?: boolean; + searchText?: string; + scopes?: LookupScope[]; + targetScope?: any; + iconAlign?: 'left' | 'right'; + disabled?: boolean; + onKeyDown?: (e: any) => void; + onBlur?: (e: any) => void; + onChange?: (searchText: string) => void; + onScopeMenuClick?: (e: any) => void; + onScopeChange?: (value: string) => void; + onPressDown?: () => void; + onSubmit?: () => void; + onComplete?: (cancel?: boolean) => void; + lookupSearchRef?: (node: HTMLDivElement) => void; +}; /** * */ -export class LookupSearch extends Component { - static propTypes = { - className: PropTypes.string, - hidden: PropTypes.bool, - searchText: PropTypes.string, - scopes: PropTypes.arrayOf( - PropTypes.shape({ - label: PropTypes.string, - value: PropTypes.string, - icon: PropTypes.string, - }) - ), - targetScope: PropTypes.any, // eslint-disable-line - iconAlign: PropTypes.oneOf(ICON_ALIGNS), - disabled: PropTypes.bool, - onKeyDown: PropTypes.func, - onBlur: PropTypes.func, - onChange: PropTypes.func, - onScopeMenuClick: PropTypes.func, - onScopeChange: PropTypes.func, - onPressDown: PropTypes.func, - onSubmit: PropTypes.func, - onComplete: PropTypes.func, - lookupSearchRef: PropTypes.func, - }; +export class LookupSearch extends Component { + input: HTMLInputElement | null = null; - constructor(props) { + node: HTMLDivElement | null = null; + + constructor(props: Readonly) { super(props); /* eslint-disable max-len */ registerStyle('lookupSearch', [ @@ -152,18 +154,22 @@ export class LookupSearch extends Component { } onLookupIconClick = () => { - this.props.onSubmit(); + if (this.props.onSubmit) { + this.props.onSubmit(); + } }; - onInputKeyDown = (e) => { + onInputKeyDown = (e: any) => { if (e.keyCode === 13) { // return key e.preventDefault(); e.stopPropagation(); const searchText = e.target.value; if (searchText) { - this.props.onSubmit(); - } else { + if (this.props.onSubmit) { + this.props.onSubmit(); + } + } else if (this.props.onComplete) { // if no search text, quit lookup search this.props.onComplete(); } @@ -171,26 +177,32 @@ export class LookupSearch extends Component { // down key e.preventDefault(); e.stopPropagation(); - this.props.onPressDown(); + if (this.props.onPressDown) { + this.props.onPressDown(); + } } else if (e.keyCode === 27) { // ESC e.preventDefault(); e.stopPropagation(); // quit lookup search (cancel) const cancel = true; - this.props.onComplete(cancel); + if (this.props.onComplete) { + this.props.onComplete(cancel); + } } if (this.props.onKeyDown) { this.props.onKeyDown(e); } }; - onInputChange = (e) => { + onInputChange = (e: any) => { const searchText = e.target.value; - this.props.onChange(searchText); + if (this.props.onChange) { + this.props.onChange(searchText); + } }; - onInputBlur = (e) => { + onInputBlur = (e: any) => { setTimeout(() => { if (!this.isFocusedInComponent()) { if (this.props.onBlur) { @@ -200,19 +212,19 @@ export class LookupSearch extends Component { }, 10); }; - onScopeMenuClick = (e) => { + onScopeMenuClick = (e: any) => { if (this.props.onScopeMenuClick) { this.props.onScopeMenuClick(e); } }; - onMenuItemClick = (scope) => { + onMenuItemClick = (scope: LookupScope) => { if (this.props.onScopeChange) { this.props.onScopeChange(scope.value); } }; - handleLookupSearchRef = (node) => { + handleLookupSearchRef = (node: HTMLDivElement) => { this.node = node; const { lookupSearchRef } = this.props; if (lookupSearchRef) { @@ -224,7 +236,7 @@ export class LookupSearch extends Component { return isElInChildren(this.node, document.activeElement); } - renderSearchInput(props) { + renderSearchInput(props: any) { const { className, hidden, searchText, iconAlign = 'right' } = props; const searchInputClassNames = classnames( 'slds-grid', @@ -274,7 +286,7 @@ export class LookupSearch extends Component { ); } - renderScopeSelector({ scopes, targetScope: target, disabled }) { + renderScopeSelector({ scopes, targetScope: target, disabled }: any) { let targetScope = scopes[0] || {}; for (const scope of scopes) { if (scope.value === target) { @@ -298,7 +310,7 @@ export class LookupSearch extends Component { onMenuItemClick={this.onMenuItemClick} onBlur={this.onInputBlur} > - {scopes.map((scope) => ( + {scopes.map((scope: LookupScope) => ( ))} @@ -317,9 +329,9 @@ export class LookupSearch extends Component { { 'slds-hide': hidden } ); const styles = { - WebkitFlexWrap: 'nowrap', + WebkitFlexWrap: 'nowrap' as const, msFlexWrap: 'nowrap', - flexWrap: 'nowrap', + flexWrap: 'nowrap' as const, }; return (
    boolean; + listRef?: (node: HTMLDivElement) => void; + onSelect?: (entry: LookupEntry | null) => void; + onBlur?: (e: React.FocusEvent) => void; + header?: JSX.Element; + footer?: JSX.Element; +} & InjectedProps; + /** * */ -class LookupCandidateList extends Component { - static propTypes = { - data: PropTypes.arrayOf(LookupEntryType), - focus: PropTypes.bool, - loading: PropTypes.bool, - filter: PropTypes.func, - align: PropTypes.oneOf(['left', 'right']), - vertAlign: PropTypes.oneOf(['top', 'bottom']), - listRef: PropTypes.func, - onSelect: PropTypes.func, - onBlur: PropTypes.func, - header: PropTypes.node, - footer: PropTypes.node, - }; +class LookupCandidateList extends Component { + node: HTMLDivElement | null = null; componentDidMount() { if (this.props.focus) { @@ -365,7 +377,7 @@ class LookupCandidateList extends Component { } } - componentDidUpdate(prevProps) { + componentDidUpdate(prevProps: Readonly) { if (this.props.focus && !prevProps.focus) { setTimeout(() => { this.focusToTargetItemEl(0); @@ -373,13 +385,13 @@ class LookupCandidateList extends Component { } } - onSelect(entry) { + onSelect(entry: LookupEntry | null) { if (this.props.onSelect) { this.props.onSelect(entry); } } - onKeyDown(e) { + onKeyDown(e: any) { if (e.keyCode === 38 || e.keyCode === 40) { // UP/DOWN e.preventDefault(); @@ -405,18 +417,20 @@ class LookupCandidateList extends Component { } } - focusToTargetItemEl(index) { + focusToTargetItemEl(index: number) { const el = this.node; if (!el) { return; } - const anchors = el.querySelectorAll('.react-slds-candidate[tabIndex]'); + const anchors = el.querySelectorAll( + '.react-slds-candidate[tabIndex]' + ); if (anchors[index]) { anchors[index].focus(); } } - renderCandidate(entry) { + renderCandidate(entry: LookupEntry) { const { category, icon, label, value, meta } = entry; return (
  • @@ -480,7 +494,7 @@ class LookupCandidateList extends Component { ...(vertAlign === 'bottom' ? { bottom: '100%' } : {}), ...(align === 'right' ? { left: 'auto', right: 0 } : {}), }; - const handleDOMRef = (node) => { + const handleDOMRef = (node: HTMLDivElement) => { this.node = node; if (listRef) { listRef(node); @@ -524,53 +538,75 @@ export const LookupCandidateListPortal = autoAlign({ triggerSelector: '.slds-lookup', })(LookupCandidateList); +export type LookupProps = { + id?: string; + className?: string; + label?: string; + disabled?: boolean; + required?: boolean; + error?: FormElementProps['error']; + iconAlign?: 'left' | 'right'; + + value?: string; + defaultValue?: string; + + selected?: LookupEntry | null; + defaultSelected?: LookupEntry; + + opened?: boolean; + defaultOpened?: boolean; + + searchText?: string; + defaultSearchText?: string; + + loading?: boolean; + data?: LookupEntry[]; + lookupFilter?: ( + entry: LookupEntry, + searchText?: string, + targetScope?: string + ) => boolean; + listHeader?: JSX.Element; + listFooter?: JSX.Element; + scopes?: LookupScope[]; + targetScope?: string; + defaultTargetScope?: string; + totalCols?: number; + cols?: number; + + onSearchTextChange?: (searchText: string) => void; + onScopeMenuClick?: (e: any) => void; + onScopeChange?: (targetScope: string) => void; + onLookupRequest?: (searchText?: string) => void; + onBlur?: () => void; + onSelect?: (e: any) => void; + onComplete?: (cancel?: boolean) => void; +}; + +export type LookupState = { + id: string; + selected?: LookupEntry | null; + opened?: boolean; + searchText?: string; + targetScope?: string; + focusFirstCandidate: boolean; +}; /** * */ -export default class Lookup extends Component { - static propTypes = { - id: PropTypes.string, - className: PropTypes.string, - label: PropTypes.string, - required: PropTypes.bool, - error: FormElement.propTypes.error, - value: PropTypes.string, - defaultValue: PropTypes.string, - selected: LookupEntryType, - defaultSelected: LookupEntryType, - opened: PropTypes.bool, - defaultOpened: PropTypes.bool, - searchText: PropTypes.string, - defaultSearchText: PropTypes.string, - loading: PropTypes.bool, - data: PropTypes.arrayOf(LookupEntryType), - lookupFilter: PropTypes.func, - listHeader: PropTypes.node, - listFooter: PropTypes.node, - scopes: PropTypes.arrayOf( - PropTypes.shape({ - label: PropTypes.string, - value: PropTypes.string, - icon: PropTypes.string, - }) - ), - targetScope: PropTypes.string, - iconAlign: PropTypes.oneOf(ICON_ALIGNS), - defaultTargetScope: PropTypes.string, - onSearchTextChange: PropTypes.func, - onScopeMenuClick: PropTypes.func, - onScopeChange: PropTypes.func, - onLookupRequest: PropTypes.func, - onBlur: PropTypes.func, - onSelect: PropTypes.func, - onComplete: PropTypes.func, - totalCols: PropTypes.number, - cols: PropTypes.number, - }; - +export class Lookup extends Component { static isFormElement = true; - constructor(props) { + node: HTMLDivElement | null = null; + + selection: HTMLDivElement | null = null; + + candidateList: HTMLDivElement | null = null; + + // eslint-disable-next-line react/sort-comp + private search: any; + + constructor(props: Readonly) { super(props); this.state = { id: `form-element-${uuid()}`, @@ -582,28 +618,28 @@ export default class Lookup extends Component { }; } - onScopeMenuClick(e) { + onScopeMenuClick(e: any) { this.setState({ opened: false }); if (this.props.onScopeMenuClick) { this.props.onScopeMenuClick(e); } } - onScopeChange(targetScope) { + onScopeChange(targetScope: string) { this.setState({ targetScope }); if (this.props.onScopeChange) { this.props.onScopeChange(targetScope); } } - onSearchTextChange(searchText) { + onSearchTextChange(searchText: string) { this.setState({ searchText }); if (this.props.onSearchTextChange) { this.props.onSearchTextChange(searchText); } } - onLookupRequest(searchText) { + onLookupRequest(searchText?: string) { this.setState({ opened: true }); if (this.props.onLookupRequest) { this.props.onLookupRequest(searchText); @@ -627,7 +663,7 @@ export default class Lookup extends Component { }, 10); } - onLookupItemSelect(selected) { + onLookupItemSelect(selected: LookupEntry | null) { if (selected) { this.setState({ selected, opened: false }); if (this.props.onSelect) { @@ -714,7 +750,7 @@ export default class Lookup extends Component { className ); const formElemProps = { id, totalCols, cols, label, required, error }; - /* eslint-disable no-unused-vars, @typescript-eslint/no-unused-vars */ + /* eslint-disable @typescript-eslint/no-unused-vars */ const { defaultSelected, defaultOpened, @@ -728,7 +764,7 @@ export default class Lookup extends Component { onLookupRequest, ...searchProps } = props; - /* eslint-enable no-unused-vars, @typescript-eslint/no-unused-vars */ + /* eslint-enable @typescript-eslint/no-unused-vars */ return ( (this.node = node)} @@ -773,7 +809,8 @@ export default class Lookup extends Component { loading={loading} filter={ lookupFilter - ? (entry) => lookupFilter(entry, searchText, targetScope) + ? (entry: LookupEntry) => + lookupFilter(entry, searchText, targetScope) : undefined } header={listHeader} diff --git a/src/scripts/MediaObject.js b/src/scripts/MediaObject.tsx similarity index 66% rename from src/scripts/MediaObject.js rename to src/scripts/MediaObject.tsx index 56396afca..f5fc0b89d 100644 --- a/src/scripts/MediaObject.js +++ b/src/scripts/MediaObject.tsx @@ -1,9 +1,14 @@ -import React, { Component } from 'react'; -import PropTypes from 'prop-types'; +import React, { Component, ReactNode } from 'react'; import classNames from 'classnames'; -export default class MediaObject extends Component { - renderFigure(figure, className) { +export type MediaObjectProps = { + figureLeft?: ReactNode; + figureRight?: ReactNode; + figureCenter?: ReactNode; +}; + +export class MediaObject extends Component { + renderFigure(figure: ReactNode, className?: string) { if (!figure) return null; return (
    @@ -25,10 +30,3 @@ export default class MediaObject extends Component { ); } } - -MediaObject.propTypes = { - figureLeft: PropTypes.node, - figureRight: PropTypes.node, - figureCenter: PropTypes.node, - children: PropTypes.node, -}; diff --git a/src/scripts/Modal.js b/src/scripts/Modal.tsx similarity index 68% rename from src/scripts/Modal.js rename to src/scripts/Modal.tsx index 58da6902f..cc13c2bdb 100644 --- a/src/scripts/Modal.js +++ b/src/scripts/Modal.tsx @@ -1,11 +1,18 @@ import React, { Component } from 'react'; -import PropTypes from 'prop-types'; import classnames from 'classnames'; import { Button } from './Button'; -export class ModalHeader extends Component { - constructor() { - super(); +export type ModalHeaderProps = { + className?: string; + title?: string; + tagline?: string; + closeButton?: boolean; + onClose?: () => void; +}; + +export class ModalHeader extends Component { + constructor(props: Readonly) { + super(props); this.onClose = this.onClose.bind(this); } @@ -39,17 +46,63 @@ export class ModalHeader extends Component { } } -ModalHeader.propTypes = { - title: PropTypes.string, - tagline: PropTypes.string, - onClose: PropTypes.func, - className: PropTypes.string, - closeButton: PropTypes.bool, +export type ModalContentProps = { + className?: string; +}; + +export const ModalContent: React.FC = ({ + className, + children, + ...props +}) => { + const ctClassNames = classnames(className, 'slds-modal__content'); + return ( +
    + {children} +
    + ); +}; + +export type ModalFooterProps = { + className?: string; + directional?: boolean; +}; + +export const ModalFooter: React.FC = ({ + className, + directional, + children, + ...props +}) => { + const ftClassNames = classnames(className, 'slds-modal__footer', { + 'slds-modal__footer--directional': directional, + }); + return ( +
    + {children} +
    + ); +}; + +export type ModalSize = 'large'; + +export type ModalProps = { + className?: string; + size?: ModalSize; + opened?: boolean; + containerStyle?: object; + onHide?: () => void; }; -class Modal extends Component { - constructor() { - super(); +export class Modal extends Component { + static Header = ModalHeader; + + static Content = ModalContent; + + static Footer = ModalFooter; + + constructor(props: Readonly) { + super(props); this.renderChildComponent = this.renderChildComponent.bind(this); } @@ -60,7 +113,7 @@ class Modal extends Component { } } - renderChildComponent(comp) { + renderChildComponent(comp: any) { if (comp.type === ModalHeader) { return React.cloneElement(comp, { onClose: this.hide.bind(this) }); } @@ -101,52 +154,3 @@ class Modal extends Component { ); } } - -const MODAL_SIZES = ['large']; - -Modal.propTypes = { - className: PropTypes.string, - size: PropTypes.oneOf(MODAL_SIZES), - opened: PropTypes.bool, - onHide: PropTypes.func, - children: PropTypes.node, - /* eslint-disable react/forbid-prop-types */ - containerStyle: PropTypes.object, -}; - -export const ModalContent = ({ className, children, ...props }) => { - const ctClassNames = classnames(className, 'slds-modal__content'); - return ( -
    - {children} -
    - ); -}; - -ModalContent.propTypes = { - className: PropTypes.string, - children: PropTypes.node, -}; - -export const ModalFooter = ({ className, directional, children, ...props }) => { - const ftClassNames = classnames(className, 'slds-modal__footer', { - 'slds-modal__footer--directional': directional, - }); - return ( -
    - {children} -
    - ); -}; - -ModalFooter.propTypes = { - className: PropTypes.string, - directional: PropTypes.bool, - children: PropTypes.node, -}; - -Modal.Header = ModalHeader; -Modal.Content = ModalContent; -Modal.Footer = ModalFooter; - -export default Modal; diff --git a/src/scripts/Notification.js b/src/scripts/Notification.tsx similarity index 62% rename from src/scripts/Notification.js rename to src/scripts/Notification.tsx index 47150b540..cf917010c 100644 --- a/src/scripts/Notification.js +++ b/src/scripts/Notification.tsx @@ -1,14 +1,27 @@ import React from 'react'; -import PropTypes from 'prop-types'; import classnames from 'classnames'; import { Button } from './Button'; -import Icon from './Icon'; +import { Icon, IconSize } from './Icon'; -const NOTIFICATION_TYPES = ['alert', 'toast']; +const NOTIFICATION_TYPES = ['alert', 'toast'] as const; -const NOTIFICATION_LEVELS = ['info', 'success', 'warning', 'error']; +const NOTIFICATION_LEVELS = ['info', 'success', 'warning', 'error'] as const; -const Notification = (props) => { +export type NotificationType = typeof NOTIFICATION_TYPES[number]; +export type NotificationLevel = typeof NOTIFICATION_LEVELS[number]; + +export type NotificationProps = { + type?: NotificationType; + className?: string; + level?: NotificationLevel; + alt?: string; + icon?: string; + iconSize?: IconSize; + alertTexture?: boolean; + onClose?: (e: React.MouseEvent) => void; +} & React.HTMLAttributes; + +export const Notification: React.FC = (props) => { const { className, type, @@ -26,7 +39,7 @@ const Notification = (props) => { ? `slds-notify--${type}` : null; const levelClassName = - type && NOTIFICATION_LEVELS.indexOf(level) >= 0 + level && NOTIFICATION_LEVELS.indexOf(level) >= 0 ? `slds-theme--${level}` : null; const alertClassNames = classnames( @@ -83,26 +96,14 @@ const Notification = (props) => { ); }; -Notification.propTypes = { - type: PropTypes.oneOf(NOTIFICATION_TYPES).isRequired, - className: PropTypes.string, - level: PropTypes.oneOf(NOTIFICATION_LEVELS), - alt: PropTypes.string, - icon: PropTypes.string, - iconSize: PropTypes.string, - children: PropTypes.node, - onClose: PropTypes.func, -}; - -export default Notification; - -const propTypes = { ...Notification.propTypes }; -delete propTypes.type; - -export const Alert = (props) => ; - -Alert.propTypes = propTypes; +type Omit = Pick>; -export const Toast = (props) => ; +export type AlertProps = Omit; +export const Alert: React.FC = (props) => ( + +); -Toast.propTypes = propTypes; +export type ToastProps = Omit; +export const Toast: React.FC = (props) => ( + +); diff --git a/src/scripts/PageHeader.js b/src/scripts/PageHeader.tsx similarity index 69% rename from src/scripts/PageHeader.js rename to src/scripts/PageHeader.tsx index fc7ba9325..8fbdbd7d1 100644 --- a/src/scripts/PageHeader.js +++ b/src/scripts/PageHeader.tsx @@ -1,26 +1,31 @@ import React, { Component } from 'react'; -import PropTypes from 'prop-types'; import classNames from 'classnames'; -import MediaObject from './MediaObject'; -import Text from './Text'; -import Grid, { Row, Col } from './Grid'; +import { MediaObject } from './MediaObject'; +import { Text, TextProps } from './Text'; +import { Grid, Row, Col, GridProps } from './Grid'; import { BreadCrumbs, Crumb } from './BreadCrumbs'; -export const PageHeaderDetailBody = ({ children, ...props }) => +export type PageHeaderDetailBodyProps = TextProps; + +export const PageHeaderDetailBody: React.FC = ({ + children, + ...props +}) => typeof children === 'string' ? ( {children} ) : ( - children + <>{children} ); -PageHeaderDetailBody.propTypes = { - children: PropTypes.node, -}; +export type PageHeaderDetailLabelProps = TextProps; -export const PageHeaderDetailLabel = ({ children, ...props }) => +export const PageHeaderDetailLabel: React.FC = ({ + children, + ...props +}) => typeof children === 'string' ? ( {children} ) : ( - children + <>{children} ); -PageHeaderDetailLabel.propTypes = { - children: PropTypes.node, -}; +export type PageHeaderDetailItemProps = { + label?: string; +} & React.LiHTMLAttributes; -export const PageHeaderDetailItem = (props) => { +export const PageHeaderDetailItem: React.FC = ( + props +) => { const { children, label, ...pprops } = props; const manuallyAssembled = !label; return ( @@ -53,12 +60,12 @@ export const PageHeaderDetailItem = (props) => { ); }; -PageHeaderDetailItem.propTypes = { - label: PropTypes.string, - children: PropTypes.node, -}; +export type PageHeaderDetailProps = GridProps; -export const PageHeaderDetail = ({ children, ...props }) => ( +export const PageHeaderDetail: React.FC = ({ + children, + ...props +}) => ( ( ); -PageHeaderDetail.propTypes = { - children: PropTypes.node, -}; +export type PageHeaderHeadingTitleProps = { + className?: string; +} & React.HTMLAttributes; -export const PageHeaderHeadingTitle = (props) => { +export const PageHeaderHeadingTitle: React.FC = ( + props +) => { const { className, children } = props; const titleClassNames = classNames( className, @@ -86,13 +95,18 @@ export const PageHeaderHeadingTitle = (props) => { ); }; -PageHeaderHeadingTitle.propTypes = { - className: PropTypes.string, - children: PropTypes.node, +export type PageHeaderHeadingProps = { + info?: string; + legend?: string; + title?: string | JSX.Element; + breadCrumbs?: Array; + leftActions?: JSX.Element; + figure?: JSX.Element; + rightActions?: JSX.Element | Array; }; -export class PageHeaderHeading extends Component { - renderInfo(info) { +export class PageHeaderHeading extends Component { + renderInfo(info: string) { return info ? ( {info} @@ -100,7 +114,7 @@ export class PageHeaderHeading extends Component { ) : null; } - renderWithMedia(figure) { + renderWithMedia(figure: JSX.Element | undefined) { const content = this.renderContent(); return figure ? ( {content} @@ -195,24 +209,10 @@ export class PageHeaderHeading extends Component { } } -PageHeaderHeading.propTypes = { - info: PropTypes.string, - legend: PropTypes.string, - title: PropTypes.oneOfType([PropTypes.string, PropTypes.node]), - breadCrumbs: PropTypes.oneOfType([PropTypes.arrayOf(Crumb), PropTypes.node]), - leftActions: PropTypes.node, - figure: PropTypes.node, - rightActions: PropTypes.node, -}; +export type PageHeaderProps = React.HTMLAttributes; -const PageHeader = (props) => ( +export const PageHeader: React.FC = (props) => (
    {props.children}
    ); - -PageHeader.propTypes = { - children: PropTypes.node, -}; - -export default PageHeader; diff --git a/src/scripts/Picklist.js b/src/scripts/Picklist.tsx similarity index 68% rename from src/scripts/Picklist.js rename to src/scripts/Picklist.tsx index 0d3659e64..ece6a3064 100644 --- a/src/scripts/Picklist.js +++ b/src/scripts/Picklist.tsx @@ -1,17 +1,57 @@ import React, { Component } from 'react'; -import PropTypes from 'prop-types'; import classnames from 'classnames'; -import FormElement from './FormElement'; -import Icon from './Icon'; +import { FormElement, FormElementProps } from './FormElement'; +import { Icon } from './Icon'; import { Button } from './Button'; -import DropdownMenu, { DropdownMenuItem } from './DropdownMenu'; +import { DropdownMenu, DropdownMenuItem } from './DropdownMenu'; import { uuid, isElInChildren } from './util'; -export default class Picklist extends Component { - constructor(props) { +export type PicklistProps = { + id?: string; + className?: string; + label?: string; + required?: boolean; + multiSelect?: boolean; + error?: FormElementProps['error']; + totalCols?: number; + cols?: number; + name?: string; + value?: string | number | (string | number)[]; + defaultValue?: string | number | (string | number)[]; + selectedText?: string; + optionsSelectedText?: string; + defaultOpened?: boolean; + disabled?: boolean; + menuSize?: string; + menuStyle?: object; + onChange?: (...args: any[]) => any; + onValueChange?: (newValue?: any, prevValue?: any) => void; + onSelect?: (...args: any[]) => any; + onComplete?: (...args: any[]) => any; + onKeyDown?: (...args: any[]) => any; + onBlur?: (...args: any[]) => any; +}; + +export type PicklistState = { + id: string; + opened?: boolean; + value: (string | number)[]; +}; + +export class Picklist extends Component { + static isFormElement = true; + + node: HTMLDivElement | null = null; + + picklistButton: HTMLButtonElement | null = null; + + dropdown: HTMLDivElement | null = null; + + constructor(props: Readonly) { super(props); - const initialValue = props.value || props.defaultValue; + const { defaultValue = [] } = props; + const initialValue = props.value || defaultValue; this.state = { id: `form-element-${uuid()}`, @@ -27,8 +67,8 @@ export default class Picklist extends Component { }, 10); }; - onPicklistItemClick = (item, e) => { - const { multiSelect } = this.props; + onPicklistItemClick = (item: any, e: any) => { + const { multiSelect = false } = this.props; this.updateItemValue(item.value); if (this.props.onChange) { @@ -56,7 +96,9 @@ export default class Picklist extends Component { onPicklistClose = () => { const picklistButtonEl = this.picklistButton; - picklistButtonEl.focus(); + if (picklistButtonEl) { + picklistButtonEl.focus(); + } this.setState({ opened: false }); }; @@ -74,7 +116,7 @@ export default class Picklist extends Component { }, 10); }; - onKeydown = (e) => { + onKeydown = (e: React.KeyboardEvent) => { if (e.keyCode === 40) { // down e.preventDefault(); @@ -111,8 +153,8 @@ export default class Picklist extends Component { return this.state.value; } - setValue(newValue) { - const { multiSelect, onValueChange } = this.props; + setValue(newValue: (string | number)[]) { + const { multiSelect = false, onValueChange } = this.props; const prevValue = this.getValue(); this.setState({ value: newValue }); @@ -134,14 +176,15 @@ export default class Picklist extends Component { // many items selected if (selectedValues.length > 1) { - return this.props.optionsSelectedText; + const { optionsSelectedText = '' } = this.props; + return optionsSelectedText; } // one item if (selectedValues.length === 1) { const selectedValue = selectedValues[0]; let selected = null; - React.Children.forEach(this.props.children, (item) => { + React.Children.forEach(this.props.children, (item: any) => { if (item.props.value === selectedValue) { selected = item.props.label || item.props.children; } @@ -150,11 +193,12 @@ export default class Picklist extends Component { } // zero items - return this.props.selectedText; + const { selectedText = '' } = this.props; + return selectedText; } - updateItemValue(itemValue) { - const { multiSelect } = this.props; + updateItemValue(itemValue: any) { + const { multiSelect = false } = this.props; if (multiSelect) { const newValue = this.getValue().slice(); @@ -187,7 +231,7 @@ export default class Picklist extends Component { if (!dropdownEl) { return; } - const firstItemEl = + const firstItemEl: HTMLAnchorElement | null = dropdownEl.querySelector( '.slds-is-selected > .react-slds-menuitem[tabIndex]' ) || dropdownEl.querySelector('.react-slds-menuitem[tabIndex]'); @@ -196,14 +240,13 @@ export default class Picklist extends Component { } } - renderPicklist(props) { - const { className, id, disabled, menuSize, menuStyle, ...pprops } = props; + renderPicklist(props: PicklistProps) { + const { className, id, disabled, menuSize, menuStyle } = props; const picklistClassNames = classnames( className, 'slds-picklist', 'slds-dropdown-trigger' ); - delete pprops.onValueChange; return (
    ); } } - -const POPOVER_POSITIONS = [ - 'top', - 'top-left', - 'top-right', - 'bottom', - 'bottom-left', - 'bottom-right', - 'left', - 'left-top', - 'left-bottom', - 'right', - 'right-top', - 'right-bottom', -]; - -const POPOVER_THEMES = ['info', 'success', 'warning', 'error']; - -Popover.propTypes = { - position: PropTypes.oneOf(POPOVER_POSITIONS), - hidden: PropTypes.bool, - theme: PropTypes.oneOf(POPOVER_THEMES), - tooltip: PropTypes.bool, - children: PropTypes.node, - hover: PropTypes.bool, - trigger: PropTypes.func, - /* eslint-disable react/forbid-prop-types */ - bodyStyle: PropTypes.object, -}; - -Popover.defaultProps = { - hidden: true, -}; diff --git a/src/scripts/Radio.js b/src/scripts/Radio.tsx similarity index 61% rename from src/scripts/Radio.js rename to src/scripts/Radio.tsx index b78cb8089..bed525fc8 100644 --- a/src/scripts/Radio.js +++ b/src/scripts/Radio.tsx @@ -1,8 +1,16 @@ -import React from 'react'; -import PropTypes from 'prop-types'; +import React, { InputHTMLAttributes } from 'react'; import classnames from 'classnames'; -const Radio = ({ +export type RadioProps = { + className?: string; + label?: string; + name?: string; + value?: string | number; + checked?: boolean; + defaultChecked?: boolean; +} & InputHTMLAttributes; + +export const Radio: React.FC = ({ className, label, name, @@ -27,14 +35,3 @@ const Radio = ({ ); }; - -Radio.propTypes = { - className: PropTypes.string, - label: PropTypes.string, - name: PropTypes.string, - value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), - checked: PropTypes.bool, - defaultChecked: PropTypes.bool, -}; - -export default Radio; diff --git a/src/scripts/RadioGroup.js b/src/scripts/RadioGroup.tsx similarity index 73% rename from src/scripts/RadioGroup.js rename to src/scripts/RadioGroup.tsx index 338d8fe10..e916adfb4 100644 --- a/src/scripts/RadioGroup.js +++ b/src/scripts/RadioGroup.tsx @@ -1,22 +1,33 @@ import React from 'react'; -import PropTypes from 'prop-types'; import classnames from 'classnames'; -import FormElement from './FormElement'; -export default class RadioGroup extends React.Component { - constructor() { - super(); +export type RadioGroupProps = { + className?: string; + label?: string; + required?: boolean; + error?: any; // FIXME: should be FormElementProps.error + name?: string; + onChange?: (e: any, value: any) => void; + totalCols?: number; + cols?: number; + style?: object; +}; + +export class RadioGroup extends React.Component { + static isFormElement = true; + constructor(props: Readonly) { + super(props); this.renderControl = this.renderControl.bind(this); } - onControlChange(value, e) { + onControlChange(value: any, e: any) { if (this.props.onChange) { this.props.onChange(e, value); } } - renderControl(radio) { + renderControl(radio: any) { return this.props.name ? React.cloneElement(radio, { name: this.props.name, @@ -35,6 +46,7 @@ export default class RadioGroup extends React.Component { cols, style, children, + onChange, // eslint-disable-line @typescript-eslint/no-unused-vars ...props } = this.props; const grpClassNames = classnames( @@ -60,7 +72,6 @@ export default class RadioGroup extends React.Component { : undefined : undefined; - delete props.onChange; return (
    @@ -79,19 +90,3 @@ export default class RadioGroup extends React.Component { ); } } - -RadioGroup.propTypes = { - className: PropTypes.string, - label: PropTypes.string, - required: PropTypes.bool, - error: FormElement.propTypes.error, - name: PropTypes.string, - onChange: PropTypes.func, - totalCols: PropTypes.number, - cols: PropTypes.number, - children: PropTypes.node, - /* eslint-disable react/forbid-prop-types */ - style: PropTypes.object, -}; - -RadioGroup.isFormElement = true; diff --git a/src/scripts/SalesPath.js b/src/scripts/SalesPath.tsx similarity index 75% rename from src/scripts/SalesPath.js rename to src/scripts/SalesPath.tsx index b45ee974d..9eec63ee5 100644 --- a/src/scripts/SalesPath.js +++ b/src/scripts/SalesPath.tsx @@ -1,17 +1,80 @@ import React from 'react'; -import PropTypes from 'prop-types'; import classnames from 'classnames'; -import Icon from './Icon'; +import { Icon } from './Icon'; + +export type PathItemProps = { + className?: string; + eventKey?: any; + type?: 'complete' | 'current' | 'incomplete'; + title?: string; + completedTitle?: string; + onSelect?: (itemKey: any) => void; +}; + +class PathItem extends React.Component { + onItemClick(itemKey: any) { + if (this.props.onSelect) { + this.props.onSelect(itemKey); + } + } + + render() { + const { className, eventKey, title, completedTitle, type } = this.props; + + const pathItemClassName = classnames( + 'slds-tabs--path__item', + `slds-is-${type}`, + className + ); + + const tabIndex = type === 'current' ? 0 : -1; + const completedText = completedTitle || 'Stage Complete'; + + return ( +
  • + + + + {type === 'complete' ? ( + {completedText} + ) : null} + + {title} + +
  • + ); + } +} + +export type SalesPathProps = { + className?: string; + defaultActiveKey?: any; + activeKey?: any; + onSelect?: (itemKey: any) => void; +}; + +export type SalesPathState = { + activeKey?: any; +}; + +export class SalesPath extends React.Component { + static PathItem = PathItem; -class SalesPath extends React.Component { - constructor() { - super(); + constructor(props: Readonly) { + super(props); this.state = {}; this.onItemClick = this.onItemClick.bind(this); } - onItemClick(itemKey) { + onItemClick(itemKey: any) { if (this.props.onSelect) { this.props.onSelect(itemKey); } @@ -19,7 +82,7 @@ class SalesPath extends React.Component { this.setState({ activeKey: itemKey }); } - renderSalesPath(activeKey, paths) { + renderSalesPath(activeKey: any, paths: any) { let typeTracker = -1; return React.Children.map(paths, (path) => { @@ -60,67 +123,3 @@ class SalesPath extends React.Component { ); } } - -SalesPath.propTypes = { - className: PropTypes.string, - onSelect: PropTypes.func, - children: PropTypes.node, - /* eslint-disable react/forbid-prop-types */ - defaultActiveKey: PropTypes.any, - activeKey: PropTypes.any, -}; - -class PathItem extends React.Component { - onItemClick(itemKey) { - if (this.props.onSelect) { - this.props.onSelect(itemKey); - } - } - - render() { - const { className, eventKey, title, completedTitle, type } = this.props; - - const pathItemClassName = classnames( - 'slds-tabs--path__item', - `slds-is-${type}`, - className - ); - - const tabIndex = type === 'current' ? 0 : -1; - const completedText = completedTitle || 'Stage Complete'; - - return ( -
  • - - - - {type === 'complete' ? ( - {completedText} - ) : null} - - {title} - -
  • - ); - } -} - -PathItem.propTypes = { - className: PropTypes.string, - eventKey: PropTypes.any, - type: PropTypes.oneOf(['complete', 'current', 'incomplete']), - title: PropTypes.string, - completedTitle: PropTypes.string, - onSelect: PropTypes.func, -}; - -SalesPath.PathItem = PathItem; - -export default SalesPath; diff --git a/src/scripts/Select.js b/src/scripts/Select.tsx similarity index 50% rename from src/scripts/Select.js rename to src/scripts/Select.tsx index 301350763..678c68df4 100644 --- a/src/scripts/Select.js +++ b/src/scripts/Select.tsx @@ -1,16 +1,34 @@ import React, { Component } from 'react'; -import PropTypes from 'prop-types'; import classnames from 'classnames'; -import FormElement from './FormElement'; +import { FormElement, FormElementProps } from './FormElement'; import { uuid } from './util'; -export default class Select extends Component { - constructor() { - super(); +type Omit = Pick>; + +export type SelectProps = { + id?: string; + className?: string; + label?: string; + required?: boolean; + totalCols?: number; + cols?: number; + error?: FormElementProps['error']; + onChange?: (e: React.ChangeEvent, value: string) => void; +} & Omit, 'onChange'>; + +export type SelectState = { + id: string; +}; + +export class Select extends Component { + static isFormElement = true; + + constructor(props: Readonly) { + super(props); this.state = { id: `form-element-${uuid()}` }; } - onChange(e) { + onChange(e: React.ChangeEvent) { const { value } = e.target; if (this.props.onChange) { this.props.onChange(e, value); @@ -28,8 +46,13 @@ export default class Select extends Component { ); } - const { className, children, ...pprops } = props; - delete pprops.onChange; + const { + className, + children, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + onChange, + ...pprops + } = props; const selectClassNames = classnames(className, 'slds-select'); return (