@mackin.com/styleguide 10.2.4 → 11.0.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 (4) hide show
  1. package/index.d.ts +11 -5
  2. package/index.esm.js +124 -124
  3. package/index.js +124 -124
  4. package/package.json +1 -1
package/index.d.ts CHANGED
@@ -33,8 +33,12 @@ interface InputOnFocusProps {
33
33
  allowUpdateOnFocus?: boolean;
34
34
  }
35
35
 
36
- type BaseInputProps$1 = Omit<TextInputProps, 'value' | 'className' | 'wrapperClassName' | 'type' | 'showErrorDisplay'> & InputOnFocusProps;
36
+ type BaseInputProps$1 = Omit<TextInputProps, 'value' | 'className' | 'wrapperClassName' | 'type' | 'showErrorDisplay' | 'id' | 'aria-label'> & InputOnFocusProps;
37
37
  interface AutocompleteProps extends BaseInputProps$1 {
38
+ /** Required for ARIA. Will be used to add unique IDs to the options. */
39
+ id: string;
40
+ /** Required for ARIA. Will be applied to both the input and the popup listbox. */
41
+ ariaLabel: string;
38
42
  value: string | undefined;
39
43
  options: string[];
40
44
  /** Applied to the Autocomplete wrapper. */
@@ -43,15 +47,12 @@ interface AutocompleteProps extends BaseInputProps$1 {
43
47
  inputClassName?: string;
44
48
  listClassName?: string;
45
49
  listItemClassName?: string;
46
- listItemButtonClassName?: string;
47
50
  /** Limits what will be show in the autocomplete options. Default is 7. */
48
51
  maxShownValues?: number;
49
52
  /** Will enable scrolling in the results list. */
50
53
  allowScroll?: boolean;
51
54
  /** 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
55
  onPickFocusWaitMs?: number;
53
- /** The option `title` attribute will be filled with the option text. Defaults to `true`. */
54
- showOptionTextAsTitle?: boolean;
55
56
  onPick: (value: string | undefined, index?: number) => void;
56
57
  }
57
58
  declare const Autocomplete: (p: AutocompleteProps) => React.JSX.Element;
@@ -130,16 +131,20 @@ interface CalendarProps {
130
131
  declare const Calendar: (p: CalendarProps) => React.JSX.Element;
131
132
 
132
133
  interface CheckboxProps extends Omit<React.DetailedHTMLProps<React.InputHTMLAttributes<HTMLInputElement>, HTMLInputElement>, 'checked' | 'onChange' | 'type'> {
134
+ /** Required for the internal label (ARIA). Pass `true` to `hideLabel` to treat this label as an aria-label. */
135
+ label: string;
133
136
  checked: boolean;
134
137
  onChange: (checked: boolean, event: React.ChangeEvent<HTMLInputElement>) => void;
135
138
  readOnly?: boolean;
136
- label?: string;
137
139
  checkedIcon?: string;
138
140
  uncheckedIcon?: string;
139
141
  /** Background color when checked based on the current theme. Mutually exclusive with 'checkedColor'. */
140
142
  checkedThemeColor?: 'primary' | 'primary2' | 'secondary';
141
143
  /** Background color when checked. Mutually exclusive with 'checkedThemeColor'. */
142
144
  checkedColor?: string;
145
+ tabIndex?: number | undefined;
146
+ /** Pass `true` to `hideLabel` to treat `label` as an aria-label. */
147
+ hideLabel?: boolean;
143
148
  }
144
149
  declare const Checkbox: (props: CheckboxProps) => React.JSX.Element;
145
150
 
@@ -865,6 +870,7 @@ interface SliderProps<T extends SliderValue> {
865
870
  sliderTextClassName?: string;
866
871
  /** Sets the aria-label value. */
867
872
  ariaLabel?: (T extends number ? string : readonly string[] | undefined) | undefined;
873
+ tabIndex?: number | undefined;
868
874
  }
869
875
  declare const Slider: <T extends SliderValue>(p: SliderProps<T>) => React__default.JSX.Element;
870
876
 
package/index.esm.js CHANGED
@@ -497,45 +497,6 @@ const Button = React.forwardRef((props, ref) => {
497
497
  return content;
498
498
  });
499
499
 
500
- /* Type is not always determinable due to the nature of custom components in typescript. For example OmniLink will not have a type of "a" but rather "OmniLink" so we have to determine
501
- if the child component is focusable based on certain properties on the control that are found on focusable components */
502
- const isChildFocusable = (props, type) => {
503
- if (props.tabIndex !== undefined && props.tabIndex !== null) {
504
- return true;
505
- }
506
- if (props.onClick !== undefined || props.onValueChange !== undefined) { //button or select
507
- return true;
508
- }
509
- if (props.href || props.cols || props.rows || props.maxLength) {
510
- return true;
511
- }
512
- if ((type === 'button' || type === 'input' || type === 'select' || type === 'textarea') && !props.disabled) {
513
- return true;
514
- }
515
- return false;
516
- };
517
- const TabIndexContainer = (props) => {
518
- const processElement = (node) => {
519
- if (!React__default.isValidElement(node)) {
520
- return node;
521
- }
522
- let updatedNode = node;
523
- // Use the props-based logic to check focusability
524
- if (isChildFocusable(node.props, node.type)) {
525
- updatedNode = React__default.cloneElement(node, { tabIndex: props.tabIndexValue });
526
- }
527
- // Recursively process children
528
- if (updatedNode.props.children) {
529
- const clonedChildren = React__default.Children.map(updatedNode.props.children, processElement);
530
- updatedNode = React__default.cloneElement(updatedNode, {
531
- children: clonedChildren,
532
- });
533
- }
534
- return updatedNode;
535
- };
536
- return (React__default.createElement(React__default.Fragment, null, React__default.Children.map(props.children, processElement)));
537
- };
538
-
539
500
  const accordianExpandTimeMs = 250;
540
501
  const accordianMaxHeight = 1020;
541
502
  const accordianTimingFunction = 'ease-in-out';
@@ -549,6 +510,7 @@ const Accordian = (props) => {
549
510
  const [open, setOpen] = React.useState(false);
550
511
  const theme = useThemeSafely();
551
512
  const content = React.useRef(null);
513
+ const [children, setChildren] = React.useState();
552
514
  const contentStyles = css({
553
515
  overflow: 'hidden',
554
516
  maxHeight: 0,
@@ -564,6 +526,7 @@ const Accordian = (props) => {
564
526
  const currentContent = content.current;
565
527
  if (currentContent) {
566
528
  if (open) {
529
+ setChildren(props.children);
567
530
  currentContent.classList.add(expandedContentStyles);
568
531
  window.setTimeout(() => {
569
532
  currentContent.classList.add(visibleStyle);
@@ -571,6 +534,11 @@ const Accordian = (props) => {
571
534
  }
572
535
  else {
573
536
  currentContent.classList.remove(visibleStyle, expandedContentStyles);
537
+ if (children !== undefined) {
538
+ window.setTimeout(() => {
539
+ setChildren(undefined);
540
+ }, accordianExpandTimeMs);
541
+ }
574
542
  }
575
543
  }
576
544
  }, [open]);
@@ -602,8 +570,7 @@ const Accordian = (props) => {
602
570
  }, rightIcon: !props.disabled ? React.createElement(Icon, { id: open ? 'collapse' : 'expand' }) : undefined },
603
571
  React.createElement("span", null, props.header)),
604
572
  React.createElement("div", { ref: content, className: cx('accordian__body', contentStyles) },
605
- React.createElement("div", { className: expandedContentWrapperStyles },
606
- React.createElement(TabIndexContainer, { tabIndexValue: open ? 0 : -1 }, props.children)))));
573
+ React.createElement("div", { className: expandedContentWrapperStyles }, children))));
607
574
  };
608
575
  const useAccordianState = (count, openIndex) => {
609
576
  const [panels, setShowPanel] = React.useState(new Array(count).fill(false).map((b, i) => {
@@ -1005,18 +972,23 @@ const TabLocker = (props) => {
1005
972
  } }, props.children));
1006
973
  };
1007
974
 
975
+ /*
976
+ ARIA info:
977
+ https://www.w3.org/WAI/ARIA/apg/patterns/combobox/
978
+ https://www.w3.org/WAI/ARIA/apg/patterns/combobox/examples/combobox-autocomplete-list/
979
+ This would be considered "List autocomplete with manual selection"
980
+ */
1008
981
  const defaultMaxShownValues = 7;
1009
- const buttonMarkerClass = 'ListItem__button';
1010
982
  const defaultOnPickFocusMs = 100;
1011
983
  const Autocomplete = (p) => {
1012
- var _a, _b;
984
+ var _a;
1013
985
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
1014
- const inputProps = __rest(p, ["value", "className", "inputWrapperClassName", "inputClassName", "listClassName", "listItemClassName", "listItemButtonClassName", "maxShownValues", "allowScroll", "options", "onPick", "onPickFocusWaitMs"]);
986
+ const inputProps = __rest(p, ["value", "className", "inputWrapperClassName", "inputClassName", "listClassName", "listItemClassName", "maxShownValues", "allowScroll", "options", "onPick", "onPickFocusWaitMs"]);
1015
987
  const theme = useThemeSafely();
1016
988
  const element = React.useRef(null);
1017
989
  const input = React.useRef(null);
1018
990
  const list = React.useRef(null);
1019
- const [selectedResultIndex, setSelectedResultIndex] = React.useState();
991
+ const [selectedResultIndex, setSelectedResultIndex] = React.useState(-1);
1020
992
  const maxShowValues = (_a = p.maxShownValues) !== null && _a !== void 0 ? _a : defaultMaxShownValues;
1021
993
  const displayOptions = React.useMemo(() => {
1022
994
  if (!p.allowScroll) {
@@ -1028,27 +1000,6 @@ const Autocomplete = (p) => {
1028
1000
  const resultsText = React.useMemo(() => {
1029
1001
  return `${getText("Showing")} ${displayOptions.length.toLocaleString()} ${getText("of")} ${p.options.length.toLocaleString()} ${getText("results")}.`;
1030
1002
  }, [language, displayOptions, p.options]);
1031
- const getNextTabElement = (fromIndex, direction) => {
1032
- var _a, _b, _c;
1033
- if (fromIndex === -1) {
1034
- let buttonIndex = 0;
1035
- if (direction === -1) {
1036
- buttonIndex = displayOptions.length - 1;
1037
- }
1038
- setSelectedResultIndex(buttonIndex);
1039
- return (_a = list.current) === null || _a === void 0 ? void 0 : _a.querySelector(`.${buttonMarkerClass}${buttonIndex}`);
1040
- }
1041
- else {
1042
- const nextIndex = fromIndex + direction;
1043
- setSelectedResultIndex(nextIndex);
1044
- if (nextIndex >= displayOptions.length || nextIndex < 0) {
1045
- return (_b = input.current) !== null && _b !== void 0 ? _b : undefined;
1046
- }
1047
- else {
1048
- return (_c = list.current) === null || _c === void 0 ? void 0 : _c.querySelector(`.${buttonMarkerClass}${nextIndex}`);
1049
- }
1050
- }
1051
- };
1052
1003
  React.useEffect(() => {
1053
1004
  const clearItems = () => {
1054
1005
  if (p.options.length) {
@@ -1064,57 +1015,118 @@ const Autocomplete = (p) => {
1064
1015
  if (p.round || theme.controls.borderRadius) {
1065
1016
  listBorderRadius = theme.controls.borderRadius || '0.5rem';
1066
1017
  }
1067
- const id = (_b = p.id) !== null && _b !== void 0 ? _b : `${Date.now()}-${Math.random().toString(36).substring(2, 9)}`;
1068
- const onPickValue = (v) => {
1018
+ const onPickValue = (v, keepFocus = true) => {
1069
1019
  var _a;
1070
1020
  // the TextInput will not respond to outer value changes if it has focus.
1071
1021
  // here we clear first and then onPickValue will re-focus after all updates.
1072
1022
  (_a = input.current) === null || _a === void 0 ? void 0 : _a.blur();
1073
1023
  setTimeout(() => {
1074
- // blur is now complete
1075
1024
  var _a;
1025
+ // blur is now complete
1076
1026
  let index = v ? p.options.findIndex(o => o === v) : undefined;
1077
1027
  if (index !== undefined && index < 0) {
1078
1028
  index = undefined;
1079
1029
  }
1080
1030
  p.onPick(v, index);
1081
1031
  // wait for the re-render. the value will not update if the control has focus
1082
- setTimeout(() => {
1083
- var _a;
1084
- (_a = input.current) === null || _a === void 0 ? void 0 : _a.focus();
1085
- }, (_a = p.onPickFocusWaitMs) !== null && _a !== void 0 ? _a : defaultOnPickFocusMs);
1032
+ if (keepFocus) {
1033
+ setTimeout(() => {
1034
+ var _a;
1035
+ (_a = input.current) === null || _a === void 0 ? void 0 : _a.focus();
1036
+ }, (_a = p.onPickFocusWaitMs) !== null && _a !== void 0 ? _a : defaultOnPickFocusMs);
1037
+ }
1086
1038
  }, 0);
1087
1039
  };
1040
+ React.useEffect(() => {
1041
+ if (selectedResultIndex === -1) {
1042
+ return;
1043
+ }
1044
+ const element = document.getElementById(getOptionId(p.id, selectedResultIndex));
1045
+ if (element) {
1046
+ element.scrollIntoView();
1047
+ }
1048
+ }, [selectedResultIndex]);
1049
+ const popupId = `${p.id}_ul`;
1050
+ const listItemStyles = css({
1051
+ paddingLeft: theme.controls.padding,
1052
+ paddingRight: theme.controls.padding,
1053
+ backgroundColor: 'white',
1054
+ cursor: 'pointer',
1055
+ color: theme.colors.font,
1056
+ height: theme.controls.height,
1057
+ transition: theme.controls.transition,
1058
+ fontsize: '1rem',
1059
+ fontWeight: 'bold',
1060
+ ':hover': {
1061
+ filter: theme.controls.hoverBrightness
1062
+ }
1063
+ });
1064
+ const listItemFocusStyles = css({
1065
+ outline: 'none',
1066
+ boxShadow: theme.controls.focusOutlineShadow,
1067
+ // prevents box shadow clipping
1068
+ position: 'relative',
1069
+ zIndex: 2
1070
+ });
1071
+ const listItemTextStyles = css({
1072
+ lineHeight: theme.controls.height
1073
+ });
1088
1074
  return (React.createElement("div", { onClick: e => {
1089
1075
  e.stopPropagation();
1090
1076
  }, onKeyDown: e => {
1091
1077
  if (e.key === 'Escape') {
1092
1078
  onPickValue(undefined);
1079
+ setSelectedResultIndex(-1);
1093
1080
  }
1094
- }, ref: element, className: cx(css({
1095
- position: 'relative',
1096
- width: '100%',
1097
- label: 'Autocomplete'
1098
- }), p.className, 'autocomplete') },
1081
+ }, ref: element, className: cx(styles.autocomplete, p.className, 'autocomplete') },
1099
1082
  React.createElement(TabLocker, { disabled: !displayOptions.length, style: { position: 'relative' } },
1100
1083
  React.createElement(TextInput, Object.assign({}, inputProps, { showErrorDisplay: false, ref: input, value: p.value, className: p.inputClassName, wrapperClassName: p.inputWrapperClassName, onKeyDown: e => {
1101
- var _a, _b, _c;
1102
- if (displayOptions.length) {
1103
- if (e.key === 'ArrowDown') {
1104
- e.preventDefault();
1105
- e.stopPropagation();
1106
- (_a = getNextTabElement(-1, 1)) === null || _a === void 0 ? void 0 : _a.focus();
1084
+ let handled = false;
1085
+ switch (e.code) {
1086
+ case 'ArrowDown': {
1087
+ if (displayOptions.length) {
1088
+ let nextIndex = selectedResultIndex + 1;
1089
+ if (nextIndex >= displayOptions.length) {
1090
+ nextIndex = 0;
1091
+ }
1092
+ setSelectedResultIndex(nextIndex);
1093
+ handled = true;
1094
+ }
1095
+ break;
1107
1096
  }
1108
- else if (e.key === 'ArrowUp') {
1109
- e.preventDefault();
1110
- e.stopPropagation();
1111
- (_b = getNextTabElement(-1, -1)) === null || _b === void 0 ? void 0 : _b.focus();
1097
+ case 'ArrowUp': {
1098
+ if (displayOptions.length) {
1099
+ let nextIndex = selectedResultIndex - 1;
1100
+ if (nextIndex < 0) {
1101
+ nextIndex = displayOptions.length - 1;
1102
+ }
1103
+ setSelectedResultIndex(nextIndex);
1104
+ handled = true;
1105
+ }
1106
+ break;
1107
+ }
1108
+ case 'Enter': {
1109
+ let pickedValue = p.value;
1110
+ if (selectedResultIndex >= 0) {
1111
+ pickedValue = displayOptions[selectedResultIndex];
1112
+ }
1113
+ onPickValue(pickedValue);
1114
+ setSelectedResultIndex(-1);
1115
+ handled = true;
1116
+ break;
1112
1117
  }
1118
+ case 'Tab': {
1119
+ onPickValue(p.value, false);
1120
+ setSelectedResultIndex(-1);
1121
+ break;
1122
+ }
1123
+ }
1124
+ if (handled) {
1125
+ e.preventDefault();
1126
+ e.stopPropagation();
1113
1127
  }
1114
- (_c = p.onKeyDown) === null || _c === void 0 ? void 0 : _c.call(p, e);
1115
- }, "aria-owns": id, "aria-expanded": !!displayOptions.length, "aria-autocomplete": "both", "aria-describedby": `${id}-aria-description` })),
1116
- 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."),
1117
- !!displayOptions.length && (React.createElement(List, { id: id, ref: list, role: "listbox", className: cx(css({
1128
+ }, 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 })),
1129
+ !!displayOptions.length && (React.createElement(List, { id: popupId, ref: list, role: "listbox", "aria-label": p.ariaLabel, className: cx(css({
1118
1130
  position: 'absolute',
1119
1131
  width: '100%',
1120
1132
  border: theme.controls.border,
@@ -1123,11 +1135,11 @@ const Autocomplete = (p) => {
1123
1135
  backgroundColor: theme.colors.bg,
1124
1136
  marginTop: `-4px !important`,
1125
1137
  zIndex: theme.zIndexes.backdrop,
1126
- 'li:first-child button': {
1138
+ 'li:first-child': {
1127
1139
  borderTopRightRadius: listBorderRadius,
1128
1140
  borderTopLeftRadius: listBorderRadius,
1129
1141
  },
1130
- 'li:last-child button': {
1142
+ 'li:last-child': {
1131
1143
  borderBottomRightRadius: listBorderRadius,
1132
1144
  borderBottomLeftRadius: listBorderRadius,
1133
1145
  }
@@ -1136,36 +1148,23 @@ const Autocomplete = (p) => {
1136
1148
  maxHeight: `calc(${theme.controls.height} * ${maxShowValues})`
1137
1149
  }), p.listClassName) },
1138
1150
  displayOptions.map((v, listItemIndex) => {
1139
- var _a;
1140
- return (React.createElement(ListItem, { key: v, variant: "full", className: p.listItemClassName, role: "option", "aria-selected": selectedResultIndex === listItemIndex },
1141
- React.createElement(Button, { title: ((_a = p.showOptionTextAsTitle) !== null && _a !== void 0 ? _a : true) ? v : undefined, onKeyDown: e => {
1142
- var _a, _b;
1143
- if (e.key === 'ArrowDown') {
1144
- e.stopPropagation();
1145
- e.preventDefault();
1146
- (_a = getNextTabElement(listItemIndex, 1)) === null || _a === void 0 ? void 0 : _a.focus();
1147
- }
1148
- else if (e.key === 'ArrowUp') {
1149
- e.stopPropagation();
1150
- e.preventDefault();
1151
- (_b = getNextTabElement(listItemIndex, -1)) === null || _b === void 0 ? void 0 : _b.focus();
1152
- }
1153
- else if (e.key === 'Enter') {
1154
- e.stopPropagation();
1155
- // this will prevent the click event from firing in addition to this enter key event.
1156
- e.preventDefault();
1157
- onPickValue(v);
1158
- }
1159
- }, className: cx(buttonMarkerClass + listItemIndex, css({
1160
- borderRadius: 0,
1161
- }), p.listItemButtonClassName), onClick: () => {
1162
- onPickValue(v);
1163
- } },
1164
- React.createElement(Text, { tag: "div", ellipsis: true, align: "left" }, v))));
1151
+ const selected = selectedResultIndex === listItemIndex;
1152
+ 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 },
1153
+ React.createElement(Text, { tag: "div", ellipsis: true, align: "left", className: listItemTextStyles }, v)));
1165
1154
  }),
1166
1155
  !p.allowScroll && displayOptions.length < p.options.length && (React.createElement(ListItem, { className: p.listItemClassName },
1167
1156
  React.createElement(Text, { tag: "div", italics: true, align: "center" }, resultsText))))))));
1168
1157
  };
1158
+ function getOptionId(baseId, optionIndex) {
1159
+ return `${baseId}_li_${optionIndex}`;
1160
+ }
1161
+ const styles = {
1162
+ autocomplete: css({
1163
+ position: 'relative',
1164
+ width: '100%',
1165
+ label: 'Autocomplete'
1166
+ }),
1167
+ };
1169
1168
 
1170
1169
  /** Returns a UID. Use this instead of a direct call to a library. */
1171
1170
  function createUid() {
@@ -1525,7 +1524,8 @@ const Calendar = (p) => {
1525
1524
  };
1526
1525
 
1527
1526
  const Checkbox = (props) => {
1528
- const { onChange, label, checkedIcon, uncheckedIcon, checkedThemeColor, checkedColor, readOnly } = props, inputProps = __rest(props, ["onChange", "label", "checkedIcon", "uncheckedIcon", "checkedThemeColor", "checkedColor", "readOnly"]);
1527
+ var _a;
1528
+ const { onChange, label, checkedIcon, uncheckedIcon, checkedThemeColor, checkedColor, readOnly, hideLabel } = props, inputProps = __rest(props, ["onChange", "label", "checkedIcon", "uncheckedIcon", "checkedThemeColor", "checkedColor", "readOnly", "hideLabel"]);
1529
1529
  const selected = checkedIcon || 'selected';
1530
1530
  const unselected = uncheckedIcon || 'unselected';
1531
1531
  const theme = useThemeSafely();
@@ -1589,7 +1589,7 @@ const Checkbox = (props) => {
1589
1589
  `;
1590
1590
  return (React.createElement("span", { className: cx('checkbox', checkboxStyles, props.className) },
1591
1591
  React.createElement("label", { className: labelStyles },
1592
- React.createElement("input", Object.assign({}, inputProps, { tabIndex: readOnly ? -1 : undefined, className: nativeCheckboxStyles, type: "checkbox", onChange: e => {
1592
+ 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 => {
1593
1593
  if (readOnly) {
1594
1594
  e.preventDefault();
1595
1595
  return;
@@ -1597,7 +1597,7 @@ const Checkbox = (props) => {
1597
1597
  return onChange(e.currentTarget.checked, e);
1598
1598
  } })),
1599
1599
  React.createElement(Icon, { className: cx('checkboxIcon', iconStyles), id: props.checked ? selected : unselected }),
1600
- label,
1600
+ !hideLabel && label,
1601
1601
  props.children)));
1602
1602
  };
1603
1603
 
@@ -3984,7 +3984,7 @@ const Slider = (p) => {
3984
3984
  }), specificThumbStyles, p.handleClassName, css({
3985
3985
  width: sliderHandleSize,
3986
3986
  height: sliderHandleSize,
3987
- })) }, rest), p.showValue && (React__default.createElement(HandleText, { sliderHandleSize: sliderHandleSize, className: p.sliderTextClassName, index: state.index, parentElement: sliderContainer.current, value: Array.isArray(currentValue.current) ? currentValue.current[state.index] : currentValue.current, renderValue: p.renderValue, renderValueWidth: p.renderValueWidth }))));
3987
+ })) }, rest, { tabIndex: p.tabIndex }), p.showValue && (React__default.createElement(HandleText, { sliderHandleSize: sliderHandleSize, className: p.sliderTextClassName, index: state.index, parentElement: sliderContainer.current, value: Array.isArray(currentValue.current) ? currentValue.current[state.index] : currentValue.current, renderValue: p.renderValue, renderValueWidth: p.renderValueWidth }))));
3988
3988
  } })));
3989
3989
  };
3990
3990
  const rectsCollideX = (r1, r2) => {
package/index.js CHANGED
@@ -515,45 +515,6 @@ const Button = React__namespace.forwardRef((props, ref) => {
515
515
  return content;
516
516
  });
517
517
 
518
- /* Type is not always determinable due to the nature of custom components in typescript. For example OmniLink will not have a type of "a" but rather "OmniLink" so we have to determine
519
- if the child component is focusable based on certain properties on the control that are found on focusable components */
520
- const isChildFocusable = (props, type) => {
521
- if (props.tabIndex !== undefined && props.tabIndex !== null) {
522
- return true;
523
- }
524
- if (props.onClick !== undefined || props.onValueChange !== undefined) { //button or select
525
- return true;
526
- }
527
- if (props.href || props.cols || props.rows || props.maxLength) {
528
- return true;
529
- }
530
- if ((type === 'button' || type === 'input' || type === 'select' || type === 'textarea') && !props.disabled) {
531
- return true;
532
- }
533
- return false;
534
- };
535
- const TabIndexContainer = (props) => {
536
- const processElement = (node) => {
537
- if (!React.isValidElement(node)) {
538
- return node;
539
- }
540
- let updatedNode = node;
541
- // Use the props-based logic to check focusability
542
- if (isChildFocusable(node.props, node.type)) {
543
- updatedNode = React.cloneElement(node, { tabIndex: props.tabIndexValue });
544
- }
545
- // Recursively process children
546
- if (updatedNode.props.children) {
547
- const clonedChildren = React.Children.map(updatedNode.props.children, processElement);
548
- updatedNode = React.cloneElement(updatedNode, {
549
- children: clonedChildren,
550
- });
551
- }
552
- return updatedNode;
553
- };
554
- return (React.createElement(React.Fragment, null, React.Children.map(props.children, processElement)));
555
- };
556
-
557
518
  const accordianExpandTimeMs = 250;
558
519
  const accordianMaxHeight = 1020;
559
520
  const accordianTimingFunction = 'ease-in-out';
@@ -567,6 +528,7 @@ const Accordian = (props) => {
567
528
  const [open, setOpen] = React__namespace.useState(false);
568
529
  const theme = useThemeSafely();
569
530
  const content = React__namespace.useRef(null);
531
+ const [children, setChildren] = React__namespace.useState();
570
532
  const contentStyles = css.css({
571
533
  overflow: 'hidden',
572
534
  maxHeight: 0,
@@ -582,6 +544,7 @@ const Accordian = (props) => {
582
544
  const currentContent = content.current;
583
545
  if (currentContent) {
584
546
  if (open) {
547
+ setChildren(props.children);
585
548
  currentContent.classList.add(expandedContentStyles);
586
549
  window.setTimeout(() => {
587
550
  currentContent.classList.add(visibleStyle);
@@ -589,6 +552,11 @@ const Accordian = (props) => {
589
552
  }
590
553
  else {
591
554
  currentContent.classList.remove(visibleStyle, expandedContentStyles);
555
+ if (children !== undefined) {
556
+ window.setTimeout(() => {
557
+ setChildren(undefined);
558
+ }, accordianExpandTimeMs);
559
+ }
592
560
  }
593
561
  }
594
562
  }, [open]);
@@ -620,8 +588,7 @@ const Accordian = (props) => {
620
588
  }, rightIcon: !props.disabled ? React__namespace.createElement(Icon, { id: open ? 'collapse' : 'expand' }) : undefined },
621
589
  React__namespace.createElement("span", null, props.header)),
622
590
  React__namespace.createElement("div", { ref: content, className: css.cx('accordian__body', contentStyles) },
623
- React__namespace.createElement("div", { className: expandedContentWrapperStyles },
624
- React__namespace.createElement(TabIndexContainer, { tabIndexValue: open ? 0 : -1 }, props.children)))));
591
+ React__namespace.createElement("div", { className: expandedContentWrapperStyles }, children))));
625
592
  };
626
593
  const useAccordianState = (count, openIndex) => {
627
594
  const [panels, setShowPanel] = React__namespace.useState(new Array(count).fill(false).map((b, i) => {
@@ -1023,18 +990,23 @@ const TabLocker = (props) => {
1023
990
  } }, props.children));
1024
991
  };
1025
992
 
993
+ /*
994
+ ARIA info:
995
+ https://www.w3.org/WAI/ARIA/apg/patterns/combobox/
996
+ https://www.w3.org/WAI/ARIA/apg/patterns/combobox/examples/combobox-autocomplete-list/
997
+ This would be considered "List autocomplete with manual selection"
998
+ */
1026
999
  const defaultMaxShownValues = 7;
1027
- const buttonMarkerClass = 'ListItem__button';
1028
1000
  const defaultOnPickFocusMs = 100;
1029
1001
  const Autocomplete = (p) => {
1030
- var _a, _b;
1002
+ var _a;
1031
1003
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
1032
- const inputProps = __rest(p, ["value", "className", "inputWrapperClassName", "inputClassName", "listClassName", "listItemClassName", "listItemButtonClassName", "maxShownValues", "allowScroll", "options", "onPick", "onPickFocusWaitMs"]);
1004
+ const inputProps = __rest(p, ["value", "className", "inputWrapperClassName", "inputClassName", "listClassName", "listItemClassName", "maxShownValues", "allowScroll", "options", "onPick", "onPickFocusWaitMs"]);
1033
1005
  const theme = useThemeSafely();
1034
1006
  const element = React__namespace.useRef(null);
1035
1007
  const input = React__namespace.useRef(null);
1036
1008
  const list = React__namespace.useRef(null);
1037
- const [selectedResultIndex, setSelectedResultIndex] = React__namespace.useState();
1009
+ const [selectedResultIndex, setSelectedResultIndex] = React__namespace.useState(-1);
1038
1010
  const maxShowValues = (_a = p.maxShownValues) !== null && _a !== void 0 ? _a : defaultMaxShownValues;
1039
1011
  const displayOptions = React__namespace.useMemo(() => {
1040
1012
  if (!p.allowScroll) {
@@ -1046,27 +1018,6 @@ const Autocomplete = (p) => {
1046
1018
  const resultsText = React__namespace.useMemo(() => {
1047
1019
  return `${getText("Showing")} ${displayOptions.length.toLocaleString()} ${getText("of")} ${p.options.length.toLocaleString()} ${getText("results")}.`;
1048
1020
  }, [language, displayOptions, p.options]);
1049
- const getNextTabElement = (fromIndex, direction) => {
1050
- var _a, _b, _c;
1051
- if (fromIndex === -1) {
1052
- let buttonIndex = 0;
1053
- if (direction === -1) {
1054
- buttonIndex = displayOptions.length - 1;
1055
- }
1056
- setSelectedResultIndex(buttonIndex);
1057
- return (_a = list.current) === null || _a === void 0 ? void 0 : _a.querySelector(`.${buttonMarkerClass}${buttonIndex}`);
1058
- }
1059
- else {
1060
- const nextIndex = fromIndex + direction;
1061
- setSelectedResultIndex(nextIndex);
1062
- if (nextIndex >= displayOptions.length || nextIndex < 0) {
1063
- return (_b = input.current) !== null && _b !== void 0 ? _b : undefined;
1064
- }
1065
- else {
1066
- return (_c = list.current) === null || _c === void 0 ? void 0 : _c.querySelector(`.${buttonMarkerClass}${nextIndex}`);
1067
- }
1068
- }
1069
- };
1070
1021
  React__namespace.useEffect(() => {
1071
1022
  const clearItems = () => {
1072
1023
  if (p.options.length) {
@@ -1082,57 +1033,118 @@ const Autocomplete = (p) => {
1082
1033
  if (p.round || theme.controls.borderRadius) {
1083
1034
  listBorderRadius = theme.controls.borderRadius || '0.5rem';
1084
1035
  }
1085
- const id = (_b = p.id) !== null && _b !== void 0 ? _b : `${Date.now()}-${Math.random().toString(36).substring(2, 9)}`;
1086
- const onPickValue = (v) => {
1036
+ const onPickValue = (v, keepFocus = true) => {
1087
1037
  var _a;
1088
1038
  // the TextInput will not respond to outer value changes if it has focus.
1089
1039
  // here we clear first and then onPickValue will re-focus after all updates.
1090
1040
  (_a = input.current) === null || _a === void 0 ? void 0 : _a.blur();
1091
1041
  setTimeout(() => {
1092
- // blur is now complete
1093
1042
  var _a;
1043
+ // blur is now complete
1094
1044
  let index = v ? p.options.findIndex(o => o === v) : undefined;
1095
1045
  if (index !== undefined && index < 0) {
1096
1046
  index = undefined;
1097
1047
  }
1098
1048
  p.onPick(v, index);
1099
1049
  // wait for the re-render. the value will not update if the control has focus
1100
- setTimeout(() => {
1101
- var _a;
1102
- (_a = input.current) === null || _a === void 0 ? void 0 : _a.focus();
1103
- }, (_a = p.onPickFocusWaitMs) !== null && _a !== void 0 ? _a : defaultOnPickFocusMs);
1050
+ if (keepFocus) {
1051
+ setTimeout(() => {
1052
+ var _a;
1053
+ (_a = input.current) === null || _a === void 0 ? void 0 : _a.focus();
1054
+ }, (_a = p.onPickFocusWaitMs) !== null && _a !== void 0 ? _a : defaultOnPickFocusMs);
1055
+ }
1104
1056
  }, 0);
1105
1057
  };
1058
+ React__namespace.useEffect(() => {
1059
+ if (selectedResultIndex === -1) {
1060
+ return;
1061
+ }
1062
+ const element = document.getElementById(getOptionId(p.id, selectedResultIndex));
1063
+ if (element) {
1064
+ element.scrollIntoView();
1065
+ }
1066
+ }, [selectedResultIndex]);
1067
+ const popupId = `${p.id}_ul`;
1068
+ const listItemStyles = css.css({
1069
+ paddingLeft: theme.controls.padding,
1070
+ paddingRight: theme.controls.padding,
1071
+ backgroundColor: 'white',
1072
+ cursor: 'pointer',
1073
+ color: theme.colors.font,
1074
+ height: theme.controls.height,
1075
+ transition: theme.controls.transition,
1076
+ fontsize: '1rem',
1077
+ fontWeight: 'bold',
1078
+ ':hover': {
1079
+ filter: theme.controls.hoverBrightness
1080
+ }
1081
+ });
1082
+ const listItemFocusStyles = css.css({
1083
+ outline: 'none',
1084
+ boxShadow: theme.controls.focusOutlineShadow,
1085
+ // prevents box shadow clipping
1086
+ position: 'relative',
1087
+ zIndex: 2
1088
+ });
1089
+ const listItemTextStyles = css.css({
1090
+ lineHeight: theme.controls.height
1091
+ });
1106
1092
  return (React__namespace.createElement("div", { onClick: e => {
1107
1093
  e.stopPropagation();
1108
1094
  }, onKeyDown: e => {
1109
1095
  if (e.key === 'Escape') {
1110
1096
  onPickValue(undefined);
1097
+ setSelectedResultIndex(-1);
1111
1098
  }
1112
- }, ref: element, className: css.cx(css.css({
1113
- position: 'relative',
1114
- width: '100%',
1115
- label: 'Autocomplete'
1116
- }), p.className, 'autocomplete') },
1099
+ }, ref: element, className: css.cx(styles.autocomplete, p.className, 'autocomplete') },
1117
1100
  React__namespace.createElement(TabLocker, { disabled: !displayOptions.length, style: { position: 'relative' } },
1118
1101
  React__namespace.createElement(TextInput, Object.assign({}, inputProps, { showErrorDisplay: false, ref: input, value: p.value, className: p.inputClassName, wrapperClassName: p.inputWrapperClassName, onKeyDown: e => {
1119
- var _a, _b, _c;
1120
- if (displayOptions.length) {
1121
- if (e.key === 'ArrowDown') {
1122
- e.preventDefault();
1123
- e.stopPropagation();
1124
- (_a = getNextTabElement(-1, 1)) === null || _a === void 0 ? void 0 : _a.focus();
1102
+ let handled = false;
1103
+ switch (e.code) {
1104
+ case 'ArrowDown': {
1105
+ if (displayOptions.length) {
1106
+ let nextIndex = selectedResultIndex + 1;
1107
+ if (nextIndex >= displayOptions.length) {
1108
+ nextIndex = 0;
1109
+ }
1110
+ setSelectedResultIndex(nextIndex);
1111
+ handled = true;
1112
+ }
1113
+ break;
1125
1114
  }
1126
- else if (e.key === 'ArrowUp') {
1127
- e.preventDefault();
1128
- e.stopPropagation();
1129
- (_b = getNextTabElement(-1, -1)) === null || _b === void 0 ? void 0 : _b.focus();
1115
+ case 'ArrowUp': {
1116
+ if (displayOptions.length) {
1117
+ let nextIndex = selectedResultIndex - 1;
1118
+ if (nextIndex < 0) {
1119
+ nextIndex = displayOptions.length - 1;
1120
+ }
1121
+ setSelectedResultIndex(nextIndex);
1122
+ handled = true;
1123
+ }
1124
+ break;
1125
+ }
1126
+ case 'Enter': {
1127
+ let pickedValue = p.value;
1128
+ if (selectedResultIndex >= 0) {
1129
+ pickedValue = displayOptions[selectedResultIndex];
1130
+ }
1131
+ onPickValue(pickedValue);
1132
+ setSelectedResultIndex(-1);
1133
+ handled = true;
1134
+ break;
1130
1135
  }
1136
+ case 'Tab': {
1137
+ onPickValue(p.value, false);
1138
+ setSelectedResultIndex(-1);
1139
+ break;
1140
+ }
1141
+ }
1142
+ if (handled) {
1143
+ e.preventDefault();
1144
+ e.stopPropagation();
1131
1145
  }
1132
- (_c = p.onKeyDown) === null || _c === void 0 ? void 0 : _c.call(p, e);
1133
- }, "aria-owns": id, "aria-expanded": !!displayOptions.length, "aria-autocomplete": "both", "aria-describedby": `${id}-aria-description` })),
1134
- 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."),
1135
- !!displayOptions.length && (React__namespace.createElement(List, { id: id, ref: list, role: "listbox", className: css.cx(css.css({
1146
+ }, 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 })),
1147
+ !!displayOptions.length && (React__namespace.createElement(List, { id: popupId, ref: list, role: "listbox", "aria-label": p.ariaLabel, className: css.cx(css.css({
1136
1148
  position: 'absolute',
1137
1149
  width: '100%',
1138
1150
  border: theme.controls.border,
@@ -1141,11 +1153,11 @@ const Autocomplete = (p) => {
1141
1153
  backgroundColor: theme.colors.bg,
1142
1154
  marginTop: `-4px !important`,
1143
1155
  zIndex: theme.zIndexes.backdrop,
1144
- 'li:first-child button': {
1156
+ 'li:first-child': {
1145
1157
  borderTopRightRadius: listBorderRadius,
1146
1158
  borderTopLeftRadius: listBorderRadius,
1147
1159
  },
1148
- 'li:last-child button': {
1160
+ 'li:last-child': {
1149
1161
  borderBottomRightRadius: listBorderRadius,
1150
1162
  borderBottomLeftRadius: listBorderRadius,
1151
1163
  }
@@ -1154,36 +1166,23 @@ const Autocomplete = (p) => {
1154
1166
  maxHeight: `calc(${theme.controls.height} * ${maxShowValues})`
1155
1167
  }), p.listClassName) },
1156
1168
  displayOptions.map((v, listItemIndex) => {
1157
- var _a;
1158
- return (React__namespace.createElement(ListItem, { key: v, variant: "full", className: p.listItemClassName, role: "option", "aria-selected": selectedResultIndex === listItemIndex },
1159
- React__namespace.createElement(Button, { title: ((_a = p.showOptionTextAsTitle) !== null && _a !== void 0 ? _a : true) ? v : undefined, onKeyDown: e => {
1160
- var _a, _b;
1161
- if (e.key === 'ArrowDown') {
1162
- e.stopPropagation();
1163
- e.preventDefault();
1164
- (_a = getNextTabElement(listItemIndex, 1)) === null || _a === void 0 ? void 0 : _a.focus();
1165
- }
1166
- else if (e.key === 'ArrowUp') {
1167
- e.stopPropagation();
1168
- e.preventDefault();
1169
- (_b = getNextTabElement(listItemIndex, -1)) === null || _b === void 0 ? void 0 : _b.focus();
1170
- }
1171
- else if (e.key === 'Enter') {
1172
- e.stopPropagation();
1173
- // this will prevent the click event from firing in addition to this enter key event.
1174
- e.preventDefault();
1175
- onPickValue(v);
1176
- }
1177
- }, className: css.cx(buttonMarkerClass + listItemIndex, css.css({
1178
- borderRadius: 0,
1179
- }), p.listItemButtonClassName), onClick: () => {
1180
- onPickValue(v);
1181
- } },
1182
- React__namespace.createElement(Text, { tag: "div", ellipsis: true, align: "left" }, v))));
1169
+ const selected = selectedResultIndex === listItemIndex;
1170
+ 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 },
1171
+ React__namespace.createElement(Text, { tag: "div", ellipsis: true, align: "left", className: listItemTextStyles }, v)));
1183
1172
  }),
1184
1173
  !p.allowScroll && displayOptions.length < p.options.length && (React__namespace.createElement(ListItem, { className: p.listItemClassName },
1185
1174
  React__namespace.createElement(Text, { tag: "div", italics: true, align: "center" }, resultsText))))))));
1186
1175
  };
1176
+ function getOptionId(baseId, optionIndex) {
1177
+ return `${baseId}_li_${optionIndex}`;
1178
+ }
1179
+ const styles = {
1180
+ autocomplete: css.css({
1181
+ position: 'relative',
1182
+ width: '100%',
1183
+ label: 'Autocomplete'
1184
+ }),
1185
+ };
1187
1186
 
1188
1187
  /** Returns a UID. Use this instead of a direct call to a library. */
1189
1188
  function createUid() {
@@ -1543,7 +1542,8 @@ const Calendar = (p) => {
1543
1542
  };
1544
1543
 
1545
1544
  const Checkbox = (props) => {
1546
- const { onChange, label, checkedIcon, uncheckedIcon, checkedThemeColor, checkedColor, readOnly } = props, inputProps = __rest(props, ["onChange", "label", "checkedIcon", "uncheckedIcon", "checkedThemeColor", "checkedColor", "readOnly"]);
1545
+ var _a;
1546
+ const { onChange, label, checkedIcon, uncheckedIcon, checkedThemeColor, checkedColor, readOnly, hideLabel } = props, inputProps = __rest(props, ["onChange", "label", "checkedIcon", "uncheckedIcon", "checkedThemeColor", "checkedColor", "readOnly", "hideLabel"]);
1547
1547
  const selected = checkedIcon || 'selected';
1548
1548
  const unselected = uncheckedIcon || 'unselected';
1549
1549
  const theme = useThemeSafely();
@@ -1607,7 +1607,7 @@ const Checkbox = (props) => {
1607
1607
  `;
1608
1608
  return (React__namespace.createElement("span", { className: css.cx('checkbox', checkboxStyles, props.className) },
1609
1609
  React__namespace.createElement("label", { className: labelStyles },
1610
- React__namespace.createElement("input", Object.assign({}, inputProps, { tabIndex: readOnly ? -1 : undefined, className: nativeCheckboxStyles, type: "checkbox", onChange: e => {
1610
+ 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 => {
1611
1611
  if (readOnly) {
1612
1612
  e.preventDefault();
1613
1613
  return;
@@ -1615,7 +1615,7 @@ const Checkbox = (props) => {
1615
1615
  return onChange(e.currentTarget.checked, e);
1616
1616
  } })),
1617
1617
  React__namespace.createElement(Icon, { className: css.cx('checkboxIcon', iconStyles), id: props.checked ? selected : unselected }),
1618
- label,
1618
+ !hideLabel && label,
1619
1619
  props.children)));
1620
1620
  };
1621
1621
 
@@ -4002,7 +4002,7 @@ const Slider = (p) => {
4002
4002
  }), specificThumbStyles, p.handleClassName, css.css({
4003
4003
  width: sliderHandleSize,
4004
4004
  height: sliderHandleSize,
4005
- })) }, rest), p.showValue && (React.createElement(HandleText, { sliderHandleSize: sliderHandleSize, className: p.sliderTextClassName, index: state.index, parentElement: sliderContainer.current, value: Array.isArray(currentValue.current) ? currentValue.current[state.index] : currentValue.current, renderValue: p.renderValue, renderValueWidth: p.renderValueWidth }))));
4005
+ })) }, rest, { tabIndex: p.tabIndex }), p.showValue && (React.createElement(HandleText, { sliderHandleSize: sliderHandleSize, className: p.sliderTextClassName, index: state.index, parentElement: sliderContainer.current, value: Array.isArray(currentValue.current) ? currentValue.current[state.index] : currentValue.current, renderValue: p.renderValue, renderValueWidth: p.renderValueWidth }))));
4006
4006
  } })));
4007
4007
  };
4008
4008
  const rectsCollideX = (r1, r2) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mackin.com/styleguide",
3
- "version": "10.2.4",
3
+ "version": "11.0.0",
4
4
  "description": "",
5
5
  "main": "./index.js",
6
6
  "module": "./index.esm.js",