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 |
|
21 | import classNames from "classnames";
|
22 | import * as React from "react";
|
23 | import { polyfill } from "react-lifecycles-compat";
|
24 |
|
25 | import { AbstractPureComponent2, Alignment, Classes, IRef, refHandler, setRef } from "../../common";
|
26 | import { DISPLAYNAME_PREFIX, HTMLInputProps, Props } from "../../common/props";
|
27 |
|
28 | // eslint-disable-next-line deprecation/deprecation
|
29 | export type ControlProps = IControlProps;
|
30 | /** @deprecated use ControlProps */
|
31 | export 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. */
|
95 | interface 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 | */
|
105 | const 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
|
149 | export type SwitchProps = ISwitchProps;
|
150 | /** @deprecated use SwitchProps */
|
151 | export 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 |
|
168 | export 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 */
|
202 | export type IRadioProps = ControlProps;
|
203 | // eslint-disable-next-line deprecation/deprecation
|
204 | export type RadioProps = IRadioProps;
|
205 |
|
206 |
|
207 | export 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
|
220 | export type CheckboxProps = ICheckboxProps;
|
221 | /** @deprecated use CheckboxProps */
|
222 | export 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 |
|
237 | export interface ICheckboxState {
|
238 | // Checkbox adds support for uncontrolled indeterminate state
|
239 | indeterminate: boolean;
|
240 | }
|
241 |
|
242 |
|
243 | export 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 | }
|