Skip to content

Commit

Permalink
Merge pull request #354 from AllenInstitute/feature/metadata-editing/…
Browse files Browse the repository at this point in the history
…custom-input-fields

Add custom input fields for editing metadata
  • Loading branch information
aswallace authored Jan 6, 2025
2 parents fd404ae + 9328a44 commit fb6a0c8
Show file tree
Hide file tree
Showing 25 changed files with 539 additions and 134 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
}

.choice-group label > span {
font-size: var(--l-paragraph-size);
font-size: var(--s-paragraph-size);
margin-right: 5px;
margin-top: 1px;
padding-left: 24px !important;
Expand Down
7 changes: 5 additions & 2 deletions packages/core/components/ComboBox/ComboBox.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,8 @@
bottom: 5px;
}

.combo-box-item :is(input, button, label){
.combo-box-item :is(input, button, label),
.options-container :is(input, button, label) {
color: var(--primary-text-color);
}

Expand Down Expand Up @@ -93,7 +94,9 @@

.combo-box-item button:not(:disabled):hover,
.combo-box-item > div:hover,
.combo-box-item > div:hover :is(input, label) {
.combo-box-item > div:hover :is(input, label),
.options-container button:not(:disabled):hover,
.options-container > div:hover :is(input, label){
background-color: var(--highlight-background-color);
color: var(--highlight-text-color);
}
Expand Down
3 changes: 1 addition & 2 deletions packages/core/components/ComboBox/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ interface Props {
multiSelect?: boolean;
options: IComboBoxOption[];
placeholder: string;
useComboBoxAsMenuWidth?: boolean;
onChange?: (option: IComboBoxOption | undefined, value?: string | undefined) => void;
}

Expand Down Expand Up @@ -67,7 +66,7 @@ export default function BaseComboBox(props: Props) {
comboBoxOptionStyles={{
rootChecked: styles.comboBoxItemChecked,
}}
useComboBoxAsMenuWidth={props?.useComboBoxAsMenuWidth}
useComboBoxAsMenuWidth
/>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,6 @@
margin: 0 10px 8px;
}

.date-range-root {
flex-grow: 1;
}

.text-field div {
background-color: var(--secondary-background-color);
border: none;
Expand All @@ -54,9 +50,3 @@
margin: 0;
padding-bottom: var(--margin);
}

.read-only-placeholder {
margin-right: 20px;
font-style: italic;
color: var(--primary-text-color);
}
47 changes: 47 additions & 0 deletions packages/core/components/DateRangePicker/DateTimePicker.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
.display-block {
display: block;
}

.dateTimeWrapper {
display: flex;
}

.date-range-root {
width: 300px;
min-width: 145px;
}

.date-range-text-field div, .time-picker {
background-color: var(--secondary-background-color);
border: none;
border-radius: var(--small-border-radius);
color: var(--primary-text-color);
}

.date-range-text-field div::after, .time-picker:focus-visible {
border: 1px solid var(--aqua);
outline: none;
}

.date-range-text-field > div, .time-picker {
border: 1px solid var(--border-color)
}

.date-range-text-field i, .time-picker i {
color: var(--primary-text-color);
}

.read-only-placeholder {
margin-right: 20px;
font-style: italic;
font-size: var(--s-paragraph-size) !important; /* override FluentUI button callout font size*/
color: var(--primary-text-color);
}

.time-picker {
height: 34px;
margin: 0 0 5px 5px;
color-scheme: dark;
width: 150px;
padding: 0 5px;
}
63 changes: 63 additions & 0 deletions packages/core/components/DateRangePicker/DateTimePicker.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { DatePicker } from "@fluentui/react";
import * as React from "react";

import styles from "./DateTimePicker.module.css";

interface DateTimePickerProps {
className?: string;
placeholder: string;
defaultDate?: Date;
onSelectDate: (date: Date | null | undefined) => void;
showTimeSelection?: boolean; // Show time input; default false
}

/**
* DatePicker that can also function as DateTimePicker
* by including showTimeSelection prop
*/
export default function DateTimePicker(props: DateTimePickerProps) {
const { onSelectDate } = props;
const [date, setDate] = React.useState<Date | undefined>(props?.defaultDate);
const [time, setTime] = React.useState<string | undefined>("");

React.useEffect(() => {
if (!date && !time) return;
// Prioritize the date from datePicked, otherwise set to today
const combinedDateTime: Date = date || new Date();
if (time) {
combinedDateTime.setHours(Number(time.split(":")[0]));
combinedDateTime.setMinutes(Number(time.split(":")[1]));
combinedDateTime.setSeconds(Number(time.split(":")[2]));
}
onSelectDate(combinedDateTime);
}, [date, time, onSelectDate]);

return (
<>
<DatePicker
className={props?.className}
styles={{
root: styles.dateRangeRoot,
readOnlyPlaceholder: styles.readOnlyPlaceholder,
textField: styles.dateRangeTextField,
}}
placeholder={props.placeholder}
onSelectDate={(date) => setDate(date || undefined)}
value={date}
/>
{props?.showTimeSelection && (
<input
type="time"
className={styles.timePicker}
step="2"
onChange={(ev) => setTime(ev.target.value)}
value={time}
/>
)}
</>
);
}

DateTimePicker.defaultProps = {
showTimeSelection: false,
};
27 changes: 8 additions & 19 deletions packages/core/components/DateRangePicker/index.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { DatePicker, Icon } from "@fluentui/react";
import { Icon } from "@fluentui/react";
import * as React from "react";

import { TertiaryButton } from "../Buttons";
import FileFilter from "../../entity/FileFilter";
import { extractDatesFromRangeOperatorFilterString } from "../../entity/AnnotationFormatter/date-time-formatter";

import styles from "./DateRangePicker.module.css";
import DateTimePicker from "./DateTimePicker";

interface DateRangePickerProps {
className?: string;
Expand Down Expand Up @@ -69,30 +70,18 @@ export default function DateRangePicker(props: DateRangePickerProps) {
<div className={props.className}>
<h3 className={styles.title}>{props.title}</h3>
<div className={styles.dateRangeContainer}>
<DatePicker
styles={{
root: styles.dateRangeRoot,
readOnlyPlaceholder: styles.readOnlyPlaceholder,
textField: styles.textField,
}}
ariaLabel="Select a start date"
placeholder={`Start of date range`}
<DateTimePicker
placeholder="Start of date range"
onSelectDate={(v) => (v ? onDateRangeSelection(v, null) : onReset())}
value={extractDateFromDateString(startDate?.toISOString())}
defaultDate={extractDateFromDateString(startDate?.toISOString())}
/>
<div className={styles.dateRangeSeparator}>
<Icon iconName="Forward" />
</div>
<DatePicker
styles={{
root: styles.dateRangeRoot,
readOnlyPlaceholder: styles.readOnlyPlaceholder,
textField: styles.textField,
}}
ariaLabel="Select an end date"
placeholder={`End of date range`}
<DateTimePicker
placeholder="End of date range"
onSelectDate={(v) => (v ? onDateRangeSelection(null, v) : onReset())}
value={extractDateFromDateString(endDate?.toISOString())}
defaultDate={extractDateFromDateString(endDate?.toISOString())}
/>
<TertiaryButton
className={styles.clearButton}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,18 +11,18 @@ describe("<DateRangePicker />", () => {
it("renders inputs for start and end dates with selectable date pickers", () => {
// Arrange
const onSearch = sinon.spy();
const { getAllByLabelText, getAllByRole, getByLabelText, getByRole } = render(
const { getAllByText, getAllByRole, getByRole, getByText } = render(
<DateRangePicker onSearch={onSearch} onReset={noop} currentRange={undefined} />
);

// Should render both input fields
expect(getAllByRole("combobox").length).to.equal(2);
expect(getAllByLabelText(/start/).length).to.equal(1);
expect(getAllByLabelText(/end/).length).to.equal(1);
expect(getAllByText(/Start/).length).to.equal(1);
expect(getAllByText(/End/).length).to.equal(1);

// Select a start date
expect(onSearch.called).to.equal(false);
fireEvent.click(getByLabelText(/start/));
fireEvent.click(getByText(/Start/));
fireEvent.click(getByRole("button", { name: /^18,\s/ }));
expect(onSearch.called).to.equal(true);

Expand All @@ -31,7 +31,7 @@ describe("<DateRangePicker />", () => {

// Select an end date
expect(onSearch.called).to.equal(false);
fireEvent.click(getByLabelText(/end/));
fireEvent.click(getByText(/End/));
fireEvent.click(getByRole("button", { name: /^20,\s/ }));
expect(onSearch.called).to.equal(true);
});
Expand Down
14 changes: 14 additions & 0 deletions packages/core/components/DurationForm/DurationForm.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
.input-wrapper {
display: flex;
}

.input-field {
margin-right: 3px;
max-height: fit-content;
}

.input-field > input {
padding: 6px;
font-size: var(--s-paragraph-size);
max-width: 70px;
}
79 changes: 79 additions & 0 deletions packages/core/components/DurationForm/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import * as React from "react";

import NumberField from "../NumberRangePicker/NumberField";
import annotationFormatterFactory, { AnnotationType } from "../../entity/AnnotationFormatter";

import styles from "./DurationForm.module.css";

interface DurationFormProps {
className?: string;
onChange: (totalDuration: number) => void;
title?: string;
}

/**
* This component renders a simple form for entering durations
*/
export default function DurationForm(props: DurationFormProps) {
const { onChange } = props;
const [days, setDurationDays] = React.useState<string>("0");
const [hours, setDurationHours] = React.useState<string>("0");
const [minutes, setDurationMinutes] = React.useState<string>("0");
const [seconds, setDurationSeconds] = React.useState<string>("0");
const durationFormatter = annotationFormatterFactory(AnnotationType.DURATION);

React.useEffect(() => {
const durationString = `${Number(days) || 0}D ${Number(hours) || 0}H ${
Number(minutes) || 0
}M ${Number(seconds) || 0}S`;
const totalDurationInMs = Number(durationFormatter.valueOf(durationString));
onChange(totalDurationInMs);
}, [days, hours, minutes, seconds, durationFormatter, onChange]);

return (
<div>
<h3 className={styles.title}>{props?.title}</h3>
<div className={styles.inputWrapper}>
<NumberField
aria-label="Days"
className={styles.inputField}
id="durationDays"
label="Days"
onChange={(event) => setDurationDays(event?.target?.value || "")}
defaultValue={days}
min={0}
/>
<NumberField
aria-label="Hours"
className={styles.inputField}
id="durationHours"
label="Hrs"
onChange={(event) => setDurationHours(event?.target?.value || "")}
defaultValue={hours}
min={0}
max={23}
/>
<NumberField
aria-label="Minutes"
className={styles.inputField}
id="durationMinutes"
label="Mins"
onChange={(event) => setDurationMinutes(event?.target?.value || "")}
defaultValue={minutes}
min={0}
max={59}
/>
<NumberField
aria-label="Seconds"
className={styles.inputField}
id="durationSeconds"
label="Secs"
onChange={(event) => setDurationSeconds(event?.target?.value || "")}
defaultValue={seconds}
min={0}
max={59}
/>
</div>
</div>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@

.text-field {
padding-bottom: var(--margin);
max-width: 300px;;
width: 300px;
}

.text-field > div > label, .text-field > div > label::after {
Expand Down
Loading

0 comments on commit fb6a0c8

Please sign in to comment.