From 0a53104ed3cfaa502b278aab2fb05c300e1a1c7c Mon Sep 17 00:00:00 2001 From: Will Scullin Date: Thu, 21 Nov 2024 09:55:06 -0800 Subject: [PATCH] Pill behavior tweaks --- .../src/assets/img/query_clear_hover.svg | 15 +- .../src/components/ActionIcon/ActionIcon.tsx | 4 +- .../src/components/DatePicker/DatePicker.tsx | 5 +- .../ExploreQueryEditor/ExploreQueryEditor.tsx | 1 + .../components/FieldButton/FieldButton.tsx | 2 +- .../FieldDetailPanel/FieldDetailPanel.tsx | 1 + .../src/components/PillInput/PillInput.tsx | 167 +++++++++++------- packages/query-composer/src/core/query.ts | 2 +- 8 files changed, 124 insertions(+), 73 deletions(-) diff --git a/packages/query-composer/src/assets/img/query_clear_hover.svg b/packages/query-composer/src/assets/img/query_clear_hover.svg index 7a960481..9b45f8c5 100644 --- a/packages/query-composer/src/assets/img/query_clear_hover.svg +++ b/packages/query-composer/src/assets/img/query_clear_hover.svg @@ -1,8 +1,9 @@ - - query_clear_hover - - - - - \ No newline at end of file + + + + + + diff --git a/packages/query-composer/src/components/ActionIcon/ActionIcon.tsx b/packages/query-composer/src/components/ActionIcon/ActionIcon.tsx index 5cd8526f..e87f5c2d 100644 --- a/packages/query-composer/src/components/ActionIcon/ActionIcon.tsx +++ b/packages/query-composer/src/components/ActionIcon/ActionIcon.tsx @@ -86,6 +86,7 @@ interface ActionIconProps { action: ActionIconName; onClick?: () => void; color?: ColorKey; + title?: string; } const iconMap: Record< @@ -127,6 +128,7 @@ export const ActionIcon: React.FC = ({ action, onClick, color, + title, }) => { const sizeProps = {width: '22px', height: '22px'}; const otherProps = { @@ -137,7 +139,7 @@ export const ActionIcon: React.FC = ({ const Icon = iconMap[action]; return ( - + ); diff --git a/packages/query-composer/src/components/DatePicker/DatePicker.tsx b/packages/query-composer/src/components/DatePicker/DatePicker.tsx index a87e261a..0fe5100b 100644 --- a/packages/query-composer/src/components/DatePicker/DatePicker.tsx +++ b/packages/query-composer/src/components/DatePicker/DatePicker.tsx @@ -31,6 +31,7 @@ import { } from '../CommonElements'; import {NumberInput} from '../NumberInput'; import {SelectDropdown} from '../SelectDropdown'; +import {COLORS} from '../../colors'; interface DatePickerProps { value: Date; @@ -686,7 +687,7 @@ const WeekButton = styled.div<{ isSelected ? ` background-color: var(--malloy-composer-form-focusBackground, #f0f6ff); - color: var(--malloy-composer-dimension-strong); + color: ${COLORS.dimension.fillStrong}; border-color: var(--malloy-composer-focus, #c3d7f7); ` : ` @@ -737,7 +738,7 @@ const Day = styled(Cell)<{ isSelected ? ` background-color: var(--malloy-composer-form-focusBackground, #f0f6ff); - color: var(--malloy-composer-dimension-strong); + color: ${COLORS.dimension.fillStrong}; border-color: var(--malloy-composer-focus, #c3d7f7); ` : ` diff --git a/packages/query-composer/src/components/ExploreQueryEditor/ExploreQueryEditor.tsx b/packages/query-composer/src/components/ExploreQueryEditor/ExploreQueryEditor.tsx index 39bd5a57..52534e85 100644 --- a/packages/query-composer/src/components/ExploreQueryEditor/ExploreQueryEditor.tsx +++ b/packages/query-composer/src/components/ExploreQueryEditor/ExploreQueryEditor.tsx @@ -179,6 +179,7 @@ export const ExploreQueryEditor: React.FC = ({ action="remove" onClick={clearQuery} color={isQueryEmpty ? 'other' : 'dimension'} + title="Clear Query" /> = ({ {unsaved ? : ''} {canRemove && ( - + = ({ }; const ContextMenuDetail = styled(ContextMenuMain)` + border-radius: 4px; padding: 20px; font-family: var(--malloy-composer-font-family, sans-serif); background-color: var(--malloy-composer-menu-background, rgb(248, 248, 248)); diff --git a/packages/query-composer/src/components/PillInput/PillInput.tsx b/packages/query-composer/src/components/PillInput/PillInput.tsx index 2f2d4336..0d44bfc1 100644 --- a/packages/query-composer/src/components/PillInput/PillInput.tsx +++ b/packages/query-composer/src/components/PillInput/PillInput.tsx @@ -21,9 +21,18 @@ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ import * as React from 'react'; -import {RefObject, useEffect, useRef, useState} from 'react'; +import { + ReactElement, + ReactNode, + RefObject, + useCallback, + useEffect, + useMemo, + useRef, + useState, +} from 'react'; import styled from 'styled-components'; -import {ColorKey, COLORS} from '../../colors'; +import {COLORS} from '../../colors'; import CloseIcon from '../../assets/img/query_clear_hover.svg?react'; import {useClickOutside} from '../../hooks'; @@ -56,47 +65,82 @@ export const PillInput: React.FC = ({ const [selectedPill, setSelectedPill] = useState( undefined ); + const pillRefs = useRef>([]); const value = controlledValue || uncontrolledValue; const setValue = setControlledValue || setUncontrolledValue; - useEffect(() => { - const handler = (event: KeyboardEvent) => { - if (event.key === 'Backspace') { - if (selectedPill !== undefined) { - const newValues = [...values]; - newValues.splice(selectedPill, 1); - setValues(newValues); - if (selectedPill === 0) { - if (values.length === 1) { - setSelectedPill(undefined); - inp.current?.focus(); - } else { - setSelectedPill(0); - } - } else { - setSelectedPill(selectedPill - 1); - } - } - } else if (event.key === 'ArrowRight') { - if (selectedPill === values.length - 1) { + console.info({selectedPill, activeElement: document.activeElement}); + + const deletePill = useCallback( + (idx: number) => { + const newValues = [...values]; + newValues.splice(idx, 1); + setValues(newValues); + if (selectedPill === 0) { + if (values.length === 1) { setSelectedPill(undefined); inp.current?.focus(); - } else if (selectedPill !== undefined) { - setSelectedPill(selectedPill + 1); - } - } else if (event.key === 'ArrowLeft') { - if (selectedPill !== undefined && selectedPill > 0) { - setSelectedPill(selectedPill - 1); + } else { + setSelectedPill(0); } } else { + setSelectedPill(idx - 1); + } + }, + [selectedPill, setValues, values] + ); + + const pills = useMemo(() => { + pillRefs.current = new Array(values.length); + return values.map( + (value, index): ReactElement => ( + { + setSelectedPill(index); + event.stopPropagation(); + }} + onDelete={() => deletePill(index)} + forwardRef={ref => { + pillRefs.current[index] = ref; + }} + > + {value} + + ) + ); + }, [deletePill, selectedPill, values]); + + useEffect(() => { + if (selectedPill !== undefined) { + pillRefs.current[selectedPill]?.focus(); + } + }, [pills, selectedPill]); + + const onKeyUp = (event: React.KeyboardEvent) => { + console.info({event}); + + if (event.key === 'Backspace') { + if (selectedPill !== undefined) { + deletePill(selectedPill); + } + } else if (event.key === 'ArrowRight') { + if (selectedPill === values.length - 1) { + setSelectedPill(undefined); inp.current?.focus(); + } else if (selectedPill !== undefined) { + setSelectedPill(selectedPill + 1); } - }; - const {current} = ref; - current?.addEventListener('keyup', handler); - return () => current?.removeEventListener('keyup', handler); - }); + } else if (event.key === 'ArrowLeft') { + if (selectedPill !== undefined && selectedPill > 0) { + setSelectedPill(selectedPill - 1); + } + } else { + inp.current?.focus(); + } + }; const commitValue = () => { if (value.length > 0) { @@ -112,23 +156,12 @@ export const PillInput: React.FC = ({ return ( inp.current?.focus()} isFocused={focused || selectedPill !== undefined} ref={ref} > - {values.map((value, index) => ( - { - setSelectedPill(index); - event.stopPropagation(); - }} - > - {value} - - ))} + {pills} = ({ ); }; +interface PillProps { + onClick: (event: React.MouseEvent) => void; + onDelete: (event: React.MouseEvent) => void; + children: ReactNode; + className?: string; + forwardRef: (ref: HTMLDivElement | null) => void; +} + +const PillInner = ({ + children, + className, + forwardRef, + onClick, + onDelete, +}: PillProps) => { + return ( +
+ {children} +
+ +
+
+ ); +}; + const OuterInput = styled.div<{ isFocused: boolean; }>` @@ -196,7 +254,7 @@ const OuterInput = styled.div<{ ${({isFocused}) => (isFocused ? `border-color: #4285F4;` : '')} `; -const Pill = styled.div<{ +const Pill = styled(PillInner)<{ isSelected: boolean; }>` ${({isSelected}) => ` @@ -209,7 +267,7 @@ const Pill = styled.div<{ `} border-radius: 5px; - color: var(--malloy-composer-dimension-strong); + color: ${COLORS.dimension.fillStrong}; display: flex; align-items: center; gap: 5px; @@ -230,16 +288,3 @@ const StyledInput = styled.input` padding: 3.75px 7px; flex-grow: 1; `; - -export const CloseIconStyled = styled(CloseIcon)<{ - color: ColorKey; -}>` - cursor: pointer; - ${({color}) => { - return ` - .cross { - fill: ${COLORS[color].fillStrong}; - } - `; - }} -`; diff --git a/packages/query-composer/src/core/query.ts b/packages/query-composer/src/core/query.ts index 3fca6050..aa2374e1 100644 --- a/packages/query-composer/src/core/query.ts +++ b/packages/query-composer/src/core/query.ts @@ -459,7 +459,7 @@ export class QueryBuilder extends SourceUtils { this.query.name = definition.as || definition.name; } - public replaceQuery(field: TurtleDef): void { + public replaceQuery(field: TurtleDef | NamedQuery): void { const stage = this.query.pipeline[0]; if (!(stage.type === 'reduce' || stage.type === 'project')) { throw new Error(`Unhandled stage type ${stage.type}`);