Passed
Push — task/improve-find-skills-modal... ( 114b93 )
by Yonathan
10:33 queued 02:21
created

resources/assets/js/components/FindSkillsDialog/SkillCategories.tsx   A

Complexity

Total Complexity 18
Complexity/F 0

Size

Lines of Code 277
Function Count 0

Duplication

Duplicated Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
wmc 18
eloc 204
mnd 18
bc 18
fnc 0
dl 0
loc 277
rs 10
bpm 0
cpm 0
noi 0
c 0
b 0
f 0
1
import * as React from "react";
2
import { useIntl } from "react-intl";
3
import {
4
  focusNextItem,
5
  focusPreviousItem,
6
  getTabList,
7
} from "../../helpers/forms";
8
import { getLocale, localizeFieldNonNull } from "../../helpers/localize";
9
import { Skill, SkillCategory } from "../../models/types";
10
import Accordion from "../H2Components/Accordion";
11
import { skillCategoryMessages as messages } from "./messages";
12
13
interface SkillCategoriesProps {
14
  /** Key of the currently selected skill category. */
15
  activeCategory: SkillCategory["key"];
16
  /** Object holding the state (expanded or collapsed) of all dialog accordions. */
17
  accordionData: { [key: string]: boolean };
18
  /** List of skills */
19
  skills: Skill[];
20
  /** List of skill categories. */
21
  skillCategories: SkillCategory[];
22
  /** Sets the state of the skill results section when a skill category is clicked. */
23
  onSkillCategoryClick: (skillCategory: SkillCategory) => void;
24
  /** Callback function that toggles the accordion's state. */
25
  toggleAccordion: (key: string, value?: boolean | null) => void;
26
}
27
28
const SkillCategories: React.FunctionComponent<SkillCategoriesProps> = ({
29
  activeCategory,
30
  accordionData,
31
  skills,
32
  skillCategories,
33
  onSkillCategoryClick,
34
  toggleAccordion,
35
}) => {
36
  const intl = useIntl();
37
  const locale = getLocale(intl.locale);
38
  const parentSkillCategories: SkillCategory[] = skillCategories.filter(
39
    (skillCategory) => !skillCategory.parent_id,
40
  );
41
42
  /**
43
   * Returns a map of skills where the key represents the category id, and the value is an array of skills in that category.
44
   */
45
  const categoryIdToSkillsMap: Map<number, Skill[]> = React.useMemo(
46
    () =>
47
      skills.reduce((map: Map<number, Skill[]>, skill): Map<
48
        number,
49
        Skill[]
50
      > => {
51
        skill.skill_category_ids.forEach((categoryId) => {
52
          if (!map.has(categoryId)) {
53
            map.set(categoryId, []);
54
          }
55
          map.get(categoryId)?.push(skill);
56
        });
57
        return map;
58
      }, new Map()),
59
    [skills],
60
  );
61
62
  /**
63
   * This function handles all keyboard events for the child skill category buttons.
64
   * @param event Event handler.
65
   * @param id Accordion id.
66
   * @param childSkillCategory Child skill category.
67
   * @param parentSkillCategoryKey Parent skill category key.
68
   */
69
  const onSkillCategoryKeyDown = (
70
    event: React.KeyboardEvent<HTMLButtonElement>,
71
    id: string,
72
    childSkillCategory: SkillCategory,
73
    parentSkillCategoryKey: string,
74
  ) => {
75
    const accordion = document.getElementById(id);
76
    if (accordion) {
77
      const content = accordion.querySelector(
78
        "[data-h2-accordion-content]",
79
      ) as HTMLElement;
80
81
      const childSkillCategoriesFocusList = getTabList(content);
82
      // ArrowLeft: close the accordion and move focus back on the trigger.
83
      // ArrowRight: Select the child skill category and move focus on the skill results.
84
      // ArrowUp & ArrowDown: Allow user to navigate through child skill categories only using the up and down arrows.
85
      switch (event.key) {
86
        case "ArrowLeft":
87
          toggleAccordion(parentSkillCategoryKey);
88
          break;
89
        case "ArrowRight":
90
          onSkillCategoryClick(childSkillCategory);
91
          break;
92
        case "ArrowUp":
93
          focusPreviousItem(childSkillCategoriesFocusList);
94
          break;
95
        case "ArrowDown":
96
          focusNextItem(childSkillCategoriesFocusList);
97
          break;
98
        default:
99
        // do nothing;
100
      }
101
    }
102
  };
103
104
  return (
105
    <ul data-h2-padding="b(left, 0)" className="no-list-style-type">
106
      {parentSkillCategories.map((parentSkillCategory) => {
107
        const { id, key } = parentSkillCategory;
108
        // Get children skill categories of parent skill category.
109
        const childrenSkillCategories = skillCategories.filter(
110
          (childSkillCategory) =>
111
            childSkillCategory.depth === 2 &&
112
            childSkillCategory.parent_id === id,
113
        );
114
115
        return (
116
          <li
117
            key={id}
118
            data-h2-bg-color={`b(gray-1, ${accordionData[key] ? ".5" : "0"})`}
119
            data-h2-padding="b(tb, 1)"
120
            data-h2-border="b(gray-2, top, solid, thin)"
121
            data-h2-margin="b(tb, 0)"
122
          >
123
            <Accordion
124
              isExpanded={accordionData[key]}
125
              id={`${id}-${key}`}
126
              triggerPos="left"
127
              toggleAccordion={() => toggleAccordion(key)}
128
            >
129
              <Accordion.Btn
130
                id={`skill-category-trigger-${id}-${key}`}
131
                data-tabable
132
                type="button"
133
                addIcon={
134
                  <i data-h2-font-weight="b(700)" className="fas fa-plus" />
135
                }
136
                removeIcon={
137
                  <i data-h2-font-weight="b(700)" className="fas fa-minus" />
138
                }
139
              >
140
                <p data-h2-font-weight="b(700)">
141
                  {localizeFieldNonNull(locale, parentSkillCategory, "name")}
142
                </p>
143
              </Accordion.Btn>
144
              {/*
145
                TODO: Restore this when discriptions are added to Skill Categories on backend.
146
                <p
147
                  data-h2-padding="b(top, .25) b(bottom, 1) b(right, .5)"
148
                  data-h2-font-color="b(black)"
149
                  data-h2-font-size="b(small)"
150
                  style={{ paddingLeft: "5rem" }}
151
                >
152
                  {localizeFieldNonNull(
153
                    locale,
154
                    parentSkillCategory,
155
                    "description",
156
                  )}
157
              </p> */}
158
              <Accordion.Content data-h2-margin="b(top, 1)">
159
                <ul
160
                  role="menu"
161
                  data-h2-padding="b(all, 0)"
162
                  className="no-list-style-type"
163
                >
164
                  {childrenSkillCategories.map((childSkillCategory) => {
165
                    return (
166
                      <li key={childSkillCategory.key}>
167
                        <div
168
                          data-h2-grid="b(middle, expanded, flush, 0)"
169
                          data-h2-margin="b(right, .5)"
170
                        >
171
                          <div
172
                            data-h2-align="b(left)"
173
                            data-h2-grid-item="b(5of6)"
174
                          >
175
                            <button
176
                              role="menuitem"
177
                              id={`${childSkillCategory.key}-skill-category`}
178
                              aria-label={intl.formatMessage(
179
                                messages.childCategoryButtonAriaLabel,
180
                                {
181
                                  numOfSkills:
182
                                    categoryIdToSkillsMap.get(
183
                                      childSkillCategory.id,
184
                                    )?.length ?? 0,
185
                                  category: localizeFieldNonNull(
186
                                    locale,
187
                                    childSkillCategory,
188
                                    "name",
189
                                  ),
190
                                },
191
                              )}
192
                              data-h2-button="hello"
193
                              type="button"
194
                              onClick={(): void =>
195
                                onSkillCategoryClick(childSkillCategory)
196
                              }
197
                              onKeyDown={(e): void =>
198
                                onSkillCategoryKeyDown(
199
                                  e,
200
                                  `${id}-${key}`,
201
                                  childSkillCategory,
202
                                  parentSkillCategory.key,
203
                                )
204
                              }
205
                            >
206
                              <p
207
                                data-h2-button-label
208
                                data-h2-font-weight="b(700)"
209
                                data-h2-display="b(block)"
210
                                data-h2-font-style={`${
211
                                  activeCategory === childSkillCategory.key
212
                                    ? "b(none)"
213
                                    : "b(underline)"
214
                                }`}
215
                                data-h2-font-color={`${
216
                                  activeCategory === childSkillCategory.key
217
                                    ? "b(theme-1)"
218
                                    : "b(black)"
219
                                }`}
220
                                data-h2-align="b(left)"
221
                              >
222
                                {localizeFieldNonNull(
223
                                  locale,
224
                                  childSkillCategory,
225
                                  "name",
226
                                )}
227
                              </p>
228
                            </button>
229
                          </div>
230
                          <p
231
                            aria-hidden="true"
232
                            data-h2-grid-item="b(1of6)"
233
                            data-h2-align="b(center)"
234
                            data-h2-radius="b(round)"
235
                            data-h2-bg-color={`${
236
                              activeCategory === childSkillCategory.key
237
                                ? "b(theme-1, 1)"
238
                                : "b(white, 1)"
239
                            }`}
240
                            data-h2-font-color={`${
241
                              activeCategory === childSkillCategory.key
242
                                ? "b(white)"
243
                                : "b(black)"
244
                            }`}
245
                          >
246
                            {categoryIdToSkillsMap.get(childSkillCategory.id)
247
                              ?.length ?? 0}
248
                          </p>
249
                          {/* Number of skills message for screen readers */}
250
                          <span data-h2-visibility="b(invisible)">
251
                            {intl.formatMessage(messages.numOfCategorySkills, {
252
                              numOfSkills:
253
                                categoryIdToSkillsMap.get(childSkillCategory.id)
254
                                  ?.length ?? 0,
255
                              category: localizeFieldNonNull(
256
                                locale,
257
                                childSkillCategory,
258
                                "name",
259
                              ),
260
                            })}
261
                          </span>
262
                        </div>
263
                      </li>
264
                    );
265
                  })}
266
                </ul>
267
              </Accordion.Content>
268
            </Accordion>
269
          </li>
270
        );
271
      })}
272
    </ul>
273
  );
274
};
275
276
export default SkillCategories;
277