@financial-times/o3-form 0.9.0 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -12,6 +12,7 @@ Provides components to construct forms.
12
12
  - [Checkbox](#checkbox)
13
13
  - [Checkbox Group](#checkbox-group)
14
14
  - [Select Input](#select-input)
15
+ - [File Upload](#file-upload)
15
16
  - [Date Input](#date-input)
16
17
  - [Contact](#contact)
17
18
  - [Licence](#licence)
@@ -535,6 +536,74 @@ import {SelectInput} from '@financial-times/o3-form';
535
536
  </SelectInput>
536
537
  ```
537
538
 
539
+ ### File Input
540
+
541
+ Use to provide an interface for users to upload files.
542
+
543
+ **TSX**
544
+ ```tsx
545
+ import {FileInput} from '@financial-times/o3-form';
546
+
547
+ <FileInputTsx
548
+ label="Driving license"
549
+ description="Photograph of the front side of your driving license" />
550
+ ```
551
+
552
+ **HTML**
553
+
554
+ ```html
555
+
556
+ <div class="o3-form-field">
557
+ <label
558
+ class="o3-form-field__label"
559
+ htmlFor="o3-form-file-input"
560
+ >
561
+ Driving license
562
+ </label>
563
+ <span
564
+ class="o3-form-input__description"
565
+ id="o3-form-description"
566
+ >
567
+ The front face of your driving license
568
+ </span>
569
+ <div class="o3-form-file-input" id="file-input-container">
570
+ <label
571
+ htmlFor="file-input"
572
+ class="o3-form-file-input__label"
573
+ tabIndex="0"
574
+ >
575
+ <span
576
+ class="o3-form-file-input__label-button o3-button o3-button--primary o3-button-icon o3-button-icon--upload">File Upload</span>
577
+ <span data-testid="file-input-label" class="o3-form-file-input__label-text">No file chosen</span>
578
+ <input
579
+ data-testid="file-input"
580
+ id="file-input"
581
+ className="o3-form-file-input__input-field"
582
+ required
583
+ aria-required="true"
584
+ type="file"
585
+ />
586
+ </label>
587
+ </div>
588
+ </div>
589
+ ```
590
+ Be sure to enable javascript to make use of delete button and uploading status:
591
+
592
+ ```js
593
+ const fileUploadElement = canvasElement.querySelector('#file-input');
594
+
595
+ if (fileUploadElement) {
596
+ new FileUploadController(fileUploadElement)
597
+ }
598
+
599
+ // Use event listeners to control the state of loading
600
+ fileUploadElement.dispatchEvent('o3Form.uploading.start');
601
+
602
+ // Remove uploading indicator
603
+ fileUploadElement.dispatchEvent('o3Form.uploading.complete');
604
+ ```
605
+
606
+
538
607
  #### Short Text Input
539
608
 
540
609
  Select input supports width control for shorter inputs:
package/cjs/CheckBox.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import * as react_jsx_runtime from 'react/jsx-runtime';
2
- import { C as CheckBoxProps, F as FormFieldsetProps } from './index-CtGGU7zN.js';
2
+ import { C as CheckBoxProps, F as FormFieldsetProps } from './index-DupfYbgc.js';
3
3
 
4
4
  declare const CheckBoxItem: (props: CheckBoxProps) => react_jsx_runtime.JSX.Element;
5
5
  declare const CheckBox: (props: CheckBoxProps) => react_jsx_runtime.JSX.Element;
@@ -1,5 +1,5 @@
1
1
  import * as react_jsx_runtime from 'react/jsx-runtime';
2
- import { D as DateInputProps } from './index-CtGGU7zN.js';
2
+ import { D as DateInputProps } from './index-DupfYbgc.js';
3
3
 
4
4
  declare const DateInput: ({ label, feedback, description, disabled, attributes, inputId, optional, }: DateInputProps) => react_jsx_runtime.JSX.Element;
5
5
 
@@ -1,5 +1,5 @@
1
1
  import * as react_jsx_runtime from 'react/jsx-runtime';
2
- import { D as DateInputProps } from './index-CtGGU7zN.js';
2
+ import { D as DateInputProps } from './index-DupfYbgc.js';
3
3
 
4
4
  declare const DateInputPicker: ({ label, feedback, description, disabled, attributes, inputId, optional, }: DateInputProps) => react_jsx_runtime.JSX.Element;
5
5
 
@@ -1,5 +1,5 @@
1
1
  import * as react_jsx_runtime from 'react/jsx-runtime';
2
- import { E as ErrorSummaryProps } from './index-CtGGU7zN.js';
2
+ import { E as ErrorSummaryProps } from './index-DupfYbgc.js';
3
3
 
4
4
  declare const ErrorSummary: ({ errors, errorMessage, }: ErrorSummaryProps) => react_jsx_runtime.JSX.Element;
5
5
 
@@ -0,0 +1,6 @@
1
+ import * as react_jsx_runtime from 'react/jsx-runtime';
2
+ import { a as FileInputProps } from './index-DupfYbgc.js';
3
+
4
+ declare const FileInput: ({ label, feedback, description, disabled, attributes, inputId, optional, isUploading }: FileInputProps) => react_jsx_runtime.JSX.Element;
5
+
6
+ export { FileInput };
@@ -0,0 +1,85 @@
1
+ "use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } } function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; }var _jsxruntime = require('react/jsx-runtime');
2
+ var _outils = require('@financial-times/o-utils');
3
+ var _FormField = require('./fieldComponents/FormField');
4
+ var _react = require('react');
5
+ const uniqueId = _outils.uidBuilder.call(void 0, "o3-form-file-input");
6
+ const FileInput = ({
7
+ label,
8
+ feedback,
9
+ description,
10
+ disabled,
11
+ attributes,
12
+ inputId,
13
+ optional,
14
+ isUploading
15
+ }) => {
16
+ const id = inputId || uniqueId("_");
17
+ const [file, setFile] = _react.useState.call(void 0, null);
18
+ const onUpload = (event) => {
19
+ setFile(_nullishCoalesce(_optionalChain([event, 'access', _ => _.target, 'access', _2 => _2.files, 'optionalAccess', _3 => _3[0]]), () => ( null)));
20
+ };
21
+ const onReset = () => {
22
+ setFile(null);
23
+ };
24
+ const inputClasses = ["o3-form-file-input"];
25
+ if (feedback && feedback.type === "error") {
26
+ inputClasses.push("o3-form-text-file--error");
27
+ }
28
+ return /* @__PURE__ */ _jsxruntime.jsx.call(void 0,
29
+ _FormField.LabeledFormField,
30
+ {
31
+ label,
32
+ feedback,
33
+ description,
34
+ inputId: id,
35
+ optional,
36
+ children: /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, _jsxruntime.Fragment, { children: [
37
+ /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, "div", { className: inputClasses.join(" "), children: [
38
+ /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, "label", { htmlFor: id, className: "o3-form-file-input__label", children: [
39
+ /* @__PURE__ */ _jsxruntime.jsx.call(void 0,
40
+ "span",
41
+ {
42
+ className: "o3-form-file-input__label-button o3-button o3-button--primary o3-button-icon o3-button-icon--upload",
43
+ children: "File Upload"
44
+ }
45
+ ),
46
+ /* @__PURE__ */ _jsxruntime.jsx.call(void 0,
47
+ "span",
48
+ {
49
+ "data-testid": "file-input-label",
50
+ className: "o3-form-file-input__label-text",
51
+ children: _optionalChain([file, 'optionalAccess', _4 => _4.name]) ? _optionalChain([file, 'optionalAccess', _5 => _5.name]) : "No file chosen"
52
+ }
53
+ )
54
+ ] }),
55
+ /* @__PURE__ */ _jsxruntime.jsx.call(void 0,
56
+ "input",
57
+ {
58
+ ...attributes,
59
+ id,
60
+ className: "o3-form-file-input__input-field",
61
+ disabled,
62
+ required: !optional,
63
+ onChange: onUpload,
64
+ "aria-required": !optional,
65
+ type: "file"
66
+ }
67
+ ),
68
+ file && /* @__PURE__ */ _jsxruntime.jsx.call(void 0,
69
+ "button",
70
+ {
71
+ type: "button",
72
+ className: "o3-form-field-input__destroy",
73
+ "aria-label": "Delete file",
74
+ onClick: onReset
75
+ }
76
+ )
77
+ ] }),
78
+ isUploading && /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "span", { className: "o3-form-file-input__uploading", children: "Uploading" })
79
+ ] })
80
+ }
81
+ );
82
+ };
83
+
84
+
85
+ exports.FileInput = FileInput;
@@ -0,0 +1,11 @@
1
+ declare class FileUploadController {
2
+ constructor(fileInput: HTMLInputElement);
3
+ private _updateStatus;
4
+ private _reset;
5
+ private _displayUpload;
6
+ private _removeUpload;
7
+ private static createUploadingElement;
8
+ private static createDestroyElement;
9
+ }
10
+
11
+ export { FileUploadController };
@@ -0,0 +1,73 @@
1
+ "use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; }class FileUploadController {
2
+ constructor(fileInput) {
3
+ this._updateStatus = (event) => {
4
+ const target = event.target;
5
+ if (!target) return;
6
+ const formField = _optionalChain([target, 'optionalAccess', _ => _.closest, 'call', _2 => _2(".o3-form-field")]);
7
+ if (!formField) return;
8
+ const inputFileContainer = formField.querySelector(".o3-form-file-input");
9
+ if (!inputFileContainer) return;
10
+ const labelText = inputFileContainer.querySelector(".o3-form-file-input__label-text");
11
+ if (!labelText) return;
12
+ if (target && target.files && target.files.length > 0 && !formField.querySelector(".o3-form-field-input__destroy")) {
13
+ inputFileContainer.appendChild(FileUploadController.createDestroyElement(target));
14
+ labelText.classList.add("o3-form-field-input__label__text--file-selected");
15
+ } else {
16
+ labelText.classList.remove("o3-form-field-input__label__text--file-selected");
17
+ _optionalChain([inputFileContainer, 'access', _3 => _3.querySelector, 'call', _4 => _4(".o3-form-field-input__destroy"), 'optionalAccess', _5 => _5.remove, 'call', _6 => _6()]);
18
+ }
19
+ labelText.textContent = _optionalChain([target, 'access', _7 => _7.files, 'optionalAccess', _8 => _8[0], 'optionalAccess', _9 => _9.name]) || "No file chosen";
20
+ };
21
+ this._reset = (event) => {
22
+ const target = event.target;
23
+ if (!target) return;
24
+ target.value = "";
25
+ _optionalChain([target, 'optionalAccess', _10 => _10.dispatchEvent, 'call', _11 => _11(new Event("change"))]);
26
+ };
27
+ this._displayUpload = (event) => {
28
+ const target = event.target;
29
+ if (!target) return;
30
+ const formField = target.closest(".o3-form-field");
31
+ if (target && formField && !formField.querySelector(".o3-form-file-input__uploading")) {
32
+ formField.appendChild(FileUploadController.createUploadingElement());
33
+ }
34
+ };
35
+ this._removeUpload = (event) => {
36
+ const target = event.target;
37
+ if (!target) return;
38
+ const formField = target.closest(".o3-form-field");
39
+ if (!formField) return;
40
+ _optionalChain([formField, 'access', _12 => _12.querySelector, 'call', _13 => _13(".o3-form-file-input__uploading"), 'optionalAccess', _14 => _14.remove, 'call', _15 => _15()]);
41
+ };
42
+ fileInput.addEventListener("change", this._updateStatus);
43
+ fileInput.addEventListener("o3Form.uploading.start", this._displayUpload);
44
+ fileInput.addEventListener("o3Form.uploading.complete", this._removeUpload);
45
+ fileInput.addEventListener("o3Form.reset", this._reset);
46
+ const labelElement = fileInput.closest(".o3-form-file-input__label");
47
+ if (!labelElement) return;
48
+ labelElement.addEventListener("keydown", (event) => {
49
+ if (event.key === " " || event.key === "Enter") {
50
+ event.preventDefault();
51
+ fileInput.click();
52
+ }
53
+ });
54
+ }
55
+ static createUploadingElement() {
56
+ const uploadingElement = document.createElement("span");
57
+ uploadingElement.classList.add("o3-form-file-input__uploading");
58
+ uploadingElement.innerText = "Uploading";
59
+ return uploadingElement;
60
+ }
61
+ static createDestroyElement(fileInput) {
62
+ const destroyElement = document.createElement("button");
63
+ destroyElement.classList.add("o3-form-field-input__destroy");
64
+ destroyElement.setAttribute("aria-label", "Delete file");
65
+ destroyElement.addEventListener("click", () => {
66
+ fileInput.dispatchEvent(new CustomEvent("o3Form.reset"));
67
+ });
68
+ return destroyElement;
69
+ }
70
+ }
71
+
72
+
73
+ exports.FileUploadController = FileUploadController;
@@ -1,5 +1,5 @@
1
1
  import * as react_jsx_runtime from 'react/jsx-runtime';
2
- import { P as PasswordInputProps } from './index-CtGGU7zN.js';
2
+ import { P as PasswordInputProps } from './index-DupfYbgc.js';
3
3
 
4
4
  declare const PasswordInput: ({ label, feedback, description, disabled, attributes, inputId, optional, forgotPasswordLink, }: PasswordInputProps) => react_jsx_runtime.JSX.Element;
5
5
 
@@ -1,5 +1,5 @@
1
1
  import * as react_jsx_runtime from 'react/jsx-runtime';
2
- import { R as RadioButtonProps, F as FormFieldsetProps } from './index-CtGGU7zN.js';
2
+ import { R as RadioButtonProps, F as FormFieldsetProps } from './index-DupfYbgc.js';
3
3
 
4
4
  declare const RadioButtonItem: (props: RadioButtonProps) => react_jsx_runtime.JSX.Element;
5
5
  declare const RadioButtonGroup: (props: FormFieldsetProps) => react_jsx_runtime.JSX.Element;
@@ -1,5 +1,5 @@
1
1
  import * as react_jsx_runtime from 'react/jsx-runtime';
2
- import { S as SelectInputProps } from './index-CtGGU7zN.js';
2
+ import { S as SelectInputProps } from './index-DupfYbgc.js';
3
3
 
4
4
  declare const SelectInput: ({ label, feedback, description, disabled, attributes, inputId, optional, children, length, }: SelectInputProps) => react_jsx_runtime.JSX.Element;
5
5
 
@@ -1,5 +1,5 @@
1
1
  import * as react_jsx_runtime from 'react/jsx-runtime';
2
- import { T as TextInputProps } from './index-CtGGU7zN.js';
2
+ import { T as TextInputProps } from './index-DupfYbgc.js';
3
3
 
4
4
  declare const TextInput: ({ label, feedback, description, disabled, length, attributes, inputId, optional, }: TextInputProps) => react_jsx_runtime.JSX.Element;
5
5
 
@@ -1,5 +1,5 @@
1
1
  import * as react_jsx_runtime from 'react/jsx-runtime';
2
- import { a as FeedbackProps } from '../index-CtGGU7zN.js';
2
+ import { b as FeedbackProps } from '../index-DupfYbgc.js';
3
3
 
4
4
  declare const Feedback: ({ message, type }: FeedbackProps) => react_jsx_runtime.JSX.Element;
5
5
 
@@ -1,5 +1,5 @@
1
1
  import * as react_jsx_runtime from 'react/jsx-runtime';
2
- import { b as FormFieldProps, F as FormFieldsetProps } from '../index-CtGGU7zN.js';
2
+ import { c as FormFieldProps, F as FormFieldsetProps } from '../index-DupfYbgc.js';
3
3
 
4
4
  declare const LabeledFormField: ({ inputId, label, description, feedback, children, optional, }: FormFieldProps) => react_jsx_runtime.JSX.Element;
5
5
  declare const TitledFormField: ({ label, description, feedback, children, optional, }: FormFieldProps) => react_jsx_runtime.JSX.Element;
@@ -11,6 +11,11 @@ interface TextInputProps extends BaseInputProps {
11
11
  length?: 2 | 3 | 4 | 5;
12
12
  feedback?: FeedbackProps;
13
13
  }
14
+ interface FileInputProps extends BaseInputProps {
15
+ isUploading?: boolean;
16
+ disabled?: boolean;
17
+ feedback?: FeedbackProps;
18
+ }
14
19
  interface DateInputProps extends BaseInputProps {
15
20
  disabled?: boolean;
16
21
  feedback?: FeedbackProps;
@@ -68,4 +73,4 @@ type ErrorSummaryProps = {
68
73
  }[];
69
74
  };
70
75
 
71
- export type { CheckBoxProps as C, DateInputProps as D, ErrorSummaryProps as E, FormFieldsetProps as F, PasswordInputProps as P, RadioButtonProps as R, SelectInputProps as S, TextInputProps as T, FeedbackProps as a, FormFieldProps as b };
76
+ export type { CheckBoxProps as C, DateInputProps as D, ErrorSummaryProps as E, FormFieldsetProps as F, PasswordInputProps as P, RadioButtonProps as R, SelectInputProps as S, TextInputProps as T, FileInputProps as a, FeedbackProps as b, FormFieldProps as c };
package/cjs/index.d.ts CHANGED
@@ -8,6 +8,7 @@ export { PasswordInput } from './PasswordInput.js';
8
8
  export { SelectInput } from './SelectInput.js';
9
9
  export { DateInput } from './DateInput.js';
10
10
  export { DateInputPicker } from './DateInputPicker.js';
11
+ export { FileInput } from './FileInput.js';
11
12
  export { TextInput } from './TextInput.js';
12
13
  import 'react/jsx-runtime';
13
- import './index-CtGGU7zN.js';
14
+ import './index-DupfYbgc.js';
package/cjs/index.js CHANGED
@@ -8,4 +8,5 @@ var _PasswordInput = require('./PasswordInput'); _createStarExport(_PasswordInpu
8
8
  var _SelectInput = require('./SelectInput'); _createStarExport(_SelectInput);
9
9
  var _DateInput = require('./DateInput'); _createStarExport(_DateInput);
10
10
  var _DateInputPicker = require('./DateInputPicker'); _createStarExport(_DateInputPicker);
11
+ var _FileInput = require('./FileInput'); _createStarExport(_FileInput);
11
12
  var _TextInput = require('./TextInput'); _createStarExport(_TextInput);
package/css/main.css CHANGED
@@ -454,6 +454,98 @@ input[type=date].o3-form-text-input::-webkit-calendar-picker-indicator {
454
454
  padding-right: var(--o3-spacing-s);
455
455
  }
456
456
 
457
+ /* src/css/components/file-input.css */
458
+ .o3-form-file-input {
459
+ display: inline-flex;
460
+ align-items: center;
461
+ }
462
+ .o3-form-file-input__input-field {
463
+ color: var(--o3-color-use-case-muted-text);
464
+ font-family: var(--o3-type-body-base-font-family);
465
+ font-size: var(--o3-type-body-base-font-size);
466
+ font-weight: var(--o3-type-body-base-font-weight);
467
+ line-height: var(--o3-type-body-base-line-height);
468
+ padding-bottom: var(--o3-spacing-5xs);
469
+ display: none;
470
+ }
471
+ .o3-form-file-input__input-field::file-selector-button {
472
+ display: none;
473
+ }
474
+ .o3-form-file-input__input-field::-webkit-file-upload-button {
475
+ display: block;
476
+ width: 0;
477
+ height: 0;
478
+ margin-left: -100%;
479
+ }
480
+ .o3-form-file-input__input-field::-ms-browse {
481
+ display: none;
482
+ }
483
+ .o3-form-file-input__input-field:is(:focus-within, :focus) {
484
+ outline: none;
485
+ box-shadow: none;
486
+ }
487
+ .o3-form-file-input__label {
488
+ cursor: pointer;
489
+ display: inline-flex;
490
+ gap: var(--o3-spacing-3xs);
491
+ align-items: center;
492
+ height: 44px;
493
+ --o3-grid-columns-to-span-count: 4;
494
+ width: var(--o3-grid-columns-to-span-width);
495
+ }
496
+ .o3-form-file-input__label:focus-within {
497
+ box-shadow: var(--_o3-focus-outline);
498
+ outline: 0;
499
+ }
500
+ @supports selector(:focus-visible) {
501
+ .o3-form-file-input__label:focus-within {
502
+ box-shadow: var(--_o3-focus-rings);
503
+ outline: 0;
504
+ }
505
+ }
506
+ .o3-form-file-input__label-button {
507
+ flex-shrink: 0;
508
+ }
509
+ .o3-form-file-input__label-text {
510
+ color: var(--o3-color-use-case-muted-text);
511
+ }
512
+ .o3-form-file-input__label-text--file-selected {
513
+ color: var(--o3-color-use-case-body-text);
514
+ }
515
+ .o3-form-file-input__uploading {
516
+ display: flex;
517
+ align-items: center;
518
+ gap: var(--o3-spacing-5xs);
519
+ }
520
+ .o3-form-file-input__uploading::before {
521
+ content: "";
522
+ display: inline-block;
523
+ width: var(--o3-spacing-3xs);
524
+ height: var(--o3-spacing-3xs);
525
+ border-width: 2px;
526
+ border-radius: 50%;
527
+ animation: loading 1s infinite linear;
528
+ border-style: solid;
529
+ border-color: rgba(51, 48, 46, .25);
530
+ border-top-color: var(--o3-color-use-case-body-text);
531
+ }
532
+ .o3-form-field-input__destroy {
533
+ background: var(--o3-icon-trash) no-repeat center;
534
+ background-size: cover;
535
+ margin-left: var(--o3-spacing-4xs);
536
+ width: 21px;
537
+ height: 21px;
538
+ border: none;
539
+ }
540
+ @keyframes loading {
541
+ 0% {
542
+ transform: rotate(0deg);
543
+ }
544
+ 100% {
545
+ transform: rotate(360deg);
546
+ }
547
+ }
548
+
457
549
  /* src/css/components/select-input.css */
458
550
  .o3-form-select-input {
459
551
  border: var(--_o3-form-input-border);
package/esm/CheckBox.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import * as react_jsx_runtime from 'react/jsx-runtime';
2
- import { C as CheckBoxProps, F as FormFieldsetProps } from './index-CtGGU7zN.js';
2
+ import { C as CheckBoxProps, F as FormFieldsetProps } from './index-DupfYbgc.js';
3
3
 
4
4
  declare const CheckBoxItem: (props: CheckBoxProps) => react_jsx_runtime.JSX.Element;
5
5
  declare const CheckBox: (props: CheckBoxProps) => react_jsx_runtime.JSX.Element;
@@ -1,5 +1,5 @@
1
1
  import * as react_jsx_runtime from 'react/jsx-runtime';
2
- import { D as DateInputProps } from './index-CtGGU7zN.js';
2
+ import { D as DateInputProps } from './index-DupfYbgc.js';
3
3
 
4
4
  declare const DateInput: ({ label, feedback, description, disabled, attributes, inputId, optional, }: DateInputProps) => react_jsx_runtime.JSX.Element;
5
5
 
@@ -1,5 +1,5 @@
1
1
  import * as react_jsx_runtime from 'react/jsx-runtime';
2
- import { D as DateInputProps } from './index-CtGGU7zN.js';
2
+ import { D as DateInputProps } from './index-DupfYbgc.js';
3
3
 
4
4
  declare const DateInputPicker: ({ label, feedback, description, disabled, attributes, inputId, optional, }: DateInputProps) => react_jsx_runtime.JSX.Element;
5
5
 
@@ -1,5 +1,5 @@
1
1
  import * as react_jsx_runtime from 'react/jsx-runtime';
2
- import { E as ErrorSummaryProps } from './index-CtGGU7zN.js';
2
+ import { E as ErrorSummaryProps } from './index-DupfYbgc.js';
3
3
 
4
4
  declare const ErrorSummary: ({ errors, errorMessage, }: ErrorSummaryProps) => react_jsx_runtime.JSX.Element;
5
5
 
@@ -0,0 +1,6 @@
1
+ import * as react_jsx_runtime from 'react/jsx-runtime';
2
+ import { a as FileInputProps } from './index-DupfYbgc.js';
3
+
4
+ declare const FileInput: ({ label, feedback, description, disabled, attributes, inputId, optional, isUploading }: FileInputProps) => react_jsx_runtime.JSX.Element;
5
+
6
+ export { FileInput };
@@ -0,0 +1,85 @@
1
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
2
+ import { uidBuilder } from "@financial-times/o-utils";
3
+ import { LabeledFormField } from "./fieldComponents/FormField";
4
+ import { useState } from "react";
5
+ const uniqueId = uidBuilder("o3-form-file-input");
6
+ const FileInput = ({
7
+ label,
8
+ feedback,
9
+ description,
10
+ disabled,
11
+ attributes,
12
+ inputId,
13
+ optional,
14
+ isUploading
15
+ }) => {
16
+ const id = inputId || uniqueId("_");
17
+ const [file, setFile] = useState(null);
18
+ const onUpload = (event) => {
19
+ setFile(event.target.files?.[0] ?? null);
20
+ };
21
+ const onReset = () => {
22
+ setFile(null);
23
+ };
24
+ const inputClasses = ["o3-form-file-input"];
25
+ if (feedback && feedback.type === "error") {
26
+ inputClasses.push("o3-form-text-file--error");
27
+ }
28
+ return /* @__PURE__ */ jsx(
29
+ LabeledFormField,
30
+ {
31
+ label,
32
+ feedback,
33
+ description,
34
+ inputId: id,
35
+ optional,
36
+ children: /* @__PURE__ */ jsxs(Fragment, { children: [
37
+ /* @__PURE__ */ jsxs("div", { className: inputClasses.join(" "), children: [
38
+ /* @__PURE__ */ jsxs("label", { htmlFor: id, className: "o3-form-file-input__label", children: [
39
+ /* @__PURE__ */ jsx(
40
+ "span",
41
+ {
42
+ className: "o3-form-file-input__label-button o3-button o3-button--primary o3-button-icon o3-button-icon--upload",
43
+ children: "File Upload"
44
+ }
45
+ ),
46
+ /* @__PURE__ */ jsx(
47
+ "span",
48
+ {
49
+ "data-testid": "file-input-label",
50
+ className: "o3-form-file-input__label-text",
51
+ children: file?.name ? file?.name : "No file chosen"
52
+ }
53
+ )
54
+ ] }),
55
+ /* @__PURE__ */ jsx(
56
+ "input",
57
+ {
58
+ ...attributes,
59
+ id,
60
+ className: "o3-form-file-input__input-field",
61
+ disabled,
62
+ required: !optional,
63
+ onChange: onUpload,
64
+ "aria-required": !optional,
65
+ type: "file"
66
+ }
67
+ ),
68
+ file && /* @__PURE__ */ jsx(
69
+ "button",
70
+ {
71
+ type: "button",
72
+ className: "o3-form-field-input__destroy",
73
+ "aria-label": "Delete file",
74
+ onClick: onReset
75
+ }
76
+ )
77
+ ] }),
78
+ isUploading && /* @__PURE__ */ jsx("span", { className: "o3-form-file-input__uploading", children: "Uploading" })
79
+ ] })
80
+ }
81
+ );
82
+ };
83
+ export {
84
+ FileInput
85
+ };
@@ -0,0 +1,11 @@
1
+ declare class FileUploadController {
2
+ constructor(fileInput: HTMLInputElement);
3
+ private _updateStatus;
4
+ private _reset;
5
+ private _displayUpload;
6
+ private _removeUpload;
7
+ private static createUploadingElement;
8
+ private static createDestroyElement;
9
+ }
10
+
11
+ export { FileUploadController };
@@ -0,0 +1,73 @@
1
+ class FileUploadController {
2
+ constructor(fileInput) {
3
+ this._updateStatus = (event) => {
4
+ const target = event.target;
5
+ if (!target) return;
6
+ const formField = target?.closest(".o3-form-field");
7
+ if (!formField) return;
8
+ const inputFileContainer = formField.querySelector(".o3-form-file-input");
9
+ if (!inputFileContainer) return;
10
+ const labelText = inputFileContainer.querySelector(".o3-form-file-input__label-text");
11
+ if (!labelText) return;
12
+ if (target && target.files && target.files.length > 0 && !formField.querySelector(".o3-form-field-input__destroy")) {
13
+ inputFileContainer.appendChild(FileUploadController.createDestroyElement(target));
14
+ labelText.classList.add("o3-form-field-input__label__text--file-selected");
15
+ } else {
16
+ labelText.classList.remove("o3-form-field-input__label__text--file-selected");
17
+ inputFileContainer.querySelector(".o3-form-field-input__destroy")?.remove();
18
+ }
19
+ labelText.textContent = target.files?.[0]?.name || "No file chosen";
20
+ };
21
+ this._reset = (event) => {
22
+ const target = event.target;
23
+ if (!target) return;
24
+ target.value = "";
25
+ target?.dispatchEvent(new Event("change"));
26
+ };
27
+ this._displayUpload = (event) => {
28
+ const target = event.target;
29
+ if (!target) return;
30
+ const formField = target.closest(".o3-form-field");
31
+ if (target && formField && !formField.querySelector(".o3-form-file-input__uploading")) {
32
+ formField.appendChild(FileUploadController.createUploadingElement());
33
+ }
34
+ };
35
+ this._removeUpload = (event) => {
36
+ const target = event.target;
37
+ if (!target) return;
38
+ const formField = target.closest(".o3-form-field");
39
+ if (!formField) return;
40
+ formField.querySelector(".o3-form-file-input__uploading")?.remove();
41
+ };
42
+ fileInput.addEventListener("change", this._updateStatus);
43
+ fileInput.addEventListener("o3Form.uploading.start", this._displayUpload);
44
+ fileInput.addEventListener("o3Form.uploading.complete", this._removeUpload);
45
+ fileInput.addEventListener("o3Form.reset", this._reset);
46
+ const labelElement = fileInput.closest(".o3-form-file-input__label");
47
+ if (!labelElement) return;
48
+ labelElement.addEventListener("keydown", (event) => {
49
+ if (event.key === " " || event.key === "Enter") {
50
+ event.preventDefault();
51
+ fileInput.click();
52
+ }
53
+ });
54
+ }
55
+ static createUploadingElement() {
56
+ const uploadingElement = document.createElement("span");
57
+ uploadingElement.classList.add("o3-form-file-input__uploading");
58
+ uploadingElement.innerText = "Uploading";
59
+ return uploadingElement;
60
+ }
61
+ static createDestroyElement(fileInput) {
62
+ const destroyElement = document.createElement("button");
63
+ destroyElement.classList.add("o3-form-field-input__destroy");
64
+ destroyElement.setAttribute("aria-label", "Delete file");
65
+ destroyElement.addEventListener("click", () => {
66
+ fileInput.dispatchEvent(new CustomEvent("o3Form.reset"));
67
+ });
68
+ return destroyElement;
69
+ }
70
+ }
71
+ export {
72
+ FileUploadController
73
+ };
@@ -1,5 +1,5 @@
1
1
  import * as react_jsx_runtime from 'react/jsx-runtime';
2
- import { P as PasswordInputProps } from './index-CtGGU7zN.js';
2
+ import { P as PasswordInputProps } from './index-DupfYbgc.js';
3
3
 
4
4
  declare const PasswordInput: ({ label, feedback, description, disabled, attributes, inputId, optional, forgotPasswordLink, }: PasswordInputProps) => react_jsx_runtime.JSX.Element;
5
5
 
@@ -1,5 +1,5 @@
1
1
  import * as react_jsx_runtime from 'react/jsx-runtime';
2
- import { R as RadioButtonProps, F as FormFieldsetProps } from './index-CtGGU7zN.js';
2
+ import { R as RadioButtonProps, F as FormFieldsetProps } from './index-DupfYbgc.js';
3
3
 
4
4
  declare const RadioButtonItem: (props: RadioButtonProps) => react_jsx_runtime.JSX.Element;
5
5
  declare const RadioButtonGroup: (props: FormFieldsetProps) => react_jsx_runtime.JSX.Element;
@@ -1,5 +1,5 @@
1
1
  import * as react_jsx_runtime from 'react/jsx-runtime';
2
- import { S as SelectInputProps } from './index-CtGGU7zN.js';
2
+ import { S as SelectInputProps } from './index-DupfYbgc.js';
3
3
 
4
4
  declare const SelectInput: ({ label, feedback, description, disabled, attributes, inputId, optional, children, length, }: SelectInputProps) => react_jsx_runtime.JSX.Element;
5
5
 
@@ -1,5 +1,5 @@
1
1
  import * as react_jsx_runtime from 'react/jsx-runtime';
2
- import { T as TextInputProps } from './index-CtGGU7zN.js';
2
+ import { T as TextInputProps } from './index-DupfYbgc.js';
3
3
 
4
4
  declare const TextInput: ({ label, feedback, description, disabled, length, attributes, inputId, optional, }: TextInputProps) => react_jsx_runtime.JSX.Element;
5
5
 
@@ -1,5 +1,5 @@
1
1
  import * as react_jsx_runtime from 'react/jsx-runtime';
2
- import { a as FeedbackProps } from '../index-CtGGU7zN.mjs';
2
+ import { b as FeedbackProps } from '../index-DupfYbgc.mjs';
3
3
 
4
4
  declare const Feedback: ({ message, type }: FeedbackProps) => react_jsx_runtime.JSX.Element;
5
5
 
@@ -1,5 +1,5 @@
1
1
  import * as react_jsx_runtime from 'react/jsx-runtime';
2
- import { b as FormFieldProps, F as FormFieldsetProps } from '../index-CtGGU7zN.mjs';
2
+ import { c as FormFieldProps, F as FormFieldsetProps } from '../index-DupfYbgc.mjs';
3
3
 
4
4
  declare const LabeledFormField: ({ inputId, label, description, feedback, children, optional, }: FormFieldProps) => react_jsx_runtime.JSX.Element;
5
5
  declare const TitledFormField: ({ label, description, feedback, children, optional, }: FormFieldProps) => react_jsx_runtime.JSX.Element;
@@ -11,6 +11,11 @@ interface TextInputProps extends BaseInputProps {
11
11
  length?: 2 | 3 | 4 | 5;
12
12
  feedback?: FeedbackProps;
13
13
  }
14
+ interface FileInputProps extends BaseInputProps {
15
+ isUploading?: boolean;
16
+ disabled?: boolean;
17
+ feedback?: FeedbackProps;
18
+ }
14
19
  interface DateInputProps extends BaseInputProps {
15
20
  disabled?: boolean;
16
21
  feedback?: FeedbackProps;
@@ -68,4 +73,4 @@ type ErrorSummaryProps = {
68
73
  }[];
69
74
  };
70
75
 
71
- export type { CheckBoxProps as C, DateInputProps as D, ErrorSummaryProps as E, FormFieldsetProps as F, PasswordInputProps as P, RadioButtonProps as R, SelectInputProps as S, TextInputProps as T, FeedbackProps as a, FormFieldProps as b };
76
+ export type { CheckBoxProps as C, DateInputProps as D, ErrorSummaryProps as E, FormFieldsetProps as F, PasswordInputProps as P, RadioButtonProps as R, SelectInputProps as S, TextInputProps as T, FileInputProps as a, FeedbackProps as b, FormFieldProps as c };
package/esm/index.d.ts CHANGED
@@ -8,6 +8,7 @@ export { PasswordInput } from './PasswordInput.js';
8
8
  export { SelectInput } from './SelectInput.js';
9
9
  export { DateInput } from './DateInput.js';
10
10
  export { DateInputPicker } from './DateInputPicker.js';
11
+ export { FileInput } from './FileInput.js';
11
12
  export { TextInput } from './TextInput.js';
12
13
  import 'react/jsx-runtime';
13
- import './index-CtGGU7zN.js';
14
+ import './index-DupfYbgc.js';
package/esm/index.js CHANGED
@@ -8,4 +8,5 @@ export * from "./PasswordInput";
8
8
  export * from "./SelectInput";
9
9
  export * from "./DateInput";
10
10
  export * from "./DateInputPicker";
11
+ export * from "./FileInput";
11
12
  export * from "./TextInput";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@financial-times/o3-form",
3
- "version": "0.9.0",
3
+ "version": "0.11.0",
4
4
  "description": "Provides a viewport-aware tooltip for annotating or or highlighting other aspects of a product's UI",
5
5
  "keywords": [
6
6
  "form",