Skip to content

Port components to ts 6 #2743

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 2 additions & 5 deletions src/app/components/form-radiogroup/form-radiogroup.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import React, {useState} from 'react';
import {treatSpaceOrEnterAsClick} from '~/helpers/events';
import type { ElementWithValidationMessage } from '../validation-message/validation-message';

type OptionItem = {
value: string;
Expand Down Expand Up @@ -37,10 +38,6 @@ function Option({
);
}

type InputElementWithValidationMessage = HTMLInputElement & {
validationMessage: string;
};

export default function FormRadioGroup({
longLabel,
name,
Expand All @@ -58,7 +55,7 @@ export default function FormRadioGroup({
const [selectedValue, setSelectedValue] = useState(checkedValue);
const validate = React.useCallback(() => {
const invalid =
ref.current?.querySelector<InputElementWithValidationMessage>(
ref.current?.querySelector<ElementWithValidationMessage>(
':invalid'
);

Expand Down
10 changes: 4 additions & 6 deletions src/app/components/multiselect/book-tags/book-tags.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,8 @@ import React from 'react';
import Multiselect, {MultiselectContextProvider} from '../multiselect';
import useSFBookContext, {SFBookContextProvider} from './sf-book-context';
import useMultiselectContext from '../multiselect-context';
import useToggleContext, {
ToggleContextProvider
} from '~/components/toggle/toggle-context';
import {IfToggleIsOpen} from '~/components/toggle/toggle';
import useToggleContext from '~/components/toggle/toggle-context';
import Toggle, {IfToggleIsOpen} from '~/components/toggle/toggle';
import ToggleControlBar from '~/components/toggle/toggle-control-bar';
import ArrowToggle from '~/components/toggle/arrow-toggle';
import BookOptions from './book-options';
Expand Down Expand Up @@ -107,14 +105,14 @@ type MultiselectArgs = Parameters<typeof Multiselect>[0];
export default function BookTagsMultiselect(passThruProps: MultiselectArgs) {
return (
<Multiselect {...passThruProps}>
<ToggleContextProvider>
<Toggle>
<ToggleControlBar Indicator={ArrowToggle}>
<TagList />
</ToggleControlBar>
<IfToggleIsOpen>
<BookOptions />
</IfToggleIsOpen>
</ToggleContextProvider>
</Toggle>
</Multiselect>
);
}
36 changes: 23 additions & 13 deletions src/app/components/multiselect/multiselect.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@ import React from 'react';
import useMultiselectContext, {
MultiselectContextProvider
} from './multiselect-context';
import ValidationMessage from '~/components/validation-message/validation-message';
import ValidationMessage, { ElementWithValidationMessage }
from '~/components/validation-message/validation-message';
import './multiselect.scss';

type ElementRef = React.MutableRefObject<
HTMLInputElement | HTMLSelectElement | null
>;
type ValidatableElementRef = React.MutableRefObject<ElementWithValidationMessage & HTMLInputElement>;
type SelectRef = React.MutableRefObject<HTMLSelectElement>;

function HiddenSelect({
name,
Expand All @@ -16,7 +16,7 @@ function HiddenSelect({
}: {
name: string;
required?: boolean;
elementRef: ElementRef;
elementRef: SelectRef;
}) {
const {selectedItems} = useMultiselectContext();

Expand All @@ -25,7 +25,7 @@ function HiddenSelect({
multiple
name={name}
required={required}
ref={elementRef as React.MutableRefObject<HTMLSelectElement>}
ref={elementRef}
hidden
>
{selectedItems.map((i) => (
Expand All @@ -42,7 +42,7 @@ function HiddenSingleField({
}: {
name: string;
required?: boolean;
elementRef: ElementRef;
elementRef: ValidatableElementRef;
}) {
const {selectedItems} = useMultiselectContext();
const value = selectedItems.map((i) => i.value).join(';');
Expand All @@ -53,13 +53,13 @@ function HiddenSingleField({
className="hidden-input"
name={name}
required={required}
ref={elementRef as React.MutableRefObject<HTMLInputElement>}
ref={elementRef}
value={value}
/>
);
}

function MSValidationMessage({elementRef}: {elementRef: ElementRef}) {
function MSValidationMessage({elementRef}: {elementRef: ValidatableElementRef}) {
const {selectedItems} = useMultiselectContext();

return (
Expand All @@ -81,6 +81,17 @@ function useOnChangeHandler(onChange?: (items: unknown[]) => void) {
// Exporting here for convenience
export {MultiselectContextProvider, useMultiselectContext};

function HiddenField({oneField, name, required, elementRef}: {
oneField?: boolean;
name: string;
required?: boolean;
elementRef: React.MutableRefObject<HTMLElement | null>
}) {
return oneField ?
<HiddenSingleField {...{name, required}} elementRef={elementRef as ValidatableElementRef} /> :
<HiddenSelect name={name} required={required} elementRef={elementRef as SelectRef} />;
}

export default function Multiselect({
name,
required,
Expand All @@ -93,17 +104,16 @@ export default function Multiselect({
oneField?: boolean;
onChange?: Parameters<typeof useOnChangeHandler>[0];
}>) {
const elementRef = React.useRef<HTMLInputElement | HTMLSelectElement>(null);
const HiddenField = oneField ? HiddenSingleField : HiddenSelect;
const elementRef = React.useRef<ElementWithValidationMessage | HTMLSelectElement>(null);

useOnChangeHandler(onChange);
return (
<React.Fragment>
<div className="multiselect">
{name && <HiddenField {...{name, required, elementRef}} />}
{name && <HiddenField {...{oneField, name, required, elementRef}} />}
{children}
</div>
<MSValidationMessage elementRef={elementRef} />
<MSValidationMessage elementRef={elementRef as ValidatableElementRef} />
</React.Fragment>
);
}
18 changes: 6 additions & 12 deletions src/app/components/select/drop-down/drop-down.tsx
Original file line number Diff line number Diff line change
@@ -1,25 +1,19 @@
import React from 'react';
import Select from '../select';
import useSelectContext, {SelectItem} from '../select-context';
import useToggleContext, {
ToggleContextProvider
} from '~/components/toggle/toggle-context';
import useToggleContext from '~/components/toggle/toggle-context';
import Toggle from '~/components/toggle/toggle';
import ToggleControlBar from '~/components/toggle/toggle-control-bar';
import ArrowToggle from '~/components/toggle/arrow-toggle';
import VerticalList from '~/components/vertical-list/vertical-list';
import VerticalList, {RenderItemProps} from '~/components/vertical-list/vertical-list';
import './drop-down.scss';

function RenderItem({
item,
current,
onMouseEnter,
onClick
}: {
item: SelectItem;
current: boolean;
onMouseEnter: React.MouseEventHandler;
onClick: React.MouseEventHandler;
}) {
}: RenderItemProps<SelectItem>) {
const {item: selectedItem} = useSelectContext();

return (
Expand Down Expand Up @@ -95,12 +89,12 @@ export default function DropDownSelect({
} & Omit<Parameters<typeof Select>[0], 'children'>) {
return (
<Select {...passThruProps}>
<ToggleContextProvider>
<Toggle>
<ToggleControlBar Indicator={ArrowToggle}>
<SelectedItem placeholder={placeholder} />
</ToggleControlBar>
<AutoFocusVerticalList options={options} />
</ToggleContextProvider>
</Toggle>
</Select>
);
}
14 changes: 10 additions & 4 deletions src/app/components/select/select.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React from 'react';
import useSelectContext, {SelectContextProvider} from './select-context';
import ValidationMessage from '~/components/validation-message/validation-message';
import ValidationMessage, { ElementWithValidationMessage } from '~/components/validation-message/validation-message';

function HiddenSelect({
name,
Expand All @@ -9,7 +9,7 @@ function HiddenSelect({
}: {
name: string;
required?: boolean;
elementRef: React.RefObject<HTMLSelectElement>;
elementRef: React.MutableRefObject<HTMLSelectElement | null>;
}) {
const {item} = useSelectContext();

Expand All @@ -23,11 +23,17 @@ function HiddenSelect({
function SValidationMessage({
elementRef
}: {
elementRef: React.RefObject<HTMLSelectElement>;
elementRef: React.MutableRefObject<HTMLSelectElement & ElementWithValidationMessage | null>;
}) {
const {item} = useSelectContext();

return <ValidationMessage watchValue={item} elementRef={elementRef} />;
if (elementRef.current === null) {
return null;
}

return <ValidationMessage
watchValue={item} elementRef={elementRef as React.MutableRefObject<ElementWithValidationMessage>}
/>;
}

export default function Select({
Expand Down
11 changes: 0 additions & 11 deletions src/app/components/toggle/arrow-toggle.js

This file was deleted.

7 changes: 7 additions & 0 deletions src/app/components/toggle/arrow-toggle.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import React from 'react';
import cn from 'classnames';
import './arrow-toggle.scss';

export default function ArrowToggle({isOpen}: {isOpen: boolean}) {
return <span className={cn('arrow-toggle with-arrow', {open: isOpen})} />;
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,14 @@ function useContextValue() {

// Multiple events might try to toggle the same direction and cancel each
// other out. This throws away the later calls within 1/8 second.
const dbToggle = useMemo(() => throttle(toggle, 125, {trailing: false}), [toggle]);
const dbToggle = useMemo(
() => throttle(toggle, 125, {trailing: false}),
[toggle]
);

return {isOpen, toggle: dbToggle, close: () => dbToggle(false)};
}

const {useContext, ContextProvider} = buildContext({useContextValue});

export {
useContext as default,
ContextProvider as ToggleContextProvider
};
export {useContext as default, ContextProvider as ToggleContextProvider};
Original file line number Diff line number Diff line change
@@ -1,15 +1,22 @@
import React from 'react';
import useToggleContext from './toggle-context';
import { useRefToFocusAfterClose } from './toggle';
import {useRefToFocusAfterClose} from './toggle';
import cn from 'classnames';
import { treatKeydownAsClick } from '~/helpers/events';
import {treatKeydownAsClick} from '~/helpers/events';
import './toggle-control-bar.scss';

export default function ToggleControlBar({ Indicator, children }) {
const { isOpen, toggle } = useToggleContext();
export type ToggleFunction = ({isOpen}: {isOpen: boolean}) => JSX.Element;

export default function ToggleControlBar({
Indicator,
children
}: React.PropsWithChildren<{
Indicator: ToggleFunction;
}>) {
const {isOpen, toggle} = useToggleContext();
const [hasBeenOpened, setHasBeenOpened] = React.useState(false);
const onKeyDown = React.useCallback(
(event) => {
(event: React.KeyboardEvent) => {
const keyList = isOpen ? ['Escape', 'Enter', ' '] : ['Enter', ' '];

treatKeydownAsClick(event, keyList);
Expand All @@ -25,28 +32,31 @@ export default function ToggleControlBar({ Indicator, children }) {
}
}, [isOpen]);

// eslint-disable-next-line complexity
React.useEffect(() => {
if (hasBeenOpened && !isOpen) {
// Restore focus to an input if there is one, otherwise to focusRef
const focusOn =
focusRef.current.querySelector('input') || focusRef.current;
focusRef?.current?.querySelector('input') || focusRef.current;

focusOn.focus();
focusOn?.focus();
}
}, [hasBeenOpened, isOpen, focusRef]);

return (
<div
className={cn('toggle-control-bar', { open: isOpen })}
tabIndex="0"
className={cn('toggle-control-bar', {open: isOpen})}
tabIndex={0}
onClick={() => toggle()}
onKeyDown={onKeyDown}
ref={focusRef}
role="combobox"
aria-controls={listboxId}
aria-haspopup={listboxId}
aria-haspopup="listbox"
>
<div role="listbox" id={listboxId}>{children}</div>
<div role="listbox" id={listboxId}>
{children}
</div>
<Indicator isOpen={isOpen} />
</div>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,32 +1,30 @@
import React from 'react';
import useToggleContext, {ToggleContextProvider} from '~/components/toggle/toggle-context';
import useToggleContext, {
ToggleContextProvider
} from '~/components/toggle/toggle-context';

export function useRefToFocusAfterClose() {
const {isOpen} = useToggleContext();
const [hasBeenOpened, setHasBeenOpened] = React.useState(false);
const ref = React.useRef();
const ref = React.useRef<HTMLDivElement>(null);

React.useEffect(() => {
if (isOpen) {
setHasBeenOpened(true);
} else if (hasBeenOpened) {
ref.current.focus();
ref.current?.focus();
}
}, [isOpen, hasBeenOpened]);

return ref;
}

export function IfToggleIsOpen({children}) {
export function IfToggleIsOpen({children}: React.PropsWithChildren<object>) {
const {isOpen} = useToggleContext();

return isOpen && children;
}

export default function Toggle({children}) {
return (
<ToggleContextProvider>
{children}
</ToggleContextProvider>
);
export default function Toggle({children}: React.PropsWithChildren<object>) {
return <ToggleContextProvider>{children}</ToggleContextProvider>;
}
Loading