Skip to content

Commit

Permalink
WIP #25 File picker in tables
Browse files Browse the repository at this point in the history
  • Loading branch information
Polleps committed Mar 9, 2023
1 parent b9ce072 commit 99fc6bd
Show file tree
Hide file tree
Showing 21 changed files with 598 additions and 220 deletions.
2 changes: 1 addition & 1 deletion data-browser/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
"react-helmet-async": "^1.3.0",
"react-hot-toast": "^2.4.0",
"react-hotkeys-hook": "^3.1.0",
"react-icons": "^4.2.0",
"react-icons": "^4.8.0",
"react-intersection-observer": "^9.4.1",
"react-is": "^18",
"react-markdown": "^8.0.3",
Expand Down
8 changes: 5 additions & 3 deletions data-browser/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -84,9 +84,11 @@ function App(): JSX.Element {
<DropdownContainer>
<DialogContainer>
<PopoverContainer>
<NavWrapper>
<AppRoutes />
</NavWrapper>
<DropdownContainer>
<NavWrapper>
<AppRoutes />
</NavWrapper>
</DropdownContainer>
</PopoverContainer>
<NetworkIndicator />
</DialogContainer>
Expand Down
2 changes: 1 addition & 1 deletion data-browser/src/components/ClassDetail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export function ClassDetail({ resource }: Props): JSX.Element {
<Detail>
<>
{'is a '}
{getIconForClass(klass)}
{getIconForClass(classes[0])}
<InlineFormattedResourceList subjects={classes} />
</>
</Detail>
Expand Down
15 changes: 9 additions & 6 deletions data-browser/src/components/Dialog/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { FaTimes } from 'react-icons/fa';
import styled, { keyframes } from 'styled-components';
import { effectTimeout } from '../../helpers/effectTimeout';
import { Button } from '../Button';
import { DropdownContainer } from '../forms/DropdownInput';
import { PopoverContainer } from '../Popover';
import { Slot } from '../Slot';
import {
Expand Down Expand Up @@ -142,12 +143,14 @@ const InnerDialog: React.FC<React.PropsWithChildren<InternalDialogProps>> = ({
<StyledDialog ref={dialogRef} onMouseDown={handleOutSideClick}>
<StyledInnerDialog ref={innerDialogRef}>
<PopoverContainer>
<CloseButtonSlot slot='close'>
<Button icon onClick={cancelDialog} aria-label='close'>
<FaTimes />
</Button>
</CloseButtonSlot>
{children}
<DropdownContainer>
<CloseButtonSlot slot='close'>
<Button icon onClick={cancelDialog} aria-label='close'>
<FaTimes />
</Button>
</CloseButtonSlot>
{children}
</DropdownContainer>
</PopoverContainer>
</StyledInnerDialog>
</StyledDialog>,
Expand Down
6 changes: 2 additions & 4 deletions data-browser/src/components/Popover.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import React, { useCallback, useContext, useEffect } from 'react';
import * as RadixPopover from '@radix-ui/react-popover';
import styled, { keyframes } from 'styled-components';
import { useDialogTreeContext } from './Dialog/dialogContext';
import { transparentize } from 'polished';
import { useDialogTreeContext } from './Dialog/dialogContext';

export interface PopoverProps {
Trigger: React.ReactNode;
Expand Down Expand Up @@ -73,9 +73,7 @@ const Content = styled(RadixPopover.Content)`
${p => p.theme.margin}rem
);
background-color: ${p => transparentize(0.2, p.theme.colors.bgBody)};
backdrop-filter: blur(5px);
/* border: 1px solid ${p => p.theme.colors.bg2}; */
/* padding: ${p => p.theme.margin}rem; */
backdrop-filter: blur(10px);
box-shadow: ${p => p.theme.boxShadowSoft};
border-radius: ${p => p.theme.radius};
position: relative;
Expand Down
220 changes: 133 additions & 87 deletions data-browser/src/components/forms/DropdownInput.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
import React, { useEffect, useRef, useState, useCallback } from 'react';
import React, {
useEffect,
useRef,
useState,
useCallback,
useContext,
} from 'react';
import { useHotkeys } from 'react-hotkeys-hook';
import * as RadixPopover from '@radix-ui/react-popover';
import { useResource, useTitle } from '@tomic/react';
import { createPortal } from 'react-dom';
import { FaCaretDown, FaTimes, FaTrash } from 'react-icons/fa';
import styled, { css } from 'styled-components';
import { Hit, useLocalSearch } from '../../helpers/useLocalSearch';
Expand All @@ -8,7 +17,7 @@ import { ResourceInline } from '../../views/ResourceInline';
import { InputOverlay, InputStyled, InputWrapper } from './InputStyles';
import ResourceLine from '../../views/ResourceLine';
import { useClickAwayListener } from '../../hooks/useClickAwayListener';
import { useResource, useTitle } from '@tomic/react';
import { useAvailableSpace } from './hooks/useAvailableSpace';

interface DropDownListProps {
required?: boolean;
Expand Down Expand Up @@ -73,6 +82,12 @@ export const DropdownInput: React.FC<DropDownListProps> = ({
const dropdownRef = useRef(null);
const openMenuButtonRef = useRef(null);
const inputRef = useRef<HTMLInputElement>(null);
const containerRef = useContext(DropdownContainerContext);
const {
ref: inputWrapperRef,
above,
below,
} = useAvailableSpace<HTMLDivElement>(isOpen);

/** Close the menu and set the value */
const handleClickOutside = useCallback(() => {
Expand Down Expand Up @@ -143,86 +158,93 @@ export const DropdownInput: React.FC<DropDownListProps> = ({
}

return (
<>
<RadixPopover.Root open={isOpen}>
<DropDownStyled>
<InputWrapper>
<ResourceInputOverlayWrapper>
{selectedItem && !isFocus && (
<StyledInputOverlay>
<ResourceInline subject={selectedItem} untabbable />
</StyledInputOverlay>
<RadixPopover.Anchor>
<InputWrapper ref={inputWrapperRef}>
<ResourceInputOverlayWrapper>
{selectedItem && !isFocus && (
<StyledInputOverlay>
<ResourceInline subject={selectedItem} untabbable />
</StyledInputOverlay>
)}
<InputStyled
onFocus={handleFocus}
disabled={disabled}
size={5}
required={required}
placeholder={placeholder}
// This might not be the most pretty approach, maybe I should use an overlay element
// That would also allow for a richer resource view in the input
value={inputValue}
onChange={handleInputChange}
ref={inputRef}
{...props}
/>
</ResourceInputOverlayWrapper>
{selectedItem ? (
<ButtonInput
disabled={disabled}
type='button'
onClick={clearSelection}
title='clear selection'
aria-label='clear selection'
>
<FaTimes />
</ButtonInput>
) : null}
{options.length > 0 && selectedItem === undefined && (
<ButtonInput
disabled={disabled}
type='button'
onClick={() => setIsOpen(!isOpen)}
title='toggle menu'
ref={openMenuButtonRef}
aria-label={'toggle menu'}
>
<FaCaretDown />
</ButtonInput>
)}
<InputStyled
onFocus={handleFocus}
disabled={disabled}
size={5}
required={required}
placeholder={placeholder}
// This might not be the most pretty approach, maybe I should use an overlay element
// That would also allow for a richer resource view in the input
value={inputValue}
onChange={handleInputChange}
ref={inputRef}
{...props}
/>
</ResourceInputOverlayWrapper>
{selectedItem ? (
<ButtonInput
disabled={disabled}
type='button'
onClick={clearSelection}
title='clear selection'
aria-label='clear selection'
>
<FaTimes />
</ButtonInput>
) : null}
{options.length > 0 && selectedItem === undefined && (
<ButtonInput
disabled={disabled}
type='button'
onClick={() => setIsOpen(!isOpen)}
title='toggle menu'
ref={openMenuButtonRef}
aria-label={'toggle menu'}
>
<FaCaretDown />
</ButtonInput>
)}
{onRemove !== undefined && (
<ButtonInput
disabled={disabled}
type='button'
onClick={onRemove}
title='remove item'
aria-label='remove item'
>
<FaTrash />
</ButtonInput>
)}
</InputWrapper>{' '}
<DropDownWrapperWrapper onMouseEnter={() => setUseKeys(false)}>
{isOpen && (
<DropDownItemsMenu
options={options}
dropdownRef={dropdownRef}
selectedIndex={selectedIndex}
setSelectedIndex={setSelectedIndex}
setInputValue={setInputValue}
setSelectedItem={setSelectedItem}
onUpdate={onUpdate}
onCreateClick={onCreateClick}
setIsOpen={setIsOpen}
isOpen={isOpen}
useKeys={useKeys}
setUseKeys={setUseKeys}
inputValue={inputValue}
classType={classType}
/>
)}
</DropDownWrapperWrapper>
{onRemove !== undefined && (
<ButtonInput
disabled={disabled}
type='button'
onClick={onRemove}
title='remove item'
aria-label='remove item'
>
<FaTrash />
</ButtonInput>
)}
</InputWrapper>
</RadixPopover.Anchor>
<RadixPopover.Portal container={containerRef.current}>
<RadixPopover.Content collisionPadding={2}>
<div onMouseEnter={() => setUseKeys(false)}>
{isOpen && (
<DropDownItemsMenu
options={options}
dropdownRef={dropdownRef}
selectedIndex={selectedIndex}
setSelectedIndex={setSelectedIndex}
setInputValue={setInputValue}
setSelectedItem={setSelectedItem}
onUpdate={onUpdate}
onCreateClick={onCreateClick}
setIsOpen={setIsOpen}
isOpen={isOpen}
useKeys={useKeys}
setUseKeys={setUseKeys}
inputValue={inputValue}
classType={classType}
maxHeight={Math.max(above, below)}
/>
)}
</div>
</RadixPopover.Content>
</RadixPopover.Portal>
</DropDownStyled>
</>
</RadixPopover.Root>
);
};

Expand All @@ -244,6 +266,7 @@ interface DropDownItemsMenuProps {
isOpen: boolean;
/** URL of the Class. Used for showing the label for `onCreateClick` */
classType?: string;
maxHeight: number;
}

function scrollIntoView(
Expand Down Expand Up @@ -275,6 +298,7 @@ function DropDownItemsMenu({
setUseKeys,
useKeys,
classType,
maxHeight,
}: DropDownItemsMenuProps): JSX.Element | null {
const searchResults = useLocalSearch(inputValue, options);
const showCreateOption = onCreateClick && !inputValue.startsWith('http');
Expand Down Expand Up @@ -354,7 +378,7 @@ function DropDownItemsMenu({
}

return (
<DropDownWrapper ref={dropdownRef}>
<DropDownWrapper ref={dropdownRef} height={maxHeight}>
{items.map((item, index) => {
if (isCreateOption(item)) {
return (
Expand Down Expand Up @@ -385,7 +409,7 @@ function DropDownItemsMenu({
);
}

/** A wrapper all dropdown items */
/** A wrapper for all dropdown items */
const ResourceInputOverlayWrapper = styled.div`
position: relative;
display: flex;
Expand All @@ -398,21 +422,21 @@ const DropDownStyled = styled.div`
flex: 1;
`;

const DropDownWrapperWrapper = styled.ul`
margin-bottom: 0;
`;
interface DropdownWrapperProps {
height: number;
}

/** A wrapper all dropdown items */
const DropDownWrapper = styled.div`
const DropDownWrapper = styled.ul<DropdownWrapperProps>`
display: flex;
flex-direction: column;
background-color: ${props => props.theme.colors.bg};
border: solid 1px ${props => props.theme.colors.bg2};
border-radius: ${props => props.theme.radius};
box-shadow: ${props => props.theme.boxShadowIntense};
position: absolute;
/* position: absolute; */
z-index: ${p => p.theme.zIndex.dropdown};
max-height: 30rem;
max-height: min(calc(${p => p.height}px - 2rem), 30rem);
overflow-y: auto;
left: 0;
right: 0;
Expand Down Expand Up @@ -476,3 +500,25 @@ const StyledInputOverlay = styled(InputOverlay)`
white-space: nowrap;
}
`;

const DropdownContainerContext = React.createContext<
React.RefObject<HTMLDivElement>
>(React.createRef());

export const DropdownContainer: React.FC<React.PropsWithChildren> = ({
children,
}) => {
const dropdownContainerRef = React.useRef<HTMLDivElement>(null);

return (
<ContainerDiv ref={dropdownContainerRef}>
<DropdownContainerContext.Provider value={dropdownContainerRef}>
{children}
</DropdownContainerContext.Provider>
</ContainerDiv>
);
};

const ContainerDiv = styled.div`
display: contents;
`;
Loading

0 comments on commit 99fc6bd

Please sign in to comment.