Passed
Push — dev ( 55f36e...3ef13f )
by Tristan
05:38 queued 12s
created

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

Complexity

Total Complexity 19
Complexity/F 0

Size

Lines of Code 297
Function Count 0

Duplication

Duplicated Lines 0
Ratio 0 %

Importance

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