@mackin.com/styleguide 10.2.6 → 11.0.1

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 (4) hide show
  1. package/index.d.ts +11 -5
  2. package/index.esm.js +140 -104
  3. package/index.js +140 -104
  4. package/package.json +1 -1
package/index.d.ts CHANGED
@@ -4,6 +4,8 @@ import { IconDefinition } from '@fortawesome/fontawesome-svg-core';
4
4
 
5
5
  type HeaderVariant = 'label' | 'link' | 'primary' | 'secondary' | 'omg' | 'primary2' | 'positive' | 'negative';
6
6
  interface AccordianProps {
7
+ /** Required for ARIA. */
8
+ id: string;
7
9
  header: JSX.Element | string;
8
10
  children: React.ReactNode;
9
11
  variant?: HeaderVariant;
@@ -33,8 +35,12 @@ interface InputOnFocusProps {
33
35
  allowUpdateOnFocus?: boolean;
34
36
  }
35
37
 
36
- type BaseInputProps$1 = Omit<TextInputProps, 'value' | 'className' | 'wrapperClassName' | 'type' | 'showErrorDisplay'> & InputOnFocusProps;
38
+ type BaseInputProps$1 = Omit<TextInputProps, 'value' | 'className' | 'wrapperClassName' | 'type' | 'showErrorDisplay' | 'id' | 'aria-label'> & InputOnFocusProps;
37
39
  interface AutocompleteProps extends BaseInputProps$1 {
40
+ /** Required for ARIA. Will be used to add unique IDs to the options. */
41
+ id: string;
42
+ /** Required for ARIA. Will be applied to both the input and the popup listbox. */
43
+ ariaLabel: string;
38
44
  value: string | undefined;
39
45
  options: string[];
40
46
  /** Applied to the Autocomplete wrapper. */
@@ -43,15 +49,12 @@ interface AutocompleteProps extends BaseInputProps$1 {
43
49
  inputClassName?: string;
44
50
  listClassName?: string;
45
51
  listItemClassName?: string;
46
- listItemButtonClassName?: string;
47
52
  /** Limits what will be show in the autocomplete options. Default is 7. */
48
53
  maxShownValues?: number;
49
54
  /** Will enable scrolling in the results list. */
50
55
  allowScroll?: boolean;
51
56
  /** Delay before the input is re-focused after picking a value. Adjust if there are issues with the displayed input value after pick. Defaults to 100ms. */
52
57
  onPickFocusWaitMs?: number;
53
- /** The option `title` attribute will be filled with the option text. Defaults to `true`. */
54
- showOptionTextAsTitle?: boolean;
55
58
  onPick: (value: string | undefined, index?: number) => void;
56
59
  }
57
60
  declare const Autocomplete: (p: AutocompleteProps) => React.JSX.Element;
@@ -130,10 +133,11 @@ interface CalendarProps {
130
133
  declare const Calendar: (p: CalendarProps) => React.JSX.Element;
131
134
 
132
135
  interface CheckboxProps extends Omit<React.DetailedHTMLProps<React.InputHTMLAttributes<HTMLInputElement>, HTMLInputElement>, 'checked' | 'onChange' | 'type'> {
136
+ /** Required for the internal label (ARIA). Pass `true` to `hideLabel` to treat this label as an aria-label. */
137
+ label: string;
133
138
  checked: boolean;
134
139
  onChange: (checked: boolean, event: React.ChangeEvent<HTMLInputElement>) => void;
135
140
  readOnly?: boolean;
136
- label?: string;
137
141
  checkedIcon?: string;
138
142
  uncheckedIcon?: string;
139
143
  /** Background color when checked based on the current theme. Mutually exclusive with 'checkedColor'. */
@@ -141,6 +145,8 @@ interface CheckboxProps extends Omit<React.DetailedHTMLProps<React.InputHTMLAttr
141
145
  /** Background color when checked. Mutually exclusive with 'checkedThemeColor'. */
142
146
  checkedColor?: string;
143
147
  tabIndex?: number | undefined;
148
+ /** Pass `true` to `hideLabel` to treat `label` as an aria-label. */
149
+ hideLabel?: boolean;
144
150
  }
145
151
  declare const Checkbox: (props: CheckboxProps) => React.JSX.Element;
146
152
 
package/index.esm.js CHANGED
@@ -292,7 +292,6 @@ const Text = (props) => {
292
292
  }, props.children);
293
293
  };
294
294
 
295
- //TB: FUTURE de-dup these styles. create individual styles and compose them manually.
296
295
  const Button = React.forwardRef((props, ref) => {
297
296
  var _a;
298
297
  const { variant, round, rightIcon, leftIcon, iconBlock, small, readOnly, waiting, enforceMinWidth, controlAlign } = props, nativeProps = __rest(props, ["variant", "round", "rightIcon", "leftIcon", "iconBlock", "small", "readOnly", "waiting", "enforceMinWidth", "controlAlign"]);
@@ -551,26 +550,33 @@ const Accordian = (props) => {
551
550
  }
552
551
  setOpen((_a = props.open) !== null && _a !== void 0 ? _a : false);
553
552
  }, [props.open]);
554
- return (React.createElement("div", { className: "accordian", "aria-expanded": open },
555
- React.createElement(Button, { readOnly: props.disabled, variant: props.variant, className: cx(css({
556
- display: 'flex',
557
- alignItems: 'center',
558
- justifyContent: 'space-between',
559
- height: 'auto',
560
- minHeight: theme.controls.height,
561
- width: ((_d = props.block) !== null && _d !== void 0 ? _d : true) ? '100%' : 'auto'
562
- }, props.className)), onClick: e => {
563
- e.stopPropagation();
564
- if (props.onChange) {
565
- props.onChange(!open);
566
- }
567
- else {
568
- setOpen(!open);
569
- }
570
- }, rightIcon: !props.disabled ? React.createElement(Icon, { id: open ? 'collapse' : 'expand' }) : undefined },
571
- React.createElement("span", null, props.header)),
553
+ const expandedPanelId = `${props.id}_expanded`;
554
+ return (React.createElement("div", { id: props.id, className: "accordian" },
555
+ React.createElement("h3", { className: css({
556
+ // required for ARIA
557
+ margin: 0,
558
+ padding: 0,
559
+ fontSize: 'inherit'
560
+ }) },
561
+ React.createElement(Button, { "aria-controls": expandedPanelId, "aria-expanded": open, "aria-disabled": props.disabled, readOnly: props.disabled, variant: props.variant, className: cx(css({
562
+ display: 'flex',
563
+ alignItems: 'center',
564
+ justifyContent: 'space-between',
565
+ height: 'auto',
566
+ minHeight: theme.controls.height,
567
+ width: ((_d = props.block) !== null && _d !== void 0 ? _d : true) ? '100%' : 'auto'
568
+ }, props.className)), onClick: e => {
569
+ e.stopPropagation();
570
+ if (props.onChange) {
571
+ props.onChange(!open);
572
+ }
573
+ else {
574
+ setOpen(!open);
575
+ }
576
+ }, rightIcon: !props.disabled ? React.createElement(Icon, { id: open ? 'collapse' : 'expand' }) : undefined },
577
+ React.createElement("span", null, props.header))),
572
578
  React.createElement("div", { ref: content, className: cx('accordian__body', contentStyles) },
573
- React.createElement("div", { className: expandedContentWrapperStyles }, children))));
579
+ React.createElement("div", { className: expandedContentWrapperStyles, id: expandedPanelId }, children))));
574
580
  };
575
581
  const useAccordianState = (count, openIndex) => {
576
582
  const [panels, setShowPanel] = React.useState(new Array(count).fill(false).map((b, i) => {
@@ -972,18 +978,23 @@ const TabLocker = (props) => {
972
978
  } }, props.children));
973
979
  };
974
980
 
981
+ /*
982
+ ARIA info:
983
+ https://www.w3.org/WAI/ARIA/apg/patterns/combobox/
984
+ https://www.w3.org/WAI/ARIA/apg/patterns/combobox/examples/combobox-autocomplete-list/
985
+ This would be considered "List autocomplete with manual selection"
986
+ */
975
987
  const defaultMaxShownValues = 7;
976
- const buttonMarkerClass = 'ListItem__button';
977
988
  const defaultOnPickFocusMs = 100;
978
989
  const Autocomplete = (p) => {
979
- var _a, _b;
990
+ var _a;
980
991
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
981
- const inputProps = __rest(p, ["value", "className", "inputWrapperClassName", "inputClassName", "listClassName", "listItemClassName", "listItemButtonClassName", "maxShownValues", "allowScroll", "options", "onPick", "onPickFocusWaitMs"]);
992
+ const inputProps = __rest(p, ["value", "className", "inputWrapperClassName", "inputClassName", "listClassName", "listItemClassName", "maxShownValues", "allowScroll", "options", "onPick", "onPickFocusWaitMs", "ariaLabel"]);
982
993
  const theme = useThemeSafely();
983
994
  const element = React.useRef(null);
984
995
  const input = React.useRef(null);
985
996
  const list = React.useRef(null);
986
- const [selectedResultIndex, setSelectedResultIndex] = React.useState();
997
+ const [selectedResultIndex, setSelectedResultIndex] = React.useState(-1);
987
998
  const maxShowValues = (_a = p.maxShownValues) !== null && _a !== void 0 ? _a : defaultMaxShownValues;
988
999
  const displayOptions = React.useMemo(() => {
989
1000
  if (!p.allowScroll) {
@@ -995,27 +1006,6 @@ const Autocomplete = (p) => {
995
1006
  const resultsText = React.useMemo(() => {
996
1007
  return `${getText("Showing")} ${displayOptions.length.toLocaleString()} ${getText("of")} ${p.options.length.toLocaleString()} ${getText("results")}.`;
997
1008
  }, [language, displayOptions, p.options]);
998
- const getNextTabElement = (fromIndex, direction) => {
999
- var _a, _b, _c;
1000
- if (fromIndex === -1) {
1001
- let buttonIndex = 0;
1002
- if (direction === -1) {
1003
- buttonIndex = displayOptions.length - 1;
1004
- }
1005
- setSelectedResultIndex(buttonIndex);
1006
- return (_a = list.current) === null || _a === void 0 ? void 0 : _a.querySelector(`.${buttonMarkerClass}${buttonIndex}`);
1007
- }
1008
- else {
1009
- const nextIndex = fromIndex + direction;
1010
- setSelectedResultIndex(nextIndex);
1011
- if (nextIndex >= displayOptions.length || nextIndex < 0) {
1012
- return (_b = input.current) !== null && _b !== void 0 ? _b : undefined;
1013
- }
1014
- else {
1015
- return (_c = list.current) === null || _c === void 0 ? void 0 : _c.querySelector(`.${buttonMarkerClass}${nextIndex}`);
1016
- }
1017
- }
1018
- };
1019
1009
  React.useEffect(() => {
1020
1010
  const clearItems = () => {
1021
1011
  if (p.options.length) {
@@ -1031,57 +1021,118 @@ const Autocomplete = (p) => {
1031
1021
  if (p.round || theme.controls.borderRadius) {
1032
1022
  listBorderRadius = theme.controls.borderRadius || '0.5rem';
1033
1023
  }
1034
- const id = (_b = p.id) !== null && _b !== void 0 ? _b : `${Date.now()}-${Math.random().toString(36).substring(2, 9)}`;
1035
- const onPickValue = (v) => {
1024
+ const onPickValue = (v, keepFocus = true) => {
1036
1025
  var _a;
1037
1026
  // the TextInput will not respond to outer value changes if it has focus.
1038
1027
  // here we clear first and then onPickValue will re-focus after all updates.
1039
1028
  (_a = input.current) === null || _a === void 0 ? void 0 : _a.blur();
1040
1029
  setTimeout(() => {
1041
- // blur is now complete
1042
1030
  var _a;
1031
+ // blur is now complete
1043
1032
  let index = v ? p.options.findIndex(o => o === v) : undefined;
1044
1033
  if (index !== undefined && index < 0) {
1045
1034
  index = undefined;
1046
1035
  }
1047
1036
  p.onPick(v, index);
1048
1037
  // wait for the re-render. the value will not update if the control has focus
1049
- setTimeout(() => {
1050
- var _a;
1051
- (_a = input.current) === null || _a === void 0 ? void 0 : _a.focus();
1052
- }, (_a = p.onPickFocusWaitMs) !== null && _a !== void 0 ? _a : defaultOnPickFocusMs);
1038
+ if (keepFocus) {
1039
+ setTimeout(() => {
1040
+ var _a;
1041
+ (_a = input.current) === null || _a === void 0 ? void 0 : _a.focus();
1042
+ }, (_a = p.onPickFocusWaitMs) !== null && _a !== void 0 ? _a : defaultOnPickFocusMs);
1043
+ }
1053
1044
  }, 0);
1054
1045
  };
1046
+ React.useEffect(() => {
1047
+ if (selectedResultIndex === -1) {
1048
+ return;
1049
+ }
1050
+ const element = document.getElementById(getOptionId(p.id, selectedResultIndex));
1051
+ if (element) {
1052
+ element.scrollIntoView();
1053
+ }
1054
+ }, [selectedResultIndex]);
1055
+ const popupId = `${p.id}_ul`;
1056
+ const listItemStyles = css({
1057
+ paddingLeft: theme.controls.padding,
1058
+ paddingRight: theme.controls.padding,
1059
+ backgroundColor: 'white',
1060
+ cursor: 'pointer',
1061
+ color: theme.colors.font,
1062
+ height: theme.controls.height,
1063
+ transition: theme.controls.transition,
1064
+ fontsize: '1rem',
1065
+ fontWeight: 'bold',
1066
+ ':hover': {
1067
+ filter: theme.controls.hoverBrightness
1068
+ }
1069
+ });
1070
+ const listItemFocusStyles = css({
1071
+ outline: 'none',
1072
+ boxShadow: theme.controls.focusOutlineShadow,
1073
+ // prevents box shadow clipping
1074
+ position: 'relative',
1075
+ zIndex: 2
1076
+ });
1077
+ const listItemTextStyles = css({
1078
+ lineHeight: theme.controls.height
1079
+ });
1055
1080
  return (React.createElement("div", { onClick: e => {
1056
1081
  e.stopPropagation();
1057
1082
  }, onKeyDown: e => {
1058
1083
  if (e.key === 'Escape') {
1059
1084
  onPickValue(undefined);
1085
+ setSelectedResultIndex(-1);
1060
1086
  }
1061
- }, ref: element, className: cx(css({
1062
- position: 'relative',
1063
- width: '100%',
1064
- label: 'Autocomplete'
1065
- }), p.className, 'autocomplete') },
1087
+ }, ref: element, className: cx(styles.autocomplete, p.className, 'autocomplete') },
1066
1088
  React.createElement(TabLocker, { disabled: !displayOptions.length, style: { position: 'relative' } },
1067
1089
  React.createElement(TextInput, Object.assign({}, inputProps, { showErrorDisplay: false, ref: input, value: p.value, className: p.inputClassName, wrapperClassName: p.inputWrapperClassName, onKeyDown: e => {
1068
- var _a, _b, _c;
1069
- if (displayOptions.length) {
1070
- if (e.key === 'ArrowDown') {
1071
- e.preventDefault();
1072
- e.stopPropagation();
1073
- (_a = getNextTabElement(-1, 1)) === null || _a === void 0 ? void 0 : _a.focus();
1090
+ let handled = false;
1091
+ switch (e.code) {
1092
+ case 'ArrowDown': {
1093
+ if (displayOptions.length) {
1094
+ let nextIndex = selectedResultIndex + 1;
1095
+ if (nextIndex >= displayOptions.length) {
1096
+ nextIndex = 0;
1097
+ }
1098
+ setSelectedResultIndex(nextIndex);
1099
+ handled = true;
1100
+ }
1101
+ break;
1074
1102
  }
1075
- else if (e.key === 'ArrowUp') {
1076
- e.preventDefault();
1077
- e.stopPropagation();
1078
- (_b = getNextTabElement(-1, -1)) === null || _b === void 0 ? void 0 : _b.focus();
1103
+ case 'ArrowUp': {
1104
+ if (displayOptions.length) {
1105
+ let nextIndex = selectedResultIndex - 1;
1106
+ if (nextIndex < 0) {
1107
+ nextIndex = displayOptions.length - 1;
1108
+ }
1109
+ setSelectedResultIndex(nextIndex);
1110
+ handled = true;
1111
+ }
1112
+ break;
1113
+ }
1114
+ case 'Enter': {
1115
+ let pickedValue = p.value;
1116
+ if (selectedResultIndex >= 0) {
1117
+ pickedValue = displayOptions[selectedResultIndex];
1118
+ }
1119
+ onPickValue(pickedValue);
1120
+ setSelectedResultIndex(-1);
1121
+ handled = true;
1122
+ break;
1079
1123
  }
1124
+ case 'Tab': {
1125
+ onPickValue(p.value, false);
1126
+ setSelectedResultIndex(-1);
1127
+ break;
1128
+ }
1129
+ }
1130
+ if (handled) {
1131
+ e.preventDefault();
1132
+ e.stopPropagation();
1080
1133
  }
1081
- (_c = p.onKeyDown) === null || _c === void 0 ? void 0 : _c.call(p, e);
1082
- }, "aria-owns": id, "aria-expanded": !!displayOptions.length, "aria-autocomplete": "both", "aria-describedby": `${id}-aria-description` })),
1083
- React.createElement("span", { id: `${id}-aria-description`, className: css({ display: "none" }) }, "When autocomplete results are available use up and down arrows to review and enter to select."),
1084
- !!displayOptions.length && (React.createElement(List, { id: id, ref: list, role: "listbox", className: cx(css({
1134
+ }, role: "combobox", "aria-expanded": !!displayOptions.length, "aria-controls": popupId, "aria-autocomplete": "list", "aria-activedescendant": selectedResultIndex >= 0 ? getOptionId(p.id, selectedResultIndex) : undefined, "aria-label": p.ariaLabel })),
1135
+ !!displayOptions.length && (React.createElement(List, { id: popupId, ref: list, role: "listbox", "aria-label": p.ariaLabel, className: cx(css({
1085
1136
  position: 'absolute',
1086
1137
  width: '100%',
1087
1138
  border: theme.controls.border,
@@ -1090,11 +1141,11 @@ const Autocomplete = (p) => {
1090
1141
  backgroundColor: theme.colors.bg,
1091
1142
  marginTop: `-4px !important`,
1092
1143
  zIndex: theme.zIndexes.backdrop,
1093
- 'li:first-child button': {
1144
+ 'li:first-child': {
1094
1145
  borderTopRightRadius: listBorderRadius,
1095
1146
  borderTopLeftRadius: listBorderRadius,
1096
1147
  },
1097
- 'li:last-child button': {
1148
+ 'li:last-child': {
1098
1149
  borderBottomRightRadius: listBorderRadius,
1099
1150
  borderBottomLeftRadius: listBorderRadius,
1100
1151
  }
@@ -1103,36 +1154,23 @@ const Autocomplete = (p) => {
1103
1154
  maxHeight: `calc(${theme.controls.height} * ${maxShowValues})`
1104
1155
  }), p.listClassName) },
1105
1156
  displayOptions.map((v, listItemIndex) => {
1106
- var _a;
1107
- return (React.createElement(ListItem, { key: v, variant: "full", className: p.listItemClassName, role: "option", "aria-selected": selectedResultIndex === listItemIndex },
1108
- React.createElement(Button, { title: ((_a = p.showOptionTextAsTitle) !== null && _a !== void 0 ? _a : true) ? v : undefined, onKeyDown: e => {
1109
- var _a, _b;
1110
- if (e.key === 'ArrowDown') {
1111
- e.stopPropagation();
1112
- e.preventDefault();
1113
- (_a = getNextTabElement(listItemIndex, 1)) === null || _a === void 0 ? void 0 : _a.focus();
1114
- }
1115
- else if (e.key === 'ArrowUp') {
1116
- e.stopPropagation();
1117
- e.preventDefault();
1118
- (_b = getNextTabElement(listItemIndex, -1)) === null || _b === void 0 ? void 0 : _b.focus();
1119
- }
1120
- else if (e.key === 'Enter') {
1121
- e.stopPropagation();
1122
- // this will prevent the click event from firing in addition to this enter key event.
1123
- e.preventDefault();
1124
- onPickValue(v);
1125
- }
1126
- }, className: cx(buttonMarkerClass + listItemIndex, css({
1127
- borderRadius: 0,
1128
- }), p.listItemButtonClassName), onClick: () => {
1129
- onPickValue(v);
1130
- } },
1131
- React.createElement(Text, { tag: "div", ellipsis: true, align: "left" }, v))));
1157
+ const selected = selectedResultIndex === listItemIndex;
1158
+ return (React.createElement(ListItem, { key: v, variant: "full", className: cx(listItemStyles, selected ? listItemFocusStyles : undefined, p.listItemClassName), role: "option", id: getOptionId(p.id, listItemIndex), "aria-selected": selected },
1159
+ React.createElement(Text, { tag: "div", ellipsis: true, align: "left", className: listItemTextStyles }, v)));
1132
1160
  }),
1133
1161
  !p.allowScroll && displayOptions.length < p.options.length && (React.createElement(ListItem, { className: p.listItemClassName },
1134
1162
  React.createElement(Text, { tag: "div", italics: true, align: "center" }, resultsText))))))));
1135
1163
  };
1164
+ function getOptionId(baseId, optionIndex) {
1165
+ return `${baseId}_li_${optionIndex}`;
1166
+ }
1167
+ const styles = {
1168
+ autocomplete: css({
1169
+ position: 'relative',
1170
+ width: '100%',
1171
+ label: 'Autocomplete'
1172
+ }),
1173
+ };
1136
1174
 
1137
1175
  /** Returns a UID. Use this instead of a direct call to a library. */
1138
1176
  function createUid() {
@@ -1493,7 +1531,7 @@ const Calendar = (p) => {
1493
1531
 
1494
1532
  const Checkbox = (props) => {
1495
1533
  var _a;
1496
- const { onChange, label, checkedIcon, uncheckedIcon, checkedThemeColor, checkedColor, readOnly } = props, inputProps = __rest(props, ["onChange", "label", "checkedIcon", "uncheckedIcon", "checkedThemeColor", "checkedColor", "readOnly"]);
1534
+ const { onChange, label, checkedIcon, uncheckedIcon, checkedThemeColor, checkedColor, readOnly, hideLabel } = props, inputProps = __rest(props, ["onChange", "label", "checkedIcon", "uncheckedIcon", "checkedThemeColor", "checkedColor", "readOnly", "hideLabel"]);
1497
1535
  const selected = checkedIcon || 'selected';
1498
1536
  const unselected = uncheckedIcon || 'unselected';
1499
1537
  const theme = useThemeSafely();
@@ -1557,7 +1595,7 @@ const Checkbox = (props) => {
1557
1595
  `;
1558
1596
  return (React.createElement("span", { className: cx('checkbox', checkboxStyles, props.className) },
1559
1597
  React.createElement("label", { className: labelStyles },
1560
- React.createElement("input", Object.assign({}, inputProps, { tabIndex: readOnly ? -1 : (_a = props.tabIndex) !== null && _a !== void 0 ? _a : undefined, className: nativeCheckboxStyles, type: "checkbox", onChange: e => {
1598
+ React.createElement("input", Object.assign({}, inputProps, { "aria-label": hideLabel ? label : undefined, tabIndex: readOnly ? -1 : (_a = props.tabIndex) !== null && _a !== void 0 ? _a : undefined, className: nativeCheckboxStyles, type: "checkbox", onChange: e => {
1561
1599
  if (readOnly) {
1562
1600
  e.preventDefault();
1563
1601
  return;
@@ -1565,7 +1603,7 @@ const Checkbox = (props) => {
1565
1603
  return onChange(e.currentTarget.checked, e);
1566
1604
  } })),
1567
1605
  React.createElement(Icon, { className: cx('checkboxIcon', iconStyles), id: props.checked ? selected : unselected }),
1568
- label,
1606
+ !hideLabel && label,
1569
1607
  props.children)));
1570
1608
  };
1571
1609
 
@@ -2860,7 +2898,6 @@ const LinkContent = (props) => {
2860
2898
  }) }, props.rightIcon)));
2861
2899
  };
2862
2900
 
2863
- //TB: FUTURE de-dup these styles. create individual styles and compose them manually.
2864
2901
  const generateLinkStyles = (props, theme) => {
2865
2902
  const disabled = props.disabled || props.waiting;
2866
2903
  let color = props.colorOverride;
@@ -4229,7 +4266,6 @@ const TextArea = React.forwardRef((props, ref) => {
4229
4266
  }
4230
4267
  else {
4231
4268
  if (reportValueOnError) {
4232
- //TB: temp, add a custom list of validators that will be run for all inputs if a pattern cannot be decided.
4233
4269
  onValueChange(localValue);
4234
4270
  }
4235
4271
  else {
package/index.js CHANGED
@@ -310,7 +310,6 @@ const Text = (props) => {
310
310
  }, props.children);
311
311
  };
312
312
 
313
- //TB: FUTURE de-dup these styles. create individual styles and compose them manually.
314
313
  const Button = React__namespace.forwardRef((props, ref) => {
315
314
  var _a;
316
315
  const { variant, round, rightIcon, leftIcon, iconBlock, small, readOnly, waiting, enforceMinWidth, controlAlign } = props, nativeProps = __rest(props, ["variant", "round", "rightIcon", "leftIcon", "iconBlock", "small", "readOnly", "waiting", "enforceMinWidth", "controlAlign"]);
@@ -569,26 +568,33 @@ const Accordian = (props) => {
569
568
  }
570
569
  setOpen((_a = props.open) !== null && _a !== void 0 ? _a : false);
571
570
  }, [props.open]);
572
- return (React__namespace.createElement("div", { className: "accordian", "aria-expanded": open },
573
- React__namespace.createElement(Button, { readOnly: props.disabled, variant: props.variant, className: css.cx(css.css({
574
- display: 'flex',
575
- alignItems: 'center',
576
- justifyContent: 'space-between',
577
- height: 'auto',
578
- minHeight: theme.controls.height,
579
- width: ((_d = props.block) !== null && _d !== void 0 ? _d : true) ? '100%' : 'auto'
580
- }, props.className)), onClick: e => {
581
- e.stopPropagation();
582
- if (props.onChange) {
583
- props.onChange(!open);
584
- }
585
- else {
586
- setOpen(!open);
587
- }
588
- }, rightIcon: !props.disabled ? React__namespace.createElement(Icon, { id: open ? 'collapse' : 'expand' }) : undefined },
589
- React__namespace.createElement("span", null, props.header)),
571
+ const expandedPanelId = `${props.id}_expanded`;
572
+ return (React__namespace.createElement("div", { id: props.id, className: "accordian" },
573
+ React__namespace.createElement("h3", { className: css.css({
574
+ // required for ARIA
575
+ margin: 0,
576
+ padding: 0,
577
+ fontSize: 'inherit'
578
+ }) },
579
+ React__namespace.createElement(Button, { "aria-controls": expandedPanelId, "aria-expanded": open, "aria-disabled": props.disabled, readOnly: props.disabled, variant: props.variant, className: css.cx(css.css({
580
+ display: 'flex',
581
+ alignItems: 'center',
582
+ justifyContent: 'space-between',
583
+ height: 'auto',
584
+ minHeight: theme.controls.height,
585
+ width: ((_d = props.block) !== null && _d !== void 0 ? _d : true) ? '100%' : 'auto'
586
+ }, props.className)), onClick: e => {
587
+ e.stopPropagation();
588
+ if (props.onChange) {
589
+ props.onChange(!open);
590
+ }
591
+ else {
592
+ setOpen(!open);
593
+ }
594
+ }, rightIcon: !props.disabled ? React__namespace.createElement(Icon, { id: open ? 'collapse' : 'expand' }) : undefined },
595
+ React__namespace.createElement("span", null, props.header))),
590
596
  React__namespace.createElement("div", { ref: content, className: css.cx('accordian__body', contentStyles) },
591
- React__namespace.createElement("div", { className: expandedContentWrapperStyles }, children))));
597
+ React__namespace.createElement("div", { className: expandedContentWrapperStyles, id: expandedPanelId }, children))));
592
598
  };
593
599
  const useAccordianState = (count, openIndex) => {
594
600
  const [panels, setShowPanel] = React__namespace.useState(new Array(count).fill(false).map((b, i) => {
@@ -990,18 +996,23 @@ const TabLocker = (props) => {
990
996
  } }, props.children));
991
997
  };
992
998
 
999
+ /*
1000
+ ARIA info:
1001
+ https://www.w3.org/WAI/ARIA/apg/patterns/combobox/
1002
+ https://www.w3.org/WAI/ARIA/apg/patterns/combobox/examples/combobox-autocomplete-list/
1003
+ This would be considered "List autocomplete with manual selection"
1004
+ */
993
1005
  const defaultMaxShownValues = 7;
994
- const buttonMarkerClass = 'ListItem__button';
995
1006
  const defaultOnPickFocusMs = 100;
996
1007
  const Autocomplete = (p) => {
997
- var _a, _b;
1008
+ var _a;
998
1009
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
999
- const inputProps = __rest(p, ["value", "className", "inputWrapperClassName", "inputClassName", "listClassName", "listItemClassName", "listItemButtonClassName", "maxShownValues", "allowScroll", "options", "onPick", "onPickFocusWaitMs"]);
1010
+ const inputProps = __rest(p, ["value", "className", "inputWrapperClassName", "inputClassName", "listClassName", "listItemClassName", "maxShownValues", "allowScroll", "options", "onPick", "onPickFocusWaitMs", "ariaLabel"]);
1000
1011
  const theme = useThemeSafely();
1001
1012
  const element = React__namespace.useRef(null);
1002
1013
  const input = React__namespace.useRef(null);
1003
1014
  const list = React__namespace.useRef(null);
1004
- const [selectedResultIndex, setSelectedResultIndex] = React__namespace.useState();
1015
+ const [selectedResultIndex, setSelectedResultIndex] = React__namespace.useState(-1);
1005
1016
  const maxShowValues = (_a = p.maxShownValues) !== null && _a !== void 0 ? _a : defaultMaxShownValues;
1006
1017
  const displayOptions = React__namespace.useMemo(() => {
1007
1018
  if (!p.allowScroll) {
@@ -1013,27 +1024,6 @@ const Autocomplete = (p) => {
1013
1024
  const resultsText = React__namespace.useMemo(() => {
1014
1025
  return `${getText("Showing")} ${displayOptions.length.toLocaleString()} ${getText("of")} ${p.options.length.toLocaleString()} ${getText("results")}.`;
1015
1026
  }, [language, displayOptions, p.options]);
1016
- const getNextTabElement = (fromIndex, direction) => {
1017
- var _a, _b, _c;
1018
- if (fromIndex === -1) {
1019
- let buttonIndex = 0;
1020
- if (direction === -1) {
1021
- buttonIndex = displayOptions.length - 1;
1022
- }
1023
- setSelectedResultIndex(buttonIndex);
1024
- return (_a = list.current) === null || _a === void 0 ? void 0 : _a.querySelector(`.${buttonMarkerClass}${buttonIndex}`);
1025
- }
1026
- else {
1027
- const nextIndex = fromIndex + direction;
1028
- setSelectedResultIndex(nextIndex);
1029
- if (nextIndex >= displayOptions.length || nextIndex < 0) {
1030
- return (_b = input.current) !== null && _b !== void 0 ? _b : undefined;
1031
- }
1032
- else {
1033
- return (_c = list.current) === null || _c === void 0 ? void 0 : _c.querySelector(`.${buttonMarkerClass}${nextIndex}`);
1034
- }
1035
- }
1036
- };
1037
1027
  React__namespace.useEffect(() => {
1038
1028
  const clearItems = () => {
1039
1029
  if (p.options.length) {
@@ -1049,57 +1039,118 @@ const Autocomplete = (p) => {
1049
1039
  if (p.round || theme.controls.borderRadius) {
1050
1040
  listBorderRadius = theme.controls.borderRadius || '0.5rem';
1051
1041
  }
1052
- const id = (_b = p.id) !== null && _b !== void 0 ? _b : `${Date.now()}-${Math.random().toString(36).substring(2, 9)}`;
1053
- const onPickValue = (v) => {
1042
+ const onPickValue = (v, keepFocus = true) => {
1054
1043
  var _a;
1055
1044
  // the TextInput will not respond to outer value changes if it has focus.
1056
1045
  // here we clear first and then onPickValue will re-focus after all updates.
1057
1046
  (_a = input.current) === null || _a === void 0 ? void 0 : _a.blur();
1058
1047
  setTimeout(() => {
1059
- // blur is now complete
1060
1048
  var _a;
1049
+ // blur is now complete
1061
1050
  let index = v ? p.options.findIndex(o => o === v) : undefined;
1062
1051
  if (index !== undefined && index < 0) {
1063
1052
  index = undefined;
1064
1053
  }
1065
1054
  p.onPick(v, index);
1066
1055
  // wait for the re-render. the value will not update if the control has focus
1067
- setTimeout(() => {
1068
- var _a;
1069
- (_a = input.current) === null || _a === void 0 ? void 0 : _a.focus();
1070
- }, (_a = p.onPickFocusWaitMs) !== null && _a !== void 0 ? _a : defaultOnPickFocusMs);
1056
+ if (keepFocus) {
1057
+ setTimeout(() => {
1058
+ var _a;
1059
+ (_a = input.current) === null || _a === void 0 ? void 0 : _a.focus();
1060
+ }, (_a = p.onPickFocusWaitMs) !== null && _a !== void 0 ? _a : defaultOnPickFocusMs);
1061
+ }
1071
1062
  }, 0);
1072
1063
  };
1064
+ React__namespace.useEffect(() => {
1065
+ if (selectedResultIndex === -1) {
1066
+ return;
1067
+ }
1068
+ const element = document.getElementById(getOptionId(p.id, selectedResultIndex));
1069
+ if (element) {
1070
+ element.scrollIntoView();
1071
+ }
1072
+ }, [selectedResultIndex]);
1073
+ const popupId = `${p.id}_ul`;
1074
+ const listItemStyles = css.css({
1075
+ paddingLeft: theme.controls.padding,
1076
+ paddingRight: theme.controls.padding,
1077
+ backgroundColor: 'white',
1078
+ cursor: 'pointer',
1079
+ color: theme.colors.font,
1080
+ height: theme.controls.height,
1081
+ transition: theme.controls.transition,
1082
+ fontsize: '1rem',
1083
+ fontWeight: 'bold',
1084
+ ':hover': {
1085
+ filter: theme.controls.hoverBrightness
1086
+ }
1087
+ });
1088
+ const listItemFocusStyles = css.css({
1089
+ outline: 'none',
1090
+ boxShadow: theme.controls.focusOutlineShadow,
1091
+ // prevents box shadow clipping
1092
+ position: 'relative',
1093
+ zIndex: 2
1094
+ });
1095
+ const listItemTextStyles = css.css({
1096
+ lineHeight: theme.controls.height
1097
+ });
1073
1098
  return (React__namespace.createElement("div", { onClick: e => {
1074
1099
  e.stopPropagation();
1075
1100
  }, onKeyDown: e => {
1076
1101
  if (e.key === 'Escape') {
1077
1102
  onPickValue(undefined);
1103
+ setSelectedResultIndex(-1);
1078
1104
  }
1079
- }, ref: element, className: css.cx(css.css({
1080
- position: 'relative',
1081
- width: '100%',
1082
- label: 'Autocomplete'
1083
- }), p.className, 'autocomplete') },
1105
+ }, ref: element, className: css.cx(styles.autocomplete, p.className, 'autocomplete') },
1084
1106
  React__namespace.createElement(TabLocker, { disabled: !displayOptions.length, style: { position: 'relative' } },
1085
1107
  React__namespace.createElement(TextInput, Object.assign({}, inputProps, { showErrorDisplay: false, ref: input, value: p.value, className: p.inputClassName, wrapperClassName: p.inputWrapperClassName, onKeyDown: e => {
1086
- var _a, _b, _c;
1087
- if (displayOptions.length) {
1088
- if (e.key === 'ArrowDown') {
1089
- e.preventDefault();
1090
- e.stopPropagation();
1091
- (_a = getNextTabElement(-1, 1)) === null || _a === void 0 ? void 0 : _a.focus();
1108
+ let handled = false;
1109
+ switch (e.code) {
1110
+ case 'ArrowDown': {
1111
+ if (displayOptions.length) {
1112
+ let nextIndex = selectedResultIndex + 1;
1113
+ if (nextIndex >= displayOptions.length) {
1114
+ nextIndex = 0;
1115
+ }
1116
+ setSelectedResultIndex(nextIndex);
1117
+ handled = true;
1118
+ }
1119
+ break;
1092
1120
  }
1093
- else if (e.key === 'ArrowUp') {
1094
- e.preventDefault();
1095
- e.stopPropagation();
1096
- (_b = getNextTabElement(-1, -1)) === null || _b === void 0 ? void 0 : _b.focus();
1121
+ case 'ArrowUp': {
1122
+ if (displayOptions.length) {
1123
+ let nextIndex = selectedResultIndex - 1;
1124
+ if (nextIndex < 0) {
1125
+ nextIndex = displayOptions.length - 1;
1126
+ }
1127
+ setSelectedResultIndex(nextIndex);
1128
+ handled = true;
1129
+ }
1130
+ break;
1131
+ }
1132
+ case 'Enter': {
1133
+ let pickedValue = p.value;
1134
+ if (selectedResultIndex >= 0) {
1135
+ pickedValue = displayOptions[selectedResultIndex];
1136
+ }
1137
+ onPickValue(pickedValue);
1138
+ setSelectedResultIndex(-1);
1139
+ handled = true;
1140
+ break;
1097
1141
  }
1142
+ case 'Tab': {
1143
+ onPickValue(p.value, false);
1144
+ setSelectedResultIndex(-1);
1145
+ break;
1146
+ }
1147
+ }
1148
+ if (handled) {
1149
+ e.preventDefault();
1150
+ e.stopPropagation();
1098
1151
  }
1099
- (_c = p.onKeyDown) === null || _c === void 0 ? void 0 : _c.call(p, e);
1100
- }, "aria-owns": id, "aria-expanded": !!displayOptions.length, "aria-autocomplete": "both", "aria-describedby": `${id}-aria-description` })),
1101
- React__namespace.createElement("span", { id: `${id}-aria-description`, className: css.css({ display: "none" }) }, "When autocomplete results are available use up and down arrows to review and enter to select."),
1102
- !!displayOptions.length && (React__namespace.createElement(List, { id: id, ref: list, role: "listbox", className: css.cx(css.css({
1152
+ }, role: "combobox", "aria-expanded": !!displayOptions.length, "aria-controls": popupId, "aria-autocomplete": "list", "aria-activedescendant": selectedResultIndex >= 0 ? getOptionId(p.id, selectedResultIndex) : undefined, "aria-label": p.ariaLabel })),
1153
+ !!displayOptions.length && (React__namespace.createElement(List, { id: popupId, ref: list, role: "listbox", "aria-label": p.ariaLabel, className: css.cx(css.css({
1103
1154
  position: 'absolute',
1104
1155
  width: '100%',
1105
1156
  border: theme.controls.border,
@@ -1108,11 +1159,11 @@ const Autocomplete = (p) => {
1108
1159
  backgroundColor: theme.colors.bg,
1109
1160
  marginTop: `-4px !important`,
1110
1161
  zIndex: theme.zIndexes.backdrop,
1111
- 'li:first-child button': {
1162
+ 'li:first-child': {
1112
1163
  borderTopRightRadius: listBorderRadius,
1113
1164
  borderTopLeftRadius: listBorderRadius,
1114
1165
  },
1115
- 'li:last-child button': {
1166
+ 'li:last-child': {
1116
1167
  borderBottomRightRadius: listBorderRadius,
1117
1168
  borderBottomLeftRadius: listBorderRadius,
1118
1169
  }
@@ -1121,36 +1172,23 @@ const Autocomplete = (p) => {
1121
1172
  maxHeight: `calc(${theme.controls.height} * ${maxShowValues})`
1122
1173
  }), p.listClassName) },
1123
1174
  displayOptions.map((v, listItemIndex) => {
1124
- var _a;
1125
- return (React__namespace.createElement(ListItem, { key: v, variant: "full", className: p.listItemClassName, role: "option", "aria-selected": selectedResultIndex === listItemIndex },
1126
- React__namespace.createElement(Button, { title: ((_a = p.showOptionTextAsTitle) !== null && _a !== void 0 ? _a : true) ? v : undefined, onKeyDown: e => {
1127
- var _a, _b;
1128
- if (e.key === 'ArrowDown') {
1129
- e.stopPropagation();
1130
- e.preventDefault();
1131
- (_a = getNextTabElement(listItemIndex, 1)) === null || _a === void 0 ? void 0 : _a.focus();
1132
- }
1133
- else if (e.key === 'ArrowUp') {
1134
- e.stopPropagation();
1135
- e.preventDefault();
1136
- (_b = getNextTabElement(listItemIndex, -1)) === null || _b === void 0 ? void 0 : _b.focus();
1137
- }
1138
- else if (e.key === 'Enter') {
1139
- e.stopPropagation();
1140
- // this will prevent the click event from firing in addition to this enter key event.
1141
- e.preventDefault();
1142
- onPickValue(v);
1143
- }
1144
- }, className: css.cx(buttonMarkerClass + listItemIndex, css.css({
1145
- borderRadius: 0,
1146
- }), p.listItemButtonClassName), onClick: () => {
1147
- onPickValue(v);
1148
- } },
1149
- React__namespace.createElement(Text, { tag: "div", ellipsis: true, align: "left" }, v))));
1175
+ const selected = selectedResultIndex === listItemIndex;
1176
+ return (React__namespace.createElement(ListItem, { key: v, variant: "full", className: css.cx(listItemStyles, selected ? listItemFocusStyles : undefined, p.listItemClassName), role: "option", id: getOptionId(p.id, listItemIndex), "aria-selected": selected },
1177
+ React__namespace.createElement(Text, { tag: "div", ellipsis: true, align: "left", className: listItemTextStyles }, v)));
1150
1178
  }),
1151
1179
  !p.allowScroll && displayOptions.length < p.options.length && (React__namespace.createElement(ListItem, { className: p.listItemClassName },
1152
1180
  React__namespace.createElement(Text, { tag: "div", italics: true, align: "center" }, resultsText))))))));
1153
1181
  };
1182
+ function getOptionId(baseId, optionIndex) {
1183
+ return `${baseId}_li_${optionIndex}`;
1184
+ }
1185
+ const styles = {
1186
+ autocomplete: css.css({
1187
+ position: 'relative',
1188
+ width: '100%',
1189
+ label: 'Autocomplete'
1190
+ }),
1191
+ };
1154
1192
 
1155
1193
  /** Returns a UID. Use this instead of a direct call to a library. */
1156
1194
  function createUid() {
@@ -1511,7 +1549,7 @@ const Calendar = (p) => {
1511
1549
 
1512
1550
  const Checkbox = (props) => {
1513
1551
  var _a;
1514
- const { onChange, label, checkedIcon, uncheckedIcon, checkedThemeColor, checkedColor, readOnly } = props, inputProps = __rest(props, ["onChange", "label", "checkedIcon", "uncheckedIcon", "checkedThemeColor", "checkedColor", "readOnly"]);
1552
+ const { onChange, label, checkedIcon, uncheckedIcon, checkedThemeColor, checkedColor, readOnly, hideLabel } = props, inputProps = __rest(props, ["onChange", "label", "checkedIcon", "uncheckedIcon", "checkedThemeColor", "checkedColor", "readOnly", "hideLabel"]);
1515
1553
  const selected = checkedIcon || 'selected';
1516
1554
  const unselected = uncheckedIcon || 'unselected';
1517
1555
  const theme = useThemeSafely();
@@ -1575,7 +1613,7 @@ const Checkbox = (props) => {
1575
1613
  `;
1576
1614
  return (React__namespace.createElement("span", { className: css.cx('checkbox', checkboxStyles, props.className) },
1577
1615
  React__namespace.createElement("label", { className: labelStyles },
1578
- React__namespace.createElement("input", Object.assign({}, inputProps, { tabIndex: readOnly ? -1 : (_a = props.tabIndex) !== null && _a !== void 0 ? _a : undefined, className: nativeCheckboxStyles, type: "checkbox", onChange: e => {
1616
+ React__namespace.createElement("input", Object.assign({}, inputProps, { "aria-label": hideLabel ? label : undefined, tabIndex: readOnly ? -1 : (_a = props.tabIndex) !== null && _a !== void 0 ? _a : undefined, className: nativeCheckboxStyles, type: "checkbox", onChange: e => {
1579
1617
  if (readOnly) {
1580
1618
  e.preventDefault();
1581
1619
  return;
@@ -1583,7 +1621,7 @@ const Checkbox = (props) => {
1583
1621
  return onChange(e.currentTarget.checked, e);
1584
1622
  } })),
1585
1623
  React__namespace.createElement(Icon, { className: css.cx('checkboxIcon', iconStyles), id: props.checked ? selected : unselected }),
1586
- label,
1624
+ !hideLabel && label,
1587
1625
  props.children)));
1588
1626
  };
1589
1627
 
@@ -2878,7 +2916,6 @@ const LinkContent = (props) => {
2878
2916
  }) }, props.rightIcon)));
2879
2917
  };
2880
2918
 
2881
- //TB: FUTURE de-dup these styles. create individual styles and compose them manually.
2882
2919
  const generateLinkStyles = (props, theme) => {
2883
2920
  const disabled = props.disabled || props.waiting;
2884
2921
  let color = props.colorOverride;
@@ -4247,7 +4284,6 @@ const TextArea = React__namespace.forwardRef((props, ref) => {
4247
4284
  }
4248
4285
  else {
4249
4286
  if (reportValueOnError) {
4250
- //TB: temp, add a custom list of validators that will be run for all inputs if a pattern cannot be decided.
4251
4287
  onValueChange(localValue);
4252
4288
  }
4253
4289
  else {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mackin.com/styleguide",
3
- "version": "10.2.6",
3
+ "version": "11.0.1",
4
4
  "description": "",
5
5
  "main": "./index.js",
6
6
  "module": "./index.esm.js",