UNPKG

9.79 kBTypeScriptView Raw
1/*
2 * Copyright 2016 Palantir Technologies, Inc. All rights reserved.
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17// we need some empty interfaces to show up in docs
18// HACKHACK: these components should go in separate files
19/* eslint-disable max-classes-per-file, @typescript-eslint/no-empty-interface */
20
21import classNames from "classnames";
22import * as React from "react";
23import { polyfill } from "react-lifecycles-compat";
24
25import { AbstractPureComponent2, Alignment, Classes, IRef, refHandler, setRef } from "../../common";
26import { DISPLAYNAME_PREFIX, HTMLInputProps, Props } from "../../common/props";
27
28// eslint-disable-next-line deprecation/deprecation
29export type ControlProps = IControlProps;
30/** @deprecated use ControlProps */
31export interface IControlProps extends Props, HTMLInputProps {
32 // NOTE: HTML props are duplicated here to provide control-specific documentation
33
34 /**
35 * Alignment of the indicator within container.
36 *
37 * @default Alignment.LEFT
38 */
39 alignIndicator?: Alignment;
40
41 /** Whether the control is checked. */
42 checked?: boolean;
43
44 /** JSX label for the control. */
45 children?: React.ReactNode;
46
47 /** Whether the control is initially checked (uncontrolled mode). */
48 defaultChecked?: boolean;
49
50 /** Whether the control is non-interactive. */
51 disabled?: boolean;
52
53 /** Ref handler that receives HTML `<input>` element backing this component. */
54 inputRef?: IRef<HTMLInputElement>;
55
56 /** Whether the control should appear as an inline element. */
57 inline?: boolean;
58
59 /**
60 * Text label for the control.
61 *
62 * Use `children` or `labelElement` to supply JSX content. This prop actually supports JSX elements,
63 * but TypeScript will throw an error because `HTMLAttributes` only allows strings.
64 */
65 label?: string;
66
67 /**
68 * JSX Element label for the control.
69 *
70 * This prop is a workaround for TypeScript consumers as the type definition for `label` only
71 * accepts strings. JavaScript consumers can provide a JSX element directly to `label`.
72 */
73 labelElement?: React.ReactNode;
74
75 /** Whether this control should use large styles. */
76 large?: boolean;
77
78 /** Event handler invoked when input value is changed. */
79 onChange?: React.FormEventHandler<HTMLInputElement>;
80
81 /**
82 * Name of the HTML tag that wraps the checkbox.
83 *
84 * By default a `<label>` is used, which effectively enlarges the click
85 * target to include all of its children. Supply a different tag name if
86 * this behavior is undesirable or you're listening to click events from a
87 * parent element (as the label can register duplicate clicks).
88 *
89 * @default "label"
90 */
91 tagName?: keyof JSX.IntrinsicElements;
92}
93
94/** Internal props for Checkbox/Radio/Switch to render correctly. */
95interface IControlInternalProps extends ControlProps {
96 type: "checkbox" | "radio";
97 typeClassName: string;
98 indicatorChildren?: React.ReactNode;
99}
100
101/**
102 * Renders common control elements, with additional props to customize appearance.
103 * This component is not exported and is only used in this file for `Checkbox`, `Radio`, and `Switch` below.
104 */
105const Control: React.FunctionComponent<IControlInternalProps> = ({
106 alignIndicator,
107 children,
108 className,
109 indicatorChildren,
110 inline,
111 inputRef,
112 label,
113 labelElement,
114 large,
115 style,
116 type,
117 typeClassName,
118 tagName = "label",
119 ...htmlProps
120}) => {
121 const classes = classNames(
122 Classes.CONTROL,
123 typeClassName,
124 {
125 [Classes.DISABLED]: htmlProps.disabled,
126 [Classes.INLINE]: inline,
127 [Classes.LARGE]: large,
128 },
129 Classes.alignmentClass(alignIndicator),
130 className,
131 );
132
133 return React.createElement(
134 tagName,
135 { className: classes, style },
136 <input {...htmlProps} ref={inputRef} type={type} />,
137 <span className={Classes.CONTROL_INDICATOR}>{indicatorChildren}</span>,
138 label,
139 labelElement,
140 children,
141 );
142};
143
144//
145// Switch
146//
147
148// eslint-disable-next-line deprecation/deprecation
149export type SwitchProps = ISwitchProps;
150/** @deprecated use SwitchProps */
151export interface ISwitchProps extends ControlProps {
152 /**
153 * Text to display inside the switch indicator when checked.
154 * If `innerLabel` is provided and this prop is omitted, then `innerLabel`
155 * will be used for both states.
156 *
157 * @default innerLabel
158 */
159 innerLabelChecked?: string;
160
161 /**
162 * Text to display inside the switch indicator when unchecked.
163 */
164 innerLabel?: string;
165}
166
167@polyfill
168export class Switch extends AbstractPureComponent2<SwitchProps> {
169 public static displayName = `${DISPLAYNAME_PREFIX}.Switch`;
170
171 public render() {
172 const { innerLabelChecked, innerLabel, ...controlProps } = this.props;
173 const switchLabels =
174 innerLabel || innerLabelChecked
175 ? [
176 <div key="checked" className={Classes.CONTROL_INDICATOR_CHILD}>
177 <div className={Classes.SWITCH_INNER_TEXT}>
178 {innerLabelChecked ? innerLabelChecked : innerLabel}
179 </div>
180 </div>,
181 <div key="unchecked" className={Classes.CONTROL_INDICATOR_CHILD}>
182 <div className={Classes.SWITCH_INNER_TEXT}>{innerLabel}</div>
183 </div>,
184 ]
185 : null;
186 return (
187 <Control
188 {...controlProps}
189 type="checkbox"
190 typeClassName={Classes.SWITCH}
191 indicatorChildren={switchLabels}
192 />
193 );
194 }
195}
196
197//
198// Radio
199//
200
201/** @deprecated use RadioProps */
202export type IRadioProps = ControlProps;
203// eslint-disable-next-line deprecation/deprecation
204export type RadioProps = IRadioProps;
205
206@polyfill
207export class Radio extends AbstractPureComponent2<RadioProps> {
208 public static displayName = `${DISPLAYNAME_PREFIX}.Radio`;
209
210 public render() {
211 return <Control {...this.props} type="radio" typeClassName={Classes.RADIO} />;
212 }
213}
214
215//
216// Checkbox
217//
218
219// eslint-disable-next-line deprecation/deprecation
220export type CheckboxProps = ICheckboxProps;
221/** @deprecated use CheckboxProps */
222export interface ICheckboxProps extends ControlProps {
223 /** Whether this checkbox is initially indeterminate (uncontrolled mode). */
224 defaultIndeterminate?: boolean;
225
226 /**
227 * Whether this checkbox is indeterminate, or "partially checked."
228 * The checkbox will appear with a small dash instead of a tick to indicate that the value
229 * is not exactly true or false.
230 *
231 * Note that this prop takes precendence over `checked`: if a checkbox is marked both
232 * `checked` and `indeterminate` via props, it will appear as indeterminate in the DOM.
233 */
234 indeterminate?: boolean;
235}
236
237export interface ICheckboxState {
238 // Checkbox adds support for uncontrolled indeterminate state
239 indeterminate: boolean;
240}
241
242@polyfill
243export class Checkbox extends AbstractPureComponent2<CheckboxProps, ICheckboxState> {
244 public static displayName = `${DISPLAYNAME_PREFIX}.Checkbox`;
245
246 public static getDerivedStateFromProps({ indeterminate }: CheckboxProps): ICheckboxState | null {
247 // put props into state if controlled by props
248 if (indeterminate != null) {
249 return { indeterminate };
250 }
251
252 return null;
253 }
254
255 public state: ICheckboxState = {
256 indeterminate: this.props.indeterminate || this.props.defaultIndeterminate || false,
257 };
258
259 // must maintain internal reference for `indeterminate` support
260 public input: HTMLInputElement | null = null;
261
262 private handleInputRef: IRef<HTMLInputElement> = refHandler(this, "input", this.props.inputRef);
263
264 public render() {
265 const { defaultIndeterminate, indeterminate, ...controlProps } = this.props;
266 return (
267 <Control
268 {...controlProps}
269 inputRef={this.handleInputRef}
270 onChange={this.handleChange}
271 type="checkbox"
272 typeClassName={Classes.CHECKBOX}
273 />
274 );
275 }
276
277 public componentDidMount() {
278 this.updateIndeterminate();
279 }
280
281 public componentDidUpdate(prevProps: CheckboxProps) {
282 this.updateIndeterminate();
283 if (prevProps.inputRef !== this.props.inputRef) {
284 setRef(prevProps.inputRef, null);
285 this.handleInputRef = refHandler(this, "input", this.props.inputRef);
286 setRef(this.props.inputRef, this.input);
287 }
288 }
289
290 private updateIndeterminate() {
291 if (this.input != null) {
292 this.input.indeterminate = this.state.indeterminate;
293 }
294 }
295
296 private handleChange = (evt: React.ChangeEvent<HTMLInputElement>) => {
297 const { indeterminate } = evt.target;
298 // update state immediately only if uncontrolled
299 if (this.props.indeterminate == null) {
300 this.setState({ indeterminate });
301 }
302 // otherwise wait for props change. always invoke handler.
303 this.props.onChange?.(evt);
304 };
305}