Skip to content
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

Add custom input fields for editing metadata #354

Merged
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 || "")}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like this component could exclusively pass around Number type values (for cases with leading zeroes it would just not yet return a value AKA undefined); NBD to keep this though

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree in theory, but running into some implementation bugs

defaultValue={days}
min={0}
/>
<NumberField
aria-label="Hours"
aswallace marked this conversation as resolved.
Show resolved Hide resolved
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
Loading