Skip to content

Commit c5be31f

Browse files
committed
Remove duplicated code and give some types more generic names
1 parent 8438007 commit c5be31f

File tree

5 files changed

+39
-155
lines changed

5 files changed

+39
-155
lines changed

components/dash-core-components/src/fragments/Dropdown.tsx

Lines changed: 14 additions & 130 deletions
Original file line numberDiff line numberDiff line change
@@ -17,69 +17,8 @@ import * as Popover from '@radix-ui/react-popover';
1717
import '../components/css/dropdown.css';
1818

1919
import isEqual from 'react-fast-compare';
20-
import {DetailedDropdownOption, DropdownProps, DropdownValue} from '../types';
21-
import {OptionsList} from '../utils/optionRendering';
22-
23-
interface DropdownOptionProps {
24-
index: number;
25-
option: DetailedDropdownOption;
26-
isSelected: boolean;
27-
onClick: (option: DetailedDropdownOption) => void;
28-
style?: React.CSSProperties;
29-
}
30-
31-
function DropdownLabel(
32-
props: DetailedDropdownOption & {index: string | number}
33-
): JSX.Element {
34-
const ctx = window.dash_component_api.useDashContext();
35-
const ExternalWrapper = window.dash_component_api.ExternalWrapper;
36-
37-
if (typeof props.label === 'object') {
38-
return (
39-
<ExternalWrapper
40-
component={props.label}
41-
componentPath={[...ctx.componentPath, props.index]}
42-
/>
43-
);
44-
}
45-
const displayLabel = `${props.label ?? props.value}`;
46-
return <span title={props.title}>{displayLabel}</span>;
47-
}
48-
49-
const DropdownOption: React.FC<DropdownOptionProps> = ({
50-
option,
51-
isSelected,
52-
onClick,
53-
style,
54-
index,
55-
}) => {
56-
return (
57-
<label
58-
className={`dash-dropdown-option ${isSelected ? 'selected' : ''}`}
59-
role="option"
60-
aria-selected={isSelected}
61-
style={style}
62-
title={option.title}
63-
>
64-
<input
65-
type="checkbox"
66-
checked={isSelected}
67-
value={
68-
typeof option.value === 'boolean'
69-
? `${option.value}`
70-
: option.value
71-
}
72-
disabled={!!option.disabled}
73-
onChange={() => onClick(option)}
74-
readOnly
75-
className="dash-dropdown-option-checkbox"
76-
/>
77-
<span className="dash-dropdown-option-text">
78-
<DropdownLabel {...option} index={index} />
79-
</span>
80-
</label>
81-
);
82-
};
20+
import {DetailedOption, DropdownProps, OptionValue} from '../types';
21+
import {OptionsList, OptionLabel} from '../utils/optionRendering';
8322

8423
const Dropdown = (props: DropdownProps) => {
8524
const {
@@ -98,12 +37,9 @@ const Dropdown = (props: DropdownProps) => {
9837
style,
9938
value,
10039
} = props;
101-
const [optionsCheck, setOptionsCheck] =
102-
useState<DetailedDropdownOption[]>();
40+
const [optionsCheck, setOptionsCheck] = useState<DetailedOption[]>();
10341
const [isOpen, setIsOpen] = useState(false);
104-
const [displayOptions, setDisplayOptions] = useState<
105-
DetailedDropdownOption[]
106-
>([]);
42+
const [displayOptions, setDisplayOptions] = useState<DetailedOption[]>([]);
10743
const persistentOptions = useRef<DropdownProps['options']>([]);
10844
const dropdownContainerRef = useRef<HTMLDivElement>(null);
10945

@@ -124,7 +60,7 @@ const Dropdown = (props: DropdownProps) => {
12460
[persistentOptions.current, searchable, search_value]
12561
);
12662

127-
const sanitizedValues: DropdownValue[] = useMemo(() => {
63+
const sanitizedValues: OptionValue[] = useMemo(() => {
12864
if (value instanceof Array) {
12965
return value;
13066
}
@@ -134,58 +70,8 @@ const Dropdown = (props: DropdownProps) => {
13470
return [value];
13571
}, [value]);
13672

137-
const toggleOption = useCallback(
138-
(option: DetailedDropdownOption) => {
139-
const isCurrentlySelected = sanitizedValues.includes(option.value);
140-
141-
// Close dropdown if closeOnSelect is true (default behavior)
142-
if (closeOnSelect !== false) {
143-
setIsOpen(false);
144-
}
145-
146-
if (multi) {
147-
let newValues: DropdownValue[];
148-
149-
if (isCurrentlySelected) {
150-
// Deselecting: only allow if clearable is true or more than one option selected
151-
if (clearable || sanitizedValues.length > 1) {
152-
newValues = sanitizedValues.filter(
153-
v => v !== option.value
154-
);
155-
} else {
156-
// Cannot deselect the last option when clearable is false
157-
return;
158-
}
159-
} else {
160-
// Selecting: add to current selection
161-
newValues = [...sanitizedValues, option.value];
162-
}
163-
164-
setProps({value: newValues});
165-
} else {
166-
let newValue: DropdownValue | null;
167-
168-
if (isCurrentlySelected) {
169-
// Deselecting: only allow if clearable is true
170-
if (clearable) {
171-
newValue = null;
172-
} else {
173-
// Cannot deselect when clearable is false
174-
return;
175-
}
176-
} else {
177-
// Selecting: set as the single value
178-
newValue = option.value;
179-
}
180-
181-
setProps({value: newValue});
182-
}
183-
},
184-
[multi, clearable, closeOnSelect, sanitizedValues]
185-
);
186-
18773
const updateSelection = useCallback(
188-
(selection: DropdownValue[]) => {
74+
(selection: OptionValue[]) => {
18975
if (closeOnSelect !== false) {
19076
setIsOpen(false);
19177
}
@@ -266,7 +152,7 @@ const Dropdown = (props: DropdownProps) => {
266152
);
267153
return (
268154
<React.Fragment key={`${option?.value}-${i}`}>
269-
{option && <DropdownLabel {...option} index={i} />}
155+
{option && <OptionLabel {...option} index={i} />}
270156
{i === sanitizedValues.length - 1 ? '' : ', '}
271157
</React.Fragment>
272158
);
@@ -283,13 +169,6 @@ const Dropdown = (props: DropdownProps) => {
283169
);
284170
}, [clearable, sanitizedValues, displayOptions, search_value]);
285171

286-
const handleOptionClick = useCallback(
287-
(option: DetailedDropdownOption) => {
288-
toggleOption(option);
289-
},
290-
[toggleOption]
291-
);
292-
293172
const handleClear = useCallback(() => {
294173
const finalValue: DropdownProps['value'] = multi ? [] : null;
295174
setProps({value: finalValue});
@@ -433,8 +312,8 @@ const Dropdown = (props: DropdownProps) => {
433312

434313
if (open) {
435314
// Sort options: selected first, then unselected
436-
const selectedOptions: DetailedDropdownOption[] = [];
437-
const unselectedOptions: DetailedDropdownOption[] = [];
315+
const selectedOptions: DetailedOption[] = [];
316+
const unselectedOptions: DetailedOption[] = [];
438317

439318
// First, collect selected options in the order they appear in the `value` array
440319
sanitizedValues.forEach(value => {
@@ -583,6 +462,11 @@ const Dropdown = (props: DropdownProps) => {
583462
onSelectionChange={updateSelection}
584463
className="dash-dropdown-options"
585464
optionClassName="dash-dropdown-option"
465+
optionStyle={{
466+
height: optionHeight
467+
? `${optionHeight}px`
468+
: undefined,
469+
}}
586470
/>
587471
</>
588472
)}

components/dash-core-components/src/types.ts

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -344,16 +344,16 @@ export interface RangeSliderProps {
344344
persistence_type?: PersistenceTypes;
345345
}
346346

347-
export type DropdownValue = string | number | boolean;
347+
export type OptionValue = string | number | boolean;
348348

349-
export type DetailedDropdownOption = {
349+
export type DetailedOption = {
350350
label: string | DashComponent | DashComponent[];
351351
/**
352352
* The value of the option. This value
353353
* corresponds to the items specified in the
354354
* `value` property.
355355
*/
356-
value: DropdownValue;
356+
value: OptionValue;
357357
/**
358358
* If true, this option is disabled and cannot be selected.
359359
*/
@@ -376,22 +376,22 @@ export type DetailedDropdownOption = {
376376
/**
377377
* Array of options where the label and the value are the same thing, or an option dict
378378
*/
379-
export type DropdownOptionsArray = (DropdownValue | DetailedDropdownOption)[];
379+
export type OptionsArray = (OptionValue | DetailedOption)[];
380380

381381
/**
382382
* Simpler `options` representation in dictionary format. The order is not guaranteed.
383383
* {`value1`: `label1`, `value2`: `label2`, ... }
384384
* which is equal to
385385
* [{label: `label1`, value: `value1`}, {label: `label2`, value: `value2`}, ...]
386386
*/
387-
export type DropdownOptionsDict = Record<string, string>;
387+
export type OptionsDict = Record<string, string>;
388388

389389
export interface DropdownProps {
390390
/**
391391
* An array of options {label: [string|number], value: [string|number]},
392392
* an optional disabled field can be used for each option
393393
*/
394-
options?: DropdownOptionsArray | DropdownOptionsDict;
394+
options?: OptionsArray | OptionsDict;
395395

396396
/**
397397
* The value of the input. If `multi` is false (the default)
@@ -401,7 +401,7 @@ export interface DropdownProps {
401401
* array of items with values corresponding to those in the
402402
* `options` prop.
403403
*/
404-
value?: DropdownValue | DropdownValue[] | null;
404+
value?: OptionValue | OptionValue[] | null;
405405

406406
/**
407407
* If true, the user can select multiple values
@@ -502,12 +502,12 @@ export interface ChecklistProps {
502502
/**
503503
* An array of options
504504
*/
505-
options?: DropdownOptionsArray | DropdownOptionsDict;
505+
options?: OptionsArray | OptionsDict;
506506

507507
/**
508508
* The currently selected value
509509
*/
510-
value?: DropdownValue[] | null;
510+
value?: OptionValue[] | null;
511511

512512
/**
513513
* Indicates whether the options labels should be displayed inline (true=horizontal)

components/dash-core-components/src/utils/dropdownSearch.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import {
55
UnorderedSearchIndex,
66
} from 'js-search';
77
import {sanitizeOptions} from './optionTypes';
8-
import {DetailedDropdownOption, DropdownProps} from '../types';
8+
import {DetailedOption, DropdownProps} from '../types';
99

1010
// Custom tokenizer, see https://github.com/bvaughn/js-search/issues/43
1111
// Split on spaces
@@ -20,8 +20,8 @@ const TOKENIZER = {
2020
};
2121

2222
interface FilteredOptionsResult {
23-
sanitizedOptions: DetailedDropdownOption[];
24-
filteredOptions: DetailedDropdownOption[];
23+
sanitizedOptions: DetailedOption[];
24+
filteredOptions: DetailedOption[];
2525
}
2626

2727
/**
@@ -83,7 +83,7 @@ export function createFilteredOptions(
8383
search.addDocuments(sanitized);
8484
}
8585

86-
const filtered = search.search(searchValue) as DetailedDropdownOption[];
86+
const filtered = search.search(searchValue) as DetailedOption[];
8787

8888
return {
8989
sanitizedOptions: sanitized || [],

components/dash-core-components/src/utils/optionRendering.tsx

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import React from 'react';
22
import {append, includes, without} from 'ramda';
3-
import {DetailedDropdownOption, DropdownValue} from 'src/types';
3+
import {DetailedOption, OptionValue} from 'src/types';
44
import '../components/css/optionslist.css';
55

66
interface StylingProps {
@@ -18,13 +18,13 @@ interface StylingProps {
1818

1919
interface OptionProps extends StylingProps {
2020
index: number;
21-
option: DetailedDropdownOption;
21+
option: DetailedOption;
2222
isSelected: boolean;
23-
onChange: (option: DetailedDropdownOption) => void;
23+
onChange: (option: DetailedOption) => void;
2424
}
2525

26-
function OptionLabel(
27-
props: DetailedDropdownOption & {index: string | number}
26+
export function OptionLabel(
27+
props: DetailedOption & {index: string | number}
2828
): JSX.Element {
2929
const ctx = window.dash_component_api.useDashContext();
3030
const ExternalWrapper = window.dash_component_api.ExternalWrapper;
@@ -108,9 +108,9 @@ export const Option: React.FC<OptionProps> = ({
108108
};
109109

110110
interface OptionsListProps extends StylingProps {
111-
options: DetailedDropdownOption[];
112-
selected: DropdownValue[];
113-
onSelectionChange: (selected: DropdownValue[]) => void;
111+
options: DetailedOption[];
112+
selected: OptionValue[];
113+
onSelectionChange: (selected: OptionValue[]) => void;
114114
}
115115

116116
export const OptionsList: React.FC<OptionsListProps> = ({
@@ -134,7 +134,7 @@ export const OptionsList: React.FC<OptionsListProps> = ({
134134
option={option}
135135
isSelected={isSelected}
136136
onChange={option => {
137-
let newValue: DropdownValue[];
137+
let newValue: OptionValue[];
138138
if (includes(option.value, selected)) {
139139
newValue = without([option.value], selected);
140140
} else {

components/dash-core-components/src/utils/optionTypes.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
import React from 'react';
2-
import {DetailedDropdownOption, DropdownProps, DropdownValue} from '../types';
2+
import {DetailedOption, DropdownProps, OptionValue} from '../types';
33

4-
const isDropdownValue = (option: unknown): option is DropdownValue => {
4+
const isDropdownValue = (option: unknown): option is OptionValue => {
55
return ['string', 'number', 'boolean'].includes(typeof option);
66
};
77

88
export const sanitizeOptions = (
99
options: DropdownProps['options']
10-
): DetailedDropdownOption[] => {
10+
): DetailedOption[] => {
1111
if (typeof options === 'object' && !(options instanceof Array)) {
1212
return Object.entries(options).map(([value, label]) => ({
1313
label: React.isValidElement(label) ? label : String(label),

0 commit comments

Comments
 (0)