@qld-gov-au/qgds-bootstrap5 2.0.10 → 2.0.12
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.
- package/.storybook/main.mjs +2 -2
- package/.storybook/preview.js +5 -2
- package/dist/assets/components/bs5/banner/banner.hbs +3 -6
- package/dist/assets/components/bs5/card/card.hbs +2 -2
- package/dist/assets/components/bs5/dateinput/dateinput.hbs +27 -27
- package/dist/assets/components/bs5/formcheck/formcheck.hbs +10 -2
- package/dist/assets/components/bs5/head/head.hbs +1 -1
- package/dist/assets/components/bs5/searchInput/searchInput.hbs +31 -29
- package/dist/assets/components/bs5/select/select.hbs +19 -19
- package/dist/assets/components/bs5/tag/tag.hbs +1 -1
- package/dist/assets/components/bs5/textarea/textarea.hbs +17 -17
- package/dist/assets/components/bs5/textbox/textbox.hbs +17 -18
- package/dist/assets/css/qld.bootstrap.css +2 -2
- package/dist/assets/css/qld.bootstrap.css.map +3 -3
- package/dist/assets/css/qld.bootstrap.legacy.css +2 -2
- package/dist/assets/css/qld.bootstrap.legacy.css.map +3 -3
- package/dist/assets/js/handlebars.helpers.bundle.js +2 -2
- package/dist/assets/js/handlebars.helpers.bundle.js.map +2 -2
- package/dist/assets/js/handlebars.init.min.js +141 -135
- package/dist/assets/js/handlebars.init.min.js.map +2 -2
- package/dist/assets/js/handlebars.partials.js +141 -135
- package/dist/assets/js/handlebars.partials.js.map +2 -2
- package/dist/assets/js/qld.bootstrap.min.js +9 -10
- package/dist/assets/js/qld.bootstrap.min.js.map +4 -4
- package/dist/assets/node/handlebars.init.min.js +59 -11
- package/dist/assets/node/handlebars.init.min.js.map +2 -2
- package/dist/components/bs5/banner/banner.hbs +3 -6
- package/dist/components/bs5/card/card.hbs +2 -2
- package/dist/components/bs5/dateinput/dateinput.hbs +27 -27
- package/dist/components/bs5/formcheck/formcheck.hbs +10 -2
- package/dist/components/bs5/head/head.hbs +1 -1
- package/dist/components/bs5/searchInput/searchInput.hbs +31 -29
- package/dist/components/bs5/select/select.hbs +19 -19
- package/dist/components/bs5/tag/tag.hbs +1 -1
- package/dist/components/bs5/textarea/textarea.hbs +17 -17
- package/dist/components/bs5/textbox/textbox.hbs +17 -18
- package/dist/package.json +1 -1
- package/dist/sample-data/card/card.data.json +4 -1
- package/dist/sample-data/dateinput/dateinput.data.json +14 -12
- package/dist/sample-data/formcheck/stories/checkbox/checkbox.data.json +4 -5
- package/dist/sample-data/formcheck/stories/radio/radio.data.json +4 -4
- package/dist/sample-data/searchInput/searchInput.data.json +19 -10
- package/dist/sample-data/select/select.data.json +12 -10
- package/dist/sample-data/tag/tag.data.json +149 -143
- package/dist/sample-data/textarea/textarea.data.json +14 -11
- package/dist/sample-data/textbox/textbox.data.json +13 -10
- package/package.json +1 -1
- package/src/components/bs5/banner/banner.hbs +3 -6
- package/src/components/bs5/banner/banner.scss +10 -7
- package/src/components/bs5/banner/banner.stories.js +2 -5
- package/src/components/bs5/button/button.scss +4 -11
- package/src/components/bs5/button/button.stories.js +17 -15
- package/src/components/bs5/card/Card.js +31 -2
- package/src/components/bs5/card/Card.mdx +4 -0
- package/src/components/bs5/card/card--icon-list-footer.stories.js +2 -24
- package/src/components/bs5/card/card--multi-action.stories.js +9 -28
- package/src/components/bs5/card/card--no-action.stories.js +5 -27
- package/src/components/bs5/card/card--single-action.stories.js +4 -33
- package/src/components/bs5/card/card.data.json +4 -1
- package/src/components/bs5/card/card.hbs +2 -2
- package/src/components/bs5/dateinput/Dateinput.js +26 -11
- package/src/components/bs5/dateinput/dateinput.data.json +14 -12
- package/src/components/bs5/dateinput/dateinput.hbs +27 -27
- package/src/components/bs5/footer/footer_formio.scss +5 -5
- package/src/components/bs5/formcheck/Formcheck.js +57 -6
- package/src/components/bs5/formcheck/_form-variables.scss +131 -0
- package/src/components/bs5/formcheck/formcheck.hbs +10 -2
- package/src/components/bs5/formcheck/formcheck.scss +229 -66
- package/src/components/bs5/formcheck/stories/bootstrap-validation/bootstrap-validation.stories.js +304 -0
- package/src/components/bs5/formcheck/stories/checkbox/checkbox.data.json +4 -5
- package/src/components/bs5/formcheck/stories/checkbox/checkbox.stories.js +19 -111
- package/src/components/bs5/formcheck/stories/radio/radio.data.json +4 -4
- package/src/components/bs5/formcheck/stories/radio/radio.stories.js +30 -122
- package/src/components/bs5/inpageAlert/inpageAlert.scss +1 -1
- package/src/components/bs5/modal/modal.scss +106 -99
- package/src/components/bs5/navbar/navbar.functions.js +122 -19
- package/src/components/bs5/pageLayout/{ThemeShowcase.stories.js → PaletteShowcase.stories.js} +36 -35
- package/src/components/bs5/searchInput/__snapshots__/searchInput.test.js.snap +24 -28
- package/src/components/bs5/searchInput/search.functions.js +93 -76
- package/src/components/bs5/searchInput/searchInput.data.json +19 -10
- package/src/components/bs5/searchInput/searchInput.hbs +31 -29
- package/src/components/bs5/searchInput/searchInput.scss +140 -196
- package/src/components/bs5/searchInput/searchInput.stories.js +35 -13
- package/src/components/bs5/searchInput/searchInput.test.js +5 -1
- package/src/components/bs5/select/Select.js +13 -5
- package/src/components/bs5/select/Select.stories.js +27 -83
- package/src/components/bs5/select/select.data.json +12 -10
- package/src/components/bs5/select/select.hbs +19 -19
- package/src/components/bs5/tag/tag--status.stories.js +1 -0
- package/src/components/bs5/tag/tag.data.json +149 -143
- package/src/components/bs5/tag/tag.hbs +1 -1
- package/src/components/bs5/tag/tag.scss +2 -5
- package/src/components/bs5/tag/tag.stories.js +1 -0
- package/src/components/bs5/textarea/Textarea.js +13 -5
- package/src/components/bs5/textarea/Textarea.stories.js +29 -55
- package/src/components/bs5/textarea/textarea.data.json +14 -11
- package/src/components/bs5/textarea/textarea.hbs +17 -17
- package/src/components/bs5/textbox/Textbox.js +16 -5
- package/src/components/bs5/textbox/Textbox.stories.js +26 -51
- package/src/components/bs5/textbox/textInput.scss +12 -232
- package/src/components/bs5/textbox/textbox.data.json +13 -10
- package/src/components/bs5/textbox/textbox.hbs +17 -18
- package/src/components/bs5/typography/typography.stories.js +1 -1
- package/src/css/functions/_index.scss +2 -0
- package/src/css/functions/remify.scss +32 -0
- package/src/css/functions/snap-line-height.scss +7 -0
- package/src/css/main.scss +1 -1
- package/src/css/mixins/focusable.scss +4 -1
- package/src/css/mixins/make-icon.scss +1 -1
- package/src/css/{qld-theme.scss → qld-palettes.scss} +30 -23
- package/src/js/handlebars.helpers.js +9 -1
- package/src/js/utils.js +142 -0
- package/src/components/bs5/formcheck/_formcheck.stories.bak.js +0 -432
|
@@ -1,32 +1,31 @@
|
|
|
1
1
|
<!-- QGDS Component: Textbox -->
|
|
2
2
|
|
|
3
|
-
<!-- Label for the first input field -->
|
|
4
3
|
<label class="qld-text-input-label {{#if isRequired}}field-required{{/if}} {{#if isDisabled}}field-disabled{{/if}}"
|
|
5
|
-
for="
|
|
4
|
+
for="{{id}}">
|
|
6
5
|
{{label-text}}
|
|
7
6
|
{{#if optional-text}}
|
|
8
7
|
<span class="label-text-optional">({{optional-text}})</span>
|
|
9
8
|
{{/if}}
|
|
10
9
|
</label>
|
|
11
10
|
|
|
12
|
-
<!-- Hint text for the first input field -->
|
|
13
11
|
{{#if hint-text}}
|
|
14
|
-
<span class="qld-hint-text" id="
|
|
12
|
+
<span class="qld-hint-text" id="{{id}}-hint">{{hint-text}}</span>
|
|
15
13
|
{{/if}}
|
|
16
14
|
|
|
17
|
-
{{#contains "qld-input-success" customClass}}
|
|
18
|
-
<span
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
{{
|
|
15
|
+
{{#contains "qld-input-success" customClass}}{{! legacy support for feedback classes `qld-input-success`}}
|
|
16
|
+
<span class="qld-input-success">{{successMessageText}}</span>
|
|
17
|
+
{{else}}
|
|
18
|
+
{{#if successMessageText}}{{! updated bootstrap style classes - `valid-feedback`}}
|
|
19
|
+
<div class="valid-feedback">{{successMessageText}}</div>
|
|
20
|
+
{{/if}}{{/contains}}
|
|
22
21
|
|
|
23
|
-
{{#contains "qld-input-error" customClass}}
|
|
24
|
-
<span
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
{{
|
|
22
|
+
{{#contains "qld-input-error" customClass}}{{! legacy support for feedback classes `qld-input-error`}}
|
|
23
|
+
<span class="qld-input-error">{{errorMessageText}}</span>
|
|
24
|
+
{{else}}
|
|
25
|
+
{{#if errorMessageText}}{{! updated bootstrap style classes - `invalid-feedback`}}
|
|
26
|
+
<div class="invalid-feedback">{{errorMessageText}}</div>
|
|
27
|
+
{{/if}}{{/contains}}
|
|
28
28
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
isRequired}}required aria-required="true" {{/if}} />
|
|
29
|
+
<input id={{id}} {{#if value}}value="{{value}}"{{/if}} class="form-control {{customClass}} {{#if isFilled}}is-filled{{/if}} {{#if isValid}}is-valid{{else}}{{#ifCond isValid "===" false}}is-invalid{{/ifCond}}{{/if}}"
|
|
30
|
+
type="text" placeholder="{{placeholder}}" {{#if isDisabled}}disabled{{/if}} {{#if
|
|
31
|
+
isRequired}}required{{/if}} {{#if hint-text}}aria-describedby="{{id}}-hint"{{/if}} />
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
@use "sass:math";
|
|
2
|
+
@use "sass:meta";
|
|
3
|
+
@use "sass:list";
|
|
4
|
+
|
|
5
|
+
///
|
|
6
|
+
/// Easily convert px to rems
|
|
7
|
+
/// @param {Number} $value The px value to convert to rem. May be unitless or supplied as px units.
|
|
8
|
+
/// @param {Number} $baseline [16px] The assumed px value of 1rem.
|
|
9
|
+
/// @todo support complex property values eg: `border: remify(4px 8px 12px 2rem)`;
|
|
10
|
+
@function remify($value, $baseline: 16px) {
|
|
11
|
+
@if (math.is-unitless($baseline)) {
|
|
12
|
+
$baseline-px: $baseline * 1px;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
@if (math.unit($baseline) != "px") {
|
|
16
|
+
@error "parameter $baseline may only use px units";
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
@if (meta.type-of($value) == "list") {
|
|
20
|
+
$result: ();
|
|
21
|
+
@each $item in $value {
|
|
22
|
+
$result: list.append($result, remify($item));
|
|
23
|
+
}
|
|
24
|
+
@return $result;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
@if (math.is-unitless($value)) {
|
|
28
|
+
$value: $value * 1px;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
@return (math.div($value, $baseline) * 1rem);
|
|
32
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
@use "remify" as *;
|
|
2
|
+
///
|
|
3
|
+
/// Calculate a lineheight snapped to nearest 4px value, based on current font size
|
|
4
|
+
/// The returned value is absolute, so only use on leaf nodes where relative line-height value doesn't need to cascade.
|
|
5
|
+
@function snap-line-height($ideal-line-height: 1.4, $grid-size: 4px) {
|
|
6
|
+
@return round(nearest, calc(1em * $ideal-line-height), remify($grid-size));
|
|
7
|
+
}
|
package/src/css/main.scss
CHANGED
|
@@ -100,7 +100,7 @@ $enable-dark-mode: true;
|
|
|
100
100
|
@import "../../node_modules/bootstrap/scss/utilities/api";
|
|
101
101
|
|
|
102
102
|
// Themes
|
|
103
|
-
@import "qld-
|
|
103
|
+
@import "qld-palettes";
|
|
104
104
|
|
|
105
105
|
//8. QLD Design System typography (bootstrap overrides and custom). Please maintain naming consistency.
|
|
106
106
|
@import "./qld-type";
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
/// @param {Boolean} $isFocusWithin [false] - Optionally apply styles via :focus-within rather than :focus-visible
|
|
5
5
|
/// @param {String} $outlineWidth: [3px] The width of the outline.
|
|
6
6
|
/// @param {String} $customSelector [null] - If passed, will override $isFocusWithin and apply focus styles instead to a custom selector string. @see SearchInput
|
|
7
|
+
/// @content
|
|
7
8
|
|
|
8
9
|
@mixin focusable(
|
|
9
10
|
$offsetOutline: true,
|
|
@@ -35,9 +36,11 @@
|
|
|
35
36
|
style: solid;
|
|
36
37
|
color: var(
|
|
37
38
|
--qld-focus-color,
|
|
38
|
-
#
|
|
39
|
+
#002e85
|
|
39
40
|
); // --qld-focus-color defined in qld-type.scss
|
|
40
41
|
offset: $offsetOutline;
|
|
41
42
|
}
|
|
43
|
+
|
|
44
|
+
@content;
|
|
42
45
|
}
|
|
43
46
|
}
|
|
@@ -11,7 +11,7 @@ $prefix: "qld-" !default;
|
|
|
11
11
|
/// @param {String} $name [null] - The name of icon
|
|
12
12
|
/// @param {String} $size ["sm"] - The icon size, valid values are "xs" | "sm" | "md" | "lg" | "xl" | "xxl"
|
|
13
13
|
/// @param {String | null} $pseudo [null] - Whether the mixin applies styles as a pseudo-element. Valid string values are "before" | "after"
|
|
14
|
-
/// @param {
|
|
14
|
+
/// @param {Boolean} $is-inline [false] - Applies extra styles to assist with vertical alignent when used inline with text.
|
|
15
15
|
/// @param {Boolean} $include-base [true] - Apply all base styles. Set to false when reusing this mixin to create modifier classes where base styles are already applied (For instance qld-icon-{name})
|
|
16
16
|
/// @content
|
|
17
17
|
@mixin make-icon(
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// Provides complete theme styling including color, background-color, and CSS variables
|
|
3
3
|
|
|
4
4
|
// Theme properties mixins to avoid duplication
|
|
5
|
-
|
|
5
|
+
%qld-palette-default {
|
|
6
6
|
background-color: var(--qld-body-bg);
|
|
7
7
|
color: var(--qld-body-color);
|
|
8
8
|
--qld-action-icon-color: var(--qld-light-action-secondary);
|
|
@@ -15,7 +15,6 @@
|
|
|
15
15
|
--qld-headings-color: #{$headings-color};
|
|
16
16
|
--qld-link-color: #{$link-color};
|
|
17
17
|
--qld-link-color-rgb: #{to-rgb($link-color)};
|
|
18
|
-
--qld-link-font-weight: 400;
|
|
19
18
|
--qld-link-hover-color: var(--qld-link-color);
|
|
20
19
|
--qld-link-visited-color: #{$color-default-color-light-link-visited};
|
|
21
20
|
--qld-link-hover-color-rgb: var(--qld-link-color-rgb);
|
|
@@ -30,12 +29,26 @@
|
|
|
30
29
|
--qld-selection-color: var(--qld-white);
|
|
31
30
|
}
|
|
32
31
|
|
|
33
|
-
|
|
32
|
+
%qld-palette-light {
|
|
33
|
+
@extend %qld-palette-default;
|
|
34
|
+
--qld-body-bg: var(--qld-light-background);
|
|
35
|
+
--qld-border-color: var(--#{$prefix}color-default-color-light-border-light);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
%qld-palette-alt {
|
|
39
|
+
@extend %qld-palette-default;
|
|
40
|
+
--qld-body-bg: var(--qld-light-alt-background);
|
|
41
|
+
--qld-border-color: var(--qld-soft-grey);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
%qld-palette-dark {
|
|
34
45
|
background-color: var(--qld-body-bg);
|
|
35
46
|
color: var(--qld-body-color);
|
|
36
47
|
--qld-action-icon-color: var(--qld-dark-action-secondary);
|
|
37
48
|
--qld-action-icon-hover-color: var(--qld-dark-action-secondary-hover);
|
|
49
|
+
--qld-body-bg: var(--qld-dark-background);
|
|
38
50
|
--qld-body-color: #fff;
|
|
51
|
+
--qld-border-color: var(--qld-dark-border);
|
|
39
52
|
--qld-focus-color: var(--qld-dark-focus);
|
|
40
53
|
--qld-headings-color: #{$color-default-color-dark-text-heading};
|
|
41
54
|
--qld-link-color: #{$color-default-color-dark-link-default};
|
|
@@ -54,42 +67,36 @@
|
|
|
54
67
|
--qld-selection-color: var(--qld-brand-primary);
|
|
55
68
|
}
|
|
56
69
|
|
|
70
|
+
%qld-palette-dark-alt {
|
|
71
|
+
@extend %qld-palette-dark;
|
|
72
|
+
--qld-body-bg: var(--qld-dark-alt-background);
|
|
73
|
+
--qld-border-color: var(--qld-dark-alt-border);
|
|
74
|
+
}
|
|
75
|
+
|
|
57
76
|
// Default theme (inherits :root variables) and root variables
|
|
58
77
|
:root,
|
|
59
78
|
.default {
|
|
60
|
-
@
|
|
79
|
+
@extend %qld-palette-default;
|
|
61
80
|
}
|
|
62
81
|
|
|
63
82
|
// Light theme
|
|
64
83
|
.light {
|
|
65
|
-
@
|
|
66
|
-
--qld-body-bg: var(--qld-light-background);
|
|
67
|
-
--qld-border-color: var(--#{$prefix}color-default-color-light-border-light);
|
|
84
|
+
@extend %qld-palette-light;
|
|
68
85
|
}
|
|
69
86
|
|
|
70
87
|
// Alt theme (light alternative)
|
|
71
88
|
.alt {
|
|
72
|
-
@
|
|
73
|
-
--qld-body-bg: var(--qld-light-alt-background);
|
|
74
|
-
--qld-border-color: var(--qld-soft-grey);
|
|
89
|
+
@extend %qld-palette-alt;
|
|
75
90
|
}
|
|
76
91
|
|
|
77
92
|
// Dark theme
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
93
|
+
// Bootstrap dark mode support
|
|
94
|
+
.dark,
|
|
95
|
+
:root[data-bs-theme="dark"] {
|
|
96
|
+
@extend %qld-palette-dark;
|
|
82
97
|
}
|
|
83
98
|
|
|
84
99
|
// Dark alt theme
|
|
85
100
|
.dark-alt {
|
|
86
|
-
@
|
|
87
|
-
--qld-body-bg: var(--qld-dark-alt-background);
|
|
88
|
-
--qld-border-color: var(--qld-dark-alt-border);
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
// Bootstrap dark mode support
|
|
92
|
-
:root[data-bs-theme="dark"] {
|
|
93
|
-
@include dark-theme-properties;
|
|
94
|
-
--qld-body-bg: var(--qld-brand-primary);
|
|
101
|
+
@extend %qld-palette-dark-alt;
|
|
95
102
|
}
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
/**
|
|
4
4
|
*
|
|
5
5
|
* @param {*} v1 The left value
|
|
6
|
-
* @param { "==" | "===" | "!=" | "!==" | "<" | "<=" | ">" | ">=" | "&&" | "||" | "contains"} operator the operator to handle comparison
|
|
6
|
+
* @param { "==" | "===" | "!=" | "!==" | "<" | "<=" | ">" | ">=" | "&&" | "||" | "in" | "contains"} operator the operator to handle comparison
|
|
7
7
|
* @param {*} v2 The right value
|
|
8
8
|
* @returns {Boolean} the result of comparison
|
|
9
9
|
* @example
|
|
@@ -92,6 +92,14 @@ export default function handlebarsHelpers(handlebars) {
|
|
|
92
92
|
return args.some((arg) => !!arg) ? options.fn(this) : options.inverse(this);
|
|
93
93
|
});
|
|
94
94
|
|
|
95
|
+
// ifAll - {{{#ifAll variable1 variable2 variable3 variable4 etc}}, if all true return true
|
|
96
|
+
handlebars.registerHelper("ifAll", function (...args) {
|
|
97
|
+
const options = args.pop(); // The last argument is the options object
|
|
98
|
+
return args.every((arg) => !!arg)
|
|
99
|
+
? options.fn(this)
|
|
100
|
+
: options.inverse(this);
|
|
101
|
+
});
|
|
102
|
+
|
|
95
103
|
// now - return current timestamp i.e {{now}}
|
|
96
104
|
handlebars.registerHelper("now", function () {
|
|
97
105
|
return new Date().toISOString();
|
package/src/js/utils.js
CHANGED
|
@@ -47,3 +47,145 @@ export function isFocusable(element) {
|
|
|
47
47
|
}
|
|
48
48
|
return false;
|
|
49
49
|
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Get all focusable elements within a container
|
|
53
|
+
* @param {HTMLElement} container The container element to search within
|
|
54
|
+
* @returns {HTMLElement[]} Array of focusable elements
|
|
55
|
+
*/
|
|
56
|
+
export function getFocusableElements(container) {
|
|
57
|
+
if (!container) return [];
|
|
58
|
+
|
|
59
|
+
const allElements = container.querySelectorAll("*");
|
|
60
|
+
return Array.from(allElements).filter((el) => isFocusable(el));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Create a focus trap for accessibility
|
|
65
|
+
* Traps keyboard focus within a container element (e.g., modal, navbar, menu)
|
|
66
|
+
* @param {HTMLElement} container The container element to trap focus within
|
|
67
|
+
* @param {Object} options Configuration options
|
|
68
|
+
* @param {HTMLElement} options.returnFocusElement Element to return focus to when deactivated
|
|
69
|
+
* @param {Function} options.onEscape Callback function when Escape key is pressed
|
|
70
|
+
* @returns {Object} Focus trap controller with activate, deactivate, and update methods
|
|
71
|
+
*/
|
|
72
|
+
export function createFocusTrap(container, options = {}) {
|
|
73
|
+
if (!container) {
|
|
74
|
+
throw new Error("Container element is required for focus trap");
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const { returnFocusElement, onEscape } = options;
|
|
78
|
+
let isActive = false;
|
|
79
|
+
let focusableElements = [];
|
|
80
|
+
let previousActiveElement = null;
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Update the list of focusable elements
|
|
84
|
+
*/
|
|
85
|
+
function updateFocusableElements() {
|
|
86
|
+
focusableElements = getFocusableElements(container);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Handle Tab key navigation within the trap
|
|
91
|
+
*/
|
|
92
|
+
function handleTabKey(event) {
|
|
93
|
+
if (!isActive || focusableElements.length === 0) return;
|
|
94
|
+
|
|
95
|
+
const firstElement = focusableElements[0];
|
|
96
|
+
const lastElement = focusableElements[focusableElements.length - 1];
|
|
97
|
+
const activeElement = document.activeElement;
|
|
98
|
+
|
|
99
|
+
// Shift + Tab (backward)
|
|
100
|
+
if (event.shiftKey) {
|
|
101
|
+
if (
|
|
102
|
+
activeElement === firstElement ||
|
|
103
|
+
!container.contains(activeElement)
|
|
104
|
+
) {
|
|
105
|
+
event.preventDefault();
|
|
106
|
+
lastElement.focus();
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
// Tab (forward)
|
|
110
|
+
else {
|
|
111
|
+
if (activeElement === lastElement || !container.contains(activeElement)) {
|
|
112
|
+
event.preventDefault();
|
|
113
|
+
firstElement.focus();
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Handle Escape key to close/deactivate
|
|
120
|
+
*/
|
|
121
|
+
function handleEscapeKey(event) {
|
|
122
|
+
if (!isActive) return;
|
|
123
|
+
|
|
124
|
+
if (event.key === "Escape" || event.key === "Esc") {
|
|
125
|
+
event.preventDefault();
|
|
126
|
+
if (typeof onEscape === "function") {
|
|
127
|
+
onEscape();
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Handle all keyboard events
|
|
134
|
+
*/
|
|
135
|
+
function handleKeyDown(event) {
|
|
136
|
+
if (event.key === "Tab") {
|
|
137
|
+
handleTabKey(event);
|
|
138
|
+
} else if (event.key === "Escape" || event.key === "Esc") {
|
|
139
|
+
handleEscapeKey(event);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Activate the focus trap
|
|
145
|
+
*/
|
|
146
|
+
function activate() {
|
|
147
|
+
if (isActive) return;
|
|
148
|
+
|
|
149
|
+
// Store the currently focused element to return focus later
|
|
150
|
+
previousActiveElement = document.activeElement;
|
|
151
|
+
|
|
152
|
+
// Update focusable elements
|
|
153
|
+
updateFocusableElements();
|
|
154
|
+
|
|
155
|
+
// Add keyboard event listener
|
|
156
|
+
document.addEventListener("keydown", handleKeyDown);
|
|
157
|
+
|
|
158
|
+
// Focus the first focusable element
|
|
159
|
+
if (focusableElements.length > 0) {
|
|
160
|
+
focusableElements[0].focus();
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
isActive = true;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Deactivate the focus trap
|
|
168
|
+
*/
|
|
169
|
+
function deactivate() {
|
|
170
|
+
if (!isActive) return;
|
|
171
|
+
|
|
172
|
+
// Remove keyboard event listener
|
|
173
|
+
document.removeEventListener("keydown", handleKeyDown);
|
|
174
|
+
|
|
175
|
+
// Return focus to the element that had focus before activation
|
|
176
|
+
const elementToFocus = returnFocusElement || previousActiveElement;
|
|
177
|
+
if (elementToFocus && typeof elementToFocus.focus === "function") {
|
|
178
|
+
elementToFocus.focus();
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
isActive = false;
|
|
182
|
+
previousActiveElement = null;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return {
|
|
186
|
+
activate,
|
|
187
|
+
deactivate,
|
|
188
|
+
update: updateFocusableElements,
|
|
189
|
+
isActive: () => isActive,
|
|
190
|
+
};
|
|
191
|
+
}
|