adobe / react-spectrum

A collection of libraries and tools that help you build adaptive, accessible, and robust user experiences.
https://react-spectrum.adobe.com
Apache License 2.0
12.95k stars 1.12k forks source link

Allow dateFields to have different formats #6266

Open yusijs opened 6 months ago

yusijs commented 6 months ago

Provide a general summary of the feature here

Our users are from multiple countries around the world but when they are travelling and work on different locations, there are some "issues" with how the date-inputs are formatted due to the locale on their temporary machines (if not on laptops). A request was made that the input format should be possible to customize to have the literal months rather than numerical.

πŸ€” Expected Behavior?

I would expect something like this:

const formatter = useDateFormatter({ month: 'short', year: 'numeric', day: '2-digit' })
const state = useDateFieldState({...props, formatter})

which would result in an input looking something like this: image

😯 Current Behavior

The format is always numerical based on the default locale: image

πŸ’ Possible Solution

No response

πŸ”¦ Context

Make it easier to have human-readable dates, without the users having to wonder if 03/05/2024 means the 3rd of May or the 5th of March, depending on which computer they are currently working from.

πŸ’» Examples

No response

🧒 Your Company/Team

No response

πŸ•· Tracking Issue

No response

binaryartifex commented 6 months ago

this. for the love of god, this.

binaryartifex commented 6 months ago

@yusijs what I ultimately ended up doing was thinking a little outside the box and just adding a 'hidden' class on the entire DateInput inner component and adding my own span with a formatted date value. everything works exactly the same as before, screen reader behaves the exact same as well. Minus the types and styles, this is my entire date picker component. The styles.input() class only has a class of hidden on it.

export const DatePicker = forwardRef<HTMLDivElement, DatePickerProps>(function DatePicker(
  { description, errorMessage, hideLabel = false, label, onChange, placeholder, ...props },
  ref,
) {
  const formatter = useDateFormatter({ dateStyle: "medium" });
  const timezone = getLocalTimeZone();
  const isInvalid = Boolean(errorMessage || props.isInvalid);
  const styles = datePickerStyles({ isInvalid });

  const formatDate = useCallback(
    (value: DateValue | null) => (value ? formatter.format(value.toDate(timezone)) : placeholder),
    [formatter, placeholder, timezone],
  );

  const handleChange = useCallback(
    (value: DateValue) => onChange && onChange(value.toString()),
    [onChange],
  );

  return (
    <AriaDatePicker
      {...props}
      className={styles.base()}
      isInvalid={isInvalid}
      onChange={handleChange}
      ref={ref}
    >
      {({ state }) => (
        <Fragment>
          <div>
            <FieldLabel hideLabel={hideLabel}>{label}</FieldLabel>
            {description && <FieldDescription>{description}</FieldDescription>}
          </div>
          <div className={styles.body()}>
            <Group className={styles.field()}>
              <span className={styles.value()}>{formatDate(state.value)}</span>
              <DateInput className={styles.input()}>
                {(segment) => <DateSegment className={styles.segment()} segment={segment} />}
              </DateInput>
              <Button className={styles.button()}>
                <Icon.Interface.Calendar />
              </Button>
            </Group>
            <FieldValidation>{errorMessage}</FieldValidation>
          </div>
          <Popover className={styles.popover()}>
            <Dialog>
              <Calendar />
            </Dialog>
          </Popover>
        </Fragment>
      )}
    </AriaDatePicker>
  );
});

image

yusijs commented 6 months ago

@binaryartifex I'm looking into creating an overlay that shows the formatted value on blur, but the input as numeric. It will work, but I'd obviously prefer this being something that just works, and also has a forgiving input.

To be clear though, I'm not using react-aria-components, but the hooks from react-aria, so I have a bit more code than you πŸ˜…

LFDanLu commented 6 months ago

Would the formatted value month value revert to a number when the user enters one of the number segments?

yusijs commented 6 months ago

Would the formatted value month value revert to a number when the user enters one of the number segments?

Ideally it would be possible to write w/ literals as well, but not sure how feasible that is. I've actually implemented the solution you mention here on my end now, as I figured a neat way to solve it yesterday. Ideally I would like it out of the box, but this works as well tbh.. :)

When using a literal month, the format often turns a bit around: Localized -> December 31, 2024 Numeric -> 31.12.2024 Replaced on blur -> 31. December. 2024

So it's not localized properly, but it works.

LFDanLu commented 6 months ago

From a gut feeling writing the literals seems like it could be pretty hard, just thinking about languages that use a IME like Chinese/Japanese/etc. Would need to experiment with it, but supporting the text representations on blur like you've done might be something we could consider adding ourselves.

yusijs commented 6 months ago

From a gut feeling writing the literals seems like it could be pretty hard, just thinking about languages that use a IME like Chinese/Japanese/etc. Would need to experiment with it, but supporting the text representations on blur like you've done might be something we could consider adding ourselves.

Yeah, I imagine it's quite a bit harder to handle in multiple locales. I've handled it previously, but that was only for en-US locale, so it was fairly straight forward to handle.

It would be a great QOL improvement for the display-value to change natively though; it's a bit tedious to handle it now imo:

    <Segment
      {...segmentProps}
      onFocus={(e) => {
        setFocus(true)
        segmentProps.onFocus(e)
      }}
      onBlur={(e) => {
        setFocus(false)
        segmentProps.onBlur(e)
      }}>
            {focus
        ? (segment.isPlaceholder || segment.type === 'literal'
          ? segment.text
          : segment.text.padStart(segment.type === 'year' ? 4 : 2, '0') )
        : value}

Being able to avoid the focus-check / custom focus-handling would be neat, and reduce the additional code I've had to add quite a bit :)