@mackin.com/styleguide 9.0.0-beta.1 → 9.2.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.
Files changed (3) hide show
  1. package/index.d.ts +60 -78
  2. package/index.js +855 -974
  3. package/package.json +1 -1
package/index.js CHANGED
@@ -6,10 +6,10 @@ var proRegularSvgIcons = require('@fortawesome/pro-regular-svg-icons');
6
6
  var proSolidSvgIcons = require('@fortawesome/pro-solid-svg-icons');
7
7
  var proLightSvgIcons = require('@fortawesome/pro-light-svg-icons');
8
8
  var reactFontawesome = require('@fortawesome/react-fontawesome');
9
- var lodash = require('lodash');
10
- var dateFns = require('date-fns');
11
9
  var nanoid = require('nanoid');
10
+ var dateFns = require('date-fns');
12
11
  var reactDom = require('react-dom');
12
+ var lodash = require('lodash');
13
13
  var reactTinyPopover = require('react-tiny-popover');
14
14
  var reactRouterDom = require('react-router-dom');
15
15
  var ReactSlider = require('react-slider');
@@ -602,51 +602,22 @@ const useAccordianState = (count, openIndex) => {
602
602
  ];
603
603
  };
604
604
 
605
- const DEFAULT_DEBOUNCE_MS = 250;
606
- const NUMBER_REGEX = /^-?\d+\.?\d*$/;
607
- const DATE_REGEX = /(\d{1,2})(?:\/|-)(\d{1,2})(?:\/|-)(\d{4})/;
608
- const DEFAULT_MAX_LENGTH = 100;
609
- const formatLocalValue = (value, type) => {
610
- let newValue = '';
611
- if (value !== undefined && value !== null) {
612
- if (type === 'date' && typeof value === 'number') {
613
- newValue = dateFns.format(value, 'MM/dd/yyyy');
614
- }
615
- else {
616
- newValue = value;
617
- }
618
- }
619
- return newValue;
620
- };
621
- /** @deprecated Use DateInput, NumberInput, or TextInput instead. */
622
- const Input = React__namespace.forwardRef((props, ref) => {
623
- var _a, _b, _c;
624
- const [localValue, setLocalValue] = React__namespace.useState(formatLocalValue(props.value, props.type));
625
- const debounceMs = (_a = props.debounceMs) !== null && _a !== void 0 ? _a : DEFAULT_DEBOUNCE_MS;
626
- const autoComplete = (_b = props.autoComplete) !== null && _b !== void 0 ? _b : 'off';
627
- const vars = React__namespace.useRef({
628
- wrappedOnChange: (props.onChange && debounceMs) ? lodash.debounce((value, name) => {
629
- var _a;
630
- (_a = props.onChange) === null || _a === void 0 ? void 0 : _a.call(props, value, name);
631
- }, debounceMs) : undefined,
632
- focused: false
633
- });
634
- const outerOnChange = (_c = vars.current.wrappedOnChange) !== null && _c !== void 0 ? _c : props.onChange;
635
- const trySyncLocalValue = () => {
636
- if (vars.current.focused) {
637
- return;
638
- }
639
- if (props.value === localValue) {
640
- return;
641
- }
642
- const newValue = formatLocalValue(props.value, props.type);
643
- setLocalValue(newValue);
644
- };
645
- React__namespace.useEffect(() => {
646
- trySyncLocalValue();
647
- }, [props.value]);
605
+ const InputErrorDisplay = (props) => {
606
+ const theme = useThemeSafely();
607
+ return (React__namespace.createElement(Text, { className: css.css({
608
+ minHeight: theme.controls.inputErrorMinHeight,
609
+ lineHeight: theme.controls.inputErrorMinHeight,
610
+ color: theme.colors.negative
611
+ }), smaller: true, noPad: true }, props.error));
612
+ };
613
+
614
+ const defaultMaxLength$1 = 100;
615
+ const BaseInput = React__namespace.forwardRef((props, ref) => {
616
+ var _a, _b;
648
617
  const theme = useThemeSafely();
618
+ const { rightControl, round, wrapperClassName, showErrorDisplay } = props, nativeProps = __rest(props, ["rightControl", "round", "wrapperClassName", "showErrorDisplay"]);
649
619
  const inputStyles = css.css({
620
+ backgroundColor: theme.colors.bg,
650
621
  fontFamily: theme.fonts.family,
651
622
  fontSize: theme.fonts.size,
652
623
  width: '100%',
@@ -671,7 +642,7 @@ const Input = React__namespace.forwardRef((props, ref) => {
671
642
  boxShadow: theme.controls.focusOutlineRequiredShadow
672
643
  }
673
644
  },
674
- }, props.round && props.type !== 'textarea' && {
645
+ }, props.round && {
675
646
  borderRadius: theme.controls.roundRadius,
676
647
  paddingLeft: `calc(${theme.controls.padding} * 2)`,
677
648
  paddingRight: `calc(${theme.controls.padding} * 2)`
@@ -682,100 +653,19 @@ const Input = React__namespace.forwardRef((props, ref) => {
682
653
  ':focus': {
683
654
  outline: 'none',
684
655
  boxShadow: 'none'
656
+ },
657
+ // FF fix to hide spinner on number elements
658
+ appearance: props.type === 'number' ? 'none' : undefined,
659
+ '::-webkit-outer-spin-button': {
660
+ appearance: 'none'
661
+ },
662
+ '::-webkit-inner-spin-button': {
663
+ appearance: 'none'
685
664
  }
686
- }, props.rightControl && props.type !== 'textarea' && {
665
+ }, props.rightControl && {
687
666
  paddingRight: theme.controls.height
688
667
  });
689
- let inputElement;
690
- const onFocus = (e) => {
691
- var _a;
692
- vars.current.focused = true;
693
- (_a = props.onFocus) === null || _a === void 0 ? void 0 : _a.call(props, e);
694
- };
695
- const onBlur = (e) => {
696
- var _a;
697
- vars.current.focused = false;
698
- trySyncLocalValue();
699
- (_a = props.onBlur) === null || _a === void 0 ? void 0 : _a.call(props, e);
700
- };
701
- let localOnChange;
702
- if (props.type === 'number') {
703
- localOnChange = e => {
704
- const value = e.target.value;
705
- if (NUMBER_REGEX.test(value)) {
706
- let numValue = parseFloat(value);
707
- if (props.min && numValue < props.min) {
708
- numValue = props.min;
709
- }
710
- if (props.max && numValue > props.max) {
711
- numValue = props.max;
712
- }
713
- setLocalValue(numValue);
714
- outerOnChange === null || outerOnChange === void 0 ? void 0 : outerOnChange(numValue, props.name, e);
715
- }
716
- else if (!value) {
717
- setLocalValue(value);
718
- outerOnChange === null || outerOnChange === void 0 ? void 0 : outerOnChange(undefined, props.name, e);
719
- }
720
- };
721
- }
722
- else if (props.type === 'date') {
723
- localOnChange = e => {
724
- const value = e.target.value;
725
- setLocalValue(value);
726
- if (outerOnChange) {
727
- const dateParts = DATE_REGEX.exec(value);
728
- if (!dateParts) {
729
- outerOnChange(undefined, props.name, e);
730
- }
731
- else {
732
- const year = parseInt(dateParts[3], 10);
733
- const month = parseInt(dateParts[1], 10);
734
- const day = parseInt(dateParts[2], 10);
735
- if (dateFns.isExists(year, month, day)) {
736
- let ms = new Date(year, month - 1, day).valueOf();
737
- if (props.min && ms < props.min) {
738
- ms = props.min;
739
- }
740
- if (props.max && ms > props.max) {
741
- ms = props.max;
742
- }
743
- outerOnChange(ms, props.name, e);
744
- }
745
- else {
746
- outerOnChange(undefined, props.name, e);
747
- }
748
- }
749
- }
750
- };
751
- }
752
- else {
753
- localOnChange = e => {
754
- const value = e.target.value;
755
- setLocalValue(value);
756
- outerOnChange === null || outerOnChange === void 0 ? void 0 : outerOnChange(value, props.name, e);
757
- };
758
- }
759
- if (props.type === 'number') {
760
- inputElement = React__namespace.createElement("input", Object.assign({}, props.inputAriaAttributes, { autoFocus: props.autoFocus, ref: ref, name: props.name, pattern: props.pattern, style: props.style, autoComplete: autoComplete, tabIndex: props.readOnly ? -1 : undefined, readOnly: props.readOnly,
761
- // set fixed default to defeat pasting stupid numbers
762
- maxLength: 50, min: props.min, max: props.max, required: props.required, disabled: props.disabled, id: props.id, className: css.cx(inputStyles, props.inputClassName), placeholder: props.placeholder, type: "number", value: localValue, onFocus: onFocus, onBlur: onBlur, onChange: localOnChange, onKeyDown: props.onKeyDown, onKeyUp: props.onKeyUp, onKeyPress: props.onKeyPress }));
763
- }
764
- else if (props.type === 'date') {
765
- inputElement = React__namespace.createElement("input", Object.assign({}, props.inputAriaAttributes, { autoFocus: props.autoFocus, ref: ref, name: props.name, pattern: props.pattern, style: props.style, autoComplete: autoComplete, tabIndex: props.readOnly ? -1 : undefined, readOnly: props.readOnly, maxLength: 10, required: props.required, disabled: props.disabled, id: props.id, className: css.cx(inputStyles, props.inputClassName), placeholder: props.placeholder, type: "text", value: localValue, onFocus: onFocus, onBlur: onBlur, onChange: localOnChange, onKeyDown: props.onKeyDown, onKeyUp: props.onKeyUp, onKeyPress: props.onKeyPress }));
766
- }
767
- else if (props.type === 'textarea') {
768
- inputElement = React__namespace.createElement("textarea", Object.assign({}, props.inputAriaAttributes, { autoFocus: props.autoFocus, ref: ref, name: props.name, style: props.style, rows: props.rows || 10, autoComplete: autoComplete, tabIndex: props.readOnly ? -1 : undefined, readOnly: props.readOnly, maxLength: props.maxLength || DEFAULT_MAX_LENGTH, required: props.required, disabled: props.disabled, id: props.id, className: css.cx(css.css `
769
- ${inputStyles}
770
- max-width: 100%;
771
- min-height: ${theme.controls.height};
772
- padding-top: 0.75rem;
773
- height:auto;`, props.inputClassName), placeholder: props.placeholder, value: localValue, onFocus: onFocus, onBlur: onBlur, onChange: localOnChange, onKeyDown: props.onKeyDown, onKeyUp: props.onKeyUp, onKeyPress: props.onKeyPress }));
774
- }
775
- else {
776
- // text, password, email, and url
777
- inputElement = React__namespace.createElement("input", Object.assign({}, props.inputAriaAttributes, { autoFocus: props.autoFocus, ref: ref, name: props.name, pattern: props.pattern, style: props.style, autoComplete: autoComplete, tabIndex: props.readOnly ? -1 : undefined, readOnly: props.readOnly, maxLength: props.maxLength || DEFAULT_MAX_LENGTH, required: props.required, disabled: props.disabled, id: props.id, className: css.cx(inputStyles, props.inputClassName), placeholder: props.placeholder, type: props.type, value: localValue, onFocus: onFocus, onBlur: onBlur, onChange: localOnChange, onKeyDown: props.onKeyDown, onKeyUp: props.onKeyUp, onKeyPress: props.onKeyPress }));
778
- }
668
+ const inputElement = React__namespace.createElement("input", Object.assign({}, nativeProps, { ref: ref, autoComplete: (_a = nativeProps.autoComplete) !== null && _a !== void 0 ? _a : 'off', tabIndex: nativeProps.readOnly ? -1 : nativeProps.tabIndex, maxLength: nativeProps.maxLength || defaultMaxLength$1, className: css.cx(inputStyles, props.className) }));
779
669
  const inputWrapperStyles = css.css `
780
670
  width:100%;
781
671
  ${props.rightControl && `
@@ -793,187 +683,362 @@ const Input = React__namespace.forwardRef((props, ref) => {
793
683
  right: calc(${theme.controls.padding} * 2);
794
684
  `}
795
685
  `;
796
- return (React__namespace.createElement("div", { className: css.cx('input', inputWrapperStyles, props.className) },
797
- inputElement,
798
- props.rightControl && props.type !== 'textarea' && React__namespace.createElement("div", { className: rightControlStyles }, props.rightControl)));
686
+ return (React__namespace.createElement("div", { className: css.css({
687
+ width: '100%',
688
+ label: 'BaseInput'
689
+ }) },
690
+ React__namespace.createElement("div", { className: css.cx('input', inputWrapperStyles, wrapperClassName) },
691
+ inputElement,
692
+ props.rightControl && (React__namespace.createElement("div", { className: rightControlStyles }, props.rightControl))),
693
+ ((_b = props.showErrorDisplay) !== null && _b !== void 0 ? _b : true) && React__namespace.createElement(InputErrorDisplay, { error: props.readOnly ? undefined : props.error })));
799
694
  });
800
695
 
801
- const List = React__namespace.forwardRef((props, ref) => {
802
- const children = props.items ? props.items.map((item, i) => React__namespace.createElement(ListItem, { key: i }, item)) : props.children;
803
- const theme = useThemeSafely();
804
- const listProps = __rest(props, ["altRowColor", "noLines", "items"]);
805
- const listStyles = css.css `
806
- margin: 0;
807
- padding: 0;
808
- list-style-type: none;
809
- ${props.altRowColor && `
810
- > .listItem:nth-of-type(even) {
811
- background-color: ${theme.colors.lightBg};
812
- }
813
- `}
814
- ${props.noLines && `
815
- > .listItem {
816
- border-bottom: none;
817
- }
818
- `}
819
- `;
820
- return (React__namespace.createElement("ul", Object.assign({}, listProps, { ref: ref, className: css.cx('list', listStyles, props.className) }), children));
821
- });
822
- const ListItem = (props) => {
823
- const liProps = __rest(props, ["variant", "noContentStyling"]);
824
- const theme = useThemeSafely();
825
- const itemStyles = css.css `
826
- border-bottom: ${theme.controls.border};
827
- &:last-child {
828
- border-bottom: none;
696
+ /** useEffect but ignores the first call on component mount. */
697
+ const useIgnoreMount = (effect, deps) => {
698
+ const mounted = React__default['default'].useRef(false);
699
+ React__default['default'].useEffect(() => {
700
+ if (!mounted.current) {
701
+ mounted.current = true;
829
702
  }
830
- `;
831
- const contentStyle = css.css({
832
- padding: `calc(${theme.controls.padding} * 1.5)`
833
- }, props.variant === 'full' && {
834
- padding: 0
835
- }, !props.noContentStyling && {
836
- '>.button': {
837
- padding: `calc(${theme.controls.padding} * 1.5)`,
838
- width: '100%',
839
- border: 'none'
840
- },
841
- '>.omniLink': {
842
- paddingLeft: `calc(${theme.controls.padding} * 1.5)`,
843
- paddingRight: `calc(${theme.controls.padding} * 1.5)`,
844
- width: '100%',
845
- border: 'none',
846
- lineHeight: theme.controls.height
847
- },
848
- '>.button:not(:focus), >.omniLink:not(:focus)': {
849
- boxShadow: 'none',
850
- },
851
- '>.omniLink:not(.omniLink--iconBlock)': {
852
- display: 'block'
703
+ else {
704
+ effect();
853
705
  }
854
- });
855
- return (React__namespace.createElement("li", Object.assign({}, liProps, { className: css.cx('listItem', itemStyles, props.className) }),
856
- React__namespace.createElement("div", { className: css.css(contentStyle) }, props.children)));
857
- };
858
-
859
- const TabLocker = (props) => {
860
- const tabLocker = React__namespace.useRef(null);
861
- return (React__namespace.createElement("div", { className: "tabLocker", style: props.style, ref: tabLocker, onKeyDown: e => {
862
- var _a, _b;
863
- if (props.disabled) {
864
- return;
865
- }
866
- if (e.key === 'Tab') {
867
- e.preventDefault();
868
- e.stopPropagation();
869
- const tabElements = Array.from((_b = (_a = tabLocker.current) === null || _a === void 0 ? void 0 : _a.querySelectorAll('a, button, input, textarea, select, details,[tabindex]:not([tabindex="-1"])')) !== null && _b !== void 0 ? _b : []).filter(el => !el.hasAttribute('disabled'));
870
- if (tabElements.length) {
871
- const direction = e.shiftKey ? -1 : 1;
872
- const index = tabElements.findIndex(x => x === document.activeElement);
873
- if (index === undefined) {
874
- tabElements[0].focus();
875
- }
876
- else if (index === tabElements.length - 1 && direction === 1) {
877
- tabElements[0].focus();
878
- }
879
- else if (index === 0 && direction === -1) {
880
- tabElements[tabElements.length - 1].focus();
881
- }
882
- else {
883
- tabElements[index + direction].focus();
884
- }
885
- }
886
- }
887
- } }, props.children));
706
+ }, deps);
888
707
  };
889
708
 
890
- //TB: FUTURE will need to use the new input
891
- const defaultMaxShownValues = 7;
892
- const buttonMarkerClass = 'ListItem__button';
893
- const Autocomplete = (p) => {
709
+ const tryClampRange = (value, min, max) => {
710
+ if (value === undefined) {
711
+ return value;
712
+ }
713
+ if (isNaN(value)) {
714
+ return undefined;
715
+ }
716
+ if (min !== undefined && value < min) {
717
+ return min;
718
+ }
719
+ if (max !== undefined && value > max) {
720
+ return max;
721
+ }
722
+ return value;
723
+ };
724
+ const isOutOfRange = (value, min, max) => {
725
+ if (min !== undefined && value < min) {
726
+ return true;
727
+ }
728
+ if (max !== undefined && value > max) {
729
+ return true;
730
+ }
731
+ return false;
732
+ };
733
+ const getStepDecimalPlaces = (step) => {
894
734
  var _a, _b;
895
- const theme = useThemeSafely();
896
- const element = React__namespace.useRef(null);
897
- const input = React__namespace.useRef(null);
898
- const list = React__namespace.useRef(null);
899
- const [values, setValues] = React__namespace.useState([]);
900
- const showValues = React__namespace.useMemo(() => values.length > 0, [values]);
901
- const maxShowValues = (_a = p.maxShownValues) !== null && _a !== void 0 ? _a : defaultMaxShownValues;
902
- const shownValues = React__namespace.useMemo(() => {
903
- if (!p.allowScroll) {
904
- return values.slice(0, maxShowValues);
905
- }
906
- return values.slice();
907
- }, [values]);
908
- const onChangeForOptions = React__namespace.useRef(lodash.debounce((value) => {
909
- if (!p.minChars || value.length >= p.minChars) {
910
- p.getOptions(value)
911
- .then(vals => {
912
- setValues(vals);
913
- }).catch(err => {
914
- // ignore it
915
- });
916
- }
917
- else {
918
- setValues([]);
919
- }
920
- }, (_b = p.getOptionsDebounceMs) !== null && _b !== void 0 ? _b : 0, { leading: false, trailing: true }));
921
- const getNextTabElement = (fromIndex, direction) => {
922
- var _a, _b, _c;
923
- if (fromIndex === -1) {
924
- let buttonIndex = 0;
925
- if (direction === -1) {
926
- buttonIndex = shownValues.length - 1;
927
- }
928
- return (_a = list.current) === null || _a === void 0 ? void 0 : _a.querySelector(`.${buttonMarkerClass}${buttonIndex}`);
929
- }
930
- else {
931
- const nextIndex = fromIndex + direction;
932
- if (nextIndex >= shownValues.length || nextIndex < 0) {
933
- return (_b = input.current) !== null && _b !== void 0 ? _b : undefined;
934
- }
935
- else {
936
- return (_c = list.current) === null || _c === void 0 ? void 0 : _c.querySelector(`.${buttonMarkerClass}${nextIndex}`);
937
- }
938
- }
939
- };
940
- React__namespace.useEffect(() => {
941
- const clearItems = () => {
942
- if (values.length > 0) {
943
- setValues([]);
944
- }
945
- };
946
- document.addEventListener('click', clearItems);
947
- return () => {
948
- document.removeEventListener('click', clearItems);
949
- };
950
- }, [values]);
951
- let listBorderRadius = '';
952
- if (p.round || theme.controls.borderRadius) {
953
- listBorderRadius = theme.controls.borderRadius || '0.5rem';
735
+ if (!step) {
736
+ return 0;
954
737
  }
955
- return (React__namespace.createElement("div", { onClick: e => {
956
- e.stopPropagation();
957
- }, onKeyDown: e => {
958
- var _a;
959
- if (e.key === 'Escape') {
960
- setValues([]);
961
- (_a = input.current) === null || _a === void 0 ? void 0 : _a.focus();
962
- }
963
- }, ref: element, className: css.cx(css.css({
964
- position: 'relative',
965
- width: '100%',
966
- label: 'Autocomplete'
967
- }), 'autocomplete') },
968
- React__namespace.createElement(TabLocker, { disabled: !showValues, style: { position: 'relative' } },
969
- React__namespace.createElement(Input, { autoFocus: p.autoFocus, inputAriaAttributes: p.inputAriaAttributes, ref: input, debounceMs: 0, type: "text", value: getAutocompleteValueText(p.value), round: p.round, rightControl: p.rightControl, placeholder: p.placeholder, id: p.id, disabled: p.disabled, className: p.className, inputClassName: p.inputClassName, maxLength: p.maxLength, required: p.required, onChange: v => {
970
- const value = v;
971
- p.onChange(value);
972
- onChangeForOptions.current(value);
973
- }, onKeyDown: e => {
974
- var _a, _b, _c;
975
- if (showValues) {
976
- if (e.key === 'ArrowDown') {
738
+ const strStep = typeof step === 'number' ? step.toString() : step;
739
+ return (_b = (_a = strStep.split('.')[1]) === null || _a === void 0 ? void 0 : _a.length) !== null && _b !== void 0 ? _b : 0;
740
+ };
741
+ const tryClampDecimals = (value, step = 0) => {
742
+ if (value === undefined) {
743
+ return value;
744
+ }
745
+ if (isNaN(value)) {
746
+ return undefined;
747
+ }
748
+ const decimals = getStepDecimalPlaces(step);
749
+ if (decimals === 0) {
750
+ return Math.floor(value);
751
+ }
752
+ return parseFloat(value.toFixed(decimals));
753
+ };
754
+
755
+ /** Common state handling for displaying validation messages with inputs. */
756
+ const useInputValidationMessage = (ref, props) => {
757
+ const [validationError, setValidationError] = React__default['default'].useState('');
758
+ const updateErrorMessage = (customErrorOverride) => {
759
+ var _a;
760
+ const customError = customErrorOverride || props.customError || '';
761
+ // set it OR clear it. either way, update it.
762
+ (_a = ref.current) === null || _a === void 0 ? void 0 : _a.setCustomValidity(customError);
763
+ setValidationError(customError || getValidationMessage(ref.current, props.patternErrorMessage));
764
+ };
765
+ React.useEffect(() => {
766
+ updateErrorMessage();
767
+ }, [props.customError]);
768
+ React__default['default'].useEffect(() => {
769
+ updateErrorMessage();
770
+ }, []);
771
+ return [validationError, updateErrorMessage];
772
+ };
773
+ const getValidationMessage = (element, patternErrorMessage) => {
774
+ var _a;
775
+ if (!element) {
776
+ return '';
777
+ }
778
+ const validity = element.validity;
779
+ if (validity.valid) {
780
+ return '';
781
+ }
782
+ if (validity.customError) {
783
+ return element.validationMessage;
784
+ }
785
+ if (validity.typeMismatch) {
786
+ switch (element.type) {
787
+ case 'url':
788
+ return `Invalid URL.`;
789
+ case 'email':
790
+ return `Invalid email.`;
791
+ default:
792
+ return element.validationMessage;
793
+ }
794
+ }
795
+ if (element instanceof HTMLInputElement) {
796
+ if (validity.rangeOverflow) {
797
+ return `Must be less than or equal to ${element.max}.`;
798
+ }
799
+ if (validity.rangeUnderflow) {
800
+ return `Must be greater than or equal to ${element.min}.`;
801
+ }
802
+ if (validity.stepMismatch) {
803
+ const decimalPlaces = getStepDecimalPlaces(element.step);
804
+ if (decimalPlaces > 0) {
805
+ const place = decimalPlaces === 1 ? 'place' : 'places';
806
+ return `Limited to ${getStepDecimalPlaces(element.step)} decimal ${place}.`;
807
+ }
808
+ else {
809
+ /*
810
+ step is buggy!
811
+
812
+ at least in Chrome, setting step=5 will cause the browser to mark the field as invalid if the number is not divisible
813
+ by 5. 55 is ok. 50 is ok. 59 is not ok.
814
+
815
+ to make things worse, if you enter an invalid number like 59 with step=5 and then clear the input, Chrome will tell
816
+ you the number 5 is now invalid and should be 4 or 9.
817
+ */
818
+ return `Must be an integer.`;
819
+ }
820
+ }
821
+ }
822
+ if (validity.tooShort) {
823
+ return `Must be at least ${((_a = element.minLength) !== null && _a !== void 0 ? _a : 0).toLocaleString()} characters in length.`;
824
+ }
825
+ if (validity.valueMissing) {
826
+ return 'Required.';
827
+ }
828
+ if (validity.patternMismatch && patternErrorMessage) {
829
+ return patternErrorMessage;
830
+ }
831
+ // unhandled. let the browser decide.
832
+ return element.validationMessage;
833
+ };
834
+
835
+ const TextInput = React__namespace.forwardRef((props, ref) => {
836
+ var _a;
837
+ const [localValue, setLocalValue] = React__namespace.useState(props.value);
838
+ const inputRef = (ref !== null && ref !== void 0 ? ref : React__namespace.useRef(null));
839
+ const [validationError, updateErrorMessage] = useInputValidationMessage(inputRef, props);
840
+ const nativeProps = __rest(props, ["emptyString", "onValueChange", "customError", "patternErrorMessage"]);
841
+ useIgnoreMount(() => {
842
+ var _a;
843
+ if ((_a = inputRef.current) === null || _a === void 0 ? void 0 : _a.checkValidity()) {
844
+ props.onValueChange(localValue);
845
+ }
846
+ else {
847
+ props.onValueChange(undefined);
848
+ }
849
+ updateErrorMessage();
850
+ }, [localValue]);
851
+ useIgnoreMount(() => {
852
+ if (document.activeElement !== inputRef.current) {
853
+ setLocalValue(props.value);
854
+ }
855
+ updateErrorMessage();
856
+ }, [props.value]);
857
+ return (React__namespace.createElement(BaseInput, Object.assign({}, nativeProps, { error: validationError, type: (_a = props.type) !== null && _a !== void 0 ? _a : 'text', ref: inputRef, value: localValue !== null && localValue !== void 0 ? localValue : '', onChange: e => {
858
+ var _a;
859
+ setLocalValue(props.emptyString ? e.target.value : e.target.value || undefined);
860
+ (_a = props.onChange) === null || _a === void 0 ? void 0 : _a.call(props, e);
861
+ }, onBlur: e => {
862
+ var _a, _b;
863
+ if (!e.target.checkValidity()) {
864
+ setLocalValue(undefined);
865
+ }
866
+ else if ((_a = props.trim) !== null && _a !== void 0 ? _a : true) {
867
+ setLocalValue(currentValue => {
868
+ return currentValue === null || currentValue === void 0 ? void 0 : currentValue.trim();
869
+ });
870
+ }
871
+ (_b = props.onBlur) === null || _b === void 0 ? void 0 : _b.call(props, e);
872
+ } })));
873
+ });
874
+
875
+ const List = React__namespace.forwardRef((props, ref) => {
876
+ const children = props.items ? props.items.map((item, i) => React__namespace.createElement(ListItem, { key: i }, item)) : props.children;
877
+ const theme = useThemeSafely();
878
+ const listProps = __rest(props, ["altRowColor", "noLines", "items"]);
879
+ const listStyles = css.css `
880
+ margin: 0;
881
+ padding: 0;
882
+ list-style-type: none;
883
+ ${props.altRowColor && `
884
+ > .listItem:nth-of-type(even) {
885
+ background-color: ${theme.colors.lightBg};
886
+ }
887
+ `}
888
+ ${props.noLines && `
889
+ > .listItem {
890
+ border-bottom: none;
891
+ }
892
+ `}
893
+ `;
894
+ return (React__namespace.createElement("ul", Object.assign({}, listProps, { ref: ref, className: css.cx('list', listStyles, props.className) }), children));
895
+ });
896
+ const ListItem = (props) => {
897
+ const liProps = __rest(props, ["variant", "noContentStyling"]);
898
+ const theme = useThemeSafely();
899
+ const itemStyles = css.css `
900
+ border-bottom: ${theme.controls.border};
901
+ &:last-child {
902
+ border-bottom: none;
903
+ }
904
+ `;
905
+ const contentStyle = css.css({
906
+ padding: `calc(${theme.controls.padding} * 1.5)`
907
+ }, props.variant === 'full' && {
908
+ padding: 0
909
+ }, !props.noContentStyling && {
910
+ '>.button': {
911
+ padding: `calc(${theme.controls.padding} * 1.5)`,
912
+ width: '100%',
913
+ border: 'none'
914
+ },
915
+ '>.omniLink': {
916
+ paddingLeft: `calc(${theme.controls.padding} * 1.5)`,
917
+ paddingRight: `calc(${theme.controls.padding} * 1.5)`,
918
+ width: '100%',
919
+ border: 'none',
920
+ lineHeight: theme.controls.height
921
+ },
922
+ '>.button:not(:focus), >.omniLink:not(:focus)': {
923
+ boxShadow: 'none',
924
+ },
925
+ '>.omniLink:not(.omniLink--iconBlock)': {
926
+ display: 'block'
927
+ }
928
+ });
929
+ return (React__namespace.createElement("li", Object.assign({}, liProps, { className: css.cx('listItem', itemStyles, props.className) }),
930
+ React__namespace.createElement("div", { className: css.css(contentStyle) }, props.children)));
931
+ };
932
+
933
+ const TabLocker = (props) => {
934
+ const tabLocker = React__namespace.useRef(null);
935
+ return (React__namespace.createElement("div", { className: "tabLocker", style: props.style, ref: tabLocker, onKeyDown: e => {
936
+ var _a, _b;
937
+ if (props.disabled) {
938
+ return;
939
+ }
940
+ if (e.key === 'Tab') {
941
+ e.preventDefault();
942
+ e.stopPropagation();
943
+ const tabElements = Array.from((_b = (_a = tabLocker.current) === null || _a === void 0 ? void 0 : _a.querySelectorAll('a, button, input, textarea, select, details,[tabindex]:not([tabindex="-1"])')) !== null && _b !== void 0 ? _b : []).filter(el => !el.hasAttribute('disabled'));
944
+ if (tabElements.length) {
945
+ const direction = e.shiftKey ? -1 : 1;
946
+ const index = tabElements.findIndex(x => x === document.activeElement);
947
+ if (index === undefined) {
948
+ tabElements[0].focus();
949
+ }
950
+ else if (index === tabElements.length - 1 && direction === 1) {
951
+ tabElements[0].focus();
952
+ }
953
+ else if (index === 0 && direction === -1) {
954
+ tabElements[tabElements.length - 1].focus();
955
+ }
956
+ else {
957
+ tabElements[index + direction].focus();
958
+ }
959
+ }
960
+ }
961
+ } }, props.children));
962
+ };
963
+
964
+ const defaultMaxShownValues = 7;
965
+ const buttonMarkerClass = 'ListItem__button';
966
+ const Autocomplete = (p) => {
967
+ var _a;
968
+ const inputProps = __rest(p, ["value", "className", "inputWrapperClassName", "inputClassName", "maxShownValues", "allowScroll", "options", "onPick"]);
969
+ const theme = useThemeSafely();
970
+ const element = React__namespace.useRef(null);
971
+ const input = React__namespace.useRef(null);
972
+ const list = React__namespace.useRef(null);
973
+ const maxShowValues = (_a = p.maxShownValues) !== null && _a !== void 0 ? _a : defaultMaxShownValues;
974
+ const displayOptions = React__namespace.useMemo(() => {
975
+ if (!p.allowScroll) {
976
+ return p.options.slice(0, maxShowValues);
977
+ }
978
+ return p.options.slice();
979
+ }, [p.options]);
980
+ const getNextTabElement = (fromIndex, direction) => {
981
+ var _a, _b, _c;
982
+ if (fromIndex === -1) {
983
+ let buttonIndex = 0;
984
+ if (direction === -1) {
985
+ buttonIndex = displayOptions.length - 1;
986
+ }
987
+ return (_a = list.current) === null || _a === void 0 ? void 0 : _a.querySelector(`.${buttonMarkerClass}${buttonIndex}`);
988
+ }
989
+ else {
990
+ const nextIndex = fromIndex + direction;
991
+ if (nextIndex >= displayOptions.length || nextIndex < 0) {
992
+ return (_b = input.current) !== null && _b !== void 0 ? _b : undefined;
993
+ }
994
+ else {
995
+ return (_c = list.current) === null || _c === void 0 ? void 0 : _c.querySelector(`.${buttonMarkerClass}${nextIndex}`);
996
+ }
997
+ }
998
+ };
999
+ React__namespace.useEffect(() => {
1000
+ const clearItems = () => {
1001
+ if (p.options.length) {
1002
+ p.onPick(undefined);
1003
+ }
1004
+ };
1005
+ document.addEventListener('click', clearItems);
1006
+ return () => {
1007
+ document.removeEventListener('click', clearItems);
1008
+ };
1009
+ }, [p.options]);
1010
+ let listBorderRadius = '';
1011
+ if (p.round || theme.controls.borderRadius) {
1012
+ listBorderRadius = theme.controls.borderRadius || '0.5rem';
1013
+ }
1014
+ const onPickValue = (v) => {
1015
+ p.onPick(v);
1016
+ // wait for the re-render. the value will not update if the control has focus
1017
+ setTimeout(() => {
1018
+ var _a;
1019
+ (_a = input.current) === null || _a === void 0 ? void 0 : _a.focus();
1020
+ }, 0);
1021
+ };
1022
+ return (React__namespace.createElement("div", { onClick: e => {
1023
+ e.stopPropagation();
1024
+ }, onKeyDown: e => {
1025
+ var _a;
1026
+ if (e.key === 'Escape') {
1027
+ // the TextInput will not respond to outer value changes if it has focus.
1028
+ // here we clear first and then onPickValue will re-focus after all updates.
1029
+ (_a = input.current) === null || _a === void 0 ? void 0 : _a.blur();
1030
+ onPickValue(undefined);
1031
+ }
1032
+ }, ref: element, className: css.cx(css.css({
1033
+ position: 'relative',
1034
+ width: '100%',
1035
+ label: 'Autocomplete'
1036
+ }), p.className, 'autocomplete') },
1037
+ React__namespace.createElement(TabLocker, { disabled: !displayOptions.length, style: { position: 'relative' } },
1038
+ React__namespace.createElement(TextInput, Object.assign({}, inputProps, { showErrorDisplay: false, ref: input, value: p.value, className: p.inputClassName, wrapperClassName: p.inputWrapperClassName, onKeyDown: e => {
1039
+ var _a, _b, _c;
1040
+ if (displayOptions.length) {
1041
+ if (e.key === 'ArrowDown') {
977
1042
  e.preventDefault();
978
1043
  e.stopPropagation();
979
1044
  (_a = getNextTabElement(-1, 1)) === null || _a === void 0 ? void 0 : _a.focus();
@@ -985,8 +1050,8 @@ const Autocomplete = (p) => {
985
1050
  }
986
1051
  }
987
1052
  (_c = p.onKeyDown) === null || _c === void 0 ? void 0 : _c.call(p, e);
988
- }, onKeyPress: e => { var _a; return (_a = p.onKeyPress) === null || _a === void 0 ? void 0 : _a.call(p, e); } }),
989
- showValues && (React__namespace.createElement(List, { ref: list, className: css.cx(css.css({
1053
+ } })),
1054
+ !!displayOptions.length && (React__namespace.createElement(List, { ref: list, className: css.cx(css.css({
990
1055
  position: 'absolute',
991
1056
  width: '100%',
992
1057
  border: theme.controls.border,
@@ -1003,12 +1068,12 @@ const Autocomplete = (p) => {
1003
1068
  borderBottomRightRadius: listBorderRadius,
1004
1069
  borderBottomLeftRadius: listBorderRadius,
1005
1070
  }
1006
- }), p.allowScroll && shownValues.length > maxShowValues && css.css({
1071
+ }), p.allowScroll && displayOptions.length > maxShowValues && css.css({
1007
1072
  overflowY: 'scroll',
1008
1073
  maxHeight: `calc(${theme.controls.height} * ${maxShowValues})`
1009
- })) },
1010
- shownValues.map((value, listItemIndex) => {
1011
- return (React__namespace.createElement(ListItem, { key: getAutocompleteValueId(value), variant: "full" },
1074
+ }), p.listClassName) },
1075
+ displayOptions.map((v, listItemIndex) => {
1076
+ return (React__namespace.createElement(ListItem, { key: v, variant: "full", className: p.listItemClassName },
1012
1077
  React__namespace.createElement(Button, { onKeyDown: e => {
1013
1078
  var _a, _b;
1014
1079
  if (e.key === 'ArrowDown') {
@@ -1021,42 +1086,23 @@ const Autocomplete = (p) => {
1021
1086
  e.preventDefault();
1022
1087
  (_b = getNextTabElement(listItemIndex, -1)) === null || _b === void 0 ? void 0 : _b.focus();
1023
1088
  }
1089
+ else if (e.key === 'Enter') {
1090
+ onPickValue(v);
1091
+ }
1024
1092
  }, className: css.cx(buttonMarkerClass + listItemIndex, css.css({
1025
1093
  borderRadius: 0,
1026
- })), onClick: () => {
1027
- p.onPick(value);
1028
- setValues([]);
1029
- setTimeout(() => {
1030
- var _a;
1031
- // we need to wait until the component is re-rendered.
1032
- // outside changes to Inputs will be ignored if the component has focus.
1033
- (_a = input.current) === null || _a === void 0 ? void 0 : _a.focus();
1034
- }, 0);
1094
+ }), p.listItemButtonClassName), onClick: () => {
1095
+ onPickValue(v);
1035
1096
  } },
1036
- React__namespace.createElement(Text, { tag: "div", ellipsis: true, align: "left" }, getAutocompleteValueText(value)))));
1097
+ React__namespace.createElement(Text, { tag: "div", ellipsis: true, align: "left" }, v))));
1037
1098
  }),
1038
- !p.allowScroll && shownValues.length < values.length && (React__namespace.createElement(ListItem, null,
1099
+ !p.allowScroll && displayOptions.length < p.options.length && (React__namespace.createElement(ListItem, { className: p.listItemClassName },
1039
1100
  React__namespace.createElement(Text, { tag: "div", italics: true, align: "center" },
1040
1101
  "Showing ",
1041
- shownValues.length.toLocaleString(),
1102
+ displayOptions.length.toLocaleString(),
1042
1103
  " of ",
1043
- values.length.toLocaleString(),
1104
+ p.options.length.toLocaleString(),
1044
1105
  " results."))))))));
1045
- };
1046
- const getAutocompleteValueText = (v) => {
1047
- if (!v) {
1048
- return '';
1049
- }
1050
- if (typeof v === 'string') {
1051
- return v;
1052
- }
1053
- return v.name;
1054
- };
1055
- const getAutocompleteValueId = (v) => {
1056
- if (typeof v === 'string') {
1057
- return v;
1058
- }
1059
- return v.id;
1060
1106
  };
1061
1107
 
1062
1108
  /** @deprecated Use Backdrop2 going forward. */
@@ -1160,266 +1206,31 @@ const Backdrop$1 = (props) => {
1160
1206
  }
1161
1207
  return () => {
1162
1208
  if (backdrop && backdrop.current && !props.allowScroll) {
1163
- document.body.classList.remove(bodyStyles);
1164
- }
1165
- };
1166
- }, [props.show]);
1167
- return (React__namespace.createElement("div", { onMouseDown: e => {
1168
- var _a;
1169
- e.stopPropagation();
1170
- e.preventDefault();
1171
- (_a = props.onClick) === null || _a === void 0 ? void 0 : _a.call(props);
1172
- }, onClick: e => {
1173
- e.stopPropagation();
1174
- e.preventDefault();
1175
- }, ref: backdrop, className: css.cx('backdrop', styles, props.className) }, props.children));
1176
- };
1177
-
1178
- /** useEffect but it will only fire when the actual truthiness of the value changes.
1179
- * Use for comparing previous states to next states without all the bullshit around useEffect and component mounting.
1180
- */
1181
- const useBooleanChanged = (effect, dep) => {
1182
- /*
1183
- Why?
1184
- useEffect with a dependency array will fire once on mount even though the dependency list doesn't change.
1185
- Components like Modal need to communicate when their show status changes.
1186
- useIgnoreMount is not enough because it only ignores the first render and is therefore a kludge.
1187
- This is what we want regardless of mount status:
1188
- true > false = Change
1189
- false > true = Change
1190
- true > true = No Change
1191
- false > false = No Change
1192
- undefined > false = No Change
1193
- undefined > true = Change
1194
- */
1195
- const lastValue = React.useRef(undefined);
1196
- React.useEffect(() => {
1197
- //console.log('[useBooleanChanged] useEffect called with', dep, 'was', lastValue.current ?? 'undefined')
1198
- if (!!lastValue.current !== !!dep) {
1199
- const previous = lastValue.current;
1200
- lastValue.current = dep;
1201
- effect(!!lastValue.current, !!previous);
1202
- //console.log('[useBooleanChanged] change called')
1203
- }
1204
- }, [dep]);
1205
- };
1206
-
1207
- const useLogger = (componentName, enabled) => {
1208
- return (...messages) => {
1209
- if (enabled) {
1210
- // tslint:disable-next-line
1211
- console.log(`[${componentName}]`, ...messages);
1212
- }
1213
- };
1214
- };
1215
-
1216
- // Taken from: https://github.com/react-bootstrap/dom-helpers/blob/master/src/scrollbarSize.ts
1217
- const canUseDom = !!(typeof window !== 'undefined' &&
1218
- window.document &&
1219
- window.document.createElement);
1220
- let size;
1221
- /** Tells you actual width of the scroll bar. This can vary by browser. */
1222
- const useScrollbarSize = (recalc) => {
1223
- if ((!size && size !== 0) || recalc) {
1224
- if (canUseDom) {
1225
- const scrollDiv = document.createElement('div');
1226
- scrollDiv.style.position = 'absolute';
1227
- scrollDiv.style.top = '-9999px';
1228
- scrollDiv.style.width = '50px';
1229
- scrollDiv.style.height = '50px';
1230
- scrollDiv.style.overflow = 'scroll';
1231
- document.body.appendChild(scrollDiv);
1232
- size = scrollDiv.offsetWidth - scrollDiv.clientWidth;
1233
- document.body.removeChild(scrollDiv);
1234
- }
1235
- }
1236
- return size;
1237
- };
1238
-
1239
- /** Add to fixed positioned elements so their contents do not jump around when scrolling is disabled. */
1240
- const modalScrollFixClassName = 'modal-scroll-fix';
1241
- const Modal = (p) => {
1242
- var _a, _b, _c, _d;
1243
- const backdrop = React.useContext(BackdropContext);
1244
- const mouseDownElement = React.useRef(undefined);
1245
- const theme = useThemeSafely();
1246
- const hasHeader = p.closeButton || p.heading;
1247
- const contentRef = React__default['default'].useRef(null);
1248
- const log = useLogger((_a = p.id) !== null && _a !== void 0 ? _a : 'Modal', (_b = p.__debug) !== null && _b !== void 0 ? _b : false);
1249
- const showing = React.useRef(p.show);
1250
- const bodyStyles = React.useRef('');
1251
- const fixedElementStyles = React.useRef('');
1252
- const addScrollStyles = () => {
1253
- var _a, _b, _c, _d;
1254
- if (!bodyStyles.current) {
1255
- bodyStyles.current = css.css({
1256
- label: 'ModalBodyOverrides_' + ((_b = (_a = p.id) === null || _a === void 0 ? void 0 : _a.replace(/\s+/, '')) !== null && _b !== void 0 ? _b : nanoid.nanoid()),
1257
- overflow: 'hidden',
1258
- paddingRight: `${useScrollbarSize()}px`
1259
- });
1260
- log('creating singleton bodyStyles', bodyStyles.current);
1261
- }
1262
- if (!fixedElementStyles.current) {
1263
- fixedElementStyles.current = css.css({
1264
- label: 'ModalElementOverrides_' + ((_d = (_c = p.id) === null || _c === void 0 ? void 0 : _c.replace(/\s+/, '')) !== null && _d !== void 0 ? _d : nanoid.nanoid()),
1265
- paddingRight: `${useScrollbarSize()}px`
1266
- });
1267
- }
1268
- document.body.classList.add(bodyStyles.current);
1269
- Array.from(document.querySelectorAll(`.${modalScrollFixClassName}`)).forEach(e => {
1270
- e.classList.add(fixedElementStyles.current);
1271
- });
1272
- };
1273
- const tryRemoveScrollStyles = () => {
1274
- if (bodyStyles.current) {
1275
- log('removing singleton', bodyStyles.current);
1276
- document.body.classList.remove(bodyStyles.current);
1277
- }
1278
- if (fixedElementStyles.current) {
1279
- Array.from(document.querySelectorAll(`.${modalScrollFixClassName}`)).forEach(e => {
1280
- e.classList.remove(fixedElementStyles.current);
1281
- });
1282
- }
1283
- };
1284
- React.useEffect(() => {
1285
- log('mounted');
1286
- return () => {
1287
- var _a;
1288
- if (showing.current) {
1289
- log(`un-mount in progress and this modal is showing. decrement the backdrop and try to remove singleton body styles.`);
1290
- backdrop.setShow(false, (_a = p.id) !== null && _a !== void 0 ? _a : 'Modal');
1291
- log('backdrop.setShow', false);
1292
- tryRemoveScrollStyles();
1293
- }
1294
- else {
1295
- log(`un-mount in progress but this modal is not showing. do nothing with the backdrop.`);
1296
- }
1297
- log('un-mounted');
1298
- };
1299
- }, []);
1300
- useBooleanChanged((show, previousShow) => {
1301
- var _a;
1302
- log('show changed', `${previousShow !== null && previousShow !== void 0 ? previousShow : 'undefined'} > ${show}`);
1303
- backdrop.setShow(show, (_a = p.id) !== null && _a !== void 0 ? _a : 'Modal');
1304
- showing.current = show;
1305
- log('backdrop.setShow', show);
1306
- if (show) {
1307
- log('this modal is showing. adding singleton bodyStyles', bodyStyles.current);
1308
- addScrollStyles();
1309
- }
1310
- else {
1311
- log('this modal is hiding. try removing singleton bodyStyles');
1312
- tryRemoveScrollStyles();
1313
- }
1314
- }, p.show);
1315
- React__default['default'].useLayoutEffect(() => {
1316
- var _a;
1317
- if (p.show === true) {
1318
- const focusSelector = (_a = p.focusSelector) !== null && _a !== void 0 ? _a : '.modalCloseButton';
1319
- // still need to wait for the next tick so the children are all rendered.
1320
- setTimeout(() => {
1321
- var _a;
1322
- const element = (_a = contentRef.current) === null || _a === void 0 ? void 0 : _a.querySelector(focusSelector);
1323
- element === null || element === void 0 ? void 0 : element.focus();
1324
- log('set focus', focusSelector);
1325
- });
1326
- }
1327
- }, [p.show]);
1328
- const modalBodyStyles = css.css({
1329
- maxHeight: p.scrollable ? undefined : '99vh',
1330
- overflow: 'hidden',
1331
- zIndex: theme.zIndexes.modal,
1332
- cursor: 'default',
1333
- margin: '1rem',
1334
- backgroundColor: p.noBackground ? undefined : theme.colors.modalBg,
1335
- border: p.noBackground ? undefined : theme.controls.border,
1336
- boxShadow: p.noBackground ? undefined : theme.controls.boxShadow,
1337
- maxWidth: (_c = p.maxWidth) !== null && _c !== void 0 ? _c : theme.breakpoints.tablet,
1338
- minWidth: (_d = p.minWidth) !== null && _d !== void 0 ? _d : (hasHeader ? '250px' : undefined),
1339
- opacity: p.show ? 1 : 0,
1340
- fontSize: theme.fonts.size,
1341
- fontFamily: theme.fonts.family,
1342
- fontWeight: 'normal',
1343
- '&:focus': {
1344
- outline: 'none'
1345
- }
1346
- });
1347
- const modalHeaderStyles = css.cx(css.css({
1348
- display: 'flex',
1349
- justifyContent: 'space-between',
1350
- alignItems: 'center',
1351
- backgroundColor: theme.colors.header,
1352
- padding: '1rem',
1353
- color: theme.colors.headerFont
1354
- }), p.headerClassName);
1355
- const modalContainerStyles = css.css([{
1356
- position: 'fixed',
1357
- height: '100%',
1358
- width: '100%',
1359
- backgroundColor: "transparent",
1360
- display: 'flex',
1361
- justifyContent: 'center',
1362
- alignItems: 'center',
1363
- cursor: p.onClick ? 'pointer' : 'default'
1364
- }, p.scrollable && {
1365
- overflowY: 'auto',
1366
- overflowX: 'hidden',
1367
- alignItems: 'flex-start'
1368
- }]);
1369
- if (p.show) {
1370
- const backdropContainer = document.getElementById(backdrop.portalId);
1371
- if (backdropContainer) {
1372
- return reactDom.createPortal((React__default['default'].createElement("div", { onClick: e => {
1373
- e.stopPropagation();
1374
- if (!mouseDownElement.current) {
1375
- if (p.onClick) {
1376
- log('backdropContainer onClick');
1377
- p.onClick();
1378
- }
1379
- }
1380
- mouseDownElement.current = undefined;
1381
- }, className: css.cx('modalContainer', modalContainerStyles) },
1382
- React__default['default'].createElement("div", { id: p.id, ref: contentRef, onClick: e => e.stopPropagation(), onMouseDown: e => {
1383
- mouseDownElement.current = e.target;
1384
- e.stopPropagation();
1385
- }, onMouseUp: e => {
1386
- mouseDownElement.current = undefined;
1387
- /*
1388
- MouseDown and MouseUp stopPropagation was added to fix bugs while clicking within the modal.
1389
- At least in the case of MouseUp, this breaks sliders and the handle grab logic appears to live on window.
1390
- Turning this off now. Should not cause any issues for MouseUp unlike MouseDown.
1391
- */
1392
- // e.stopPropagation()
1393
- }, className: css.cx('modalBody', modalBodyStyles, p.className) },
1394
- React__default['default'].createElement(TabLocker, null,
1395
- hasHeader && (React__default['default'].createElement("header", { className: css.cx('modalHeader', modalHeaderStyles) },
1396
- p.heading ? React__default['default'].createElement(Text, { className: css.css({
1397
- margin: 0,
1398
- flexGrow: 1
1399
- }), tag: "h1", bold: true }, p.heading) : React__default['default'].createElement("span", null),
1400
- p.closeButton && p.onClick ? React__default['default'].createElement(Button, { className: css.cx('modalCloseButton', css.css({
1401
- color: theme.colors.headerFont,
1402
- marginLeft: '1rem',
1403
- backgroundColor: 'transparent'
1404
- })), variant: "icon", onClick: p.onClick },
1405
- React__default['default'].createElement(Icon, { id: "close" })) : React__default['default'].createElement("span", null))),
1406
- p.children)))), backdropContainer);
1407
- }
1408
- }
1409
- return null;
1209
+ document.body.classList.remove(bodyStyles);
1210
+ }
1211
+ };
1212
+ }, [props.show]);
1213
+ return (React__namespace.createElement("div", { onMouseDown: e => {
1214
+ var _a;
1215
+ e.stopPropagation();
1216
+ e.preventDefault();
1217
+ (_a = props.onClick) === null || _a === void 0 ? void 0 : _a.call(props);
1218
+ }, onClick: e => {
1219
+ e.stopPropagation();
1220
+ e.preventDefault();
1221
+ }, ref: backdrop, className: css.cx('backdrop', styles, props.className) }, props.children));
1410
1222
  };
1411
1223
 
1412
- /** useEffect but ignores the first call on component mount. */
1413
- const useIgnoreMount = (effect, deps) => {
1414
- const mounted = React__default['default'].useRef(false);
1415
- React__default['default'].useEffect(() => {
1416
- if (!mounted.current) {
1417
- mounted.current = true;
1418
- }
1419
- else {
1420
- effect();
1224
+ /** Add to fixed positioned elements so their contents do not jump around when scrolling is disabled. */
1225
+ const modalScrollFixClassName = 'modal-scroll-fix';
1226
+
1227
+ const useLogger = (componentName, enabled) => {
1228
+ return (...messages) => {
1229
+ if (enabled) {
1230
+ // tslint:disable-next-line
1231
+ console.log(`[${componentName}]`, ...messages);
1421
1232
  }
1422
- }, deps);
1233
+ };
1423
1234
  };
1424
1235
 
1425
1236
  const portalId = 'backdrop';
@@ -1691,6 +1502,227 @@ const Checkbox = (props) => {
1691
1502
  props.children)));
1692
1503
  };
1693
1504
 
1505
+ /** useEffect but it will only fire when the actual truthiness of the value changes.
1506
+ * Use for comparing previous states to next states without all the bullshit around useEffect and component mounting.
1507
+ */
1508
+ const useBooleanChanged = (effect, dep) => {
1509
+ /*
1510
+ Why?
1511
+ useEffect with a dependency array will fire once on mount even though the dependency list doesn't change.
1512
+ Components like Modal need to communicate when their show status changes.
1513
+ useIgnoreMount is not enough because it only ignores the first render and is therefore a kludge.
1514
+ This is what we want regardless of mount status:
1515
+ true > false = Change
1516
+ false > true = Change
1517
+ true > true = No Change
1518
+ false > false = No Change
1519
+ undefined > false = No Change
1520
+ undefined > true = Change
1521
+ */
1522
+ const lastValue = React.useRef(undefined);
1523
+ React.useEffect(() => {
1524
+ if (!!lastValue.current !== !!dep) {
1525
+ const previous = lastValue.current;
1526
+ lastValue.current = dep;
1527
+ effect(!!lastValue.current, !!previous);
1528
+ }
1529
+ }, [dep]);
1530
+ };
1531
+
1532
+ // Taken from: https://github.com/react-bootstrap/dom-helpers/blob/master/src/scrollbarSize.ts
1533
+ const canUseDom = !!(typeof window !== 'undefined' &&
1534
+ window.document &&
1535
+ window.document.createElement);
1536
+ let size;
1537
+ /** Tells you actual width of the scroll bar. This can vary by browser. */
1538
+ const useScrollbarSize = (recalc) => {
1539
+ if ((!size && size !== 0) || recalc) {
1540
+ if (canUseDom) {
1541
+ const scrollDiv = document.createElement('div');
1542
+ scrollDiv.style.position = 'absolute';
1543
+ scrollDiv.style.top = '-9999px';
1544
+ scrollDiv.style.width = '50px';
1545
+ scrollDiv.style.height = '50px';
1546
+ scrollDiv.style.overflow = 'scroll';
1547
+ document.body.appendChild(scrollDiv);
1548
+ size = scrollDiv.offsetWidth - scrollDiv.clientWidth;
1549
+ document.body.removeChild(scrollDiv);
1550
+ }
1551
+ }
1552
+ return size;
1553
+ };
1554
+
1555
+ const Modal = (p) => {
1556
+ var _a, _b, _c, _d;
1557
+ const backdrop = React.useContext(BackdropContext);
1558
+ const mouseDownElement = React.useRef(undefined);
1559
+ const theme = useThemeSafely();
1560
+ const hasHeader = p.closeButton || p.heading;
1561
+ const contentRef = React__default['default'].useRef(null);
1562
+ const log = useLogger((_a = p.id) !== null && _a !== void 0 ? _a : 'Modal', (_b = p.__debug) !== null && _b !== void 0 ? _b : false);
1563
+ const showing = React.useRef(p.show);
1564
+ const bodyStyles = React.useRef('');
1565
+ const fixedElementStyles = React.useRef('');
1566
+ const addScrollStyles = () => {
1567
+ var _a, _b, _c, _d;
1568
+ if (!bodyStyles.current) {
1569
+ bodyStyles.current = css.css({
1570
+ label: 'ModalBodyOverrides_' + ((_b = (_a = p.id) === null || _a === void 0 ? void 0 : _a.replace(/\s+/, '')) !== null && _b !== void 0 ? _b : nanoid.nanoid()),
1571
+ overflow: 'hidden',
1572
+ paddingRight: `${useScrollbarSize()}px`
1573
+ });
1574
+ log('creating singleton bodyStyles', bodyStyles.current);
1575
+ }
1576
+ if (!fixedElementStyles.current) {
1577
+ fixedElementStyles.current = css.css({
1578
+ label: 'ModalElementOverrides_' + ((_d = (_c = p.id) === null || _c === void 0 ? void 0 : _c.replace(/\s+/, '')) !== null && _d !== void 0 ? _d : nanoid.nanoid()),
1579
+ paddingRight: `${useScrollbarSize()}px`
1580
+ });
1581
+ }
1582
+ document.body.classList.add(bodyStyles.current);
1583
+ Array.from(document.querySelectorAll(`.${modalScrollFixClassName}`)).forEach(e => {
1584
+ e.classList.add(fixedElementStyles.current);
1585
+ });
1586
+ };
1587
+ const tryRemoveScrollStyles = () => {
1588
+ if (bodyStyles.current) {
1589
+ log('removing singleton', bodyStyles.current);
1590
+ document.body.classList.remove(bodyStyles.current);
1591
+ }
1592
+ if (fixedElementStyles.current) {
1593
+ Array.from(document.querySelectorAll(`.${modalScrollFixClassName}`)).forEach(e => {
1594
+ e.classList.remove(fixedElementStyles.current);
1595
+ });
1596
+ }
1597
+ };
1598
+ React.useEffect(() => {
1599
+ log('mounted');
1600
+ return () => {
1601
+ var _a;
1602
+ if (showing.current) {
1603
+ log(`un-mount in progress and this modal is showing. decrement the backdrop and try to remove singleton body styles.`);
1604
+ backdrop.setShow(false, (_a = p.id) !== null && _a !== void 0 ? _a : 'Modal');
1605
+ log('backdrop.setShow', false);
1606
+ tryRemoveScrollStyles();
1607
+ }
1608
+ else {
1609
+ log(`un-mount in progress but this modal is not showing. do nothing with the backdrop.`);
1610
+ }
1611
+ log('un-mounted');
1612
+ };
1613
+ }, []);
1614
+ useBooleanChanged((show, previousShow) => {
1615
+ var _a;
1616
+ log('show changed', `${previousShow !== null && previousShow !== void 0 ? previousShow : 'undefined'} > ${show}`);
1617
+ backdrop.setShow(show, (_a = p.id) !== null && _a !== void 0 ? _a : 'Modal');
1618
+ showing.current = show;
1619
+ log('backdrop.setShow', show);
1620
+ if (show) {
1621
+ log('this modal is showing. adding singleton bodyStyles', bodyStyles.current);
1622
+ addScrollStyles();
1623
+ }
1624
+ else {
1625
+ log('this modal is hiding. try removing singleton bodyStyles');
1626
+ tryRemoveScrollStyles();
1627
+ }
1628
+ }, p.show);
1629
+ React__default['default'].useLayoutEffect(() => {
1630
+ var _a;
1631
+ if (p.show === true) {
1632
+ const focusSelector = (_a = p.focusSelector) !== null && _a !== void 0 ? _a : '.modalCloseButton';
1633
+ // still need to wait for the next tick so the children are all rendered.
1634
+ setTimeout(() => {
1635
+ var _a;
1636
+ const element = (_a = contentRef.current) === null || _a === void 0 ? void 0 : _a.querySelector(focusSelector);
1637
+ element === null || element === void 0 ? void 0 : element.focus();
1638
+ log('set focus', focusSelector);
1639
+ });
1640
+ }
1641
+ }, [p.show]);
1642
+ const modalBodyStyles = css.css({
1643
+ maxHeight: p.scrollable ? undefined : '99vh',
1644
+ overflow: 'hidden',
1645
+ zIndex: theme.zIndexes.modal,
1646
+ cursor: 'default',
1647
+ margin: '1rem',
1648
+ backgroundColor: p.noBackground ? undefined : theme.colors.modalBg,
1649
+ border: p.noBackground ? undefined : theme.controls.border,
1650
+ boxShadow: p.noBackground ? undefined : theme.controls.boxShadow,
1651
+ maxWidth: (_c = p.maxWidth) !== null && _c !== void 0 ? _c : theme.breakpoints.tablet,
1652
+ minWidth: (_d = p.minWidth) !== null && _d !== void 0 ? _d : (hasHeader ? '250px' : undefined),
1653
+ opacity: p.show ? 1 : 0,
1654
+ fontSize: theme.fonts.size,
1655
+ fontFamily: theme.fonts.family,
1656
+ fontWeight: 'normal',
1657
+ '&:focus': {
1658
+ outline: 'none'
1659
+ }
1660
+ });
1661
+ const modalHeaderStyles = css.cx(css.css({
1662
+ display: 'flex',
1663
+ justifyContent: 'space-between',
1664
+ alignItems: 'center',
1665
+ backgroundColor: theme.colors.header,
1666
+ padding: '1rem',
1667
+ color: theme.colors.headerFont
1668
+ }), p.headerClassName);
1669
+ const modalContainerStyles = css.css([{
1670
+ position: 'fixed',
1671
+ height: '100%',
1672
+ width: '100%',
1673
+ backgroundColor: "transparent",
1674
+ display: 'flex',
1675
+ justifyContent: 'center',
1676
+ alignItems: 'center',
1677
+ cursor: p.onClick ? 'pointer' : 'default'
1678
+ }, p.scrollable && {
1679
+ overflowY: 'auto',
1680
+ overflowX: 'hidden',
1681
+ alignItems: 'flex-start'
1682
+ }]);
1683
+ if (p.show) {
1684
+ const backdropContainer = document.getElementById(backdrop.portalId);
1685
+ if (backdropContainer) {
1686
+ return reactDom.createPortal((React__default['default'].createElement("div", { onClick: e => {
1687
+ e.stopPropagation();
1688
+ if (!mouseDownElement.current) {
1689
+ if (p.onClick) {
1690
+ log('backdropContainer onClick');
1691
+ p.onClick();
1692
+ }
1693
+ }
1694
+ mouseDownElement.current = undefined;
1695
+ }, className: css.cx('modalContainer', modalContainerStyles) },
1696
+ React__default['default'].createElement("div", { id: p.id, ref: contentRef, onClick: e => e.stopPropagation(), onMouseDown: e => {
1697
+ mouseDownElement.current = e.target;
1698
+ e.stopPropagation();
1699
+ }, onMouseUp: e => {
1700
+ mouseDownElement.current = undefined;
1701
+ /*
1702
+ MouseDown and MouseUp stopPropagation was added to fix bugs while clicking within the modal.
1703
+ At least in the case of MouseUp, this breaks sliders and the handle grab logic appears to live on window.
1704
+ Turning this off now. Should not cause any issues for MouseUp unlike MouseDown.
1705
+ */
1706
+ // e.stopPropagation()
1707
+ }, className: css.cx('modalBody', modalBodyStyles, p.className) },
1708
+ React__default['default'].createElement(TabLocker, null,
1709
+ hasHeader && (React__default['default'].createElement("header", { className: css.cx('modalHeader', modalHeaderStyles) },
1710
+ p.heading ? React__default['default'].createElement(Text, { className: css.css({
1711
+ margin: 0,
1712
+ flexGrow: 1
1713
+ }), tag: "h1", bold: true }, p.heading) : React__default['default'].createElement("span", null),
1714
+ p.closeButton && p.onClick ? React__default['default'].createElement(Button, { className: css.cx('modalCloseButton', css.css({
1715
+ color: theme.colors.headerFont,
1716
+ marginLeft: '1rem',
1717
+ backgroundColor: 'transparent'
1718
+ })), variant: "icon", onClick: p.onClick },
1719
+ React__default['default'].createElement(Icon, { id: "close" })) : React__default['default'].createElement("span", null))),
1720
+ p.children)))), backdropContainer);
1721
+ }
1722
+ }
1723
+ return null;
1724
+ };
1725
+
1694
1726
  const ConfirmModal = (props) => {
1695
1727
  const theme = useThemeSafely();
1696
1728
  const modalStyle = css.css `
@@ -1709,11 +1741,15 @@ const ConfirmModal = (props) => {
1709
1741
  React__namespace.createElement(Button, { className: css.css({ margin: '0 0.5rem' }), enforceMinWidth: true, onClick: props.onCancel }, props.cancelText || 'Cancel')))));
1710
1742
  };
1711
1743
 
1712
- const CopyButton = (props) => {
1744
+ const CopyButton = React__namespace.forwardRef((props, ref) => {
1745
+ const buttonProps = __rest(props, ["selector"]);
1713
1746
  const [copied, setCopied] = React__namespace.useState(false);
1714
- return (React__namespace.createElement(Button, { title: copied ? 'Copied!' : (props.title || 'Copy to clipboard'), variant: "icon", onBlur: () => {
1747
+ return (React__namespace.createElement(Button, Object.assign({}, buttonProps, { ref: ref, title: copied ? 'Copied!' : (props.title || 'Copy to clipboard'), variant: "icon", onBlur: e => {
1748
+ var _a;
1715
1749
  setCopied(false);
1750
+ (_a = props.onBlur) === null || _a === void 0 ? void 0 : _a.call(props, e);
1716
1751
  }, onClick: e => {
1752
+ var _a;
1717
1753
  const button = e.currentTarget;
1718
1754
  let copySuccess = false;
1719
1755
  try {
@@ -1727,9 +1763,10 @@ const CopyButton = (props) => {
1727
1763
  // You done wrong.
1728
1764
  }
1729
1765
  setCopied(copySuccess);
1730
- } },
1766
+ (_a = props.onClick) === null || _a === void 0 ? void 0 : _a.call(props, e);
1767
+ } }),
1731
1768
  React__namespace.createElement(Icon, { id: copied ? 'paste' : 'copy' })));
1732
- };
1769
+ });
1733
1770
 
1734
1771
  const Divider = (p) => {
1735
1772
  const theme = useThemeSafely();
@@ -2172,343 +2209,126 @@ const Header = (props) => {
2172
2209
  React__namespace.createElement("div", null, props.centerOffsetElements)))));
2173
2210
  };
2174
2211
 
2175
- const Highlight = (props) => {
2176
- const theme = useThemeSafely();
2177
- const highlightStyles = css.css `
2178
- > mark {
2179
- background-color: ${theme.colors.textHighlight};
2180
- }
2181
- `;
2182
- let text = props.text;
2183
- if (props.text && props.highlightText) {
2184
- const replaceText = props.highlightText.trim();
2185
- if (replaceText) {
2186
- text = props.text.replace(new RegExp(`(${replaceText})`, 'gi'), `<mark>$1</mark>`);
2187
- }
2188
- }
2189
- return (React__namespace.createElement("span", { className: css.cx('highlight', highlightStyles), dangerouslySetInnerHTML: { __html: text } }));
2190
- };
2191
-
2192
- const Image = React__namespace.forwardRef((p, ref) => {
2193
- return (React__namespace.createElement("img", Object.assign({}, p, { ref: ref, className: css.cx('image', css.css({
2194
- label: 'Image',
2195
- maxWidth: '100%',
2196
- maxHeight: '100%',
2197
- }), p.className) })));
2198
- });
2199
-
2200
- const Popover = (p) => {
2201
- var _a, _b;
2202
- const theme = useThemeSafely();
2203
- const resposition = (_a = p.reposition) !== null && _a !== void 0 ? _a : true;
2204
- return (React__namespace.createElement(reactTinyPopover.Popover, { containerClassName: css.css({
2205
- zIndex: theme.zIndexes.tooltip
2206
- }), reposition: resposition, isOpen: p.isOpen, positions: (_b = p.positions) !== null && _b !== void 0 ? _b : ['right', 'top', 'left', 'bottom'], onClickOutside: p.onClickOutside, content: ({ position, childRect, popoverRect }) => {
2207
- var _a, _b, _c, _d;
2208
- return (React__namespace.createElement(reactTinyPopover.ArrowContainer, { position: position, childRect: childRect, popoverRect: popoverRect, arrowColor: (_a = p.arrorColor) !== null && _a !== void 0 ? _a : theme.colors.border, arrowSize: 10 },
2209
- React__namespace.createElement(TabLocker, null,
2210
- React__namespace.createElement("div", { className: css.css({
2211
- border: (_b = p.border) !== null && _b !== void 0 ? _b : theme.controls.border,
2212
- borderRadius: (_c = p.border) !== null && _c !== void 0 ? _c : theme.controls.borderRadius,
2213
- boxShadow: theme.controls.boxShadow,
2214
- backgroundColor: (_d = p.backgroundColor) !== null && _d !== void 0 ? _d : theme.colors.bg,
2215
- }) }, p.content))));
2216
- } },
2217
- React__namespace.createElement("span", null, p.parent)));
2218
- };
2219
-
2220
- const InfoTip = (props) => {
2221
- var _a, _b, _c;
2222
- const [showTip, setShowTip] = React__namespace.useState(false);
2223
- const theme = useThemeSafely();
2224
- const bgColor = (_a = props.bgColor) !== null && _a !== void 0 ? _a : theme.colors.nav;
2225
- const fontColor = (_b = props.fontColor) !== null && _b !== void 0 ? _b : theme.colors.navFont;
2226
- const onClick = () => {
2227
- if (props.onClick) {
2228
- props.onClick();
2229
- }
2230
- else if (props.content) {
2231
- openTip();
2232
- }
2233
- };
2234
- const onMouseOver = () => {
2235
- if (props.variant === 'modal') {
2236
- return;
2237
- }
2238
- if (props.loadOnHover) {
2239
- props.loadOnHover().then(() => {
2240
- openTip();
2241
- }).catch(err => {
2242
- /* Not my responsiblity. */
2243
- });
2244
- }
2245
- else {
2246
- openTip();
2247
- }
2248
- };
2249
- const onMouseOut = () => {
2250
- if (props.variant === 'modal') {
2251
- return;
2252
- }
2253
- closeTip();
2254
- };
2255
- const openTip = () => {
2256
- if (!props.content) {
2257
- return;
2258
- }
2259
- setShowTip(props.disabled ? false : true);
2260
- };
2261
- const closeTip = () => {
2262
- if (!props.content) {
2263
- return;
2264
- }
2265
- setShowTip(false);
2266
- if (props.onClose) {
2267
- props.onClose();
2268
- }
2269
- };
2270
- const buttonStyles = css.css `
2271
- font-weight: bold;
2272
- width: 1.5rem;
2273
- min-width:1.5rem;
2274
- height: 1.5rem;
2275
- padding: 0 !important;
2276
- font-family: serif;
2277
- display:inline-block;
2278
- `;
2279
- const button = React__namespace.createElement(Button, { className: buttonStyles, disabled: props.disabled, variant: 'circle', tabIndex: props.tabIndex, onClick: onClick, onMouseEnter: onMouseOver, onMouseLeave: onMouseOut }, "i");
2280
- if (props.variant === 'modal') {
2281
- return (React__namespace.createElement(React__namespace.Fragment, null,
2282
- button,
2283
- React__namespace.createElement(Modal, { id: props.modalId, __debug: props.__modalDebug, show: showTip, heading: props.modalHeader, onClick: closeTip, className: css.css({
2284
- whiteSpace: 'normal'
2285
- }), closeButton: true },
2286
- React__namespace.createElement("div", { className: css.css({ padding: '1rem' }) }, props.content))));
2287
- }
2288
- else {
2289
- return (React__namespace.createElement(Popover, { positions: props.positions, reposition: (_c = props.reposition) !== null && _c !== void 0 ? _c : false, isOpen: showTip, onClickOutside: closeTip, arrorColor: bgColor, border: '', backgroundColor: bgColor, parent: button, content: (React__namespace.createElement("div", { className: css.css({
2290
- padding: '0.5rem',
2291
- fontSize: '0.75rem',
2292
- maxWidth: '22rem'
2293
- }), style: { backgroundColor: bgColor, color: fontColor }, onMouseOut: closeTip }, props.content)) }));
2294
- }
2295
- };
2296
-
2297
- const InputErrorDisplay = (props) => {
2298
- const theme = useThemeSafely();
2299
- return (React__namespace.createElement(Text, { className: css.css({
2300
- minHeight: theme.controls.inputErrorMinHeight,
2301
- lineHeight: theme.controls.inputErrorMinHeight,
2302
- color: theme.colors.negative
2303
- }), smaller: true, noPad: true }, props.error));
2304
- };
2305
-
2306
- const defaultMaxLength$1 = 100;
2307
- const BaseInput = React__namespace.forwardRef((props, ref) => {
2308
- var _a, _b;
2309
- const theme = useThemeSafely();
2310
- const { rightControl, round, wrapperClassName, showErrorDisplay } = props, nativeProps = __rest(props, ["rightControl", "round", "wrapperClassName", "showErrorDisplay"]);
2311
- const inputStyles = css.css({
2312
- backgroundColor: theme.colors.bg,
2313
- fontFamily: theme.fonts.family,
2314
- fontSize: theme.fonts.size,
2315
- width: '100%',
2316
- border: theme.controls.border,
2317
- borderRadius: theme.controls.borderRadius,
2318
- color: theme.colors.font,
2319
- paddingLeft: theme.controls.padding,
2320
- paddingRight: theme.controls.padding,
2321
- height: theme.controls.height,
2322
- transition: theme.controls.transition,
2323
- ':focus': {
2324
- outline: 'none',
2325
- boxShadow: theme.controls.focusOutlineShadow
2326
- },
2327
- ':disabled': {
2328
- backgroundColor: theme.colors.disabled,
2329
- cursor: 'not-allowed'
2330
- },
2331
- ':invalid': {
2332
- borderColor: theme.colors.required,
2333
- ':focus': {
2334
- boxShadow: theme.controls.focusOutlineRequiredShadow
2335
- }
2336
- },
2337
- }, props.round && {
2338
- borderRadius: theme.controls.roundRadius,
2339
- paddingLeft: `calc(${theme.controls.padding} * 2)`,
2340
- paddingRight: `calc(${theme.controls.padding} * 2)`
2341
- }, props.readOnly && {
2342
- backgroundColor: 'transparent',
2343
- cursor: 'default',
2344
- border: 'none',
2345
- ':focus': {
2346
- outline: 'none',
2347
- boxShadow: 'none'
2348
- },
2349
- // FF fix to hide spinner on number elements
2350
- appearance: props.type === 'number' ? 'none' : undefined,
2351
- '::-webkit-outer-spin-button': {
2352
- appearance: 'none'
2353
- },
2354
- '::-webkit-inner-spin-button': {
2355
- appearance: 'none'
2356
- }
2357
- }, props.rightControl && {
2358
- paddingRight: theme.controls.height
2359
- });
2360
- const inputElement = React__namespace.createElement("input", Object.assign({}, nativeProps, { ref: ref, autoComplete: (_a = nativeProps.autoComplete) !== null && _a !== void 0 ? _a : 'off', tabIndex: nativeProps.readOnly ? -1 : nativeProps.tabIndex, maxLength: nativeProps.maxLength || defaultMaxLength$1, className: css.cx(inputStyles, props.className) }));
2361
- const inputWrapperStyles = css.css `
2362
- width:100%;
2363
- ${props.rightControl && `
2364
- position: relative;
2365
- `}
2366
- `;
2367
- const rightControlStyles = props.rightControl && css.css `
2368
- position: absolute;
2369
- right: ${theme.controls.padding};
2370
- top: 0;
2371
- bottom: 0;
2372
- display: flex;
2373
- align-items: center;
2374
- ${props.round && `
2375
- right: calc(${theme.controls.padding} * 2);
2376
- `}
2377
- `;
2378
- return (React__namespace.createElement("div", { className: css.css({
2379
- width: '100%',
2380
- label: 'BaseInput'
2381
- }) },
2382
- React__namespace.createElement("div", { className: css.cx('input', inputWrapperStyles, wrapperClassName) },
2383
- inputElement,
2384
- props.rightControl && (React__namespace.createElement("div", { className: rightControlStyles }, props.rightControl))),
2385
- ((_b = props.showErrorDisplay) !== null && _b !== void 0 ? _b : true) && React__namespace.createElement(InputErrorDisplay, { error: props.readOnly ? undefined : props.error })));
2386
- });
2387
-
2388
- const tryClampRange = (value, min, max) => {
2389
- if (value === undefined) {
2390
- return value;
2391
- }
2392
- if (isNaN(value)) {
2393
- return undefined;
2394
- }
2395
- if (min !== undefined && value < min) {
2396
- return min;
2397
- }
2398
- if (max !== undefined && value > max) {
2399
- return max;
2400
- }
2401
- return value;
2402
- };
2403
- const isOutOfRange = (value, min, max) => {
2404
- if (min !== undefined && value < min) {
2405
- return true;
2406
- }
2407
- if (max !== undefined && value > max) {
2408
- return true;
2409
- }
2410
- return false;
2411
- };
2412
- const getStepDecimalPlaces = (step) => {
2413
- var _a, _b;
2414
- if (!step) {
2415
- return 0;
2416
- }
2417
- const strStep = typeof step === 'number' ? step.toString() : step;
2418
- return (_b = (_a = strStep.split('.')[1]) === null || _a === void 0 ? void 0 : _a.length) !== null && _b !== void 0 ? _b : 0;
2419
- };
2420
- const tryClampDecimals = (value, step = 0) => {
2421
- if (value === undefined) {
2422
- return value;
2423
- }
2424
- if (isNaN(value)) {
2425
- return undefined;
2426
- }
2427
- const decimals = getStepDecimalPlaces(step);
2428
- if (decimals === 0) {
2429
- return Math.floor(value);
2212
+ const Highlight = (props) => {
2213
+ const theme = useThemeSafely();
2214
+ const highlightStyles = css.css `
2215
+ > mark {
2216
+ background-color: ${theme.colors.textHighlight};
2217
+ }
2218
+ `;
2219
+ let text = props.text;
2220
+ if (props.text && props.highlightText) {
2221
+ const replaceText = props.highlightText.trim();
2222
+ if (replaceText) {
2223
+ text = props.text.replace(new RegExp(`(${replaceText})`, 'gi'), `<mark>$1</mark>`);
2224
+ }
2430
2225
  }
2431
- return parseFloat(value.toFixed(decimals));
2226
+ return (React__namespace.createElement("span", { className: css.cx('highlight', highlightStyles), dangerouslySetInnerHTML: { __html: text } }));
2432
2227
  };
2433
2228
 
2434
- /** Common state handling for displaying validation messages with inputs. */
2435
- const useInputValidationMessage = (ref, props) => {
2436
- const [validationError, setValidationError] = React__default['default'].useState('');
2437
- const updateErrorMessage = (customErrorOverride) => {
2438
- var _a;
2439
- const customError = customErrorOverride || props.customError || '';
2440
- // set it OR clear it. either way, update it.
2441
- (_a = ref.current) === null || _a === void 0 ? void 0 : _a.setCustomValidity(customError);
2442
- setValidationError(customError || getValidationMessage(ref.current, props.patternErrorMessage));
2229
+ const Image = React__namespace.forwardRef((p, ref) => {
2230
+ return (React__namespace.createElement("img", Object.assign({}, p, { ref: ref, className: css.cx('image', css.css({
2231
+ label: 'Image',
2232
+ maxWidth: '100%',
2233
+ maxHeight: '100%',
2234
+ }), p.className) })));
2235
+ });
2236
+
2237
+ const Popover = (p) => {
2238
+ var _a, _b;
2239
+ const theme = useThemeSafely();
2240
+ const resposition = (_a = p.reposition) !== null && _a !== void 0 ? _a : true;
2241
+ return (React__namespace.createElement(reactTinyPopover.Popover, { containerClassName: css.css({
2242
+ zIndex: theme.zIndexes.tooltip
2243
+ }), reposition: resposition, isOpen: p.isOpen, positions: (_b = p.positions) !== null && _b !== void 0 ? _b : ['right', 'top', 'left', 'bottom'], onClickOutside: p.onClickOutside, content: ({ position, childRect, popoverRect }) => {
2244
+ var _a, _b, _c, _d;
2245
+ return (React__namespace.createElement(reactTinyPopover.ArrowContainer, { position: position, childRect: childRect, popoverRect: popoverRect, arrowColor: (_a = p.arrorColor) !== null && _a !== void 0 ? _a : theme.colors.border, arrowSize: 10 },
2246
+ React__namespace.createElement(TabLocker, null,
2247
+ React__namespace.createElement("div", { className: css.css({
2248
+ border: (_b = p.border) !== null && _b !== void 0 ? _b : theme.controls.border,
2249
+ borderRadius: (_c = p.border) !== null && _c !== void 0 ? _c : theme.controls.borderRadius,
2250
+ boxShadow: theme.controls.boxShadow,
2251
+ backgroundColor: (_d = p.backgroundColor) !== null && _d !== void 0 ? _d : theme.colors.bg,
2252
+ }) }, p.content))));
2253
+ } },
2254
+ React__namespace.createElement("span", null, p.parent)));
2255
+ };
2256
+
2257
+ const InfoTip = (props) => {
2258
+ var _a, _b, _c;
2259
+ const [showTip, setShowTip] = React__namespace.useState(false);
2260
+ const theme = useThemeSafely();
2261
+ const bgColor = (_a = props.bgColor) !== null && _a !== void 0 ? _a : theme.colors.nav;
2262
+ const fontColor = (_b = props.fontColor) !== null && _b !== void 0 ? _b : theme.colors.navFont;
2263
+ const onClick = () => {
2264
+ if (props.onClick) {
2265
+ props.onClick();
2266
+ }
2267
+ else if (props.content) {
2268
+ openTip();
2269
+ }
2443
2270
  };
2444
- React.useEffect(() => {
2445
- updateErrorMessage();
2446
- }, [props.customError]);
2447
- React__default['default'].useEffect(() => {
2448
- updateErrorMessage();
2449
- }, []);
2450
- return [validationError, updateErrorMessage];
2451
- };
2452
- const getValidationMessage = (element, patternErrorMessage) => {
2453
- var _a;
2454
- if (!element) {
2455
- return '';
2456
- }
2457
- const validity = element.validity;
2458
- if (validity.valid) {
2459
- return '';
2460
- }
2461
- if (validity.customError) {
2462
- return element.validationMessage;
2463
- }
2464
- if (validity.typeMismatch) {
2465
- switch (element.type) {
2466
- case 'url':
2467
- return `Invalid URL.`;
2468
- case 'email':
2469
- return `Invalid email.`;
2470
- default:
2471
- return element.validationMessage;
2271
+ const onMouseOver = () => {
2272
+ if (props.variant === 'modal') {
2273
+ return;
2472
2274
  }
2473
- }
2474
- if (element instanceof HTMLInputElement) {
2475
- if (validity.rangeOverflow) {
2476
- return `Must be less than or equal to ${element.max}.`;
2275
+ if (props.loadOnHover) {
2276
+ props.loadOnHover().then(() => {
2277
+ openTip();
2278
+ }).catch(err => {
2279
+ /* Not my responsiblity. */
2280
+ });
2477
2281
  }
2478
- if (validity.rangeUnderflow) {
2479
- return `Must be greater than or equal to ${element.min}.`;
2282
+ else {
2283
+ openTip();
2480
2284
  }
2481
- if (validity.stepMismatch) {
2482
- const decimalPlaces = getStepDecimalPlaces(element.step);
2483
- if (decimalPlaces > 0) {
2484
- const place = decimalPlaces === 1 ? 'place' : 'places';
2485
- return `Limited to ${getStepDecimalPlaces(element.step)} decimal ${place}.`;
2486
- }
2487
- else {
2488
- /*
2489
- step is buggy!
2490
-
2491
- at least in Chrome, setting step=5 will cause the browser to mark the field as invalid if the number is not divisible
2492
- by 5. 55 is ok. 50 is ok. 59 is not ok.
2493
-
2494
- to make things worse, if you enter an invalid number like 59 with step=5 and then clear the input, Chrome will tell
2495
- you the number 5 is now invalid and should be 4 or 9.
2496
- */
2497
- return `Must be an integer.`;
2498
- }
2285
+ };
2286
+ const onMouseOut = () => {
2287
+ if (props.variant === 'modal') {
2288
+ return;
2499
2289
  }
2290
+ closeTip();
2291
+ };
2292
+ const openTip = () => {
2293
+ if (!props.content) {
2294
+ return;
2295
+ }
2296
+ setShowTip(props.disabled ? false : true);
2297
+ };
2298
+ const closeTip = () => {
2299
+ if (!props.content) {
2300
+ return;
2301
+ }
2302
+ setShowTip(false);
2303
+ if (props.onClose) {
2304
+ props.onClose();
2305
+ }
2306
+ };
2307
+ const buttonStyles = css.css `
2308
+ font-weight: bold;
2309
+ width: 1.5rem;
2310
+ min-width:1.5rem;
2311
+ height: 1.5rem;
2312
+ padding: 0 !important;
2313
+ font-family: serif;
2314
+ display:inline-block;
2315
+ `;
2316
+ const button = React__namespace.createElement(Button, { className: buttonStyles, disabled: props.disabled, variant: 'circle', tabIndex: props.tabIndex, onClick: onClick, onMouseEnter: onMouseOver, onMouseLeave: onMouseOut }, "i");
2317
+ if (props.variant === 'modal') {
2318
+ return (React__namespace.createElement(React__namespace.Fragment, null,
2319
+ button,
2320
+ React__namespace.createElement(Modal, { id: props.modalId, __debug: props.__modalDebug, show: showTip, heading: props.modalHeader, onClick: closeTip, className: css.css({
2321
+ whiteSpace: 'normal'
2322
+ }), closeButton: true },
2323
+ React__namespace.createElement("div", { className: css.css({ padding: '1rem' }) }, props.content))));
2500
2324
  }
2501
- if (validity.tooShort) {
2502
- return `Must be at least ${((_a = element.minLength) !== null && _a !== void 0 ? _a : 0).toLocaleString()} characters in length.`;
2503
- }
2504
- if (validity.valueMissing) {
2505
- return 'Required.';
2506
- }
2507
- if (validity.patternMismatch && patternErrorMessage) {
2508
- return patternErrorMessage;
2325
+ else {
2326
+ return (React__namespace.createElement(Popover, { positions: props.positions, reposition: (_c = props.reposition) !== null && _c !== void 0 ? _c : false, isOpen: showTip, onClickOutside: closeTip, arrorColor: bgColor, border: '', backgroundColor: bgColor, parent: button, content: (React__namespace.createElement("div", { className: css.css({
2327
+ padding: '0.5rem',
2328
+ fontSize: '0.75rem',
2329
+ maxWidth: '22rem'
2330
+ }), style: { backgroundColor: bgColor, color: fontColor }, onMouseOut: closeTip }, props.content)) }));
2509
2331
  }
2510
- // unhandled. let the browser decide.
2511
- return element.validationMessage;
2512
2332
  };
2513
2333
 
2514
2334
  const dateRegex = /(\d{1,2})(?:\/|-)(\d{1,2})(?:\/|-)(\d{4})/;
@@ -2751,46 +2571,6 @@ const parseNumber = (rawValue) => {
2751
2571
  return value;
2752
2572
  };
2753
2573
 
2754
- const TextInput = React__namespace.forwardRef((props, ref) => {
2755
- var _a;
2756
- const [localValue, setLocalValue] = React__namespace.useState(props.value);
2757
- const inputRef = (ref !== null && ref !== void 0 ? ref : React__namespace.useRef(null));
2758
- const [validationError, updateErrorMessage] = useInputValidationMessage(inputRef, props);
2759
- const nativeProps = __rest(props, ["emptyString", "onValueChange", "customError", "patternErrorMessage"]);
2760
- useIgnoreMount(() => {
2761
- var _a;
2762
- if ((_a = inputRef.current) === null || _a === void 0 ? void 0 : _a.checkValidity()) {
2763
- props.onValueChange(localValue);
2764
- }
2765
- else {
2766
- props.onValueChange(undefined);
2767
- }
2768
- updateErrorMessage();
2769
- }, [localValue]);
2770
- useIgnoreMount(() => {
2771
- if (document.activeElement !== inputRef.current) {
2772
- setLocalValue(props.value);
2773
- }
2774
- updateErrorMessage();
2775
- }, [props.value]);
2776
- return (React__namespace.createElement(BaseInput, Object.assign({}, nativeProps, { error: validationError, type: (_a = props.type) !== null && _a !== void 0 ? _a : 'text', ref: inputRef, value: localValue !== null && localValue !== void 0 ? localValue : '', onChange: e => {
2777
- var _a;
2778
- setLocalValue(props.emptyString ? e.target.value : e.target.value || undefined);
2779
- (_a = props.onChange) === null || _a === void 0 ? void 0 : _a.call(props, e);
2780
- }, onBlur: e => {
2781
- var _a, _b;
2782
- if (!e.target.checkValidity()) {
2783
- setLocalValue(undefined);
2784
- }
2785
- else if ((_a = props.trim) !== null && _a !== void 0 ? _a : true) {
2786
- setLocalValue(currentValue => {
2787
- return currentValue === null || currentValue === void 0 ? void 0 : currentValue.trim();
2788
- });
2789
- }
2790
- (_b = props.onBlur) === null || _b === void 0 ? void 0 : _b.call(props, e);
2791
- } })));
2792
- });
2793
-
2794
2574
  const Label = (props) => {
2795
2575
  var _a, _b, _c;
2796
2576
  const labelProps = __rest(props, ["text", "static", "orientation", "align", "noWrap", "subText", "optional", "controlAlign"]);
@@ -4392,8 +4172,12 @@ const ToggleButtonGroup = (props) => {
4392
4172
  };
4393
4173
 
4394
4174
  const TogglePasswordInput = React__namespace.forwardRef((props, ref) => {
4175
+ const { onVisibilityChanged } = props, inputProps = __rest(props, ["onVisibilityChanged"]);
4395
4176
  const [show, setShow] = React__namespace.useState(false);
4396
- return (React__namespace.createElement(TextInput, Object.assign({}, props, { ref: ref, type: show ? 'text' : 'password', rightControl: (React__namespace.createElement(Button, { small: true, style: {
4177
+ useIgnoreMount(() => {
4178
+ onVisibilityChanged === null || onVisibilityChanged === void 0 ? void 0 : onVisibilityChanged(show);
4179
+ }, [show]);
4180
+ return (React__namespace.createElement(TextInput, Object.assign({}, inputProps, { ref: ref, type: show ? 'text' : 'password', rightControl: (React__namespace.createElement(Button, { small: true, style: {
4397
4181
  // small button is required here due to the icon pushing outside the boundries of the
4398
4182
  // parent textbox. increasing the font size here to fill the small button.
4399
4183
  fontSize: '1rem'
@@ -4871,8 +4655,106 @@ const TabContainer = (p) => {
4871
4655
  }), p.contentClassName) }, p.tabs[tabIndex].getContent())));
4872
4656
  };
4873
4657
 
4658
+ const defaultMinChars = 3;
4659
+ /** Extracted logic around autocomplete functionality for Autocomplete.tsx. */
4660
+ class AutocompleteController {
4661
+ constructor(getOptions, config) {
4662
+ var _a;
4663
+ this._value = undefined;
4664
+ this._options = [];
4665
+ this._minChars = (_a = config === null || config === void 0 ? void 0 : config.minChars) !== null && _a !== void 0 ? _a : defaultMinChars;
4666
+ if (config === null || config === void 0 ? void 0 : config.debounceMs) {
4667
+ this.getOptions = lodash.debounce(getOptions, config.debounceMs, {
4668
+ leading: false,
4669
+ trailing: true
4670
+ });
4671
+ }
4672
+ else {
4673
+ this.getOptions = getOptions;
4674
+ }
4675
+ }
4676
+ get value() {
4677
+ return this._value;
4678
+ }
4679
+ get options() {
4680
+ return this._options;
4681
+ }
4682
+ async onChange(newValue) {
4683
+ var _a;
4684
+ // don't make getOptions calls if the value hasn't changed.
4685
+ if (newValue === this.value) {
4686
+ return;
4687
+ }
4688
+ // nullish should not make the getOptions call and instead clear everything.
4689
+ if (!newValue) {
4690
+ this._value = newValue;
4691
+ this._options = [];
4692
+ return;
4693
+ }
4694
+ // sub min chars should clear everything and not attempt the getOptions call.
4695
+ if (newValue.length < this._minChars) {
4696
+ this._value = newValue;
4697
+ this._options = [];
4698
+ return;
4699
+ }
4700
+ try {
4701
+ this._value = newValue;
4702
+ // debounce (if used) will return undefined until the actual function is executed.
4703
+ // after that it will return the result of the last execution.
4704
+ this._options = (_a = (await this.getOptions(newValue))) !== null && _a !== void 0 ? _a : [];
4705
+ }
4706
+ catch (err) {
4707
+ // the getOptions method needs to handle it's own errors
4708
+ }
4709
+ }
4710
+ onPick(newValue) {
4711
+ this._value = newValue;
4712
+ this._options = [];
4713
+ }
4714
+ }
4715
+
4716
+ /** Extracted logic around autocomplete functionality for Autocomplete.tsx that supports Entity (id/name) mapping. */
4717
+ class AutocompleteEntityController {
4718
+ constructor(getOptions, config) {
4719
+ this._options = [];
4720
+ const getStringOptions = async (value) => {
4721
+ this._options = await getOptions(value);
4722
+ return this._options.map(o => o.name);
4723
+ };
4724
+ this._ctrl = new AutocompleteController(getStringOptions, config);
4725
+ }
4726
+ get entity() {
4727
+ return this._pickedEntity;
4728
+ }
4729
+ get entities() {
4730
+ return this._options;
4731
+ }
4732
+ get value() {
4733
+ return this._ctrl.value;
4734
+ }
4735
+ get options() {
4736
+ return this._options.map(o => o.name);
4737
+ }
4738
+ async onChange(newValue) {
4739
+ await this._ctrl.onChange(newValue);
4740
+ this.trySyncCtrlOptions();
4741
+ }
4742
+ onPick(newValue) {
4743
+ this._ctrl.onPick(newValue);
4744
+ this._pickedEntity = this._options.find(o => o.name === this._ctrl.value);
4745
+ this.trySyncCtrlOptions();
4746
+ }
4747
+ trySyncCtrlOptions() {
4748
+ if (!this._ctrl.options.length) {
4749
+ this._options = [];
4750
+ }
4751
+ }
4752
+ }
4753
+
4874
4754
  exports.Accordian = Accordian;
4875
4755
  exports.Autocomplete = Autocomplete;
4756
+ exports.AutocompleteController = AutocompleteController;
4757
+ exports.AutocompleteEntityController = AutocompleteEntityController;
4876
4758
  exports.Backdrop = Backdrop$1;
4877
4759
  exports.Backdrop2 = Backdrop;
4878
4760
  exports.BoundMemoryPager = BoundMemoryPager;
@@ -4897,7 +4779,6 @@ exports.Icon = Icon;
4897
4779
  exports.Image = Image;
4898
4780
  exports.InfoPanel = InfoPanel;
4899
4781
  exports.InfoTip = InfoTip;
4900
- exports.Input = Input;
4901
4782
  exports.ItemPager = ItemPager;
4902
4783
  exports.Label = Label;
4903
4784
  exports.Link = Link;